webentwicklung-frage-antwort-db.com.de

Handhabung großer Datei-Uploads mit Flask

Was wäre der beste Weg, um sehr große Dateiuploads (1 GB +) mit Flask zu verarbeiten?

Meine Anwendung verwendet im Wesentlichen mehrere Dateien, denen eine eindeutige Dateinummer zugewiesen ist, und speichert sie auf dem Server, je nachdem, wo der Benutzer ausgewählt hat.

Wie können wir Dateiuploads als Hintergrundaufgabe ausführen, damit der Benutzer den Browser nicht für 1 Stunde dreht und stattdessen direkt zur nächsten Seite wechseln kann?

  • Der Flaschenentwicklungsserver ist in der Lage, riesige Dateien aufzunehmen (50 GB waren 1,5 Stunden, das Hochladen war schnell, aber das Schreiben der Datei in eine leere Datei war schmerzhaft langsam)
  • Wenn ich die App mit Twisted umwickle, stürzt die App bei großen Dateien ab
  • Ich habe versucht, Sellerie mit Redis zu verwenden, aber dies scheint keine Option für gepostete Uploads zu sein
  • Ich bin unter Windows und habe weniger Optionen für Webserver 
13
Infinity8

Ich denke, der einfachste Weg, um herumzukommen, sendet einfach die Datei in vielen kleinen Teilen/Stücken. Es gibt also zwei Teile für diese Arbeit, das Frontend (Website) und das Backend (Server). Für den Frontend-Teil können Sie etwas wie Dropzone.js verwenden, das keine zusätzlichen Abhängigkeiten und hat anständige CSS enthalten. Alles, was Sie tun müssen, ist die Klasse dropzone zu einem Formular hinzuzufügen, das automatisch in eines der speziellen Drag & Drop-Felder umgewandelt wird (Sie können auch klicken und auswählen).

Standardmäßig werden die Dateien jedoch nicht in Dropzone aufgeteilt. Zum Glück ist es wirklich leicht zu aktivieren. Hier ist ein Beispiel zum Hochladen von Dateien mit aktivierten DropzoneJS und chunking:

<html lang="en">
<head>

    <meta charset="UTF-8">

    <link rel="stylesheet" 
     href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.4.0/min/dropzone.min.css"/>

    <link rel="stylesheet" 
     href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.4.0/min/basic.min.css"/>

    <script type="application/javascript" 
     src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.4.0/min/dropzone.min.js">
    </script>

    <title>File Dropper</title>
</head>
<body>

<form method="POST" action='/upload' class="dropzone dz-clickable" 
      id="dropper" enctype="multipart/form-data">
</form>

<script type="application/javascript">
    Dropzone.options.dropper = {
        paramName: 'file',
        chunking: true,
        forceChunking: true,
        url: '/upload',
        maxFilesize: 1025, // megabytes
        chunkSize: 1000000 // bytes
    }
</script>
</body>
</html>

Und hier ist der Back-End-Teil mit Kolben:

import logging
import os

from flask import render_template, Blueprint, request, make_response
from werkzeug.utils import secure_filename

from pydrop.config import config

blueprint = Blueprint('templated', __name__, template_folder='templates')

log = logging.getLogger('pydrop')


@blueprint.route('/')
@blueprint.route('/index')
def index():
    # Route to serve the upload form
    return render_template('index.html',
                           page_name='Main',
                           project_name="pydrop")


@blueprint.route('/upload', methods=['POST'])
def upload():
    file = request.files['file']

    save_path = os.path.join(config.data_dir, secure_filename(file.filename))
    current_chunk = int(request.form['dzchunkindex'])

    # If the file already exists it's ok if we are appending to it,
    # but not if it's new file that would overwrite the existing one
    if os.path.exists(save_path) and current_chunk == 0:
        # 400 and 500s will tell dropzone that an error occurred and show an error
        return make_response(('File already exists', 400))

    try:
        with open(save_path, 'ab') as f:
            f.seek(int(request.form['dzchunkbyteoffset']))
            f.write(file.stream.read())
    except OSError:
        # log.exception will include the traceback so we can see what's wrong 
        log.exception('Could not write to file')
        return make_response(("Not sure why,"
                              " but we couldn't write the file to disk", 500))

    total_chunks = int(request.form['dztotalchunkcount'])

    if current_chunk + 1 == total_chunks:
        # This was the last chunk, the file should be complete and the size we expect
        if os.path.getsize(save_path) != int(request.form['dztotalfilesize']):
            log.error(f"File {file.filename} was completed, "
                      f"but has a size mismatch."
                      f"Was {os.path.getsize(save_path)} but we"
                      f" expected {request.form['dztotalfilesize']} ")
            return make_response(('Size mismatch', 500))
        else:
            log.info(f'File {file.filename} has been uploaded successfully')
    else:
        log.debug(f'Chunk {current_chunk + 1} of {total_chunks} '
                  f'for file {file.filename} complete')

    return make_response(("Chunk upload successful", 200))
