思い立って3時間で直リンク禁止の画像ダウンロードサイトを立ち上げたときのデプロイ譚

ひょんな事から、画像ファイルの配布（だけの）サイトがあったらいいな。という話になったので、じゃあ久し振りにやってみようという事になったのですが、ここ数年でも意外と色々環境が変わっていたり、すっかり忘れていたりして戸惑った部分があったので、デプロイ譚としてここにまとめ＆公開をさせて頂こうと思います。
いたらない実装や建て付けなどもあると思いますが、意外と必要な要素が凝縮されていると思うので、始めてトライする人や同じようなことをしたいという人には面白い内容になるかも知れません。

# 1. サーバーの準備

自宅のサーバーでサイトを立ち上げる時代はとっくに終わっているので、レンタルサーバーを採用します。
AWSのEC2という手もありますが、必要の無いレイヤーは手放すのが昨今の定石です。
EC2も大好きですが、EC2ですとOSやWeb Serverの面倒まで見なくてはならないので、工数もコストもリスクも増大します。

### 1-1. レンタルサーバーを探す

いつも使っていたQuiccaという100円レンタルサーバーを覚えていたので見に行きました。
既に新規会員登録は終了しているということで、いったんネットで最新の格安レンタルサーバーを確認してみます。
レンタルサーバーを選定する際の確認条件を挙げておきます。要件によって採用する確認条件は変わります。

1. 費用、拡張性（後から上位のプランに切り替えられるか）
1. 保存容量（今回の様に画像を多く扱う場合は特に注意）
1. 使用できる開発言語（特にASP系を使いたい場合は選択肢が少ないので要注意です）
1. データベースの利用可否（大概 MySQLが使えるはず）
1. 転送容量制限
1. SSLの利用可否と費用

一通り見てみましたが、これといった大きな決め手もなかったのでQuicca Plustにアカウント登録することにしました。
月額払いやお試し利用も可能ですが、面倒なので1年契約で2,400円ほど（寄稿時点）となりました。

# 2. ドメインの準備

共有ドメインを使えば無料でスルーできます。
ただ、今回はブランディングを若干重視しているためドメインを取ることにしました。
以前から利用しているお名前.comにログインして良さげなドメインを物色します。
ドメインが取れたらレンタルサーバーから提供されているIPアドレスをwwwサブドメインのAレコード、CNAMEレコードとしてお名前.comの設定に登録します。これにより、クライアント → お名前.com → 指定されたIPアドレス（レンタルサーバー）という名前解決の建て付けが成立します。

# 3. SSLの準備

Let's Encryptionの無料SSLを設定しなくては……と思いきや、今回採用したQuicca Plusでは標準（ボタン1つ）でこちらに対応していました。そのため、直ぐに https://test.com/ でアクセス出来るようになりました。
このSSLに対応していないと、ブラウザー上で危険なサイトだの、安全ではありませんだのといった文言が踊ることになります。
何かをダウンロードするサイトであれば特に対応しておきたいポイントです。

# 4. サイト(html, css)の準備

ここはデザインの問題なので、手っ取り早く「クレジット表記無し無料」でそれっぽいデザインのものを拝借して採用しました。

# 5. JavaScriptの実装（直リンク不可＆ダウンロード周り）

ここからやっと創作的なパートに入ってきます。デザインのみのテンプレートサイトにJavaScriptを実装していきます。
$がエラーになったので、インポートするのも面倒だったため今回はjQuery無しでの実装にしました。

### 5-1. 直リンク禁止のダウンロードボタンをJavaScriptで実装

画像データをダウンロードしてもらうサイトを標榜しているわけですが、画像データで避けたいのが直リンクです。
直リンクは

```javascript:htmlで直リンクされるパターン
<img src="gazou.png">直リンク禁止だよ！</a>
```

といったタグを記述したときに https://test.com/gazou.png に直接リンクを張られてしまうパターンです。
これを避ける方法は恐らくあまたあると思われますが、今回は時短優先でJavaScriptのみで実装を済ませたかったので、imgタグに後からバイナリーとして画像データを流し込むという方法を採用することにしました。ダウンロードもこのaタグのsrcをダウンロードさせるという方法で実装します。

imgタグのsrcに流し込むバイナリーデータはどうするのかといえば、"data:image/png;base64,~"の形式でテキスト化した画像データを用意するという方法を取ります。4MBの画像データをテキスト化（要はBASE64エンコード）すると、相当な量のテキストになります。

### 5-2. 肥大化するBASE64データをhtmlから切り離す

