testdeep123 commited on
Commit
79ef1fc
·
verified ·
1 Parent(s): 27fc0b7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1463 -518
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import tempfile
3
  import requests
 
4
  from flask import Flask, render_template_string, request, redirect, send_file, jsonify
5
  from huggingface_hub import HfApi, hf_hub_download, upload_file, delete_file
6
 
@@ -19,386 +20,878 @@ TEMPLATE = """
19
  <title>HuggingFace Drive - {{ path or 'Root' }}</title>
20
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
21
  <style>
22
- /* Reset and Base Styles */
23
- * {
24
- margin: 0;
25
- padding: 0;
26
- box-sizing: border-box;
27
- }
28
-
29
- body {
30
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
31
- background-color: #0d1117;
32
- color: #e5e7eb;
33
- line-height: 1.5;
34
- min-height: 100vh;
35
- display: flex;
36
- flex-direction: column;
37
- }
38
-
39
- .container {
40
- max-width: 1200px;
41
- margin: 0 auto;
42
- padding: 2rem 1rem;
43
- flex: 1;
44
- }
45
-
46
- /* Header */
47
- .header {
48
- background: rgba(31, 41, 55, 0.8);
49
- border-radius: 1rem;
50
- padding: 1.5rem;
51
- margin-bottom: 2rem;
52
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
53
- }
54
-
55
- .title {
56
- font-size: 2rem;
57
- font-weight: bold;
58
- margin-bottom: 1rem;
59
- display: flex;
60
- align-items: center;
61
- gap: 0.5rem;
62
- color: #667eea;
63
- }
64
-
65
- /* Breadcrumb */
66
- .breadcrumb {
67
- display: flex;
68
- flex-wrap: wrap;
69
- gap: 0.5rem;
70
- margin-bottom: 1.5rem;
71
- }
72
-
73
- .breadcrumb-item {
74
- background: rgba(255, 255, 255, 0.1);
75
- padding: 0.5rem 1rem;
76
- border-radius: 0.5rem;
77
- cursor: pointer;
78
- transition: background 0.3s;
79
- display: flex;
80
- align-items: center;
81
- gap: 0.5rem;
82
- }
83
-
84
- .breadcrumb-item:hover {
85
- background: rgba(102, 126, 234, 0.3);
86
- }
87
-
88
- .breadcrumb-item.active {
89
- background: #667eea;
90
- color: white;
91
- }
92
-
93
- .breadcrumb-separator {
94
- color: #9ca3af;
95
- }
96
-
97
- /* Actions */
98
- .actions {
99
- display: flex;
100
- gap: 1rem;
101
- flex-wrap: wrap;
102
- }
103
-
104
- .btn {
105
- padding: 0.75rem 1.25rem;
106
- border-radius: 0.75rem;
107
- cursor: pointer;
108
- font-weight: 600;
109
- transition: all 0.3s;
110
- display: inline-flex;
111
- align-items: center;
112
- gap: 0.5rem;
113
- border: none;
114
- }
115
-
116
- .btn-primary {
117
- background: linear-gradient(135deg, #667eea, #764ba2);
118
- color: white;
119
- }
120
-
121
- .btn-primary:hover {
122
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.5);
123
- transform: translateY(-2px);
124
- }
125
-
126
- .btn-secondary {
127
- background: rgba(255, 255, 255, 0.1);
128
- color: #e5e7eb;
129
- }
130
-
131
- .btn-secondary:hover {
132
- background: rgba(255, 255, 255, 0.2);
133
- }
134
-
135
- .btn-danger {
136
- background: linear-gradient(135deg, #ef4444, #dc2626);
137
- color: white;
138
- }
139
-
140
- .btn-danger:hover {
141
- box-shadow: 0 4px 15px rgba(239, 68, 68, 0.5);
142
- }
143
-
144
- .btn:disabled {
145
- opacity: 0.6;
146
- cursor: not-allowed;
147
- }
148
-
149
- .file-input-wrapper {
150
- position: relative;
151
- }
152
-
153
- .file-input {
154
- position: absolute;
155
- top: 0;
156
- left: 0;
157
- opacity: 0;
158
- width: 100%;
159
- height: 100%;
160
- cursor: pointer;
161
- }
162
-
163
- /* File Grid */
164
- .file-grid {
165
- display: grid;
166
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
167
- gap: 1.5rem;
168
- }
169
-
170
- .file-item {
171
- background: rgba(31, 41, 55, 0.8);
172
- border-radius: 1rem;
173
- padding: 1rem;
174
- transition: all 0.3s;
175
- position: relative;
176
- }
177
-
178
- .file-item:hover {
179
- transform: translateY(-4px);
180
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
181
- }
182
-
183
- .file-content {
184
- display: flex;
185
- align-items: center;
186
- gap: 1rem;
187
- }
188
-
189
- .file-icon {
190
- font-size: 2rem;
191
- width: 3rem;
192
- height: 3rem;
193
- display: flex;
194
- align-items: center;
195
- justify-content: center;
196
- border-radius: 0.5rem;
197
- background: rgba(255, 255, 255, 0.1);
198
- }
199
-
200
- .folder-icon {
201
- background: linear-gradient(135deg, #fbbf24, #f59e0b);
202
- }
203
-
204
- .file-name {
205
- white-space: nowrap;
206
- overflow: hidden;
207
- text-overflow: ellipsis;
208
- }
209
-
210
- /* Dropdown */
211
- .dropdown {
212
- position: absolute;
213
- top: 0.5rem;
214
- right: 0.5rem;
215
- }
216
-
217
- .dropdown-toggle {
218
- background: transparent;
219
- border: none;
220
- cursor: pointer;
221
- padding: 0.5rem;
222
- color: #9ca3af;
223
- }
224
-
225
- .dropdown-menu {
226
- position: absolute;
227
- right: 0;
228
- background: #1f2937;
229
- border-radius: 0.5rem;
230
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
231
- min-width: 150px;
232
- display: none;
233
- }
234
-
235
- .dropdown.active .dropdown-menu {
236
- display: block;
237
- }
238
-
239
- .dropdown-item {
240
- padding: 0.75rem 1rem;
241
- cursor: pointer;
242
- display: flex;
243
- align-items: center;
244
- gap: 0.5rem;
245
- transition: background 0.3s;
246
- }
247
-
248
- .dropdown-item:hover {
249
- background: rgba(255, 255, 255, 0.1);
250
- }
251
-
252
- .dropdown-item.danger:hover {
253
- background: rgba(239, 68, 68, 0.2);
254
- }
255
-
256
- /* Modals */
257
- .modal-overlay {
258
- position: fixed;
259
- top: 0;
260
- left: 0;
261
- width: 100%;
262
- height: 100%;
263
- background: rgba(0, 0, 0, 0.7);
264
- display: flex;
265
- align-items: center;
266
- justify-content: center;
267
- z-index: 1000;
268
- opacity: 0;
269
- visibility: hidden;
270
- transition: all 0.3s;
271
- }
272
-
273
- .modal-overlay.active {
274
- opacity: 1;
275
- visibility: visible;
276
- }
277
-
278
- .modal {
279
- background: #1f2937;
280
- border-radius: 1rem;
281
- padding: 1.5rem;
282
- max-width: 400px;
283
- width: 90%;
284
- transform: scale(0.9);
285
- transition: transform 0.3s;
286
- }
287
-
288
- .modal-overlay.active .modal {
289
- transform: scale(1);
290
- }
291
-
292
- .modal-title {
293
- font-size: 1.25rem;
294
- margin-bottom: 1rem;
295
- }
296
-
297
- .modal-body {
298
- margin-bottom: 1.5rem;
299
- }
300
-
301
- .modal-input {
302
- width: 100%;
303
- padding: 0.75rem;
304
- border-radius: 0.5rem;
305
- background: rgba(255, 255, 255, 0.05);
306
- border: 1px solid rgba(255, 255, 255, 0.1);
307
- color: #e5e7eb;
308
- }
309
-
310
- .modal-actions {
311
- display: flex;
312
- gap: 1rem;
313
- justify-content: flex-end;
314
- }
315
-
316
- /* Loading */
317
- .loading-spinner {
318
- border: 3px solid rgba(255, 255, 255, 0.3);
319
- border-top-color: white;
320
- border-radius: 50%;
321
- width: 1.5rem;
322
- height: 1.5rem;
323
- animation: spin 1s linear infinite;
324
- display: none;
325
- }
326
-
327
- @keyframes spin {
328
- to { transform: rotate(360deg); }
329
- }
330
-
331
- .upload-overlay {
332
- position: fixed;
333
- top: 0;
334
- left: 0;
335
- width: 100%;
336
- height: 100%;
337
- background: rgba(0, 0, 0, 0.8);
338
- display: flex;
339
- flex-direction: column;
340
- align-items: center;
341
- justify-content: center;
342
- z-index: 2000;
343
- opacity: 0;
344
- visibility: hidden;
345
- transition: all 0.3s;
346
- }
347
-
348
- .upload-overlay.active {
349
- opacity: 1;
350
- visibility: visible;
351
- }
352
-
353
- .upload-overlay-text {
354
- margin-top: 1rem;
355
- font-size: 1.25rem;
356
- }
357
-
358
- /* Responsive */
359
- @media (max-width: 768px) {
360
- .container {
361
- padding: 1rem;
362
- }
363
-
364
- .title {
365
- font-size: 1.5rem;
366
- }
367
-
368
- .actions {
369
- flex-direction: column;
370
- }
371
-
372
- .btn {
373
- width: 100%;
374
- }
375
-
376
- .file-grid {
377
- grid-template-columns: 1fr;
378
- }
379
-
380
- .modal-actions {
381
- flex-direction: column;
382
- }
383
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  </style>
385
  </head>
386
  <body>
387
  <!-- Upload Overlay -->
388
  <div class="upload-overlay" id="uploadOverlay">
389
- <div class="loading-spinner" style="display: block; width: 3rem; height: 3rem;"></div>
390
- <p class="upload-overlay-text">Processing...</p>
391
  </div>
392
-
393
  <div class="container">
394
  <header class="header">
395
  <h1 class="title">
 
 
 
 
396
  HuggingFace Drive
397
  </h1>
398
-
399
- <!-- Breadcrumb Navigation -->
400
  <nav class="breadcrumb">
401
  <span class="breadcrumb-item {{ 'active' if not path else '' }}" onclick="nav('')">
 
 
 
402
  Home
403
  </span>
404
  {% if path %}
@@ -408,67 +901,118 @@ TEMPLATE = """
408
  {% set current_path = parts[:i+1]|join('/') %}
