Skip to content Skip to sidebar Skip to footer

Flask Form Check to See if File Upload Is Empty

A common feature in web applications is to let users upload files to the server. The HTTP protocol documents the machinery for a client to upload a file in RFC 1867, and our favorite web framework Flask fully supports it, but there are many implementation details that autumn outside of the formal specification that are unclear for many developers. Things such as where to store uploaded files, how to use them later on, or how to protect the server against malicious file uploads generate a lot of confusion and doubtfulness.

In this article I'chiliad going to prove yous how to implement a robust file upload feature for your Flask server that is compatible with the standard file upload support in your web browser as well equally the cool JavaScript-based upload widgets:

Basic file upload form

A Bones File Upload Grade

From a high-level perspective, a client uploading a file is treated the same as any other course data submission. In other words, you have to define an HTML form with a file field in it.

Here is a simple HTML page with a form that accepts a file:

          <!doctype html> <html>   <head>     <title>File Upload</title>   </head>   <body>     <h1>File Upload</h1>     <form method="Postal service" action="" enctype="multipart/grade-data">       <p><input blazon="file" name="file"></p>       <p><input type="submit" value="Submit"></p>     </form>   </body> </html>                  
Basic file upload form

As you probably know, the method attribute of the <class> chemical element can be Go or Post. With Get, the information is submitted in the query string of the request URL, while with Mail it goes in the request body. When files are beingness included in the form, you must use POST, every bit it would be incommunicable to submit file data in the query string.

The enctype attribute in the <form> element is normally not included with forms that don't have files. This aspect defines how the browser should format the data earlier information technology is submitted to the server. The HTML specification defines 3 possible values for it:

  • awarding/x-www-grade-urlencoded: This is the default, and the best format for any forms except those that contain file fields.
  • multipart/form-data: This format is required when at least one of the fields in the form is a file field.
  • text/plain: This format has no practical utilize, then you should ignore it.

The bodily file field is the standard <input> chemical element that nosotros use for most other form fields, with the type set to file. In the instance to a higher place I haven't included any boosted attributes, simply the file field supports ii that are sometimes useful:

  • multiple can be used to let multiple files to exist uploaded in a single file field. Example:
                      <input type="file" proper name="file" multiple>                  
  • accept tin can be used to filter the allowed file types that can be selected, either by file extension or by media type. Examples:
                      <input type="file" proper noun="doc_file" accept=".physician,.docx">     <input type="file" name="image_file" take="image/*">                  

Accepting File Submissions with Flask

For regular forms, Flask provides access to submitted form fields in the request.grade dictionary. File fields, nonetheless, are included in the request.files dictionary. The asking.course and asking.files dictionaries are actually "multi-dicts", a specialized lexicon implementation that supports duplicate keys. This is necessary because forms tin include multiple fields with the same proper name, as is often the case with groups of bank check boxes. This likewise happens with file fields that let multiple files.

Ignoring important aspects such equally validation and security for the moment, the brusk Flask application shown below accepts a file uploaded with the class shown in the previous section, and writes the submitted file to the electric current directory:

          from flask import Flask, render_template, request, redirect, url_for  app = Flask(__name__)  @app.route('/') def index():     return render_template('alphabetize.html')  @app.route('/', methods=['Postal service']) def upload_file():     uploaded_file = request.files['file']     if uploaded_file.filename != '':         uploaded_file.save(uploaded_file.filename)     return redirect(url_for('alphabetize'))                  

The upload_file() function is decorated with @app.route and then that it is invoked when the browser sends a Postal service request. Annotation how the same root URL is dissever between ii view functions, with index() set up to accept the GET requests and upload_file() the Mail service ones.

The uploaded_file variable holds the submitted file object. This is an example of class FileStorage, which Flask imports from Werkzeug.

The filename aspect in the FileStorage provides the filename submitted past the client. If the user submits the course without selecting a file in the file field, then the filename is going to be an empty string, then it is important to e'er check the filename to decide if a file is available or non.

