Search code examples
pythonflaskdownloadjinja2wtforms

Flask WTForms application creates file to download. Users getting someone else's file (same name). How to fix?


My application creates a file in the server from input on a WTForms. The file name never changes. As a new user comes in, fills out the form and and the old file is overwritten. Problem is when 2 users happen to use the application simultaneously and get served the wrong file (Time between creating file and downloading file may sometimes be 1 or 2 minutes). I want to avoid this problem. Here is my code and folder structure:

  • FlaskApp

    • modules

         FileCreate.py
      
         Forms.py   
      
    • templates

         InputPage.html
      
    • WebApp.py

WebApp.py:

from flask import Flask, render_template, send_file, request
from modules import Forms, FileCreate
app = Flask(__name__)

@app.route("/index", methods=["GET", "POST"])
def vFileCreate():
    form = Forms.clInputFromUser()
    if form.validate_on_submit() and request.method == "POST":
        listdata = form.stringdata.data
        userdata = FileCreate.clFileCreate(listdata, form)
        userdata.file_save()
        return render_template("InputPage.html", form=form, userdata=userdata)
    else:
        return render_template("InputPage.html")
if __name__ == '__main__':
   app.run(debug=True)

@app.route("/download_file")
def vDownloadFile():
    PATH = "UserReport.txt"
    return send_file(PATH, as_attachment=True, download_name="Sample_Report.txt")

Forms.py:

from flask_wtf import FlaskForm

class clInputFromUser(FlaskForm):
    stringdata = TextAreaField("Paste Column of Data Here: ")
    submit = SubmitField("Analyze Data and Create File")

FileCreate.py:

class clFileCreate:
    def __init__(self, listdata, form):
        self.formdata = form
        self.data = formdata.stringdata.data
        fp = open('UserReport.txt', 'w')
        fp.write(self.data)
        fp.close()

InputPage.html:

<!DOCTYPE html>
<html>
<head>
    <title>Form Page</title>
</head>
<body>
    <form method="POST">
        {{ form.hidden_tag() }}
        <div>
            {{ form.stringdata.label }}<br>
            {{ form.stringdata(size=32) }}
        </div>
        <div>
            {{ form.submit() }}
        </div>
    </form>
    <a href="{{ url_for('vDownloadFile') }}"><button type="button" class="btn btn-success">Download Report</button></a>
</body>
</html>

What options do I have to make sure the correct file is served to the user that just filled out the form. Due to restrictions no database, no CRON jobs on the server to clean files, no identification of user.

Idea1: I could rerun the class just before the Download but this needs the form to submit again and I do not know how to make that work.

Idea2: I could stream the file but I do not know how to move the stream variable to the download function in the download_file route since they are 2 different functions.

Idea3: I could identify the session, I do not know if this is possible, and ensure the user gets the correct file by making the file name unique to the session, but then I do not know how to delete the file after download or pass the unique file name to the download_file function from the class. (see restrictions).

Any ideas?

NOTE: This code is a sample to show the structure. I did not test it since the question is more about concept of how to approach the issue and not specific code.


Solution

  • Maybe the APScheduler is an alternative to using a cron job. To save you work, I recommend the Flask-APScheduler package.

    The following example creates a user folder whose unique name is stored in the session store. Every hour the scheduler deletes all folders that have not been modified for over an hour. The data can therefore be assigned to each anonymous user and can be downloaded by them within a period of at least one hour. A database is not required.

    from flask import (
        Flask, 
        redirect, 
        render_template, 
        request,
        send_from_directory,  
        session, 
        url_for
    )
    from flask_apscheduler import APScheduler 
    from flask_wtf import FlaskForm
    from wtforms import SubmitField, TextAreaField
    from wtforms.validators import Length
    import os, shutil, time, uuid
    
    app = Flask(__name__)
    app.config.from_mapping(
        SECRET_KEY='your secret here', 
        SCHEDULER_API_ENABLED = True, 
        UPLOAD_FOLDER=os.path.join(app.instance_path, 'uploads')
    )
    scheduler = APScheduler()
    scheduler.init_app(app)
    scheduler.start()
    
    os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
    
    @scheduler.task('interval', id='prune_job', hours=1, args=(app.config['UPLOAD_FOLDER'], ))
    def prune(path, hours=1):
        now = time.time()
        for f in os.listdir(path):
            p = os.path.join(path, f)
            if os.stat(p).st_mtime < now - hours*60*60:
                shutil.rmtree(p, ignore_errors=True)
    
    class ExampleForm(FlaskForm):
        content = TextAreaField('Your data here:', 
            validators=[
                Length(min=4, max=32)
            ]
        )
        submit = SubmitField('Analyze')
    
    @app.route('/', methods=['GET', 'POST'])
    def index():
        if not 'uid' in session:
            session['uid'] = str(uuid.uuid4())
    
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], session['uid'])
    
        form = ExampleForm(request.form)
        if form.validate_on_submit():
            os.makedirs(filepath, exist_ok=True)
    
            filename = 'UserReport.txt'
            with open(os.path.join(filepath, filename), 'w') as f:
                f.write(form.content.data)
    
            return redirect(request.url)
    
        files = os.listdir(filepath) if os.path.exists(filepath) else [] 
        return render_template('index.html', **locals())
    
    @app.route('/download/<path:filename>')
    def download(filename):
        return send_from_directory(
            os.path.join(app.config['UPLOAD_FOLDER'], session.get('uid')), 
            filename, 
            as_attachment=True
        )
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Index</title>
    </head>
    <body>
        <div>
            <form method="POST">
                {{ form.csrf_token }}
                <div>
                    {{ form.content.label() }}
                    {{ form.content() }}
                    {% if form.content.errors -%}
                        <ul>
                            {% for error in form.content.errors -%}
                            <li>{{ error }}</li>
                            {% endfor -%}
                        </ul>
                    {% endif -%}
                </div>
                {{ form.submit() }}
            </form>
        </div>
    
        <div>
            <ul>
                {% for filename in files -%}
                <li><a href="{{ url_for('download', filename=filename) }}" target="_blank">{{ filename }}</a></li>
                {% endfor -%}
            </ul>
        </div>
    </body>
    </html>