Bot commited on
Commit
b1853e0
·
1 Parent(s): 27845ec

Update to FastAPI download server

Browse files
Files changed (3) hide show
  1. Dockerfile +8 -18
  2. app.py +129 -0
  3. requirements.txt +4 -0
Dockerfile CHANGED
@@ -1,24 +1,14 @@
1
- FROM ghcr.io/livebook-dev/livebook:latest-cuda12
2
 
3
- ENV LIVEBOOK_APP_SERVICE_NAME="🐳 Hugging Face - $SPACE_TITLE"
4
- ENV LIVEBOOK_APP_SERVICE_URL="https://huggingface.co/spaces/$SPACE_AUTHOR_NAME/$SPACE_REPO_NAME"
5
- ENV LIVEBOOK_UPDATE_INSTRUCTIONS_URL="https://livebook.dev"
6
- ENV LIVEBOOK_WITHIN_IFRAME="true"
7
- ENV LIVEBOOK_APPS_PATH="/public-apps"
8
- ENV LIVEBOOK_APPS_PATH_WARMUP="manual"
9
- ENV LIVEBOOK_DATA_PATH="/data"
10
- ENV LIVEBOOK_PORT="7860"
11
 
12
- EXPOSE 7860
 
13
 
14
- RUN mkdir -p /data
15
- RUN chmod 777 /data
16
 
17
- # The Space container runs with user ID 1000, which corresponds to
18
- # ubuntu in the Livebook image.
19
- ENV HOME="/home/ubuntu"
20
 
21
- USER ubuntu
22
 
23
- COPY --chown=ubuntu public-apps/ /public-apps
24
- RUN /app/bin/warmup_apps
 
1
+ FROM python:3.10-slim
2
 
3
+ WORKDIR /app
 
 
 
 
 
 
 
4
 
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
 
8
+ COPY app.py .
 
9
 
10
+ RUN mkdir -p /data && chmod 777 /data
 
 
11
 
12
+ EXPOSE 7860
13
 
14
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
app.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import time
4
+ import uuid
5
+ import aiohttp
6
+ from fastapi import FastAPI, BackgroundTasks, Request
7
+ from fastapi.responses import FileResponse, StreamingResponse
8
+ from pydantic import BaseModel
9
+
10
+ app = FastAPI()
11
+
12
+ # In-memory store for download tasks
13
+ # tasks = { "task_id": { "url": str, "status": "downloading"|"completed"|"error", "total_size": int, "downloaded": int, "speed": float, "file_path": str } }
14
+ tasks = {}
15
+
16
+ DATA_DIR = "/data"
17
+ os.makedirs(DATA_DIR, exist_ok=True)
18
+
19
+ class DownloadRequest(BaseModel):
20
+ url: str
21
+
22
+ async def download_file(task_id: str, url: str):
23
+ file_path = os.path.join(DATA_DIR, f"{task_id}.bin")
24
+ tasks[task_id] = {
25
+ "url": url,
26
+ "status": "downloading",
27
+ "total_size": 0,
28
+ "downloaded": 0,
29
+ "speed": 0.0,
30
+ "file_path": file_path
31
+ }
32
+
33
+ try:
34
+ async with aiohttp.ClientSession() as session:
35
+ async with session.get(url) as response:
36
+ response.raise_for_status()
37
+ total_size = int(response.headers.get('Content-Length', 0))
38
+ tasks[task_id]["total_size"] = total_size
39
+
40
+ downloaded = 0
41
+ start_time = time.time()
42
+ last_time = start_time
43
+ last_downloaded = 0
44
+
45
+ with open(file_path, 'wb') as f:
46
+ async for chunk in response.content.iter_chunked(1024 * 1024): # 1MB chunks to save memory
47
+ if not chunk:
48
+ break
49
+ f.write(chunk)
50
+ downloaded += len(chunk)
51
+ tasks[task_id]["downloaded"] = downloaded
52
+
53
+ current_time = time.time()
54
+ if current_time - last_time >= 1.0: # Update speed every second
55
+ speed = (downloaded - last_downloaded) / (current_time - last_time)
56
+ tasks[task_id]["speed"] = speed
57
+ last_time = current_time
58
+ last_downloaded = downloaded
59
+
60
+ tasks[task_id]["status"] = "completed"
61
+ tasks[task_id]["speed"] = 0.0
62
+ except Exception as e:
63
+ tasks[task_id]["status"] = "error"
64
+ tasks[task_id]["error"] = str(e)
65
+
66
+ @app.post("/start_download")
67
+ async def start_download(req: DownloadRequest, background_tasks: BackgroundTasks):
68
+ task_id = str(uuid.uuid4())
69
+ background_tasks.add_task(download_file, task_id, req.url)
70
+ return {"task_id": task_id}
71
+
72
+ @app.get("/status/{task_id}")
73
+ async def get_status(task_id: str):
74
+ if task_id not in tasks:
75
+ return {"error": "Task not found"}
76
+ return tasks[task_id]
77
+
78
+ @app.get("/download/{task_id}")
79
+ async def download(task_id: str):
80
+ if task_id not in tasks or tasks[task_id]["status"] != "completed":
81
+ return {"error": "File not ready"}
82
+ return FileResponse(tasks[task_id]["file_path"], filename=f"downloaded_{task_id}.bin")
83
+
84
+ @app.get("/stream/{task_id}")
85
+ async def stream(task_id: str, request: Request):
86
+ if task_id not in tasks or tasks[task_id]["status"] != "completed":
87
+ return {"error": "File not ready"}
88
+
89
+ file_path = tasks[task_id]["file_path"]
90
+ file_size = os.path.getsize(file_path)
91
+
92
+ range_header = request.headers.get("Range")
93
+ if range_header:
94
+ # Simple range request handling for streaming
95
+ byte1, byte2 = 0, None
96
+ match = range_header.replace("bytes=", "").split("-")
97
+ if match[0]:
98
+ byte1 = int(match[0])
99
+ if len(match) > 1 and match[1]:
100
+ byte2 = int(match[1])
101
+
102
+ length = file_size - byte1
103
+ if byte2 is not None:
104
+ length = byte2 + 1 - byte1
105
+
106
+ def file_iterator(start, length):
107
+ with open(file_path, "rb") as f:
108
+ f.seek(start)
109
+ chunk_size = 1024 * 1024
110
+ while length > 0:
111
+ read_size = min(chunk_size, length)
112
+ data = f.read(read_size)
113
+ if not data:
114
+ break
115
+ yield data
116
+ length -= len(data)
117
+
118
+ headers = {
119
+ "Content-Range": f"bytes {byte1}-{byte1+length-1}/{file_size}",
120
+ "Accept-Ranges": "bytes",
121
+ "Content-Length": str(length),
122
+ }
123
+ return StreamingResponse(file_iterator(byte1, length), status_code=206, headers=headers)
124
+ else:
125
+ return FileResponse(file_path)
126
+
127
+ if __name__ == "__main__":
128
+ import uvicorn
129
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ aiohttp
4
+ pydantic