testdeep123 commited on
Commit
4a69f98
·
verified ·
1 Parent(s): b1c4e38

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +699 -627
app.py CHANGED
@@ -1,681 +1,753 @@
1
  import os
2
  import tempfile
3
  from flask import Flask, render_template_string, request, redirect, send_file, jsonify
4
- from huggingface_hub import HfApi, hf_hub_download, upload_file, delete_file, HfFolder
5
- from urllib.parse import unquote, quote
6
-
7
- # --- FIX FOR PERMISSION ERROR ---
8
- # In restricted environments (like some containers), the default Hugging Face cache
9
- # directory (~/.cache/huggingface) might not be writable. We override this by setting
10
- # the HF_HOME environment variable to a writable directory.
11
- # This must be done BEFORE any huggingface_hub functions are called.
12
- HF_CACHE_DIR = os.path.join(tempfile.gettempdir(), "hf_home")
13
- os.makedirs(HF_CACHE_DIR, exist_ok=True)
14
- os.environ["HF_HOME"] = HF_CACHE_DIR
15
- # --------------------------------
16
-
17
- # --- Environment Setup ---
18
- # IMPORTANT: Set these environment variables before running the script.
19
- # You can get a token from https://huggingface.co/settings/tokens
20
  REPO_ID = os.getenv("REPO_ID")
21
  HF_TOKEN = os.getenv("HF_TOKEN")
22
 
23
- # For local testing, you can uncomment and set them here:
24
- # REPO_ID = "YourUsername/YourDatasetRepoName"
25
- # HF_TOKEN = "hf_..."
26
-
27
- # Ensure the hf_hub library uses the token. This will now write to the writable HF_HOME dir.
28
- if HF_TOKEN:
29
- HfFolder.save_token(HF_TOKEN)
30
-
31
  app = Flask(__name__)
32
  api = HfApi()
33
 
