映画『福田村事件』感想

9月1日、ユーロスペースで『福田村事件』を観た。

www.youtube.com

関東大震災直後の千葉で香川からの行商15名のうち子供と妊婦を含む9名が、地元の自警団に朝鮮人と間違えられて殺害された実在の事件を基にした劇映画。

事件が起きたのは1923年(大正12年)9月6日、つまり100年前の今日だ。

監督は著名なドキュメンタリー監督の森達也氏。失礼ながらドキュメンタリー作家の初劇映画ということで正直期待していなかったが、観てみると意外にもしっかりした映画で驚いてしまった。

前半は狭い村でのドロドロとした愛憎が組み立てられていき、その情念が後半での虐殺事件に流れ込んでいく。冒頭から鍬や包丁など、日常の中に暴力の予感を忍ばせるのも上手いし、豆腐、白磁の指輪、朝鮮飴といった純白モチーフの使い方も手慣れている。

実はこの作品、森達也監督と同時期にたまたま劇映画畑の脚本スタッフ3名も「福田村事件」の映画化企画を走らせていて、じゃあ一緒にやろうと合流したという経緯がある。その結果、じっとりとした人間ドラマとジャーナリスト的な考証の両輪が互いに作品の強度を強め合っていると感じた。

国粋主義を背景にした民衆の暴走を描く都合上、リベラルな立場から当時の軍国主義を一方的に断罪する話になるのを危惧していたが、登場する全ての人物が虚栄心や不安、情欲、臆病さといった"人間としての弱さ"を抱えており、(味付けに賛否はあるだろうが)臭みは感じなかった。被害者の行商たちも無垢には描かれず、おのおのが複層的な差別構造の中にあることが提示される。

大正時代を描いた日本のフィクションにおいて、韓国併合によって日本人となっていた朝鮮人は通常透明化されるため、日本社会における生活者としての彼らが描かれている時点ですでに興味深い。当時はまだ日本の地域ごとの違いが激しく、香川と千葉の風俗や方言の違いも事件の背景になったという物語上の要請からも「ジーヤン(じゃんけん)」など地域の風俗の違いが多数紹介されるのだが、これも楽しめる。

俳優陣も、『Winny(2023)』で金子勇氏を主演した東出昌大、『怪物(2023)』で教師を演じた永山瑛太在郷軍人会の分会長を怪演する水道橋博士など、みんな上手くて緊張感が途切れない。鈴木慶一の音楽も良い。

映画は、結局震災のショックの中で不法な暴力を振るったのは朝鮮人ではなく、アンオフィシャルな「自警団」だったことを描く。当初は内務省の要請した自警活動であったが、彼らはやがて政府のコントロールを離れて暴走していく。

ではなぜ暴走してしまったのか。韓国併合以降の民族独立運動の高まりと国内での差別的扱いから、誰もが「仕返しに暴動を起こしてもおかしくない」と疑心暗鬼に陥ったから。事件の背景も、映画はそうやって紐解いていく。

本作はアイデンティティの物語だ。映画の最後、ある生存者が殺害された胎児を含む10人のフルネームを読み上げるシーンがあるのだが*1、その「名前」の扱い方が強く印象に残った。

各地を巡業する行商団は地域社会におけるマージナルな存在として描かれるが、しかし最後に読み上げられた彼らの本名を通じて、われわれは否応なく彼らと一種の繋がりを感じてしまう。名前には拭いがたく個人と国家を接続する力があるということに気付かされる。「みんな名前があったんです」からのスタッフロール(一つの映画を世に届けるために力を合わせた、たくさんの人の「名前」)は胸にこみ上げるものがあった。

映画を見終えて帰り道に想起したのは、最近流行りの自警団「私人逮捕系YouTuber」である。彼らは「警察は頼りにならねえ」とばかりに"不逞の輩"を私的に取り締まろうとする。これは、暴力は法と適正手続の範囲内でのみ正当化されるという近代国家の原則からはみ出す、血生臭い治安維持への欲望の顕れだ。

そしてユダヤ陰謀論者が経営するSNSでは、今日もアジア系移民への反感を煽る流言飛語が飛び交っている。2011年の東日本大震災ではSNSはここまで普及していなかったが、今また同じような大震災が起きたら…と思うと、本当に真剣に対処しなければいけない問題だと思う。

www.sankei.com

ところで、『福田村事件』を観たら、是非パンフレットも買ってほしい。監督・脚本・キャストインタビューから、関東大震災前後の近代史年表、全編の脚本(!)までついた大ボリュームで、時代背景に興味を持ったら、この冊子を起点に調べていけるようになってる。作中のあるセリフは朝鮮語で語られ字幕もつかないのだが、何と言ったのか知りたい人は脚本の該当箇所を読むといいだろう。

脚本家や研究者がこの事件が郷土史や防災史の資料からも削られていると語る部分では先日の松野官房長官の「朝鮮人虐殺の事実を把握できる証拠はない」発言を思い出し、うすら寒い思いがする。

このパンフレットには9月1日に朝鮮人虐殺を題材にした漫画『追燈』を無料公開した岡田索雲先生の寄稿文も掲載されており、その中で岡田氏自身による『追燈』の「ネタばらし」がさらっと触れられている。大したことではないのだが、わたしは全然その解釈に思い至っていなくて、創作物を読み解く能力がないなあとちょっと落胆した。ぜひ、『追燈』と合わせてお楽しみを。

『福田村事件』パンフレット

*1:事件関係者の名前は映画の中では変更されている

公式GoクライアントindigoでBlueskyのAPIを使ってみた

みなさんはまだXで消耗してますか?Blueskyに移住中の小野マトペです。

Go言語でBlueskyにポストを投稿するコードを書いたのですが、ベータ版とあってドキュメントも少なくやや難儀したので、メモとして残します。記事を通じて、Blueskyのアーキテクチャのユニークさも少しだけ伝われば良いなと思います。

クライアントには、Blueskyの公式Goリポジトリ github.com/bluesky-social/indigo のクライアント実装を使います。ただし、開発中で、今後使い方が変わる可能性もあるので気をつけてください。

github.com

概要

Bluesky は、大規模分散ソーシャルアプリケーションのための汎用連合プロトコル AT Protocol 上に構築されるアプリケーション実装であるという建て付けです。AT Protocolでは、クライアントやサーバーは XRPC というHTTPベースの独自のRPCによって相互に通信します。

https://atproto.com/guides/overview

そのため、indigo 内のパッケージも、xrpc api/atproto api/bsky の階層に応じて分割されています。(AT Protocolはatproto、Blueskyはbskyと略されることが一般的です)

  • github.com/bluesky-social/indigo/xrpc
    • XRPC関連実装
  • github.com/bluesky-social/indigo/api/atproto
  • github.com/bluesky-social/indigo/api/bsky

XRPCのスキーマLexiconというスキーマ定義言語によって定義されています。github.com/bluesky-social/atproto リポジトリにあるスキーマ定義ファイルから、atprotoとbskyの各種プログラミング言語用の実装がジェネレートされます。そのGo版実装が indigoリポジトリの/apiディレクトリ にあるので、今回はこれを使うことにします。

