将上传的文件从 JS 发送到 Python CGI 总是会产生 ampty 字典

Sending uploaded file from JS to Python CGI always results in ampty dictionary

提问人:Mr_BananaPants 提问时间:9/20/2023 更新时间:9/21/2023 访问量:28

问:

我有一个带有文件输入元素的 HTML 表单。提交表单时,会执行JS功能,检查上传的文件是否为PDF文件且小于10MB。所有这些都可以正常工作,并在控制台中记录正确的文件大小。

然后 JS 函数通过 HTTP POST 请求将文件发送到 Python 服务器。但是,发送的文件始终是空的对象/字典。当我在发送之前,所有文件数据都会被记录下来,但是当我在检查器中查看“网络”选项卡时,它只是一个空对象/字典:console.log(file);

enter image description here

但是,当我更改我的 HTML 表单时,它将数据直接发送到 Python CGI 脚本而无需先调用 JS 函数,它可以完美地工作。

这是我的 HTML 代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="./js/new.js"></script>

    <title>test</title>
  </head>
  <body>
    <script></script>
    <header>
      <h1>File upload test</h1>
    </header>
    <main>
      <fieldset>
        <form
          action="javascript:addFile()"
          id="DataForm"
        >
          <div>
            <label for="file_contract">contract:</label
            ><input
              type="file"
              accept="application/pdf"
              id="file_contract"
              name="file_contract"
            />
          </div>
          <button type="button" class="outlined" onclick="cancel()">
            Annuleer
          </button>

          <input type="submit" value="Bewaar" />
        </form>
      </fieldset>
    </main>
    <script>
      function cancel() {
        const host = window.location.host;
        const redirectUrl = `http://${host}`;
        window.location.replace(redirectUrl);
      }
    </script>
  </body>
</html>

JavaScript 代码:

async function addFile() {
  const formData = new FormData(document.forms["DataForm"]);
  // get uploaded files
  // Function to check and append a file to the formData
  function handleFileUpload(fileInputId) {
    console.log("uploading file...");
    const fileInput = document.getElementById(fileInputId);
    const file = fileInput.files[0];

    // Check if a file is selected

    // Ensure the file is a PDF
    if (file.type === "application/pdf") {
      console.log(`filesize = ${file.size}`);
      // 10MB in bytes
      formData.append(fileInputId, fileInput.files[0]);
    }

    return true;
  }

  // Handle each file upload separately
  if (!handleFileUpload("file_contract")) {
    console.log("form submission prevented");
    return false; // Prevent form submission
  }

  const data = {
    answers: Object.fromEntries(formData),
  };

  console.log(data["answers"]["file_contract"]);

  try {
    const response = await fetch("../../cgi-bin/addRecord.py", {
      method: "POST",
      headers: {
        "Content-type": "application/json; charset=UTF-8",
      },
      body: JSON.stringify(data),
    });

    const responseData = await response.json();

    if (responseData.hasOwnProperty("status")) {
      if (responseData["status"] === "failed") {
        // session is invalid / expired
        alert("Login sessie vervallen. Log opnieuw in.");
      } else if (responseData.status === "success") {
        const host = window.location.host;
        const redirectUrl = `http://${host}`;
        console.log("redirecting... (commented out)");
        // window.location.replace(redirectUrl);
      } else {
        alert(
          "Fout bij het opslaan. Probeer het nog eens of contacteer Sandra."
        );
      }
    }
  } catch (error) {
    console.error(error);
    alert("Fout bij het opslaan. Probeer het nog eens of contacteer Sandra.");
  }
}

Python CGI:

#!/usr/bin/env python3

import cgi
import hashlib
import json
import os
import sys
import logging

# Get the absolute path of the current script
dirname = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

if os.path.exists(f'{dirname + "/log.log"}'):
    # If it exists, open it in write mode to clear its contents
    with open(f'{dirname + "/log.log"}', 'w'):
        pass

logging.basicConfig(filename=f'{dirname + "/log.log"}', level=logging.DEBUG)


def generate_response(status, data=None):
    response = {"status": status}
    if data:
        response["data"] = data

    print("Content-Type: application/json")
    print()
    print(json.dumps(response))


def calculate_file_hash(file_data):
    # Create an MD5 hash object
    md5_hash = hashlib.md5()

    # Read the file data in chunks to efficiently handle large files
    for chunk in iter(lambda: file_data.read(4096), b''):
        md5_hash.update(chunk)

    # Return the hexadecimal representation of the MD5 hash
    return md5_hash.hexdigest()


def main():
    # read form data from HTTP POST request
    data = sys.stdin.read(int(os.environ.get('CONTENT_LENGTH', 0)))
    post_data = json.loads(data)

    answers = post_data["answers"]

    logging.debug(str(answers))

    if "file_contract" in answers:
        contract_file = answers['file_contract']
        contract_file_filename = contract_file.filename
        contract_file_hash = calculate_file_hash(contract_file.file)
        # save the file data to the werkmap
        # Save the PDF file to a folder
        contract_filename_path = os.path.join(dirname, "documenten", contract_file_hash)
        with open(contract_filename_path, 'wb') as contract_file_handle:
            contract_file_handle.write(contract_file.file.read())

    generate_response("success")


if __name__ == "__main__":
    main()

javascript python 文件 上传 CGI

评论

