cronjob / app.py
testdeep123's picture
Update app.py
3841702 verified
raw
history blame
38.7 kB
import os
import tempfile
import requests
from urllib.parse import urlparse, unquote
from flask import Flask, render_template_string, request, redirect, send_file, jsonify
from huggingface_hub import HfApi, hf_hub_download, upload_file, delete_file
# Environment
REPO_ID = os.getenv("REPO_ID")
HF_TOKEN = os.getenv("HF_TOKEN")
app = Flask(__name__)
api = HfApi()
TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HuggingFace Drive - {{ path or 'Root' }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#0a0a0a;--bg-secondary:#1a1a1a;--bg-tertiary:#2a2a2a;--text:#e0e0e0;--text-muted:#888;--primary:#3b82f6;--primary-hover:#2563eb;--success:#10b981;--success-hover:#059669;--danger:#ef4444;--danger-hover:#dc2626;--border:#333;--shadow:0 4px 20px rgba(0,0,0,0.3);--radius:8px}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;overflow-x:hidden}.container{max-width:1200px;margin:0 auto;padding:1rem;min-height:100vh}.header{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:2rem;padding:1rem;background:var(--bg-secondary);border-radius:var(--radius);box-shadow:var(--shadow);backdrop-filter:blur(10px);transition:all 0.3s ease}.title{display:flex;align-items:center;gap:0.5rem;font-size:1.5rem;font-weight:700;color:var(--primary);margin:0}.title-icon{width:24px;height:24px;transition:transform 0.3s ease}.title:hover .title-icon{transform:rotate(10deg) scale(1.1)}.breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:0.5rem;font-size:0.9rem}.breadcrumb-item{display:flex;align-items:center;gap:0.25rem;padding:0.25rem 0.5rem;border-radius:4px;cursor:pointer;transition:all 0.2s ease;color:var(--text-muted)}.breadcrumb-item:hover{background:var(--bg-tertiary);color:var(--text);transform:translateY(-1px)}.breadcrumb-item.active{color:var(--primary);background:rgba(59,130,246,0.1)}.breadcrumb-separator{color:var(--text-muted);user-select:none}.actions{display:flex;flex-wrap:wrap;gap:0.5rem}.btn{display:flex;align-items:center;gap:0.5rem;padding:0.5rem 1rem;border:none;border-radius:var(--radius);font-size:0.875rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;text-decoration:none;position:relative;overflow:hidden}.btn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.1),transparent);transition:left 0.5s ease;z-index:1}.btn:hover::before{left:100%}.btn-primary{background:var(--primary);color:#fff}.btn-primary:hover{background:var(--primary-hover);transform:translateY(-2px);box-shadow:0 6px 20px rgba(59,130,246,0.3)}.btn-secondary{background:var(--bg-tertiary);color:var(--text);border:1px solid var(--border)}.btn-secondary:hover{background:var(--bg-secondary);border-color:var(--primary);transform:translateY(-2px)}.btn-success{background:var(--success);color:#fff}.btn-success:hover{background:var(--success-hover);transform:translateY(-2px);box-shadow:0 6px 20px rgba(16,185,129,0.3)}.btn-danger{background:var(--danger);color:#fff}.btn-danger:hover{background:var(--danger-hover);transform:translateY(-2px);box-shadow:0 6px 20px rgba(239,68,68,0.3)}.file-input-wrapper{position:relative;overflow:hidden;display:inline-block}.file-input{position:absolute;left:-9999px;opacity:0}.icon{width:16px;height:16px;flex-shrink:0}.file-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:1rem;animation:fadeInUp 0.5s ease}.file-item{background:var(--bg-secondary);border-radius:var(--radius);overflow:hidden;transition:all 0.3s ease;position:relative;border:1px solid var(--border)}.file-item:hover{transform:translateY(-4px);box-shadow:var(--shadow);border-color:var(--primary)}.file-item-content{padding:1rem;cursor:pointer;display:flex;align-items:center;gap:1rem}.file-info{display:flex;align-items:center;gap:1rem;flex:1}.file-icon{width:40px;height:40px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:all 0.3s ease}.file-icon.dir{background:rgba(59,130,246,0.1);color:var(--primary)}.file-icon.file{background:rgba(16,185,129,0.1);color:var(--success)}.file-item:hover .file-icon{transform:scale(1.1)}.file-details{flex:1}.file-name{font-weight:500;margin-bottom:0.25rem;word-break:break-word}.file-meta{font-size:0.75rem;color:var(--text-muted)}.dropdown{position:relative}.dropdown-toggle{background:none;border:none;color:var(--text-muted);cursor:pointer;padding:0.5rem;border-radius:4px;transition:all 0.2s ease}.dropdown-toggle:hover{background:var(--bg-tertiary);color:var(--text)}.dropdown-menu{position:absolute;top:100%;right:0;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);min-width:150px;z-index:1000;opacity:0;visibility:hidden;transform:translateY(-10px);transition:all 0.2s ease}.dropdown.active .dropdown-menu{opacity:1;visibility:visible;transform:translateY(0)}.dropdown-item{display:flex;align-items:center;gap:0.5rem;width:100%;padding:0.5rem 1rem;border:none;background:none;color:var(--text);font-size:0.875rem;cursor:pointer;transition:all 0.2s ease;text-align:left}.dropdown-item:hover{background:var(--bg-tertiary)}.dropdown-item.danger{color:var(--danger)}.dropdown-item.danger:hover{background:rgba(239,68,68,0.1)}.empty-state{text-align:center;padding:4rem 2rem;color:var(--text-muted)}.empty-state .icon{width:64px;height:64px;margin:0 auto 1rem;opacity:0.5}.empty-state h3{margin-bottom:0.5rem;color:var(--text)}.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:2000;opacity:0;visibility:hidden;transition:all 0.3s ease;backdrop-filter:blur(4px)}.modal-overlay.active{opacity:1;visibility:visible}.modal{background:var(--bg-secondary);border-radius:var(--radius);box-shadow:var(--shadow);width:90%;max-width:400px;max-height:90vh;overflow-y:auto;transform:scale(0.9);transition:all 0.3s ease;border:1px solid var(--border)}.modal-overlay.active .modal{transform:scale(1)}.modal-title{padding:1.5rem 1.5rem 0;font-size:1.25rem;font-weight:600}.modal-body{padding:1.5rem}.modal-input{width:100%;padding:0.75rem;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-tertiary);color:var(--text);font-size:0.875rem;transition:all 0.2s ease}.modal-input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(59,130,246,0.1)}.modal-actions{padding:0 1.5rem 1.5rem;display:flex;gap:0.5rem;justify-content:flex-end}.upload-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:3000;opacity:0;visibility:hidden;transition:all 0.3s ease}.upload-overlay.active{opacity:1;visibility:visible}.loading-spinner{width:40px;height:40px;border:3px solid var(--border);border-top:3px solid var(--primary);border-radius:50%;animation:spin 1s linear infinite;margin-bottom:1rem}.upload-text{font-size:1.125rem;color:var(--text)}.btn .loading-spinner{width:16px;height:16px;border-width:2px;margin:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes fadeInUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@media (max-width:768px){.container{padding:0.5rem}.header{flex-direction:column;align-items:stretch;gap:1rem}.title{justify-content:center}.breadcrumb{justify-content:center}.actions{justify-content:center}.file-grid{grid-template-columns:1fr;gap:0.5rem}.modal{width:95%;margin:1rem}.modal-actions{flex-direction:column}.btn{justify-content:center;padding:0.75rem 1rem}}@media (max-width:480px){.file-item-content{padding:0.75rem}.file-icon{width:32px;height:32px}.file-name{font-size:0.875rem}.actions{flex-direction:column}.btn{width:100%}}@media (prefers-reduced-motion:reduce){*{animation-duration:0.01ms!important;animation-iteration-count:1!important;transition-duration:0.01ms!important}}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:var(--bg-tertiary)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
</style>
</head>
<body>
<!-- Upload Overlay -->
<div class="upload-overlay" id="uploadOverlay">
<div class="loading-spinner"></div>
<p class="upload-text">Processing...</p>
</div>
<div class="container">
<header class="header">
<h1 class="title">
<svg class="title-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h4a2 2 0 012 2v0a2 2 0 01-2 2H10a2 2 0 01-2-2v0z"></path>
</svg>
HuggingFace Drive
</h1>
<!-- Breadcrumb -->
<nav class="breadcrumb">
<span class="breadcrumb-item {{ 'active' if not path else '' }}" onclick="nav('')">
<svg class="icon" fill="currentColor" viewBox="0 0 20 20">
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/>
</svg>
Home
</span>
{% if path %}
{% set parts = path.split('/') %}
{% for i in range(parts|length) %}
<span class="breadcrumb-separator">›</span>
{% set current_path = parts[:i+1]|join('/') %}
<span class="breadcrumb-item {{ 'active' if current_path == path else '' }}"
onclick="nav('{{ current_path }}')">
<svg class="icon" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
</svg>
{{ parts[i] }}
</span>
{% endfor %}
{% endif %}
</nav>
<!-- Actions -->
<div class="actions">
<form action="/upload" method="post" enctype="multipart/form-data" id="uploadForm">
<div class="file-input-wrapper">
<label for="fileInput" class="btn btn-primary">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
</svg>
Upload File
</label>
<input type="file" name="file" required class="file-input" id="fileInput">
<input type="hidden" name="path" value="{{ path }}">
</div>
</form>
<button class="btn btn-secondary" onclick="showFolderModal('{{ path }}')">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
New Folder
</button>
<button class="btn btn-success" onclick="showUrlDownloadModal('{{ path }}')">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Download from URL
</button>
</div>
</header>
<!-- File Grid -->
<main>
{% if items %}
<div class="file-grid">
{% for item in items %}
<div class="file-item">
<div class="file-item-content" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% else %}download('{{ item.path }}'){% endif %}">
<div class="file-info">
<div class="file-icon {{ item.type }}">
{% if item.type == 'dir' %}
<svg class="icon" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
</svg>
{% else %}
<svg class="icon" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
</svg>
{% endif %}
</div>
<div class="file-details">
<div class="file-name">{{ item.name }}</div>
<div class="file-meta">{{ item.type|title }}</div>
</div>
</div>
</div>
<div class="dropdown">
<button class="dropdown-toggle" onclick="toggleDropdown(event, this)">
<svg class="icon" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/>
</svg>
</button>
<div class="dropdown-menu">
{% if item.type == 'file' %}
<button class="dropdown-item" onclick="download('{{ item.path }}')">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Download
</button>
{% endif %}
<button class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Rename
</button>
<button class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
Delete
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<h3>No files yet</h3>
<p>Upload your first file or create a folder to get started.</p>
</div>
{% endif %}
</main>
</div>
<!-- Modals -->
<!-- Rename Modal -->
<div class="modal-overlay" id="renameModal">
<div class="modal">
<h3 class="modal-title">Rename Item</h3>
<div class="modal-body">
<input type="text" class="modal-input" id="renameInput" placeholder="Enter new name">
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal('renameModal')">Cancel</button>
<button class="btn btn-primary" onclick="confirmRename()" id="renameConfirmBtn">
<span id="renameBtnText">Rename</span>
<span id="renameBtnSpinner" class="loading-spinner" style="display: none;"></span>
</button>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal-overlay" id="deleteModal">
<div class="modal">
<h3 class="modal-title">Confirm Deletion</h3>
<div class="modal-body">
<p>Are you sure you want to delete <strong id="deleteItemName"></strong>? This action cannot be undone.</p>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
<button class="btn btn-danger" onclick="confirmDelete()" id="deleteConfirmBtn">
<span id="deleteBtnText">Delete</span>
<span id="deleteBtnSpinner" class="loading-spinner" style="display: none;"></span>
</button>
</div>
</div>
</div>
<!-- Folder Modal -->
<div class="modal-overlay" id="folderModal">
<div class="modal">
<h3 class="modal-title">Create New Folder</h3>
<div class="modal-body">
<input type="text" class="modal-input" id="folderInput" placeholder="Enter folder name">
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal('folderModal')">Cancel</button>
<button class="btn btn-primary" onclick="confirmCreateFolder()" id="folderConfirmBtn">
<span id="folderBtnText">Create</span>
<span id="folderBtnSpinner" class="loading-spinner" style="display: none;"></span>
</button>
</div>
</div>
</div>
<!-- URL Download Modal -->
<div class="modal-overlay" id="urlDownloadModal">
<div class="modal">
<h3 class="modal-title">Download from URL</h3>
<div class="modal-body">
<input type="text" class="modal-input" id="urlInput" placeholder="https://example.com/file.pdf" style="margin-bottom: 1rem;">
<input type="text" class="modal-input" id="filenameInput" placeholder="Custom filename (optional)">
<p style="font-size: 0.875rem; color: var(--text-muted); margin-top: 0.5rem;">
The file will be downloaded directly to your HuggingFace repository.
</p>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal('urlDownloadModal')">Cancel</button>
<button class="btn btn-success" onclick="confirmUrlDownload()" id="urlDownloadConfirmBtn">
<span id="urlDownloadBtnText">Download</span>
<span id="urlDownloadBtnSpinner" class="loading-spinner" style="display: none;"></span>
</button>
</div>
</div>
</div>
<script>
// Global state
let currentRenamePath = '';
let currentDeletePath = '';
let currentFolderPath = '';
let currentUrlDownloadPath = '';
let activeDropdown = null;
// Navigation
function nav(path) {
window.location.href = '/?path=' + encodeURIComponent(path);
}
function download(path) {
window.open('/download?path=' + encodeURIComponent(path), '_blank');
}
// Dropdown management
function toggleDropdown(event, button) {
event.stopPropagation();
const dropdown = button.closest('.dropdown');
if (activeDropdown && activeDropdown !== dropdown) {
activeDropdown.classList.remove('active');
}
dropdown.classList.toggle('active');
activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
}
function closeAllDropdowns() {
if (activeDropdown) {
activeDropdown.classList.remove('active');
activeDropdown = null;
}
}
// Loading state management
function showSpinner(btnId, textId, spinnerId) {
const btn = document.getElementById(btnId);
const text = document.getElementById(textId);
const spinner = document.getElementById(spinnerId);
btn.disabled = true;
text.style.display = 'none';
spinner.style.display = 'inline-block';
}
function hideSpinner(btnId, textId, spinnerId) {
const btn = document.getElementById(btnId);
const text = document.getElementById(textId);
const spinner = document.getElementById(spinnerId);
btn.disabled = false;
text.style.display = 'inline-block';
spinner.style.display = 'none';
}
// Modal management
function showModal(modalId) {
const modal = document.getElementById(modalId);
modal.classList.add('active');
document.body.style.overflow = 'hidden';
// Focus first input in modal
const firstInput = modal.querySelector('.modal-input');
if (firstInput) {
setTimeout(() => {
firstInput.focus();
if (firstInput.type === 'text') {
firstInput.select();
}
}, 100);
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
modal.classList.remove('active');
document.body.style.overflow = '';
}
// Modal show functions
function showRenameModal(path, currentName) {
currentRenamePath = path;
document.getElementById('renameInput').value = currentName;
showModal('renameModal');
closeAllDropdowns();
}
function showDeleteModal(path, name) {
currentDeletePath = path;
document.getElementById('deleteItemName').textContent = name;
showModal('deleteModal');
closeAllDropdowns();
}
function showFolderModal(path) {
currentFolderPath = path;
document.getElementById('folderInput').value = '';
showModal('folderModal');
}
function showUrlDownloadModal(path) {
currentUrlDownloadPath = path;
document.getElementById('urlInput').value = '';
document.getElementById('filenameInput').value = '';
showModal('urlDownloadModal');
}
// Generic action performer
async function performAction(url, body, btnId, textId, spinnerId, modalToClose, successMessage) {
showSpinner(btnId, textId, spinnerId);
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const result = await response.json();
if (response.ok) {
if (modalToClose) closeModal(modalToClose);
if (successMessage) {
// Show brief success message
showUploadOverlay(successMessage);
setTimeout(() => hideUploadOverlay(), 1500);
}
setTimeout(() => window.location.reload(), successMessage ? 1500 : 0);
} else {
throw new Error(result.error || 'Operation failed');
}
} catch (error) {
console.error('Action failed:', error);
alert(`Operation failed: ${error.message}`);
} finally {
hideSpinner(btnId, textId, spinnerId);
}
}
// Action confirmations
function confirmRename() {
const newName = document.getElementById('renameInput').value.trim();
if (!newName) {
alert('Please enter a valid name');
return;
}
performAction(
'/rename',
{ old_path: currentRenamePath, new_path: newName },
'renameConfirmBtn',
'renameBtnText',
'renameBtnSpinner',
'renameModal',
'Item renamed successfully!'
);
}
function confirmDelete() {
performAction(
'/delete',
{ path: currentDeletePath },
'deleteConfirmBtn',
'deleteBtnText',
'deleteBtnSpinner',
'deleteModal',
'Item deleted successfully!'
);
}
function confirmCreateFolder() {
const folderName = document.getElementById('folderInput').value.trim();
if (!folderName) {
alert('Please enter a folder name');
return;
}
const folderPath = currentFolderPath ? `${currentFolderPath}/${folderName}` : folderName;
performAction(
'/create_folder',
{ path: folderPath },
'folderConfirmBtn',
'folderBtnText',
'folderBtnSpinner',
'folderModal',
'Folder created successfully!'
);
}
function confirmUrlDownload() {
const url = document.getElementById('urlInput').value.trim();
const customFilename = document.getElementById('filenameInput').value.trim();
if (!url) {
alert('Please enter a valid URL');
return;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
alert('Please enter a valid HTTP/HTTPS URL');
return;
}
performAction(
'/download_url',
{
url: url,
path: currentUrlDownloadPath,
filename: customFilename || null
},
'urlDownloadConfirmBtn',
'urlDownloadBtnText',
'urlDownloadBtnSpinner',
'urlDownloadModal',
'File downloaded successfully!'
);
}
// Upload overlay management
function showUploadOverlay(message = 'Processing...') {
const overlay = document.getElementById('uploadOverlay');
const text = overlay.querySelector('.upload-text');
text.textContent = message;
overlay.classList.add('active');
}
function hideUploadOverlay() {
document.getElementById('uploadOverlay').classList.remove('active');
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
// File input change handler
document.getElementById('fileInput').addEventListener('change', function(e) {
if (this.files.length > 0) {
showUploadOverlay('Uploading file...');
document.getElementById('uploadForm').submit();
}
});
// Modal keyboard handlers
const setupModalKeyListener = (inputId, confirmFunction) => {
const input = document.getElementById(inputId);
if (input) {
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
confirmFunction();
}
});
}
};
setupModalKeyListener('renameInput', confirmRename);
setupModalKeyListener('folderInput', confirmCreateFolder);
setupModalKeyListener('urlInput', confirmUrlDownload);
setupModalKeyListener('filenameInput', confirmUrlDownload);
// Global click handler
document.addEventListener('click', (e) => {
// Close dropdowns when clicking outside
if (!e.target.closest('.dropdown')) {
closeAllDropdowns();
}
// Close modal when clicking overlay
if (e.target.classList.contains('modal-overlay')) {
closeModal(e.target.id);
}
});
// Global keyboard handler
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const openModal = document.querySelector('.modal-overlay.active');
if (openModal) {
closeModal(openModal.id);
} else {
closeAllDropdowns();
}
}
});
});
// Prevent form submission on enter in modals (except for specific inputs)
document.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.closest('.modal') && e.target.tagName !== 'BUTTON') {
const modal = e.target.closest('.modal-overlay');
if (modal) {
e.preventDefault();
}
}
});
</script>
</body>
</html>
"""
def list_folder(path=""):
"""List files and folders in the given path"""
prefix = path.strip("/") + ("/" if path else "")
try:
all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
except Exception as e:
print(f"Error listing files: {e}")
return []
seen = set()
items = []
for f in all_files:
if not f.startswith(prefix):
continue
rest = f[len(prefix):]
if "/" in rest:
# This is a subdirectory
dir_name = rest.split("/")[0]
dir_path = (prefix + dir_name).strip("/")
if dir_path not in seen:
seen.add(dir_path)
items.append({
"type": "dir",
"name": dir_name,
"path": dir_path
})
else:
# This is a file
if rest: # Skip empty filenames
items.append({
"type": "file",
"name": rest,
"path": (prefix + rest).strip("/")
})
# Sort directories first, then files, both alphabetically
items.sort(key=lambda x: (x["type"] != "dir", x["name"].lower()))
return items
def get_filename_from_url(url, custom_filename=None):
"""Extract filename from URL or use custom filename"""
if custom_filename:
return custom_filename
# Parse URL and get the path
parsed_url = urlparse(url)
path = unquote(parsed_url.path)
# Get filename from path
filename = os.path.basename(path)
# If no filename found, generate one
if not filename or '.' not in filename:
filename = f"downloaded_file_{int(time.time())}"
return filename
@app.route("/", methods=["GET"])
def index():
"""Main page - file browser"""
path = request.args.get("path", "").strip("/")
items = list_folder(path)
return render_template_string(TEMPLATE, items=items, path=path)
@app.route("/download", methods=["GET"])
def download():
"""Download a file from the repository"""
file_path = request.args.get("path", "")
if not file_path:
return "No file path provided", 400
try:
# Download file to temporary location
local_path = hf_hub_download(
repo_id=REPO_ID,
filename=file_path,
repo_type="dataset",
token=HF_TOKEN,
cache_dir=tempfile.gettempdir()
)
# Send file to user
return send_file(
local_path,
as_attachment=True,
download_name=os.path.basename(file_path)
)
except Exception as e:
return f"Error downloading file: {str(e)}", 500
@app.route("/upload", methods=["POST"])
def upload():
"""Upload a file to the repository"""
if 'file' not in request.files:
return "No file provided", 400
file = request.files["file"]
if file.filename == '':
return "No file selected", 400
path = request.form.get("path", "").strip("/")
# Create destination path
dest_path = f"{path}/{file.filename}".strip("/")
# Save file to temporary location
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
file.save(tmp_file.name)
try:
# Upload to HuggingFace
upload_file(
path_or_fileobj=tmp_file.name,
path_in_repo=dest_path,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN
)
except Exception as e:
os.unlink(tmp_file.name)
return f"Error uploading file: {str(e)}", 500
finally:
# Clean up temporary file
try:
os.unlink(tmp_file.name)
except:
pass
return redirect(f"/?path={path}")
@app.route("/download_url", methods=["POST"])
def download_url():
"""Download a file from URL and upload to repository"""
data = request.get_json()
url = data.get("url", "").strip()
path = data.get("path", "").strip("/")
custom_filename = data.get("filename", "").strip()
if not url:
return jsonify({"error": "No URL provided"}), 400
if not url.startswith(('http://', 'https://')):
return jsonify({"error": "Invalid URL. Must start with http:// or https://"}), 400
try:
# Get filename
filename = get_filename_from_url(url, custom_filename)
dest_path = f"{path}/{filename}".strip("/")
# Download file from URL
response = requests.get(url, stream=True, timeout=30)
response.raise_for_status()
# Check if content-length is reasonable (max 500MB)
content_length = response.headers.get('content-length')
if content_length and int(content_length) > 500 * 1024 * 1024:
return jsonify({"error": "File too large. Maximum size is 500MB"}), 400
# Save to temporary file
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
# Download in chunks
for chunk in response.iter_content(chunk_size=8192):
tmp_file.write(chunk)
tmp_file.flush()
# Upload to HuggingFace
upload_file(
path_or_fileobj=tmp_file.name,
path_in_repo=dest_path,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN
)
# Clean up
os.unlink(tmp_file.name)
return jsonify({"status": "success", "message": "File downloaded and uploaded successfully"})
except requests.exceptions.RequestException as e:
return jsonify({"error": f"Failed to download from URL: {str(e)}"}), 400
except Exception as e:
return jsonify({"error": f"Upload failed: {str(e)}"}), 500
@app.route("/delete", methods=["POST"])
def delete():
"""Delete a file or folder from the repository"""
data = request.get_json()
delete_path = data.get("path", "").strip("/")
if not delete_path:
return jsonify({"error": "No path provided"}), 400
try:
all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
deleted_count = 0
for file_path in all_files:
# Delete exact match or files within the folder
if file_path == delete_path or file_path.startswith(delete_path.rstrip("/") + "/"):
delete_file(
repo_id=REPO_ID,
path_in_repo=file_path,
repo_type="dataset",
token=HF_TOKEN
)
deleted_count += 1
if deleted_count == 0:
return jsonify({"error": "No files found to delete"}), 404
return jsonify({"status": "success", "message": f"Deleted {deleted_count} file(s)"})
except Exception as e:
return jsonify({"error": f"Delete failed: {str(e)}"}), 500
@app.route("/create_folder", methods=["POST"])
def create_folder():
"""Create a new folder by uploading a .keep file"""
data = request.get_json()
folder_path = data.get("path", "").strip("/")
if not folder_path:
return jsonify({"error": "No folder path provided"}), 400
# Create .keep file path
keep_file_path = f"{folder_path}/.keep"
try:
# Create temporary empty file
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
tmp_file.write(b"# This file keeps the folder in git\n")
tmp_file.flush()
# Upload .keep file
upload_file(
path_or_fileobj=tmp_file.name,
path_in_repo=keep_file_path,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN
)
# Clean up
os.unlink(tmp_file.name)
return jsonify({"status": "success", "message": "Folder created successfully"})
except Exception as e:
return jsonify({"error": f"Failed to create folder: {str(e)}"}), 500
@app.route("/rename", methods=["POST"])
def rename():
"""Rename a file or folder"""
data = request.get_json()
old_path = data.get("old_path", "").strip("/")
new_name = data.get("new_path", "").strip()
if not old_path or not new_name:
return jsonify({"error": "Missing old path or new name"}), 400
try:
# Get parent directory
parent_dir = "/".join(old_path.split("/")[:-1]) if "/" in old_path else ""
new_path = f"{parent_dir}/{new_name}".strip("/")
all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
renamed_count = 0
for file_path in all_files:
if file_path == old_path or file_path.startswith(old_path + "/"):
# Calculate new file path
relative_path = file_path[len(old_path):].lstrip("/")
new_file_path = (new_path + "/" + relative_path).strip("/")
# Download original file
local_path = hf_hub_download(
repo_id=REPO_ID,
filename=file_path,
repo_type="dataset",
token=HF_TOKEN,
cache_dir=tempfile.gettempdir()
)
# Upload with new name
upload_file(
path_or_fileobj=local_path,
path_in_repo=new_file_path,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN
)
# Delete original file
delete_file(
repo_id=REPO_ID,
path_in_repo=file_path,
repo_type="dataset",
token=HF_TOKEN
)
renamed_count += 1
if renamed_count == 0:
return jsonify({"error": "No files found to rename"}), 404
return jsonify({"status": "success", "message": f"Renamed {renamed_count} file(s)"})
except Exception as e:
return jsonify({"error": f"Rename failed: {str(e)}"}), 500
if __name__ == "__main__":
# Add missing import
import time
app.run(debug=True, host="0.0.0.0", port=7860)