(2002.12.07/2002.12.12更新)

ftpのファイル同期


目 的:複数のPCと一つのftpサーバ(HP用)間でファイルの同期を取る。具体的には、仕事場と自宅の両方でHPのメンテナンスがしたい。
問題点:@RxFtpを使用した場合、ftpサーバ上のファイルのフル形式のタイムスタンプを簡単に取得する方法がない。
Aファイルのタイムスタンプはダウンロード/アップロードの度に更新されてしまう。

■片道の更新なら何とかなる…

今までは@の問題で頭を悩ませて来たが、QUOTE MTDM命令を使ったり、タイムスタンプ情報をファイルとしてアップロードしておく、などの解決策が考えられていた。実際、アップロードだけまたはダウンロードだけの更新判断ならばこの方法でも十分だし、そもそもフル形式タイムスタンプは必ずしも必要ではないことも判った。

【注】古くて時刻がわからないファイルは、すべて00:00:00として扱えばよい。そうすると、ローカルのファイルと中身が同じであっても一度は更新対象になってしまうが、同時にタイムスタンプも更新されるため(Aの特徴)、その後は更新対象から外れる。もっとも、半年立つと自動的に省略形表示になって時間情報が欠落するので、中身の変更の有無にかかわらず半年に一度は更新されることにはなるが。

■問題は相方向の同期

ところが、複数のクライアント間で同期を取るとなると、必ずアップロードとダウンロードの両方で更新判断をする必要が出る。そうなるとAの問題は致命的。アップ/ダウンの度にタイムスタンプが更新されるため、中身が変更されていなくても常に更新必要ファイルと判断されてしまう。

理想的な解決方法としては、アップ/ダウン時のタイムスタンプを、元ファイルのタイムスタンプとすること。ローカル側ならばそれもできないことはない。実はOS/2だとタイムスタンプの変更はちと厄介で、REXXだけで処理するのは厳しいかも知れないが(C++には専用の関数がある)、wgetを使えば何とかなる。しかし、ftpサーバ上のファイルのタイムスタンプを任意に変更することは不可能ではないか? 少なくとも、RxFTPでは無理。QUOTEで処理できる命令でタイムスタンプが変更出来れば別だが望み薄。

【独り言】ftpサーバでは根本的に別の考え方でタイムスタンプを付けてんのかな? たとえば、パケットのヘッダ部から時間情報を抽出するとか…。で、それがなければ現在時刻を設定するとか…。その辺りはでんでん知らないからなあ。本気でやるなら、こっちの方向で考えるべきだろう。

■根本的な発想転換

もう一つ考えうる方法としては、タイムスタンプを更新の判断基準としては使用せず、更新履歴を別ファイルとして持たせること。つまり、最終アップロード時の日時、マシン名、更新ファイル数、更新ファイル名をファイルに書き出しておく方法。また、ローカル側には、マシンごとに最終アップ/ダウン日時の情報を持たせておく。

ダウンロードするときは、最終ダウンロード日時よりも新しい更新履歴から落すべきファイルを判断する。ただし、自マシンでアップしたファイルはダウンする必要がないので外す。具体的には、以下のような感じになる。

* 20021011221545 mypc1 2
/html/index.htm
/html/camera/om1.htm
* 20021205103614 mypc2 5
/html/pc/iroiro.htm
/html/index.htm
……
行頭の「*」はデータの区切りのマーカー。で、このファイルは当然累積的に大きくなっていくから、どこかでサイズ調整が必要。まあ、上限を3000行程度にして、古いものから順に廃棄して行くのが妥当かと…(行数ではなく時間で区切るべきかな?)。

また、シンクロ(アップ&ダウン)の具体的な順序は、

  1. ローカルから、最終シンクロ日よりも新しいタイムスタンプを持つファイルを抽出してアップロードする。
  2. リモートから更新履歴ファイルを入手し、最終シンクロ日よりも新しいファイルをダウンロードする。
  3. 最終シンクロ日を更新する。
  4. 更新履歴ファイルを更新してアップロードする。

■問題点

もちろん、この手法にも幾つか問題点がある。
  1. シンクロ日や更新履歴の初回の設定が難しい。
  2. 同じファイルを何度も更新した場合、同じファイルを更新回数分ダウンロードしてしまう。
  3. 複数のPC上で、同一ファイルをシンクロ操作をずに更新してしまうと不整合が発生する。
  4. 他の方法でアップ/ダウンしたファイルが管理できない。
