MiniWeb-demo / src /miniweb /app /uploads.py
sparklabuofu's picture
Update the demo
22de727 verified
Raw History Blame Contribute Delete
2.92 kB
"""Files a visitor's requests leave on disk, kept apart for each session.
A few sites keep a file rather than a row: password-managers stores entry icons, NoteCanvas
(handwritten-notes-whiteboards) draws the images of its notes. Each session gets its own folder,
MINIWEB_UPLOADS_DIR/<session id>/<area>/ (data/uploads by default, a folder used for nothing else), so
- a visitor, or one of several agents evaluated on one server as the same user, never overwrites or sees another
session's files: the URL stored in the data stays the same for everyone, and each session is served its own file;
- a file name sent by the browser cannot reach outside the folder;
- the files go with the session's data (db.reset_session, db.evict_stale_sessions, db.reset_all).
"""
import os
import re
import shutil
from werkzeug.exceptions import BadRequest, NotFound
from werkzeug.utils import secure_filename
from helpers.paths import REPO_ROOT
ROOT = REPO_ROOT / os.environ.get("MINIWEB_UPLOADS_DIR", "data/uploads")
_SID = re.compile(r"[\w-]{1,64}") # db._get_session_id makes 12 hex digits and dashes
def _folder(sid, area):
if not _SID.fullmatch(sid or ""):
raise BadRequest("bad session")
return ROOT / sid / area
def path(area, name):
"""Where this session keeps the file `name` of `area` (a site, or "generated"); the folder is created. The name
loses any folder part and unsafe characters."""
from miniweb.app import db
safe = secure_filename(name or "")
if not safe:
raise BadRequest("bad file name")
folder = _folder(db._get_session_id(), area)
folder.mkdir(parents=True, exist_ok=True)
return folder / safe
def find(area, name):
"""This session's file `name` of `area`, or None. Never starts a session."""
from flask import has_request_context, session
sid = session.get("_data_overlay_sid") if has_request_context() else None
if not sid or not name or secure_filename(name) != name or not _SID.fullmatch(sid):
return None
file = ROOT / sid / area / name
return file if file.is_file() else None
def send(area, name):
"""Serve this session's file `name` of `area`; 404 when the session has none."""
from flask import send_file
file = find(area, name)
if file is None:
raise NotFound()
response = send_file(file, max_age=0)
# an uploaded SVG must not run scripts, and a browser must not read a file as another type
response.headers["Content-Security-Policy"] = "sandbox"
response.headers["X-Content-Type-Options"] = "nosniff"
return response
def drop(sids):
"""Delete the files of these sessions."""
for sid in sids:
if _SID.fullmatch(sid or ""):
shutil.rmtree(ROOT / sid, ignore_errors=True)
def drop_all():
"""Delete the files of every session."""
if ROOT.is_dir():
drop(entry.name for entry in ROOT.iterdir() if entry.is_dir())