atproto.com

install

とりあえず始めましょう。

$ go get github.com/bluesky-social/indigo

認証

なにはなくともまずは認証です。

ユーザー認証はAT Protocolレベルの操作です。 ハンドル名とパスワードをInputパラメータとして AT ProtocolレベルLexiconの atproto.ServerCreateSession を呼び出すと認証セッションが生成され、JWTなどのセッション情報が返却されます。このセッション情報を xrpc.Client 構造体の Auth フィールドに設定することで、そのClientを認証セッションと共に使うことができるようになります。

package main

import (
    "context"

    "github.com/bluesky-social/indigo/api/atproto"
    "github.com/bluesky-social/indigo/xrpc"
)

func main() {
    cli := &xrpc.Client{
        Host: "https://bsky.social",
    }

    input := &atproto.ServerCreateSession_Input{
        Identifier: "[your-bluesky-handle]",
        Password:   "[your-password]",
    }
    output, err := atproto.ServerCreateSession(context.TODO(), cli, input)
    if err != nil {
        log.Fatal(err)
    }
    cli.Auth = &xrpc.AuthInfo{
        AccessJwt:  output.AccessJwt,
        RefreshJwt: output.RefreshJwt,
        Handle:     output.Handle,
        Did:        output.Did,
    }
    _ = cli
}

プロフィール取得

セッションを開始したら、さっそくユーザーのプロフィールを取得してみましょう。

先ほどのセッション作成は AT Protocol レベルのLexiconでしたが、プロフィール取得はBlueskyレベルのLexiconなので、 github.com/bluesky-social/indigo/api/bsky パッケージをimportします。引数に私のハンドルの "matope.bsky.social" を指定します。

profile, err := bsky.ActorGetProfile(context.TODO(), cli, "matope.bsky.social")
if err != nil {
    log.Fatal(err)
}
pp.Print(profile)

profile をお好きなpretty printerでダンプすると、こんな感じの詳細プロファイルビューが取れます。

&bsky.ActorDefs_ProfileViewDetailed{
  Avatar:         &"https://cdn.bsky.social/imgproxy/0BAuv8Ek6y_0lY2sdnnPb03ZGzYSCVWLa_r9pohTqVY/rs:fill:1000:1000:1:0/plain/bafkreic4qii34cqymxz5en2m5vtff7duvdggp2ljrn2g46gc4d3zr6cv5y@jpeg",
  Banner:         &"https://cdn.bsky.social/imgproxy/Uymx6dZXSfo7fTSbfop5Xa73mOGqT-7mMftziXq0sJI/rs:fill:3000:1000:1:0/plain/bafkreiadta7acsqzwhif3amdgax63ch4cx7knrub7nxhgebvq6d6i5colq@jpeg",
  Description:    &"Software Engineer / Golang / Liberalist / ADHD診断済み  / Long-COVID療養中\nhttps://twitter.com/ono_matope",
  Did:            "did:plc:kzxl37blybhp7kvn2clme7j2",
  DisplayName:    &"小野マトペ",
  FollowersCount: &1501,
  FollowsCount:   &1157,
  Handle:         "matope.bsky.social",
  IndexedAt:      &"2023-08-16T00:26:28.486Z",
  Labels:         []*atproto.LabelDefs_Label{},
  PostsCount:     &1628,
  Viewer:         &bsky.ActorDefs_ViewerState{
    BlockedBy:   &false,
    Blocking:    (*string)(nil),
    FollowedBy:  (*string)(nil),
    Following:   (*string)(nil),
    Muted:       &false,
    MutedByList: (*bsky.GraphDefs_ListViewBasic)(nil),
  },
}.

ユーザー投稿取得

つぎはユーザー投稿の取得です。

func bsky.FeedGetAuthorFeed(ctx context.Context, c xrpc.Client, actor string, cursor string, limit int64) (bsky.FeedGetAuthorFeed_Output, error)

actor引数は、"matope.bsky.social" のようなハンドルか、またはDID(ユーザーの不変の識別子。CreateSessionActorGetProfileで取得できます)で指定します。

