python-multipart

repository·main·Indexed 19 days ago

https://github.com/kludex/python-multipart

An Apache2-licensed streaming multipart/form-data parser for Python, optimized for handling large file uploads efficiently without loading them entirely into memory. It provides high-level utilities like parse_form(), a flexible MultipartParser class for incremental data feeding, and specialized parsers for form data, octet-streams, and query strings. The library also includes Base64 and QuotedPrintable decoders, as well as FileConfig for managing upload directories and memory thresholds.

Tokens
8.4K
Snippets
26
Records
34
Agent score
68%

What's inside python-multipart

  1. Overview of python-multipart

    main
    python-multipart is an Apache2-licensed streaming multipart parser for Python. It is designed to handle multipart/form-data uploads using a streaming approach, which is highly efficient for processing large files without loading them entirely into memory.
  2. Run fuzz tests for python-multipart

    main

    The fuzz/ directory contains coverage-guided fuzz tests for python-multipart using the Atheris library. Fuzzing automatically provides invalid, unexpected, or random data to the program to discover bugs.

    To run a specific fuzz target (for example, fuzz_form.py), execute the script with Python. The fuzzer will continue generating random inputs indefinitely until it finds a bug or is manually terminated.

    If the fuzzer discovers a bug, it will generate testcases named crash-* or timeout-* in the directory from which you ran the command. You can rerun the fuzzer on a specific discovered testcase to reproduce the issue by passing the file path as a command-line argument.

    # Run the fuzz target
    python fuzz/fuzz_form.py
    
    # Rerun the fuzzer on a specific discovered testcase
    python fuzz/fuzz_form.py /path/to/testcase
  3. Use development scripts for python-multipart

    main

    The repository provides several scripts to manage the development lifecycle, including dependency installation, testing, linting, and compatibility checks. These scripts are located in the scripts/ directory.

    # Install dependencies
    ./scripts/setup
    
    # Run the test suite
    ./scripts/test
    
    # Run code formatting
    ./scripts/lint
    
    # Run lint in check mode and the type checker
    ./scripts/check
    
    # Check backward-compatibility for the 'multipart' name
    ./scripts/rename
  4. Use BaseParser for streaming data with callbacks

    main

    The BaseParser is the foundation for all streaming parsers in this library. It uses a callback-based system to notify the consumer of parsing events without copying data unnecessarily.

    Callbacks are registered via set_callback(name, func). There are two types of callbacks:

    1. Notification callbacks: Called with no arguments (e.g., on_start, on_end).
    2. Data callbacks: Called with (data, start, end), where data[start:end] represents the slice of interest. This avoids expensive data copying.

    Common callback names include start, data, end, field_start, field_name, field_data, field_end, part_begin, part_data, part_end, header_begin, header_field, header_value, header_end, headers_finished, and on_end.

    from python_multipart.multipart import BaseParser
    
    class MyParser(BaseParser):
        def __init__(self, callbacks):
            super().__init__()
            self.callbacks = callbacks
    
    # Example usage pattern for a consumer of a parser:
    def my_data_handler(data, start, end):
        print(f"Found data: {data[start:end]}")
    
    # The parser would then be used by calling .write(chunk) in a loop
  5. Streaming multipart parsing with MultipartParser

    main

    For more control or streaming requirements (e.g., computing hashes without loading files into memory), use the MultipartParser class. This allows you to feed data into the parser incrementally using the .write() method.

    Setup:

    1. Extract the boundary from the Content-Type header (use python_multipart.multipart.parse_options_header to assist).
    2. Define a callbacks dictionary containing:
      • on_part_begin: Called when a new part starts.
      • on_part_data: Called with (data, start, end) when a chunk of data is available.
      • on_part_end: Called when a part is fully processed.
    3. Initialize MultipartParser(boundary, callbacks).
    4. Feed data to the parser using parser.write(data) in a loop.
    import python_multipart
    from python_multipart.multipart import parse_options_header
    
    # 1. Extract boundary
    content_type, params = parse_options_header(environ['CONTENT_TYPE'])
    boundary = params.get(b'boundary')
    
    # 2. Define callbacks
    callbacks = {
        'on_part_begin': lambda: print("Part started"),
        'on_part_data': lambda data, start, end: print(f"Data chunk: {len(data)}"),
        'on_part_end': lambda: print("Part ended"),
    }
    
    # 3. Initialize parser
    parser = python_multipart.MultipartParser(boundary, callbacks)
    
    # 4. Stream data
    while True:
        chunk = input_stream.read(8192)
        if not chunk:
            break
        parser.write(chunk)
  6. Parse Content-Type options with parse_options_header

    main

    Use python_multipart.multipart.parse_options_header to parse a Content-Type header string into its base type and its parameters (like boundary). It returns a tuple of (content_type, params) where params is a dictionary of the options.

    from python_multipart.multipart import parse_options_header
    
    content_type, params = parse_options_header('multipart/form-data; boundary=something')
    # content_type == 'multipart/form-data'
    # params == {'boundary': 'something'}
  7. Parse multipart forms with parse_form()

    main

    Use python_multipart.parse_form() for a high-level, simple way to parse incoming multipart/form-data requests. This function requires a headers dictionary, an input stream (like wsgi.input), and two callback functions: one for fields and one for files.

    Callbacks:

    • on_field(field): Triggered for each form field. The field object has a field_name attribute.
    • on_file(file): Triggered for each uploaded file. The file object has a field_name attribute.
    import python_multipart
    
    def on_field(field):
        print(f"Field: {field.field_name}")
    
    def on_file(file):
        print(f"File: {file.field_name}")
    
    # headers must contain 'Content-Type' and 'Content-Length'
    headers = {'Content-Type': 'multipart/form-data; boundary=...', 'Content-Length': '123'}
    
    python_multipart.parse_form(headers, input_stream, on_field, on_file)
  8. Configure FormParser via DEFAULT_CONFIG

    main

    The FormParser uses a configuration dictionary to control behavior. You can pass a custom dictionary to the config argument during initialization to override these values.

    Default Configuration Keys

    KeyDefault ValueDescription
    MAX_BODY_SIZEfloat("inf")Maximum total size of the request body in bytes.
    MAX_HEADER_COUNTDEFAULT_MAX_HEADER_COUNTMaximum number of headers allowed per part.
    MAX_HEADER_SIZEDEFAULT_MAX_HEADER_SIZEMaximum size of a single header line.
    MAX_MEMORY_FILE_SIZE1048576 (1MB)Threshold for keeping file data in memory.
    UPLOAD_DIRNoneDirectory where uploaded files are stored.
    UPLOAD_DELETE_TMPTrueWhether to delete temporary files after parsing.
    UPLOAD_KEEP_FILENAMEFalseWhether to keep the original filename.
    UPLOAD_KEEP_EXTENSIONSFalseWhether to keep original file extensions.
    UPLOAD_ERROR_ON_BAD_CTEFalseIf True, raises an error on invalid Content-Transfer-Encoding.
    custom_config = {
        "MAX_BODY_SIZE": 5 * 1024 * 1024,  # 5MB
        "UPLOAD_DIR": "/tmp/uploads",
        "UPLOAD_ERROR_ON_BAD_CTE": True
    }
    
    parser = FormParser(
        content_type="multipart/form-data",
        boundary="boundary",
        on_field=my_field_cb,
        on_file=my_file_cb,
        config=custom_config
    )
  9. Configure File upload behavior with FileConfig

    main

    When handling file uploads, you can provide a FileConfig dictionary to control how files are stored and managed. This is useful for preventing memory exhaustion or managing disk space.

    Key configuration options include:

    • UPLOAD_DIR: A directory path where files will be stored. If None, system temporary locations are used.
    • UPLOAD_DELETE_TMP: If True (default), automatically created temporary files are deleted.
    • UPLOAD_KEEP_FILENAME: If True, the uploaded filename is used (stripped to its basename). If False, a temporary name is used.
    • UPLOAD_KEEP_EXTENSIONS: If True, the original file extension is maintained. If False, a .tmp extension is used.
    • MAX_MEMORY_FILE_SIZE: The threshold (in bytes) after which the file is moved from memory to a temporary file on disk. Default is 1 MiB.
    config = {
        'UPLOAD_DIR': '/tmp/uploads',
        'UPLOAD_DELETE_TMP': True,
        'UPLOAD_KEEP_FILENAME': True,
        'UPLOAD_KEEP_EXTENSIONS': True,
        'MAX_MEMORY_FILE_SIZE': 1024 * 1024  # 1 MiB
    }
  10. Instantiate a MultipartParser with callbacks

    main

    To parse multipart data manually, instantiate a MultipartParser. You must provide a boundary and a callbacks dictionary containing functions to handle various stages of the parsing lifecycle.

    Supported callback keys include:

    • on_part_begin: Triggered when a new part starts.
    • on_part_data: Triggered when part data is received.
    • on_part_end: Triggered when a part ends.
    • on_header_field: Triggered for each header field name.
    • on_header_value: Triggered for each header field value.
    • on_header_end: Triggered when headers for a part are finished.
    • on_headers_finished: Triggered when all headers are processed.
    • on_end: Triggered when the entire stream is finished.

    You can also pass a config dictionary to set limits like MAX_BODY_SIZE, MAX_HEADER_COUNT, and MAX_HEADER_SIZE.

    parser = MultipartParser(
        boundary,
        callbacks={
            "on_part_begin": on_part_begin,
            "on_part_data": on_part_data,
            "on_part_end": on_part_end,
            "on_header_field": on_header_field,
            "on_header_value": on_header_value,
            "on_header_end": on_header_end,
            "on_headers_finished": on_headers_finished,
            "on_end": _on_end,
        },
        max_size=MAX_BODY_SIZE,
        max_header_count=MAX_HEADER_COUNT,
        max_header_size=MAX_HEADER_SIZE,
    )
  11. Parse Content-Type headers with parse_options_header

    main

    Use parse_options_header to decompose a Content-Type header into its primary type and its associated parameters (like boundary or charset).

    It handles both str and bytes inputs. If bytes are provided, they are decoded using latin-1. It also includes logic to handle common edge cases, such as stripping directory components from filename parameters to prevent directory traversal attacks.

    from python_multipart.multipart import parse_options_header
    
    ctype, params = parse_options_header("multipart/form-data; boundary=something")
    # ctype: b'multipart/form-data'
    # params: {b'boundary': b'something'}