When Flask receives a file submission it does not automatically write information technology to disk. This is actually a good thing, because it gives the application the opportunity to review and validate the file submission, as yous will see later. The bodily file data tin be accessed from the stream attribute. If the awarding just wants to relieve the file to deejay, so information technology can call the save() method, passing the desired path equally an statement. If the file's save() method is non called, then the file is discarded.

Desire to test file uploads with this application? Make a directory for your application and write the code above as app.py. And so create a templates subdirectory, and write the HTML page from the previous section equally templates/index.html. Create a virtual environment and install Flask on information technology, and then run the application with flask run. Every time you submit a file, the server volition write a copy of information technology in the electric current directory.

Before I motility on to the topic of security, I'm going to discuss a few variations on the code shown above that y'all may find useful. Every bit I mentioned before, the file upload field tin can be configured to accept multiple files. If you use request.files['file'] equally above you will get but one of the submitted files, but with the getlist() method yous can access all of them in a for-loop:

                      for uploaded_file in request.files.getlist('file'):         if uploaded_file.filename != '':             uploaded_file.relieve(uploaded_file.filename)                  

Many people code their form handling routes in Flask using a unmarried view role for both the GET and POST requests. A version of the example application using a single view office could be coded as follows:

          @app.route('/', methods=['Get', 'Mail']) def index():     if request.method == 'Mail service':         uploaded_file = request.files['file']         if uploaded_file.filename != '':             uploaded_file.save(uploaded_file.filename)         return redirect(url_for('index'))     return render_template('index.html')                  

Finally, if y'all utilise the Flask-WTF extension to handle your forms, you lot tin use the FileField object for your file uploads. The course used in the examples you lot've seen then far can exist written using Flask-WTF every bit follows:

          from flask_wtf import FlaskForm from flask_wtf.file import FileField from wtforms import SubmitField  class MyForm(FlaskForm):     file = FileField('File')     submit = SubmitField('Submit')                  

Notation that the FileField object comes from the flask_wtf package, unlike virtually other field classes, which are imported directly from the wtforms package. Flask-WTF provides two validators for file fields, FileRequired, which performs a cheque similar to the empty string check, and FileAllowed, which ensures the file extension is included in an allowed extensions list.

When you use a Flask-WTF course, the information attribute of the file field object points to the FileStorage instance, so saving a file to deejay works in the same way as in the examples higher up.

Securing file uploads

The file upload example presented in the previous department is an extremely simplistic implementation that is non very robust. One of the most of import rules in web development is that data submitted by clients should never be trusted, and for that reason when working with regular forms, an extension such as Flask-WTF performs strict validation of all fields before the form is accustomed and the data incorporated into the awarding. For forms that include file fields at that place needs to be validation as well, because without file validation the server leaves the door open up to attacks. For example:

  • An attacker can upload a file that is so large that the disk space in the server is completely filled, causing the server to malfunction.
  • An aggressor tin craft an upload request that uses a filename such as ../../../.bashrc or similar, with the effort to trick the server into rewriting system configuration files.
  • An attacker can upload files with viruses or other types of malware in a identify where the application, for instance, expects images.

Limiting the size of uploaded files

To prevent clients from uploading very big files, you can apply a configuration option provided by Flask. The MAX_CONTENT_LENGTH selection controls the maximum size a request torso can have. While this isn't an option that is specific to file uploads, setting a maximum request trunk size effectively makes Flask discard whatsoever incoming requests that are larger than the allowed amount with a 413 status code.

Let'southward modify the app.py example from the previous section to only accept requests that are up to 1MB in size:

          app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024                  

If y'all try to upload a file that is larger than 1MB, the awarding will at present refuse it.

Validating filenames

We tin can't really trust that the filenames provided by the client are valid and safe to use, so filenames coming with uploaded files have to be validated.

