Skip to content

Commit 0e6bde9

Browse files
author
孙圣翔
committed
add video preview, image left,right arrow
1 parent c3219b5 commit 0e6bde9

File tree

8 files changed

+257
-35
lines changed

8 files changed

+257
-35
lines changed

MANIFEST.in

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
recursive-include servefs/static *
2+
global-exclude *.pyc
3+
global-exclude *.pyo
4+
global-exclude *.pyd
5+
global-exclude __pycache__
6+
global-exclude .git*

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
dev:
22
npx nodemon -e "js html py" --exec "uvicorn servefs.main:app --port 7001"
3+
4+
reload:
5+
uvicorn servefs.main:app --port 7001 --reload
6+
7+
8+
format:
9+
poetry run isort .

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description = "A modern HTTP file server with web UI"
55
authors = ["Your Name <your.email@example.com>"]
66
readme = "README.md"
77
packages = [{include = "servefs"}]
8+
include = ["servefs/static/**/*"]
89
homepage = "https://github.com/codeskyblue/servefs"
910
classifiers = [
1011
"Development Status :: 4 - Beta",

servefs/routes/page.py

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,91 @@
11
import mimetypes
2+
import os
23
from pathlib import Path
4+
from typing import AsyncIterator, Union
35

6+
import aiofiles
47
from fastapi import APIRouter, HTTPException, Request
5-
from fastapi.responses import FileResponse, HTMLResponse
8+
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
69
from fastapi.staticfiles import StaticFiles
710

811
# Get current module path
912
PACKAGE_DIR = Path(__file__).parent.parent
1013

1114
router = APIRouter(tags=["page"])
1215

16+
async def stream_file_range(file_path: Path, start: int, end: int) -> AsyncIterator[bytes]:
17+
"""以块的方式流式读取文件"""
18+
chunk_size = 4 * 1024 * 1024 # 4MB 块大小,提高传输速度
19+
async with aiofiles.open(file_path, mode="rb") as f:
20+
await f.seek(start)
21+
bytes_remaining = end - start + 1
22+
while bytes_remaining > 0:
23+
chunk = await f.read(min(chunk_size, bytes_remaining))
24+
if not chunk:
25+
break
26+
bytes_remaining -= len(chunk)
27+
yield chunk
28+
29+
async def handle_file_request(
30+
file_path: Path,
31+
range_header: str = None,
32+
as_attachment: bool = False
33+
) -> Union[FileResponse, StreamingResponse]:
34+
"""处理文件请求的通用函数"""
35+
try:
36+
if not file_path.exists() or not file_path.is_file():
37+
raise HTTPException(status_code=404, detail="File not found")
38+
39+
file_size = os.path.getsize(file_path)
40+
mime_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
41+
42+
# 如果是下载请求,强制使用 application/octet-stream
43+
if as_attachment:
44+
mime_type = "application/octet-stream"
45+
46+
# 如果没有 Range 头,直接返回完整文件
47+
if not range_header:
48+
headers = {}
49+
if as_attachment:
50+
headers["Content-Disposition"] = f'attachment; filename="{file_path.name}"'
51+
else:
52+
headers["Content-Disposition"] = f'inline; filename="{file_path.name}"'
53+
54+
return FileResponse(
55+
path=file_path,
56+
media_type=mime_type,
57+
headers=headers
58+
)
59+
60+
try:
61+
# 解析 Range 头
62+
start, end = range_header.replace("bytes=", "").split("-")
63+
start = int(start) if start else 0
64+
end = min(int(end), file_size - 1) if end else file_size - 1
65+
66+
if start >= file_size:
67+
raise HTTPException(status_code=416, detail="Range not satisfiable")
68+
69+
headers = {
70+
"Content-Range": f"bytes {start}-{end}/{file_size}",
71+
"Accept-Ranges": "bytes",
72+
"Content-Length": str(end - start + 1),
73+
"Content-Type": mime_type,
74+
"Content-Disposition": "attachment" if as_attachment else "inline" + f'; filename="{file_path.name}"'
75+
}
76+
77+
return StreamingResponse(
78+
stream_file_range(file_path, start, end),
79+
status_code=206,
80+
headers=headers
81+
)
82+
83+
except (ValueError, IndexError):
84+
raise HTTPException(status_code=416, detail="Invalid range header")
85+
86+
except Exception as e:
87+
raise HTTPException(status_code=500, detail=str(e))
88+
1389
# Mount static files for direct access to static assets
1490
def init_static_files(app):
1591
"""Initialize static file serving"""
@@ -29,38 +105,30 @@ async def serve_blob_path(path: str):
29105

30106
@router.get("/raw/{file_path:path}")
31107
async def get_raw_file(file_path: str, request: Request):
32-
"""Get raw file content"""
108+
"""Get raw file content with Range support"""
33109
try:
34110
file_path = request.app.state.ROOT_DIR / file_path
35-
if not file_path.exists() or not file_path.is_file():
36-
raise HTTPException(status_code=404, detail="File not found")
37-
38-
# Get file MIME type
39-
mime_type, _ = mimetypes.guess_type(str(file_path))
40-
if mime_type is None:
41-
mime_type = "application/octet-stream"
42-
43-
return FileResponse(
44-
path=file_path,
45-
media_type=mime_type,
46-
filename=file_path.name
111+
return await handle_file_request(
112+
file_path=file_path,
113+
range_header=request.headers.get("range"),
114+
as_attachment=False
47115
)
48116
except Exception as e:
117+
if isinstance(e, HTTPException):
118+
raise e
49119
raise HTTPException(status_code=500, detail=str(e))
50120

51-
52121
@router.get("/download/{file_path:path}")
53122
async def download_file(file_path: str, request: Request):
54-
"""Download file"""
123+
"""Download file with Range support"""
55124
try:
56125
file_path = request.app.state.ROOT_DIR / file_path
57-
if not file_path.exists() or not file_path.is_file():
58-
return {"error": "File not found"}
59-
60-
return FileResponse(
61-
file_path,
62-
filename=file_path.name,
63-
media_type="application/octet-stream"
126+
return await handle_file_request(
127+
file_path=file_path,
128+
range_header=request.headers.get("range"),
129+
as_attachment=True
64130
)
65131
except Exception as e:
66-
return {"error": str(e)}
132+
if isinstance(e, HTTPException):
133+
raise e
134+
raise HTTPException(status_code=500, detail=str(e))

servefs/static/favicon.ico

139 KB
Binary file not shown.

servefs/static/favicon.webp

25.9 KB
Binary file not shown.

0 commit comments

Comments
 (0)