[PHP]効率よくFTP上のファイル情報を取得する方法

Read More

PHPでFTP上の詳細なファイル情報を取得する方法について。


PHPにはFTP操作関数としてftp_nlist()やftp_rawlist()、ftp_mdtm()などがあるが、扱うファイル数がだんだん増えてくると比例して処理時間が遅くなってくる。
FTPへの問い合わせの数を極力減らしどうにか処理軽減を図りたいと思うようになった。

そこで、FTPへの問い合わせは一度でファイルとディレクトリの情報を使い勝手のよい形にしてリストで取得する方法、というのを考えてみた。



--
FTPから取得したファイルやディレクトリにはパーミッションが付いているがこれがシンボル形式になっているのでこのままだと何かと使い勝手が悪い。
まずは先んじてFTP情報のパーミッションを変換する関数を作っておく必要がある。

■ファイルのパーミッションをシンボル形式から八進数に変換する関数
<?php
/**
 * ファイル・ディレクトリのパーミッションを8進数にして返却する
 * 
 * 第1引数にシンボル形式の値を指定する
 * 
 * 参考サイト)
 * ttp://unageanu.hatenablog.com/entry/20091218/1261139095
 * 
 * @param string $symbol // ex) '-rw-r--r--'
 * @return string or null
 */
function perm_2_octal($symbol=null)
{
  if(preg_match('/^[\-dbclps]([\-r])([\-w])([\-sxS])([\-r])([\-w])([\-sxS])([\-r])([\-w])([\-xtT])$/u', $symbol, $match))
  {
    $i = 0;
    if($match[1]=='r') $i |= 4 << 6;
    if($match[2]=='w') $i |= 2 << 6;
    if($match[3]=='x' or $match[3]=='s') $i |= 1 << 6;
    if($match[3]=='s' or $match[3]=='S') $i |= 4 << 9;
    if($match[4]=='r') $i |= 4 << 3;
    if($match[5]=='w') $i |= 2 << 3;
    if($match[6]=='x' or $match[6]=='s') $i |= 1 << 3;
    if($match[6]=='s' or $match[6]=='S') $i |= 2 << 9;
    if($match[7]=='r') $i |= 4;
    if($match[8]=='w') $i |= 2;
    if($match[9]=='x' or $match[9]=='t') $i |= 1;
    if($match[9]=='t' or $match[9]=='T') $i |= 1 << 9;

    return sprintf('%04o', $i);
  }
  
  return null;
}
?>


関数のコメントの部分に書いてあるURL「うなの日記」さんのものを流用して作った。
元の言語はRubyと思われるがこれを適当にPHP用に書き直した。
PHPに書き直して数パターンしか試してないのでちゃんと動いているか、やや自信がない。

コマンドプロンプトから簡単な動作テストをしてみた。
■動作テスト
<?php
echo perm_2_octal('-r--r--r--'). "\r\n";
echo perm_2_octal('-rwxrwxrwx'). "\r\n";
echo perm_2_octal('dr-xrw--wx'). "\r\n";
echo perm_2_octal('-r-srwSrwt'). "\r\n";
echo perm_2_octal('---Srwsr-T'). "\r\n";
?>


■結果
0444
0777
0563
7567
7074


たぶんダイジョブ。



--
さて長々と書くのも面倒になってきたのでサクッと行こう。
FTP上のディレクトリを指定してその直下にあるファイルやディレクトリの情報を使い勝手の良い形にしつつリストで取得してみる。

■指定したディレクトリ直下のファイル・ディレクトリの情報をリストで返却する関数
<?php
/**
 * 指定ディレクトリ直下のファイル・ディレクトリの情報をリストで返却する
 * 
 * ※独自関数perm_2_octal()を関数内で使用
 * ※第二引数はエスケープ処理されない。スペースやその他の文字を含む ファイル名では問題が発生する可能性があることに注意。
 * ※第二引数にファイルパスは指定できないので注意。厳密にはできるけど'path'の値が二重になる。
 * 
 * @param resouce $ftp_conn
 * @param string $dir_path	// 一覧を表示するディレクトリ。option(-t 日付の降順、-rt 日付の昇順)を含めることが可能。例: ftp_list_info($ftp_conn, "-rt /your/dir");
 * @return array
 */
