尝试将 PDF 文件发布到 S3 预签名 URL 时出现 412(前提条件失败)

问题描述 投票:0回答:1

这是我尝试将文件从浏览器上传到 S3 存储桶的旅程的第二部分。查看第一部分

我正在努力做什么

将文件 (PDF) 从我的浏览器客户端(Vue/Nuxt 应用程序)上传到 S3 存储桶。

我正在使用什么

前端

Nuxt、Vue、Axios、Fetch。

后端

JS,无服务器框架,可轻松部署 AWS。

权限

权限很好,除非有一些我不知道的关于发布到 S3 存储桶的权限。 S3 存储桶设置为允许 CORS;所有的方法,所有的起源。 不,此处显示的 Lambda 没有不正确的权限;如果是这样,我将无法成功获取预签名 URL。

它应该如何工作

基于 thisthis,我认为我可以通过首先从下面所示的后端代码检索预签名的 URL(称为

s3.getSignedUrl('putObject'))将 PDF 放入(或发布)到 S3 存储桶
。我能够轻松地将此 URL 获取到前端,但将 PDF 放置(或发布)到返回的 URL 不起作用。

我在第一部分所做的事情被证明是无用的,而且我一直无法找到原因的答案,所以我决定尝试另一个名为

createPresignedPost
的 JS AWS SDK 函数。

我收到的不是 403 错误,而是 412 错误。阅读后,我似乎没有将正确的选项传递到 POST 请求中。看了这个之后,我如下所示设置了我的请求,但仍然收到错误。

代码

后端(fileReceiver.js):

const getUploadUrl = async () => {
  const fileId = uuidv4();

  let params = {
    Bucket: 'the-chumiest-bucket',
    ContentType: 'application/pdf',
    Conditions: [
      {'bucket': 'the-chumiest-bucket'}, // added to both places 
      ['starts-with', '$key', 'path/to/where/the/file/should/go/'],
      {'acl': 'public-read'},
      {'success_action_status': '200'},
      ['content-length-range', 1, 1024 * 1024 * 15],
      ['starts-with', '$Content-Type', 'application/pdf'],
    ]
  };

  return new Promise((resolve, reject) => {
    s3.createPresignedPost(params, function(err, data) {
      if (err) {
        console.error('Presigning post data encountered an error', err);
        reject(err);
      } else {
        //data.Fields.key = 'path/to/uploads/${filename}';
        console.log('The post data is', data);
        resolve({
          'statusCode': 200,
          'headers': { 
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': '*',
            'Access-Control-Allow-Credentials': true,
            'Content-Type': 'application/pdf',
          },
          'body': JSON.stringify({
            'data': data,
          }),
        });
      }
    });
  });

};

前端(fileUploader.js):

我想构造我的请求以匹配 thisthis

uploadFile: async function(e) {
      const response = await axios({
        method: 'get',
        url: API_GATEWAY_URL,
        data: {
          'fileName': this.fileNames[0],
          'contentType': this.file.type || 'application/pdf',
          'fileLength': this.file.length,
        },
        body: {
          'fileName': this.fileNames[0],
          'contentType': this.file.type || 'application/pdf',
          'fileLength': this.file.length,
        },
      });

      console.log('upload file response:', response);

      let binary = atob(this.file.split(',')[1]);
      let array = [];

      for (let i = 0; i < binary.length; i++) {
        array.push(binary.charCodeAt(i));
      }

      let fields = response.data.data.fields;
      let blobData = new Blob([new Uint8Array(array)], {type: 'application/pdf'});

      if (blobData.type != 'application/pdf') {
        console.error('Filetype is wrong! Upload a PFD or youre dead to me');
      }

      console.log(`Uploading "${this.fileNames[0]}" to bucket "${response.data.data.fields.bucket}"`);

      const postHeaders = {
        'Policy': fields.Policy,
        'X-Amz-Algorithm': fields['X-Amz-Algorithm'],
        'X-Amz-Credential': fields['X-Amz-Credential'],
        'X-Amz-Security-Token': fields['X-Amz-Security-Token'],
        'X-Amz-Signature': fields['X-Amz-Signature'],
        'Content-Type': blobData.type || 'application/pdf',
        'acl': 'public-read',
        'key': 'path/to/where/the/file/should/go/' + this.fileNames[0],
      }

      const result = await fetch(response.data.data.url, {
        method: 'post',
        body: blobData,
        headers: postHeaders,
      });

      this.uploadUrl = response.data.uploadURL.split('?')[0];
    },

