cronjob / app.py
testdeep123's picture
Update app.py
79ef1fc verified
raw
history blame
47.1 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 {
--primary: #6366f1;
--primary-hover: #5855eb;
--primary-light: rgba(99, 102, 241, 0.1);
--secondary: #64748b;
--secondary-hover: #475569;
--success: #10b981;
--success-hover: #059669;
--warning: #f59e0b;
--warning-hover: #d97706;
--danger: #ef4444;
--danger-hover: #dc2626;
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--bg-card: #1e293b;
--bg-overlay: rgba(15, 23, 42, 0.9);
--text-primary: #f8fafc;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
--border: #334155;
--border-light: #475569;
--border-focus: var(--primary);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.25);
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-spring: 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Base Styles */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Layout */
.container {
max-width: 1200px;
margin: 0 auto;
padding: clamp(1rem, 3vw, 1.5rem);
}
/* Header */
.header {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: clamp(1.5rem, 4vw, 2rem);
margin-bottom: 2rem;
box-shadow: var(--shadow-lg);
animation: slideDown var(--transition-spring);
will-change: transform;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.title {
font-size: clamp(1.75rem, 5vw, 2.5rem);
font-weight: 800;
color: var(--primary);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: clamp(0.5rem, 2vw, 1rem);
animation: fadeIn var(--transition-slow) var(--transition-fast);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.title-icon {
width: clamp(2.5rem, 6vw, 3rem);
height: clamp(2.5rem, 6vw, 3rem);
color: var(--primary);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
/* Breadcrumb */
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 2rem;
flex-wrap: wrap;
animation: fadeIn var(--transition-slow) calc(var(--transition-fast) * 2);
}
.breadcrumb-item {
background: var(--bg-tertiary);
color: var(--text-secondary);
padding: 0.5rem 1rem;
border-radius: var(--radius-md);
font-size: clamp(0.75rem, 2vw, 0.875rem);
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
gap: 0.5rem;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transform: translateZ(0);
backface-visibility: hidden;
}
.breadcrumb-item:hover {
background: var(--primary);
color: white;
transform: translateY(-2px) scale(1.05);
box-shadow: var(--shadow-md);
}
.breadcrumb-item.active {
background: var(--primary);
color: white;
animation: scaleIn var(--transition-spring);
}
@keyframes scaleIn {
from {
transform: scale(0.9);
}
to {
transform: scale(1);
}
}
.breadcrumb-separator {
color: var(--text-muted);
font-size: 1.25rem;
opacity: 0.5;
}
/* Actions */
.actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
gap: 1rem;
animation: fadeIn var(--transition-slow) calc(var(--transition-fast) * 3);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--radius-md);
font-weight: 600;
font-size: clamp(0.8rem, 2vw, 0.875rem);
cursor: pointer;
transition: all var(--transition-fast);
text-decoration: none;
min-height: 2.75rem;
position: relative;
overflow: hidden;
transform: translateZ(0);
backface-visibility: hidden;
-webkit-tap-highlight-color: transparent;
}
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
transform: translate(-50%, -50%);
transition: width var(--transition-slow), height var(--transition-slow);
}
.btn:active::before {
width: 300px;
height: 300px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn-primary {
background: var(--primary);
color: white;
box-shadow: var(--shadow-md);
}
.btn-primary:hover:not(:disabled) {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border);
}
.btn-secondary:hover:not(:disabled) {
background: var(--secondary);
border-color: var(--border-light);
transform: translateY(-1px);
}
.btn-success {
background: var(--success);
color: white;
}
.btn-success:hover:not(:disabled) {
background: var(--success-hover);
transform: translateY(-2px);
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-danger:hover:not(:disabled) {
background: var(--danger-hover);
transform: translateY(-2px);
}
/* File Input */
.file-input-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
width: 100%;
}
.file-input {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
/* File Grid */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
gap: clamp(1rem, 2vw, 1.5rem);
animation: staggerIn var(--transition-slow);
}
@keyframes staggerIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.file-item {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
transition: all var(--transition-base);
position: relative;
transform: translateZ(0);
backface-visibility: hidden;
animation: itemFadeIn var(--transition-slow) backwards;
animation-delay: calc(var(--item-index, 0) * 50ms);
}
@keyframes itemFadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.file-item:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-xl);
border-color: var(--primary);
}
.file-item-content {
padding: clamp(1rem, 3vw, 1.5rem);
cursor: pointer;
}
.file-info {
display: flex;
align-items: center;
gap: clamp(0.75rem, 2vw, 1rem);
}
.file-icon {
width: clamp(2.5rem, 6vw, 3rem);
height: clamp(2.5rem, 6vw, 3rem);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: clamp(1.25rem, 3vw, 1.5rem);
transition: transform var(--transition-fast);
}
.file-item:hover .file-icon {
transform: scale(1.1) rotate(5deg);
}
.file-icon.folder {
background: var(--warning);
color: white;
}
.file-icon.file {
background: var(--primary-light);
color: var(--primary);
}
.file-details {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.25rem;
word-break: break-word;
font-size: clamp(0.875rem, 2vw, 1rem);
}
.file-meta {
font-size: clamp(0.7rem, 1.5vw, 0.75rem);
color: var(--text-muted);
}
/* Dropdown */
.dropdown {
position: absolute;
top: 1rem;
right: 1rem;
}
.dropdown-toggle {
width: 2.5rem;
height: 2.5rem;
border-radius: var(--radius-md);
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
transform: translateZ(0);
}
.dropdown-toggle:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
transform: scale(1.1);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 0.5rem;
min-width: 180px;
opacity: 0;
visibility: hidden;
transform: translateY(-10px) scale(0.95);
transition: all var(--transition-fast);
z-index: 1000;
box-shadow: var(--shadow-xl);
backdrop-filter: blur(10px);
}
.dropdown.active .dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
font-size: 0.875rem;
color: var(--text-secondary);
width: 100%;
text-align: left;
border: none;
background: none;
position: relative;
overflow: hidden;
}
.dropdown-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 0;
height: 100%;
background: var(--bg-tertiary);
transition: width var(--transition-fast);
z-index: -1;
}
.dropdown-item:hover::before {
width: 100%;
}
.dropdown-item:hover {
color: var(--text-primary);
transform: translateX(4px);
}
.dropdown-item.danger:hover {
background: var(--danger);
color: white;
}
.dropdown-item.danger:hover::before {
background: var(--danger);
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: var(--bg-overlay);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
opacity: 0;
visibility: hidden;
transition: all var(--transition-base);
padding: 1rem;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
padding: clamp(1.5rem, 4vw, 2rem);
width: 100%;
max-width: min(500px, 90vw);
transform: scale(0.9) translateY(20px);
transition: all var(--transition-spring);
box-shadow: var(--shadow-xl);
max-height: 90vh;
overflow-y: auto;
}
.modal-overlay.active .modal {
transform: scale(1) translateY(0);
}
.modal-title {
font-size: clamp(1.25rem, 3vw, 1.5rem);
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--text-primary);
animation: slideInLeft var(--transition-base) var(--transition-fast);
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.modal-body {
margin-bottom: 2rem;
animation: fadeIn var(--transition-base) calc(var(--transition-fast) * 2);
}
.modal-input, .modal-textarea {
width: 100%;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: clamp(0.75rem, 2vw, 1rem);
color: var(--text-primary);
font-size: clamp(0.875rem, 2vw, 1rem);
transition: all var(--transition-fast);
resize: vertical;
}
.modal-textarea {
min-height: 120px;
font-family: inherit;
}
.modal-input:focus, .modal-textarea:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
transform: translateY(-1px);
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
animation: slideInUp var(--transition-base) calc(var(--transition-fast) * 3);
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Upload Overlay */
.upload-overlay {
position: fixed;
inset: 0;
background: var(--bg-overlay);
backdrop-filter: blur(12px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
opacity: 0;
visibility: hidden;
transition: all var(--transition-base);
gap: 1.5rem;
}
.upload-overlay.active {
opacity: 1;
visibility: visible;
}
.upload-text {
font-size: clamp(1rem, 3vw, 1.25rem);
font-weight: 600;
color: var(--text-primary);
animation: fadeInScale var(--transition-spring);
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Loading Spinner */
.loading-spinner {
width: 2rem;
height: 2rem;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
will-change: transform;
}
.upload-overlay .loading-spinner {
width: clamp(2.5rem, 6vw, 3rem);
height: clamp(2.5rem, 6vw, 3rem);
border-width: 4px;
animation: spinScale 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes spinScale {
0% { transform: rotate(0deg) scale(1); }
50% { transform: rotate(180deg) scale(1.1); }
100% { transform: rotate(360deg) scale(1); }
}
/* Utilities */
.icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
transition: transform var(--transition-fast);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Empty State */
.empty-state {
text-align: center;
padding: clamp(3rem, 8vw, 4rem) 2rem;
color: var(--text-muted);
animation: fadeIn var(--transition-slow);
}
.empty-state .icon {
width: clamp(3rem, 8vw, 4rem);
height: clamp(3rem, 8vw, 4rem);
margin: 0 auto 1rem;
opacity: 0.5;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.empty-state h3 {
font-size: clamp(1.25rem, 3vw, 1.5rem);
margin-bottom: 0.5rem;
}
.empty-state p {
font-size: clamp(0.875rem, 2vw, 1rem);
}
/* Performance optimizations */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Responsive Design */
@media (max-width: 768px) {
.breadcrumb-separator {
display: none;
}
.breadcrumb {
gap: 0.25rem;
}
.breadcrumb-item {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
}
.file-grid {
grid-template-columns: 1fr;
}
.modal-actions {
flex-direction: column-reverse;
}
.modal-actions .btn {
width: 100%;
}
.dropdown-menu {
position: fixed;
top: auto;
bottom: 1rem;
right: 1rem;
left: 1rem;
max-width: 400px;
margin: 0 auto;
}
}
@media (max-width: 480px) {
.breadcrumb-item {
max-width: 120px;
}
.file-item {
animation: none;
}
.title {
flex-wrap: wrap;
text-align: center;
justify-content: center;
}
.actions {
gap: 0.75rem;
}
}
/* Touch device optimizations */
@media (hover: none) and (pointer: coarse) {
.btn {
min-height: 3rem;
padding: 1rem 1.5rem;
}
.dropdown-toggle {
width: 3rem;
height: 3rem;
}
.file-item:active {
transform: scale(0.98);
}
.btn:active {
transform: scale(0.95);
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--primary: #818cf8;
--bg-primary: #000;
--bg-secondary: #1a1a1a;
--text-primary: #fff;
--border: #666;
}
}
/* Dark mode optimizations */
@media (prefers-color-scheme: dark) {
.loading-spinner {
filter: brightness(1.2);
}
}
/* Print styles */
@media print {
.header, .actions, .dropdown, .modal-overlay, .upload-overlay {
display: none !important;
}
.file-grid {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.file-item {
border: 1px solid #000;
page-break-inside: avoid;
}
}
</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)