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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +298 -992
app.py CHANGED
@@ -1,7 +1,6 @@
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,393 +19,255 @@ TEMPLATE = """
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 {
365
- opacity: 1;
366
- visibility: visible;
367
- transform: translateY(0);
368
  }
369
 
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,15 +276,13 @@ TEMPLATE = """
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 {
@@ -431,37 +290,21 @@ TEMPLATE = """
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 {
@@ -470,21 +313,36 @@ TEMPLATE = """
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 {
@@ -492,128 +350,35 @@ TEMPLATE = """
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>
@@ -621,26 +386,19 @@ TEMPLATE = """
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,118 +408,67 @@ TEMPLATE = """
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>
@@ -771,31 +478,27 @@ TEMPLATE = """
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>
788
  <div class="modal-actions">
789
  <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
790
  <button class="btn btn-danger" onclick="confirmDelete()" id="deleteConfirmBtn">
791
- <span id="deleteBtnText">Delete</span>
792
- <span id="deleteBtnSpinner" class="loading-spinner" style="display: none;"></span>
793
  </button>
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>
@@ -805,328 +508,147 @@ TEMPLATE = """
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>
811
  </div>
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');
857
-
858
  if (activeDropdown && activeDropdown !== dropdown) {
859
  activeDropdown.classList.remove('active');
860
  }
861
-
862
  dropdown.classList.toggle('active');
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
 
926
  function showDeleteModal(path, name) {
927
  currentDeletePath = path;
928
  document.getElementById('deleteItemName').textContent = name;
929
  showModal('deleteModal');
930
- closeAllDropdowns();
931
  }
932
 
933
  function showFolderModal(path) {
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() {
1010
- const folderName = document.getElementById('folderInput').value.trim();
1011
- if (!folderName) {
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,321 +657,105 @@ TEMPLATE = """
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)
 
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
  <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 {
 
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 {
 
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 {
 
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 {
 
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>
 
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
  {% 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
  <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
  <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
  """
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)