import os
import shutil
import uuid
import string
import random
import zipfile
import io
from datetime import datetime, timedelta
from pathlib import Path
from contextlib import asynccontextmanager
from fastapi import FastAPI, UploadFile, File, HTTPException, Request, Form, Query
from fastapi.responses import HTMLResponse, FileResponse, StreamingResponse
import aiofiles
from apscheduler.schedulers.background import BackgroundScheduler
# ------------------- Configuration -------------------
BASE_DIR = Path("/tmp/file_sharer")
UPLOAD_DIR = BASE_DIR / "uploads"
CHUNK_DIR = BASE_DIR / "chunks"
SQLITE_DB = BASE_DIR / "database.db"
BASE_DIR.mkdir(parents=True, exist_ok=True)
UPLOAD_DIR.mkdir(exist_ok=True)
CHUNK_DIR.mkdir(exist_ok=True)
CHUNK_SIZE = 5 * 1024 * 1024 # 5 MB per chunk
MAX_FILE_SIZE = 50 * 1024 * 1024 * 1024 # 50 GB per file
# ------------------- Database (SQLite) -------------------
import sqlite3
def init_db():
conn = sqlite3.connect(str(SQLITE_DB))
conn.execute("""
CREATE TABLE IF NOT EXISTS uploads (
id TEXT PRIMARY KEY,
short_code TEXT UNIQUE,
delete_token TEXT UNIQUE,
total_size INTEGER DEFAULT 0,
file_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP DEFAULT (datetime('now', '+7 days')),
is_complete INTEGER DEFAULT 0,
is_deleted INTEGER DEFAULT 0
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
upload_id TEXT,
original_name TEXT,
stored_name TEXT,
size INTEGER,
FOREIGN KEY(upload_id) REFERENCES uploads(id)
)
""")
conn.commit()
conn.close()
def get_conn():
conn = sqlite3.connect(str(SQLITE_DB))
conn.row_factory = sqlite3.Row
return conn
def generate_short_code(length=6):
chars = string.ascii_lowercase + string.digits
while True:
code = ''.join(random.choices(chars, k=length))
if not get_conn().execute("SELECT 1 FROM uploads WHERE short_code=?", (code,)).fetchone():
return code
# ------------------- Helper functions -------------------
async def cleanup_expired():
conn = get_conn()
expired = conn.execute(
"SELECT id FROM uploads WHERE expires_at < datetime('now') AND is_deleted=0"
).fetchall()
for row in expired:
await delete_upload(row['id'], conn=conn, manual=False)
conn.close()
async def delete_upload(upload_id: str, conn=None, manual: bool = True):
if conn is None:
conn = get_conn()
own_conn = True
else:
own_conn = False
try:
upload_path = UPLOAD_DIR / upload_id
chunk_path = CHUNK_DIR / upload_id
for path in (upload_path, chunk_path):
if path.exists():
shutil.rmtree(path, ignore_errors=True)
conn.execute("UPDATE uploads SET is_deleted=1 WHERE id=?", (upload_id,))
conn.execute("DELETE FROM files WHERE upload_id=?", (upload_id,))
conn.commit()
finally:
if own_conn:
conn.close()
# ------------------- FastAPI with lifespan -------------------
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
scheduler = BackgroundScheduler()
scheduler.add_job(cleanup_expired, 'interval', hours=1)
scheduler.start()
app.state.scheduler = scheduler
yield
scheduler.shutdown()
app = FastAPI(title="Free File Sharing", lifespan=lifespan)
# ------------------- Beautiful error page -------------------
def error_html(message: str, status_code: int = 404) -> str:
return f"""
Error {status_code}
"""
# ------------------- Main upload page -------------------
@app.get("/", response_class=HTMLResponse)
async def index():
return HTMLResponse("""
Free File Sharing
📦
File Sharing
Free, no registration, up to 50 GB
Uploading... 0%
0%
Cancel
✅ Link ready:
Files available for 7 days
🗑 Delete now
""")
# ------------------- Download page (like Mega) -------------------
def format_size(size_bytes):
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024*1024:
return f"{size_bytes/1024:.1f} KB"
elif size_bytes < 1024*1024*1024:
return f"{size_bytes/(1024*1024):.1f} MB"
else:
return f"{size_bytes/(1024*1024*1024):.2f} GB"
@app.get("/{short_code}")
async def download_page(short_code: str, request: Request, direct: bool = Query(False)):
conn = get_conn()
upload = conn.execute(
"SELECT id, is_complete, is_deleted, created_at, expires_at FROM uploads WHERE short_code=?",
(short_code,)
).fetchone()
if not upload or upload['is_deleted']:
conn.close()
return HTMLResponse(status_code=404, content=error_html("Link not found or files have been deleted."))
if not upload['is_complete']:
conn.close()
return HTMLResponse("Files are still uploading, please try later ", status_code=202)
files = conn.execute("SELECT original_name, size FROM files WHERE upload_id=?", (upload['id'],)).fetchall()
conn.close()
# Если запрошена прямая загрузка (параметр ?direct или путь /dl/...)
if direct:
return await serve_direct_download(upload['id'], short_code, files)
# Иначе показываем красивую страницу
total_size = sum(f['size'] for f in files)
file_names = [f['original_name'] for f in files]
display_name = file_names[0] if len(files) == 1 else f"{len(files)} files"
created = upload['created_at']
expires = upload['expires_at']
html = f"""
Download - {display_name}
"""
return HTMLResponse(html)
@app.get("/dl/{short_code}")
async def direct_download(short_code: str, request: Request):
conn = get_conn()
upload = conn.execute(
"SELECT id, is_complete, is_deleted FROM uploads WHERE short_code=?",
(short_code,)
).fetchone()
if not upload or upload['is_deleted'] or not upload['is_complete']:
conn.close()
return HTMLResponse(status_code=404, content=error_html("File not found."))
files = conn.execute("SELECT original_name, size FROM files WHERE upload_id=?", (upload['id'],)).fetchall()
conn.close()
return await serve_direct_download(upload['id'], short_code, files)
async def serve_direct_download(upload_id: str, short_code: str, files):
upload_dir = UPLOAD_DIR / upload_id
if not upload_dir.exists():
return HTMLResponse(status_code=404, content=error_html("Files missing."))
if len(files) == 1:
file_path = upload_dir / files[0]['original_name']
if not file_path.exists():
return HTMLResponse(status_code=404, content=error_html("File missing."))
return FileResponse(file_path, filename=files[0]['original_name'])
else:
zip_filename = f"files_{short_code}.zip"
async def zip_stream():
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
for f in files:
file_path = upload_dir / f['original_name']
if file_path.exists():
zf.write(file_path, f['original_name'])
zip_buffer.seek(0)
while True:
data = zip_buffer.read(8192)
if not data:
break
yield data
return StreamingResponse(zip_stream(), media_type="application/zip", headers={
"Content-Disposition": f'attachment; filename="{zip_filename}"'
})
# ------------------- Upload endpoints (unchanged) -------------------
@app.post("/upload/init")
async def init_upload(data: dict):
file_count = data.get("file_count", 1)
upload_id = str(uuid.uuid4())
short_code = generate_short_code()
delete_token = str(uuid.uuid4())
conn = get_conn()
conn.execute(
"INSERT INTO uploads (id, short_code, delete_token, file_count, created_at, expires_at) VALUES (?,?,?,?, datetime('now'), datetime('now', '+7 days'))",
(upload_id, short_code, delete_token, file_count)
)
conn.commit()
conn.close()
(CHUNK_DIR / upload_id).mkdir(exist_ok=True)
return {"upload_id": upload_id, "short_code": short_code, "delete_token": delete_token}
@app.put("/upload/{upload_id}/{chunk_index}")
async def upload_chunk(
upload_id: str,
chunk_index: int,
chunk: UploadFile = File(...),
file_name: str = Form(...),
file_size: int = Form(...),
total_chunks: int = Form(...)
):
conn = get_conn()
upload = conn.execute("SELECT id FROM uploads WHERE id=? AND is_complete=0 AND is_deleted=0", (upload_id,)).fetchone()
conn.close()
if not upload:
raise HTTPException(status_code=404, detail="Upload not found or already completed")
chunk_dir = CHUNK_DIR / upload_id
chunk_path = chunk_dir / f"{file_name}.part{chunk_index}"
async with aiofiles.open(chunk_path, 'wb') as out_file:
content = await chunk.read()
await out_file.write(content)
if chunk_index == 0:
conn = get_conn()
conn.execute(
"INSERT OR REPLACE INTO files (upload_id, original_name, stored_name, size) VALUES (?,?,?,?)",
(upload_id, file_name, file_name, file_size)
)
conn.commit()
conn.close()
return {"status": "chunk_uploaded"}
@app.post("/upload/{upload_id}/complete")
async def complete_upload(upload_id: str):
conn = get_conn()
upload = conn.execute("SELECT id, short_code FROM uploads WHERE id=? AND is_complete=0 AND is_deleted=0", (upload_id,)).fetchone()
if not upload:
conn.close()
raise HTTPException(status_code=404, detail="Upload not found")
chunk_dir = CHUNK_DIR / upload_id
target_dir = UPLOAD_DIR / upload_id
target_dir.mkdir(exist_ok=True)
chunks = {}
for chunk_file in chunk_dir.iterdir():
parts = chunk_file.name.rsplit('.part', 1)
if len(parts) != 2:
continue
orig_name = parts[0]
idx = int(parts[1])
chunks.setdefault(orig_name, {})[idx] = chunk_file
for orig_name, parts_dict in chunks.items():
sorted_indices = sorted(parts_dict.keys())
target_path = target_dir / orig_name
async with aiofiles.open(target_path, 'wb') as outfile:
for i in sorted_indices:
part_path = parts_dict[i]
async with aiofiles.open(part_path, 'rb') as infile:
while True:
data = await infile.read(1024*1024)
if not data:
break
await outfile.write(data)
for part_path in parts_dict.values():
part_path.unlink(missing_ok=True)
conn.execute("UPDATE uploads SET is_complete=1 WHERE id=?", (upload_id,))
conn.commit()
conn.close()
shutil.rmtree(chunk_dir, ignore_errors=True)
return {"status": "completed", "short_code": upload['short_code']}
@app.delete("/delete/{delete_token}")
async def delete_by_token(delete_token: str):
conn = get_conn()
upload = conn.execute("SELECT id FROM uploads WHERE delete_token=? AND is_deleted=0", (delete_token,)).fetchone()
conn.close()
if not upload:
raise HTTPException(status_code=404, detail="Invalid token or files already deleted")
await delete_upload(upload['id'])
return {"status": "deleted"}