0赞 furas 9/20/2023
check 而不是 .将某些对象转换为 .甚至给空的,它需要.类似的问题也可能发生在您的对象上。它可能需要转换。但我还没有测试它。console.log( JSON.stringify(data) )console.log( data )JSONJSON.stringify(formData){}JSON.stringify( Objects.fromEntries(formData) )FileObjects.fromEntries()
0赞 furas 9/20/2023
我不确定,但要像数组一样发送文件,您可能需要读取此文件()并转换为(),因为无法发送某些值。JSONreader = FileReader()base64reader.readAsDataURL()JSON

答:

1赞 furas 9/21/2023 #1

问题是.它无法将对象转换为字符串。JSON.stringifyFileJSON

像往常一样发送它会更简单(并向此表单添加其他元素)。FormData

如果您真的必须发送它,那么您可能需要使用和功能将文件内容转换为 .以 JSON 形式发送数据(即图像、数据文件)是流行的方法。JSONFileReaderreadAsDataURL()Base64

const file = formData.get('file_contract');
  
const reader = new FileReader();
  
reader.onload = your_function_executed_after_converting;

reader.readAsDataURL(file);

最小的工作示例。我删除了测试此问题不需要的元素。

async function addFile() {
  const formData = new FormData(document.forms["DataForm"]);
  const file = formData.get('file_contract');
  
  const reader = new FileReader();
  
  reader.onload = () => {
      //const data = {'answers': reader.result}  // send only file contemt
      const data = {'answers': 
                     {
                       'data': reader.result,  // file content as `Base64`
                       'name': file.name, 
                       'size': file.size, 
                       'type': file.type,
                       'lastModified': file.lastModified,
                     }
                   }
      
      fetch("/cgi-bin/addRecord.py", {
        method: "POST",
        headers: {
           //'Accept': 'application/json, text/plain, */*',   // expect JSON or TEXT as response from server
           'Content-Type': 'application/json; charset=UTF-8'  // inform server that you send JSON
        },        
        body: JSON.stringify(data),  
      }).then((response) => {
          if(response.ok) {
            const responseData = response.json();
            //TODO: use responseData 
          }
      });
  }  
  
  reader.readAsDataURL(file);  // convert to `Base64` and execute `onload()`
}

因为它以 (带前缀) 的形式发送数据,所以稍后在 Python 中您必须删除前缀并解码回正常状态。base64application/pdf;base64,application/pdf;base64,base64bytes

import base64

file_as_bytes = base64.b64decode(file_as_base64)

我使用框架而不是 .flaskCGI

这是我用来测试它的完整工作代码。
所有代码(HTML、JavaScript、Python)都在一个文件中。

import os
from flask import Flask, request, Response
import base64

app = Flask(__name__)


@app.route('/')
def index():

    return '''<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="code.js"></script>
    <title>test</title>
  </head>
  <body>
  
    <form method="POST" enctype="multipart/form-data" action="javascript:addFile()" id="DataForm">
      <div>
        <label for="file_contract">contract:</label>
        <input type="file" accept="application/pdf" id="file_contract" name="file_contract" />
      </div>
      <button type="button" onclick="cancel()">Cancel</button>
      <input type="submit" value="Submit" />
    </form>

  </body>
</html>
'''

@app.route('/code.js')
def code():
    text = '''async function addFile() {
  const formData = new FormData(document.forms["DataForm"]);
  const file = formData.get('file_contract');
  
  const reader = new FileReader();
  
  reader.onload = () => {
      //const data = {'answers': reader.result}  // send only file contemt
      const data = {'answers': 
                     {
                       'data': reader.result,  // file content as `Base64`
                       'name': file.name, 
                       'size': file.size, 
                       'type': file.type,
                       'lastModified': file.lastModified,
                     }
                   }
      
      fetch("/addRecord", {
        method: "POST",
        headers: {
           //'Accept': 'application/json, text/plain, */*',   // expect JSON or TEXT as response from server
           'Content-Type': 'application/json; charset=UTF-8'  // inform server that you send JSON
        },        
        body: JSON.stringify(data),  
      }).then((response) => {
          if(response.ok) {
            const responseData = response.json();
            //TODO: use responseData 
          }
      });
  }  
  
  reader.readAsDataURL(file);  // convert to `Base64` and execute `onload()`
}
'''
        
    return Response(text, mimetype='application/javascript')


@app.route('/addRecord', methods=['POST'])
def add_record():
    print('args :', request.args)
    print('form :', request.form)
    if request.is_json:
        print('json :', request.json)
    print('files:', request.files)
    print('data :', request.data[:100])  # crop data to 100 chars - to display only short example
    
    # ----------------------------------------
    
    file_info = request.json['answers']
    data = file_info['data']   # file data with prefix `application/pdf;base64,`
    
    # find beginning of data (after prefix `application/pdf;base64,`)
    file_start = data.find(',') + 1  # +1 == len(',')
    # crop data to remove prefix    
    file_data = data[file_start:]

    file_as_bytes = base64.b64decode(file_data)
    print(file_as_bytes[:100])  # crop data to 100 chars - to display only short example 
    
    # write data to file on disk (using "bytes mode")
    with open(file_info['name'], 'wb') as f_out:
        f_out.write(file_as_bytes)
        
    #file_as_text  = file_as_bytes.decode("utf-8")
    #print(file_as_text[:100])  # crop data to 100 chars - to display only short example
    
    return {'status': 'OK'}  # it will send dictionary as JSON


if __name__ == '__main__':
    #app.debug = True 
    #app.run(threaded=False, processes=3)
    #app.run(use_reloader=False)
    app.run()

enter image description here

评论

0赞 Mr_BananaPants 9/21/2023
你是完全正确的!非常感谢您对我的帮助以及对示例的非常详细的解释!