Writing a multi-file-upload Python-web app with user authentication | by Daniel Ellis | Towards Data Science

When building a webserver we often wish to present an idea or topic. In the case of a static website, this can be done by including the relevant information within the source files. In more dynamic examples, an API (application programming interface) can be used to pre-process information before returning it to the user.
In this example, we design a simple flask app which only requires a user key and a web browser to work. The idea behind this is that anyone can use it, and it is a device (and operating system), independent.
As usual, we start by installing the relevant libraries for the server. The simplest way to do this is through the use of pip (pythons package manager).
pip install flask, werkzeug
Next, create our HTML template for the login page. To do this we start in our application directory and make a new file named templates . Within this, we create a file named upload.html and paste the following code within it.
In essence, we have a form which contains our password input, a file upload input and a submit button. When working this will look as follows
Next, we define a config file for our upload options. To do this create a file named config.py in your main app folder. Within this, we can specify a maximum accepted file size, the upload destination, and what file extensions we can choose to accept. Copy the code below into config.py and adjust accordingly.
A note on file types: File extensions do not guarantee file type and we should avoid executing any uploaded files. This is why we later introduce a password — such that only approved users can upload.
We begin with a basic flask template, which imports our information and serves the upload.html page when navigating to http://127.0.0.1:4000/upload
Now we have our app, we can add a number of functions to ensure that we have the correct directory, and if not create it.
if not os.path.isdir(upload_dest):    os.mkdir(upload_dest)
We can set the maximum file upload size (using the value from config.py)
app.config['MAX_CONTENT_LENGTH'] = file_mb_max * 1024 * 1024
and also check the file extensions (again defined in config.py)
def allowed_file(filename):    return '.' in filename and filename.rsplit('.', 1)[1].lower() in                 extensions
Personally, I have added these between app.secret and the @app.route sections within the code.
Finally, we have yet to tell the program what to do upon uploading. We do so this by parsing the post request from the upload page. For information on what this means — have a read of:
towardsdatascience.com
Here we check if the request.method is a POST, and if so handle any files attached. The first section deals with an empty request, whilst the second iterates through each file, checks that they have the correct file extension, and if so saves them to our desired location (upload_dest).
@app.route('/upload', methods=['POST'])def upload_file():    if request.method == 'POST':    if 'files[]' not in request.files:            flash('No files found, try again.')            return redirect(request.url)    files = request.files.getlist('files[]')    for file in files:            if file and allowed_file(file.filename):                filename = secure_filename(file.filename)                file.save(os.path.join( upload_dest, filename))    flash('File(s) uploaded')    return redirect('/upload')
Finally, before continuing on to adding passwords, we want to make sure that the upload functions as intended. To do this we run the python script, navigate to http://127.0.0.1:4000/upload, select a few files and click upload. If this has been successful, and you are using the configuration above, your files should now reside within an uploads_folder directory nested in the same folder as your app.
So far we have created a Flask app which we can use to upload files. Next, we want to add a layer of basic security, such that we can track what files have been uploaded and by whom. In addition, this allows us to reject anyone not authorised to commit data to the directory.
We begin by installing our database tool. This can sometimes cause problems for people, so the installation instructions for Mac and Linux have been included below.
MAC
brew install SQLCipherpip install pysqlcipher3
LINUX
sudo apt install sqlcipher libsqlcipher0 libsqlcipher-devsudo -H pip3 install pysqlcipher3python3 -c 'import pysqlcipher3; print(pysqlcipher3.__path__)'
We start by connecting to the database. If you have ever used databases before, the pysqlcipher syntax is pretty much the same as sqlite, or any postgresql libraries. We begin by importing the libraries
from pysqlcipher3 import dbapi2 as sqlite3from config import app_key, db_loc
And then connecting to the database:
conn = sqlite3.connect(db_loc)# where this is /path/test.dbcursor = conn.cursor()
Finally, we need to specify an encryption key for our database in order to be able to access the data.
cursor.execute("PRAGMA key='%s'"%app_key)
If you do not do this, or use a different key, you will receive the following error: DatabaseError: file is not a database when trying to read information from the database schema.
Having opened our database, and entered our key, we can now create a table to store our values. We do this by executing a create table SQL command with the cursor execute function. The simplest usage case would require a name and an upload_key.
cursor.execute(            '''            CREATE TABLE IF NOT EXISTS upload (            id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,            name TEXT NOT NULL,            upload_key TEXT UNIQUE            );            '''    )
Additionally, I have set a condition that each key has to be unique, as we are not using a user name to log on.
Finally, we have to commit our new table to the database and close it.
conn.commit()## conn.close() ## close only when we have finished everything, otherwise we have to reopen the database each time
We need users to be able to use the upload tool, so we can add some using the insert command.
cursor.execute(            '''            INSERT INTO upload (name, dir, uploadcode)            VALUES ("bob", "bobs top secret upload key")            '''conn.commit()
As a check, we want to see if there is a name associated with the upload_key. This can be done with the select function, coupled with a where conditional.
user_code = 'bobs top secret upload key'cursor.execute('select * from upload where uploadcode="%s"'%user_code)result = cursor.fetchall() # get all the results e.g. [("bob",)]
Now we have a database and an upload script, we can combine the two.
Firstly we add the required library
from pysqlcipher3 import dbapi2 as sqlite3# from config import app_key, db_loc # already imported
under the from config_simple import *line.
If you are using the HTML code from earlier, we already have a password input field :<p> upload_key: <input name=”psw” type=”password” /></p>
As this is within the submission form we can read it as part of the POST request in the @app.route wrapper.
user_code = str(request.form.get('psw'))
We combine this with our database read to form a conditional checking if there is anyone with that access key /password.
cursor.execute('select * from upload where uploadcode="%s"'%user_code)        result = cursor.fetchall()if len(result)==0:             flash('Not a valid Code')            return redirect(request.url)
However, since the database can only be opened and read from the same computational thread as the website we need to place
conn = sqlite3.connect(db_loc)cursor = conn.cursor()cursor.execute("PRAGMA key='%s'"%app_key)
before the cursor.execute block, and a conn.close() after the result = line. (see app.py in the GitHub repo at the end)
And there we have it — the most basic of submission boxes which allows for the uploading of files by pre-approved users. There are however a number of improvements that you may wish to make (these are all in the additional files shown in the GitHub repository below).
Checking the filename string for an extension can cause problems. Files may be named incorrectly, or even not have an extension (as was the case with many of my files). Instead, we can filter on the file.mimetype values. These are of the format image/png etc.
Drag and Drop uploading. It can often be cumbersome to manually select files, especially if they are in different locations. Dragging them from a file browser makes life much simpler for the user.
Automatic uploads. Another common mistake is to select a file, but forget to click submit. Having a window that works immediately after you drop a file in it can help aid productivity and user satisfaction
File previews. If the user is uploading images, it can always be helpful to have miniature previews available. This allows a last-minute check such that you are not repeatedly submitting the same file.
File failure indicators. Although there is a message informing us of the upload status, a visual cue (ie a lighter preview image) makes it much easier to see if something has not worked.
Unique download spaces — if you have many users, it can become confusing which file belongs to who (not to mention the overwriting problem). Instead, we can add a designated space for each user, and save the paths within the database.
Adding all the additional suggestions above and we the following output.
Here we see, two files have failed, with one succeeding in the middle. In this case, it was due to invalid user credentials and an intentionally invalid filetype.
Disclaimer: this is not the most secure method of checking credentials, however for the purposes of many tasks it can be a perfectly adequate solution.
All the example code for this tutorial has been dumped (quite literally) into a Github repository.
github.com
If you have any improvements, feel free to submit a pull request.