|
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 = 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() |
|
|
|
|
|
unique_path = generate_unique_filename(file.filename) |
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
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) |