Upload 5 files
Browse files- Dockerfile +26 -0
- README.md +3 -2
- main.py +116 -0
- requirements.txt +3 -0
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a lightweight Debian-based Python image
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Install the four core conversion engines
|
| 5 |
+
RUN apt-get update && apt-get install -y \
|
| 6 |
+
ffmpeg \
|
| 7 |
+
libreoffice \
|
| 8 |
+
imagemagick \
|
| 9 |
+
pandoc \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Set the working directory
|
| 13 |
+
WORKDIR /app
|
| 14 |
+
|
| 15 |
+
# Copy and install Python dependencies
|
| 16 |
+
COPY requirements.txt .
|
| 17 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 18 |
+
|
| 19 |
+
# Copy the API code
|
| 20 |
+
COPY . .
|
| 21 |
+
|
| 22 |
+
# Expose the mandatory port for Hugging Face Spaces
|
| 23 |
+
EXPOSE 7860
|
| 24 |
+
|
| 25 |
+
# Start the FastAPI server
|
| 26 |
+
CMD ["python", "main.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
---
|
| 2 |
title: Backend Filehandler
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: gray
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
title: Backend Filehandler
|
| 3 |
+
emoji: 🌍
|
| 4 |
+
colorFrom: gray
|
| 5 |
colorTo: gray
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
short_description: 'Backend '
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
main.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import shutil
|
| 4 |
+
import tempfile
|
| 5 |
+
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Security, Depends
|
| 6 |
+
from fastapi.responses import FileResponse
|
| 7 |
+
from fastapi.security.api_key import APIKeyHeader
|
| 8 |
+
from tempfile import NamedTemporaryFile
|
| 9 |
+
import uvicorn
|
| 10 |
+
|
| 11 |
+
app = FastAPI(title="Universal File Converter API - SECURED")
|
| 12 |
+
|
| 13 |
+
# =========================================================
|
| 14 |
+
# BRIDGE CONTROL (SECURITY)
|
| 15 |
+
# =========================================================
|
| 16 |
+
# Define the header name the backend will look for
|
| 17 |
+
API_KEY_NAME = "X-Bridge-Key"
|
| 18 |
+
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
| 19 |
+
|
| 20 |
+
# This key must match the frontend exactly
|
| 21 |
+
API_SECRET_KEY = os.environ.get("BRIDGE_SECRET_KEY", "my_secure_bridge_token_2026")
|
| 22 |
+
|
| 23 |
+
async def get_api_key(api_key_header: str = Security(api_key_header)):
|
| 24 |
+
"""Verifies the Bridge Control badge."""
|
| 25 |
+
if api_key_header == API_SECRET_KEY:
|
| 26 |
+
return api_key_header
|
| 27 |
+
raise HTTPException(
|
| 28 |
+
status_code=403,
|
| 29 |
+
detail="Bridge Control: Access Denied. Unauthorized connection attempt."
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# =========================================================
|
| 33 |
+
# THE 300+ FORMAT ROUTING DICTIONARY
|
| 34 |
+
# =========================================================
|
| 35 |
+
CONVERSION_ENGINES = {
|
| 36 |
+
**{ext: 'imagemagick' for ext in['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'tiff', 'ico', 'svg', 'heic', 'eps', 'ps', 'tga', 'jp2', 'pcx', 'pnm', 'pgm', 'ppm', 'pbm', 'xbm', 'xpm', 'cr2', 'crw', 'nef', 'arw', 'dng', 'orf', 'raf', 'rw2', 'pef', 'srw', 'exr', 'hdr', 'dds', 'avif', 'jxl', 'dib', 'jpe', 'tpf', 'vtf', 'wmf', 'emf', 'cdr', 'cpt', 'svgz', 'cgm', 'djvu', 'pcd', 'pict', 'raw', 'xcf']},
|
| 37 |
+
**{ext: 'ffmpeg' for ext in['mp4', 'avi', 'mkv', 'webm', 'mov', 'flv', 'wmv', 'm4v', 'mpeg', '3gp', 'ogv', 'asf', 'rm', 'rmvb', 'ts', 'm2ts', 'vob', 'mxf', 'roq', 'nsv', 'f4v', 'f4p', 'f4a', 'f4b', 'drc', 'yuv', 'nut', 'amv', 'svt', 'ivf', 'mpe', 'mp2v', 'm2v', 'm4p', 'mpg', 'wtv', 'gxf', 'mjpg', 'mts', 'xvid', 'divx', 'mp3', 'wav', 'flac', 'ogg', 'aac', 'm4a', 'wma', 'aiff', 'opus', 'alac', 'amr', 'au', 'snd', 'mid', 'midi', 'kar', 'mka', 'mac', 'ape', 'spx', 'rka', 'shn', 'tta', 'smp', 'dts', 'pcm', 'gsm', 'dct', 'voc', 'aup', 'band', 'bwf', 'cda', 'ds2', 'dss', 'dvf', 'm4r', 'm4b', 'mus', 'qcp', 'ra', 'ram', 'rx2', 's3m', 'srm', 'uax', 'xm', 'it', 'ac3', 'caf']},
|
| 38 |
+
**{ext: 'libreoffice' for ext in['pdf', 'docx', 'doc', 'odt', 'rtf', 'wpd', 'wps', 'sdw', 'sgl', 'lwp', 'docm', 'dot', 'dotx', 'dotm', 'pages', 'xps', 'oxps', 'abw', 'log', 'msg', 'eml', 'p7s', '1st', 'ans', 'asc', 'diz', 'err', 'faq', 'readme', 'sub', 'ltr', 'dvi', 'xlsx', 'xls', 'ods', 'csv', 'tsv', 'xlsm', 'xlsb', 'xltx', 'xltm', 'xlw', 'xlr', 'wk1', 'wk2', 'wk3', 'wk4', 'wq1', 'sd1', 'dbf', 'dif', 'slk', 'prn', 'numbers', 'fods', 'ots', 'gnumeric', 'pptx', 'ppt', 'odp', 'pptm', 'pps', 'ppsx', 'ppsm', 'pot', 'potx', 'potm', 'sda', 'sdd', 'sti', 'thmx', 'key', 'pez', 'otp']},
|
| 39 |
+
**{ext: 'calibre' for ext in['epub', 'mobi', 'azw3', 'fb2', 'lit', 'prc', 'tcr', 'cbr', 'cbz', 'cb7', 'cbt', 'cba', 'chm', 'djv', 'ibooks', 'kfx', 'pdb', 'snb', 'tmd', 'lrf']},
|
| 40 |
+
**{ext: 'pandoc' for ext in ['html', 'md', 'rst', 'tex']},
|
| 41 |
+
**{ext: 'assimp' for ext in['stl', 'obj', 'fbx', 'dae', '3ds', 'blend', 'ply', 'gltf', 'glb', 'step', 'stp', 'iges', 'igs', 'dxf', 'dwg', 'x3d', 'wrl', 'amf']},
|
| 42 |
+
**{ext: 'fontforge' for ext in['ttf', 'otf', 'woff', 'woff2', 'eot', 'dfont', 'fon', 'pfa', 'pfb', 'suit', 'fnt']},
|
| 43 |
+
**{ext: 'archive_engine' for ext in['zip', 'tar', 'gz', 'bz2', '7z', 'rar', 'xz', 'lzma', 'wim', 'swm', 'cab', 'iso', 'dmg', 'img', 'vdi', 'vhd', 'vmdk', 'squashfs', 'qcow2', 'z', 'taz', 'tgz', 'tbz2', 'tz2', 'tbz', 'jar', 'apk', 'xpi', 'crx', 'nupkg', 'pak', 'lz', 'sit', 'sitx', 'pea', 'ar', 'cpio', 'shar', 'lha', 'lzh', 'alz']},
|
| 44 |
+
**{ext: 'rename' for ext in['py', 'js', 'java', 'c', 'cpp', 'cs', 'rb', 'go', 'rs', 'php', 'swift', 'kt', 'sh', 'bat', 'ps1', 'pl', 'lua', 'm', 'r', 'vbs', 'asm', 'dart', 'ts', 'css', 'scss', 'less', 'txt', 'xml', 'json', 'yaml', 'yml', 'rss', 'atom', 'rdf', 'owl', 'xsd', 'dtd', 'xslt', 'xsl', 'plist', 'ini', 'cfg', 'conf', 'toml', 'sql', 'hdf5', 'h5', 'nc', 'sqlite', 'db', 'mdb', 'accdb', 'bson', 'avro', 'parquet']}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
def execute_conversion(input_path: str, output_path: str, target_ext: str):
|
| 48 |
+
engine = CONVERSION_ENGINES.get(target_ext)
|
| 49 |
+
if not engine:
|
| 50 |
+
raise ValueError(f"Target format '{target_ext}' is not mapped to an engine.")
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
if engine == 'rename':
|
| 54 |
+
shutil.copy(input_path, output_path)
|
| 55 |
+
return
|
| 56 |
+
elif engine == 'libreoffice':
|
| 57 |
+
out_dir = os.path.dirname(output_path)
|
| 58 |
+
cmd =['soffice', '--headless', '--convert-to', target_ext, '--outdir', out_dir, input_path]
|
| 59 |
+
elif engine == 'imagemagick':
|
| 60 |
+
cmd =['magick', input_path, output_path]
|
| 61 |
+
elif engine == 'ffmpeg':
|
| 62 |
+
cmd =['ffmpeg', '-y', '-i', input_path, output_path]
|
| 63 |
+
elif engine == 'pandoc':
|
| 64 |
+
cmd =['pandoc', input_path, '-o', output_path]
|
| 65 |
+
elif engine == 'calibre':
|
| 66 |
+
cmd =['ebook-convert', input_path, output_path]
|
| 67 |
+
elif engine == 'assimp':
|
| 68 |
+
cmd = ['assimp', 'export', input_path, output_path]
|
| 69 |
+
elif engine == 'fontforge':
|
| 70 |
+
script = f'Open("{input_path}"); Generate("{output_path}")'
|
| 71 |
+
cmd = ['fontforge', '-lang=ff', '-c', script]
|
| 72 |
+
elif engine == 'archive_engine':
|
| 73 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 74 |
+
subprocess.run(['7z', 'x', f'-o{temp_dir}', input_path], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 75 |
+
subprocess.run(['7z', 'a', output_path, f'{temp_dir}/*'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 76 |
+
return
|
| 77 |
+
|
| 78 |
+
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 79 |
+
except FileNotFoundError:
|
| 80 |
+
raise RuntimeError(f"Required server tool for engine '{engine}' is missing or not installed.")
|
| 81 |
+
except subprocess.CalledProcessError as e:
|
| 82 |
+
err_msg = e.stderr.decode('utf-8') if e.stderr else str(e)
|
| 83 |
+
raise RuntimeError(f"Engine Error ({engine}): {err_msg}")
|
| 84 |
+
|
| 85 |
+
@app.post("/convert")
|
| 86 |
+
async def convert_file(
|
| 87 |
+
file: UploadFile = File(...),
|
| 88 |
+
target_format: str = Form(...),
|
| 89 |
+
api_key: str = Depends(get_api_key) # <-- SECURITY BRIDGE CHECK HAPPENS HERE
|
| 90 |
+
):
|
| 91 |
+
target_format = target_format.lower().strip('.')
|
| 92 |
+
|
| 93 |
+
filename_parts = file.filename.rsplit('.', 1)
|
| 94 |
+
input_ext = filename_parts[-1] if len(filename_parts) > 1 else "tmp"
|
| 95 |
+
base_name = filename_parts[0] if len(filename_parts) > 1 else "file"
|
| 96 |
+
|
| 97 |
+
# Memory-safe chunked streaming (Prevents RAM explosions)
|
| 98 |
+
with NamedTemporaryFile(delete=False, suffix=f".{input_ext}") as tmp_in:
|
| 99 |
+
shutil.copyfileobj(file.file, tmp_in)
|
| 100 |
+
input_path = tmp_in.name
|
| 101 |
+
|
| 102 |
+
output_path = input_path.rsplit('.', 1)[0] + f".{target_format}"
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
execute_conversion(input_path, output_path, target_format)
|
| 106 |
+
|
| 107 |
+
return FileResponse(
|
| 108 |
+
path=output_path,
|
| 109 |
+
filename=f"{base_name}_converted.{target_format}",
|
| 110 |
+
media_type='application/octet-stream'
|
| 111 |
+
)
|
| 112 |
+
except Exception as e:
|
| 113 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
python-multipart
|