htmlに"data:image/png;base64,~"を埋め込んでいてはhtmlが見づらくてしかたありません。
そのため、これを切り離して別ファイルにすることを考えます。
別ファイルとして切り離したファイルを読み込む方法として、jsonpを使うことにします。
```json:jsonp
callback({"data":"data:image/png;base64,~"});
```
というテキストファイルにすることで、以下のJavaScriptで画像データを無事（idがgazouの）imgタグに流し込むことが出来ました。

```JavaScript:jsonpファイルの読み込み
<script>
  var callback = function(json) {
    document.getElementById("gazou").src = json.data;
  }
</script>
<script src="data/data.js"></script>
```
JavaScriptが動かないと画像データが入らないわけですから、直リンクが不可能なわけです。画像データをBASE64に変換するサイトは多数存在するので、秘匿性がそこまで高くない画像データであれば、それらを使うのが手っ取り早いです。
自作してもJavaScriptで簡単に作れるものと思いますので、自分色に染め上げてみてください。

## 5-3. ダウンロードの実装

以下の通りです。

```html:download.html
<button onClick="downloadImage('img001')" type="button">ダウンロード</button>     
```

```JavaScript:download.js
function downloadImage(image) {
  const src = document.getElementById(image).getAttribute("src");
  let xhr = new XMLHttpRequest();
  xhr.open('GET', src, true);
  xhr.responseType = "blob";
  xhr.onload = downloadImageToLocal;
  xhr.send();
}

function downloadImageToLocal() {
  let dlLink = document.createElement("a");

  const dataUrl = URL.createObjectURL(this.response);
  dlLink.href = dataUrl;

  const fileName = `img.${this.response.type.replace("image/", "")}`;
  dlLink.download = fileName;

  document.body.insertAdjacentElement("beforeEnd", dlLink);
  dlLink.click();
  dlLink.remove();

  setTimeout(function() {
    window.URL.revokeObjectURL(dataUrl);
  }, 1000);
}
```

# 6. ダウンロード数履歴記録の実装(IFTTT)

ダウンロードされた回数をカウントしたいという話になりました。PHPで実装するのかとか、外部のCGIを利用できないかとか考えられる実装方法が複数ありますが、今回は時短を優先してIFTTT経由でGoogle Spreadsheetに記録することを考えました。
IFTTTには元々Spreadsheetに書き込むアクションが用意されているため、WebHookと組み合わせればカウントアップが出来そうだと考えました。実際にはカウントアップではなくて行追加ということになりましたが、取り敢えずダウンロード数もそれ程見込まれないためこれで様子を見ることになりました。

### 6-1. JavaScriptだけで頑張ってみる

以下のコードを前出のdownloadImage(image)の先頭に追加しました。
【注意】こちらの実装は一部の環境、端末で動作しませんでした。最終的にPHPの力を借りることにしたので、結果だけ先に知りたい方は飛ばしてください。

```javascript:counter.js
  const occured = new Date();
  const counter = `https://maker.ifttt.com/trigger/MYCOUNTER/with/key/MYACCOUNT?value1=${image}&value2=${occured}&value3=${navigator.userAgent}`;
  let xhrCounter = new XMLHttpRequest();
  xhrCounter.open('GET', counter);
  xhrCounter.send(null);
```

IFTTについては別途記事を挙げておりますのでそちらをご覧下さい。

### 6-2. 毎度おなじみCORS制約登場

毎度想定はしているブラウザーのアナフィラキシー反応が発生です。
CORS制約のためiftttのWebHookをコールできません。
そこで、レンタルサーバーのWebServer設定に幾つかの設定を入れてみることにしました。Quicca PlusはNginxを使っているようですね。こちらの設定をマニュアルで追加出来たので以下を追加しました。

```javascript:Nginxの設定でCORSに対処
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS, HEAD";
add_header Access-Control-Allow-Headers "Content-Type, Origin, Authorization, Accept";
add_header Access-Control-Allow-Credentials true;
```

いまだにコンソールにエラーは出るものの……Mac上のFirefoxからはiftttのWebHookがコールされるようになりました。

### 6-3. 履歴が残らない端末／環境がある

ここまでやって、どうしてもダウンロード履歴が残らない端末／環境が出てきました。
どう考えてもCORS関連に因んだ問題に見えるため、ここで諦めて自分のドメイン配下のプログラムを経由することにしました。ここまでJavaScriptのみで頑張ってきましたが、PHPの登場になってしまいました。

```php:counter.php
<?
  // バリデーション
  if( ! preg_match('/^hogehoge\d{1-3}$/', $_GET["value1"])) exit;

  // 結局裏でIFTTTをコール
  $url = 'https://maker.ifttt.com/trigger/MYCOUNTER/with/key/MYACCOUNT';
  $data = http_build_query(
    array(
      'value1' => $_GET["value1"],
      'value2' => $_GET["value2"],
      'value3' => $_GET["value3"]
    )
  );
  $header = Array(
    "Content-Type: application/x-www-form-urlencoded",
    "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
  );
  $options = array('http' =>
    array(
      'method' => 'GET',
      'header'  => implode("\r\n", $header), 
    )
  );
  $contents = file_get_contents($url . '?' . $data, false, stream_context_create($options));
  echo 'ok';