34
- # --- THE ALL-IN-ONE TEMPLATE (HTML, CSS, JS) ---
35
  TEMPLATE = """
36
  <!DOCTYPE html>
37
- <html lang="en">
38
  <head>
39
- <meta charset="UTF-8">
40
- <title>HuggingFace Drive - {{ path or 'Root' }}</title>
41
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
42
- <link rel="preconnect" href="https://fonts.googleapis.com">
43
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
44
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
45
- <style>
46
- :root {
47
- --bg-deep-space: #0D0C22;
48
- --bg-surface: #1A1936;
49
- --bg-surface-hover: #212042;
50
- --primary-accent: #8A2BE2;
51
- --primary-accent-glow: rgba(138, 43, 226, 0.5);
52
- --secondary-accent: #4D4DFF;
53
- --text-primary: #F0F0F0;
54
- --text-secondary: #A0A0B0;
55
- --border-color: #2A294A;
56
- --danger-color: #FF5577;
57
- --danger-glow: rgba(255, 85, 119, 0.4);
58
- --font-family: 'Inter', sans-serif;
59
- }
60
-
61
- * {
62
- margin: 0;
63
- padding: 0;
64
- box-sizing: border-box;
65
- }
66
-
67
- body {
68
- background-color: var(--bg-deep-space);
69
- color: var(--text-primary);
70
- font-family: var(--font-family);
71
- min-height: 100vh;
72
- overflow-x: hidden;
73
- }
74
-
75
- .container {
76
- max-width: 1200px;
77
- margin: 0 auto;
78
- padding: 32px;
79
- }
80
-
81
- /* Header Panel */
82
- .header-panel {
83
- background: var(--bg-surface);
84
- border: 1px solid var(--border-color);
85
- border-radius: 20px;
86
- padding: 24px;
87
- margin-bottom: 32px;
88
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
89
- }
90
-
91
- .title-bar {
92
- display: flex;
93
- align-items: center;
94
- gap: 12px;
95
- margin-bottom: 20px;
96
- }
97
-
98
- .title {
99
- font-size: 2.25rem;
100
- font-weight: 700;
101
- background: linear-gradient(90deg, var(--primary-accent), var(--secondary-accent));
102
- -webkit-background-clip: text;
103
- -webkit-text-fill-color: transparent;
104
- background-clip: text;
105
- }
106
-
107
- /* Breadcrumbs */
108
- .breadcrumb {
109
- display: flex;
110
- align-items: center;
111
- gap: 6px;
112
- flex-wrap: wrap;
113
- margin-bottom: 24px;
114
- }
115
- .breadcrumb-item {
116
- color: var(--text-secondary);
117
- font-size: 0.9rem;
118
- cursor: pointer;
119
- transition: color 0.2s ease;
120
- }
121
- .breadcrumb-item:hover { color: var(--text-primary); }
122
- .breadcrumb-item.active { color: var(--text-primary); font-weight: 500; cursor: default; }
123
- .breadcrumb-separator { color: var(--text-secondary); }
124
-
125
- /* Actions */
126
- .actions {
127
- display: flex;
128
- gap: 12px;
129
- flex-wrap: wrap;
130
- }
131
- .btn {
132
- background: linear-gradient(90deg, var(--primary-accent), var(--secondary-accent));
133
- color: white;
134
- border: none;
135
- padding: 10px 20px;
136
- border-radius: 10px;
137
- cursor: pointer;
138
- font-weight: 600;
139
- font-size: 0.9rem;
140
- transition: all 0.3s ease;
141
- box-shadow: 0 4px 15px rgba(0,0,0,0.2), 0 0 20px var(--primary-accent-glow);
142
- position: relative;
143
- overflow: hidden;
144
- }
145
- .btn:hover {
146
- transform: translateY(-2px);
147
- box-shadow: 0 6px 20px rgba(0,0,0,0.3), 0 0 30px var(--primary-accent-glow);
148
- }
149
- .btn-secondary {
150
- background: var(--bg-surface-hover);
151
- box-shadow: 0 4px 15px rgba(0,0,0,0.2);
152
- }
153
- #fileInput { display: none; }
154
-
155
- /* File Grid */
156
- .file-grid {
157
- display: grid;
158
- gap: 20px;
159
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
160
- }
161
-
162
- .file-item {
163
- background: var(--bg-surface);
164
- border: 1px solid var(--border-color);
165
- border-radius: 16px;
166
- padding: 20px;
167
- transition: all 0.3s ease;
168
- cursor: pointer;
169
- position: relative;
170
- overflow: hidden;
171
- }
172
- /* --- The Magic Glow Effect --- */
173
- .file-item::before {
174
- content: "";
175
- position: absolute;
176
- left: var(--x, 50%);
177
- top: var(--y, 50%);
178
- transform: translate(-50%, -50%);
179
- width: 0;
180
- height: 0;
181
- background: radial-gradient(circle closest-side, var(--primary-accent-glow), transparent);
182
- border-radius: 50%;
183
- opacity: 0;
184
- transition: width 0.4s ease, height 0.4s ease, opacity 0.4s ease;
185
- }
186
- .file-item:hover::before {
187
- width: 300px;
188
- height: 300px;
189
- opacity: 0.5;
190
- }
191
- .file-item:hover {
192
- transform: translateY(-5px) scale(1.02);
193
- border-color: var(--primary-accent);
194
- box-shadow: 0 15px 30px rgba(0,0,0,0.4);
195
- }
196
-
197
- .file-content {
198
- display: flex;
199
- align-items: center;
200
- justify-content: space-between;
201
- position: relative; z-index: 2;
202
- }
203
- .file-info {
204
- display: flex;
205
- align-items: center;
206
- gap: 16px;
207
- flex-grow: 1;
208
- min-width: 0; /* Prevents text overflow issues */
209
- }
210
- .file-icon { font-size: 1.8rem; }
211
- .file-name {
212
- font-weight: 500;
213
- font-size: 1rem;
214
- white-space: nowrap;
215
- overflow: hidden;
216
- text-overflow: ellipsis;
217
- }
218
-
219
- /* Dropdown Menu */
220
- .dropdown { position: relative; }
221
- .dropdown-toggle {
222
- background: transparent; border: none; color: var(--text-secondary);
223
- padding: 8px; border-radius: 50%; width: 36px; height: 36px;
224
- cursor: pointer; transition: all 0.2s ease; font-size: 1.25rem;
225
- }
226
- .dropdown-toggle:hover { background: var(--bg-surface-hover); color: var(--text-primary); }
227
- .dropdown-menu {
228
- position: absolute; top: 110%; right: 0;
229
- background: #111028; backdrop-filter: blur(10px);
230
- border: 1px solid var(--border-color); border-radius: 10px;
231
- padding: 8px; min-width: 160px;
232
- opacity: 0; visibility: hidden;
233
- transform: translateY(10px) scale(0.95);
234
- transition: all 0.2s ease; z-index: 100;
235
- box-shadow: 0 10px 30px rgba(0,0,0,0.5);
236
- }
237
- .dropdown.active .dropdown-menu {
238
- opacity: 1; visibility: visible;
239
- transform: translateY(0) scale(1);
240
- }
241
- .dropdown-item {
242
- display: flex; align-items: center; gap: 10px;
243
- padding: 10px 12px; border-radius: 6px; cursor: pointer;
244
- transition: all 0.2s ease; font-size: 0.9rem;
245
- color: var(--text-secondary);
246
- }
247
- .dropdown-item:hover { background: var(--bg-surface-hover); color: var(--text-primary); }
248
- .dropdown-item.danger:hover {
249
- background: rgba(255, 85, 119, 0.1);
250
- color: var(--danger-color);
251
- }
252
-
253
- /* Modals */
254
- .modal-overlay {
255
- position: fixed; inset: 0; background: rgba(0,0,0,0.7);
256
- backdrop-filter: blur(5px); z-index: 2000;
257
- opacity: 0; visibility: hidden; transition: all 0.3s ease;
258
- }
259
- .modal-overlay.active { opacity: 1; visibility: visible; }
260
- .modal {
261
- position: fixed; top: 50%; left: 50%;
262
- background: var(--bg-surface); border: 1px solid var(--border-color);
263
- border-radius: 20px; padding: 32px;
264
- max-width: 420px; width: 90%;
265
- box-shadow: 0 20px 60px rgba(0,0,0,0.6);
266
- opacity: 0; visibility: hidden;
267
- transform: translate(-50%, -50%) scale(0.9);
268
- transition: all 0.3s ease; z-index: 2001;
269
- }
270
- .modal-overlay.active .modal {
271
- opacity: 1; visibility: visible; transform: translate(-50%, -50%) scale(1);
272
- }
273
- .modal-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 16px; }
274
- .modal-body p { margin-bottom: 20px; color: var(--text-secondary); line-height: 1.6; }
275
- .modal-body strong { color: var(--primary-accent); font-weight: 500; }
276
- .modal-input {
277
- width: 100%; background: var(--bg-deep-space);
278
- border: 1px solid var(--border-color); border-radius: 10px;
279
- padding: 12px 16px; color: var(--text-primary);
280
- font-size: 1rem; font-family: var(--font-family); margin-bottom: 24px;
281
- }
282
- .modal-input:focus {
283
- outline: none; border-color: var(--primary-accent);
284
- box-shadow: 0 0 0 3px var(--primary-accent-glow);
285
- }
286
- .modal-actions { display: flex; gap: 12px; justify-content: flex-end; }
287
- .btn-danger {
288
- background: var(--danger-color);
289
- box-shadow: 0 4px 15px rgba(0,0,0,0.2), 0 0 20px var(--danger-glow);
290
- }
291
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  </head>
293
  <body>
294
- <div class="container">
295
- <div class="header-panel">
296
- <div class="title-bar">
297
- <span class="title">🚀 HuggingFace Drive</span>
298
- </div>
299
- <div class="breadcrumb">
300
- {% for crumb in breadcrumbs %}
301
- <span class="breadcrumb-item {{ 'active' if loop.last else '' }}" onclick="nav('{{ crumb.path }}')">{{ crumb.name }}</span>
302
- {% if not loop.last %}<span class="breadcrumb-separator">/</span>{% endif %}
303
- {% endfor %}
304
- </div>
305
- <div class="actions">
306
- <form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
307
- <input type="file" name="file" id="fileInput" onchange="this.form.submit()">
308
- <input type="hidden" name="path" value="{{ path }}">
309
- <label for="fileInput" class="btn">📤 Upload File</label>
310
- </form>
311
- <button class="btn btn-secondary" onclick="showCreateFolderModal('{{ path }}')">📁 New Folder</button>
312
- </div>
313
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
- <div class="file-grid">
316
- {% for item in items %}
317
- <div class="file-item" onmousemove="updateGlow(event, this)" onclick="handleItemClick(event, '{{ item.type }}', '{{ item.path }}')">
318
- <div class="file-content">
319
- <div class="file-info">
320
- <div class="file-icon">{{ '📁' if item.type=='dir' else '📄' }}</div>
321
- <div class="file-name">{{ item.name }}</div>
322
- </div>
323
- <div class="dropdown" onclick="event.stopPropagation()">
324
- <button class="dropdown-toggle" onclick="toggleDropdown(this)">⋮</button>
325
- <div class="dropdown-menu">
326
- {% if item.type == 'file' %}
327
- <div class="dropdown-item" onclick="download('{{ item.path }}')">📥 Download</div>
328
- {% endif %}
329
- <div class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">✏️ Rename</div>
330
- <div class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">🗑️ Delete</div>
331
- </div>
332
- </div>
 
 
 
 
333
  </div>
 
 
 
 
334
  </div>
335
- {% endfor %}
336
- {% if not items %}
337
- <p style="grid-column: 1 / -1; text-align: center; color: var(--text-secondary); padding: 40px;">This folder is empty.</p>
338
- {% endif %}
339
  </div>
 
340
  </div>
341
-
342
- <!-- Modals -->
343
- <div class="modal-overlay" id="modalOverlay" onclick="hideAllModals()"></div>
344
-
345
- <div class="modal" id="renameModal">
346
- <h3 class="modal-title">Rename Item</h3>
347
- <input type="text" class="modal-input" id="renameInput" placeholder="Enter new name">
348
- <div class="modal-actions">
349
- <button class="btn btn-secondary" onclick="hideAllModals()">Cancel</button>
350
- <button class="btn" onclick="confirmRename()">Rename</button>
351
- </div>
352
  </div>
353
-
354
- <div class="modal" id="deleteModal">
355
- <h3 class="modal-title">Confirm Deletion</h3>
356
- <div class="modal-body">
357
- <p>Are you sure you want to permanently delete <strong></strong>? This action cannot be undone.</p>
358
- </div>
359
- <div class="modal-actions">
360
- <button class="btn btn-secondary" onclick="hideAllModals()">Cancel</button>
361
- <button class="btn btn-danger" onclick="confirmDelete()">Delete</button>
362
- </div>
 
363
  </div>
364
-
365
- <div class="modal" id="createFolderModal">
366
- <h3 class="modal-title">Create New Folder</h3>
367
- <input type="text" class="modal-input" id="folderNameInput" placeholder="Enter folder name">
368
- <div class="modal-actions">
369
- <button class="btn btn-secondary" onclick="hideAllModals()">Cancel</button>
370
- <button class="btn" onclick="confirmCreateFolder()">Create</button>
371
- </div>
372
- </div>
373
-
374
- <script>
375
- let currentActionContext = {};
376
- let activeDropdown = null;
377
-
378
- // --- Navigation & Actions ---
379
- function nav(path) { location.href = '/?path=' + encodeURIComponent(path); }
380
- function download(path) { window.open('/download?path=' + encodeURIComponent(path), '_blank'); }
381
-
382
- function handleItemClick(event, type, path) {
383
- // Only navigate if the click is not on the dropdown area
384
- if (!event.target.closest('.dropdown')) {
385
- if (type === 'dir') {
386
- nav(path);
387
- }
388
- }
389
- }
390
-
391
- // --- Dynamic Glow Effect ---
392
- function updateGlow(event, element) {
393
- const rect = element.getBoundingClientRect();
394
- const x = event.clientX - rect.left;
395
- const y = event.clientY - rect.top;
396
- element.style.setProperty('--x', `${x}px`);
397
- element.style.setProperty('--y', `${y}px`);
398
- }
399
-
400
- // --- Dropdown Logic ---
401
- function toggleDropdown(button) {
402
- const dropdown = button.closest('.dropdown');
403
- if (activeDropdown && activeDropdown !== dropdown) {
404
- activeDropdown.classList.remove('active');
405
- }
406
- dropdown.classList.toggle('active');
407
- activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
408
- }
409
-
410
- function closeAllDropdowns() {
411
- document.querySelectorAll('.dropdown.active').forEach(d => d.classList.remove('active'));
412
- activeDropdown = null;
413
- }
414
-
415
- // --- Modal Logic ---
416
- function showModal(modalId) {
417
- closeAllDropdowns();
418
- document.getElementById('modalOverlay').classList.add('active');
419
- const modal = document.getElementById(modalId);
420
- modal.classList.add('active');
421
-
422
- const input = modal.querySelector('.modal-input');
423
- if (input) setTimeout(() => input.focus(), 150);
424
- }
425
-
426
- function hideAllModals() {
427
- document.getElementById('modalOverlay').classList.remove('active');
428
- document.querySelectorAll('.modal').forEach(m => m.classList.remove('active'));
429
- }
430
-
431
- function showRenameModal(path, name) {
432
- currentActionContext = { path, name };
433
- document.getElementById('renameInput').value = name;
434
- showModal('renameModal');
435
- }
436
-
437
- function showDeleteModal(path, name) {
438
- currentActionContext = { path };
439
- document.querySelector('#deleteModal .modal-body strong').textContent = name;
440
- showModal('deleteModal');
441
- }
442
-
443
- function showCreateFolderModal(parentPath) {
444
- currentActionContext = { parentPath };
445
- document.getElementById('folderNameInput').value = '';
446
- showModal('createFolderModal');
447
- }
448
-
449
- // --- API Call Functions ---
450
- async function apiPost(endpoint, body) {
451
- try {
452
- const response = await fetch(endpoint, {
453
- method: 'POST',
454
- headers: { 'Content-Type': 'application/json' },
455
- body: JSON.stringify(body)
456
- });
457
- if (response.ok) {
458
- hideAllModals();
459
- location.reload();
460
- } else {
461
- alert('An error occurred. Please check the console.');
462
- console.error('API Error:', await response.text());
463
- hideAllModals();
464
- }
465
- } catch (error) {
466
- alert('A network error occurred. Please check the console.');
467
- console.error('Fetch Error:', error);
468
- hideAllModals();
469
- }
470
- }
471
-
472
- function confirmRename() {
473
- const newName = document.getElementById('renameInput').value.trim();
474
- if (newName && newName !== currentActionContext.name) {
475
- apiPost('/rename', { old_path: currentActionContext.path, new_name: newName });
476
- } else {
477
- hideAllModals();
478
- }
479
- }
480
-
481
- function confirmDelete() {
482
- apiPost('/delete', { path: currentActionContext.path });
483
- }
484
-
485
- function confirmCreateFolder() {
486
- const name = document.getElementById('folderNameInput').value.trim();
487
- if (name) {
488
- const folderPath = currentActionContext.parentPath ? `${currentActionContext.parentPath}/${name}` : name;
489
- apiPost('/create_folder', { path: folderPath });
490
- }
491
- }
492
-
493
- // --- Event Listeners ---
494
- document.addEventListener('click', (e) => {
495
- if (!e.target.closest('.dropdown')) closeAllDropdowns();
496
  });
497
-
498
- document.addEventListener('keydown', (e) => {
499
- if (e.key === 'Escape') hideAllModals();
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  });
501
 
502
- document.querySelectorAll('.modal-input').forEach(input => {
503
- input.addEventListener('keydown', (e) => {
504
- if (e.key === 'Enter') {
505
- // Trigger the primary action of the modal
506
- const modal = e.target.closest('.modal');
507
- if (modal) {
508
- const primaryButton = modal.querySelector('.btn:not(.btn-secondary)');
509
- if (primaryButton) primaryButton.click();
510
- }
511
- }
512
- })
 
 
 
 
 
 
 
 
513
  });
514
-
515
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  </body>
517
  </html>
518
  """
