ftpでミラーリングするツール(lftp, ncftp, syncftp gem)の転送スキップルールをざっくり調べてみた

# 背景

[linux - How to use rsync over FTP - Server Fault](http://serverfault.com/questions/24622/how-to-use-rsync-over-ftp/24833#24833)によると、
ftpでミラーリングするのにはrsyncは使えずlftpとncftpなら対応しているそうです。

ミラーリングするときには既に転送済みのファイルはスキップして欲しいわけですが、どういう条件のときにスキップするかをざっくり調べてみました。

# lftp

* [LFTP - sophisticated file transfer program](http://lftp.yar.ru/)
* [github.com/lavv17/lftp](https://github.com/lavv17/lftp)

lftpはファイルの更新日時とファイルサイズを転送元と転送先のファイルで比較しています。


下記のコードでファイルのタイムスタンプの構造体が定義されています。tsが通算秒でts_precが精度(単位:秒)になっています。

```cpp:https://github.com/lavv17/lftp/blob/v4.4.15/src/FileSet.h#L36-L45
struct FileTimestamp
{     
   time_t ts;
   int ts_prec;
   FileTimestamp() : ts(NO_DATE_YET), ts_prec(0) {}
   void set(time_t ts1,int ts1_prec) { ts=ts1; ts_prec=ts1_prec; }
   bool is_set() { return ts!=NO_DATE && ts!=NO_DATE_YET; }
   operator time_t() const { return ts; }
   time_t operator=(time_t t) { set(t,0); return t; }
};
```

更新日時を比較しているのが以下の箇所です。file->date + file->date.ts_precのうち、file->dateはFileTimestampのoperator time_t()でtsを返していて、それとts_precを足したものを転送元と転送先のファイルで比較しています。また

```cpp:https://github.com/lavv17/lftp/blob/v4.4.15/src/MirrorJob.cc#L226-L247
	 FileInfo *old=target_set->FindByName(file->name);
	 if(old)
	 {
	    if((flags&CONTINUE)
	    && (old->defined&file->TYPE) && old->filetype==old->NORMAL
	    && (flags&IGNORE_TIME ||
	    	((file->defined&file->DATE) && (old->defined&old->DATE)
	    	&& file->date + file->date.ts_prec < old->date - old->date.ts_prec))
	    && (file->defined&file->SIZE) && (old->defined&old->SIZE)
	    && file->size >= old->size)
	    {
               cont_this=true;
               if(target_is_local && !script_only)
               {
                  if(access(target_name,W_OK)==-1)
                  {
                     // try to enable write access.
                     chmod(target_name,st.st_mode|0200);
                  }
               }
               stats.mod_files++;
            }
```


```cpp:https://github.com/lavv17/lftp/blob/v4.4.15/src/FtpListInfo.cc#L667-L682
   if(parse_year_or_time(t,&date.tm_year,&date.tm_hour,&date.tm_min)==-1)
      ERR;

   date.tm_isdst=-1;
   date.tm_sec=30;
   int prec=30;

   if(date.tm_year==-1)
      date.tm_year=guess_year(date.tm_mon,date.tm_mday,date.tm_hour,date.tm_min) - 1900;
   else
   {
      date.tm_hour=12;
      prec=12*60*60;
   }

   fi->SetDate(mktime_from_tz(&date,tz),prec);
```

```cpp:https://github.com/lavv17/lftp/blob/v4.4.15/src/misc.cc#L393-L417
int parse_year_or_time(const char *year_or_time,int *year,int *hour,int *minute)
{
   if(year_or_time[2]==':')
   {
      if(2!=sscanf(year_or_time,"%2d:%2d",hour,minute))
	 return -1;
      *year=-1;
   }
   else
   {
      if(1!=sscanf(year_or_time,"%d",year))
	 return -1;;
      *hour=*minute=0;
   }
   return 0;
}
int guess_year(int month,int day,int hour,int minute)
{
   const struct tm &now=SMTask::now;
   int year=now.tm_year+1900;
   if(month     *32+        day
    > now.tm_mon*32+now.tm_mday+6)
      year--;
   return year;
}
```

parse_year_or_time()でftpのファイルリスト情報の日付部分が"%2d:%2d"のときはhh:mm、"%d"のときはyearと判定してします。yearのときはFtpListInfo.ccに戻ってprecを12*60*60としています。hh:mmのときはprecは30秒です。

ソースの他の箇所でも状況に応じてprecの値を設定しています。

# ncftp

* [NcFTP Software](http://www.ncftp.com/)

[ncftpget(1) manual page](http://www.ncftp.com/ncftp/doc/ncftpget.html)と[ncftpput(1) manual page](http://www.ncftp.com/ncftp/doc/ncftpput.html)を見ると、-Rオプションでディレクトリ以下を再帰的に転送できますが、ファイルの更新日時やサイズをチェックしてスキップしたりするわけではなく常にすべてのファイルを転送するようです。

# syncftpというrubygem

* [syncftp | RubyGems.org | your community gem host](https://rubygems.org/gems/syncftp)
* [github.com/glejeune/syncftp](https://github.com/glejeune/syncftp)

一度実行するとリモートディレクトリに.syncftpというファイルを作成して、ファイルのmd5チェックサムを保存します。次回のミラーリング実行時にリモートに.syncftpファイルがある場合はそれを利用して転送が必要かを判断しています。

```rb:https://github.com/glejeune/syncftp/blob/54c4a42f20a97508ef9fbf03bd4e44c11d1d85fa/lib/syncftp.rb#L148-L162
      # Read remote .syncftp
      begin
        ftp.gettextfile( remote+"/"+".syncftp", tmpname )
        @remote_md5s = YAML.load( File.open( tmpname ).read )
      rescue Net::FTPPermError => e
        raise Net::FTPPermError, e.message, caller if ftp.remote_file_exist?( remote+"/"+".syncftp" )
      end
      
      # Do the job Bob !
      send_dir( ftp, local, remote )
      
      # Write new .syncftp
      File.open( tmpname, 'w' ) do |out|
        YAML.dump( @local_md5s, out )
      end
```