PROD upload: file mtime metadata drives date/time, optional date override, collision suffixes, path traversal guard

This commit is contained in:
2026-08-28 15:27:00 +01:00
parent 780544d002
commit 9d4f12e5df
2 changed files with 57 additions and 25 deletions
+27 -13
View File
@@ -151,26 +151,35 @@ class PlayerHandler(BaseHTTPRequestHandler):
max_bytes = 2 * 1024 * 1024 * 1024
if not 0 < length <= max_bytes:
return self.send_error(413, "Missing or oversized body (limit 2 GiB)")
filename = parse_qs(urlparse(self.path).query).get("filename", [""])[0]
match = VIDEO_RE.fullmatch(filename)
if not match:
return self.send_error(400, "Filename must be HH.MM.SS.mp4 or HH.MM.SS.avi (e.g. 09.28.36.mp4)")
hour, minute, second = map(int, match.groups()[:3])
if hour > 23 or minute > 59 or second > 59:
return self.send_error(400, "Invalid time in filename")
params = parse_qs(urlparse(self.path).query)
filename = params.get("filename", [""])[0]
if not re.fullmatch(r"[A-Za-z0-9._ -]{1,120}\.(mp4|avi)", filename, re.IGNORECASE):
return self.send_error(400, "filename must be a simple MP4/AVI name")
suffix = filename.rsplit(".", 1)[1].lower()
try:
day = normalize_date(parse_qs(urlparse(self.path).query).get("date", [""])[0])
day = normalize_date(params.get("date", [""])[0])
except ValueError:
return self.send_error(400, "date must be YYYYMMDD or YYYY-MM-DD")
suffix = match.group(4).lower()
if suffix not in ("mp4", "avi"):
return self.send_error(400, "Only .mp4 or .avi uploads are accepted")
try:
start = parse_clock(params.get("time", [""])[0])
if not (0 <= start < 86400):
raise ValueError
except ValueError:
return self.send_error(400, "time must be HH:MM[:SS] within one day")
target_dir = self.server.output_root / day
if suffix == "avi":
# AVI sources mirror the real recorder: stored in the source tree for the Encoding menu
target_dir = self.server.source_root / day
target = target_dir / f"{hour:02d}.{minute:02d}.{second:02d}.{suffix}"
# Archive-canonical name from the actual start time; collisions with a
# different original name get a numeric suffix.
base = f"{start // 3600:02d}.{start % 3600 // 60:02d}.{start % 60:02d}"
target = target_dir / f"{base}.{suffix}"
serial = 1
while target.exists() or temporary_exists(target_dir, target.name):
base_alt = f"{base}-{serial:02d}"
target = target_dir / f"{base_alt}.{suffix}"
serial += 1
temporary = target.with_suffix(f".uploading.{suffix}")
try:
target_dir.mkdir(parents=True, exist_ok=True)
@@ -193,7 +202,7 @@ class PlayerHandler(BaseHTTPRequestHandler):
if suffix == "mp4" and not any(clip["name"] == target.name for clip in self.server.recordings[day]):
self.server.recordings[day].append({
"name": target.name,
"start": hour * 3600 + minute * 60 + second,
"start": start,
"duration": 120,
"path": target,
})
@@ -332,6 +341,11 @@ class PlayerHandler(BaseHTTPRequestHandler):
print(f"{self.client_address[0]} - {message % args}")
def temporary_exists(directory, final_name):
stem = final_name.rsplit(".", 1)[0]
return any(path.name.startswith(stem + ".uploading.") for path in directory.glob("*.uploading.*")) if directory.is_dir() else False
def main():
parser = argparse.ArgumentParser(description="Play dated recorder AVI files in a browser")
parser.add_argument(