superbrain-server 1.0.27 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/api.py +72 -18
package/package.json
CHANGED
package/payload/api.py
CHANGED
|
@@ -132,6 +132,11 @@ _active_processes: dict = {} # shortcode -> subprocess.Popen
|
|
|
132
132
|
_active_processes_lock = threading.Lock()
|
|
133
133
|
|
|
134
134
|
_STATIC_DIR = Path(__file__).parent / "static"
|
|
135
|
+
_THUMBNAILS_DIR = _STATIC_DIR / "thumbnails"
|
|
136
|
+
_THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
|
|
138
|
+
from fastapi.staticfiles import StaticFiles
|
|
139
|
+
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
|
|
135
140
|
|
|
136
141
|
@app.get("/favicon.ico", include_in_schema=False)
|
|
137
142
|
async def favicon():
|
|
@@ -1270,6 +1275,7 @@ async def reset_database(
|
|
|
1270
1275
|
|
|
1271
1276
|
@app.get("/export")
|
|
1272
1277
|
async def export_data(
|
|
1278
|
+
background_tasks: __import__('fastapi').BackgroundTasks,
|
|
1273
1279
|
token: str = Depends(verify_token),
|
|
1274
1280
|
limit: int = Query(default=10000, ge=1, le=50000, description="Max posts to export"),
|
|
1275
1281
|
offset: int = Query(default=0, ge=0, description="Offset for pagination"),
|
|
@@ -1280,45 +1286,81 @@ async def export_data(
|
|
|
1280
1286
|
- Requires API authentication
|
|
1281
1287
|
- Supports pagination with limit and offset
|
|
1282
1288
|
"""
|
|
1289
|
+
import tempfile
|
|
1290
|
+
import os
|
|
1291
|
+
import httpx
|
|
1292
|
+
|
|
1283
1293
|
try:
|
|
1284
1294
|
db = get_db()
|
|
1285
1295
|
|
|
1286
1296
|
# Get posts with pagination using SQLite
|
|
1287
1297
|
posts = db.get_all_posts(limit=limit, offset=offset)
|
|
1298
|
+
# Convert sqlite3.Row objects to fully mutable dictionaries
|
|
1299
|
+
posts_list = [dict(p) for p in posts]
|
|
1288
1300
|
|
|
1289
|
-
# Get all collections
|
|
1290
1301
|
collections = db.get_all_collections()
|
|
1291
|
-
|
|
1292
|
-
# Get stats
|
|
1293
1302
|
stats = db.get_stats()
|
|
1294
1303
|
|
|
1295
1304
|
export_payload = {
|
|
1296
1305
|
"version": "1.0",
|
|
1297
1306
|
"exported_at": datetime.now().isoformat(),
|
|
1298
|
-
"posts":
|
|
1307
|
+
"posts": posts_list,
|
|
1299
1308
|
"collections": collections,
|
|
1300
1309
|
"stats": stats
|
|
1301
1310
|
}
|
|
1302
1311
|
|
|
1303
1312
|
if format.lower() == "zip":
|
|
1304
|
-
# Create a zip file
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
zip_file.writestr("superbrain_export.json", json.dumps(export_payload, default=str))
|
|
1309
|
-
|
|
1310
|
-
# Reset buffer pointer
|
|
1311
|
-
zip_buffer.seek(0)
|
|
1313
|
+
# Create a zip file on disk to avoid memory explosion
|
|
1314
|
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
|
|
1315
|
+
zip_path = tmp.name
|
|
1316
|
+
tmp.close()
|
|
1312
1317
|
|
|
1313
|
-
|
|
1318
|
+
async def download_image(client, sem, post_dict):
|
|
1319
|
+
url = post_dict.get('thumbnail_url') or post_dict.get('thumbnail')
|
|
1320
|
+
if not url or url.startswith("/static/"):
|
|
1321
|
+
return None
|
|
1322
|
+
|
|
1323
|
+
shortcode = post_dict.get('shortcode')
|
|
1324
|
+
if not shortcode: return None
|
|
1325
|
+
|
|
1326
|
+
ext = "jpg"
|
|
1327
|
+
if ".png" in url.lower(): ext = "png"
|
|
1328
|
+
elif ".webp" in url.lower(): ext = "webp"
|
|
1329
|
+
|
|
1330
|
+
path_in_zip = f"thumbnails/{shortcode}.{ext}"
|
|
1331
|
+
try:
|
|
1332
|
+
async with sem:
|
|
1333
|
+
resp = await client.get(url, follow_redirects=True, timeout=12.0)
|
|
1334
|
+
if resp.status_code == 200:
|
|
1335
|
+
post_dict['thumbnail'] = f"/static/{path_in_zip}"
|
|
1336
|
+
return (path_in_zip, resp.content)
|
|
1337
|
+
except Exception as e:
|
|
1338
|
+
logger.warning(f"Failed to fetch thumbnail for {shortcode}: {e}")
|
|
1339
|
+
return None
|
|
1340
|
+
|
|
1341
|
+
# Fetch all thumbnails concurrently in batches
|
|
1342
|
+
sem = asyncio.Semaphore(15)
|
|
1343
|
+
tasks = []
|
|
1344
|
+
async with httpx.AsyncClient(verify=False) as client:
|
|
1345
|
+
for post in export_payload["posts"]:
|
|
1346
|
+
tasks.append(download_image(client, sem, post))
|
|
1347
|
+
|
|
1348
|
+
downloaded_images = await asyncio.gather(*tasks)
|
|
1349
|
+
|
|
1350
|
+
# Write everything to the zip sequentially to save RAM
|
|
1351
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
1352
|
+
# The export_payload now has updated 'thumbnail' fields pointing locally
|
|
1353
|
+
zip_file.writestr("superbrain_export.json", json.dumps(export_payload, default=str))
|
|
1354
|
+
for res in downloaded_images:
|
|
1355
|
+
if res:
|
|
1356
|
+
path_in_zip, content = res
|
|
1357
|
+
zip_file.writestr(path_in_zip, content)
|
|
1358
|
+
|
|
1314
1359
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1315
1360
|
filename = f"superbrain_export_{timestamp}.zip"
|
|
1316
1361
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
media_type="application/zip",
|
|
1320
|
-
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
|
1321
|
-
)
|
|
1362
|
+
background_tasks.add_task(os.remove, zip_path)
|
|
1363
|
+
return FileResponse(zip_path, media_type="application/zip", filename=filename)
|
|
1322
1364
|
|
|
1323
1365
|
# Default JSON response
|
|
1324
1366
|
return export_payload
|
|
@@ -1351,6 +1393,7 @@ async def import_data_file(
|
|
|
1351
1393
|
Import data from a ZIP or JSON file.
|
|
1352
1394
|
- Requires API authentication
|
|
1353
1395
|
- mode=merge or replace
|
|
1396
|
+
- Extracts thumbnails from ZIP archive if present
|
|
1354
1397
|
"""
|
|
1355
1398
|
try:
|
|
1356
1399
|
content = await file.read()
|
|
@@ -1370,6 +1413,17 @@ async def import_data_file(
|
|
|
1370
1413
|
|
|
1371
1414
|
with z.open(target_file) as f:
|
|
1372
1415
|
data = json.load(f)
|
|
1416
|
+
|
|
1417
|
+
# Extract any custom image thumbnails from /thumbnails directory to the static folder mapped statically
|
|
1418
|
+
thumbnails_dir = _STATIC_DIR / "thumbnails"
|
|
1419
|
+
thumbnails_dir.mkdir(parents=True, exist_ok=True)
|
|
1420
|
+
|
|
1421
|
+
for name in z.namelist():
|
|
1422
|
+
if name.startswith("thumbnails/") and not name.endswith("/"):
|
|
1423
|
+
img_filename = name.split("/")[-1]
|
|
1424
|
+
with z.open(name) as zf, open(thumbnails_dir / img_filename, "wb") as out_f:
|
|
1425
|
+
out_f.write(zf.read())
|
|
1426
|
+
|
|
1373
1427
|
except zipfile.BadZipFile:
|
|
1374
1428
|
raise HTTPException(status_code=400, detail="Invalid ZIP file")
|
|
1375
1429
|
else:
|