feed, err := bsky.FeedGetAuthorFeed(context.TODO(), cli, ”matope.bsky.social", "", 10)
if err != nil {
  return err
}

出力はこんな感じになります。

&bsky.FeedGetAuthorFeed_Output{
  Cursor: &"1692252773757::bafyreihuynzpx5457nryn5b4hllhpknl4fihgt4knhhieactxrlqc7ooyq",
  Feed:   []*bsky.FeedDefs_FeedViewPost{
    &bsky.FeedDefs_FeedViewPost{
      Post: &bsky.FeedDefs_PostView{
        LexiconTypeID: "",
        Author:        &bsky.ActorDefs_ProfileViewBasic{
          Avatar:      &"https://cdn.bsky.social/imgproxy/0BAuv8Ek6y_0lY2sdnnPb03ZGzYSCVWLa_r9pohTqVY/rs:fill:1000:1000:1:0/plain/bafkreic4qii34cqymxz5en2m5vtff7duvdggp2ljrn2g46gc4d3zr6cv5y@jpeg",
          Did:         "did:plc:kzxl37blybhp7kvn2clme7j2",
          DisplayName: &"小野マトペ",
          Handle:      "matope.bsky.social",
          Labels:      []*atproto.LabelDefs_Label{},
          Viewer:      &bsky.ActorDefs_ViewerState{
            BlockedBy:   &false,
            Blocking:    (*string)(nil),
            FollowedBy:  (*string)(nil),
            Following:   (*string)(nil),
            Muted:       &false,
            MutedByList: (*bsky.GraphDefs_ListViewBasic)(nil),
          },
        },
        Cid:       "bafyreiborxodz3x66y32744ymmh6sqmv4np5konehhp5qcjb3g4mhcbco4",
        Embed:     (*bsky.FeedDefs_PostView_Embed)(nil),
        IndexedAt: "2023-08-17T07:04:05.876Z",
        Labels:    []*atproto.LabelDefs_Label{},
        LikeCount: &0,
        Record:    &util.LexiconTypeDecoder{
          Val: &bsky.FeedPost{
            LexiconTypeID: "app.bsky.feed.post",
            CreatedAt:     "2023-08-17T07:04:05.496Z",
            Embed:         (*bsky.FeedPost_Embed)(nil),
            Entities:      []*bsky.FeedPost_Entity{},
            Facets:        []*bsky.RichtextFacet{},
            Langs:         []string{
              "ja",
            },
            Reply: (*bsky.FeedPost_ReplyRef)(nil),
            Text:  "テスト",
          },
        },
        ReplyCount:  &0,
        RepostCount: &0,
        Uri:         "at://did:plc:kzxl37blybhp7kvn2clme7j2/app.bsky.feed.post/3k5564cyinh2j",
        Viewer:      &bsky.FeedDefs_ViewerState{
          Like:   (*string)(nil),
          Repost: (*string)(nil),
        },
      },
      Reason: (*bsky.FeedDefs_FeedViewPost_Reason)(nil),
      Reply:  (*bsky.FeedDefs_ReplyRef)(nil),
    },
...

タイムライン取得

タイムラインの取得は、 bsky.FeedGetTimeline RPCメソッドを使います。

func bsky.FeedGetTimeline(ctx context.Context, c xrpc.Client, algorithm string, cursor string, limit int64) (bsky.FeedGetTimeline_Output, error)

algorithm 引数があるのが興味深いですが、とりあえず空文字で大丈夫なようです。

tl, err := bsky.FeedGetTimeline(context.TODO(), cli, "", "", 10)
if err != nil {
    return err
}

ポストの投稿

次に、Blueskyにポストを投稿してみましょう。ここまではbskyで定義されていたLexiconを使っていましたが、ポストの投稿では、atprotoレベルのLexiconを使います。

import (
    // ...
    lexutil "github.com/bluesky-social/indigo/lex/util"
)

// ...

input := &atproto.RepoCreateRecord_Input{
    Collection: "app.bsky.feed.post",
    Repo:       cli.Auth.Did, // "matope.bsky.social" のDID
    Record: &lexutil.LexiconTypeDecoder{&bsky.FeedPost{
        Val: &bsky.FeedPost{
            Text:      text,
            CreatedAt: time.Now().Format(util.ISO8601),
            Langs:     []string{"ja"},
        }},
    }},
}

resp, err := atproto.RepoCreateRecord(context.TODO(), cli, input)
if err != nil {
    return err
}
pp.Print(resp)

コード的には atproto.RepoCreateRecord 関数を使い、Input構造体に

  • Repo → ユーザーのDID
  • Collection → "app.bsky.feed.post"
  • Record → bsky.FeedPost 構造体

をセットしています。Repo、Collection、Recordといったキーワードがユニークですね。AT Protocolでは、あるユーザーの持つデータはサーバー上の、ユーザーごとのリポジトリに格納されます。リポジトリDAG-CBORというコーデックでエンコードされたレコードを保持するキーバリューデータベースです。ここでは、

matope.bsky.social リポジトリapp.bsky.feed.post コレクション以下に、(サーバサイドで決定されるタイムスタンプをキーとして)、app.bsky.feed.post スキーマに従うレコードを作成せよ」

という操作を行なっているわけです。

Langsはポスト言語の設定です。Blueskyは投稿ごとにコンテンツの言語メタデータを(複数)設定できるので、ここで設定しておきます。値は(これを入れておかないと、閲覧者のコンテンツ言語と異なる場合に "Translate this post" リンクが出てしまう)

作成したRecordの識別子情報が出力されました。(ここではCIDとURIが何であるかには踏み込みません。私がよく分かっていないからですが…)

&atproto.RepoCreateRecord_Output{
  Cid: "bafyreiffpevc6ew665lqhocz2226q2runa7ubpwqzipszm7moy76zm4mja",
  Uri: "at://did:plc:kzxl37blybhp7kvn2clme7j2/app.bsky.feed.post/3k52rbvnqfg2b",
}.

いいね

上で作成した投稿にいいねをつけてみましょう。下のコードをよく見てください。先ほとど同じ、 atproto.RepoCreateRecord RPCメソッドを呼び出していて、また、 Collection の指定が app.bsky.feed.like になり、 Record の中身が bsky.FeedLike 型になっています。このRecordのSubjectに、先ほど得られたCIDとURIをコピペします。

func like(cli *xrpc.Client) error {
    input := &atproto.RepoCreateRecord_Input{
        Collection: "app.bsky.feed.like",
        Repo:       cli.Auth.Did,
        Record: &lexutil.LexiconTypeDecoder{
            Val: &bsky.FeedLike{
                CreatedAt: time.Now().Format(util.ISO8601),
                Subject: &atproto.RepoStrongRef{
                    Uri: "at://did:plc:kzxl37blybhp7kvn2clme7j2/app.bsky.feed.post/3k52rbvnqfg2b",
                    Cid: "bafyreiffpevc6ew665lqhocz2226q2runa7ubpwqzipszm7moy76zm4mja",
                },
            },
        },
    }
    out, err := atproto.RepoCreateRecord(context.TODO(), cli, input)
    if err != nil {
        return err
    }
    pp.Print(out)

いいねができました。

セルフいいね

つまり、投稿も、いいねも、AT Protocolとしてはリポジトリ上のレコード作成という点で等価であり、違いはパス(コレクション+キー)とデータ定義だけなのです。ちなみに、フォローも同じくRepoCreateRecord Lexiconで、app.bsky.graph.follow コレクションへのレコードを作成します。

  • 投稿
    • コレクション: app.bksy.feed.post
    • スキーマ: bsky.FeedPost
  • いいね
    • コレクション: app.bksy.feed.like
    • スキーマ: bsky.FeedLike
  • フォロー
    • コレクション: app.bsky.graph.follow
    • スキーマ: bsky.GraphFollow

いかがでしたか?このように、分散レポジトリ/コレクション/レコードの階層的データ構造によってATProtocolがプラットフォームとしての拡張性・相互運用性を実現していることの一端をご理解いただけたのではないでしょうか。それではよいブルスコライフを。

カメラ初心者がカメラ沼に入ってみた。(長文)

会社を辞めたので、旅行なんかしたいと思ったことないのだけど、海外旅行をしてみようと思いたち、衝動的にExpediaとAirbnbでバリ島への航空券と宿泊先を予約した(出発8日前のチケットとか、取れるんですね)。で、どうせ海外行くならいい機会だからちゃんとしたカメラ買って風景とか撮りたいなとなり、以前からうっすら欲しかったカメラを買うことにした。私と同じカメラわからん人の助けになるように、購入に至るまでの検討過程をここに記す。また参考にしたWebサイトなども可能な限り添付していく。何を買ったかだけ知りたい人は末尾までスクロールしてくれ。ちなみに今これを書いている時点でカメラを箱から出していない。旅行までに練習する時間があるだろうか…。というか旅行の準備なにもしてない…。

やりたいこと

  • 旅先で写真やムービーを撮りたい
  • 日常でカメラを持ち歩いて、面白い構図とか撮りたい。
  • いい感じにボケてる写真とか撮りたい。
  • 予算は最大でコミコミ20万まで出してもいいかもしれないけど、できれば10万円前後に抑えたい。

ボディ要件

やりたいことから導き出される要件はこんな感じ

コンパクトであること

旅にせよ日常にせよ、「サッと取り出して取る」性は確保しておきたい。ガジェットの軽さは心の軽さに繋がる。あくまで日常の延長に写真の撮影を組み込みたいのだが、一眼レフやフルサイズミラーレスのように大きなカメラは、構えた瞬間に「カメラマン」になってしまうような気がして気後れしてしまう。そのため、今回は上級者向けであるフルサイズのカメラは除外し、マイクロフォーサーズAPS-Cのミラーレスとした。

関係ないけど、一見そうは見えない小さなガジェットで大きなパフォーマンスを発揮するガジェットが私は好きだ。そこらへんは私がゲーミングPCとして Intel NUCを好んだのと似ているかもしれない。

matope.hatenablog.com

ムービー撮影機能が強力であること(特に4K動画)

せっかく高品位レンズとセンサーを買うので、必要とあらばムービーにも使いたい。日常ではムービーの撮影機会はなかなか無いかもしれないが、少なくとも旅では歩きながら撮ったりもするだろう。そしてそのムービーはいい感じでなくてはならない(いい感じじゃなくて良いならスマホ使った方が便利なので)。

4K録画であることはそれほど必須ではないが、撮れるに越したことはない。

www.youtube.com

強力な手ぶれ補正

デジカメの手ぶれ補正は、写真を撮るときには単なる暗所でのブレ防止というだけでなく、一部の機種では夜景を手持ちだけで撮れるなど、表現の広がりをになう。これは特に旅行では使いどころがありそうである。

5軸手ぶれ補正 E-M5 Mark II | デジタル一眼カメラ OM-D | オリンパス

また、旅でのムービー撮影は歩きながらの撮影が想定されるが、その歩行による見苦しい手ブレを取り除くことはムービーを「いい感じ」にする上で極めて重要であろう。デジカメの手ブレ補正には、ボディ内手ぶれ補正レンズ内手ぶれ補正、そしてその両方を組み合わせた方式がある。一般的にはレンズ内手ブレ補正のみのものは手ブレ補正の効果が少ないようだ。

手ぶれ補正の効果については以下の動画が分かりやすい。

レンズ内手ぶれ補正のみのα6400の手ブレを検証する動画

自撮り対応

ムービー撮影では自分を被写体にするケースが想定されるので、構図の確認のために自撮りに対応しているとうれしい。デジカメにおける自撮り対応とは、液晶ファインダーが縦(180度チルト)か横(バリアングル)かに180度回転するもののことを言う。

バリアングル液晶とチルト液晶の違い - フォトスク

hoboshuhu.com

hoboshuhu.com

防塵防滴

デジタルカメラは極めて繊細なガジェットで、旅先での急な雨や砂浜などでの扱いは注意を要すると聞く。一部の機種は「防塵防滴」を謳っており、ラフな扱いが許されるようだ。それは良いのでチェック項目に追加した。

第一ラウンド:ネット調査

そう言うわけで、「コンパクト」「4K動画撮影機能」「自撮り対応」「手ぶれ補正機能の充実」を中心に見ていきたい。とはいえカメラ全然わからんので、この辺から読み始めて、出てくる機種の価格コムと公式ページを回ってスペックを確認していった。

キヤノン

EOS Kiss M

キヤノン:EOS Kiss M|概要

  • コンパクト:本体重量 351g(軽量)
  • 4K動画:対応
  • 自撮り:対応(バリアングル液晶)
  • 手ぶれ補正:レンズ内手ぶれ補正のみ
  • 防塵防滴:非対応

キヤノン EOS KISS シリーズ初のミラーレス EOS Kiss M。EF-M規格レンズの選択肢が少ないことに否定的な意見が多いが、それは自分に問題になるかは分からなかった。店頭で触ってみた感じは、操作がわかりやすくて好印象だった。しかし、手ぶれ補正が「レンズ内手ぶれ補正」のみで弱かったり 、動画撮影中のAFが弱いらしいというレビューがあったので除外した。

ニコン

ヨドバシの売り場でニコンの機種は全て大きくて重かったので「プロ用なのかな」と考えあまり調査せずスルーした。

ソニー

ソニーのαシリーズは差別化のために機能を出し惜しみしているような…。

オリンパス

オリンパスのレンズはパナソニックとともにマイクロフォーサーズ規格を採用しており、選択肢が多いらしい。また、強力なボディ内手ぶれ補正による暗所や夜景の撮影能力をアピールする機種が多くみられた。

OM-D E-M10 Mark III

OM-D E-M10 Mark III | デジタル一眼カメラ OM-D | オリンパス

  • コンパクト: 本体重量 362g(軽量)
  • 手ぶれ補正:強力なボディ内手ぶれ補正。
  • 4K動画:対応(4K@30p)
  • 自撮り:非対応
  • 防塵防滴:非対応

E-M10はコンパクトだし4Kも撮れるけど自撮りができないので除外。惜しい。

OM-D E-M5 Mark II

OM-D E-M5 Mark II | デジタル一眼カメラ OM-D | オリンパス

  • コンパクト: 本体重量 417g(そこそこ軽量)
  • 手ぶれ補正:強力なボディ内手ぶれ補正
  • 自撮り:対応(バリアングル式)
  • 4K動画:非対応
  • 防塵防滴:対応

E-M5 Mk2 は自撮りができるのに4K動画に対応していないのが惜しい。しかし、オリンパス的には動画専用に強化された手ブレ補正やAF機能から「シネマクオリティ」と推しているので、4K撮影こそできないものの「動画の品質」自体には信頼をおけそうだ。公式の作例動画も良さそうである。この機種はすぐに後継機が出るという噂があり、そちらでは4Kに対応するのではないか。私は旅行が来週なので待てないが…。

www.youtube.com

参考にしたレビュー

dent-sweden.com

dc.watch.impress.co.jp

www.youtube.com

www.youtube.com

パナソニック

オリンパスとならんでマイクロフォーサーズ陣営のパナソニックオリンパスはスチルに強いが、パナソニックは動画につよいという定評があるらしい。

LUMIX DC-GF10/GF90/GF9

DC-GF10/GF90 | Gシリーズ 一眼カメラ | デジタルカメラ LUMIX(ルミックス) | Panasonic

  • コンパクト:本体重量 269g(とても軽量)
  • 自撮り:対応(180度チルト)
  • 手ぶれ補正:なし
  • 4K動画:対応
  • 防塵防滴:非対応

ちいさくてかわいい入門機。キットにパンケーキレンズとかついてる。かわいいのに4K動画も取れたりする。USB充電機能とかもうれしい。しかし手ブレ補正はついていないので除外。他の機種にはだいたいついている、スマホと連携してGPS情報を設定する機能がないのも旅の記録としては不便かもしれない。

LUMIX DC-GX7 Mark III

DC-GX7MK3 | Gシリーズ 一眼カメラ | デジタルカメラ LUMIX(ルミックス) | Panasonic

  • コンパクト:本体重量 407g(そこそこ軽量)
  • 自撮り:非対応
  • 手ぶれ補正:ボディ内蔵
  • 4K動画:対応(4k@30p)
  • 防塵防滴:非対応

良さそうなカメラだが自撮り非対応なので除外。あと外部マイク端子がないようだ。ムービーとして使うなら、やはり小型でもいいのでマイクは装着しておきたいところだ。

www.youtube.com

新製品レビュー:Panasonic LUMIX GX7 Mark III (外観・機能編) - デジカメ Watch

LUMIX DMC-G8

DMC-G8|デジタルカメラ LUMIX(ルミックス)|Panasonic

  • コンパクト:本体重量 453g (ちょっと重く大きい)
  • 4K動画:対応(4K@30p)
  • 自撮り:対応(バリアングル式)
  • 手ぶれ補正:ボディ内蔵+レンズ内蔵式
  • 防塵防滴:対応

4K動画、バリアングル、強力な手ぶれ補正と機能スペックが揃ったが、コンパクトさの観点ではでちょっと尻込みしてしまう。しかし有力候補だ。「生活の面白い視点を切り取る」がやや薄れ、「お仕事のムービーを撮影する」に近づいていく、のをどこまで許容するか、である。

www.youtube.com

LUMIX DC-G9 PRO

DC-G9 | Gシリーズ 一眼カメラ | デジタルカメラ LUMIX(ルミックス) | Panasonic

  • 4K動画:対応(4K@60p)
  • 自撮り:対応(バリアングル式)
  • 手ぶれ補正:ボディ内蔵+レンズ内蔵式
  • 防塵防滴:対応
  • 総重量:本体重量 586g(重い)

4K@60p対応かつ自撮り対応と、動画機能のかなりの充実を感じる。LUMIXマイクロフォーサーズカメラとしては最上位機種である。機能的には申し分ないが、いかんせん旅に持って出るには大きく、重い。価格的にもボディのみでおよそ12万と、予算オーバーだ。

www.youtube.com

LUMIX DC-G99

DC-G99 | Gシリーズ 一眼カメラ | デジタルカメラ LUMIX(ルミックス) | Panasonic

  • 4K動画:対応(4K@60p)
  • 自撮り:対応
  • 手ぶれ補正:ボディ内蔵
  • 防塵防滴:対応
  • 総重量:484 g(やや重い)

G8とG9 PROの後継機らしい。実物は見ずにG9の後継機なら重いだろうとスルーしてしまったのだが、今見ると重量はG8に近く、後述する瞳AFの改良も入っており、検討に値する機種だったかもしれない。しかし、いずれにせよG8の時点でサイズに気後れしてはいたし、中古価格もこなれておらず、レンズキットで13万からあるとはいえキットレンズに不満がある(後述)ので、やはり予算オーバーではあった。

e-m-wonderful.com

第一ラウンド総評

上記に挙げた機種のうち、まず「タッチAF」「内蔵手ブレ補正」は必須と考え、これらを装備していない機種を除外していった。その結果…「小さい」「自撮り可能」「4K動画撮影可能」の三つの要件を同時に満たすカメラが、なんと存在しないことがわかった。トリレンマ!

オリンパスOM-D E-M10 Mark III オリンパス OM-D E-M5 Mark II パナソニック LUMIX DC-G7 Mark III パナソニック LUMIX DCM-G8
小型軽量
自撮り × ×
4K動画 ×
防塵防滴 × ×

そのため3要件のうち、一つを諦めて二つを選ぶことで購入する製品を確定させる必要が出てきた。

もっとも優先しておきたい要件は「自撮り可能」だったので、まず E-M10 Mark III と DC-G7 Mark III が脱落した。ムービーで自分を被写体にできないと不便、というのは想像しやすい。次に優先したい要件は「コンパクトさ」で、もっとも優先度が低いのは4K動画の撮影能力なので、「小さい+自撮り可能」のE-M5 Mark IIで確定…かと思われたが、スペック上のサイズ・重量の差が体感として許容可能であれば、4K動画が撮影できるG8 LUMIXの方が魅力的な選択肢だ。ここから先は実際に実機を触ってみないと判断がつかないだろう。

店頭でのインプレッション

そんなわけで、E-M5 Mark II と LUMIX DCM-G8 まで絞り込んだうえで、Twitterで教えてもらった中野のフジヤカメラ本店で店員さんに相談のうえ、機種を決定することにした。フジヤカメラさんは新品・中古のカメラを扱うお店で、初心者だと言うと店員さんがとても丁寧に教えてくれる。「F値ってなんですか?」とか「ダイヤルのAとかPとかって何ですか?え、Aってオートだと思ってました…」「パンケーキレンズとかどうなんですか」とか色々教えてもらった。

で、E-M5 Mark II と LUMIX G8 の中古品を触らせてもらった。

  • フォームファクタ
    • たしかにG8の方が少し大きいが、両者を持ち比べてみるとE-M5の方は中身が詰まっている感じでズッシリした感じ。しかし、重量に関しては標準ズームレンズを装着してしまうと殆ど関係なくなると感じた。ガジェットのコンパクトさを維持するには短い単焦点レンズが必要なようだ。
    • G8はグリップがついてる分確実にホールドしやすいと感じた。大きいのも悪いことばかりではない。しかし、自撮りすると、グリップが逆向きになるのでとても持ちにくいという問題が発生した。(やはり歩き自撮りはスマホの領分だろうか…)
    • E-M5の瞳AFの方が強力なようだ。自撮りモードにした時、E-M5はすぐに瞳を認識して私にフォーカスを合わせてくれたが、G8はうまくフォーカスしてくれず、わたわたとタッチAFで調整しなくてはいけなかった。自撮り中はカメラを持ちにくいので、オートフォーカスの信頼性が重要なことがわかった。G9やG99では瞳AFが強化されているようだ。
  • 手ぶれ補正は、明確にオリンパスの方が優れているとのことだ。本体のみで超強力というのが重要らしい。パナソニックはレンズ内手ぶれ補正に対応したレンズを選ぶ必要ができてしまう。
  • 4K動画はまだまだ観る環境も整ってないし、そこはフルHDで妥協しても良さそう。

以上のインプレッションから、 E-M5 Mark IIに決定した

レンズ編

さて、ここで話は終わらない。一緒に使うレンズを決めないといけない。わからないので、とりあえずGCM-G8とE-M5 Mark II のキットレンズを起点に説明してもらった。二つのカメラはどちらもマイクロフォーサーズなので、どれを選んでも良い。

標準ズームレンズ

LUMIX G VARIO 12-60mm / F3.5-5.6 ASPH/ POWER O.I.S.

G8のキットレンズ。いわゆる標準ズームレンズ。防塵防滴。ズームレンズとしてはサイズが小さいのが魅力。レンズ内手ぶれ補正あり。一本で35mm換算で広角側24mm相当から望遠側120mm相当までカバーできる(マイクロフォーサーズ焦点距離は、2倍すると一般的な画角の数字になる)。しかし、F3.5-F5.6というのは、結構暗くてボケが作れないらしい。ボケを作るには、明るい=F値が小さいレンズを選ばないといけない。一回帰って価格コムでネットで作例を見たが、確かにVARIOの作例は他と比べて霞がかかったような印象があり、正直「こんな写真が撮りたい!」と心がときめくものはあまり見かけなかった(プロが撮ったものはさすがに良いのだが、アマチュアの作品の方が参考になるだろう)。ときめきは大事なのである。

M.ZUIKO DIGITAL ED 12-40mm/F2.8

EM-5 Mark IIのキットレンズ。同じく防塵防滴の標準ズームレンズだ。EM-5のレンズなので、手ぶれ補正は実装されていないことに注意。望遠側が80mm相当までと、上の12-60(120mm相当まで)より少し少ないが、覗かせてもらうとほとんど分からない程度の差だった。サイズが12-60より一回り大きい点を敬遠したが、しかしF2.8通し(望遠側まで全部F2.8)というF値の小ささからくる画面の明るさ、価格コムで作例を見ても明らかに優れていた。これはときめく。

家に帰って、似たスペックのパナライカ12-60/2.8-4.0も検討したが、F2.8通しの方が良い、と判断してM.ZUIKO DIGITAL ED12-40/F2.8に決定した。

sims-lab.com

追記: id:DocSeri さんの記事がとても参考になったのだった。 docseri.hatenablog.jp

単焦点レンズ

標準ズームレンズがあれば広角から望遠までカバーできるが、やや大きいのがネックだ。単焦点レンズはズームができない代わりに小さく、日常的にカメラを持ち出してスナップを撮るには適している。また、F値が小さく明るく撮れる(F1.4〜1.8とかある)特徴がある。一つくらい持っていてもバチは当たらなさそうである。

単焦点レンズはズームができないので、まず自分が一番撮りたい画角を理解しておく必要がある。下記サイトが参考になった。

logcamera.com

検討の結果、自分がカッコいいなー、こういうスナップ撮りたいなー、と思う写真は50mmっぽい雰囲気があるように思った。作品として締まっている感じがする。あと35mmとかだとあんまりボケを作って楽しむ感じじゃなさそうだし。俺はボケが作りたいんだ!スマホじゃできないから!

と言うわけで比較対象としたのが以下の3製品である。評価メモは全て価格コムの作例のみで判断している。

  • M.ZUIKO DIGITAL 25mm F1.8
    • 3万円くらい。個人的に好きなボケ味
  • LUMIX G 25mm/F1.7 ASPH
  • LEICA DG SUMMILUX 25mm/F1.4 ASPH.
    • 4万円くらい。いわゆるパナライカ。F1.4ですんごいボケる。とろけるようなボケ味…ほしい…

と、これもここまで絞り込んでからフジヤカメラで触らせてもらった。ZUIKOはかなり小さく、同じオリンパスのE-M5 Mark IIとデザインの親和性が高かった。パナライカも触らせてもらった。中古価格差は小さかったのだが、やや大きめであることと、カメラ内部の機構のせいでAFが駆動するたびにカチャカチャと耳障りな音がするため諦め、今回は M.ZUIKO DIGITAL 25mm F1.8 に決定した。

パナライカの方がやや明るく撮れるしボケの綺麗さも上ですが、まあ相当厳密に比較しないと違いは分からないですね、とのことであった。

お買い上げ

はいというわけで、購入したのは E-M5 Mark II + M.ZUIKO DIGITAL ED 12-40mm/F2.8 レンズキット(新品)と、M.ZUIKO DIGITAL 25mm F1.8 単焦点レンズ(中古)の組み合わせでした。

本体キットは中古で買ってもよかったのだけど、価格差が10%程度だったのと、バッテリーは消耗品なので不安があったこと、フラッシュの欠品もあったため新品に。中古でもお店の半年保証はつきます。単焦点は中古でコストを抑えました。

周辺機器含めたコストを書いておきますね。液晶保護フィルムは店員さんに熟練の技で貼ってもらいました。

品目 価格
本体キット ¥101,400
単焦点レンズ ¥24,840
レンズプロテクター(ズームレンズ用) ¥2,680
プロテクター(単焦点レンズ用) ¥1,680
SDカード(64GB) ¥1,680
予備バッテリー ¥5,630
液晶保護フィルム ¥840
合計 ¥138,750

今回Twitterレンズ沼の人たちに色々教えてもらったり、店員さんにみっちり教えてもらったり、いい沼や…となりました。ありがとうございます。楽しみます。

ほめられた。やったぜ。

gorilla/csrf で安全なWebフォームを作る

こんにちは。GoでWeb開発していますか?私はしていません。Goに限らず、既成のWebアプリケーションフレームワークを使わずに自前でWebフォームを作る場合、なにも考えずに書くと CSRF (Cross Site Request Forgery) 脆弱性を作りこみ、不正なユーザー操作を実行されてしまう可能性があります。

ダメな例

例えば以下のGoコードで作成されるフォームにはCSRF脆弱性があります。SubmitSignupForm ハンドラは、受け取ったリクエストが自分のサイト上のフォームからサブミットされたものかチェックしていないので、攻撃者が他のサイト上のフォームを使い、第三者のユーザーのブラウザで任意の操作を実行させることができてしまいます。

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")
    http.ListenAndServe(":8080", r)
}

func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, `
<html>
  <body>
      <form method="POST" action="/signup/post" accept-charset="UTF-8">
          <input type="text" name="name">
          <input type="text" name="email">
          <input type="submit" value="Sign up!">
      </form>
  </body>
</html>`)
}

