【问题标题】:Upload byte array from axios to Node server从 axios 上传字节数组到 Node 服务器
【发布时间】:2022-02-17 15:42:53
【问题描述】:

背景

用于 Microsoft Office 插件的 Javascript 库允许您通过 getFileAsync() api 获取 DOCX 文件的原始内容,该 API 一次返回最多 4MB 的切片。您一直使用滑动窗口方法调用该函数,直到您完成整个内容。我需要将这些切片上传到服务器并重新加入它们以重新创建原始 DOCX 文件。

我的尝试

我在客户端使用 axios,在我的节点服务器上使用基于 busboy 的 express-chunked-file-upload 中间件。当我递归调用getFileAsync 时,我得到一个原始字节数组,然后我将其转换为Blob 并在post 将其添加到节点服务器之前附加到FormData。整个事情都有效,我在服务器上得到了切片。但是,写入服务器磁盘的块远大于我上传的 blob,通常为 3 倍左右,因此显然没有收到我发送的内容。

我怀疑这可能与流编码有关,但节点中间件没有公开任何设置编码的选项。

这是代码的当前状态:

客户端

public sendActiveDocument(uploadAs: string, sliceSize: number): Promise<boolean> {
  return new Promise<boolean>((resolve) => {
    Office.context.document.getFileAsync(Office.FileType.Compressed,
      { sliceSize: sliceSize },

      async (result) => {
        if (result.status == Office.AsyncResultStatus.Succeeded) {

          // Get the File object from the result.
          const myFile = result.value;
          const state = {
            file: myFile,
            filename: uploadAs,
            counter: 0,
            sliceCount: myFile.sliceCount,
            chunkSize: sliceSize
          } as getFileState;

          console.log("Getting file of " + myFile.size + " bytes");
          const hash = makeId(12)
          this.getSlice(state, hash).then(resolve(true))
        } else {
          resolve(false)
        }
      })
  })
}

private async getSlice(state: getFileState, fileHash: string): Promise<boolean> {
  const result = await this.getSliceAsyncPromise(state.file, state.counter)

  if (result.status == Office.AsyncResultStatus.Succeeded) {

    const data = result.value.data;

    if (data) { 
      const formData = new FormData();
      formData.append("file", new Blob([data]), state.filename);

      const boundary = makeId(12);

      const start = state.counter * state.chunkSize
      const end = (state.counter + 1) * state.chunkSize
      const total = state.file.size

      return await Axios.post('/upload', formData, {
        headers: {
          "Content-Type": `multipart/form-data; boundary=${boundary}`,
          "file-chunk-id": fileHash,
          "file-chunk-size": state.chunkSize,
          "Content-Range": 'bytes ' + start + '-' + end + '/' + total,
        },
      }).then(async res => {
        if (res.status === 200) {
          state.counter++;

          if (state.counter < state.sliceCount) {
            return await this.getSlice(state, fileHash);
          }
          else {
            this.closeFile(state);
            return true
          }
        }
        else {
          return false
        }
      }).catch(err => {
        console.log(err)
        this.closeFile(state)
        return false
      })
    } else {
      return false
    }
  }
  else {
    console.log(result.status);
    return false
  }
}

private getSliceAsyncPromise(file: Office.File, sliceNumber: number): Promise<Office.AsyncResult<Office.Slice>> {
  return new Promise(function (resolve) {
    file.getSliceAsync(sliceNumber, result => resolve(result))
  })
}

服务器端

此代码完全来自 npm 包(上面的链接),所以我不应该在这里更改任何内容,但仍然供参考:

makeMiddleware = () => {
    return (req, res, next) => {
        const busboy = new Busboy({ headers: req.headers });
        busboy.on('file', (fieldName, file, filename, _0, _1) => {

            if (this.fileField !== fieldName) {  // Current field is not handled.
                return next();
            }

            const chunkSize = req.headers[this.chunkSizeHeader] || 500000;  // Default: 500Kb.
            const chunkId = req.headers[this.chunkIdHeader] || 'unique-file-id';  // If not specified, will reuse same chunk id.
            // NOTE: Using the same chunk id for multiple file uploads in parallel will corrupt the result.

            const contentRangeHeader = req.headers['content-range'];
            let contentRange;

            const errorMessage = util.format(
                'Invalid Content-Range header: %s', contentRangeHeader
            );

            try {
                contentRange = parse(contentRangeHeader);
            } catch (err) {
                return next(new Error(errorMessage));
            }

            if (!contentRange) {
                return next(new Error(errorMessage));
            }

            const part = contentRange.start / chunkSize;
            const partFilename = util.format('%i.part', part);

            const tmpDir = util.format('/tmp/%s', chunkId);
            this._makeSureDirExists(tmpDir);

            const partPath = path.join(tmpDir, partFilename);

            const writableStream = fs.createWriteStream(partPath);
            file.pipe(writableStream);

            file.on('end', () => {
                req.filePart = part;
                if (this._isLastPart(contentRange)) {
                    req.isLastPart = true;
                    this._buildOriginalFile(chunkId, chunkSize, contentRange, filename).then(() => {
                        next();
                    }).catch(_ => {
                        const errorMessage = 'Failed merging parts.';
                        next(new Error(errorMessage));
                    });
                } else {
                    req.isLastPart = false;
                    next();
                }
            });
        });

        req.pipe(busboy);
    };
}

更新

看来我至少找到了问题所在。 busboy 似乎正在将我的字节数组作为文本写入输出文件。当我上传字节数组[80,75,3,4,20,0,6,0,8,0,0,0,33,0,44,25] 时,我得到80,75,3,4,20,0,6,0,8,0,0,0,33,0,44,25(作为文本)。现在需要弄清楚如何强制它把它写成二进制流。

【问题讨论】:

    标签: javascript node.js axios busboy


    【解决方案1】:

    想通了。以防万一它对任何人有帮助,busboyoffice.jsaxios 没有问题。我只需要将传入的数据块转换为Uint8Array,然后再从中创建一个 blob。所以而不是:

    formData.append("file", new Blob([data]), state.filename);
    

    像这样:

    const blob = new Blob([ new Uint8Array(data) ])
    formData.append("file", blob, state.filename);
    

    它就像一个魅力。

    【讨论】:

      猜你喜欢
      • 2011-06-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-30
      • 2023-03-25
      • 2019-12-14
      • 1970-01-01
      相关资源
      最近更新 更多