519
 
520
- # --- Helper & Backend Functions ---
521
-
522
  def list_folder(path=""):
523
- """Lists files and directories in a given path of the repository."""
524
- try:
525
- prefix = path.strip("/") + ("/" if path else "")
526
- all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset")
527
-
528
- seen_dirs = set()
529
- items = []
530
-
531
- for f in all_files:
532
- if not f.startswith(prefix):
533
- continue
534
-
535
- relative_path = f[len(prefix):]
536
-
537
- if "/" in relative_path:
538
- dir_name = relative_path.split("/")[0]
539
- dir_path = (prefix + dir_name).strip("/")
540
- if dir_path not in seen_dirs:
541
- seen_dirs.add(dir_path)
542
- items.append({"type": "dir", "name": dir_name, "path": dir_path})
543
- elif relative_path and relative_path != '.gitkeep' and relative_path != '.keep':
544
- items.append({"type": "file", "name": relative_path, "path": f})
545
-
546
- items.sort(key=lambda x: (x["type"] != "dir", x["name"].lower()))
547
- return items
548
- except Exception as e:
549
- print(f"Error listing folder '{path}': {e}")
550
- return []
551
 
552
  @app.route("/", methods=["GET"])
553
  def index():
554
- if not REPO_ID or not HF_TOKEN:
555
- return "<h1>Configuration Error</h1><p>Please set REPO_ID and HF_TOKEN environment variables.</p>", 500
556
-
557
- path = unquote(request.args.get("path", "")).strip("/")
558
-
559
- breadcrumbs = [{"name": "Home", "path": ""}]
560
- if path:
561
- current_path_agg = ""
562
- for part in path.split("/"):
563
- current_path_agg = f"{current_path_agg}/{part}".strip("/")
564
- breadcrumbs.append({"name": part, "path": current_path_agg})
565
-
566
- items = list_folder(path)
567
- return render_template_string(TEMPLATE, items=items, path=path, breadcrumbs=breadcrumbs)
568
 
