Skip to content

Commit d442c91

Browse files
committed
a ok version
1 parent b033cc1 commit d442c91

File tree

6 files changed

+359
-196
lines changed

6 files changed

+359
-196
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ classifiers = [
2222
]
2323

2424
[tool.poetry.scripts]
25-
simplehttpserver = "simplehttpserver.cli:main"
25+
simplehttpserver = "simplehttpserver.cli:app"
2626

2727
[tool.poetry.dependencies]
2828
python = "^3.8"

simplehttpserver/cli.py

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
import sys
32
from pathlib import Path
43
from typing import Optional
54

@@ -12,54 +11,92 @@
1211
name="simplehttpserver",
1312
help="A modern HTTP file server with web UI",
1413
add_completion=False,
14+
no_args_is_help=True,
1515
)
1616

1717
console = Console()
1818

1919
def version_callback(value: bool):
20+
"""Display version information"""
2021
if value:
2122
from importlib.metadata import version
2223
try:
2324
v = version("simplehttpserver")
2425
console.print(f"[bold]simplehttpserver[/bold] version: {v}")
2526
except:
26-
console.print("[bold]simplehttpserver[/bold] version: 0.1.0")
27+
console.print("[bold]simplehttpserver[/bold] version: 0.0.0")
2728
raise typer.Exit()
2829

