photo / app.py
JIMMYGGG's picture
Update app.py
7a859ba verified
raw
history blame
23 kB
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'
@app.get("/", response_class=HTMLResponse)
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>
"""
@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 = `![${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))
@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")
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))
@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()
# 添加认证头进行上传
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)