豪鬼メモ

一瞬千撃

写真の削除機能

鋭意製作中の写真アルバム閲覧Webサービスに写真を削除する機能をつけた。一口に削除と言っても、なかなか深い話題なので、メモがてらに経緯をまとめておく。
f:id:fridaynight:20181006103734j:plain

デモ

まずは、こちらのデモサイトにアクセスされたい。唐突にパスワードを聞かれるので「abc」と入力する。ログイン機能はついているが、シングルユーザを前提としたシステムである。複数人で使うのは自由だが、アカウントは共通だ。家族やごく少人数の信用できるグループ内で写真を共有するのが目的なので、アカウント管理やアクセス権設定等の煩雑になりがちな機能は割愛した。
f:id:fridaynight:20181010004345p:plain

ログインすると、写真の一覧が表示される。前回と特に変わっていないように見えるが、機能的にはかなり進化している。右クリックなどの、「コンテキストメニュー」を表示する動作によって、個々のリソースを選択することができるようになっている。マウスやトラックパッドに右ボタンがあれば右クリックをすればよい。Macbookや最近のノートPCだと2本指でクリックしてもいい。Controlキーを押しながらクリックしてもいい。モバイル端末だと長押しをする。個々の写真を選択することもできるし、それらを含むコレクション(フォルダ)全体を選択することもできる。
f:id:fridaynight:20181010004527p:plain

選択したリソースは、削除できる。画面右上にあるメニューアイコンから開くメニューに「Selected」というセクションがあるので、そこにある「Remove Resources」をクリックすると、選択した写真が削除される。なお、デモサイトのデータは1時間毎に元に戻るので、遠慮なく消して構わない。
f:id:fridaynight:20181010004607p:plain

削除したデータは、実際にはゴミ箱に移動されるだけだ。ゴミ箱の中を見るには、メニューの「Go to」セクションにある「Trash」をクリックする。ゴミ箱は単に「/__trash__」という名前のコレクションにすぎないので、ゴミ箱の中のデータも通常と同じように閲覧したり選択したりできる。ゴミ箱の中で選択したデータは元の場所に復元することができる。写真やコレクションを選んでからメニューの「Restore Resources」をクリックすると、選択した写真は元の場所に戻される。ゴミ箱の中でさらに「Remove Resources」を選択すると、そのデータは完全に削除される。
f:id:fridaynight:20181010004650p:plain

削除機能の重要性

後で見返す価値のある写真だけをアルバムの中に残しておいた方が、追体験の質が上がるので、保存する写真の取捨選択をするのは極めて重要だ。一方で、写真データをアルバムに追加する処理は基本的にバッチ処理で行うことが多いだろう。その日に撮った写真100枚くらいを全て放り込む人もいるだろうし、Lightroomなどである程度絞り込んでから、気に入った写真だけを保存する人もいるだろう。いずれにせよ、アルバムに入れた写真を後で手軽に消せるという機能があることで、多めに写真を入れておくことが可能になる。

保存写真の取捨選択をする際には二段階に分けて行うのが望ましい。最初の段階では、ピンボケやブレや露出ミスや構図ミスなどが著しくてどうしようもない写真を捨てる。一連の連写の中からベストショットのみを残す操作もここで行う。その際には、過度に否定的・批判的にならず、良いところが少しでもある写真は残しておくのが望ましい。基本的に現像ソフトやデジカメのUIを使って、撮影したその日や翌日くらいこの最初の取捨選択は行うことが多いだろう。そこで生き残った写真は全てアルバムに放り込んでおく。

その後、一週間ほど寝かしておくとよい。それくらいの時が経つと、撮影した時の思い出が薄れてくるので、写真を撮った時の気分によるバイアスも和らいで、客観的に写真を眺めることができるようになるらしい。その状態で2回目の取捨選択を行う。一週間経ってもまだ良い写真だと思えるのであれば、ピントや露出や構図に多少問題があろうがなかろうが、それは保存すべき写真なのだ。逆に、技術的に完璧で撮った当日に最高の出来だと思っていた写真も、一週間経ってから感動を呼び起せないのであれば、それは保存する価値がない。

当家では妻と私がそれぞれ撮った写真を共通のアルバムに入れているのだが、重複した内容のものを削除する作業もアルバム内で行う。暇な時間に写真を見返したついでに重複や駄作を消すだけなので、そんなに頑張って編集する感じではない。いつでも消せるので、頑張って消す作業をする必要がないのだ。友人家族とお出かけした際に撮った写真をその友人に送ったりもするのだが、うちとしては長期保存しないけど妻が友人に送るかもしれないからしばらくアルバムに置いておくみたいなこともよくある。

設定

アルバムの編集機能を適切に運用するには、list_image.pyをCGIスクリプトとして設置した後に、スクリプトファイルを編集して、パスワードを設定することが必要となる。ファイルの冒頭付近にCGI_PASSWORD_HASHという定数の定義があるので、その値にパスワードの文字列のMD5ダイジェストの16進表記を設定する。MD5のデータはmd5sumなどのコマンドを使って得ても良いが、list_image.py自身で行うこともできる。