无服务器配置(serverless.yml):

service: ocr-space-service

provider:
  name: aws
  region: ca-central-1
  stage: ${opt:stage, 'dev'}
  timeout: 20

plugins:
  - serverless-plugin-existing-s3
  - serverless-step-functions
  - serverless-pseudo-parameters
  - serverless-plugin-include-dependencies

layers:
  spaceOcrLayer:
    package:
      artifact: spaceOcrLayer.zip
    allowedAccounts:
      - "*"

functions:
  fileReceiver:
    handler: src/node/fileReceiver.handler
    role:
    events:
      - http:
          path: /doc-parser/get-url
          method: get
          cors: true
  startStateMachine:
    handler: src/start_state_machine.lambda_handler
    role: 
    runtime: python3.7
    layers:
      - {Ref: SpaceOcrLayerLambdaLayer}
    events:
      - existingS3:
          bucket: ingenio-documents
          events:
            - s3:ObjectCreated:*
          rules:
            - prefix: 
            - suffix: .pdf
  startOcrSpaceProcess:
    handler: src/start_ocr_space.lambda_handler
    role: 
    runtime: python3.7
    layers:
      - {Ref: SpaceOcrLayerLambdaLayer}
  parseOcrSpaceOutput:
    handler: src/parse_ocr_space_output.lambda_handler
    role: 
    runtime: python3.7
    layers:
      - {Ref: SpaceOcrLayerLambdaLayer}
  renamePdf:
    handler: src/rename_pdf.lambda_handler
    role: 
    runtime: python3.7
    layers:
      - {Ref: SpaceOcrLayerLambdaLayer}
  parseCorpSearchOutput:
    handler: src/node/pdfParser.handler
    role: 
    runtime: nodejs10.x
  saveFileToProcessed:
    handler: src/node/saveFileToProcessed.handler
    role: 
    runtime: nodejs10.x

stepFunctions:
  stateMachines:
    ocrSpaceStepFunc:
      name: ocrSpaceStepFunc
      definition:
        StartAt: StartOcrSpaceProcess
        States:
          StartOcrSpaceProcess:
            Type: Task
            Resource: "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:#{AWS::StackName}-startOcrSpaceProcess"
            Next: IsDocCorpSearchChoice
            Catch:
            - ErrorEquals: ["HandledError"]
              Next: HandledErrorFallback
          IsDocCorpSearchChoice:
            Type: Choice
            Choices:
              - Variable: $.docIsCorpSearch
                NumericEquals: 1
                Next: ParseCorpSearchOutput
              - Variable: $.docIsCorpSearch
                NumericEquals: 0
                Next: ParseOcrSpaceOutput
          ParseCorpSearchOutput:
            Type: Task
            Resource: "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:#{AWS::StackName}-parseCorpSearchOutput"
            Next: SaveFileToProcessed
            Catch:
              - ErrorEquals: ["SqsMessageError"]
                Next: CorpSearchSqsErrorFallback
              - ErrorEquals: ["DownloadFileError"]
                Next: CorpSearchDownloadFileErrorFallback
              - ErrorEquals: ["HandledError"]
                Next: HandledNodeErrorFallback
          SaveFileToProcessed:
            Type: Task
            Resource: "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:#{AWS::StackName}-saveFileToProcessed"
            End: true
          ParseOcrSpaceOutput:
            Type: Task
            Resource: "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:#{AWS::StackName}-parseOcrSpaceOutput"
            Next: RenamePdf
            Catch:
            - ErrorEquals: ["HandledError"]
              Next: HandledErrorFallback
          RenamePdf:
            Type: Task
            Resource: "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:#{AWS::StackName}-renamePdf"
            End: true
            Catch:
              - ErrorEquals: ["HandledError"]
                Next: HandledErrorFallback
              - ErrorEquals: ["AccessDeniedException"]
                Next: AccessDeniedFallback
          AccessDeniedFallback:
            Type: Fail
            Cause: "Access was denied for copying an S3 object"
          HandledErrorFallback:
            Type: Fail
            Cause: "HandledError occurred"
          CorpSearchSqsErrorFallback:
            Type: Fail
            Cause: "SQS Message send action resulted in error"
          CorpSearchDownloadFileErrorFallback:
            Type: Fail
            Cause: "Downloading file from S3 resulted in error"
          HandledNodeErrorFallback:
            Type: Fail
            Cause: "HandledError occurred"

