Spaces:
Running
Running
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 | |
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' | |
async def root(): | |
return """ | |
<html> | |
<head> | |
<title>登录</title> | |
<style> | |
.login-container { | |
width: 300px; | |
margin: 100px auto; | |
padding: 20px; | |
border: 1px solid #ccc; | |
border-radius: 5px; | |
text-align: center; | |
} | |
.input-field { | |
width: 100%; | |
padding: 10px; | |
margin: 10px 0; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
} | |
.submit-button { | |
width: 100%; | |
padding: 10px; | |
background-color: #4CAF50; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
} | |
.submit-button:hover { | |
background-color: #45a049; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="login-container"> | |
<h2>请输入访问密码</h2> | |
<form id="loginForm"> | |
<input type="password" name="password" class="input-field" placeholder="输入密码" required> | |
<button type="submit" class="submit-button">登录</button> | |
</form> | |
<div id="error-message" style="color: red; margin-top: 10px;"></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 = '密码错误,请重试'; | |
} | |
} catch (error) { | |
document.getElementById('error-message').textContent = '发生错误,请重试'; | |
} | |
}); | |
</script> | |
</body> | |
</html> | |
""" | |
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 = `![${fileName}](${url})`; | |
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').appendChild(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.appendChild(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)) | |
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") | |
filename = url.split('/')[-1] | |
if not filename or '.' not in filename: | |
ext = get_image_extension(content_type) | |
filename = f"downloaded_image.{ext}" | |
# 添加认证头上传到HuggingFace | |
response = api.upload_file( | |
path_or_fileobj=response.content, | |
path_in_repo=f"images/{filename}", | |
repo_id=hf_dataset_id, | |
repo_type="dataset", | |
token=hf_token # 显式传入token | |
) | |
image_url = f"https://{PROXY_DOMAIN}/datasets/{hf_dataset_id}/resolve/main/images/{filename}" | |
return {"url": image_url, "filename": filename} | |
except Exception as e: | |
raise HTTPException(status_code=500, detail=str(e)) | |
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() | |
# 添加认证头进行上传 | |
response = api.upload_file( | |
path_or_fileobj=contents, | |
path_in_repo=f"images/{file.filename}", | |
repo_id=hf_dataset_id, | |
repo_type="dataset", | |
token=hf_token # 显式传入token | |
) | |
# 修改返回的URL格式 | |
image_url = f"https://{PROXY_DOMAIN}/datasets/{hf_dataset_id}/resolve/main/images/{file.filename}" | |
return {"url": image_url} | |
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) |