1.の問題はけっこう面倒。今まで別方法で管理していたサイトを、新たにこの方法で管理しようとしたときに問題になる。ファイルの新旧判断を独自に行っているので、何等かの前提をおいて管理し始めないといけない。たとえば、最終更新日は無条件に今から1か月前にするとか、更新履歴ファイルが存在しないときは、全ファイルをダウンロードする、あるいは全くダウンロードしない、など。個人で使うときはともかく、汎用ツールとして公開するときは、きちんとしたガイドラインを作らないと…。

2.の問題は技術的にそれほど難しくない。ダウンロード済みのファイル名を記憶しておいて、重複ダウンロードを避ければよい。

3.の問題は難問だが、これは手法に寄らず、更新管理には常につきまとう問題。運用に気を付けるか、不整合を発見したときにワーニングを出すなどの処置で対処するしかない。

4.の問題はもうどうしようもない。しかし、1.の問題の特殊な場合と見なせばよいわけで、他の方法でアップ/ダウンした後は再初期化処理を行うという方法で対処する。まあ、リモートのタイムスタンプを変更できれば一気に解決するんだけどねえ。

■サンプルコード

ということで、とりあえずテスト版(要RxFtpライブラリ)。
■ftpsync.cmd
/*****************************************************************************/
/*                                                                           */
/* ftpsync -- remote-local synchronizer 2002.12.04 Nogure,Ten                */
/*                                                                           */
/* 2002.12.07 テスト版作成(アップロードのみ)                               */
/* 2002.12.09 テスト版作成(ダウンロードも)                                 */
/* 2002.12.15 若干の改良(確認機能)                                         */
/* 2003.01.01 非接続時の中断機能                                             */
/*                                                                           */
/*****************************************************************************/

Call RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'
Call SysLoadFuncs
rc = RxFuncAdd("FtpLoadFuncs","rxFtp","FtpLoadFuncs")
rc = FtpLoadFuncs()
NUMERIC DIGITS 14

SAY '------------------------------------'
SAY 'ftpsync -- remote-local synchronizer'
SAY '   ver.alpha-1 2002.12.09 Nogure,Ten'
SAY '------------------------------------'

envfile='ftpsync.env';		/* 環境ファイル(最終シンクロ日)*/
updfile='ftpsync.dat';		/* 更新履歴ファイル              */
tmpfile='ftpsync.$$$';		/* テンポラリ・ファイル          */
maxline=3000;			/* 更新履歴ファイルの最大行数    */

/*---------------------------------------------------------------------------*/
/* 環境ファイル・テンポラリファイルの指定                                    */
/*---------------------------------------------------------------------------*/

PARSE SOURCE ThisProgram
ThisProgram=DelWord(ThisProgram,1,2);
ProgDir=FileSpec("Drive",ThisProgram)||FileSpec("Path",ThisProgram);
envfile=ProgDir||envfile;
tmpfile=ProgDir||tmpfile;
CALL SysFileDelete tmpfile;

/*---------------------------------------------------------------------------*/
/* 環境ファイルの読み込み                                                    */
/*---------------------------------------------------------------------------*/

Do While Lines(envfile)
  st=Linein(envfile);
  PARSE VAR st stKey stVal dmy;
  If stKey='HOSTNAME'     Then hostname    =stVal;
  If stKey='REMOTEHOST'   Then remotehost  =stVal;
  If stKey='USERID'       Then userid      =stVal;
  If stKey='PASSWORD'     Then password    =stVal;
  If stKey='REMOTEHOME'   Then remotehome  =stVal;
  If stKey='LOCALHOME'    Then localhome   =stVal;
  If stKey='LASTSYNCHRO'  Then lastsynchro =stVal;
End;
rc=STREAM(envfile,'C','C');

/*---------------------------------------------------------------------------*/
/* 現在の全ローカル・ファイルのタイムスタンプの取得                          */
/*---------------------------------------------------------------------------*/

Call SysFileTree localhome||'\*.*', files, 'FSO'
Do n=1 to files.0
  nowFile.n.name=files.n;
  dt = stream(nowFile.n.name,'C','query datetime');
  If substr(dt,7,2)>80 then century=19; else century=20
  nowFile.n.time = century||substr(dt,7,2)||left(dt,2)||substr(dt,4,2),
  ||substr(dt,11,2)||substr(dt,14,2)||substr(dt,17,2);
End;
nowFile.0=files.0

