PHP
Node.js

静的なサイトを別のサイトから FTP/FTPS で更新する

More than 1 year has passed since last update.

概要

スクリプト言語やデータベースが使えないサイトを更新するために、別のサイトから FTP/FTPS 通信で更新することに取り組む際に考えたことをまとめました。

スクリプト言語が使えないサイトを利用する動機

スクリプト言語を必要としないサイトの例として、個人の学習記録、企業の広報やプログラミング言語のマニュアル、自治体が挙げられます。Github Page を運用管理するためのツールはたくさん開発されています。

スクリプト言語が使えないサイトを利用するメリットはサイトの管理者やサーバーの管理者に要求される前提知識が少なくてすむことです。特にスクリプト言語の脆弱性のための対応をしなくてすみます。結果として、無料で利用できるホスティングサービスはたくさんあります。なかには無料で独自ドメインの割り当てができたり、PHP が利用できるサービスもあります (「ウェブクロウの無料プランで PHP 7.0 と CloudFlare の SSL/TLS を利用する」)。

SPA を採用する動機

SPA (Single Page Application) を採用するメリットはコンテンツとデザインの分離によるメンテナンス性の向上です。今回の場合、スクリプト言語を使うことはできないので、プレーンテキストや JSON で保存することになります。

さらに仮想 DOM を導入すれば、フォームなどの画面のパーツを npm で管理することができるようになります。npm で管理することで、パーツを入れ替えたり、複数のアプリケーションで共通化することがやりやすくなります。

2016年4月の時点では静的なサイトを対象とした SPA のジェネレーターを見つけることはできませんでしたが、今後の台頭を期待しています。

表示と編集をわけて考える

利用時間から考える

利用時間に関する要件を考えてみましょう。

表示機能は365日24時間にわたって利用できることがのぞましいのに対して、編集機能はそうではないということです。たとえば、個人のサイトを運営管理しているのであれば、仕事が終わった後の夕方から夜までのあいだに編集できればよいでしょう。

クラウドサービスの事例

Heroku で編集サイトを運用することを考えてみましょう。無料プランの場合、1日24時間のうち6時間を眠らせる必要があります。24時間編集できる必要があれば、有料プランを選ぶ必要があります。

この場合、表示と編集をわけることで、必要なお金が少なくてすむようなるということです。

必要なときに必要な機能を提供するというアイディアは AWS API Gateway と Lambda を組み合わせる取り組みからも見ることができます。AWS Lambda を使えばサーバーが不要になるということで2015年から「サーバレスアーキテクチャ」というキーワードを目にするようになりました (参考「サーバーレスアーキテクチャという技術分野についての簡単な調査」)。

必要なときに必要な機能を提供するというアイディアは固定費を最小限に抑えると言い換えることもできます。IoT を対象とした SIM サービスである SORACOM Air の固定費は1日に10円です。SORACOM は AWS を活用しています。

SSL/TLS の事例

別の事例は SSL/TLS の利用です。格安のホスティングサービスの場合、共有 SSL のドメインを別に用意していることがあります。独自ドメインも SSL/TLS に対応させる場合、別料金や SSL 証明書の管理が必要になります。

そのため、WordPress のチュートリアルのなかには共有 SSL 対応を述べたものがあります。

HTTP セッションの事例

最後の事例は HTTP セッションです。表示だけのサイトでセッションを発行しないのであれば、サーバーにセッション情報を保存しなくてすむので、サーバーの負荷を減らすことができます。

改竄が困難な JSON Web Token (JWT) を使うことで、サーバーがセッション情報を保存しなくてすむようにする取り組みがあります。PHP のコミュニティでは PSR-7 (HTTP メッセージの標準化) を前提としたセッションのライブラリの開発が進められています (「PSR7Session で JWT によるストレージ不要の HTTP セッションを試す」)。

コマンドクエリ分離の原則

プログラミングの方針について考えてみましょう。

読み書きの分離として思い浮かぶのはコマンドクエリ分離の原則 (CQS: Command Query Segregation) です (マーティン・ファウラーの解説)。コマンドは副作用のある操作で、クエリは副作用のない操作です。つまり、コマンドは編集機能、クエリは表示機能に相当します。コマンドとクエリをわけて考えることで、ふるまいが予測しやすくなったり、結果としてテストのコードも書きやすくなるということです。アーキテクチャレベルでのコマンドとクエリの分離に取り組む人もいます (「Greg Young流CQRS」)。

Electron による可能性

2015年頃から Electron によるデスクトップアプリが注目を集めるようになっています。Electron を導入する開発側のメリットは開発のしきいが低いことです (30分で出来る、JavaScript (Electron) でデスクトップアプリを作って配布するまで)。

開発側のデメリットは発展途上の技術なので、セキュリティ対策が周知でなかったり、未知の脆弱性があることです。

