🧊

input[type=file]な要素にwebkitdirectory属性をつけてFormDataに載せると、filenameがnameではなくwebkitRelativePathになる

タイトルが長いけど、

  • input[type=file, webkitdirectory]な要素にFileを載せて
  • form要素やfetch({ body: new FormData() })でサーバーに送ると
  • サーバー側で取れるFilenameに入ってるのは
  • クライアント側でのFilenameではなくwebkitRelativePath

という話。

このご時世にwebkitなんてプレフィックスがついてるからお察しではあるけど、いわゆるSpecにもそれらしい記述は見つけられなかった。

File and Directory Entries API https://wicg.github.io/entries-api/

バグだ!って言いたいわけではなく、単に驚いたのでメモっておくだけ。

再現手順

Svelteのコード例。

<input
  type="file"
  multiple
  webkitdirectory
  on:input={(ev) => {
    const formData = new FormData();

    for (const file of ev.currentTarget.files) {
      formData.append("files", file);
      console.log("👀", file);
    }

    fetch("/api/files/upload", {
      method: "POST",
      body: formData,
    });
  }}
/>

ここでconsole.log(file)してるところでは、

  • name: ファイル名のみ(パス情報なし)
  • webkitRelativePath: 相対パスを含むファイル名

という風にちゃんと区別できてる。

が、それをサーバー側で取得してみると、

const data = await req.formData();

for (const file of data.get(files) ?? []) {
  console.log("👀", file);
}

namenameではなく、webkitRelativePathのパス情報つきになってる。

もっというと、webkitRelativePathundefinedになってた。(Cloudflare Workers、もといworkerdでは)

やってるのはブラウザ

そもそもPOSTされてるデータがそうなってる。

# webkitdirectoryなし
Content-Disposition: form-data; name="files"; filename="0.json"

# webkitdirectoryあり
Content-Disposition: form-data; name="files"; filename="myfiles/0.json"

フィールドは1つしかないし、パースの手間はあるけど、パス情報からファイル名は取れるし上位互換とも言えなくは・・・?

まぁサーバー側でwebkitRelativePathを生成したいモチベーションもないやろうし、納得といえば納得ではあるけど。

webkitdirectoryで拾ったファイルを送ってます!っていちいちAPIにシグナルするのもどうかと思うし、標準化って大変ね・・・。

2023/11/15: 追記

この記事を書いた時点では、SvelteKitはv1.27.0だった。

ところが、追記時点で最新のv1.27.6にしたところ、サーバー側で取得できる値がwebkitRelativePath相当ではなく、nameになってしまった。(そのせいで手元のアプリが何もしてないのに壊れた状態に)

どこに原因があるのかは追ってない、諦めた・・。