feat: download - fase 02

This commit is contained in:
kodi
2026-03-14 12:40:41 +01:00
parent 610a648fd1
commit dab87878cc
9 changed files with 215 additions and 45 deletions
@@ -4,6 +4,8 @@ import asyncio
import sys
import tempfile
import unittest
import zipfile
from io import BytesIO
from pathlib import Path
import httpx
@@ -55,6 +57,75 @@ class DownloadApiGoldenTest(unittest.TestCase):
def test_download_directory_type_conflict(self) -> None:
(self.root / "docs").mkdir()
(self.root / "docs" / "a.txt").write_text("a", encoding="utf-8")
response = self._get("/api/files/download?path=storage1/docs")
self.assertEqual(response.status_code, 200)
self.assertIn('attachment; filename="docs.zip"', response.headers.get("content-disposition", ""))
with zipfile.ZipFile(BytesIO(response.content)) as archive:
self.assertIn("docs/", archive.namelist())
self.assertIn("docs/a.txt", archive.namelist())
self.assertEqual(archive.read("docs/a.txt"), b"a")
def test_download_multi_file_selection_as_zip(self) -> None:
(self.root / "a.txt").write_text("A", encoding="utf-8")
(self.root / "b.txt").write_text("B", encoding="utf-8")
response = self._get("/api/files/download?path=storage1/a.txt&path=storage1/b.txt")
self.assertEqual(response.status_code, 200)
self.assertRegex(
response.headers.get("content-disposition", ""),
r'attachment; filename="kodidownload-\d{8}-\d{6}\.zip"',
)
with zipfile.ZipFile(BytesIO(response.content)) as archive:
self.assertIn("a.txt", archive.namelist())
self.assertIn("b.txt", archive.namelist())
self.assertEqual(archive.read("a.txt"), b"A")
self.assertEqual(archive.read("b.txt"), b"B")
def test_download_multi_directory_selection_as_zip(self) -> None:
(self.root / "dir1" / "sub").mkdir(parents=True)
(self.root / "dir2").mkdir()
(self.root / "dir1" / "sub" / "a.txt").write_text("A", encoding="utf-8")
(self.root / "dir2" / "b.txt").write_text("B", encoding="utf-8")
response = self._get("/api/files/download?path=storage1/dir1&path=storage1/dir2")
self.assertEqual(response.status_code, 200)
self.assertRegex(
response.headers.get("content-disposition", ""),
r'attachment; filename="kodidownload-\d{8}-\d{6}\.zip"',
)
with zipfile.ZipFile(BytesIO(response.content)) as archive:
self.assertIn("dir1/", archive.namelist())
self.assertIn("dir1/sub/", archive.namelist())
self.assertIn("dir1/sub/a.txt", archive.namelist())
self.assertIn("dir2/b.txt", archive.namelist())
def test_download_mixed_file_and_directory_selection_as_zip(self) -> None:
(self.root / "readme.txt").write_text("R", encoding="utf-8")
(self.root / "photos" / "nested").mkdir(parents=True)
(self.root / "photos" / "nested" / "img.txt").write_text("P", encoding="utf-8")
response = self._get("/api/files/download?path=storage1/readme.txt&path=storage1/photos")
self.assertEqual(response.status_code, 200)
self.assertRegex(
response.headers.get("content-disposition", ""),
r'attachment; filename="kodidownload-\d{8}-\d{6}\.zip"',
)
with zipfile.ZipFile(BytesIO(response.content)) as archive:
self.assertIn("readme.txt", archive.namelist())
self.assertIn("photos/", archive.namelist())
self.assertIn("photos/nested/img.txt", archive.namelist())
def test_download_directory_with_symlink_rejected(self) -> None:
target = self.root / "real.txt"
target.write_text("x", encoding="utf-8")
(self.root / "docs").mkdir()
(self.root / "docs" / "link.txt").symlink_to(target)
response = self._get("/api/files/download?path=storage1/docs")