/*---------------------------------------------------------------------------*/
/* 更新アップロードするファイルの抽出                                        */
/*---------------------------------------------------------------------------*/

m=0
Do n=1 to nowFile.0
  If nowFile.n.time>lastsynchro Then
  Do
    m=m+1;
    updFile.m.name=nowFile.n.name
    SAY '*** NEW/UPD ***' nowFile.n.name
  End;
End;
updFile.0=m;

/*---------------------------------------------------------------------------*/
/* 更新アップロード対象がない場合の処理                                      */
/*---------------------------------------------------------------------------*/

IF updFile.0=0 THEN
DO
  SAY;
  SAY '*** NO NEW FILE (UPLOAD) ***';
  SAY;/* ダウンロードしないで終わっちゃいかんのだ */
END;

/*---------------------------------------------------------------------------*/
/* ユーザー情報と転送モードの設定                                            */
/*---------------------------------------------------------------------------*/

rc = FtpSetUser(remotehost,userid,password);
rc = FtpSetBinary("Binary");

/*---------------------------------------------------------------------------*/
/* アップロード                                                              */
/*---------------------------------------------------------------------------*/

IF updFile.0>0 THEN
DO
  SAY '=== HIT ENTER KEY TO UPLOAD ==='
  PULL
END;

Do n=1 to updFile.0
  p=length(localhome)+1;
  remoteFname=remotehome||substr(updFile.n.name,p);
  remoteFname=translate(remoteFname,'/','\');
  SAY '*** UPLOAD' updFile.n.name '>>' remoteFname;
  rc = FtpPut(updFile.n.name, remoteFname);
  IF RC<>0 THEN
  DO
    SAY "!!! CONNECTION FAILED !!!"
    EXIT;
  END;
End;

/*---------------------------------------------------------------------------*/
/* 更新履歴ファイルのダウンロード                                            */
/*---------------------------------------------------------------------------*/

/* 本来はローカルファイルは一旦削除しておくべきだが… */
remoteUpdfile=remotehome||'/'||updfile;
localUpdfile=ProgDir||updfile;/* 保存場所は一考の余地ありだが… */
rc=FtpGet(localUpdfile,remoteUpdfile);

IF (rc<>0)&(FTPERRNO='FTPHOST') THEN
DO
  SAY "!!! CONNECTION FAILED !!!"
  EXIT;
END;

/*---------------------------------------------------------------------------*/
/* ftpsync.datがアップロードされていない場合(ダウンロードしない)           */
/*---------------------------------------------------------------------------*/

rc=STREAM(localUpdfile,'c','OPEN READ')
IF rc\='READY:' THEN
DO
  SAY;
  SAY '*** FTPSYNC.DAT NOT FOUND ! ***';
  SAY '*** NO FILE DOWNLOAD ***'
  SAY;
  CALL LINEOUT localUpdfile,'';	/* これでダウンロード・ループをスキップ */
END;

/*---------------------------------------------------------------------------*/
/* ダウンロードするファイルのピックアップ                                    */
/*---------------------------------------------------------------------------*/

flFirst=1;	/* 初回処理判別フラグ               */
L=0;		/* ftpsync.dat行数カウンタ          */
dlFile.0=0;	/* DLピックアップ済みファイル       */
nDL=0;		/* DLピックアップ済みファイル数     */

DO WHILE LINES(localUpdfile)>0
  st=LINEIN(localUpdfile);L=L+1;
  IF left(st,1)='*' Then
  DO
    PARSE VAR st dmy dt id nfl dmy/* 日時、ホスト名、ファイル数 */
    IF flFirst=1 THEN
    Do/* 初回処理:場合によっては全ファイルのDLを実行 */
      If dt>lastsynchro Then /*最終シンクロ日が最も古いアップ日よりも古い*/
      Do
        SAY '*** ALL FILE DOWN LOAD(未実装)***'
        /*CALL ALL FILE DOWNLOAD*/
        /*LEAVE;*//*Do Whileループから脱出*/
	/*後ろのルーチンでLを参照しているので注意*/
      End;
    End;

    flFirst=0;/* lastsynchroよりも新しく、かつ他のマシンでアップしたもの */
    If (dt>lastsynchro)&(id<>hostname) Then
    Do n=1 to nfl
      st=linein(localUpdfile);L=L+1;

      flgDL=0;/* 重複DLの回避 */
      DO nn=1 to dlFile.0
        IF st=dlFile.nn THEN
        DO
          flgDL=1;/* 重複発見 */
          LEAVE;
        END;
      END;

      IF flgDL=0 THEN/* DLするファイルのピックアップ */
      DO
        SAY '*** DOWNLOAD ***' st;
	nDL=nDL+1;
	dlFile.nDL=st;
        dlFile.0=nDL;
      END;
    End;

  End;
End;
rc=stream(localUpdfile,'C','C');

IF dlFile.0=0 THEN
DO
  SAY ;
  SAY '*** NO NEW FILE (DOWNLOAD) ***';
  SAY ;
END;

/*---------------------------------------------------------------------------*/
/* ダウンロードの実行                                                        */
/*---------------------------------------------------------------------------*/

IF dlFile.0>0 THEN
DO
  SAY '=== HIT ENTER KEY TO DOWNLOAD ==='
  PULL
END;

Do n=1 to dlFile.0
  p=length(remotehome)+1;
  localFname=localhome||substr(dlFile.n,p);
  localFname=TRANSLATE(localFname,'\','/');
  SAY '*** DOWNLOAD' dlFile.n '>>' localFname;
  rc=FtpGet(localFname,dlFile.n);
  IF RC<>0 THEN
  DO
    SAY "!!! CONNECTION FAILED !!!"
    EXIT;
  END;
End;

/*---------------------------------------------------------------------------*/
/* 最終シンクロ日時の更新                                                    */
/*---------------------------------------------------------------------------*/

TIMES=TIME();
lastsynchro=DATE(sorted)||Left(TIMES,2)|| Substr(times,4,2)||right(times,2);

/*---------------------------------------------------------------------------*/
/* 更新履歴ファイルの更新                                                    */
/*---------------------------------------------------------------------------*/

IF updFile.0>0 THEN
Do
  LL=L+updFile.0+1;	/* +1はヘッダ分(時刻とマシン名とファイル数) */
  Ld=LL-maxline;	/* ファイルの上限は行数で判断している       k*/

  /*** 古いデータを書き出す ***/

  Do n=1 to L		/*Lはftpsync.datの行数(前の処理で取得)*/
    st=Linein(localUpdfile);
    IF n>=LD THEN CALL LINEOUT tmpfile, st;
  End;
  CALL STREAM localUpdfile,'C','C'

  /*** 更新分のデータを追加する ***/

  CALL LINEOUT tmpfile,'*' lastsynchro hostname updFile.0
  Do n=1 to updFile.0
    p=length(localhome)+1;
    remoteFname=remotehome||substr(updFile.n.name,p);
    remoteFname=translate(remoteFname,'/','\');
    CALL LINEOUT tmpfile, remoteFname;
  End;
  CALL STREAM tmpfile,'C','C'

  /*** ローカルのftpsync.datの更新 ****/

  CALL SysFileDelete localUpdfile;
  '@COPY' tmpfile localUpdfile '>nul'
  CALL SysFileDelete tmpfile;

  /*** 更新履歴ファイルのアップロード***/

  SAY '*** FTPSYNC.DAT UPLOAD ***'
  rc = FtpPut(localUpdfile, remoteUpdfile);
End;

/*---------------------------------------------------------------------------*/
/* ftpサーバの切断                                                           */
/*---------------------------------------------------------------------------*/
rc = FtpLogoff();

/*---------------------------------------------------------------------------*/
/* 環境ファイルの最終シンクロ日時の更新                                      */
/*---------------------------------------------------------------------------*/

CALL SysFileDelete envfile;
CALL LINEOUT envfile,'HOSTNAME' hostname
CALL LINEOUT envfile,'REMOTEHOST' remotehost
CALL LINEOUT envfile,'USERID' userid;
CALL LINEOUT envfile,'PASSWORD' password;
CALL LINEOUT envfile,'REMOTEHOME' remotehome;
CALL LINEOUT envfile,'LOCALHOME' localhome;
CALL LINEOUT envfile,'LASTSYNCHRO' lastsynchro;
CALL STREAM envfile,'C','C';
SAY
SAY '*** FTPSYNC.ENV UPDATE ***'

EXIT;
■ftpsync.dat
HOSTNAME mypc1
REMOTEHOST www.abc.efg.ne.jp
USERID pokosuke
PASSWORD tororoimo
REMOTEHOME /html
LOCALHOME g:\myhome\html
LASTSYNCHRO 20021211122417

【逆襲のOS/2目次】 【ホーム】