?>
```
肝心のJavaScriptからは、こちらのcounter.phpを呼ぶようにします。自分のドメインは以下のリソースをコールしているのでCORSのアレルギー反応は起きないというわけです。これで問題なくあらゆる環境にてダウンロードカウンターが動くようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/132af9b6-42e8-ce49-ea04-32c640c7c9ca.png)
└─ こんな感じでリアルタイムにダウンロード日時とUserAgentが打刻されていきます。

# 7. iPadからフルメンテナンス（エディター編）

これでいったんリリースが可能にはなったのですが、iPadからindex.htmlの編集や画像の差し替えなどを行いたいということで対応が必要になりました。いったん自宅のファイルサーバーにアップしたものが、定期的にレンタルサーバーに上がる世界を構築していきます。

### 7-1. iPad用のテキストエディタ

初めてリサーチしましたが、取り敢えず [LiquidLogic](https://www.megasoft.co.jp/liquidlogic/)（リキッドロジック）というアプリが色分け表示も可能なので良さそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/df9db1f9-9081-8f69-6fbb-6b97b8f4d676.png)

使い込んだわけではありませんが、FTPの設定をして直接FTPサーバー上のファイルを編集できるのは便利ですね。今回は自家製FTPサーバー上のファイルを編集する形になりますが、場合によっては直接レンタルサーバー上のファイルをこのアプリで騙取してしまう運用も出来てしまうということです。
残念な点としては、折角FTPクライアントになってはいるのですが、ファイルアプリや写真アプリの中にある画像ファイルをFTPサーバーにアップロードすることが出来ないという点です。

そのため、FileExplorerという別のアプリを使って画像ファイルの追加や差し替えは行う事になりました。
それでも、この2つのアプリのおかげでiPadだけでサイトメンテナンスが出来るようになりました。

# 8. iPadからフルメンテナンス（自動アップロード編）

私の環境ではいったん自家製のFTPサーバーに全てのリソースが保存されるようになっている事は前出の通りです。
これは、当該のファイルサーバーが定期的にバックアップがとられていることにも起因しています。
因みに以下のようなちょっと分かりづらい構造で稼働しています。

・ファイルサーバー：MacMiniに繋がれたHDD5TB x 5台
・バックアッププロセス：MacMini上で動作するCCC
・バックアップ先：MacMiniに繋がれた28TBのHDD
・FTPサーバー：MacMini上のVMWareとして上がっているWindowsサーバー上のFileZilla Server

いずれにしても、自家製FTPサーバーにアップロードしただけでは実際のレンタルサーバーにアップロードされませんので、これを定期的にアップロードされるようにしていきます。

WinSCPでも良かったのですが、取り敢えず手元にあったFFFTPを使うことにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206965/579da46d-4b68-dc8e-57ce-f0687dd8c79f.png)

いったんFFFTPのプロファイルを作成して、自家製FTPサーバーにアップロードされているリソースをレンタルサーバーのFTPサーバー上にシンクロアップ出来ることが確認出来たら、Windowsのタスクを追加します。
-s "プロファイル名" -m -f -q
とします。FFFTPの環境設定で上書き保存、削除時の確認をOFFにしておくことをお勧めします。
このタスクが30分に1回実行されるようにトリガーを2つ追加しました。

# 9. アップロード前の確認

編集して自家製FTPサーバーにアップロードしたindex.htmlをブラウザーでどう見えるのか確認したいという要件が上がってきました。
既にDDNSにて自宅サーバーにリーチする事はできる状態ですので、ルーターの設定上で適当なポートに穴を空けて、ポートフォワーディングでWindowsサーバー上で起動しているWebServerに転送します。
この時、Windowsのファイヤーウォール設定でも当該のWebServerプロセスへのアクセスに穴を空ける必要があります。

以上