2016年4月の時点で次の記事を挙げることができます。

編集機能をもつサイトを用意する代わりに Electron を使ってサイトを更新する機能をもつデスクトップアプリケーションを用意する選択肢が考えられます。両方併用することも考えられます。

ユーザーとしてのメリットは編集サイトを管理しなくてすむことや、ユーザーがブラウザーや FTP ソフトを使わなくてすむことです。パソコンの操作に慣れていない人の負担を減らしたり、不特定多数の人が更新しやすくなります。

FTP/FTPS によるファイル更新のコードの例

PHP

ftp

ftp 拡張モジュールがインストールされている必要があります。

$server = 'サーバー';
$user = 'ユーザー';
$password = 'パスワード';

$source = 'ソースのファイル名';
$destination = '保存先のファイル名';

$conn = ftp_connect($server);
ftp_login($conn, $user, $password);
ftp_put($conn, $destination, $source, FTP_BINARY);
ftp_close($conn);

curl

ftp モジュールがインストールされていない場合の選択肢になるでしょう。curl の公式サイトのサンプルコードを利用させてもらいましょう。FTPS 通信を利用するのであれば、CURLOPT_USE_SSL オプションを指定します。

client.php
// https://curl.haxx.se/libcurl/php/examples/ftpupload.html
$ftpserver = 'FTP サーバーのドメイン';
$ftpuser   = 'ユーザー名';
$ftppass   = 'パスワード';
$ftppath   = '/path/to';
$localfile = 'テキストファイル';

$remoteurl = "ftp://${ftpuser}:${ftppass}@${ftpserver}${ftppath}/${localfile}";

$fp = fopen($localfile, 'rb');
$options = [
  // http://stackoverflow.com/a/32510182/531320
  CURLOPT_USE_SSL => CURLUSESSL_ALL,
  CURLOPT_URL => $remoteurl,
  CURLOPT_UPLOAD => 1,
  CURLOPT_INFILE => $fp,
  CURLOPT_INFILESIZE => filesize($localfile)
];
$ch = curl_init();
curl_setopt_array($ch, $options);
$error = curl_exec($ch);
curl_close($ch);

Flysystem

ファイル抽象化ライブラリです。アダプター方式で SFTP もサポートします。Composer によるインストールは次のとおりです。

composer require league/flysystem

コードの例は次のとおりです。

use League\Flysystem\Filesystem;
use League\Flysystem\Adapter\Ftp as Adapter;
use League\Flysystem\Adapter\Local;

$remote = new Filesystem(new Adapter([
    'host' => 'ドメイン',
    'username' => 'ユーザー名',
    'password' => 'パスワード',

    /** オプションの設定項目 */
    'port' => 21,
    'root' => '/path/to',
    'passive' => true,
    'ssl' => true,
    'timeout' => 30,
]));

$local = new Filesystem(new Local(__DIR__));
$file = 'test.txt';
$content = $local->read($file);
$remote->put($file, $content);

Gaufrette

ファイル抽象化ライブラリです。Composer によるインストールは次のとおりです。

composer require knplabs/gaufrette

コードの例は次のとおりです。

use Gaufrette\Adapter\Ftp as FtpAdapter;
use Gaufrette\Adapter\Local as LocalAdapter;
use Gaufrette\Filesystem;

$ftpAdapter = new FtpAdapter('/path/to', 'サーバーのドメイン', [
    'port'     => 21,
    'username' => 'ユーザー名',
    'password' => 'パスワード',
    'passive'  => true,
    'create'   => true,
    'mode'     => FTP_BINARY,
    'ssl'      => true,
]);

$remote = new Filesystem($ftpAdapter);

$file = 'hello.txt';
$local = new Filesystem(new LocalAdapter(__DIR__));
$content = $local->read($file);

$remote->write($file, $content, true);

php-ftp-client

FTP 通信 だけを対象としたラッパーライブラリです。Composer によるインストールは次のとおりです。

composer require nicolab/php-ftp-client

ディレクトリをまるごとアップロードするためのメソッドが用意されています。

client.php
$ftp = new \FtpClient\FtpClient();
$ftp->connect($host);
$ftp->login($login, $password);
$ftp->putAll($source_directory, $target_directory);

Node.js

ftp パッケージを導入します。

npm install ftp --save

SSL/TLS を有効にするには secure プロパティに true を指定します。

client.js
const Client = require('ftp');
const c = new Client();

c.connect({
  host: 'ホスト名',
  user: 'ユーザー名',
  password: 'パスワード',
  secure: true
});

c.on('ready', () => {
  console.log('ok');

  c.put('ftp.txt', 'ftp.txt', err => {
    if (err) throw err;
    c.end();
  });
});

c.on('error', err => {
  console.log(err);
});

c.on('greeting', message => {
  console.log(message);
});