A very simple validation to perform is to make certain that the file extension is ane that the application is willing to accept, which is similar to what the FileAllowed validator does when using Flask-WTF. Let's say the application accepts images, then it tin configure the list of canonical file extensions:

          app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png', '.gif']                  

For every uploaded file, the application can brand certain that the file extension is i of the allowed ones:

                      filename = uploaded_file.filename     if filename != '':         file_ext = os.path.splitext(filename)[i]         if file_ext non in current_app.config['UPLOAD_EXTENSIONS']:             abort(400)                  

With this logic, any filenames that do not take one of the canonical file extensions is going to be responded with a 400 error.

In addition to the file extension, it is as well important to validate the filename, and any path given with it. If your application does non care about the filename provided by the client, the almost secure way to handle the upload is to ignore the customer provided filename and generate your own filename instead, that y'all pass to the save() method. An instance utilize example where this technique works well is with avatar prototype uploads. Each user's avatar can be saved with the user id equally filename, and then the filename provided by the client can be discarded. If your awarding uses Flask-Login, you lot could implement the post-obit save() call:

          uploaded_file.save(os.path.join('static/avatars', current_user.get_id()))                  

In other cases it may be amend to preserve the filenames provided by the client, so the filename must be sanitized outset. For those cases Werkzeug provides the secure_filename() function. Let'due south see how this function works by running a few tests in a Python session:

          >>> from werkzeug.utils import secure_filename >>> secure_filename('foo.jpg') 'foo.jpg' >>> secure_filename('/some/path/foo.jpg') 'some_path_foo.jpg' >>> secure_filename('../../../.bashrc') 'bashrc'                  

As you see in the examples, no thing how complicated or malicious the filename is, the secure_filename() role reduces it to a flat filename.

Allow'southward incorporate secure_filename() into the case upload server, and also add a configuration variable that defines a defended location for file uploads. Here is the complete app.py source file with secure filenames:

          import os from flask import Flask, render_template, request, redirect, url_for, abort from werkzeug.utils import secure_filename  app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png', '.gif'] app.config['UPLOAD_PATH'] = 'uploads'  @app.route('/') def index():     return render_template('index.html')  @app.route('/', methods=['Mail']) def upload_files():     uploaded_file = request.files['file']     filename = secure_filename(uploaded_file.filename)     if filename != '':         file_ext = os.path.splitext(filename)[1]         if file_ext not in app.config['UPLOAD_EXTENSIONS']:             abort(400)         uploaded_file.relieve(os.path.join(app.config['UPLOAD_PATH'], filename))     return redirect(url_for('index'))                  

Validating file contents

The tertiary layer of validation that I'chiliad going to discuss is the most complex. If your application accepts uploads of a certain file type, it should ideally perform some form of content validation and reject whatever files that are of a unlike blazon.

How you reach content validation largely depends on the file types your application accepts. For the example application in this commodity I'm using images, and then I can use the imghdr package from the Python standard library to validate that the header of the file is, in fact, an image.

Let's write a validate_image() function that performs content validation on images:

          import imghdr  def validate_image(stream):     header = stream.read(512)     stream.seek(0)     format = imghdr.what(None, header)     if not format:         return None     render '.' + (format if format != 'jpeg' else 'jpg')                  

This function takes a byte stream as an argument. It starts by reading 512 bytes from the stream, and then resetting the stream pointer dorsum, because later when the save() role is called we want it to see the unabridged stream. The first 512 bytes of the image data are going to exist sufficient to identify the format of the prototype.

The imghdr.what() function can look at a file stored on disk if the beginning argument is the filename, or else information technology can look at data stored in memory if the start statement is None and the data is passed in the second argument. The FileStorage object gives usa a stream, and so the most user-friendly option is to read a safe corporeality of data from it and laissez passer it as a byte sequence in the second argument.