569
  @app.route("/download", methods=["GET"])
570
  def download():
571
- path = unquote(request.args.get("path", ""))
572
- try:
573
- # The HF_HOME env var automatically handles the cache location
574
- local_file = hf_hub_download(repo_id=REPO_ID, filename=path, repo_type="dataset")
575
- return send_file(local_file, as_attachment=True, download_name=os.path.basename(path))
576
- except Exception as e:
577
- return f"Error downloading file: {e}", 500
578
 
579
  @app.route("/upload", methods=["POST"])
580
  def upload():
581
- file = request.files.get("file")
582
- if not file or not file.filename:
583
- return redirect(request.referrer)
584
-
585
- path_in_repo = request.form.get("path", "").strip("/")
586
- dest_path = f"{path_in_repo}/{file.filename}".strip("/")
587
-
588
- with tempfile.NamedTemporaryFile(delete=False) as tmp:
589
- file.save(tmp.name)
590
- tmp_path = tmp.name
591
- try:
592
- upload_file(path_or_fileobj=tmp_path, path_in_repo=dest_path, repo_id=REPO_ID, repo_type="dataset", commit_message=f"Upload {file.filename}")
593
- finally:
594
- os.remove(tmp_path)
595
- return redirect(f"/?path={quote(path_in_repo)}")
596
 
597
  @app.route("/delete", methods=["POST"])
598
  def delete():
599
- path_to_delete = request.get_json()["path"].strip("/")
600
- all_repo_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset")
601
-
602
- # Check if the path is a file or a directory by looking for exact matches (file)
603
- # vs. paths that start with the given path + "/" (directory)
604
- is_file = any(f == path_to_delete for f in all_repo_files)
605
-
606
- if is_file:
607
- delete_file(repo_id=REPO_ID, path_in_repo=path_to_delete, repo_type="dataset", commit_message=f"Delete file {path_to_delete}")
608
- else: # It's a folder
609
- files_in_folder = [f for f in all_repo_files if f.startswith(path_to_delete + "/")]
610
- for f in files_in_folder:
611
- delete_file(repo_id=REPO_ID, path_in_repo=f, repo_type="dataset", commit_message=f"Delete file {f} from folder {path_to_delete}")
612
- # Delete placeholder files if they exist to remove the empty folder
613
- for placeholder in ['.gitkeep', '.keep']:
614
- try:
615
- delete_file(repo_id=REPO_ID, path_in_repo=f"{path_to_delete}/{placeholder}", repo_type="dataset", commit_message=f"Delete folder placeholder for {path_to_delete}", ignore_patterns=None)
616
- except Exception:
617
- pass # Placeholder did not exist, which is fine
618
-
619
  return jsonify(status="ok")
620
 
621
  @app.route("/create_folder", methods=["POST"])
622
  def create_folder():
623
- folder_path = request.get_json()["path"].strip("/")
624
- keep_file_path = f"{folder_path}/.gitkeep"
625
-
626
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".tmp") as tmp:
627
- tmp.write("") # Create an empty file
628
- tmp_path = tmp.name
629
-
630
- try:
631
- upload_file(path_or_fileobj=tmp_path, path_in_repo=keep_file_path, repo_id=REPO_ID, repo_type="dataset", commit_message=f"Create folder {folder_path}")
632
- finally:
633
- os.remove(tmp_path)
634
-
635
  return jsonify(status="ok")
636
 
637
  @app.route("/rename", methods=["POST"])
638
  def rename():
639
  data = request.get_json()
