Skip to content

Commit a76054a

Browse files
committed
Using short link like imgbb
1 parent 4e708ec commit a76054a

3 files changed

Lines changed: 38 additions & 5 deletions

File tree

app/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717
CACHE_MAX_AGE_SECONDS = int(os.getenv("CACHE_MAX_AGE_SECONDS", "3600"))
1818
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin-dev-password")
1919
ADMIN_LOCK_STEP_SECONDS = int(os.getenv("ADMIN_LOCK_STEP_SECONDS", str(5 * 60)))
20+
FILE_ID_LENGTH = max(4, min(32, int(os.getenv("FILE_ID_LENGTH", "7"))))

app/storage.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1+
from __future__ import annotations
2+
13
import os
2-
import uuid
4+
import secrets
5+
import string
36
from datetime import datetime, timedelta
47
from sqlmodel import Session, select
5-
from app.config import UPLOAD_DIR, DELETE_AFTER_HOURS
8+
from app.config import UPLOAD_DIR, DELETE_AFTER_HOURS, FILE_ID_LENGTH
69
from app.models import File
710

811
os.makedirs(UPLOAD_DIR, exist_ok=True)
912

13+
_SLUG_ALPHABET = string.ascii_letters + string.digits
14+
_MAX_SLUG_ATTEMPTS = 5
15+
16+
17+
def _generate_file_id(length: int = FILE_ID_LENGTH) -> str:
18+
return "".join(secrets.choice(_SLUG_ALPHABET) for _ in range(length))
19+
20+
21+
def _reserve_path(ext: str) -> tuple[str, str]:
22+
for _ in range(_MAX_SLUG_ATTEMPTS):
23+
file_id = _generate_file_id()
24+
stored_name = f"{file_id}{ext}"
25+
path = os.path.join(UPLOAD_DIR, stored_name)
26+
if not os.path.exists(path):
27+
return stored_name, path
28+
raise RuntimeError("Unable to allocate a unique file slug")
29+
30+
1031
def save_file(file_bytes: bytes, original_name: str, content_type: str) -> str:
1132
ext = os.path.splitext(original_name)[1] or ".bin"
12-
file_id = str(uuid.uuid4())
13-
stored_name = f"{file_id}{ext}"
14-
path = os.path.join(UPLOAD_DIR, stored_name)
33+
stored_name, path = _reserve_path(ext)
1534
with open(path, "wb") as f:
1635
f.write(file_bytes)
1736
return stored_name, len(file_bytes)

tests/test_app.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import importlib
2+
import re
23
import sys
34
from datetime import datetime, timedelta
45
from pathlib import Path
@@ -71,6 +72,18 @@ def test_upload_list_serve_and_cache_headers(client):
7172
assert serve_response.headers["Cache-Control"] == "public, max-age=120"
7273

7374

75+
def test_upload_slug_is_short(client):
76+
response = client.post("/upload", files={"file": ("slug.txt", b"x", "text/plain")})
77+
assert response.status_code == 200
78+
payload = response.json()
79+
from app import config as app_config
80+
81+
slug = payload["id"]
82+
assert len(slug) == app_config.FILE_ID_LENGTH
83+
assert re.fullmatch(rf"[A-Za-z0-9]{{{app_config.FILE_ID_LENGTH}}}", slug)
84+
assert payload["url"].split("/")[-1].startswith(slug)
85+
86+
7487
def test_directory_traversal_blocked(client):
7588
outside_file = client.upload_dir.parent / "top_secret.txt" # type: ignore[attr-defined]
7689
outside_file.write_text("nope")

0 commit comments

Comments
 (0)