web-dev-qa-db-fra.com

Gestion des envois de fichiers volumineux avec Flask

Quel serait le meilleur moyen de gérer les envois de fichiers très volumineux (1 Go +) avec Flask?

Mon application prend essentiellement plusieurs fichiers et leur attribue un numéro de fichier unique, puis l’enregistre sur le serveur en fonction de la sélection de l’utilisateur.

Comment pouvons-nous exécuter des téléchargements de fichiers en tant que tâche en arrière-plan afin que l'utilisateur ne laisse pas le navigateur tourner pendant 1 heure et puisse à la place passer directement à la page suivante?

  • Le serveur de développement de Flask est capable de prendre des fichiers volumineux (50 Go ont pris 1,5 heure, le téléchargement a été rapide, mais l'écriture du fichier dans un fichier vierge a été extrêmement lente)
  • Si j'emballe l'application avec Twisted, l'application se bloque sur les gros fichiers
  • J'ai essayé d'utiliser Celery avec Redis, mais cela ne semble pas être une option pour les téléchargements postés.
  • Je suis sous Windows et j'ai moins d'options pour les serveurs Web. 
13
Infinity8

Je pense que le moyen le plus simple de contourner le problème consiste simplement à envoyer le fichier en plusieurs petites pièces/morceaux. Donc, il y aura deux parties pour faire ce travail, le front-end (site web) et le backend (serveur) . Pour la partie front-end, vous pouvez utiliser quelque chose comme Dropzone.js qui n'a pas de dépendances supplémentaires et CSS décent inclus. Tout ce que vous avez à faire est d’ajouter la classe dropzone à un formulaire et il le transforme automatiquement en l’un de leurs champs spéciaux de glisser-déposer (vous pouvez également cliquer sur et sélectionner).

Cependant, par défaut, Dropzone ne coupe pas les fichiers. Heureusement, il est très facile à activer. Voici un exemple de formulaire de téléchargement de fichier avec les options DropzoneJS et 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>

Et voici la partie Back-end utilisant le flacon:

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

Utilisez copy_current_request_context, il dupliquera le contexte request.so vous pourrez utiliser un thread ou autre pour rendre votre tâche exécutée en arrière-plan.

peut-être qu’un exemple le montrera clairement.Je l’ai testé avec un fichier 3.37G-debian-9.5.0-AMD64-DVD-1.iso.

# 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)

c'est tempalte, ça devrait être templates\upload.html

<!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

Lorsque vous téléchargez un fichier, vous ne pouvez tout simplement pas quitter la page et la faire continuer. La page doit rester ouverte pour continuer le téléchargement.

Vous pouvez par exemple ouvrir un nouvel onglet uniquement pour gérer le téléchargement et alerter l'utilisateur lorsqu'il ferme par inadvertance le nouvel onglet avant la fin du téléchargement. Ainsi, le téléchargement sera séparé de ce que l'utilisateur fait sur la page d'origine afin qu'il puisse toujours naviguer sans annuler le téléchargement. L'onglet de téléchargement peut également simplement se fermer lorsqu'il se termine.

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);
    });

Sur le serveur, vous pouvez utiliser request.stream pour lire le fichier téléchargé en morceaux afin d'éviter d'avoir à attendre que tout se charge en mémoire en premier.

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)

Vous pourriez être en mesure d'utiliser FormData api au lieu d'un flux d'octets, mais je ne sais pas si vous pouvez les diffuser en flacon.

0
daz