The return value of imghdr.what() is the detected image format. The role supports a variety of formats, amongst them the popular jpeg, png and gif. If not known paradigm format is detected, so the return value is None. If a format is detected, the proper name of the format is returned. The most user-friendly is to return the format as a file extension, because the application can and so ensure that the detected extension matches the file extension, so the validate_image() part converts the detected format into a file extension. This is equally elementary as calculation a dot as prefix for all image formats except jpeg, which usually uses the .jpg extension, and then this case is treated every bit an exception.

Here is the complete app.py, with all the features from the previous sections plus content validation:

          import imghdr import os from flask import Flask, render_template, request, redirect, url_for, abort from werkzeug.utils import secure_filename  app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png', '.gif'] app.config['UPLOAD_PATH'] = 'uploads'  def validate_image(stream):     header = stream.read(512)     stream.seek(0)      format = imghdr.what(None, header)     if not format:         render None     return '.' + (format if format != 'jpeg' else 'jpg')  @app.route('/') def alphabetize():     return render_template('index.html')  @app.road('/', methods=['Post']) def upload_files():     uploaded_file = request.files['file']     filename = secure_filename(uploaded_file.filename)     if filename != '':         file_ext = os.path.splitext(filename)[i]         if file_ext not in app.config['UPLOAD_EXTENSIONS'] or \                 file_ext != validate_image(uploaded_file.stream):             abort(400)         uploaded_file.relieve(bone.path.join(app.config['UPLOAD_PATH'], filename))     return redirect(url_for('index'))                  

The but change in the view role to incorporate this last validation logic is here:

                      if file_ext non in app.config['UPLOAD_EXTENSIONS'] or \                 file_ext != validate_image(uploaded_file.stream):             arrest(400)                  

This expanded check get-go makes sure that the file extension is in the allowed listing, and then ensures that the detected file extension from looking at the data stream is the same as the file extension.

Before you test this version of the application create a directory named uploads (or the path that yous divers in the UPLOAD_PATH configuration variable, if unlike) so that files tin be saved there.

Using Uploaded Files

Y'all at present know how to handle file uploads. For some applications this is all that is needed, every bit the files are used for some internal process. But for a big number of applications, in item those with social features such as avatars, the files that are uploaded by users have to be integrated with the application. Using the example of avatars, once a user uploads their avatar image, any mention of the username requires the uploaded image to appear to the side.

I carve up file uploads into 2 large groups, depending on whether the files uploaded by users are intended for public use, or they are private to each user. The avatar images discussed several times in this article are clearly in the first group, as these avatars are intended to be publicly shared with other users. On the other side, an awarding that performs editing operations on uploaded images would probably be in the second group, considering you'd want each user to simply have access to their own images.

Consuming public uploads

When images are of a public nature, the easiest fashion to make the images available for use by the application is to put the upload directory inside the application's static folder. For example, an avatars subdirectory tin can be created inside static, and so avatar images can be saved in that location using the user id every bit proper name.

Referencing these uploads stored in a subdirectory of the static folder is washed in the same manner as regular static files of the awarding, using the url_for() role. I previously suggested using the user id as a filename, when saving an uploaded avatar image. This was the mode the images were saved:

          uploaded_file.save(os.path.join('static/avatars', current_user.get_id()))                  

With this implementation, given a user_id, the URL for the user's avatar can be generated as follows:

          url_for('static', filename='avatars/' + str(user_id))                  

Alternatively, the uploads tin be saved to a directory outside of the static folder, and then a new road can be added to serve them. In the example app.py application file uploads are saved to the location set in the UPLOAD_PATH configuration variable. To serve these files from that location, we tin implement the following route:

          from flask import send_from_directory  @app.route('/uploads/<filename>') def upload(filename):     return send_from_directory(app.config['UPLOAD_PATH'], filename)                  

One reward that this solution has over storing uploads inside the static folder is that here yous can implement additional restrictions before these files are returned, either direct with Python logic inside the body of the function, or with decorators. For case, if y'all want to only provide admission to the uploads to logged in users, y'all can add together Flask-Login'due south @login_required decorator to this route, or whatever other authentication or role checking mechanism that you utilise for your normal routes.

