Flask based File Hosting (web app & api & python module & cli app)
This guide will walk you through creating a basic file hosting web application using Flask, a lightweight web framework for Python. The application will include features such as user login, file uploads, and file listing. We'll also explore adding a simple API for interacting with the application.
Prerequistes¶
- python >=3.9
Setup Environment¶
- Create a
requirements.txtfile:
- Set up a Python virtual environment:
Open your terminal and follow these steps
Constants Configuration¶
Create a constants.py file:
import os
from pathlib import Path
from dotenv import load_dotenv
UPLOAD_FOLDER = Path('uploads')
UPLOAD_FOLDER.mkdir(exist_ok=True)
DATA_FILE = Path('data.json')
DEBUG = False # Set this to True in the development environment
# Load environment variables from .env file
load_dotenv()
if not DEBUG:
USERNAME = os.getenv("S_USERNAME")
PASSWORD = os.getenv("S_PASSWORD")
FLASK_SECRET_KEY = os.getenv("FLASK_SECRET_KEY")
else:
USERNAME = "my-username"
PASSWORD = "my-password"
FLASK_SECRET_KEY = 'my-secret-key'
assert USERNAME
assert PASSWORD
assert FLASK_SECRET_KEY
Creating the Flask App¶
Create a file named app.py and set up the initial Flask app:
from datetime import datetime, timedelta
from functools import wraps
import json
import os
import mimetypes
from slugify import slugify
from flask import Flask, render_template, request, redirect, send_from_directory
from flask import session, jsonify
import constants
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = constants.UPLOAD_FOLDER
app.config['DATA_FILE'] = constants.DATA_FILE
app.config['SECRET_KEY'] = constants.FLASK_SECRET_KEY
app.config['DEBUG'] = constants.DEBUG
app.config['USERNAME'] = constants.USERNAME
app.config['PASSWORD'] = constants.PASSWORD
(Optional) Simple Hello World App¶
Replace the contents of app.py with a basic "Hello, World!" Flask app:
# ... (Previous Code: Initial Setup)
@app.route('/')
def hello():
return 'Hello, World!'
if __name__ == '__main__':
app.run(debug=True)
Run the app using:
Visit http://127.0.0.1:5000/ to see the "Hello, World!" message.
Adding a Web Page to List Uploaded Files¶
-
Create a
templatefolder and download the index.html file into it. -
Modify the Flask app in
app.pyto include the file listing:
# ... (Initial Setup)
@app.route('/')
def index():
list_files = os.listdir(app.config['UPLOAD_FOLDER'])
files = [(filename, "") for filename in list_files]
return render_template('index.html', files=files)
if __name__ == '__main__':
if not os.path.exists(app.config['UPLOAD_FOLDER']):
os.makedirs(app.config['UPLOAD_FOLDER'])
app.run()
Ensure the 'uploads' folder contains some files, then run the app to see the list.
Adding a Login Page¶
-
Download the login.html file into the templates folder.
-
Add login-related functions to
app.py:
# ... (Previous code)
def validate_credentials(username, password):
res = (username == app.config['USERNAME'] and password == app.config['PASSWORD'])
session['logged_in'] = res
return res
def is_logged_in():
return 'logged_in' in session and session['logged_in']
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not is_logged_in():
session['previous_url'] = request.url
return redirect('/login')
return f(*args, **kwargs)
return decorated_function
Here is how it works
-
login_requiredis a wrapper that use the functionis_logged_into check if a user is logged in -
validate_credentialscheck if theusernameandpasswordsent by the user match those we have fromconstants.py
Now, we will create the login page and add the login wrapper to the home page
@app.route('/')
@login_required
def index():
list_files = os.listdir(app.config['UPLOAD_FOLDER'])
files = [(filename, "") for filename in list_files]
return render_template('index.html', files=files)
@app.route('/login', methods=['GET', 'POST'])
def login():
sucessful_login_redirect = lambda : redirect(session.pop('previous_url') if 'previous_url' in session else "\\")
default_login_render = lambda : render_template('login.html')
if is_logged_in():
return sucessful_login_redirect()
if request.method != 'POST':
return default_login_render()
username = request.form['username']
password = request.form['password']
if validate_credentials(username, password):
return sucessful_login_redirect()
return default_login_render()
Here is how it works
- The login page use the template
index.html, a form with two fields:usernameandpassword. But if he is already logged in, he may be redirected to another page. - When he add his credentials and send them, we will get
usernameandpasswordfrom the form. - Then we will use the function
validate_credentialsto check them. - If the credentials match, the user is redirected to the page he asks for. For example, in an unauthenticated user go the the home page, he is redirected to the login page. And if his crede,tials match, he is redirected back to the home page.
create your credentials to access the app¶
As i've 've said, i use the most basic authentication for this simple login protected web app.To add the credentials for the web app, create a file .env like .env.example
.env
Warning
Modify it to match the credentials for your app
Also, go into the constants.py file and make sure DEBUG is set to False to use it
(Optional) Session Timeout and Logout Page¶
You can add a timeout of the session. So a user will not stay logged in forever. It is important for data sentivive related apps. You can also let the user logout if he wants. There is a logout bouton in the home page.
-
Set the session timeout:
-
Add a logout page:
Adding File Download and Upload Features¶
-
Add functions for file download:
-
Add functions for file upload:
# ... (Previous code) @app.route('/uploads/<path:filename>') def download(filename): return send_from_directory(app.config['UPLOAD_FOLDER'], filename) def slugify_filename(filename): # Split the filename and extension _ = filename.rsplit('.', 1) if len(_)<2: return base, extension = _ # Slugify the base part slug_base = slugify(base) # Join the slugified base with the original extension slug_filename = f"{slug_base}.{extension}" return slug_filename def handle_file_saving(file): filename = slugify_filename(file.filename) file_save = app.config['UPLOAD_FOLDER'] / filename print(f"saving {file_save.resolve()}") file.save(file_save) return filename @app.route('/upload', methods=['POST']) @login_required def upload(): file = request.files['file'] if file: filename = handle_file_saving(file) return redirect('/')
Here is how it works
We have added a upload endpoint
- that will receive user files and save them using the
handle_file_savingfunction - that is protected with
login_required - The function
slugify_filenamewill rewrite the filename to use only lowercase alphanumeric characters an-as separators instead of space handle_file_savingwill save the file in theuploadsdirectory
The complete code with file download and upload features can be found in the GitHub repository.
(Optional) Adding Endpoints for Open File, Raw Content, and API¶
-
Add endpoints for opening a file, displaying raw content, and API:
# ... (Previous code) @app.route('/open/<path:filename>') @login_required def open_file(filename): file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) if not os.path.exists(file_path): return "File not found" mime_type = get_content_type(file_path) # Map .md and .mmd extensions to text/plain if mime_type == 'text/markdown' or mime_type == 'text/x-markdown': mime_type = 'text/plain' if mime_type: with open(file_path, 'rb') as file: file_content = file.read() return Response(file_content, content_type=mime_type) return "Unknown file type" @app.route('/raw/<path:filename>') @login_required def raw_file(filename): file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) if not os.path.exists(file_path): return "File not found" with open(file_path, 'rb') as file: file_content = file.read() return file_content
The code for these features is available in the GitHub repository.
(Optional) Modifying File Upload to Filter Files¶
To filter the files, you can use a database to add which files to show. To be simple, i've used a json file.
So the
- home page will look into the json file to show the files
-
the upload page will save the file on the server and also add it in the json file
-
Modify the listing feature to filter files using a JSON file:
def load_data_from_json(): if os.path.exists(app.config['DATA_FILE']): with open(app.config['DATA_FILE'], 'r') as file: try: return json.load(file) except json.JSONDecodeError: pass return {} def get_files_with_dates(): data = load_data_from_json() return [(filename, data[filename]) for filename in sorted(data, key=data.get) if (app.config['UPLOAD_FOLDER']/filename).exists()] @app.route('/') @login_required def index(): files = get_files_with_dates() return render_template('index.html', files=files) -
Modify the upload feature to filter files using a JSON file:
def update_data_file(filename): data = load_data_from_json() data[filename] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(app.config['DATA_FILE'], 'w') as file: json.dump(data, file) def handle_file_saving(file): filename = slugify_filename(file.filename) file_save = app.config['UPLOAD_FOLDER'] / filename print(f"saving {file_save.resolve()}") file.save(file_save) update_data_file(filename) return filename @app.route('/upload', methods=['POST']) @login_required def upload(): file = request.files['file'] if file: filename = handle_file_saving(file) return redirect('/')
The complete code with file filtering and other features is available in the GitHub repository.
(Optional) Adding an api along the web page¶
-
login
-
get all the files
-
upload a file
@app.route('/api/upload', methods=['POST']) def api_upload(): if not is_logged_in(): return jsonify({'message': 'Unauthorized'}), 401 file = request.files['file'] if file: filename = handle_file_saving(file) return jsonify({'message': f'File uploaded: {filename}'}) else: return jsonify({'message': 'No file provided'}), 400 -
download a file
(Bonus) How to use the api¶
- You can access the api with the routes
http://localhost:5000/api/* - The file cli_app/cli_app.py to access the api along with a context manager to handle sessions
- you can read the api documentation
(Bonus) How to use the cli app¶
- The script cli_app/sharefile.py provides a cli app to access the api context manager
- Using your cli, you can list, upload and download files. The api will be called behind the hood by cli_app/cli_app.py
- you can read the cli-app documentation
(Bonus) Serving Static Files¶
If you want to serve static files, add the following endpoint:
@app.route('/<path:filename>')
def static_files(filename):
return send_from_directory('static', filename)
Create a 'static' folder and place your static files inside it.
It can be interesting for custom css/js files and others
You can find all this code in the repository https://github.com/Hermann-web/simple-file-hosting-with-flask