testdeep123 commited on
Commit
cac46a3
·
verified ·
1 Parent(s): a88b84c

Update app.py

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