func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
    _ = r.ParseForm()
    fmt.Fprintf(w, "%v\n", r.PostForm)
    // TODO: ユーザー登録
}

CSRFについては「はまちちゃん事件」が有名です。mixiにログイン済みのユーザーにCSRF脆弱性のあったmixiの発言投稿用URLリンクを踏ませることで、ユーザーの意図しない発言を投稿させたというものです。CSRF対策にはいくつかの手法がありますが、権威あるセキュリティ団体であるOWASPが各手法の解説とメリット・デメリットを網羅しています。詳しく知りたい方はそちらを参照してください。

github.com

gorilla/csrf

信頼できるWebアプリケーションフレームワークではCSRFへの対策実装され、デフォルトで有効になっていますが、Goの net/http はWAFではないので、別途WAFを導入しないのであれば、自分でセキュリティ対策を導入する必要があります。そこで、今回は gorilla/csrf を使ってCSRF対策を実装します。

github.com

gorilla/csrf は net/http だけでなく、Echo や Gin といった人気のWAFと協調して動作することを目的としたCSRF対策パッケージです。

Cookieの二重送信(gorillaの場合)

gorilla/csrf が採用する対策手法は、OWASP の分類で言えば "Double Submit Cookie" (Cookieの二重送信) に分類されます。 Double Submit Cookieは、要点だけ言えばセッション開始時に暗号的に強い乱数でトークンを生成しておき、それをブラウザのCookieとフォームのhidden input要素の両方に送信するというものです。もちろん、サブミット時にはサーバーでCookieとフォームのトークンの一致を検証し、一致しなければエラーとします。この手法は、トークン情報をステートとして保持する必要がないため、実装・運用コストが低いのが特徴です。