409
  <span class="breadcrumb-item {{ 'active' if current_path == path else '' }}"
410
  onclick="nav('{{ current_path }}')">
 
 
 
411
  {{ parts[i] }}
412
  </span>
413
  {% endfor %}
414
  {% endif %}
415
  </nav>
 
416
  <!-- Actions -->
417
  <div class="actions">
418
  <form action="/upload" method="post" enctype="multipart/form-data" id="uploadForm">
419
  <div class="file-input-wrapper">
420
- <button type="button" class="btn btn-primary">
421
- Upload File
422
- </button>
423
- <input type="file" name="file" required class="file-input" id="fileInput">
424
- <input type="hidden" name="path" value="{{ path }}">
 
 
 
425
  </div>
426
  </form>
 
427
  <button class="btn btn-secondary" onclick="showFolderModal('{{ path }}')">
 
 
 
428
  New Folder
429
  </button>
430
- <button class="btn btn-secondary" onclick="showDownloadUrlModal('{{ path }}')">
 
 
 
 
431
  Download from URL
432
  </button>
433
  </div>
434
  </header>
 
435
  <!-- File Grid -->
436
- <main class="file-grid">
437
- {% for item in items %}
438
- <div class="file-item">
439
- <div class="file-content" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% else %}download('{{ item.path }}'){% endif %}">
440
- <div class="file-icon {{ 'folder-icon' if item.type=='dir' }}">
441
- {% if item.type == 'dir' %}
442
- Folder
443
- {% else %}
444
- File
445
- {% endif %}
446
- </div>
447
- <div class="file-name" title="{{ item.name }}">{{ item.name }}</div>
448
- </div>
449
- <div class="dropdown">
450
- <button class="dropdown-toggle" onclick="toggleDropdown(this)">
451
-
452
- </button>
453
- <div class="dropdown-menu">
454
- {% if item.type == 'file' %}
455
- <div class="dropdown-item" onclick="download('{{ item.path }}')">
456
- Download
 
457
  </div>
458
- {% endif %}
459
- <div class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">
460
- Rename
461
  </div>
462
- <div class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">
463
- Delete
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  </div>
465
  </div>
466
- </div>
467
  </div>
468
- {% endfor %}
 
 
 
 
 
 
 
 
469
  </main>
470
  </div>
 
471
  <!-- Modals -->
 
472
  <div class="modal-overlay" id="renameModal">
473
  <div class="modal">
474
  <h3 class="modal-title">Rename Item</h3>
@@ -478,27 +1022,31 @@ TEMPLATE = """
478
  <div class="modal-actions">
479
  <button class="btn btn-secondary" onclick="closeModal('renameModal')">Cancel</button>
480
  <button class="btn btn-primary" onclick="confirmRename()" id="renameConfirmBtn">
481
- Rename
482
- <span class="loading-spinner" id="renameBtnSpinner"></span>
483
  </button>
484
  </div>
485
  </div>
486
  </div>
 
 
487
  <div class="modal-overlay" id="deleteModal">
488
  <div class="modal">
489
  <h3 class="modal-title">Confirm Deletion</h3>
490
  <div class="modal-body">
491
- <p>Are you sure you want to delete <strong id="deleteItemName"></strong>?</p>
492
  </div>
493
  <div class="modal-actions">
494
  <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
495
  <button class="btn btn-danger" onclick="confirmDelete()" id="deleteConfirmBtn">
496
- Delete
497
- <span class="loading-spinner" id="deleteBtnSpinner"></span>
498
  </button>
499
  </div>
500
  </div>
501
  </div>
 
 
502
  <div class="modal-overlay" id="folderModal">
503
  <div class="modal">
504
  <h3 class="modal-title">Create New Folder</h3>
@@ -508,147 +1056,328 @@ TEMPLATE = """
508
  <div class="modal-actions">
509
  <button class="btn btn-secondary" onclick="closeModal('folderModal')">Cancel</button>
510
  <button class="btn btn-primary" onclick="confirmCreateFolder()" id="folderConfirmBtn">
511
- Create
512
- <span class="loading-spinner" id="folderBtnSpinner"></span>
513
  </button>
514
  </div>
515
  </div>
516
  </div>
517
- <div class="modal-overlay" id="downloadUrlModal">
 
 
518
  <div class="modal">
519
  <h3 class="modal-title">Download from URL</h3>
520
  <div class="modal-body">
521
- <input type="text" class="modal-input" id="urlInput" placeholder="Enter URL">
522
- <input type="text" class="modal-input" id="filenameInput" placeholder="Optional filename" style="margin-top: 1rem;">
 
 
 
523
  </div>
524
  <div class="modal-actions">
525
- <button class="btn btn-secondary" onclick="closeModal('downloadUrlModal')">Cancel</button>
526
- <button class="btn btn-primary" onclick="confirmDownloadUrl()" id="downloadUrlConfirmBtn">
527
- Download
528
- <span class="loading-spinner" id="downloadUrlBtnSpinner"></span>
529
  </button>
530
  </div>
531
  </div>
532
  </div>
533
 
534
  <script>
 
535
  let currentRenamePath = '';
536
  let currentDeletePath = '';
537
  let currentFolderPath = '';
538
- let currentDownloadPath = '';
539
  let activeDropdown = null;
540
 
541
- function nav(p) {
542
- location.href = '/?path=' + encodeURIComponent(p);
 
543
  }
544
-
545
  function download(path) {
546
  window.open('/download?path=' + encodeURIComponent(path), '_blank');
547
  }
548
 
549
- function toggleDropdown(button) {
 
 
550
  const dropdown = button.closest('.dropdown');
 
551
  if (activeDropdown && activeDropdown !== dropdown) {
552
  activeDropdown.classList.remove('active');
553
  }
 
554
  dropdown.classList.toggle('active');
555
  activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
556
  }