Let's use this implementation idea to evidence uploaded files in our instance application. Here is a new complete version of app.py:

          import imghdr import os from flask import Flask, render_template, request, redirect, url_for, abort, \     send_from_directory from werkzeug.utils import secure_filename  app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png', '.gif'] app.config['UPLOAD_PATH'] = 'uploads'  def validate_image(stream):     header = stream.read(512)  # 512 bytes should be enough for a header check     stream.seek(0)  # reset stream pointer     format = imghdr.what(None, header)     if not format:         render None     return '.' + (format if format != 'jpeg' else 'jpg')  @app.road('/') def index():     files = bone.listdir(app.config['UPLOAD_PATH'])     return render_template('index.html', files=files)  @app.road('/', methods=['Mail service']) def upload_files():     uploaded_file = request.files['file']     filename = secure_filename(uploaded_file.filename)     if filename != '':         file_ext = os.path.splitext(filename)[1]         if file_ext non in app.config['UPLOAD_EXTENSIONS'] or \                 file_ext != validate_image(uploaded_file.stream):             abort(400)         uploaded_file.save(os.path.join(app.config['UPLOAD_PATH'], filename))     render redirect(url_for('index'))  @app.route('/uploads/<filename>') def upload(filename):     return send_from_directory(app.config['UPLOAD_PATH'], filename)                  

In addition to the new upload() function, the index() view function gets the listing of files in the upload location using bone.listdir() and sends information technology downward to the template for rendering. The index.html template updated to prove uploads is shown beneath:

          <!doctype html> <html>   <head>     <title>File Upload</title>   </head>   <body>     <h1>File Upload</h1>     <form method="POST" activity="" enctype="multipart/course-data">       <p><input type="file" name="file"></p>       <p><input type="submit" value="Submit"></p>     </grade>     <hr>     {% for file in files %}       <img src="{{ url_for('upload', filename=file) }}" mode="width: 64px">     {% endfor %}   </body> </html>                  

With these changes, every time you upload an image, a thumbnail is added at the bottom of the page:

Basic file upload form

Consuming private uploads

When users upload private files to the application, boosted checks need to exist in place to forestall sharing files from one user with unauthorized parties. The solution for these cases crave variations of the upload() view function shown above, with boosted admission checks.

A common requirement is to but share uploaded files with their owner. A convenient way to store uploads when this requirement is present is to use a dissever directory for each user. For example, uploads for a given user tin be saved to the uploads/<user_id> directory, and then the uploads() office tin can be modified to simply serve uploads from the user'south ain upload directory, making it impossible for one user to see files from another. Below you can run into a possible implementation of this technique, in one case over again assuming Flask-Login is used:

          @app.route('/uploads/<filename>') @login_required def upload(filename):     render send_from_directory(os.path.join(         app.config['UPLOAD_PATH'], current_user.get_id()), filename)                  

Showing upload progress

Up until now nosotros have relied on the native file upload widget provided past the web browser to initiate our file uploads. I'm sure we tin can all concord that this widget is not very appealing. Not merely that, simply the lack of an upload progress brandish makes it unusable for uploads of large files, as the user receives no feedback during the entire upload process. While the scope of this article is to encompass the server side, I thought it would be useful to requite you a few ideas on how to implement a modern JavaScript-based file upload widget that displays upload progress.

The good news is that on the server there aren't any big changes needed, the upload mechanism works in the same way regardless of what method you employ in the browser to initiate the upload. To show you an instance implementation I'm going to supersede the HTML form in index.html with one that is compatible with dropzone.js, a popular file upload client.

Here is a new version of templates/index.html that loads the dropzone CSS and JavaScript files from a CDN, and implements an upload grade according to the dropzone documentation:

          <html>   <head>     <title>File Upload</title>     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.7.1/min/dropzone.min.css">   </caput>   <body>     <h1>File Upload</h1>     <course action="{{ url_for('upload_files') }}" class="dropzone">     </form>     <script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/five.seven.ane/min/dropzone.min.js"></script>   </torso> </html>                  