一方、Cookieの二重送信には問題点も指摘されています。

要約すると、一定条件のもとではCookieを不正に書き換えることは可能であり、その場合に Double Submit Cookie の安全性は成立しないというものです。これについて、gorilla/csrfCookie に署名つき暗号化を施すことで回避しています。OWASPチートシートにも、Cookieの暗号化はうまく動作すると書かれています。

Including the token in an encrypted cookie - often within the authentication cookie - and then at the server side matching it (after decrypting authentication cookie) with the token in hidden form field or parameter/header for ajax calls mitigates both the issues mentioned above. This works because a sub domain has no way to over-write an properly crafted encrypted cookie without the necessary information such as encryption key.

また、gorilla/csrf では、リクエスト時に Unique-per-Request な one time padを生成し、CookieのtokenをXORでマスクしたものをフォームに送信する工夫を加えることで、BREACH攻撃を緩和しています。

プレーンな Double Submit Cookie の弱点を回避して、概ね安全なように思います。強いていえばユーザー自身が外部のWebページにフォームを送信してしまえば偽のフォームを作成可能ですが、そのケースはほぼ意図的な漏洩ですし、そこまで守るかどうかはポリシー次第(それも防ぎたかったらパスワード入力を求めるとか)じゃないでしょうか。

gorilla/csrfCSRF対策の流れを紹介します。