557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  function showModal(modalId) {
559
- document.getElementById(modalId).classList.add('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  }
561
 
562
  function closeModal(modalId) {
563
- document.getElementById(modalId).classList.remove('active');
 
 
564
  }
565
 
 
566
  function showRenameModal(path, currentName) {
567
  currentRenamePath = path;
568
  document.getElementById('renameInput').value = currentName;
569
  showModal('renameModal');
 
570
  }
571
 
572
  function showDeleteModal(path, name) {
573
  currentDeletePath = path;
574
  document.getElementById('deleteItemName').textContent = name;
575
  showModal('deleteModal');
 
576
  }
577
 
578
  function showFolderModal(path) {
579
  currentFolderPath = path;
 
580
  showModal('folderModal');
581
  }
582
 
583
- function showDownloadUrlModal(path) {
584
- currentDownloadPath = path;
585
  document.getElementById('urlInput').value = '';
586
  document.getElementById('filenameInput').value = '';
587
- showModal('downloadUrlModal');
588
  }
589
 
590
- async function performAction(url, body, btnId, spinnerId, modalId) {
591
- const btn = document.getElementById(btnId);
592
- const spinner = document.getElementById(spinnerId);
593
- btn.disabled = true;
594
- spinner.style.display = 'block';
595
  try {
596
  const response = await fetch(url, {
597
  method: 'POST',
598
  headers: { 'Content-Type': 'application/json' },
599
  body: JSON.stringify(body)
600
  });
 
 
 
601
  if (response.ok) {
602
- if (modalId) closeModal(modalId);
603
- location.reload();
 
 
 
 
 
604
  } else {
605
- alert('Operation failed: ' + await response.text());
606
  }
607
  } catch (error) {
608
- alert('Error: ' + error.message);
 
609
  } finally {
610
- btn.disabled = false;
611
- spinner.style.display = 'none';
612
  }
613
  }
614
 
 
615
  function confirmRename() {
616
  const newName = document.getElementById('renameInput').value.trim();
617
- if (!newName) return alert('Enter a name');
618
- performAction('/rename', { old_path: currentRenamePath, new_path: newName }, 'renameConfirmBtn', 'renameBtnSpinner', 'renameModal');
 
 
 
 
 
 
 
 
 
 
 
 
619
  }
620
 
621
  function confirmDelete() {
622
- performAction('/delete', { path: currentDeletePath }, 'deleteConfirmBtn', 'deleteBtnSpinner', 'deleteModal');
 
 
 
 
 
 
 
 
623
  }
624
 
625
  function confirmCreateFolder() {
626
- const name = document.getElementById('folderInput').value.trim();
627
- if (!name) return alert('Enter a name');
628
- const path = currentFolderPath ? currentFolderPath + '/' + name : name;
629
- performAction('/create_folder', { path }, 'folderConfirmBtn', 'folderBtnSpinner', 'folderModal');
 
 
 
 
 
 
 
 
 
 
 
 
630
  }
631
 
632
- function confirmDownloadUrl() {
633
  const url = document.getElementById('urlInput').value.trim();
634
- const filename = document.getElementById('filenameInput').value.trim();
635
- if (!url) return alert('Enter a URL');
636
- const body = { url, filename, path: currentDownloadPath };
637
- document.getElementById('uploadOverlay').classList.add('active');
638
- performAction('/download_url', body, 'downloadUrlConfirmBtn', 'downloadUrlBtnSpinner', 'downloadUrlModal');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  }
640
 
641
- document.addEventListener('click', (e) => {
642
- if (!e.target.closest('.dropdown')) {
643
- if (activeDropdown) activeDropdown.classList.remove('active');
644
- }
645
- if (e.target.classList.contains('modal-overlay')) closeModal(e.target.id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646
  });
647
 
648
- document.getElementById('fileInput').addEventListener('change', () => {
649
- if (document.getElementById('fileInput').files.length) {
650
- document.getElementById('uploadOverlay').classList.add('active');
651
- document.getElementById('uploadForm').submit();
 
 
 
652
  }
653
  });
654
  </script>
@@ -657,105 +1386,321 @@ TEMPLATE = """
657
  """
658
 
659
  def list_folder(path=""):
 
660
  prefix = path.strip("/") + ("/" if path else "")
661
- all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
 
 
 
 
 
 
662
  seen = set()
663
  items = []
 
664
  for f in all_files:
665
- if not f.startswith(prefix): continue
 
 
666
  rest = f[len(prefix):]
667
  if "/" in rest:
 
668
  dir_name = rest.split("/")[0]
669
  dir_path = (prefix + dir_name).strip("/")
670
  if dir_path not in seen:
671
  seen.add(dir_path)
672
- items.append({"type":"dir","name":dir_name,"path":dir_path})
 
 
 
 
673
  else:
674
- items.append({"type":"file","name":rest,"path":(prefix + rest).strip("/")})
675
- items.sort(key=lambda x: (x["type"]!="dir", x["name"].lower()))
 
 
 
 
 
 
 
 
676
  return items
677
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  @app.route("/", methods=["GET"])
679
  def index():
680
- path = request.args.get("path","").strip("/")
681
- return render_template_string(TEMPLATE, items=list_folder(path), path=path)
 
 
682
 
683
  @app.route("/download", methods=["GET"])
684
  def download():
685
- p = request.args.get("path","")
686
- local = hf_hub_download(repo_id=REPO_ID, filename=p, repo_type="dataset", token=HF_TOKEN, cache_dir=tempfile.gettempdir())
687
- return send_file(local, as_attachment=True, download_name=os.path.basename(p))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
 
689
  @app.route("/upload", methods=["POST"])
690
  def upload():
 
 
 
 
691
  file = request.files["file"]
692
- path = request.form.get("path","").strip("/")
693
- dest = f"{path}/{file.filename}".strip("/")
694
- tmp = tempfile.NamedTemporaryFile(delete=False)
695
- file.save(tmp.name)
696
- upload_file(path_or_fileobj=tmp.name, path_in_repo=dest, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
  return redirect(f"/?path={path}")
698
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  @app.route("/delete", methods=["POST"])
700
  def delete():
701
- d = request.get_json()["path"]
702
- all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
703
- for f in all_files:
704
- if f == d or f.startswith(d.rstrip("/")+"/"):
705
- delete_file(repo_id=REPO_ID, path_in_repo=f, repo_type="dataset", token=HF_TOKEN)
706
- return jsonify(status="ok")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
 
708
  @app.route("/create_folder", methods=["POST"])
709
  def create_folder():
710
- folder = request.get_json()["path"].strip("/")
711
- keep = f"{folder}/.keep"
712
- tmp = tempfile.NamedTemporaryFile(delete=False)
713
- tmp.write(b"")
714
- tmp.flush()
715
- upload_file(path_or_fileobj=tmp.name, path_in_repo=keep, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
716
- return jsonify(status="ok")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
 
718
  @app.route("/rename", methods=["POST"])
719
  def rename():
 
720
  data = request.get_json()
721
- old, new = data["old_path"].strip("/"), data["new_path"].strip("/")
722
- all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
723
- for f in all_files:
724
- if f == old or f.startswith(old+"/"):
725
- rel = f[len(old):].lstrip("/")
726
- newp = (new + "/" + rel).strip("/")
727
- local = hf_hub_download(repo_id=REPO_ID, filename=f, repo_type="dataset",
728
- token=HF_TOKEN, cache_dir=tempfile.gettempdir())
729
- upload_file(path_or_fileobj=local, path_in_repo=newp, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
730
- delete_file(repo_id=REPO_ID, path_in_repo=f, repo_type="dataset", token=HF_TOKEN)
731
- return jsonify(status="ok")
732
-
733
- @app.route("/download_url", methods=["POST"])
734
- def download_url():
735
- data = request.get_json()
736
- url = data["url"]
737
- filename = data.get("filename")
738
- path = data.get("path", "").strip("/")
739
 
740
  try:
741
- response = requests.get(url, stream=True)
742
- response.raise_for_status()
 
743
 
744
- if not filename:
745
- from urllib.parse import urlparse
746
- filename = os.path.basename(urlparse(url).path) or "downloaded_file"
747
 
748
- dest = f"{path}/{filename}".strip("/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
 
750
- with tempfile.NamedTemporaryFile(delete=False) as tmp:
751
- for chunk in response.iter_content(chunk_size=8192):
752
- tmp.write(chunk)
 
753
 
754
- upload_file(path_or_fileobj=tmp.name, path_in_repo=dest, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
755
- os.unlink(tmp.name)
756
- return jsonify(status="ok")
757
  except Exception as e:
758
- return str(e), 400
759
 
760
  if __name__ == "__main__":
761
- app.run(debug=True, host="0.0.0.0", port=7860)
 
 
 
1
  import os
2
  import tempfile
3
  import requests
4
+ from urllib.parse import urlparse, unquote
5
  from flask import Flask, render_template_string, request, redirect, send_file, jsonify
6
  from huggingface_hub import HfApi, hf_hub_download, upload_file, delete_file
7
 
 
20
  <title>HuggingFace Drive - {{ path or 'Root' }}</title>
21
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
  <style>
23
+ * {
24
+ margin: 0;
25
+ padding: 0;
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ :root {
30
+ --primary: #6366f1;
31
+ --primary-hover: #5855eb;
32
+ --primary-light: rgba(99, 102, 241, 0.1);
33
+ --secondary: #64748b;
34
+ --secondary-hover: #475569;
35
+ --success: #10b981;
36
+ --success-hover: #059669;
37
+ --warning: #f59e0b;
38
+ --warning-hover: #d97706;
39
+ --danger: #ef4444;
40
+ --danger-hover: #dc2626;
41
+
42
+ --bg-primary: #0f172a;
43
+ --bg-secondary: #1e293b;
44
+ --bg-tertiary: #334155;
45
+ --bg-card: #1e293b;
46
+ --bg-overlay: rgba(15, 23, 42, 0.9);
47
+
48
+ --text-primary: #f8fafc;
49
+ --text-secondary: #cbd5e1;
50
+ --text-muted: #94a3b8;
51
+
52
+ --border: #334155;
53
+ --border-light: #475569;
54
+ --border-focus: var(--primary);
55
+
56
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
57
+ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
58
+ --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
59
+ --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.25);
60
+
61
+ --radius-sm: 0.375rem;
62
+ --radius-md: 0.5rem;
63
+ --radius-lg: 0.75rem;
64
+ --radius-xl: 1rem;
65
+
66
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
67
+ --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
68
+ --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
69
+ --transition-spring: 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
70
+ }
71
+
72
+ /* Base Styles */
73
+ body {
74
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
75
+ background: var(--bg-primary);
76
+ color: var(--text-primary);
77
+ line-height: 1.6;
78
+ min-height: 100vh;
79
+ -webkit-font-smoothing: antialiased;
80
+ -moz-osx-font-smoothing: grayscale;
81
+ }
82
+
83
+ /* Layout */
84
+ .container {
85
+ max-width: 1200px;
86
+ margin: 0 auto;
87
+ padding: clamp(1rem, 3vw, 1.5rem);
88
+ }
89
+
90
+ /* Header */
91
+ .header {
92
+ background: var(--bg-card);
93
+ border: 1px solid var(--border);
94
+ border-radius: var(--radius-xl);
95
+ padding: clamp(1.5rem, 4vw, 2rem);
96
+ margin-bottom: 2rem;
97
+ box-shadow: var(--shadow-lg);
98
+ animation: slideDown var(--transition-spring);
99
+ will-change: transform;
100
+ }
101
+
102
+ @keyframes slideDown {
103
+ from {
104
+ opacity: 0;
105
+ transform: translateY(-20px);
106
+ }
107
+ to {
108
+ opacity: 1;
109
+ transform: translateY(0);
110
+ }
111
+ }
112
+
113
+ .title {
114
+ font-size: clamp(1.75rem, 5vw, 2.5rem);
115
+ font-weight: 800;
116
+ color: var(--primary);
117
+ margin-bottom: 1.5rem;
118
+ display: flex;
119
+ align-items: center;
120
+ gap: clamp(0.5rem, 2vw, 1rem);
121
+ animation: fadeIn var(--transition-slow) var(--transition-fast);
122
+ }
123
+
124
+ @keyframes fadeIn {
125
+ from {
126
+ opacity: 0;
127
+ }
128
+ to {
129
+ opacity: 1;
130
+ }
131
+ }
132
+
133
+ .title-icon {
134
+ width: clamp(2.5rem, 6vw, 3rem);
135
+ height: clamp(2.5rem, 6vw, 3rem);
136
+ color: var(--primary);
137
+ animation: pulse 2s ease-in-out infinite;
138
+ }
139
+
140
+ @keyframes pulse {
141
+ 0%, 100% {
142
+ transform: scale(1);
143
+ }
144
+ 50% {
145
+ transform: scale(1.05);
146
+ }
147
+ }
148
+
149
+ /* Breadcrumb */
150
+ .breadcrumb {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 0.5rem;
154
+ margin-bottom: 2rem;
155
+ flex-wrap: wrap;
156
+ animation: fadeIn var(--transition-slow) calc(var(--transition-fast) * 2);
157
+ }
158
+
159
+ .breadcrumb-item {
160
+ background: var(--bg-tertiary);
161
+ color: var(--text-secondary);
162
+ padding: 0.5rem 1rem;
163
+ border-radius: var(--radius-md);
164
+ font-size: clamp(0.75rem, 2vw, 0.875rem);
165
+ cursor: pointer;
166
+ transition: all var(--transition-fast);
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 0.5rem;
170
+ max-width: 200px;
171
+ overflow: hidden;
172
+ text-overflow: ellipsis;
173
+ white-space: nowrap;
174
+ transform: translateZ(0);
175
+ backface-visibility: hidden;
176
+ }
177
+
178
+ .breadcrumb-item:hover {
179
+ background: var(--primary);
180
+ color: white;
181
+ transform: translateY(-2px) scale(1.05);
182
+ box-shadow: var(--shadow-md);
183
+ }
184
+
185
+ .breadcrumb-item.active {
186
+ background: var(--primary);
187
+ color: white;
188
+ animation: scaleIn var(--transition-spring);
189
+ }
190
+
191
+ @keyframes scaleIn {
192
+ from {
193
+ transform: scale(0.9);
194
+ }
195
+ to {
196
+ transform: scale(1);
197
+ }
198
+ }
199
+
200
+ .breadcrumb-separator {
201
+ color: var(--text-muted);
202
+ font-size: 1.25rem;
203
+ opacity: 0.5;
204
+ }
205
+
206
+ /* Actions */
207
+ .actions {
208
+ display: grid;
209
+ grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
210
+ gap: 1rem;
211
+ animation: fadeIn var(--transition-slow) calc(var(--transition-fast) * 3);
212
+ }
213
+
214
+ /* Buttons */
215
+ .btn {
216
+ display: inline-flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+ gap: 0.5rem;
220
+ padding: 0.75rem 1.5rem;
221
+ border: none;
222
+ border-radius: var(--radius-md);
223
+ font-weight: 600;
224
+ font-size: clamp(0.8rem, 2vw, 0.875rem);
225
+ cursor: pointer;
226
+ transition: all var(--transition-fast);
227
+ text-decoration: none;
228
+ min-height: 2.75rem;
229
+ position: relative;
230
+ overflow: hidden;
231
+ transform: translateZ(0);
232
+ backface-visibility: hidden;
233
+ -webkit-tap-highlight-color: transparent;
234
+ }
235
+
236
+ .btn::before {
237
+ content: '';
238
+ position: absolute;
239
+ top: 50%;
240
+ left: 50%;
241
+ width: 0;
242
+ height: 0;
243
+ border-radius: 50%;
244
+ background: rgba(255, 255, 255, 0.2);
245
+ transform: translate(-50%, -50%);
246
+ transition: width var(--transition-slow), height var(--transition-slow);
247
+ }
248
+
249
+ .btn:active::before {
250
+ width: 300px;
251
+ height: 300px;
252
+ }
253
+
254
+ .btn:disabled {
255
+ opacity: 0.6;
256
+ cursor: not-allowed;
257
+ transform: none !important;
258
+ }
259
+
260
+ .btn-primary {
261
+ background: var(--primary);
262
+ color: white;
263
+ box-shadow: var(--shadow-md);
264
+ }
265
+
266
+ .btn-primary:hover:not(:disabled) {
267
+ background: var(--primary-hover);
268
+ transform: translateY(-2px);
269
+ box-shadow: var(--shadow-lg);
270
+ }
271
+
272
+ .btn-secondary {
273
+ background: var(--bg-tertiary);
274
+ color: var(--text-primary);
275
+ border: 1px solid var(--border);
276
+ }
277
+
278
+ .btn-secondary:hover:not(:disabled) {
279
+ background: var(--secondary);
280
+ border-color: var(--border-light);
281
+ transform: translateY(-1px);
282
+ }
283
+
284
+ .btn-success {
285
+ background: var(--success);
286
+ color: white;
287
+ }
288
+
289
+ .btn-success:hover:not(:disabled) {
290
+ background: var(--success-hover);
291
+ transform: translateY(-2px);
292
+ }
293
+
294
+ .btn-danger {
295
+ background: var(--danger);
296
+ color: white;
297
+ }
298
+
299
+ .btn-danger:hover:not(:disabled) {
300
+ background: var(--danger-hover);
301
+ transform: translateY(-2px);
302
+ }
303
+
304
+ /* File Input */
305
+ .file-input-wrapper {
306
+ position: relative;
307
+ overflow: hidden;
308
+ display: inline-block;
309
+ width: 100%;
310
+ }
311
+
312
+ .file-input {
313
+ position: absolute;
314
+ left: 0;
315
+ top: 0;
316
+ width: 100%;
317
+ height: 100%;
318
+ opacity: 0;
319
+ cursor: pointer;
320
+ }
321
+
322
+ /* File Grid */
323
+ .file-grid {
324
+ display: grid;
325
+ grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
326
+ gap: clamp(1rem, 2vw, 1.5rem);
327
+ animation: staggerIn var(--transition-slow);
328
+ }
329
+
330
+ @keyframes staggerIn {
331
+ from {
332
+ opacity: 0;
333
+ }
334
+ to {
335
+ opacity: 1;
336
+ }
337
+ }
338
+
339
+ .file-item {
340
+ background: var(--bg-card);
341
+ border: 1px solid var(--border);
342
+ border-radius: var(--radius-lg);
343
+ overflow: hidden;
344
+ transition: all var(--transition-base);
345
+ position: relative;
346
+ transform: translateZ(0);
347
+ backface-visibility: hidden;
348
+ animation: itemFadeIn var(--transition-slow) backwards;
349
+ animation-delay: calc(var(--item-index, 0) * 50ms);
350
+ }
351
+
352
+ @keyframes itemFadeIn {
353
+ from {
354
+ opacity: 0;
355
+ transform: translateY(20px);
356
+ }
357
+ to {
358
+ opacity: 1;
359
+ transform: translateY(0);
360
+ }
361
+ }
362
+
363
+ .file-item:hover {
364
+ transform: translateY(-4px);
365
+ box-shadow: var(--shadow-xl);
366
+ border-color: var(--primary);
367
+ }
368
+
369
+ .file-item-content {
370
+ padding: clamp(1rem, 3vw, 1.5rem);
371
+ cursor: pointer;
372
+ }
373
+
374
+ .file-info {
375
+ display: flex;
376
+ align-items: center;
377
+ gap: clamp(0.75rem, 2vw, 1rem);
378
+ }
379
+
380
+ .file-icon {
381
+ width: clamp(2.5rem, 6vw, 3rem);
382
+ height: clamp(2.5rem, 6vw, 3rem);
383
+ border-radius: var(--radius-md);
384
+ display: flex;
385
+ align-items: center;
386
+ justify-content: center;
387
+ flex-shrink: 0;
388
+ font-size: clamp(1.25rem, 3vw, 1.5rem);
389
+ transition: transform var(--transition-fast);
390
+ }
391
+
392
+ .file-item:hover .file-icon {
393
+ transform: scale(1.1) rotate(5deg);
394
+ }
395
+
396
+ .file-icon.folder {
397
+ background: var(--warning);
398
+ color: white;
399
+ }
400
+
401
+ .file-icon.file {
402
+ background: var(--primary-light);
403
+ color: var(--primary);
404
+ }
405
+
406
+ .file-details {
407
+ flex: 1;
408
+ min-width: 0;
409
+ }
410
+
411
+ .file-name {
412
+ font-weight: 600;
413
+ color: var(--text-primary);
414
+ margin-bottom: 0.25rem;
415
+ word-break: break-word;
416
+ font-size: clamp(0.875rem, 2vw, 1rem);
417
+ }
418
+
419
+ .file-meta {
420
+ font-size: clamp(0.7rem, 1.5vw, 0.75rem);
421
+ color: var(--text-muted);
422
+ }
423
+
424
+ /* Dropdown */
425
+ .dropdown {
426
+ position: absolute;
427
+ top: 1rem;
428
+ right: 1rem;
429
+ }
430
+
431
+ .dropdown-toggle {
432
+ width: 2.5rem;
433
+ height: 2.5rem;
434
+ border-radius: var(--radius-md);
435
+ background: var(--bg-tertiary);
436
+ border: 1px solid var(--border);
437
+ color: var(--text-secondary);
438
+ cursor: pointer;
439
+ transition: all var(--transition-fast);
440
+ display: flex;
441
+ align-items: center;
442
+ justify-content: center;
443
+ transform: translateZ(0);
444
+ }
445
+
446
+ .dropdown-toggle:hover {
447
+ background: var(--primary);
448
+ color: white;
449
+ border-color: var(--primary);
450
+ transform: scale(1.1);
451
+ }
452
+
453
+ .dropdown-menu {
454
+ position: absolute;
455
+ top: calc(100% + 0.5rem);
456
+ right: 0;
457
+ background: var(--bg-card);
458
+ border: 1px solid var(--border);
459
+ border-radius: var(--radius-lg);
460
+ padding: 0.5rem;
461
+ min-width: 180px;
462
+ opacity: 0;
463
+ visibility: hidden;
464
+ transform: translateY(-10px) scale(0.95);
465
+ transition: all var(--transition-fast);
466
+ z-index: 1000;
467
+ box-shadow: var(--shadow-xl);
468
+ backdrop-filter: blur(10px);
469
+ }
470
+
471
+ .dropdown.active .dropdown-menu {
472
+ opacity: 1;
473
+ visibility: visible;
474
+ transform: translateY(0) scale(1);
475
+ }
476
+
477
+ .dropdown-item {
478
+ display: flex;
479
+ align-items: center;
480
+ gap: 0.75rem;
481
+ padding: 0.75rem;
482
+ border-radius: var(--radius-md);
483
+ cursor: pointer;
484
+ transition: all var(--transition-fast);
485
+ font-size: 0.875rem;
486
+ color: var(--text-secondary);
487
+ width: 100%;
488
+ text-align: left;
489
+ border: none;
490
+ background: none;
491
+ position: relative;
492
+ overflow: hidden;
493
+ }
494
+
495
+ .dropdown-item::before {
496
+ content: '';
497
+ position: absolute;
498
+ left: 0;
499
+ top: 0;
500
+ width: 0;
501
+ height: 100%;
502
+ background: var(--bg-tertiary);
503
+ transition: width var(--transition-fast);
504
+ z-index: -1;
505
+ }
506
+
507
+ .dropdown-item:hover::before {
508
+ width: 100%;
509
+ }
510
+
511
+ .dropdown-item:hover {
512
+ color: var(--text-primary);
513
+ transform: translateX(4px);
514
+ }
515
+
516
+ .dropdown-item.danger:hover {
517
+ background: var(--danger);
518
+ color: white;
519
+ }
520
+
521
+ .dropdown-item.danger:hover::before {
522
+ background: var(--danger);
523
+ }
524
+
525
+ /* Modal */
526
+ .modal-overlay {
527
+ position: fixed;
528
+ inset: 0;
529
+ background: var(--bg-overlay);
530
+ backdrop-filter: blur(8px);
531
+ display: flex;
532
+ align-items: center;
533
+ justify-content: center;
534
+ z-index: 2000;
535
+ opacity: 0;
536
+ visibility: hidden;
537
+ transition: all var(--transition-base);
538
+ padding: 1rem;
539
+ }
540
+
541
+ .modal-overlay.active {
542
+ opacity: 1;
543
+ visibility: visible;
544
+ }
545
+
546
+ .modal {
547
+ background: var(--bg-card);
548
+ border: 1px solid var(--border);
549
+ border-radius: var(--radius-xl);
550
+ padding: clamp(1.5rem, 4vw, 2rem);
551
+ width: 100%;
552
+ max-width: min(500px, 90vw);
553
+ transform: scale(0.9) translateY(20px);
554
+ transition: all var(--transition-spring);
555
+ box-shadow: var(--shadow-xl);
556
+ max-height: 90vh;
557
+ overflow-y: auto;
558
+ }
559
+
560
+ .modal-overlay.active .modal {
561
+ transform: scale(1) translateY(0);
562
+ }
563
+
564
+ .modal-title {
565
+ font-size: clamp(1.25rem, 3vw, 1.5rem);
566
+ font-weight: 700;
567
+ margin-bottom: 1.5rem;
568
+ color: var(--text-primary);
569
+ animation: slideInLeft var(--transition-base) var(--transition-fast);
570
+ }
571
+
572
+ @keyframes slideInLeft {
573
+ from {
574
+ opacity: 0;
575
+ transform: translateX(-20px);
576
+ }
577
+ to {
578
+ opacity: 1;
579
+ transform: translateX(0);
580
+ }
581
+ }
582
+
583
+ .modal-body {
584
+ margin-bottom: 2rem;
585
+ animation: fadeIn var(--transition-base) calc(var(--transition-fast) * 2);
586
+ }
587
+
588
+ .modal-input, .modal-textarea {
589
+ width: 100%;
590
+ background: var(--bg-tertiary);
591
+ border: 1px solid var(--border);
592
+ border-radius: var(--radius-md);
593
+ padding: clamp(0.75rem, 2vw, 1rem);
594
+ color: var(--text-primary);
595
+ font-size: clamp(0.875rem, 2vw, 1rem);
596
+ transition: all var(--transition-fast);
597
+ resize: vertical;
598
+ }
599
+
600
+ .modal-textarea {
601
+ min-height: 120px;
602
+ font-family: inherit;
603
+ }
604
+
605
+ .modal-input:focus, .modal-textarea:focus {
606
+ outline: none;
607
+ border-color: var(--border-focus);
608
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
609
+ transform: translateY(-1px);
610
+ }
611
+
612
+ .modal-actions {
613
+ display: flex;
614
+ gap: 1rem;
615
+ justify-content: flex-end;
616
+ animation: slideInUp var(--transition-base) calc(var(--transition-fast) * 3);
617
+ }
618
+
619
+ @keyframes slideInUp {
620
+ from {
621
+ opacity: 0;
622
+ transform: translateY(10px);
623
+ }
624
+ to {
625
+ opacity: 1;
626
+ transform: translateY(0);
627
+ }
628
+ }
629
+
630
+ /* Upload Overlay */
631
+ .upload-overlay {
632
+ position: fixed;
633
+ inset: 0;
634
+ background: var(--bg-overlay);
635
+ backdrop-filter: blur(12px);
636
+ display: flex;
637
+ flex-direction: column;
638
+ align-items: center;
639
+ justify-content: center;
640
+ z-index: 9999;
641
+ opacity: 0;
642
+ visibility: hidden;
643
+ transition: all var(--transition-base);
644
+ gap: 1.5rem;
645
+ }
646
+
647
+ .upload-overlay.active {
648
+ opacity: 1;
649
+ visibility: visible;
650
+ }
651
+
652
+ .upload-text {
653
+ font-size: clamp(1rem, 3vw, 1.25rem);
654
+ font-weight: 600;
655
+ color: var(--text-primary);
656
+ animation: fadeInScale var(--transition-spring);
657
+ }
658
+
659
+ @keyframes fadeInScale {
660
+ from {
661
+ opacity: 0;
662
+ transform: scale(0.8);
663
+ }
664
+ to {
665
+ opacity: 1;
666
+ transform: scale(1);
667
+ }
668
+ }
669
+
670
+ /* Loading Spinner */
671
+ .loading-spinner {
672
+ width: 2rem;
673
+ height: 2rem;
674
+ border: 3px solid rgba(255, 255, 255, 0.3);
675
+ border-top-color: var(--primary);
676
+ border-radius: 50%;
677
+ animation: spin 0.8s linear infinite;
678
+ will-change: transform;
679
+ }
680
+
681
+ .upload-overlay .loading-spinner {
682
+ width: clamp(2.5rem, 6vw, 3rem);
683
+ height: clamp(2.5rem, 6vw, 3rem);
684
+ border-width: 4px;
685
+ animation: spinScale 0.8s linear infinite;
686
+ }
687
+
688
+ @keyframes spin {
689
+ to { transform: rotate(360deg); }
690
+ }
691
+
692
+ @keyframes spinScale {
693
+ 0% { transform: rotate(0deg) scale(1); }
694
+ 50% { transform: rotate(180deg) scale(1.1); }
695
+ 100% { transform: rotate(360deg) scale(1); }
696
+ }
697
+
698
+ /* Utilities */
699
+ .icon {
700
+ width: 1.25rem;
701
+ height: 1.25rem;
702
+ flex-shrink: 0;
703
+ transition: transform var(--transition-fast);
704
+ }
705
+
706
+ .sr-only {
707
+ position: absolute;
708
+ width: 1px;
709
+ height: 1px;
710
+ padding: 0;
711
+ margin: -1px;
712
+ overflow: hidden;
713
+ clip: rect(0, 0, 0, 0);
714
+ white-space: nowrap;
715
+ border: 0;
716
+ }
717
+
718
+ /* Empty State */
719
+ .empty-state {
720
+ text-align: center;
721
+ padding: clamp(3rem, 8vw, 4rem) 2rem;
722
+ color: var(--text-muted);
723
+ animation: fadeIn var(--transition-slow);
724
+ }
725
+
726
+ .empty-state .icon {
727
+ width: clamp(3rem, 8vw, 4rem);
728
+ height: clamp(3rem, 8vw, 4rem);
729
+ margin: 0 auto 1rem;
730
+ opacity: 0.5;
731
+ animation: float 3s ease-in-out infinite;
732
+ }
733
+
734
+ @keyframes float {
735
+ 0%, 100% { transform: translateY(0); }
736
+ 50% { transform: translateY(-10px); }
737
+ }
738
+
739
+ .empty-state h3 {
740
+ font-size: clamp(1.25rem, 3vw, 1.5rem);
741
+ margin-bottom: 0.5rem;
742
+ }
743
+
744
+ .empty-state p {
745
+ font-size: clamp(0.875rem, 2vw, 1rem);
746
+ }
747
+
748
+ /* Performance optimizations */
749
+ @media (prefers-reduced-motion: reduce) {
750
+ *, *::before, *::after {
751
+ animation-duration: 0.01ms !important;
752
+ animation-iteration-count: 1 !important;
753
+ transition-duration: 0.01ms !important;
754
+ }
755
+ }
756
+
757
+ /* Responsive Design */
758
+ @media (max-width: 768px) {
759
+ .breadcrumb-separator {
760
+ display: none;
761
+ }
762
+
763
+ .breadcrumb {
764
+ gap: 0.25rem;
765
+ }
766
+
767
+ .breadcrumb-item {
768
+ padding: 0.375rem 0.75rem;
769
+ font-size: 0.75rem;
770
+ }
771
+
772
+ .file-grid {
773
+ grid-template-columns: 1fr;
774
+ }
775
+
776
+ .modal-actions {
777
+ flex-direction: column-reverse;
778
+ }
779
+
780
+ .modal-actions .btn {
781
+ width: 100%;
782
+ }
783
+
784
+ .dropdown-menu {
785
+ position: fixed;
786
+ top: auto;
787
+ bottom: 1rem;
788
+ right: 1rem;
789
+ left: 1rem;
790
+ max-width: 400px;
791
+ margin: 0 auto;
792
+ }
793
+ }
794
+
795
+ @media (max-width: 480px) {
796
+ .breadcrumb-item {
797
+ max-width: 120px;
798
+ }
799
+
800
+ .file-item {
801
+ animation: none;
802
+ }
803
+
804
+ .title {
805
+ flex-wrap: wrap;
806
+ text-align: center;
807
+ justify-content: center;
808
+ }
809
+
810
+ .actions {
811
+ gap: 0.75rem;
812
+ }
813
+ }
814
+
815
+ /* Touch device optimizations */
816
+ @media (hover: none) and (pointer: coarse) {
817
+ .btn {
818
+ min-height: 3rem;
819
+ padding: 1rem 1.5rem;
820
+ }
821
+
822
+ .dropdown-toggle {
823
+ width: 3rem;
824
+ height: 3rem;
825
+ }
826
+
827
+ .file-item:active {
828
+ transform: scale(0.98);
829
+ }
830
+
831
+ .btn:active {
832
+ transform: scale(0.95);
833
+ }
834
+ }
835
+
836
+ /* High contrast mode support */
837
+ @media (prefers-contrast: high) {
838
+ :root {
839
+ --primary: #818cf8;
840
+ --bg-primary: #000;
841
+ --bg-secondary: #1a1a1a;
842
+ --text-primary: #fff;
843
+ --border: #666;
844
+ }
845
+ }
846
+
847
+ /* Dark mode optimizations */
848
+ @media (prefers-color-scheme: dark) {
849
+ .loading-spinner {
850
+ filter: brightness(1.2);
851
+ }
852
+ }
853
+
854
+ /* Print styles */
855
+ @media print {
856
+ .header, .actions, .dropdown, .modal-overlay, .upload-overlay {
857
+ display: none !important;
858
+ }
859
+
860
+ .file-grid {
861
+ grid-template-columns: 1fr;
862
+ gap: 0.5rem;
863
+ }
864
+
865
+ .file-item {
866
+ border: 1px solid #000;
867
+ page-break-inside: avoid;
868
+ }
869
+ }
870
  </style>
871
  </head>
872
  <body>
873
  <!-- Upload Overlay -->
874
  <div class="upload-overlay" id="uploadOverlay">
875
+ <div class="loading-spinner"></div>
876
+ <p class="upload-text">Processing...</p>
877
  </div>
878
+
879
  <div class="container">
880
  <header class="header">
881
  <h1 class="title">
882
+ <svg class="title-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
883
+ <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>
884
+ <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>
885
+ </svg>
886
  HuggingFace Drive
887
  </h1>
888
+
889
+ <!-- Breadcrumb -->
890
  <nav class="breadcrumb">
891
  <span class="breadcrumb-item {{ 'active' if not path else '' }}" onclick="nav('')">
892
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
893
+ <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"/>
894
+ </svg>
895
  Home
896
  </span>
897
  {% if path %}
 
901
  {% set current_path = parts[:i+1]|join('/') %}
902
  <span class="breadcrumb-item {{ 'active' if current_path == path else '' }}"
903
  onclick="nav('{{ current_path }}')">
904
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
905
+ <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
906
+ </svg>
907
  {{ parts[i] }}
908
  </span>
909
  {% endfor %}
910
  {% endif %}
911
  </nav>
912
+
913
  <!-- Actions -->
914
  <div class="actions">
915
  <form action="/upload" method="post" enctype="multipart/form-data" id="uploadForm">
916
  <div class="file-input-wrapper">
917
+ <label for="fileInput" class="btn btn-primary">
918
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
919
+ <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"/>
920
+ </svg>
921
+ Upload File
922
+ </label>
923
+ <input type="file" name="file" required class="file-input" id="fileInput">
924
+ <input type="hidden" name="path" value="{{ path }}">
925
  </div>
926
  </form>
927
+
928
  <button class="btn btn-secondary" onclick="showFolderModal('{{ path }}')">
929
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
930
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
931
+ </svg>
932
  New Folder
933
  </button>
934
+
935
+ <button class="btn btn-success" onclick="showUrlDownloadModal('{{ path }}')">
936
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
937
+ <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"/>
938
+ </svg>
939
  Download from URL
940
  </button>
941
  </div>
942
  </header>
943
+
944
  <!-- File Grid -->
945
+ <main>
946
+ {% if items %}
947
+ <div class="file-grid">
948
+ {% for item in items %}
949
+ <div class="file-item">
950
+ <div class="file-item-content" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% else %}download('{{ item.path }}'){% endif %}">
951
+ <div class="file-info">
952
+ <div class="file-icon {{ item.type }}">
953
+ {% if item.type == 'dir' %}
954
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
955
+ <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
956
+ </svg>
957
+ {% else %}
958
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
959
+ <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"/>
960
+ </svg>
961
+ {% endif %}
962
+ </div>
963
+ <div class="file-details">
964
+ <div class="file-name">{{ item.name }}</div>
965
+ <div class="file-meta">{{ item.type|title }}</div>
966
+ </div>
967
  </div>
 
 
 
968
  </div>
969
+
970
+ <div class="dropdown">
971
+ <button class="dropdown-toggle" onclick="toggleDropdown(event, this)">
972
+ <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
973
+ <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"/>
974
+ </svg>
975
+ </button>
976
+ <div class="dropdown-menu">
977
+ {% if item.type == 'file' %}
978
+ <button class="dropdown-item" onclick="download('{{ item.path }}')">
979
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
980
+ <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"/>
981
+ </svg>
982
+ Download
983
+ </button>
984
+ {% endif %}
985
+ <button class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">
986
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
987
+ <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"/>
988
+ </svg>
989
+ Rename
990
+ </button>
991
+ <button class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">
992
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
993
+ <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"/>
994
+ </svg>
995
+ Delete
996
+ </button>
997
+ </div>
998
  </div>
999
  </div>
1000
+ {% endfor %}
1001
  </div>
1002
+ {% else %}
1003
+ <div class="empty-state">
1004
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1005
+ <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"/>
1006
+ </svg>
1007
+ <h3>No files yet</h3>
1008
+ <p>Upload your first file or create a folder to get started.</p>
1009
+ </div>
1010
+ {% endif %}
1011
  </main>
1012
  </div>
1013
+
1014
  <!-- Modals -->
1015
+ <!-- Rename Modal -->
1016
  <div class="modal-overlay" id="renameModal">
1017
  <div class="modal">
1018
  <h3 class="modal-title">Rename Item</h3>
 
1022
  <div class="modal-actions">
1023
  <button class="btn btn-secondary" onclick="closeModal('renameModal')">Cancel</button>
1024
  <button class="btn btn-primary" onclick="confirmRename()" id="renameConfirmBtn">
1025
+ <span id="renameBtnText">Rename</span>
1026
+ <span id="renameBtnSpinner" class="loading-spinner" style="display: none;"></span>
1027
  </button>
1028
  </div>
1029
  </div>
1030
  </div>
1031
+
1032
+ <!-- Delete Modal -->
1033
  <div class="modal-overlay" id="deleteModal">
1034
  <div class="modal">
1035
  <h3 class="modal-title">Confirm Deletion</h3>
1036
  <div class="modal-body">
1037
+ <p>Are you sure you want to delete <strong id="deleteItemName"></strong>? This action cannot be undone.</p>
1038
  </div>
1039
  <div class="modal-actions">
1040
  <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
1041
  <button class="btn btn-danger" onclick="confirmDelete()" id="deleteConfirmBtn">
1042
+ <span id="deleteBtnText">Delete</span>
1043
+ <span id="deleteBtnSpinner" class="loading-spinner" style="display: none;"></span>
1044
  </button>
1045
  </div>
1046
  </div>
1047
  </div>
1048
+
1049
+ <!-- Folder Modal -->
1050
  <div class="modal-overlay" id="folderModal">
1051
  <div class="modal">
1052
  <h3 class="modal-title">Create New Folder</h3>
 
1056
  <div class="modal-actions">
1057
  <button class="btn btn-secondary" onclick="closeModal('folderModal')">Cancel</button>
1058
  <button class="btn btn-primary" onclick="confirmCreateFolder()" id="folderConfirmBtn">
1059
+ <span id="folderBtnText">Create</span>
1060
+ <span id="folderBtnSpinner" class="loading-spinner" style="display: none;"></span>
1061
  </button>
1062
  </div>
1063
  </div>
1064
  </div>
1065
+
1066
+ <!-- URL Download Modal -->
1067
+ <div class="modal-overlay" id="urlDownloadModal">
1068
  <div class="modal">
1069
  <h3 class="modal-title">Download from URL</h3>
1070
  <div class="modal-body">
1071
+ <input type="text" class="modal-input" id="urlInput" placeholder="https://example.com/file.pdf" style="margin-bottom: 1rem;">
1072
+ <input type="text" class="modal-input" id="filenameInput" placeholder="Custom filename (optional)">
1073
+ <p style="font-size: 0.875rem; color: var(--text-muted); margin-top: 0.5rem;">
1074
+ The file will be downloaded directly to your HuggingFace repository.
1075
+ </p>
1076
  </div>
1077
  <div class="modal-actions">
1078
+ <button class="btn btn-secondary" onclick="closeModal('urlDownloadModal')">Cancel</button>
1079
+ <button class="btn btn-success" onclick="confirmUrlDownload()" id="urlDownloadConfirmBtn">
1080
+ <span id="urlDownloadBtnText">Download</span>
1081
+ <span id="urlDownloadBtnSpinner" class="loading-spinner" style="display: none;"></span>
1082
  </button>
1083
  </div>
1084
  </div>
1085
  </div>
1086
 
1087
  <script>
1088
+ // Global state
1089
  let currentRenamePath = '';
1090
  let currentDeletePath = '';
1091
  let currentFolderPath = '';
1092
+ let currentUrlDownloadPath = '';
1093
  let activeDropdown = null;
1094
 
1095
+ // Navigation
1096
+ function nav(path) {
1097
+ window.location.href = '/?path=' + encodeURIComponent(path);
1098
  }
1099
+
1100
  function download(path) {
1101
  window.open('/download?path=' + encodeURIComponent(path), '_blank');
1102
  }
1103
 
1104
+ // Dropdown management
1105
+ function toggleDropdown(event, button) {
1106
+ event.stopPropagation();
1107
  const dropdown = button.closest('.dropdown');
1108
+
1109
  if (activeDropdown && activeDropdown !== dropdown) {
1110
  activeDropdown.classList.remove('active');
1111
  }
1112
+
1113
  dropdown.classList.toggle('active');
1114
  activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
1115
  }
1116
 
1117
+ function closeAllDropdowns() {
1118
+ if (activeDropdown) {
1119
+ activeDropdown.classList.remove('active');
1120
+ activeDropdown = null;
1121
+ }
1122
+ }
1123
+
1124
+ // Loading state management
1125
+ function showSpinner(btnId, textId, spinnerId) {
1126
+ const btn = document.getElementById(btnId);
1127
+ const text = document.getElementById(textId);
1128
+ const spinner = document.getElementById(spinnerId);
1129
+
1130
+ btn.disabled = true;
1131
+ text.style.display = 'none';
1132
+ spinner.style.display = 'inline-block';
1133
+ }
1134
+
1135
+ function hideSpinner(btnId, textId, spinnerId) {
1136
+ const btn = document.getElementById(btnId);
1137
+ const text = document.getElementById(textId);
1138
+ const spinner = document.getElementById(spinnerId);
1139
+
1140
+ btn.disabled = false;
1141
+ text.style.display = 'inline-block';
1142
+ spinner.style.display = 'none';
1143
+ }
1144
+
1145
+ // Modal management
1146
  function showModal(modalId) {
1147
+ const modal = document.getElementById(modalId);
1148
+ modal.classList.add('active');
1149
+ document.body.style.overflow = 'hidden';
1150
+
1151
+ // Focus first input in modal
1152
+ const firstInput = modal.querySelector('.modal-input');
1153
+ if (firstInput) {
1154
+ setTimeout(() => {
1155
+ firstInput.focus();
1156
+ if (firstInput.type === 'text') {
1157
+ firstInput.select();
1158
+ }
1159
+ }, 100);
1160
+ }
1161
  }
1162
 
1163
  function closeModal(modalId) {
1164
+ const modal = document.getElementById(modalId);
1165
+ modal.classList.remove('active');
1166
+ document.body.style.overflow = '';
1167
  }
1168
 
1169
+ // Modal show functions
1170
  function showRenameModal(path, currentName) {
1171
  currentRenamePath = path;
1172
  document.getElementById('renameInput').value = currentName;
1173
  showModal('renameModal');
1174
+ closeAllDropdowns();
1175
  }
1176
 
1177
  function showDeleteModal(path, name) {
1178
  currentDeletePath = path;
1179
  document.getElementById('deleteItemName').textContent = name;
1180
  showModal('deleteModal');
1181
+ closeAllDropdowns();
1182
  }
1183
 
1184
  function showFolderModal(path) {
1185
  currentFolderPath = path;
1186
+ document.getElementById('folderInput').value = '';
1187
  showModal('folderModal');
1188
  }
1189
 
1190
+ function showUrlDownloadModal(path) {
1191
+ currentUrlDownloadPath = path;
1192
  document.getElementById('urlInput').value = '';
1193
  document.getElementById('filenameInput').value = '';
1194
+ showModal('urlDownloadModal');
1195
  }
1196
 
1197
+ // Generic action performer
1198
+ async function performAction(url, body, btnId, textId, spinnerId, modalToClose, successMessage) {
1199
+ showSpinner(btnId, textId, spinnerId);
1200
+
 
1201
  try {
1202
  const response = await fetch(url, {
1203
  method: 'POST',
1204
  headers: { 'Content-Type': 'application/json' },
1205
  body: JSON.stringify(body)
1206
  });
1207
+
1208
+ const result = await response.json();
1209
+
1210
  if (response.ok) {
1211
+ if (modalToClose) closeModal(modalToClose);
1212
+ if (successMessage) {
1213
+ // Show brief success message
1214
+ showUploadOverlay(successMessage);
1215
+ setTimeout(() => hideUploadOverlay(), 1500);
1216
+ }
1217
+ setTimeout(() => window.location.reload(), successMessage ? 1500 : 0);
1218
  } else {
1219
+ throw new Error(result.error || 'Operation failed');
1220
  }
1221
  } catch (error) {
1222
+ console.error('Action failed:', error);
1223
+ alert(`Operation failed: ${error.message}`);
1224
  } finally {
1225
+ hideSpinner(btnId, textId, spinnerId);
 
1226
  }
1227
  }
1228
 
1229
+ // Action confirmations
1230
  function confirmRename() {
1231
  const newName = document.getElementById('renameInput').value.trim();
1232
+ if (!newName) {
1233
+ alert('Please enter a valid name');
1234
+ return;
1235
+ }
1236
+
1237
+ performAction(
1238
+ '/rename',
1239
+ { old_path: currentRenamePath, new_path: newName },
1240
+ 'renameConfirmBtn',
1241
+ 'renameBtnText',
1242
+ 'renameBtnSpinner',
1243
+ 'renameModal',
1244
+ 'Item renamed successfully!'
1245
+ );
1246
  }
1247
 
1248
  function confirmDelete() {
1249
+ performAction(
1250
+ '/delete',
1251
+ { path: currentDeletePath },
1252
+ 'deleteConfirmBtn',
1253
+ 'deleteBtnText',
1254
+ 'deleteBtnSpinner',
1255
+ 'deleteModal',
1256
+ 'Item deleted successfully!'
1257
+ );
1258
  }
1259
 
1260
  function confirmCreateFolder() {
1261
+ const folderName = document.getElementById('folderInput').value.trim();
1262
+ if (!folderName) {
1263
+ alert('Please enter a folder name');
1264
+ return;
1265
+ }
1266
+
1267
+ const folderPath = currentFolderPath ? `${currentFolderPath}/${folderName}` : folderName;
1268
+ performAction(
1269
+ '/create_folder',
1270
+ { path: folderPath },
1271
+ 'folderConfirmBtn',
1272
+ 'folderBtnText',
1273
+ 'folderBtnSpinner',
1274
+ 'folderModal',
1275
+ 'Folder created successfully!'
1276
+ );
1277
  }
1278
 
1279
+ function confirmUrlDownload() {
1280
  const url = document.getElementById('urlInput').value.trim();
1281
+ const customFilename = document.getElementById('filenameInput').value.trim();
1282
+
1283
+ if (!url) {
1284
+ alert('Please enter a valid URL');
1285
+ return;
1286
+ }
1287
+
1288
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
1289
+ alert('Please enter a valid HTTP/HTTPS URL');
1290
+ return;
1291
+ }
1292
+
1293
+ performAction(
1294
+ '/download_url',
1295
+ {
1296
+ url: url,
1297
+ path: currentUrlDownloadPath,
1298
+ filename: customFilename || null
1299
+ },
1300
+ 'urlDownloadConfirmBtn',
1301
+ 'urlDownloadBtnText',
1302
+ 'urlDownloadBtnSpinner',
1303
+ 'urlDownloadModal',
1304
+ 'File downloaded successfully!'
1305
+ );
1306
  }
1307
 
1308
+ // Upload overlay management
1309
+ function showUploadOverlay(message = 'Processing...') {
1310
+ const overlay = document.getElementById('uploadOverlay');
1311
+ const text = overlay.querySelector('.upload-text');
1312
+ text.textContent = message;
1313
+ overlay.classList.add('active');
1314
+ }
1315
+
1316
+ function hideUploadOverlay() {
1317
+ document.getElementById('uploadOverlay').classList.remove('active');
1318
+ }
1319
+
1320
+ // Event listeners
1321
+ document.addEventListener('DOMContentLoaded', function() {
1322
+ // File input change handler
1323
+ document.getElementById('fileInput').addEventListener('change', function(e) {
1324
+ if (this.files.length > 0) {
1325
+ showUploadOverlay('Uploading file...');
1326
+ document.getElementById('uploadForm').submit();
1327
+ }
1328
+ });
1329
+
1330
+ // Modal keyboard handlers
1331
+ const setupModalKeyListener = (inputId, confirmFunction) => {
1332
+ const input = document.getElementById(inputId);
1333
+ if (input) {
1334
+ input.addEventListener('keypress', (e) => {
1335
+ if (e.key === 'Enter') {
1336
+ e.preventDefault();
1337
+ confirmFunction();
1338
+ }
1339
+ });
1340
+ }
1341
+ };
1342
+
1343
+ setupModalKeyListener('renameInput', confirmRename);
1344
+ setupModalKeyListener('folderInput', confirmCreateFolder);
1345
+ setupModalKeyListener('urlInput', confirmUrlDownload);
1346
+ setupModalKeyListener('filenameInput', confirmUrlDownload);
1347
+
1348
+ // Global click handler
1349
+ document.addEventListener('click', (e) => {
1350
+ // Close dropdowns when clicking outside
1351
+ if (!e.target.closest('.dropdown')) {
1352
+ closeAllDropdowns();
1353
+ }
1354
+
1355
+ // Close modal when clicking overlay
1356
+ if (e.target.classList.contains('modal-overlay')) {
1357
+ closeModal(e.target.id);
1358
+ }
1359
+ });
1360
+
1361
+ // Global keyboard handler
1362
+ document.addEventListener('keydown', (e) => {
1363
+ if (e.key === 'Escape') {
1364
+ const openModal = document.querySelector('.modal-overlay.active');
1365
+ if (openModal) {
1366
+ closeModal(openModal.id);
1367
+ } else {
1368
+ closeAllDropdowns();
1369
+ }
1370
+ }
1371
+ });
1372
  });
1373
 
1374
+ // Prevent form submission on enter in modals (except for specific inputs)
1375
+ document.addEventListener('keydown', function(e) {
1376
+ if (e.key === 'Enter' && e.target.closest('.modal') && e.target.tagName !== 'BUTTON') {
1377
+ const modal = e.target.closest('.modal-overlay');
1378
+ if (modal) {
1379
+ e.preventDefault();
1380
+ }
1381
  }
1382
  });
1383
  </script>
 
1386
  """
1387
 
1388
  def list_folder(path=""):
1389
+ """List files and folders in the given path"""
1390
  prefix = path.strip("/") + ("/" if path else "")
1391
+
1392
+ try:
1393
+ all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
1394
+ except Exception as e:
1395
+ print(f"Error listing files: {e}")
1396
+ return []
1397
+
1398
  seen = set()
1399
  items = []
1400
+
1401
  for f in all_files:
1402
+ if not f.startswith(prefix):
1403
+ continue
1404
+
1405
  rest = f[len(prefix):]
1406
  if "/" in rest:
1407
+ # This is a subdirectory
1408
  dir_name = rest.split("/")[0]
1409
  dir_path = (prefix + dir_name).strip("/")
1410
  if dir_path not in seen:
1411
  seen.add(dir_path)
1412
+ items.append({
1413
+ "type": "dir",
1414
+ "name": dir_name,
1415
+ "path": dir_path
1416
+ })
1417
  else:
1418
+ # This is a file
1419
+ if rest: # Skip empty filenames
1420
+ items.append({
1421
+ "type": "file",
1422
+ "name": rest,
1423
+ "path": (prefix + rest).strip("/")
1424
+ })
1425
+
1426
+ # Sort directories first, then files, both alphabetically
1427
+ items.sort(key=lambda x: (x["type"] != "dir", x["name"].lower()))
1428
  return items
1429
 
1430
+ def get_filename_from_url(url, custom_filename=None):
1431
+ """Extract filename from URL or use custom filename"""
1432
+ if custom_filename:
1433
+ return custom_filename
1434
+
1435
+ # Parse URL and get the path
1436
+ parsed_url = urlparse(url)
1437
+ path = unquote(parsed_url.path)
1438
+
1439
+ # Get filename from path
1440
+ filename = os.path.basename(path)
1441
+
1442
+ # If no filename found, generate one
1443
+ if not filename or '.' not in filename:
1444
+ filename = f"downloaded_file_{int(time.time())}"
1445
+
1446
+ return filename
1447
+
1448
  @app.route("/", methods=["GET"])
1449
  def index():
1450
+ """Main page - file browser"""
1451
+ path = request.args.get("path", "").strip("/")
1452
+ items = list_folder(path)
1453
+ return render_template_string(TEMPLATE, items=items, path=path)
1454
 
1455
  @app.route("/download", methods=["GET"])
1456
  def download():
1457
+ """Download a file from the repository"""
1458
+ file_path = request.args.get("path", "")
1459
+ if not file_path:
1460
+ return "No file path provided", 400
1461
+
1462
+ try:
1463
+ # Download file to temporary location
1464
+ local_path = hf_hub_download(
1465
+ repo_id=REPO_ID,
1466
+ filename=file_path,
1467
+ repo_type="dataset",
1468
+ token=HF_TOKEN,
1469
+ cache_dir=tempfile.gettempdir()
1470
+ )
1471
+
1472
+ # Send file to user
1473
+ return send_file(
1474
+ local_path,
1475
+ as_attachment=True,
1476
+ download_name=os.path.basename(file_path)
1477
+ )
1478
+ except Exception as e:
1479
+ return f"Error downloading file: {str(e)}", 500
1480
 
1481
  @app.route("/upload", methods=["POST"])
1482
  def upload():
1483
+ """Upload a file to the repository"""
1484
+ if 'file' not in request.files:
1485
+ return "No file provided", 400
1486
+
1487
  file = request.files["file"]
1488
+ if file.filename == '':
1489
+ return "No file selected", 400
1490
+
1491
+ path = request.form.get("path", "").strip("/")
1492
+
1493
+ # Create destination path
1494
+ dest_path = f"{path}/{file.filename}".strip("/")
1495
+
1496
+ # Save file to temporary location
1497
+ with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
1498
+ file.save(tmp_file.name)
1499
+
1500
+ try:
1501
+ # Upload to HuggingFace
1502
+ upload_file(
1503
+ path_or_fileobj=tmp_file.name,
1504
+ path_in_repo=dest_path,
1505
+ repo_id=REPO_ID,
1506
+ repo_type="dataset",
1507
+ token=HF_TOKEN
1508
+ )
1509
+ except Exception as e:
1510
+ os.unlink(tmp_file.name)
1511
+ return f"Error uploading file: {str(e)}", 500
1512
+ finally:
1513
+ # Clean up temporary file
1514
+ try:
1515
+ os.unlink(tmp_file.name)
1516
+ except:
1517
+ pass
1518
+
1519
  return redirect(f"/?path={path}")
1520
 
1521
+ @app.route("/download_url", methods=["POST"])
1522
+ def download_url():
1523
+ """Download a file from URL and upload to repository"""
1524
+ data = request.get_json()
1525
+ url = data.get("url", "").strip()
1526
+ path = data.get("path", "").strip("/")
1527
+ custom_filename = data.get("filename", "").strip()
1528
+
1529
+ if not url:
1530
+ return jsonify({"error": "No URL provided"}), 400
1531
+
1532
+ if not url.startswith(('http://', 'https://')):
1533
+ return jsonify({"error": "Invalid URL. Must start with http:// or https://"}), 400
1534
+
1535
+ try:
1536
+ # Get filename
1537
+ filename = get_filename_from_url(url, custom_filename)
1538
+ dest_path = f"{path}/{filename}".strip("/")
1539
+
1540
+ # Download file from URL
1541
+ response = requests.get(url, stream=True, timeout=30)
1542
+ response.raise_for_status()
1543
+
1544
+ # Check if content-length is reasonable (max 500MB)
1545
+ content_length = response.headers.get('content-length')
1546
+ if content_length and int(content_length) > 500 * 1024 * 1024:
1547
+ return jsonify({"error": "File too large. Maximum size is 500MB"}), 400
1548
+
1549
+ # Save to temporary file
1550
+ with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
1551
+ # Download in chunks
1552
+ for chunk in response.iter_content(chunk_size=8192):
1553
+ tmp_file.write(chunk)
1554
+
1555
+ tmp_file.flush()
1556
+
1557
+ # Upload to HuggingFace
1558
+ upload_file(
1559
+ path_or_fileobj=tmp_file.name,
1560
+ path_in_repo=dest_path,
1561
+ repo_id=REPO_ID,
1562
+ repo_type="dataset",
1563
+ token=HF_TOKEN
1564
+ )
1565
+
1566
+ # Clean up
1567
+ os.unlink(tmp_file.name)
1568
+
1569
+ return jsonify({"status": "success", "message": "File downloaded and uploaded successfully"})
1570
+
1571
+ except requests.exceptions.RequestException as e:
1572
+ return jsonify({"error": f"Failed to download from URL: {str(e)}"}), 400
1573
+ except Exception as e:
1574
+ return jsonify({"error": f"Upload failed: {str(e)}"}), 500
1575
+
1576
  @app.route("/delete", methods=["POST"])
1577
  def delete():
1578
+ """Delete a file or folder from the repository"""
1579
+ data = request.get_json()
1580
+ delete_path = data.get("path", "").strip("/")
1581
+
1582
+ if not delete_path:
1583
+ return jsonify({"error": "No path provided"}), 400
1584
+
1585
+ try:
1586
+ all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
1587
+ deleted_count = 0
1588
+
1589
+ for file_path in all_files:
1590
+ # Delete exact match or files within the folder
1591
+ if file_path == delete_path or file_path.startswith(delete_path.rstrip("/") + "/"):
1592
+ delete_file(
1593
+ repo_id=REPO_ID,
1594
+ path_in_repo=file_path,
1595
+ repo_type="dataset",
1596
+ token=HF_TOKEN
1597
+ )
1598
+ deleted_count += 1
1599
+
1600
+ if deleted_count == 0:
1601
+ return jsonify({"error": "No files found to delete"}), 404
1602
+
1603
+ return jsonify({"status": "success", "message": f"Deleted {deleted_count} file(s)"})
1604
+
1605
+ except Exception as e:
1606
+ return jsonify({"error": f"Delete failed: {str(e)}"}), 500
1607
 
1608
  @app.route("/create_folder", methods=["POST"])
1609
  def create_folder():
1610
+ """Create a new folder by uploading a .keep file"""
1611
+ data = request.get_json()
1612
+ folder_path = data.get("path", "").strip("/")
1613
+
1614
+ if not folder_path:
1615
+ return jsonify({"error": "No folder path provided"}), 400
1616
+
1617
+ # Create .keep file path
1618
+ keep_file_path = f"{folder_path}/.keep"
1619
+
1620
+ try:
1621
+ # Create temporary empty file
1622
+ with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
1623
+ tmp_file.write(b"# This file keeps the folder in git\n")
1624
+ tmp_file.flush()
1625
+
1626
+ # Upload .keep file
1627
+ upload_file(
1628
+ path_or_fileobj=tmp_file.name,
1629
+ path_in_repo=keep_file_path,
1630
+ repo_id=REPO_ID,
1631
+ repo_type="dataset",
1632
+ token=HF_TOKEN
1633
+ )
1634
+
1635
+ # Clean up
1636
+ os.unlink(tmp_file.name)
1637
+
1638
+ return jsonify({"status": "success", "message": "Folder created successfully"})
1639
+
1640
+ except Exception as e:
1641
+ return jsonify({"error": f"Failed to create folder: {str(e)}"}), 500
1642
 
1643
  @app.route("/rename", methods=["POST"])
1644
  def rename():
1645
+ """Rename a file or folder"""
1646
  data = request.get_json()
1647
+ old_path = data.get("old_path", "").strip("/")
1648
+ new_name = data.get("new_path", "").strip()
1649
+
1650
+ if not old_path or not new_name:
1651
+ return jsonify({"error": "Missing old path or new name"}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
1652
 
1653
  try:
1654
+ # Get parent directory
1655
+ parent_dir = "/".join(old_path.split("/")[:-1]) if "/" in old_path else ""
1656
+ new_path = f"{parent_dir}/{new_name}".strip("/")
1657
 
1658
+ all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
1659
+ renamed_count = 0
 
1660
 
1661
+ for file_path in all_files:
1662
+ if file_path == old_path or file_path.startswith(old_path + "/"):
1663
+ # Calculate new file path
1664
+ relative_path = file_path[len(old_path):].lstrip("/")
1665
+ new_file_path = (new_path + "/" + relative_path).strip("/")
1666
+
1667
+ # Download original file
1668
+ local_path = hf_hub_download(
1669
+ repo_id=REPO_ID,
1670
+ filename=file_path,
1671
+ repo_type="dataset",
1672
+ token=HF_TOKEN,
1673
+ cache_dir=tempfile.gettempdir()
1674
+ )
1675
+
1676
+ # Upload with new name
1677
+ upload_file(
1678
+ path_or_fileobj=local_path,
1679
+ path_in_repo=new_file_path,
1680
+ repo_id=REPO_ID,
1681
+ repo_type="dataset",
1682
+ token=HF_TOKEN
1683
+ )
1684
+
1685
+ # Delete original file
1686
+ delete_file(
1687
+ repo_id=REPO_ID,
1688
+ path_in_repo=file_path,
1689
+ repo_type="dataset",
1690
+ token=HF_TOKEN
1691
+ )
1692
+
1693
+ renamed_count += 1
1694
 
1695
+ if renamed_count == 0:
1696
+ return jsonify({"error": "No files found to rename"}), 404
1697
+
1698
+ return jsonify({"status": "success", "message": f"Renamed {renamed_count} file(s)"})
1699
 
 
 
 
1700
  except Exception as e:
1701
+ return jsonify({"error": f"Rename failed: {str(e)}"}), 500
1702
 
1703
  if __name__ == "__main__":
1704
+ # Add missing import
1705
+ import time
1706
+ app.run(debug=True, host="0.0.0.0", port=7860)