The one interesting matter that I've plant when implementing dropzone is that it requires the action attribute in the <form> element to exist set, fifty-fifty though normal forms accept an empty action to indicate that the submission goes to the same URL.

Outset the server with this new version of the template, and this is what y'all'll get:

Basic file upload form

That's basically it! You lot can at present drib files and they'll be uploaded to the server with a progress bar and a final indication of success or failure.

If the file upload fails, either due to the file being besides large or invalid, dropzone wants to display an mistake message. Because our server is currently returning the standard Flask error pages for the 413 and 400 errors, you volition run into some HTML gibberish in the error popup. To correct this we can update the server to return its error responses as text.

The 413 error for the file too large condition is generated by Flask when the request payload is bigger than the size gear up in the configuration. To override the default error page we take to utilise the app.errorhandler decorator:

          @app.errorhandler(413) def too_large(e):     return "File is too large", 413                  

The second mistake status is generated by the application when any of the validation checks fails. In this case the error was generated with a arrest(400) telephone call. Instead of that the response can be generated directly:

                      if file_ext not in app.config['UPLOAD_EXTENSIONS'] or \                 file_ext != validate_image(uploaded_file.stream):             return "Invalid prototype", 400                  

The final modify that I'm going to make isn't really necessary, but information technology saves a bit of bandwidth. For a successful upload the server returned a redirect() back to the principal route. This caused the upload grade to be displayed again, and also to refresh the list of upload thumbnails at the lesser of the page. None of that is necessary now considering the uploads are done equally groundwork requests by dropzone, so nosotros tin eliminate that redirect and switch to an empty response with a code 204.

Here is the complete and updated version of app.py designed to work with dropzone.js:

          import imghdr import os from flask import Flask, render_template, request, redirect, url_for, arrest, \     send_from_directory from werkzeug.utils import secure_filename  app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024 app.config['UPLOAD_EXTENSIONS'] = ['.jpg', '.png', '.gif'] app.config['UPLOAD_PATH'] = 'uploads'  def validate_image(stream):     header = stream.read(512)     stream.seek(0)     format = imghdr.what(None, header)     if non format:         return None     render '.' + (format if format != 'jpeg' else 'jpg')  @app.errorhandler(413) def too_large(east):     return "File is as well big", 413  @app.road('/') def alphabetize():     files = os.listdir(app.config['UPLOAD_PATH'])     return render_template('index.html', files=files)  @app.route('/', methods=['Postal service']) def upload_files():     uploaded_file = asking.files['file']     filename = secure_filename(uploaded_file.filename)     if filename != '':         file_ext = bone.path.splitext(filename)[ane]         if file_ext not in app.config['UPLOAD_EXTENSIONS'] or \                 file_ext != validate_image(uploaded_file.stream):             return "Invalid paradigm", 400         uploaded_file.save(os.path.join(app.config['UPLOAD_PATH'], filename))     render '', 204  @app.road('/uploads/<filename>') def upload(filename):     return send_from_directory(app.config['UPLOAD_PATH'], filename)                  

Restart the awarding with this update and at present errors will accept a proper message:

Basic file upload form

The dropzone.js library is very flexible and has many options for customization, so I encourage y'all to visit their documentation to learn how to adjust it to your needs. Y'all can also look for other JavaScript file upload libraries, equally they all follow the HTTP standard, which means that your Flask server is going to work well with all of them.

Conclusion

This was a long overdue topic for me, I can't believe I have never written annihilation on file uploads! I'd love you hear what you think nearly this topic, and if you remember there are aspects of this characteristic that I oasis't covered in this commodity. Feel gratuitous to permit me know below in the comments!

alexanderyoulthad95.blogspot.com

Source: https://blog.miguelgrinberg.com/post/handling-file-uploads-with-flask

Enregistrer un commentaire for "Flask Form Check to See if File Upload Is Empty"