30+
@app.command(help="Start the HTTP file server")
2931
def main(
30-
root: str = ".",
31-
host: str = "127.0.0.1",
32-
port: int = 8000,
33-
version: Optional[bool] = None,
32+
root: Path = typer.Option(
33+
Path("."),
34+
"--root",
35+
"-r",
36+
help="Root directory path, defaults to current directory",
37+
exists=True,
38+
dir_okay=True,
39+
file_okay=False,
40+
),
41+
port: int = typer.Option(
42+
8000,
43+
"--port",
44+
"-p",
45+
help="Server port, defaults to 8000",
46+
min=1,
47+
max=65535,
48+
),
49+
host: str = typer.Option(
50+
"127.0.0.1",
51+
"--host",
52+
help="Server host address, defaults to 127.0.0.1",
53+
),
54+
reload: bool = typer.Option(
55+
False,
56+
"--reload",
57+
help="Enable developer mode with automatic code reloading",
58+
),
59+
version: Optional[bool] = typer.Option(
60+
None,
61+
"--version",
62+
"-v",
63+
help="Show version information",
64+
is_eager=True,
65+
callback=version_callback,
66+
),
3467
):
3568
"""
36-
Start the HTTP file server with web UI
37-
"""
38-
if version:
39-
version_callback(version)
40-
return
69+
A modern HTTP file server with web UI
4170
42-
# 设置根目录环境变量
43-
root_path = Path(root).absolute()
71+
Features:
72+
- File upload and download
73+
- File preview (supports images and text files)
74+
- File management (delete, rename, etc.)
75+
- Beautiful web interface
76+
"""
77+
# Set root directory environment variable
78+
root_path = root.absolute()
4479
os.environ["FILE_SERVER_ROOT"] = str(root_path)
4580

46-
# 显示服务器信息
81+
# Display server information
4782
console.print(Panel.fit(
4883
f"[bold green]Starting server at[/bold green]\n"
4984
f"[bold]http://{host}:{port}[/bold]\n"
5085
f"[bold blue]Root directory:[/bold blue] {os.environ['FILE_SERVER_ROOT']}\n"
86+
f"[bold yellow]Developer mode:[/bold yellow] {'enabled' if reload else 'disabled'}\n"
5187
"\n[dim]Press Ctrl+C to quit[/dim]",
5288
title="Simple HTTP Server",
5389
border_style="blue",
5490
))
5591

56-
# 启动服务器
92+
# Start the server
5793
uvicorn.run(
5894
"simplehttpserver.main:app",
5995
host=host,
6096
port=port,
97+
reload=reload,
6198
log_level="info",
6299
)
63100

64101
if __name__ == "__main__":
65-
typer.run(main)
102+
app()

simplehttpserver/main.py

Lines changed: 10 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,180 +1,21 @@
1-
from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form
2-
from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
3-
from fastapi.staticfiles import StaticFiles
1+
from fastapi import FastAPI
42
from pathlib import Path
5-
import mimetypes
6-
import shutil
73
import os
4+
from .routes.api import router as api_router
5+
from .routes.page import router as page_router, init_static_files
86

97
app = FastAPI(title="File Browser")
108

119
# 从环境变量或默认值获取根目录
1210
ROOT_DIR = Path(os.getenv("FILE_SERVER_ROOT", "./files"))
1311
ROOT_DIR.mkdir(parents=True, exist_ok=True)
1412

15-
# 获取当前模块的路径
16-
PACKAGE_DIR = Path(__file__).parent
13+
# 设置 ROOT_DIR 到 app.state 中,以便在路由中使用
14+
app.state.ROOT_DIR = ROOT_DIR
1715

18-
# API routes
19-
@app.get("/api/files")
20-
async def list_files(path: str = ""):
21-
"""List files and directories at the given path"""
22-
try:
23-
target_path = ROOT_DIR / path
24-
if not target_path.exists():
25-
return {"error": "Path not found"}
26-
27-
items = []
28-
for item in target_path.iterdir():
29-
try:
30-
is_dir = item.is_dir()
31-
item_info = {
32-
"name": item.name,
33-
"path": str(item.relative_to(ROOT_DIR)),
34-
"type": "directory" if is_dir else "file",
35-
"size": 0 if is_dir else item.stat().st_size,
36-
"download_url": f"/download/{item.relative_to(ROOT_DIR)}"
37-
}
38-
items.append(item_info)
39-
except Exception:
40-
continue
16+
# 初始化静态文件服务
17+
init_static_files(app)
4118

42-
# 按目录优先、名字升序排序
43-
items.sort(key=lambda x: (x["type"] != "directory", x["name"].lower()))
44-
45-
return {
46-
"items": items,
47-
"current_path": path
48-
}
49-
except Exception as e:
50-
return {"error": str(e)}
51-
52-
@app.get("/download/{file_path:path}")
53-
async def download_file(file_path: str):
54-
"""Download file"""
55-
try:
56-
file_path = 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"
64-
)
65-
except Exception as e:
66-
return {"error": str(e)}
67-
68-
@app.put("/api/files/{file_path:path}")
69-
async def update_file(file_path: str, request: Request):
70-
"""Update file content"""
71-
try:
72-
data = await request.json()
73-
content = data.get("content", "")
74-
75-
file_path = ROOT_DIR / file_path
76-
if not file_path.exists():
77-
return {"error": "File not found"}
78-
79-
file_path.write_text(content)
80-
return {"message": "File updated successfully"}
81-
except Exception as e:
82-
return {"error": str(e)}
83-
84-
@app.delete("/api/files/{file_path:path}")
85-
async def delete_file(file_path: str):
86-
"""Delete file or directory"""
87-
try:
88-
target_path = ROOT_DIR / file_path
89-
if not target_path.exists():
90-
return {"error": "File or directory not found"}
91-
92-
if target_path.is_file():
93-
target_path.unlink()
94-
else:
95-
import shutil
96-
shutil.rmtree(target_path)
97-
98-
return {"message": "Deleted successfully"}
99-
except Exception as e:
100-
return {"error": str(e)}
101-
102-
@app.post("/api/upload/{path:path}")
103-
async def upload_files(path: str, files: list[UploadFile] = File(...), paths: list[str] = Form(...)):
104-
"""Upload files to the specified path"""
105-
try:
106-
target_path = ROOT_DIR / path
107-
108-
uploaded_files = []
109-
for file, relative_path in zip(files, paths):
110-
try:
111-
# 将 Windows 路径分隔符转换为 POSIX 格式
112-
relative_path = relative_path.replace("\\", "/")
113-
114-
# 构建完整的目标路径
115-
file_path = target_path / relative_path
116-
117-
# 确保父目录存在
118-
file_path.parent.mkdir(parents=True, exist_ok=True)
119-
120-
# 如果文件已存在,添加数字后缀
121-
original_path = file_path
122-
counter = 1
123-
while file_path.exists():
124-
stem = original_path.stem
125-
suffix = original_path.suffix
126-
file_path = original_path.parent / f"{stem}_{counter}{suffix}"
127-
counter += 1
128-
129-
# 保存文件
130-
with open(file_path, "wb") as f:
131-
shutil.copyfileobj(file.file, f)
132-
133-
uploaded_files.append({
134-
"name": file_path.name,
135-
"path": str(file_path.relative_to(ROOT_DIR)),
136-
"size": file_path.stat().st_size,
137-
"download_url": f"/download/{file_path.relative_to(ROOT_DIR)}"
138-
})
139-
except Exception as e:
140-
raise HTTPException(status_code=500, detail=f"Failed to upload {relative_path}: {str(e)}")
141-
142-
return {"files": uploaded_files}
143-
except Exception as e:
144-
raise HTTPException(status_code=500, detail=str(e))
145-
146-
# Mount static files for direct access to static assets
147-
app.mount("/static", StaticFiles(directory=PACKAGE_DIR / "static"), name="static")
148-
149-
# Serve index.html for the root path
150-
@app.get("/", response_class=HTMLResponse)
151-
async def serve_root():
152-
"""Serve index.html"""
153-
return (PACKAGE_DIR / "static/index.html").read_text(encoding="utf-8")
154-
155-
# Redirect /blob/{path} to index.html for client-side routing
156-
@app.get("/blob/{path:path}", response_class=HTMLResponse)
157-
async def serve_blob_path(path: str):
158-
"""Serve index.html for blob paths"""
159-
return (PACKAGE_DIR / "static/index.html").read_text(encoding="utf-8")
160-
161-
@app.get("/raw/{file_path:path}")
162-
async def get_raw_file(file_path: str):
163-
"""Get raw file content"""
164-
try:
165-
file_path = ROOT_DIR / file_path
166-
if not file_path.exists() or not file_path.is_file():
167-
raise HTTPException(status_code=404, detail="File not found")
168-
169-
# 获取文件的 MIME 类型
170-
mime_type, _ = mimetypes.guess_type(str(file_path))
171-
if mime_type is None:
172-
mime_type = "application/octet-stream"
173-
174-
return FileResponse(
175-
path=file_path,
176-
media_type=mime_type,
177-
filename=file_path.name
178-
)
179-
except Exception as e:
180-
raise HTTPException(status_code=500, detail=str(e))
19+
# 包含路由
20+
app.include_router(api_router)
21+
app.include_router(page_router)

0 commit comments

Comments
 (0)