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

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

基本的な考え

クライアント

  • 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

関連記事

Nuxt.js と Ant Design Vue 2 テーマカスタマイズ
JavaScript ライブラリ aos.js 使ってスクロール連動アニメーション
Jsconfig と Tsconfig
Cookie localStorage sessionStorage の違い
国際化 i18n と地域化 L10N
javascript 文字列と配列検索 indexOf findIndex find some includes 関数の違い
Javascript DataTables で excel 風 table 作る
javascript 配列 重複排除
bootstrap vuejs 使って generate する際に babel が icons ファイル max 500KB エラー
キーコード取得 & キーコード一覧
開発時によく使うゼロ埋めパディング作業まとめ
javascript  指定場所にスムーズにスクロールする方法
react 強制的にレンダリングする方法
Javascript var let const 変数宣言の違い
Javascript 電話番号フォーマット
vuejs back to top button component 作成
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 ダウンロード
開発におけるコーディングルール・規約