初回訪問時

最初にユーザーがWebサイトに訪れたとき、サーバーは暗号的に強い乱数生成器から basetoken を乱数生成します。そして basetoken を auth_key で署名つき暗号化したものを、レスポンスの _gorilla_csrf Cookie に格納します。この暗号は署名つき暗号なので、改竄に対する耐性があります。

f:id:ono_matope:20190605011412p:plain
初回訪問時

フォーム表示時

次に、サーバーが _gorilla_csrf Cookie を持ったクライアントにフォームを表示する際、サーバーはまず OTP (one-time-pad) を乱数生成します。この OTPと、basetokenをOTPでマスクした結果を結合し、CSRFトークンを生成します( csrfToken = OTP + XOR(basetoken, OTP) )。このCSRFトークンは、フォームのhidden inputとしてユーザーにレスポンスされます。

f:id:ono_matope:20190605092057p:plain
フォーム表示時

サブミット時

ユーザーがフォームをサブミットしてきたとき、サーバーはフォームのCSRFトークンからOTPを取り出し、Cookieのbasetokenを使って先ほどと同じ操作( OTP + XOR(basetoken, OTP)) をおこない、その結果がフォームのCSRFトークンと一致するか確かめることで、リクエストが正しく要求されたものか判別します。

f:id:ono_matope:20190605102640p:plain
サブミット時

