# 大きいファイルをスライス分割してアップロード

大きいファイルをアップロードする際に、通常のやり方だとサーバー処理のタイムアウトやサイズオーバーでアップロードできない場合が出てきます。短時間で大きいサイズのファイルをサーバーにアップロードするのに、少し工夫が必要。

基本的な考え

クライアント

  • javascript slice でファイル分割
    • スライスサイズ指定
  • 並行処理でサーバーへアップロード
    • 同時プロセス数制限
    • スライスアップロード

バックエンド

  • スライス結合

# javascript slice でファイル分割

File オブジェクトは特別な種類の Blob オブジェクト、Blob データを slice でファイル分割します。

let size = 1024 * 1024 * 2; // スライスサイズ設定 2MB
let fileChunks = [];
let index = 0; // index値
for (let cur = 0; cur < file.size; cur += size) {
  fileChunks.push({
    hash: index++,
    chunk: file.slice(cur, cur + size),
  });
}

slice 構文 arr.slice([start[, end]])

# 並行処理でサーバーへアップロード

非同期で並行通信しながら、await Promise.race()で最大数まで制御します。
スライスを順番で送信、送信失敗がある際にあとから再送信。

// 非同期並行処理
const uploadFileChunks = async function (list) {
  if (list.length === 0) {
    await axios({
      method: "get",
      url: "/merge",
      params: {
        filename: file.name,
      },
    });
    return;
  }
  let pool = []; // 並行処理プール
  let max = 5; // 最大並行処理数
  let finish = 0; // 送信完了数
  let failList = []; // 失敗リスト

  for (let i = 0; i < list.length; i++) {
    let item = list[i];
    let formData = new FormData();
    formData.append("filename", file.name);
    formData.append("hash", item.hash);
    formData.append("chunk", item.chunk);

    // アップロードタスク
    let task = axios({
      method: "post",
      url: "/upload",
      data: formData,
    });

    task
      .then((data) => {
        let index = pool.findIndex((t) => t === task);
        pool.splice(index);
      })
      .catch(() => {
        failList.push(item);
      })
      .finally(() => {
        finish++;
        // すべてアップロード完了後処理
        if (finish === list.length) {
          uploadFileChunks(failList);
        }
      });
    pool.push(task);
    if (pool.length === max) {
      // 並行処理最大数超えないように制御
      await Promise.race(pool);
    }
  }
};

# クライアント全ソース

<section>
  <input type="file" id="fileUpload" />
  <button id="uploadBtn">upload</button>
</section>
// axios使う
axios.defaults.baseURL = "http://localhost:3000";

let file = null;

document.getElementById("fileUpload").onchange = function ({
  target: { files },
}) {
  file = files[0];
};
// upload
document.getElementById("uploadBtn").onclick = function () {
  if (!file) return;
  // スライスプール作成
  let size = 1024 * 1024 * 2; // スライスサイズ設定 2MB
  let fileChunks = [];
  let index = 0; // index値
  for (let cur = 0; cur < file.size; cur += size) {
    fileChunks.push({
      hash: index++,
      chunk: file.slice(cur, cur + size),
    });
  }
  // 非同期並行処理
  const uploadFileChunks = async function (list) {
    if (list.length === 0) {
      await axios({
        method: "get",
        url: "/merge",
        params: {
          filename: file.name,
        },
      });
      return;
    }
    let pool = []; // 並行処理プール
    let max = 3; // 最大並行処理数
    let finish = 0; // 送信完了数
    let failList = []; // 失敗リスト

    for (let i = 0; i < list.length; i++) {
      let item = list[i];
      let formData = new FormData();
      formData.append("filename", file.name);
      formData.append("hash", item.hash);
      formData.append("chunk", item.chunk);

      // アップロードタスク
      let task = axios({
        method: "post",
        url: "/upload",
        data: formData,
      });

      task
        .then((data) => {
          let index = pool.findIndex((t) => t === task);
          pool.splice(index);
        })
        .catch(() => {
          failList.push(item);
        })
        .finally(() => {
          finish++;
          // すべてアップロード完了後処理
          if (finish === list.length) {
            uploadFileChunks(failList);
          }
        });
      pool.push(task);
      if (pool.length === max) {
        // 並行処理最大数超えないように制御
        await Promise.race(pool);
      }
    }
  };
  uploadFileChunks(fileChunks);
};