$ list_image.py --generate_hash foobarbaz
6df23dc03f9b54cc38a0fc1483df6e21

家庭内LANで閉じて運用する場合などにはパスワードを設定しないという選択もあり得る。その場合には、CGI_PASSWORD_HASHの値を空文字列にすればよい。また、CGIスクリプトからの更新操作を禁止したい場合には、ALLOW_UPDATE_FROM_CGIの値をFalseにすればよい。

CGIスクリプトからの更新を行う場合、当然ながら当該CGIスクリプトの実行ユーザがアルバムディレクトリのデータを更新できる権限を持っている必要がある。WebサーバまたはFastCGI等のモジュールの設定を適宜行うことになるだろう。また、インターネット上で運用する場合には、平文で認証データを流すとまずいので、HTTPSのWebサーバを使うべきだ。デモサイトは敢えて素のHTTPで動かしているけれども。

実装メモ

閲覧機能だけであればWebサーバの認証機構だけで運用しても問題なかったが、更新機能をつけるとどうしても自前でフォーム認証とセッション管理を実装することが必要になってしまう。そうなると、個人用のツールとはいえ、セキュリティホールとの戦いになるので、そこそこ慎重に設計と実装を進めねばならない。

ブラウザを立ち上げてアクセスする度に毎回ログイン画面でパスワードを入力するのはだるいので、永続クッキーとセッションキーを使った自動ログインを実現したい。初回のログイン時にはパスワード用のフォームを表示してpasswordという名前のパラメータとしてパスワードを送らせる。受け取ったパスワードのハッシュ値が事前に定数として登録しているハッシュ値と一致した場合、ログイン成功となる。一方で、サーバ上では真乱数を使って予測できないシード値を生成して一時ファイルとして保存しておく。ログイン成功時には、パスワードと秘密のシード値とその日の日付を結合した文字列のハッシュ値をセッションキーとして生成し、クッキーとしてクライアントに返す。以後、クライアントはそのセッションキーを送り返してくるが、そのセッションキーがサーバ側で生成したものと同じであればセッションを継続する。この方法だとパスワードの平文を送るのはログイン時の一回のみで、かつクライアントに保存されるクッキーデータにはパスワードは含まれない。もちろんセッションキーのコンテナであるクッキーが漏洩すればセッションハイジャックされることになるのだが、それを抑止するのはHTTPSSSL)の仕事である。日付をセッションキーに含めると日付を跨いだセッションで支障が出そうだが、過去31日のセッションキーをサーバ側で生成して照合するので、問題ない。なおかつ、セッションキーは毎回のアクセスで更新されるので、アクセスの間隔が31日以上開かなければ、再ログインは求められない。この31日という設定は定数CGI_SESSION_DURATIOが司る。ログアウト時には、クッキーのExpiresパラメータを過去にして期限切れさせた上で、303ステータスでログイン画面にリダイレクトさせればよい。

データの削除操作は、RestfulなAPIとして実装されている。例えば /foo/bar/baz というリソースを削除するには、以下のリクエストを発行すればよい。

DELETE /foo/bar/baz HTTP/1.1
...

実際には、認証用の情報も送る必要がある。最も単純な方法は、パスワードのパラメータを平文でつければよい。通信経路がSSLで保護されていれば問題ない。パスワードが指定できるクライアントにはその他のセキュリティチェックを行う意味がないので、簡素なインターフェイスになる。

DELETE /foo/bar/baz HTTP/1.1
Content-Length: 12
Content-Type: application/x-www-form-urlencoded

password=abc

curlを使ってデモサイトに対して上記をを実行するなら、こんな感じになる。

$ curl -i -X DELETE "http://fallabs.com/cram_image/demo/list_image.cgi/2018-03-31/20180331123342-b4802562" -d "password=abc"
HTTP/1.1 200 OK
Date: Tue, 09 Oct 2018 15:09:20 GMT
Server: Apache/2.4.3 (Unix) OpenSSL/1.0.1c
Set-Cookie: session=f5d1ef215d3681c423b730ec325b1d10; Path=/cram_image/demo/list_image.cgi; Expires=Fri, 09 Nov 2018 15:09:20 GMT
Transfer-Encoding: chunked
Content-Type: application/json

{
  "result": {
    "id": "/2018-03-31/20180331123342-b4802562", 
    "type": "record", 
    "record_serial": 0, 
    "data_url": "/cram_image/demo/album/2018-03-31/20180331123342-b4802562-data.jpg", 
    "metadata_url": "/cram_image/demo/album/2018-03-31/20180331123342-b4802562-meta.tsv", 
    "viewimage_url": "/cram_image/demo/album/2018-03-31/20180331123342-b4802562-view.jpg", 
    "thumbnail_url": "/cram_image/demo/album/2018-03-31/20180331123342-b4802562-thumb.jpg"
  }, 
  "stats": {
    "num_total_records": 0, 
    "num_total_collections": 0, 
    "num_scanned_records": 0, 
    "num_scanned_collections": 0, 
    "num_skipped_records": 0, 
    "num_skipped_collections": 0, 
    "elapsed_time": 0.0005791187286376953
  }, 
  "updates": [
    {
      "op": "remove", 
      "id": "/2018-03-31/20180331123342-b4802562"
    }
  ]
}