4
Abdul Rehman

Verwenden Sie copy_current_request_context, damit wird der Kontext request. dupliziert. Sie können also Thread oder etwas anderes verwenden, um den Hintergrund Ihrer Aufgabe auszuführen.

vielleicht wird ein Beispiel es klarer machen. Ich habe es mit einer 3.37G-Datei debian-9.5.0-AMD64-DVD-1.iso getestet.

# coding:utf-8

from flask import Flask,render_template,request,redirect,url_for
from werkzeug.utils import secure_filename
import os
from time import sleep
from flask import copy_current_request_context
import threading
import datetime
app = Flask(__name__)
@app.route('/upload', methods=['POST','GET'])
def upload():
    @copy_current_request_context
    def save_file(closeAfterWrite):
        print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + " i am doing")
        f = request.files['file']
        basepath = os.path.dirname(__file__) 
        upload_path = os.path.join(basepath, '',secure_filename(f.filename)) 
        f.save(upload_path)
        closeAfterWrite()
        print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + " write done")
    def passExit():
        pass
    if request.method == 'POST':
        f= request.files['file']
        normalExit = f.stream.close
        f.stream.close = passExit
        t = threading.Thread(target=save_file,args=(normalExit,))
        t.start()
        return redirect(url_for('upload'))
    return render_template('upload.html')

if __== '__main__':
    app.run(debug=True)

dies ist tempalte, es sollte template\upload.html sein

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>example</h1>
    <form action="" enctype='multipart/form-data' method='POST'>
        <input type="file" name="file">
        <input type="submit" value="upload">
    </form>
</body>
</html>
1
obgnaw

Beim Hochladen einer Datei können Sie die Seite einfach nicht verlassen und fortfahren lassen. Die Seite muss geöffnet bleiben, um den Upload fortsetzen zu können.

Sie könnten beispielsweise eine neue Registerkarte öffnen, um den Upload zu handhaben und den Benutzer zu warnen, wenn er die neue Registerkarte versehentlich schließt, bevor der Upload abgeschlossen ist. Auf diese Weise wird der Upload von den Aktivitäten des Benutzers auf der Originalseite getrennt, sodass er weiterhin navigieren kann, ohne den Upload abzubrechen. Der Upload-Tab kann sich auch schließen, wenn er fertig ist.

index.js

    // get value from <input id="upload" type="file"> on page
    var upload = document.getElementById('upload');
    upload.addEventListener('input', function () {
        // open new tab and stick the selected file in it
        var file = upload.files[0];
        var uploadTab = window.open('/upload-page', '_blank');
        if (uploadTab) {
            uploadTab.file = file;
        } else {
            alert('Failed to open new tab');
        }
    });

upload-page.js

    window.addEventListener('beforeunload', function () {
        return 'The upload will cancel if you leave the page, continue?';
    });
    window.addEventListener('load', function () {
        var req = new XMLHttpRequest();
        req.addEventListener('progress', function (evt) {
            var percentage = '' + (evt.loaded / evt.total * 100) + '%';
            // use percentage to update progress bar or something
        });
        req.addEventListener('load', function () {
            alert('Upload Finished');
            window.removeEventListener('beforeunload');
            window.close();
        });
        req.addRequestHeader('Content-Type', 'application/octet-stream');
        req.open('POST', '/upload/'+encodeURIComponent(window.file.name));
        req.send(window.file);
    });

Auf dem Server können Sie request.stream verwenden, um die hochgeladene Datei in Blöcken zu lesen, um nicht erst warten zu müssen, bis die gesamte Sache in den Speicher geladen wurde.

server.py

@app('/upload/<filename>', methods=['POST'])
def upload(filename):
    filename = urllib.parse.unquote(filename)
    bytes_left = int(request.headers.get('content-length'))
    with open(os.path.join('uploads', filename), 'wb') as upload:
        chunk_size = 5120
        while bytes_left > 0:
            chunk = request.stream.read(chunk_size)
            upload.write(chunk)
            bytes_left -= len(chunk)
        return make_response('Upload Complete', 200)

Möglicherweise können Sie FormData-API anstelle eines Octet-Streams verwenden, aber ich bin nicht sicher, ob Sie diese in der Flasche streamen können.

0
daz