帖子回复

POST https://s3.{regionName}.amazonaws.com/{bucketName} 412(前提条件失败)

PreconditionFailed
您指定的至少一个前提条件不满足Bucket POST 必须是封装类型 multipart/form-data934186E69EF6F90EAPU1d8pkKL3XxXjZ8T1oXkWuDRECAPZROklZbHBv+lmNRv/ivoLO/8BhoS8QYXA98850RhrGwhI=

编辑

看起来请求正在被 412 处理,因为它期待

multipart/form-data
,而不是我指定的
application/pdf

新代码

我编写了一个函数,将 PDF 转换为 Base64 并将其作为

FormData
对象的一部分上传。

convertToBase64(file) {
      if (file.length > 0) {
        let fileToLoad = file[0];
        let fileReader = new FileReader();
        let base64;
        fileReader.onload = (fileLoadedEvent) => {
          console.log('fileLoadedEvent', fileLoadedEvent);
          base64 = fileLoadedEvent.target.result;
          console.log('base64', base64);
        };
        fileReader.readAsDataURL(fileToLoad);
      } else {
        console.log('file length was 0');
      }
    },

...

let formData = new FormData();
formData.append('pdfFile', this.convertToBase64(this.file));

// upload code from above...

还是没有爱!不确定我想做的事情是否可能。

javascript amazon-web-services amazon-s3 http-post serverless
1个回答
0
投票

我也遇到过这个问题,但已经解决了。

具体方法如下。

首先,这是始终有效的设置:

我有一个 nextjs 应用程序,它是后端 API 和带有测试按钮/功能的索引页面的组合,用于手动测试 API。

  1. 客户端调用API获取签名URL,直接从浏览器上传pdf。
  2. API 调用
    createSignedPost
    并将签名发送给客户端。
  3. 客户端使用
    formData
    将签名和浏览器选择的文件结合起来,然后使用内置的
    fetch()
    将文件
    POST
    发送到 AWS。

我决定编写一个基于节点的 cli 来模拟浏览器并运行所有测试。

对于 cli,我使用 node-fetch-cookies 来跟踪 cookie,模拟登录并向 API 发送登录请求。

注意:

node-fetch-cookies
使用
node-fetch
进行异步 http(s) 调用。

使用

node-fetch-cookies
我尝试使用
formData
并传入文件的完整路径(失败),并使用
fs.createReadStream()
作为文件(也失败)。

什么有效:

我必须更换

node-fetch-cookies
fetch()
调用并使用内置节点
fetch()
调用。

这是演示其工作原理的伪代码:

const api = new API();
const file = fs.createReadStream("/path/to/file");

api
  .getSignedUrl()
  .then(async (signature) => {
    const { url, fields } = signature;
    const formData = new FormData();
    Object.entries({ ...fields, file }).forEach(([key, value]) => {
      formData.append(key, value);
    });
    // built-in node fetch()
    return fetch(url, {
      method: "POST",
      body: formData,
    });
  })
  .then((response) => {
    if (!response.ok) {
      throw new Error(`Upload failed ${response.statusText}`);
    }
    console.log("file uploaded");
  });

奖金:

如果您需要在浏览器中执行此操作,只需更换

file
即可使用用户选择的文件。

具体方法如下。

代替:

const file = fs.createReadStream("/path/to/file");

有一个像这样的表单元素:

<input type="file" name="myFile" onChange={(e) => documentGetLocalFile(e)} />

和这样的处理程序:

var file;
documentGetLocalFile(event) {
  if (event.target.files && event.target.files[0]) {
    file = event.target.files[0];
  }
}

然后确保您的

formData
使用此
file
代替您上面删除的
fs.createReadStream()
文件。

© www.soinside.com 2019 - 2024. All rights reserved.