640
- old_path = data["old_path"].strip("/")
641
- new_name = data["new_name"].strip("/")
642
-
643
- parent_dir = os.path.dirname(old_path)
644
- new_path = os.path.join(parent_dir, new_name).replace("\\", "/") if parent_dir else new_name
645
-
646
- all_repo_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset")
647
- is_file = any(f == old_path for f in all_repo_files)
648
-
649
- operations = []
650
- if is_file:
651
- operations.append((old_path, new_path))
652
- else: # It's a folder
653
- folder_prefix = old_path + "/"
654
- for f in all_repo_files:
655
- if f.startswith(folder_prefix):
656
- new_file_path = new_path + f[len(old_path):]
657
- operations.append((f, new_file_path))
658
-
659
- for old_file, new_file in operations:
660
- # This move is done server-side on the Hub, no local download needed
661
- api.move_file(
662
- from_path=old_file,
663
- to_path=new_file,
664
- repo_id=REPO_ID,
665
- repo_type="dataset",
666
- )
667
-
668
  return jsonify(status="ok")
669
 
670
-
671
  if __name__ == "__main__":
672
- if not REPO_ID or not HF_TOKEN:
673
- print("\n" + "="*50)
674
- print("FATAL ERROR: Environment variables are not set.")
675
- print("Please set REPO_ID (e.g., 'YourUser/YourDataset')")
676
- print("and HF_TOKEN (your 'hf_...' access token).")
677
- print("="*50 + "\n")
678
- else:
679
- print(f"\n✅ Server starting for repository: {REPO_ID}")
680
- print(f"✅ Access the UI at http://127.0.0.1:7860")
681
- app.run(debug=False, host="0.0.0.0", port=7860)
 
1
  import os
2
  import tempfile
3
  from flask import Flask, render_template_string, request, redirect, send_file, jsonify
4
+ from huggingface_hub import HfApi, hf_hub_download, upload_file, delete_file
5
+
6
+ # Environment
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  REPO_ID = os.getenv("REPO_ID")
8
  HF_TOKEN = os.getenv("HF_TOKEN")
9
 
 
 
 
 
 
 
 
 
10
  app = Flask(__name__)
11
  api = HfApi()