How to use gorilla/csrf

さて、原理はわかったので使ってみましょう。

package main

import (
    "fmt"
    "html/template"
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")

    // Protect ミドルウェアは、非冪等なHTTPメソッドの場合にサブミットされたフォーム
    // をチェックし、CSRFトークンが一致しているか検証する。
    //
    // auth-key はコードに書き込まず、/dev/urandom したものを外部から与えること。
    // キーが変わるとそれまでに発行したトークンの検証に失敗する。
    //
    // HTTP な開発環境では opt に csrf.Secure(false) 指定が必要。本番では外すこと。
    h := csrf.Protect([]byte("32-byte-long-auth-key"), csrf.Secure(false))(r)
    http.ListenAndServe(":8080", h)
}

func ShowSignupForm(w http.ResponseWriter, r *http.Request) {

    // {{.CSRFField}} にCSRFトークンの隠しinputを埋め込む。
    t, _ := template.New("form").Parse(`
<html>
  <body>
      <form method="POST" action="/signup/post" accept-charset="UTF-8">
          <input type="text" name="name">
          <input type="text" name="email">
          {{.CSRFField}}
          <input type="submit" value="Sign up!">
      </form>
  </body>
</html> `)
    t.Execute(w, map[string]interface{}{
        "CSRFField": csrf.TemplateField(r),
    })
}

func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
    _ = r.ParseForm()
    fmt.Fprintf(w, "%v\n", r.PostForm)
    // TODO: ユーザー登録
}

csrf.ProtectCSRF保護機能を提供するHTTPミドルウェアを作成します。このミドルウェアは、POSTなどの非安全リクエストを受けた時にリクエストボディのフォームを自動的にチェックし、CSRFトークンが適格であれば次のハンドラを実行し、適格でなければエラー画面を表示して終了します。そのため、アプリケーション側のHTTPハンドラは(サブミットについては)CSRF検証済みであることを前提に開発することができます。OWASPチートシートにも、セキュリティ対策で怖いのはうっかりミスなので自動的な対策をせよと書かれているので、この仕様は歓迎できます。

一方、csrf.TemplateField(r *http.Request) template.HTML は、リクエストのCSRFトークンのhidden inputタグをリターンします。これをtemplateで置換してHTMLフォームを表示すると、gorilla.csrf.Token という名前の 隠し inputタグが埋め込まれます。

f:id:ono_matope:20190605144134p:plain
出力されるHTMLフォーム

同時に、Cookieにも _gorilla_csrf が記録されているのがわかります(内容は暗号化されているので確認も改竄もできません)。

f:id:ono_matope:20190605142544p:plain
Cookie

そこで、わざとフォームの値を書き換えてからSubmitすると、ちゃんとエラーになります。

f:id:ono_matope:20190605141731p:plain
シンプルなエラー画面

ところで、csrf.Protectはトークン不一致に簡単なエラー画面を出力するのですが、オプションでカスタムのエラーハンドラを指定させることができます。以下のように、何らかのセキュリティ対応を取れるようなログを落とすと良いと思います。

   h := csrf.Protect([]byte("32-byte-long-auth-key"),
        csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Println("CSRF攻撃の疑いのあるリクエストが発行されました")
            fmt.Fprintf(w, "ニャーン (%s)", csrf.FailureReason(r).Error())
        })),
        csrf.Secure(false),
    )(r)

f:id:ono_matope:20190605142904p:plain
にゃーん

以上です。

外付け(として使う)SSDって今何買えばいいのか調べたメモ

昨日の記事で言ったようにうっかりSSD+ケースを買ってしまったのだが、最近はUSB3.1 Gen2もThunderbolt3も普及してきてデータ転送帯域がどんどん広がっているし、m.2?NVMe?とかもなんかちっちゃくてかっこいいし、SSDも価格爆下がり中って聞くし、「最近の外付けSSDはどうなの?」みたいなところをちょっと調べてみた。買ってから調べるという。容量は1TBを前提に調べた。

外付けSSDについての調査だが、単品のSSDにケースを調達して外付けSSDに自作する、ということを選択肢に入れて調べたのでほとんど内蔵SSDについてです。あとNANDとかTLCとかには触れません。

まず、SSDの製品を分類するには、サイズ規格インターフェイスに注目する必要がある。

サイズ規格は以下のふたつがある。

覚えておく必要があるインターフェイスも二つだけだ。

  • SATA3.0
  • PCI-Express (PCIe 3.0)
    • 1レーンで転送速度8Gbps (1GB/s). 最大4レーン32Gbps (4GB/s)での転送が可能
    • コントローラのプロトコルはNVMe。NVMeとはPCIe上でブロックデバイスを効率よく動作させるために導入されたプロトコルである。