# バックエンド スライス結合

// upload処理
server.post("/upload", (req, res) => {
  const form = new multiparty.Form();
  form.parse(req, function (err, fields, files) {
    let filename = fields.filename[0];
    let hash = fields.hash[0];
    let chunk = files.chunk[0];
    let dir = `${STATIC_TEMPORARY}/${filename}`;
    // console.log(filename, hash, chunk)
    try {
      if (!fs.existsSync(dir)) fs.mkdirSync(dir);
      const buffer = fs.readFileSync(chunk.path);
      const ws = fs.createWriteStream(`${dir}/${hash}`);
      ws.write(buffer);
      ws.close();
      res.send(`${filename}-${hash} done`);
    } catch (error) {
      console.error(error);
      res.status(500).send(`${filename}-${hash} failed`);
    }
  });
});

// 結合処理
server.get("/merge", async (req, res) => {
  const { filename } = req.query;
  try {
    let len = 0;
    const bufferList = fs
      .readdirSync(`${STATIC_TEMPORARY}/${filename}`)
      .map((hash, index) => {
        const buffer = fs.readFileSync(
          `${STATIC_TEMPORARY}/${filename}/${index}`
        );
        len += buffer.length;
        return buffer;
      });
    // 結合
    const buffer = Buffer.concat(bufferList, len);
    const ws = fs.createWriteStream(`${STATIC_FILES}/${filename}`);
    ws.write(buffer);
    ws.close();
    res.send(`merge success`);
  } catch (error) {
    console.error(error);
  }
});

server.listen(3000, (_) => {
  console.log("http://localhost:3000/");
});
2022-02-02
  • javascript

関連記事

javascript  指定場所にスムーズにスクロールする方法
Nuxt.js と Ant Design Vue 2 テーマカスタマイズ
react 強制的にレンダリングする方法
JavaScript ライブラリ aos.js 使ってスクロール連動アニメーション
Jsconfig と Tsconfig
Cookie localStorage sessionStorage の違い
Javascript var let const 変数宣言の違い
Javascript 電話番号フォーマット
国際化 i18n と地域化 L10N
javascript 文字列と配列検索 indexOf findIndex find some includes 関数の違い
Javascript DataTables で excel 風 table 作る
javascript 配列 重複排除
vuejs back to top button component 作成
bootstrap vuejs 使って generate する際に babel が icons ファイル max 500KB エラー
Nuxtjs 動的なルーティング静的ページ自動生成
キーコード取得 & キーコード一覧
Sweet Alert swal に複数 content
moment.js 使って日本語曜日対応
javascript 開発で出会った TypeError
開発時によく使うゼロ埋めパディング作業まとめ
ReferenceError: location is not defined
vuejs で omisejapan の決済フォーム作成した時のメモ
ブラウザーで動く javascript のクッキー操作ライブラリ js-cookie
javascript reduce 連想配列の合計計算覚えよう
nuxtjs と codeigniter で jwt システム構築
nodejs 使う時のエラーたち
javascript で作る html から PDF 変換 pdfmake の日本語対応
javascript 楽しく遊ぼう!メッセージつぶやくウシ cowsay
Javascript vuejs の validation 正規表現でフォームチェック作ったときのメモ
正規表現一覧 よく使う検索・置換のパターン
javascript password generator ランダム文字列パスワード作成
javascript 日本語 shift-js 対応 CSV ダウンロード
開発におけるコーディングルール・規約