12
 
 
13
  TEMPLATE = """
14
  <!DOCTYPE html>
15
+ <html>
16
  <head>
17
+ <meta charset="UTF-8">
18
+ <title>HuggingFace Drive - {{ path or 'Root' }}</title>
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
+ <style>
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%);
29
+ color: #e2e8f0;
30
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
31
+ min-height: 100vh;
32
+ overflow-x: hidden;
33
+ }
34
+
35
+ .container {
36
+ max-width: 1200px;
37
+ margin: 0 auto;
38
+ padding: 20px;
39
+ }
40
+
41
+ .header {
42
+ background: rgba(255, 255, 255, 0.05);
43
+ backdrop-filter: blur(10px);
44
+ border-radius: 16px;
45
+ padding: 24px;
46
+ margin-bottom: 24px;
47
+ border: 1px solid rgba(255, 255, 255, 0.1);
48
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
49
+ }
50
+
51
+ .title {
52
+ font-size: 2rem;
53
+ font-weight: 700;
54
+ margin-bottom: 16px;
55
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
56
+ -webkit-background-clip: text;
57
+ -webkit-text-fill-color: transparent;
58
+ background-clip: text;
59
+ }
60
+
61
+ .breadcrumb {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 8px;
65
+ margin-bottom: 20px;
66
+ flex-wrap: wrap;
67
+ }
68
+
69
+ .breadcrumb-item {
70
+ background: rgba(255, 255, 255, 0.1);
71
+ color: #94a3b8;
72
+ padding: 6px 12px;
73
+ border-radius: 20px;
74
+ font-size: 0.875rem;
75
+ cursor: pointer;
76
+ transition: all 0.3s ease;
77
+ border: 1px solid rgba(255, 255, 255, 0.1);
78
+ }
79
+
80
+ .breadcrumb-item:hover {
81
+ background: rgba(102, 126, 234, 0.2);
82
+ color: #e2e8f0;
83
+ transform: translateY(-1px);
84
+ }
85
+
86
+ .breadcrumb-item.active {
87
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
88
+ color: white;
89
+ }
90
+
91
+ .breadcrumb-separator {
92
+ color: #64748b;
93
+ font-size: 0.875rem;
94
+ }
95
+
96
+ .actions {
97
+ display: flex;
98
+ gap: 12px;
99
+ margin-bottom: 24px;
100
+ flex-wrap: wrap;
101
+ }
102
+
103
+ .upload-container {
104
+ position: relative;
105
+ overflow: hidden;
106
+ display: inline-block;
107
+ }
108
+
109
+ .file-input {
110
+ position: absolute;
111
+ left: -9999px;
112
+ opacity: 0;
113
+ }
114
+
115
+ .btn {
116
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
117
+ color: white;
118
+ border: none;
119
+ padding: 12px 24px;
120
+ border-radius: 12px;
121
+ cursor: pointer;
122
+ font-weight: 600;
123
+ font-size: 0.875rem;
124
+ transition: all 0.3s ease;
125
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
126
+ border: 1px solid rgba(255, 255, 255, 0.1);
127
+ }
128
+
129
+ .btn:hover {
130
+ transform: translateY(-2px);
131
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
132
+ }
133
+
134
+ .btn-secondary {
135
+ background: rgba(255, 255, 255, 0.1);
136
+ color: #e2e8f0;
137
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
138
+ }
139
+
140
+ .btn-secondary:hover {
141
+ background: rgba(255, 255, 255, 0.15);
142
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
143
+ }
144
+
145
+ .file-grid {
146
+ display: grid;
147
+ gap: 16px;
148
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
149
+ }
150
+
151
+ .file-item {
152
+ background: rgba(255, 255, 255, 0.05);
153
+ backdrop-filter: blur(10px);
154
+ border: 1px solid rgba(255, 255, 255, 0.1);
155
+ border-radius: 16px;
156
+ padding: 20px;
157
+ transition: all 0.3s ease;
158
+ cursor: pointer;
159
+ position: relative;
160
+ overflow: hidden;
161
+ }
162
+
163
+ .file-item::before {
164
+ content: '';
165
+ position: absolute;
166
+ top: 0;
167
+ left: -100%;
168
+ width: 100%;
169
+ height: 100%;
170
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
171
+ transition: left 0.5s ease;
172
+ }
173
+
174
+ .file-item:hover::before {
175
+ left: 100%;
176
+ }
177
+
178
+ .file-item:hover {
179
+ transform: translateY(-4px);
180
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
181
+ border-color: rgba(102, 126, 234, 0.3);
182
+ }
183
+
184
+ .file-content {
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: space-between;
188
+ position: relative;
189
+ z-index: 2;
190
+ }
191
+
192
+ .file-info {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 12px;
196
+ flex: 1;
197
+ }
198
+
199
+ .file-icon {
200
+ font-size: 1.5rem;
201
+ width: 40px;
202
+ height: 40px;
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ border-radius: 10px;
207
+ background: rgba(255, 255, 255, 0.1);
208
+ }
209
+
210
+ .folder-icon {
211
+ background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
212
+ }
213
+
214
+ .file-name {
215
+ font-weight: 500;
216
+ font-size: 1rem;
217
+ color: #f1f5f9;
218
+ }
219
+
220
+ .dropdown {
221
+ position: relative;
222
+ }
223
+
224
+ .dropdown-toggle {
225
+ background: rgba(255, 255, 255, 0.1);
226
+ border: none;
227
+ color: #94a3b8;
228
+ padding: 8px;
229
+ border-radius: 8px;
230
+ cursor: pointer;
231
+ transition: all 0.3s ease;
232
+ font-size: 1.25rem;
233
+ width: 36px;
234
+ height: 36px;
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ }
239
+
240
+ .dropdown-toggle:hover {
241
+ background: rgba(255, 255, 255, 0.2);
242
+ color: #e2e8f0;
243
+ }
244
+
245
+ .dropdown-menu {
246
+ position: absolute;
247
+ top: 100%;
248
+ right: 0;
249
+ background: rgba(15, 15, 35, 0.95);
250
+ backdrop-filter: blur(20px);
251
+ border: 1px solid rgba(255, 255, 255, 0.1);
252
+ border-radius: 12px;
253
+ padding: 8px;
254
+ min-width: 150px;
255
+ opacity: 0;
256
+ visibility: hidden;
257
+ transform: translateY(-10px);
258
+ transition: all 0.3s ease;
259
+ z-index: 1000;
260
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
261
+ }
262
+
263
+ .dropdown.active .dropdown-menu {
264
+ opacity: 1;
265
+ visibility: visible;
266
+ transform: translateY(0);
267
+ }
268
+
269
+ .dropdown-item {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 8px;
273
+ padding: 10px 12px;
274
+ border-radius: 8px;
275
+ cursor: pointer;
276
+ transition: all 0.2s ease;
277
+ font-size: 0.875rem;
278
+ color: #cbd5e1;
279
+ }
280
+
281
+ .dropdown-item:hover {
282
+ background: rgba(255, 255, 255, 0.1);
283
+ color: #f1f5f9;
284
+ }
285
+
286
+ .dropdown-item.danger:hover {
287
+ background: rgba(239, 68, 68, 0.2);
288
+ color: #fca5a5;
289
+ }
290
+
291
+ /* Modal Styles */
292
+ .modal-overlay {
293
+ position: fixed;
294
+ top: 0;
295
+ left: 0;
296
+ width: 100%;
297
+ height: 100%;
298
+ background: rgba(0, 0, 0, 0.7);
299
+ backdrop-filter: blur(5px);
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ z-index: 2000;
304
+ opacity: 0;
305
+ visibility: hidden;
306
+ transition: all 0.3s ease;
307
+ }
308
+
309
+ .modal-overlay.active {
310
+ opacity: 1;
311
+ visibility: visible;
312
+ }
313
+
314
+ .modal {
315
+ background: rgba(15, 15, 35, 0.95);
316
+ backdrop-filter: blur(20px);
317
+ border: 1px solid rgba(255, 255, 255, 0.1);
318
+ border-radius: 20px;
319
+ padding: 32px;
320
+ max-width: 400px;
321
+ width: 90%;
322
+ transform: scale(0.8);
323
+ transition: all 0.3s ease;
324
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
325
+ }
326
+
327
+ .modal-overlay.active .modal {
328
+ transform: scale(1);
329
+ }
330
+
331
+ .modal-title {
332
+ font-size: 1.25rem;
333
+ font-weight: 600;
334
+ margin-bottom: 16px;
335
+ color: #f1f5f9;
336
+ }
337
+
338
+ .modal-input {
339
+ width: 100%;
340
+ background: rgba(255, 255, 255, 0.1);
341
+ border: 1px solid rgba(255, 255, 255, 0.2);
342
+ border-radius: 12px;
343
+ padding: 12px 16px;
344
+ color: #f1f5f9;
345
+ font-size: 1rem;
346
+ margin-bottom: 20px;
347
+ }
348
+
349
+ .modal-input:focus {
350
+ outline: none;
351
+ border-color: #667eea;
352
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
353
+ }
354
+
355
+ .modal-actions {
356
+ display: flex;
357
+ gap: 12px;
358
+ justify-content: flex-end;
359
+ }
360
+
361
+ .btn-danger {
362
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
363
+ box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
364
+ }
365
+
366
+ .btn-danger:hover {
367
+ box-shadow: 0 8px 25px rgba(239, 68, 68, 0.4);
368
+ }
369
+
370
+ /* Loading Animation */
371
+ .loading {
372
+ display: inline-block;
373
+ width: 20px;
374
+ height: 20px;
375
+ border: 2px solid rgba(255, 255, 255, 0.3);
376
+ border-radius: 50%;
377
+ border-top-color: #667eea;
378
+ animation: spin 1s ease-in-out infinite;
379
+ }
380
+
381
+ @keyframes spin {
382
+ to { transform: rotate(360deg); }
383
+ }
384
+
385
+ /* Responsive Design */
386
+ @media (max-width: 768px) {
387
+ .container {
388
+ padding: 16px;
389
+ }
390
+
391
+ .file-grid {
392
+ grid-template-columns: 1fr;
393
+ }
394
+
395
+ .actions {
396
+ flex-direction: column;
397
+ }
398
+
399
+ .breadcrumb {
400
+ flex-direction: column;
401
+ align-items: flex-start;
402
+ }
403
+ }
404
+
405
+ /* Custom Scrollbar */
406
+ ::-webkit-scrollbar {
407
+ width: 8px;
408
+ }
409
+
410
+ ::-webkit-scrollbar-track {
411
+ background: rgba(255, 255, 255, 0.1);
412
+ }
413
+
414
+ ::-webkit-scrollbar-thumb {
415
+ background: rgba(102, 126, 234, 0.5);
416
+ border-radius: 4px;
417
+ }
418
+
419
+ ::-webkit-scrollbar-thumb:hover {
420
+ background: rgba(102, 126, 234, 0.7);
421
+ }
422
+ </style>
423
  </head>
424
  <body>
425
+ <div class="container">
426
+ <div class="header">
427
+ <h1 class="title">🚀 HuggingFace Drive</h1>
428
+
429
+ <!-- Breadcrumb Navigation -->
430
+ <div class="breadcrumb">
431
+ <span class="breadcrumb-item {{ 'active' if not path else '' }}" onclick="nav('')">
432
+ 🏠 Home
433
+ </span>
434
+ {% if path %}
435
+ {% set parts = path.split('/') %}
436
+ {% for i in range(parts|length) %}
437
+ <span class="breadcrumb-separator">›</span>
438
+ {% set current_path = parts[:i+1]|join('/') %}
439
+ <span class="breadcrumb-item {{ 'active' if current_path == path else '' }}"
440
+ onclick="nav('{{ current_path }}')">
441
+ 📁 {{ parts[i] }}
442
+ </span>
443
+ {% endfor %}
444
+ {% endif %}
445
+ </div>
446
+
447
+ <!-- Actions -->
448
+ <div class="actions">
449
+ <form action="/upload" method="post" enctype="multipart/form-data" class="upload-container">
450
+ <input type="file" name="file" required class="file-input" id="fileInput">
451
+ <input type="hidden" name="path" value="{{ path }}">
452
+ <label for="fileInput" class="btn">
453
+ 📤 Upload File
454
+ </label>
455
+ </form>
456
+ <button class="btn btn-secondary" onclick="createFolder('{{ path }}')">
457
+ 📁 New Folder
458
+ </button>
459
+ </div>
460
+ </div>
461
 
462
+ <!-- File Grid -->
463
+ <div class="file-grid">
464
+ {% for item in items %}
465
+ <div class="file-item">
466
+ <div class="file-content">
467
+ <div class="file-info" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% endif %}">
468
+ <div class="file-icon {{ 'folder-icon' if item.type=='dir' else '' }}">
469
+ {{ '📁' if item.type=='dir' else '📄' }}
470
+ </div>
471
+ <div class="file-name">{{ item.name }}</div>
472
+ </div>
473
+
474
+ <div class="dropdown">
475
+ <button class="dropdown-toggle" onclick="toggleDropdown(this)">⋮</button>
476
+ <div class="dropdown-menu">
477
+ {% if item.type == 'file' %}
478
+ <div class="dropdown-item" onclick="download('{{ item.path }}')">
479
+ 📥 Download
480
+ </div>
481
+ {% endif %}
482
+ <div class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">
483
+ ✏️ Rename
484
  </div>
485
+ <div class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}')">
486
+ 🗑️ Delete
487
+ </div>
488
+ </div>
489
  </div>
490
+ </div>
 
 
 
491
  </div>
492
+ {% endfor %}
493
  </div>
494
+ </div>
495
+
496
+ <!-- Rename Modal -->
497
+ <div class="modal-overlay" id="renameModal">
498
+ <div class="modal">
499
+ <h3 class="modal-title">Rename Item</h3>
500
+ <input type="text" class="modal-input" id="renameInput" placeholder="Enter new name">
501
+ <div class="modal-actions">
502
+ <button class="btn btn-secondary" onclick="closeModal('renameModal')">Cancel</button>
503
+ <button class="btn" onclick="confirmRename()">Rename</button>
504
+ </div>
505
  </div>
506
+ </div>
507
+
508
+ <!-- Delete Confirmation Modal -->
509
+ <div class="modal-overlay" id="deleteModal">
510
+ <div class="modal">
511
+ <h3 class="modal-title">Confirm Deletion</h3>
512
+ <p style="margin-bottom: 20px; color: #cbd5e1;">Are you sure you want to delete this item? This action cannot be undone.</p>
513
+ <div class="modal-actions">
514
+ <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
515
+ <button class="btn btn-danger" onclick="confirmDelete()">Delete</button>
516
+ </div>
517
  </div>
518
+ </div>
519
+
520
+ <script>
521
+ let currentRenamePath = '';
522
+ let currentDeletePath = '';
523
+ let activeDropdown = null;
524
+
525
+ function nav(p) {
526
+ location.href = '/?path=' + encodeURIComponent(p);
527
+ }
528
+
529
+ function download(path) {
530
+ window.open('/download?path=' + encodeURIComponent(path), '_blank');
531
+ }
532
+
533
+ function toggleDropdown(button) {
534
+ const dropdown = button.closest('.dropdown');
535
+
536
+ // Close other dropdowns
537
+ if (activeDropdown && activeDropdown !== dropdown) {
538
+ activeDropdown.classList.remove('active');
539
+ }
540
+
541
+ dropdown.classList.toggle('active');
542
+ activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
543
+ }
544
+
545
+ function showRenameModal(path, currentName) {
546
+ currentRenamePath = path;
547
+ document.getElementById('renameInput').value = currentName;
548
+ showModal('renameModal');
549
+ setTimeout(() => document.getElementById('renameInput').focus(), 100);
550
+ closeAllDropdowns();
551
+ }
552
+
553
+ function showDeleteModal(path) {
554
+ currentDeletePath = path;
555
+ showModal('deleteModal');
556
+ closeAllDropdowns();
557
+ }
558
+
559
+ function showModal(modalId) {
560
+ document.getElementById(modalId).classList.add('active');
561
+ }
562
+
563
+ function closeModal(modalId) {
564
+ document.getElementById(modalId).classList.remove('active');
565
+ }
566
+
567
+ function closeAllDropdowns() {
568
+ document.querySelectorAll('.dropdown.active').forEach(dropdown => {
569
+ dropdown.classList.remove('active');
570
+ });
571
+ activeDropdown = null;
572
+ }
573
+
574
+ async function confirmRename() {
575
+ const newName = document.getElementById('renameInput').value.trim();
576
+ if (!newName) return;
577
+
578
+ try {
579
+ const response = await fetch('/rename', {
580
+ method: 'POST',
581
+ headers: { 'Content-Type': 'application/json' },
582
+ body: JSON.stringify({
583
+ old_path: currentRenamePath,
584
+ new_path: newName
585
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  });
587
+
588
+ if (response.ok) {
589
+ closeModal('renameModal');
590
+ location.reload();
591
+ }
592
+ } catch (error) {
593
+ console.error('Rename failed:', error);
594
+ }
595
+ }
596
+
597
+ async function confirmDelete() {
598
+ try {
599
+ const response = await fetch('/delete', {
600
+ method: 'POST',
601
+ headers: { 'Content-Type': 'application/json' },
602
+ body: JSON.stringify({ path: currentDeletePath })
603
  });
604
 
605
+ if (response.ok) {
606
+ closeModal('deleteModal');
607
+ location.reload();
608
+ }
609
+ } catch (error) {
610
+ console.error('Delete failed:', error);
611
+ }
612
+ }
613
+
614
+ async function createFolder(currentPath) {
615
+ const name = prompt('Enter folder name:');
616
+ if (!name) return;
617
+
618
+ try {
619
+ const folderPath = currentPath ? `${currentPath}/${name}` : name;
620
+ const response = await fetch('/create_folder', {
621
+ method: 'POST',
622
+ headers: { 'Content-Type': 'application/json' },
623
+ body: JSON.stringify({ path: folderPath })
624
  });
625
+
626
+ if (response.ok) {
627
+ location.reload();
628
+ }
629
+ } catch (error) {
630
+ console.error('Create folder failed:', error);
631
+ }
632
+ }
633
+
634
+ // Close dropdowns when clicking outside
635
+ document.addEventListener('click', function(e) {
636
+ if (!e.target.closest('.dropdown')) {
637
+ closeAllDropdowns();
638
+ }
639
+ });
640
+
641
+ // Handle file input change for better UX
642
+ document.getElementById('fileInput').addEventListener('change', function(e) {
643
+ if (e.target.files.length > 0) {
644
+ e.target.closest('form').submit();
645
+ }
646
+ });
647
+
648
+ // Handle modal close on overlay click
649
+ document.querySelectorAll('.modal-overlay').forEach(overlay => {
650
+ overlay.addEventListener('click', function(e) {
651
+ if (e.target === overlay) {
652
+ overlay.classList.remove('active');
653
+ }
654
+ });
655
+ });
656
+
657
+ // Handle Enter key in rename modal
658
+ document.getElementById('renameInput').addEventListener('keypress', function(e) {
659
+ if (e.key === 'Enter') {
660
+ confirmRename();
661
+ }
662
+ });
663
+ </script>
664
  </body>
665
  </html>
666
  """