つまり、SSD市場には、以下の3カテゴリが存在していることになる。

外付け化のためのケーブルインターフェイス

PCIeはSATAの6倍以上でるのかーすごいなという感じだが、私は私のMacに外付けして使いたいので、つなげるやつをさがす。

2.5インチ + SATA3.0用のUSB 3.1 Gen2変換ケース

Amazonで大量に見つかる。どれも2,000円から3,000円程度と、安価。

Amazon.co.jp: USB3.1 gen2 ssd 2.5 sata ケース: パソコン・周辺機器ストア

m.2 + SATA3.0用のUSB 3.1 Gen2 変換ケース

m.2の変換ケースは、「Bキー、Mキー」などサポートするカードに相性があるので注意。

m.2 + PCIe3.0x4変換ケース

Thunderbolt3-PCIe3.0変換ケース

変わり種で、Thunderbolt3-PCIe3.0変換ケースというのもあった。ただ、書き込み性能の表記がないがレビューによると900MB/sしかでないらしい。

外付けSSD製品

USB接続の外付けSSD製品は、基本的にSATA3.0構成のようで、USB 3.1のGen1を搭載した安価なものとGen2を搭載した高価なものに二極化している。 Gen1の製品はリード性能300MB/s〜400MB/sでGB単価が14円/GBから、Gen2の製品はリード性能が560MB/sでGB単価が23円から、となっているようだ。

kakaku.com

フォームファクタに関しては、実はいまは外付けSSDは2.5インチ製品は少なくなっており、ほとんどがコンパクトなm.2仕様のようだ。2.5インチ: 価格.com - インターフェイス:USBのSSD 人気売れ筋ランキング (規格サイズ:2.5インチ)

考察・あなたが何を買うべきか

関連するパフォーマンス指標を書き起こしてみた。まず、PCにUSBで接続したい場合、データ転送速度はUSBの転送速度である500MB/s(USB 3.1 Gen1)や1GB/s(USB 3.1 Gen2)で律速する。この場合、ドライブにUSBの転送速度を超えたあまり高性能なものを買っても活かせないことに注意。

f:id:ono_matope:20190503055433p:plain

その上で、獲得したい転送速度に応じて最適な購入ルートを考えたみた。

転送速度を気にしない人向け (Gen1コース)

外付けSSDのGen1のものなら1,3000円/1TBくらいで買えるので、転送速度が300MB/s〜400MB/sでいいよという人はわざわざ内蔵ドライブとケースで自作せず、普通に外付けを買うといいと思います。

転送速度は560MB/sくらいはほしいという人(SATA+Gen2コース)

単価の安いSATA SSDに、安いUSB 3.1 Gen2-SATA変換ケースをつけると手頃に560MB/s帯のポータブルSSDになる。Gen2の製品は外付けドライブだと23円/GB程度するが、以下の構成は15GB/円で作れて嬉しい。

代表構成: 12,339 + 2,999 = 15,338円 / 1TB。WDのSSDTranscendのケースとのセットバージョンもある。

パッケージ品で良さそうなのはこれとか。でも23,763円/1TB。丈夫にできてそうってメリットはある。

転送速度は1000MB/sほしいよという人向け(PCIe+Gen2コース)

わかる。MacBook Pro 2018の内蔵SSDの転送速度が3,200MB/sに対して、300MB/sや500MB/sではなにか物足りない。もうちょっとハイスペックを楽しみたい。そんなあなたは m.2/PCIe3x4のSSDに、USB 3.1 Gen2変換ケースをの組み合わせで1000MB/sなポータブルストレージを作りましょう。既製品の外付けドライブではこの選択肢はそもそも存在しないのかな。ちょっと見つかんなかったです。

代表構成:¥15,980 + ¥5,680 = 21,660円 / 1TB

Crucial SSD M.2 1000GB P1シリーズ Type2280 PCIe3.0x4 NVMe 5年保証 CT1000P1SSD8JP

Crucial SSD M.2 1000GB P1シリーズ Type2280 PCIe3.0x4 NVMe 5年保証 CT1000P1SSD8JP

USB-C付属バージョン

akiba-pc.watch.impress.co.jp

金に糸目をつけない人向け (Thunderbolt 3プロ向け機材コース)

Thunderbolt3 SSD変換ケースとか、

そもそもThunderbolt3 SSDとか買うといいんじゃないでしょうか。

blog.livedoor.jp

聞いたことないメーカーだけど3万円で 1,600MB/s でる謎の Thunderbolt3 1TB SSD とかある

容量が十分あるにも関わらずAfter Effects がディスクキャッシュを保存する容量がないと言い張るときはTimeMachineローカルスナップショットを消せ

TL;DR: TimeMachineローカルスナップショットを消せ。

macOS Mojave / APFS 環境で、たまにAfter Effectsを起動すると「ディスクキャッシュフォルダーが存在するドライブには、環境設定で指定されている容量を全て保存できるだけの空き容量がありません。空き容量を増やすか、メディア&キャッシュ環境設定でフォルダーまたは最大ディスクキャッシュサイズを変更してください。」と警告ダイアログが出現する。

f:id:ono_matope:20190502224711p:plain

しかし、私はディスクキャッシュサイズをMacintosh HDに最大40GB程度に設定しており、Finderによるとそのドライブには十分な容量がある。

f:id:ono_matope:20190502225117p:plain

f:id:ono_matope:20190502225040p:plain

何の気なしに df してみたところ、'/' のファイルシステムに空きが18GBしかないと出てきた。Finderの空き容量表示と全く違うではないか。

f:id:ono_matope:20190502225318p:plain

ところで、High Sierra 以降のAPFSが有効なシステムでは、TimeMachineが自動的にMacintosh HDにローカルスナップショットを作成する。

ローカルスナップショットが消費しているストレージ容量について心配する必要はありません。ファイルのダウンロード、ファイルのコピー、新しいソフトウェアのインストールなどのタスクに必要な容量が使われてしまうことはないからです。

スナップショットが消費している容量は、Mac では空き容量に算入されます。その上さらに、Time Machine は空き容量の多いディスクにしかローカルスナップショットを保存しないようになっているほか、スナップショットが古くなったり、ほかのことに容量が必要になったりした場合は、自動的にスナップショットを削除してくれます。

Time Machine のローカルスナップショットについて - Apple サポート

作成されるローカルスナップショットは、OSのディスクキャッシュのように「ファイルシステムの向こう側で」動的に管理されるように設計されているようだが、怪しい。ためしにTimeMachineローカルスナップショットを削除した。

blog.h-wd.info

f:id:ono_matope:20190502230318p:plain

f:id:ono_matope:20190502230342p:plain

すると、df のAvailが129GBまで回復し、AEのディスク容量警告も出なくなった。めでたし。

f:id:ono_matope:20190502230501p:plain

容量が本当に足りないのかと思ってSSD買っちゃったんだけど、返品しようかな…。ちょっと考える。