function ftp_list_info($ftp_conn=null, $dir_path=null)
{
  $ret=array();
  
  $items = ftp_rawlist($ftp_conn, $dir_path);
  if(is_array($items))
  {
    if($pos=strpos($dir_path, ' ')) $dir_path = substr($dir_path, $pos+1); // option対策
    foreach($items as $item)
    {
      if(preg_match("/^([drwx\-]+)([\s]+)([0-9]+)([\s]+)([0-9]+)([\s]+)([a-zA-Z0-9\.]+)([\s]+)([0-9]+)([\s]+)([a-zA-Z]+)([\s ]+)([0-9]+)([\s]+)([0-9:]+)([\s]+)([a-zA-Z0-9\.\-\_ ]+)$/si", $item, $match))
      {
        if($match[3]!=='1' and ($match[17]==='.' or $match[17]==='..'))
        {
          // do nothing
        }
        else
        {
          $name = $match[17];
          $ret[$name]['name'] = $match[17];
          $ret[$name]['path'] = rtrim($dir_path, '/'). '/'. $match[17];
          $ret[$name]['size'] = $match[9];
          $ret[$name]['type'] = ($match[3]==='1')? 'file': 'directory';
          $ret[$name]['perm_symbol'] = $match[1];
          $ret[$name]['perm_octal'] = perm_2_octal($match[1]);
          $ret[$name]['owner'] = $match[5];
          $ret[$name]['group'] = $match[7];
          $ret[$name]['last_modified'] = date('Y/m/d H:i:s', strtotime($match[11]. " ". $match[13]. " ". $match[15]));
          $ret[$name]['mdtm'] = strtotime($match[11]. " ". $match[13]. " ". $match[15]);
        }
      }
    }
  }
  
  return $ret;
}
?>



コマンドプロンプトから適当にFTP上のディレクトリを指定してみる。

■動作テスト
<?php
// FTP接続のリンクID取得部分は省略
$res = ftp_list_info($ftp_conn_id, '/test_amb');
echo var_export($res);
?>



■結果
array (
  'done' => 
  array (
    'name' => 'done',
    'path' => '/test_amb/done',
    'size' => '0',
    'type' => 'file',
    'perm_symbol' => '-rw-r--r--',
    'perm_octal' => '0644',
    'owner' => '10007',
    'group' => '10007',
    'last_modified' => '2017/07/03 15:02:00',
    'mdtm' => 1499061720,
  ),
  'err.txt' => 
  array (
    'name' => 'err.txt',
    'path' => '/test_amb/err.txt',
    'size' => '391',
    'type' => 'file',
    'perm_symbol' => '-rw-r--r--',
    'perm_octal' => '0644',
    'owner' => '10007',
    'group' => '10007',
    'last_modified' => '2017/07/03 15:02:00',
    'mdtm' => 1499061720,
  ),
  'image' => 
  array (
    'name' => 'image',
    'path' => '/test_amb/image',
    'size' => '4096',
    'type' => 'directory',
    'perm_symbol' => 'drwxr-xr-x',
    'perm_octal' => '0755',
    'owner' => '10007',
    'group' => '10007',
    'last_modified' => '2017/02/10 00:00:00',
    'mdtm' => 1486652400,
  ),
  'new-demo6' => 
  array (
    'name' => 'new-demo6',
    'path' => '/test_amb/new-demo6',
    'size' => '4096',
    'type' => 'directory',
    'perm_symbol' => 'drwxr-xr-x',
    'perm_octal' => '0755',
    'owner' => '10007',
    'group' => '10007',
    'last_modified' => '2017/07/03 03:16:00',
    'mdtm' => 1499019360,
  ),
)


nameはファイル、もしくはディレクトリの名前。
typeでfileかdirectoryかが判別可能。
カレントディレクトリと一つ上に戻るディレクトリ(呼び名がわからんペアレントディレクトリ)はリスト的に邪魔なのでプログラム内で無視している。

last_modifiedはサーバのもつ日付によって左右されるのでサーバ時間がGMTなら日本時間にするのに9時間足す必要がある点に注意。
mdtmはサーバのもつファイルのタイムスタンプ。GMT環境下ならlast_modifiedをどうこうするよりこちらの値を直に使った方が話が早い。

あとFTP上で一定期間が過ぎたファイルはファイルの時間が見えなくなるので時間が必然的に00:00:00になる。
一定期間はたぶん半年なんだと思うけど、これがサーバ設定によるものなのかどうかはわからん。

perm_octalは八進数の「文字列」なので扱う上では若干注意が必要。




--
うん、これで一回の問い合わせでFTP上のファイルの情報がごそっときれいに取れるな。
AWSのS3に格納したファイルとFTP上のファイルの時間を比較するのに最初は一つ一つのファイルごとに時間を取ってたんだけどこれならだいぶ軽減する。

あと気になる点はFTP上に置いてあるファイル数によってはメモリが足りなくなるパターンかなあ。

第3引数で必要なキーだけを指定して取得できるようにするのもアリ、か?





-- 2017/10/24 追記
取得したファイル情報リストの順序を変更する方法。
<?php
// 日付の降順
$res = ftp_list_info($ftp_conn_id, '-t /test_amb');
// 日付の昇順
$res = ftp_list_info($ftp_conn_id, '-rt /test_amb');
?>

ftp_list_info()内でftp_rawlist()を呼び出している部分は実際にはftp_nlist()でも代用可能だがその場合はオプション(-l 詳細)が固定される。
そうすると第二引数での自由なオプション指定ができなくなってしまうので本関数内ではftp_nlist()は使わないものとする。


-- 2017/10/30 一部修正
ftp_list_info()の戻り値にキーmdtmを追加。
関係する文章回りも修正。









コメント投稿

名前 *
(10文字以内)
コメント *
(1000文字以内)