667
 
 
 
668
  def list_folder(path=""):
669
+ prefix = path.strip("/") + ("/" if path else "")
670
+ all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
671
+ seen = set()
672
+ items = []
673
+ for f in all_files:
674
+ if not f.startswith(prefix): continue
675
+ rest = f[len(prefix):]
676
+ if "/" in rest:
677
+ dir_name = rest.split("/")[0]
678
+ dir_path = (prefix + dir_name).strip("/")
679
+ if dir_path not in seen:
680
+ seen.add(dir_path)
681
+ items.append({"type":"dir","name":dir_name,"path":dir_path})
682
+ else:
683
+ items.append({"type":"file","name":rest,"path":(prefix + rest).strip("/")})
684
+ # sort dirs then files
685
+ items.sort(key=lambda x: (x["type"]!="dir", x["name"].lower()))
686
+ return items
 
 
 
 
 
 
 
 
 
 
687
 
688
  @app.route("/", methods=["GET"])
689
  def index():
690
+ path = request.args.get("path","").strip("/")
691
+ return render_template_string(TEMPLATE, items=list_folder(path), path=path)
 
 
 
 
 
 
 
 
 
 
 
 
692
 
693
  @app.route("/download", methods=["GET"])
694
  def download():
695
+ p = request.args.get("path","")
696
+ # download via hf_hub_download into /tmp
697
+ local = hf_hub_download(repo_id=REPO_ID, filename=p, repo_type="dataset", token=HF_TOKEN, cache_dir=tempfile.gettempdir())
698
+ return send_file(local, as_attachment=True, download_name=os.path.basename(p))
 
 
 
