testdeep123 commited on
Commit
ae31e69
·
verified ·
1 Parent(s): fd04a23

Update app.py

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