Webブラウザ上の永続クッキーにはなるべくパスワードを持たせたくないので、Webブラウザ経由でアクセスする場合にはセッションキーを使うことになる。セッションキーは閲覧時には単にクッキーとして送れば良いが、更新操作の際には同じデータをauth_tokenというパラメータとして明示的につける必要がある。これはCSRF攻撃の対策だ。また、念のため、Refererが同一のCGIスクリプトであることも必須とする。セッションキーをそのまま認証トークンとして使うことに議論があるらしいが、これをさらにハッシュ化したりしてもセキュリティレベルは変わらない(と高木先生も言っている)。

DELETE /foo/bar/baz HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: session=0123456789abcdef0123456789abcdef
Referer: http://fallabs.com/cram_image/demo/list_image.cgi
Content-Length: 43
Content-Type: application/x-www-form-urlencoded

auth_token=0123456789abcdef0123456789abcdef

Webサーバやプロキシの設定によってはDELETEメソッドを受け付けない場合があるだろうから、POSTメソッドでも削除を指示することができる。ゴミ箱のデータを元の位置に復元したい場合には、remove=1の代わりにrestore=1を送ることになる。

POST /foo/bar/baz HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: session=0123456789abcdef0123456789abcdef
Referer: http://fallabs.com/cram_image/demo/list_image.cgi
Content-Length: 52
Content-Type: application/x-www-form-urlencoded

auth_token=0123456789abcdef0123456789abcdef&remove=1

デモサイトのHTML文書から複数のリソースを削除するために、JavascriptXMLHttpRequest(XHR)を駆使している。ユーザが選択したリソースの各々のIDに対し、上述のPOSTのリクエストを非同期で発行しているだけだ。XHRはクッキーやRefererの設定をよろしく行ってくれるので、適切なコンテントボディを設定すれば更新処理が簡単に実装できる。

UIの設計と実装もなかなか悩ましかった。まず、削除対象のリソースをどうやって指定させるかを考えなければならない。最も素朴な方法はチェックボックスで選択することだが、UI的にちょっとオジン臭いし、モバイル端末だと押しにくくてイラつくので、右クリックや長押しで選択することにした。ちょうどコンテキストメニューが同じ操作体系なので、そのイベントを上書きすることで実装できた。個々の写真を選ぶのもコレクションで一気に選ぶのも同じ操作でできるので、慣れるとかなり使いやすい。実装でちょっと面倒だったのが、モバイルでサムネイル等のリンクを長押しした場合にクリックイベントが同時に発生してページ遷移してしまうことだった。これに関してはクリックイベントを0.5秒間だけ無効にして対処した。同様にして、長押しの指を離す時に指の位置がずれるとフリックイベントが検出されてしまう問題もあったが、これも0.5秒間だけ無効にしてお茶を濁した。

削除対象の選択のためにコンテキストメニューを上書きしてしまうと、右クリックで画像を保存するといった操作もできなくなってしまう。それでは困ることもあるので、サムネイル以外の画像データに対するコンテキストメニューはさらに上書きしてデフォルトの動作を有効にすることで問題を回避している。また、削除操作をした結果、ページ内のリソース数がゼロになったなら、自動的に次の兄弟に進むか、それがなければ前の兄弟に戻るか、それもなければ親に戻るようになっている。これによって、ある日の写真一枚一枚を見ながら削除していくという使い方ができるようになる。

アルバム管理の場合、ゴミ箱はかなり重要な機能だ。写真の取捨選択をするにはある程度寝かす方がいいと述べたが、捨てた写真もある程度寝かしておくと突然恋しくなったりするものだ。なので、捨ててから1ヶ月程度はゴミ箱に入れておくとよい。当家の場合、私が消した写真は実は妻のお気に入りだったということがよくあるので、ゴミ箱からサルベージできるようにしておくことは必須である。そのため、ゴミ箱からデータを削除する操作も選択的にできるようにしてある。「ゴミ箱を空にする」で全てのデータを消してしまうのではなく、ゴミ箱の中の古い半分だけ消すという操作もできるようになっているのだ。もちろんそれがだるい場合はゴミ箱全体を選択して削除してもいい。

これで、個々の写真が選択できるようになったので、任意の写真やコレクションを選択してスライドショー表示をしたりアーカイブデータのダウンロードをしたりといった機能を実装する足がかりもできた。それらも追い追い実装していこう。

まとめ

写真アルバム閲覧Webサービスに削除機能を統合することで、閲覧と取捨選択という操作をシームレスに行うことができるようになり、写真閲覧ライフが捗るようになった。達成している機能が地味な割に技術的には面倒臭いことが多いのだが、頑張って実装してよかった。

SONY デジタルカメラ DSC-RX100 1.0型センサー F1.8レンズ搭載 ブラック Cyber-shot DSC-RX100

SONY デジタルカメラ DSC-RX100 1.0型センサー F1.8レンズ搭載 ブラック Cyber-shot DSC-RX100