699
 
700
  @app.route("/upload", methods=["POST"])
701
  def upload():
702
+ file = request.files["file"]
703
+ path = request.form.get("path","").strip("/")
704
+ dest = f"{path}/{file.filename}".strip("/")
705
+ tmp = tempfile.NamedTemporaryFile(delete=False)
706
+ file.save(tmp.name)
707
+ upload_file(path_or_fileobj=tmp.name, path_in_repo=dest, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
708
+ return redirect(f"/?path={path}")
 
 
 
 
 
 
 
 
709
 
710
  @app.route("/delete", methods=["POST"])
711
  def delete():
712
+ d = request.get_json()["path"]
713
+ all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
714
+ for f in all_files:
715
+ if f == d or f.startswith(d.rstrip("/")+"/"):
716
+ delete_file(repo_id=REPO_ID, path_in_repo=f, repo_type="dataset", token=HF_TOKEN)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
  return jsonify(status="ok")
718
 
719
  @app.route("/create_folder", methods=["POST"])
720
  def create_folder():
721
+ folder = request.get_json()["path"].strip("/")
722
+ keep = f"{folder}/.keep"
723
+ tmp = tempfile.NamedTemporaryFile(delete=False)
724
+ tmp.write(b"")
725
+ tmp.flush()
726
+ upload_file(path_or_fileobj=tmp.name, path_in_repo=keep, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
 
 
 
 
 
 
727
  return jsonify(status="ok")
728
 
729
  @app.route("/rename", methods=["POST"])
730
  def rename():
731
  data = request.get_json()
732
+ old, new = data["old_path"].strip("/"), data["new_path"].strip("/")
733
+ all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
734
+ for f in all_files:
735
+ if f == old or f.startswith(old+"/"):
736
+ rel = f[len(old):].lstrip("/")
737
+ newp = (new + "/" + rel).strip("/")
738
+ local = hf_hub_download(repo_id=REPO_ID, filename=f, repo_type="dataset",
739
+ token=HF_TOKEN, cache_dir=tempfile.gettempdir())
740
+ upload_file(path_or_fileobj=local, path_in_repo=newp, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
741
+ delete_file(repo_id=REPO_ID, path_in_repo=f, repo_type="dataset", token=HF_TOKEN)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
  return jsonify(status="ok")
743
 
 
744
  if __name__ == "__main__":
745
+ app.run(debug=True, host="0.0.0.0", port=7860)
746
+
747
+
748
+
749
+ make the UI best of the best with UX and also grate animations and UI elements to make it great looking and user friendly. also add a custom pop up box fore rename and make sure popup for delete option
750
+ now write me one py code with thml css js python all in one code like my original one
751
+
752
+ rewrite all the css and designs and make it pro and award winning design
753
+