GitHub Action Deploy commited on
Commit
e241afb
·
0 Parent(s):

Auto-deploy from api/ folder (61d877d)

Browse files
Files changed (4) hide show
  1. Dockerfile +10 -0
  2. README.md +7 -0
  3. app.py +322 -0
  4. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . .
6
+
7
+ RUN pip install --no-cache-dir --upgrade pip && \
8
+ pip install --no-cache-dir -r requirements.txt
9
+
10
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: fcetool
3
+ sdk: docker
4
+ pinned: false
5
+ ---
6
+
7
+
app.py ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import re
4
+ from urllib.parse import urlparse
5
+ from fastapi import FastAPI, HTTPException, Request
6
+ from fastapi.responses import JSONResponse
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from asyncio import Semaphore
9
+ import asyncio
10
+
11
+ from slowapi import Limiter
12
+ from slowapi.util import get_remote_address
13
+ from slowapi.errors import RateLimitExceeded
14
+
15
+ from huggingface_hub import HfApi
16
+ import requests
17
+ import firmware_content_extractor as fce
18
+
19
+ def get_real_ip(request: Request):
20
+ forwarded_for = request.headers.get("x-forwarded-for")
21
+ if forwarded_for:
22
+ return forwarded_for.split(",")[0]
23
+ return request.client.host
24
+
25
+ limiter = Limiter(key_func=get_real_ip)
26
+
27
+ app = FastAPI()
28
+
29
+ app.state.limiter = limiter
30
+
31
+ app.add_middleware(
32
+ CORSMiddleware,
33
+ allow_origins=["*"],
34
+ allow_methods=["*"],
35
+ )
36
+
37
+ SUPPORTED_TARGETS = {"boot.img","init_boot.img","dtbo.img","super_empty.img","vbmeta.img","vendor_boot.img","vendor_kernel_boot.img","preloader.img","recovery.img","logo.img","abl.img","hyp.img","modem.img","tz.img","xbl.img","lk.img","tee.img","md1img.img","preloader_emmc.img","preloader_raw.img","preloader_ufs.img","vbmeta_system.img","vbmeta_vendor.img","system_dlkm.img","vendor_dlkm.img","aop.img","aop_config.img","bluetooth.img","cpucp.img","cpucp_dtb.img","devcfg.img","dsp.img","featenabler.img","imagefv.img","keymaster.img","qupfw.img","shrm.img","uefi.img","uefisecapp.img","xbl_config.img","xbl_ramdump.img","scp.img","spmfw.img","sspm.img"}
38
+
39
+ extraction_semaphore = Semaphore(4)
40
+
41
+ TEMP_DIR = "/tmp/extracted"
42
+ os.makedirs(TEMP_DIR, exist_ok=True)
43
+
44
+ HF_TOKEN = os.getenv("HF_TOKEN")
45
+ DATASET_REPO = "offici5l/fcetool"
46
+
47
+ if HF_TOKEN:
48
+ hf_api = HfApi(token=HF_TOKEN)
49
+ else:
50
+ hf_api = None
51
+
52
+ def sanitize_path(path: str) -> str:
53
+ path = re.sub(r'[<>:"|?*]', '_', path)
54
+ path = path.replace(' ', '_')
55
+ return path
56
+
57
+ def generate_storage_path(url: str) -> str:
58
+ parsed = urlparse(url)
59
+ domain = parsed.netloc
60
+ path = parsed.path.lstrip('/')
61
+
62
+ if path.endswith('.zip'):
63
+ path = path[:-4]
64
+
65
+ path = sanitize_path(path)
66
+ domain = sanitize_path(domain)
67
+
68
+ full_path = f"{domain}/{path}"
69
+ return full_path
70
+
71
+ def check_file_in_dataset(storage_path: str, target: str) -> bool:
72
+ if not hf_api:
73
+ return False
74
+
75
+ try:
76
+ path_in_repo = f"{storage_path}/{target}"
77
+ exists = hf_api.file_exists(
78
+ repo_id=DATASET_REPO,
79
+ filename=path_in_repo,
80
+ repo_type="dataset"
81
+ )
82
+ return exists
83
+ except Exception:
84
+ try:
85
+ url = f"https://huggingface.co/datasets/{DATASET_REPO}/resolve/main/{storage_path}/{target}"
86
+ response = requests.get(url, stream=True, timeout=5)
87
+ response.close()
88
+ return response.status_code == 200
89
+ except:
90
+ return False
91
+
92
+ async def upload_to_dataset(file_path: str, storage_path: str, target: str) -> str:
93
+ if not hf_api:
94
+ raise Exception("HF_TOKEN not configured")
95
+
96
+ path_in_repo = f"{storage_path}/{target}"
97
+
98
+ loop = asyncio.get_event_loop()
99
+ await loop.run_in_executor(
100
+ None,
101
+ lambda: hf_api.upload_file(
102
+ path_or_fileobj=file_path,
103
+ path_in_repo=path_in_repo,
104
+ repo_id=DATASET_REPO,
105
+ repo_type="dataset",
106
+ commit_message=f"Add {target} from {storage_path}"
107
+ )
108
+ )
109
+
110
+ download_url = f"https://huggingface.co/datasets/{DATASET_REPO}/resolve/main/{path_in_repo}"
111
+ return download_url
112
+
113
+ @app.exception_handler(RateLimitExceeded)
114
+ async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
115
+ return JSONResponse(
116
+ status_code=429,
117
+ content={
118
+ "status": "error",
119
+ "message": "Rate limit exceeded. Please try again later."
120
+ }
121
+ )
122
+
123
+ @app.exception_handler(HTTPException)
124
+ async def http_exception_handler(request: Request, exc: HTTPException):
125
+ return JSONResponse(
126
+ status_code=exc.status_code,
127
+ content={
128
+ "status": "error",
129
+ "message": exc.detail
130
+ }
131
+ )
132
+
133
+ @app.exception_handler(Exception)
134
+ async def global_exception_handler(request: Request, exc: Exception):
135
+ return JSONResponse(
136
+ status_code=500,
137
+ content={
138
+ "status": "error",
139
+ "message": f"Internal Server Error: {str(exc)}"
140
+ }
141
+ )
142
+
143
+ @app.post("/extract")
144
+ @limiter.limit("3/minute")
145
+ async def extract_target(request: Request, payload: dict):
146
+ if extraction_semaphore.locked():
147
+ return JSONResponse(
148
+ status_code=429,
149
+ content={
150
+ "status": "error",
151
+ "message": "Server is at full capacity. Please try again in 1-2 minutes."
152
+ }
153
+ )
154
+
155
+ url = payload.get("url")
156
+ target = payload.get("target")
157
+
158
+ if not url or not target:
159
+ return JSONResponse(
160
+ status_code=400,
161
+ content={
162
+ "status": "error",
163
+ "message": "Missing 'url' or 'target' parameter in JSON body."
164
+ }
165
+ )
166
+
167
+ if target not in SUPPORTED_TARGETS:
168
+ return JSONResponse(
169
+ status_code=400,
170
+ content={
171
+ "status": "error",
172
+ "message": f"Unsupported target type. Supported: {', '.join(SUPPORTED_TARGETS)}"
173
+ }
174
+ )
175
+
176
+ start_time = time.time()
177
+ storage_path = generate_storage_path(url)
178
+
179
+ if hf_api and check_file_in_dataset(storage_path, target):
180
+ cache_url = f"https://huggingface.co/datasets/{DATASET_REPO}/resolve/main/{storage_path}/{target}"
181
+ return JSONResponse(
182
+ status_code=200,
183
+ content={
184
+ "status": "cached",
185
+ "message": "File already exists in dataset (from cache)",
186
+ "download_url": cache_url,
187
+ "target": target,
188
+ "duration_seconds": int(time.time() - start_time)
189
+ }
190
+ )
191
+
192
+ folder_name = storage_path.replace('/', '_')
193
+ out_dir = os.path.join(TEMP_DIR, folder_name)
194
+ raw_file_path = os.path.normpath(os.path.join(out_dir, target))
195
+
196
+ os.makedirs(out_dir, exist_ok=True)
197
+
198
+ try:
199
+ async with extraction_semaphore:
200
+ result = await fce.extract_async(url, target, out_dir)
201
+
202
+ if result.get("success") and os.path.exists(raw_file_path):
203
+ if hf_api:
204
+ try:
205
+ download_url = await upload_to_dataset(raw_file_path, storage_path, target)
206
+
207
+ os.remove(raw_file_path)
208
+ if os.path.exists(out_dir) and not os.listdir(out_dir):
209
+ os.rmdir(out_dir)
210
+
211
+ end_time = time.time()
212
+ duration = int(end_time - start_time)
213
+
214
+ return JSONResponse(
215
+ status_code=200,
216
+ content={
217
+ "status": "completed",
218
+ "message": "Extraction completed and uploaded to dataset",
219
+ "download_url": download_url,
220
+ "target": target,
221
+ "duration_seconds": duration
222
+ }
223
+ )
224
+
225
+ except Exception as upload_error:
226
+ if os.path.exists(raw_file_path):
227
+ os.remove(raw_file_path)
228
+ if os.path.exists(out_dir) and not os.listdir(out_dir):
229
+ os.rmdir(out_dir)
230
+
231
+ return JSONResponse(
232
+ status_code=500,
233
+ content={
234
+ "status": "failed",
235
+ "message": f"Upload to dataset failed: {str(upload_error)}",
236
+ "duration_seconds": int(time.time() - start_time)
237
+ }
238
+ )
239
+ else:
240
+ if os.path.exists(raw_file_path):
241
+ os.remove(raw_file_path)
242
+ if os.path.exists(out_dir) and not os.listdir(out_dir):
243
+ os.rmdir(out_dir)
244
+
245
+ return JSONResponse(
246
+ status_code=500,
247
+ content={
248
+ "status": "failed",
249
+ "message": "HF_TOKEN not configured. Cannot upload to dataset.",
250
+ "duration_seconds": int(time.time() - start_time)
251
+ }
252
+ )
253
+ else:
254
+ if os.path.exists(out_dir) and not os.listdir(out_dir):
255
+ os.rmdir(out_dir)
256
+
257
+ return JSONResponse(
258
+ status_code=400,
259
+ content={
260
+ "status": "failed",
261
+ "message": result.get("error", "Extraction failed"),
262
+ "duration_seconds": int(time.time() - start_time)
263
+ }
264
+ )
265
+
266
+ except Exception as e:
267
+ if os.path.exists(out_dir) and not os.listdir(out_dir):
268
+ os.rmdir(out_dir)
269
+ return JSONResponse(
270
+ status_code=500,
271
+ content={
272
+ "status": "error",
273
+ "message": str(e)
274
+ }
275
+ )
276
+
277
+ @app.get("/files/{storage_path:path}/{target}")
278
+ async def get_file_info(storage_path: str, target: str):
279
+ if check_file_in_dataset(storage_path, target):
280
+ download_url = f"https://huggingface.co/datasets/{DATASET_REPO}/resolve/main/{storage_path}/{target}"
281
+ return JSONResponse(
282
+ status_code=200,
283
+ content={
284
+ "status": "exists",
285
+ "message": "File found in dataset",
286
+ "download_url": download_url,
287
+ "target": target
288
+ }
289
+ )
290
+ else:
291
+ return JSONResponse(
292
+ status_code=404,
293
+ content={
294
+ "status": "error",
295
+ "message": "File not found in dataset"
296
+ }
297
+ )
298
+
299
+ @app.head("/health")
300
+ async def health_check():
301
+ return JSONResponse(
302
+ status_code=200,
303
+ content={
304
+ "status": "ok",
305
+ "message": "Service is healthy"
306
+ }
307
+ )
308
+
309
+ @app.get("/")
310
+ def home():
311
+ hf_status = "enabled" if hf_api else "disabled"
312
+ return JSONResponse(
313
+ status_code=200,
314
+ content={
315
+ "status": "online",
316
+ "message": "Service is running",
317
+ "mode": "Direct-Upload-to-Dataset",
318
+ "method": "POST /extract",
319
+ "dataset": DATASET_REPO,
320
+ "hf_integration": hf_status
321
+ }
322
+ )
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ slowapi
4
+ huggingface_hub
5
+ requests
6
+ fcetool