File size: 2,922 Bytes
22de727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
"""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())