from fastapi import FastAPI, File, UploadFile, HTTPException, Request from fastapi.responses import HTMLResponse from fastapi.middleware.cors import CORSMiddleware from huggingface_hub import HfApi import os from dotenv import load_dotenv import uvicorn import requests from io import BytesIO import re from urllib.parse import urlparse from datetime import datetime import os import hashlib import random import string load_dotenv() app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 环境变量配置 hf_token = os.getenv("HF_TOKEN") hf_dataset_id = os.getenv("HF_DATASET_ID") ACCESS_PASSWORD = os.getenv("ACCESS_PASSWORD", "your_default_password") PROXY_DOMAIN = os.getenv("PROXY_DOMAIN", "huggingface.co") # 初始化API并添加token api = HfApi(token=hf_token) # 设置通用请求头 headers = { "Authorization": f"Bearer {hf_token}", 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } def is_valid_image_url(url): try: parsed = urlparse(url) return bool(parsed.netloc) and bool(parsed.scheme) except: return False def get_image_extension(content_type): content_type = content_type.lower() if 'jpeg' in content_type or 'jpg' in content_type: return 'jpg' elif 'png' in content_type: return 'png' elif 'gif' in content_type: return 'gif' elif 'webp' in content_type: return 'webp' return 'jpg' def generate_random_string(length=6): """Generate a random string of fixed length""" letters = string.ascii_lowercase + string.digits return ''.join(random.choice(letters) for _ in range(length)) def generate_unique_filename(original_filename): current_date = datetime.now().strftime('%Y-%m-%d') ext = os.path.splitext(original_filename)[1] if not ext: ext = '.jpg' timestamp = datetime.now().strftime('%H%M%S') random_str = generate_random_string() content = f"{timestamp}{random_str}{original_filename}".encode('utf-8') hash_value = hashlib.md5(content).hexdigest()[:12] unique_filename = f"{hash_value}{ext}" return f"{current_date}/{unique_filename}" @app.get("/", response_class=HTMLResponse) async def root(): return """ <html> <head> <title>Login</title> <style> body { background-color: #f0f2f5; font-family: Arial, sans-serif; } .login-container { width: 300px; margin: 100px auto; padding: 20px; border: 1px solid #ccc; border-radius: 10px; background-color: #fff; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); text-align: center; } .input-field { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; font-size: 16px; } .submit-button { width: 100%; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 5px; font-size: 16px; cursor: pointer; transition: background-color 0.3s; } .submit-button:hover { background-color: #0056b3; } .error-message { color: red; margin-top: 10px; } </style> </head> <body> <div class="login-container"> <h2>Enter Access Password</h2> <form id="loginForm"> <input type="password" name="password" class="input-field" placeholder="Enter Password" required> <button type="submit" class="submit-button">Login</button> </form> <div id="error-message" class="error-message"></div> </div> <script> document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const password = e.target.password.value; try { const response = await fetch('/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password }) }); if (response.ok) { const html = await response.text(); document.open(); document.write(html); document.close(); } else { document.getElementById('error-message').textContent = 'Incorrect password, please try again'; } } catch (error) { document.getElementById('error-message').textContent = 'An error occurred, please try again'; } }); </script> </body> </html> """ @app.post("/verify") async def verify_password(request: Request): try: data = await request.json() password = data.get("password") if password == ACCESS_PASSWORD: return HTMLResponse(""" <html> <head> <title>HuggingFace Dataset Images</title> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif; } body { background-color: #f5f5f5; color: #333; line-height: 1.6; } .container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .header { text-align: center; margin-bottom: 2rem; padding: 1rem; background: white; border-radius: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .header h1 { color: #2d3748; font-size: 1.8rem; font-weight: 600; margin-bottom: 0.5rem; } .upload-container { background: white; border-radius: 12px; padding: 2rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .upload-area { min-height: 200px; padding: 2rem; margin: 1rem 0; background: #f8fafc; border: 2px dashed #cbd5e0; border-radius: 12px; transition: all 0.3s ease; display: flex; flex-direction: column; align-items: center; justify-content: center; } .upload-area:hover { border-color: #4299e1; background: #ebf8ff; } .upload-methods { text-align: center; margin-bottom: 1.5rem; color: #4a5568; } .upload-methods p { margin: 0.5rem 0; font-size: 0.95rem; } .url-input { width: 80%; padding: 0.75rem 1rem; margin: 1rem 0; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 0.95rem; transition: all 0.3s ease; } .url-input:focus { outline: none; border-color: #4299e1; box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2); } .upload-button { background-color: #4299e1; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 8px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.3s ease; } .upload-button:hover { background-color: #3182ce; transform: translateY(-1px); } .file-list { margin-top: 2rem; } .file-item { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; transition: all 0.3s ease; } .file-item:hover { border-color: #4299e1; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .file-name { color: #2d3748; font-weight: 500; margin-bottom: 0.5rem; } .progress { width: 100%; height: 8px; background-color: #edf2f7; border-radius: 4px; overflow: hidden; margin: 0.5rem 0; } .progress-bar { height: 100%; background-color: #48bb78; width: 0%; transition: width 0.3s ease-in-out; } .copy-buttons { display: flex; gap: 0.5rem; margin-top: 1rem; } .copy-button { background-color: #edf2f7; color: #4a5568; padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.875rem; cursor: pointer; transition: all 0.3s ease; } .copy-button:hover { background-color: #e2e8f0; color: #2d3748; } .result { margin: 0.5rem 0; } .result a { color: #4299e1; text-decoration: none; word-break: break-all; } .result a:hover { text-decoration: underline; } @media (max-width: 768px) { .container { padding: 1rem; } .upload-area { padding: 1rem; } .url-input { width: 100%; } .copy-buttons { flex-wrap: wrap; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>HuggingFace Dataset Images</h1> </div> <div class="upload-container"> <div class="upload-area" id="dropZone"> <div class="upload-methods"> <p>支持多种上传方式:</p> <p>1. 拖拽图片到此处</p> <p>2. 点击选择文件</p> <p>3. 粘贴图片或图片URL</p> <p>4. 输入图片URL后按回车</p> </div> <input type="text" id="urlInput" class="url-input" placeholder="输入linux.do启动"> <input type="file" id="fileInput" multiple accept="image/*" style="display: none;"> <button class="upload-button" onclick="document.getElementById('fileInput').click()"> 选择文件 </button> </div> <div class="file-list" id="fileList"></div> </div> </div> <script> const MAX_CONCURRENT_UPLOADS = 10; let uploadQueue = []; let activeUploads = 0; async function processUrl(url) { try { const response = await fetch('/fetch-url/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url }) }); if (!response.ok) { throw new Error('获取图片失败'); } const data = await response.json(); return data; } catch (error) { throw error; } } function createFileElement(file) { const element = document.createElement('div'); element.className = 'file-item'; element.innerHTML = ` <div class="file-name">文件名: ${file.name}</div> <div class="progress"> <div class="progress-bar"></div> </div> <div class="result"></div> <div class="copy-buttons" style="display: none;"> <button class="copy-button" onclick="copyText(this, 'markdown')">复制 Markdown</button> <button class="copy-button" onclick="copyText(this, 'html')">复制 HTML</button> <button class="copy-button" onclick="copyText(this, 'url')">复制 URL</button> </div> `; return element; } // 添加复制函数 async function copyText(button, type) { const fileItem = button.closest('.file-item'); const url = fileItem.querySelector('.result a').href; const fileName = fileItem.querySelector('.file-name').textContent.split(': ')[1]; let textToCopy = ''; switch(type) { case 'markdown': textToCopy = ``; break; case 'html': textToCopy = `<img src="${url}" alt="${fileName}">`; break; case 'url': textToCopy = url; break; } try { await navigator.clipboard.writeText(textToCopy); const originalText = button.textContent; button.textContent = '已复制!'; setTimeout(() => { button.textContent = originalText; }, 1000); } catch (err) { console.error('复制失败:', err); alert('复制失败,请手动复制'); } } // 添加uploadFile函数中的复制按钮处理 async function uploadFile(file, element) { activeUploads++; const progressBar = element.querySelector('.progress-bar'); const resultDiv = element.querySelector('.result'); const copyButtons = element.querySelector('.copy-buttons'); const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/upload/', { method: 'POST', body: formData }); const data = await response.json(); progressBar.style.width = '100%'; resultDiv.innerHTML = `<a href="${data.url}" target="_blank">${data.url}</a>`; copyButtons.style.display = 'block'; } catch (error) { resultDiv.innerHTML = `<p style="color: red;">上传失败:${error.message}</p>`; } activeUploads--; processUploadQueue(); } // URL输入框处理 document.getElementById('urlInput').addEventListener('keypress', async (e) => { if (e.key === 'Enter') { e.preventDefault(); const url = e.target.value.trim(); if (url) { const element = createFileElement({ name: url.split('/').pop() || 'image.jpg' }); document.getElementById('fileList').prepend(element); try { const data = await processUrl(url); element.querySelector('.progress-bar').style.width = '100%'; element.querySelector('.result').innerHTML = `<a href="${data.url}" target="_blank">${data.url}</a>`; element.querySelector('.copy-buttons').style.display = 'block'; e.target.value = ''; } catch (error) { element.querySelector('.result').innerHTML = `<p style="color: red;">上传失败:${error.message}</p>`; } } } }); // 处理拖拽上传 const dropZone = document.getElementById('dropZone'); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.background = '#e1e1e1'; }); dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.style.background = '#f9f9f9'; }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.background = '#f9f9f9'; handleFiles(e.dataTransfer.files); }); // 处理文件选择 document.getElementById('fileInput').addEventListener('change', (e) => { handleFiles(e.target.files); }); // 处理粘贴上传 document.addEventListener('paste', async (e) => { const items = e.clipboardData.items; for (let item of items) { if (item.type.indexOf('image') !== -1) { const file = item.getAsFile(); handleFiles([file]); } else if (item.type === 'text/plain') { item.getAsString(async text => { text = text.trim(); if (text.match(/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp)$/i)) { const element = createFileElement({ name: text.split('/').pop() || 'image.jpg' }); document.getElementById('fileList').appendChild(element); try { const data = await processUrl(text); element.querySelector('.progress-bar').style.width = '100%'; element.querySelector('.result').innerHTML = `<a href="${data.url}" target="_blank">${data.url}</a>`; element.querySelector('.copy-buttons').style.display = 'block'; } catch (error) { element.querySelector('.result').innerHTML = `<p style="color: red;">上传失败:${error.message}</p>`; } } }); } } }); function handleFiles(files) { const fileList = document.getElementById('fileList'); for (let file of files) { if (!file.type.startsWith('image/')) continue; const element = createFileElement(file); fileList.prepend(element); if (activeUploads < MAX_CONCURRENT_UPLOADS) { uploadFile(file, element); } else { uploadQueue.push({ file, element }); } } } </script> </body> </html> """) else: raise HTTPException(status_code=401, detail="Invalid password") except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/upload/") async def upload_image(file: UploadFile = File(...)): if not file: raise HTTPException(status_code=400, detail="No file uploaded") if not file.content_type.startswith('image/'): raise HTTPException(status_code=400, detail="File must be an image") try: contents = await file.read() # Generate unique path unique_path = generate_unique_filename(file.filename) # Upload to HuggingFace response = api.upload_file( path_or_fileobj=contents, path_in_repo=f"images/{unique_path}", repo_id=hf_dataset_id, repo_type="dataset", token=hf_token ) # 修改返回URL格式 image_url = f"https://{PROXY_DOMAIN}/datasets/{hf_dataset_id}/resolve/main/images/{unique_path}" return {"url": image_url} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/fetch-url/") async def fetch_url(request: Request): try: data = await request.json() url = data.get("url") if not url: raise HTTPException(status_code=400, detail="No URL provided") if not is_valid_image_url(url): raise HTTPException(status_code=400, detail="Invalid image URL") response = requests.get(url, headers=headers, timeout=10) if not response.ok: raise HTTPException(status_code=400, detail="Failed to fetch image") content_type = response.headers.get('content-type', '') if not content_type.startswith('image/'): raise HTTPException(status_code=400, detail="URL does not point to an image") original_filename = url.split('/')[-1] if not original_filename or '.' not in original_filename: ext = get_image_extension(content_type) original_filename = f"downloaded_image.{ext}" unique_path = generate_unique_filename(original_filename) response = api.upload_file( path_or_fileobj=response.content, path_in_repo=f"images/{unique_path}", repo_id=hf_dataset_id, repo_type="dataset", token=hf_token ) # 修改返回URL格式 image_url = f"https://{PROXY_DOMAIN}/datasets/{hf_dataset_id}/resolve/main/images/{unique_path}" return {"url": image_url, "filename": original_filename} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=7860)