testdeep123 commited on
Commit
65096b4
·
verified ·
1 Parent(s): 15f5037

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +506 -620
app.py CHANGED
@@ -1,745 +1,631 @@
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
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
+ # Save the token for hf_hub library
18
+ if HF_TOKEN:
19
+ HfFolder.save_token(HF_TOKEN)
20
+
21
  app = Flask(__name__)
22
  api = HfApi()
23
 
24
+ # --- Combined HTML, CSS, and JS Template ---
25
  TEMPLATE = """
26
  <!DOCTYPE html>
27
+ <html lang="en">
28
  <head>
29
  <meta charset="UTF-8">
 
30
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
31
+ <title>HuggingFace Drive - {{ path or 'Root' }}</title>
32
  <style>
33
+ :root {
34
+ --bg-color: #121212;
35
+ --surface-color: #1e1e1e;
36
+ --surface-hover: #2a2a2a;
37
+ --primary-text: #e0e0e0;
38
+ --secondary-text: #a0a0a0;
39
+ --accent-color: #8ab4f8;
40
+ --danger-color: #f28b82;
41
+ --border-color: #383838;
42
+ --shadow-color: rgba(0,0,0,0.5);
43
  }
 
44
  body {
45
+ background-color: var(--bg-color);
46
+ color: var(--primary-text);
47
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
48
+ margin: 0;
49
+ padding: 20px;
50
  }
 
51
  .container {
52
+ max-width: 900px;
53
  margin: 0 auto;
 
54
  }
55
+ h2 {
56
+ margin-top: 0;
57
+ font-weight: 500;
58
+ border-bottom: 1px solid var(--border-color);
59
+ padding-bottom: 10px;
 
 
 
 
60
  }
61
+ a {
62
+ color: var(--accent-color);
63
+ text-decoration: none;
64
+ font-weight: 500;
 
 
 
 
 
65
  }
66
+ a:hover { text-decoration: underline; }
67
+
68
+ /* Breadcrumbs */
69
+ .breadcrumbs {
 
70
  margin-bottom: 20px;
71
+ font-size: 1.1em;
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
+ .breadcrumbs a, .breadcrumbs span {
74
+ margin-right: 8px;
 
 
 
75
  }
76
+ .breadcrumbs span.separator {
77
+ color: var(--secondary-text);
 
 
78
  }
79
+
80
+ /* Main Actions */
 
 
 
 
81
  .actions {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  display: flex;
83
  align-items: center;
84
+ gap: 10px;
85
+ margin-bottom: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
+ .button, button, input[type="submit"] {
88
+ background-color: var(--surface-hover);
89
+ color: var(--primary-text);
90
+ border: 1px solid var(--border-color);
91
+ padding: 8px 14px;
92
+ border-radius: 6px;
 
93
  cursor: pointer;
94
+ font-size: 0.9em;
95
+ transition: background-color 0.2s ease, transform 0.1s ease;
96
+ }
97
+ .button:hover, button:hover, input[type="submit"]:hover { background-color: #333; }
98
+ .button:active, button:active, input[type="submit"]:active { transform: scale(0.98); }
99
+
100
+ input[type="file"] {
101
+ font-size: 0.9em;
102
+ }
103
+ input[type="file"]::file-selector-button {
104
+ background-color: var(--surface-hover);
105
+ color: var(--primary-text);
106
+ border: 1px solid var(--border-color);
107
+ padding: 8px 14px;
108
+ border-radius: 6px;
109
+ cursor: pointer;
110
+ transition: background-color 0.2s ease;
111
+ margin-right: 10px;
112
+ }
113
+ input[type="file"]::file-selector-button:hover { background-color: #333; }
114
+
115
+ /* Item List */
116
+ .item {
117
  display: flex;
118
  align-items: center;
119
+ padding: 12px;
120
+ margin: 4px 0;
121
+ background-color: var(--surface-color);
122
+ border-radius: 8px;
123
+ transition: background-color 0.2s ease;
124
  }
125
+ .item:hover { background-color: var(--surface-hover); }
126
+ .item-name {
127
+ flex-grow: 1;
128
+ cursor: pointer;
129
+ font-size: 1.1em;
130
+ }
131
+ .item-icon { margin-right: 12px; font-size: 1.2em; }
132
+
133
+ /* Kebab Menu */
134
+ .item-actions { position: relative; }
135
+ .kebab-button {
136
+ background: none;
137
+ border: none;
138
+ font-size: 1.5em;
139
+ line-height: 1;
140
+ padding: 4px 8px;
141
+ border-radius: 50%;
142
  }
143
+ .kebab-menu {
144
+ display: none;
145
  position: absolute;
 
146
  right: 0;
147
+ top: 100%;
148
+ background-color: #333;
149
+ border: 1px solid var(--border-color);
150
+ border-radius: 6px;
151
+ padding: 5px 0;
152
+ z-index: 10;
153
+ min-width: 120px;
154
+ box-shadow: 0 4px 12px var(--shadow-color);
155
+ }
156
+ .kebab-menu.show { display: block; }
157
+ .kebab-menu-item {
158
+ display: block;
159
+ background: none;
160
+ border: none;
161
+ color: var(--primary-text);
162
+ padding: 8px 16px;
163
+ width: 100%;
164
+ text-align: left;
 
 
 
 
 
 
 
 
165
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  }
167
+ .kebab-menu-item:hover { background-color: #444; }
168
+ .kebab-menu-item.danger { color: var(--danger-color); }
169
+
170
+ /* Modals */
171
+ .overlay {
172
  position: fixed;
173
+ top: 0; left: 0; right: 0; bottom: 0;
174
+ background-color: rgba(0,0,0,0.7);
175
+ z-index: 99;
 
 
 
 
 
 
 
176
  opacity: 0;
177
+ pointer-events: none;
178
+ transition: opacity 0.3s ease;
179
  }
180
+ .overlay.show {
 
181
  opacity: 1;
182
+ pointer-events: all;
183
  }
 
184
  .modal {
185
+ position: fixed;
186
+ top: 50%; left: 50%;
187
+ transform: translate(-50%, -50%) scale(0.95);
188
+ background-color: var(--surface-color);
189
+ padding: 24px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  border-radius: 12px;
191
+ z-index: 100;
192
+ width: 90%;
193
+ max-width: 400px;
194
+ box-shadow: 0 8px 24px var(--shadow-color);
195
+ opacity: 0;
196
+ pointer-events: none;
197
+ transition: opacity 0.3s ease, transform 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  }
199
+ .modal.show {
200
+ opacity: 1;
201
+ transform: translate(-50%, -50%) scale(1);
202
+ pointer-events: all;
203
+ }
204
+ .modal-header { font-size: 1.2em; margin-bottom: 16px; font-weight: 500; }
205
+ .modal-body { margin-bottom: 20px; }
206
+ .modal-body p { margin: 0 0 10px; }
207
+ .modal-body p strong { color: var(--accent-color); }
208
+ .modal-footer { display: flex; justify-content: flex-end; gap: 10px; }
209
+
210
+ .form-input {
211
+ width: 100%;
212
+ padding: 10px;
213
+ background: var(--bg-color);
214
+ border: 1px solid var(--border-color);
215
+ border-radius: 6px;
216
+ color: var(--primary-text);
217
+ font-size: 1em;
218
+ }
219
+ .danger-button { background-color: var(--danger-color); color: #000; }
220
+ .danger-button:hover { background-color: #e57373; }
221
+ </style>
222
+ </head>
223
+ <body>
224
 
225
+ <!-- Overlay and Modals -->
226
+ <div id="overlay" onclick="hideModals()"></div>
 
 
227
 
228
+ <div id="renameModal" class="modal">
229
+ <div class="modal-header">Rename Item</div>
230
+ <div class="modal-body">
231
+ <input type="text" id="renameInput" class="form-input" placeholder="New name">
232
+ </div>
233
+ <div class="modal-footer">
234
+ <button onclick="hideModals()">Cancel</button>
235
+ <button class="button" onclick="submitRename()">Rename</button>
236
+ </div>
237
+ </div>
238
+
239
+ <div id="deleteModal" class="modal">
240
+ <div class="modal-header">Confirm Deletion</div>
241
+ <div class="modal-body">
242
+ <p>Are you sure you want to delete this item?</p>
243
+ <p><strong id="deleteItemName"></strong></p>
244
+ <p>This action cannot be undone.</p>
245
+ </div>
246
+ <div class="modal-footer">
247
+ <button onclick="hideModals()">Cancel</button>
248
+ <button class="danger-button" onclick="submitDelete()">Delete</button>
249
+ </div>
250
+ </div>
251
 
252
+ <div id="createFolderModal" class="modal">
253
+ <div class="modal-header">Create New Folder</div>
254
+ <div class="modal-body">
255
+ <input type="text" id="folderNameInput" class="form-input" placeholder="Folder name">
256
+ </div>
257
+ <div class="modal-footer">
258
+ <button onclick="hideModals()">Cancel</button>
259
+ <button class="button" onclick="submitCreateFolder()">Create</button>
260
+ </div>
261
+ </div>
262
 
 
 
 
 
 
 
263
  <div class="container">
264
+ <h2>📁 HuggingFace Drive</h2>
265
+
266
+ <!-- Breadcrumbs -->
267
+ <nav class="breadcrumbs">
268
+ {% for crumb in breadcrumbs %}
269
+ {% if not loop.last %}
270
+ <a href="#" onclick="nav('{{ crumb.path }}')">{{ crumb.name }}</a>
271
+ <span class="separator">></span>
272
+ {% else %}
273
+ <span>{{ crumb.name }}</span>
 
 
 
 
 
 
 
 
274
  {% endif %}
275
+ {% endfor %}
276
+ </nav>
277
+
278
+ <!-- Actions -->
279
+ <div class="actions">
280
+ <form action="/upload" method="post" enctype="multipart/form-data" style="display: contents;">
281
+ <input type="file" name="file" onchange="this.form.submit()" title="Select a file to upload">
282
+ <input type="hidden" name="path" value="{{ path }}">
283
+ </form>
284
+ <button onclick="showCreateFolderModal('{{ path }}')">New Folder</button>
 
 
 
 
 
285
  </div>
286
+
287
+ <!-- Item List -->
288
+ <div class="item-list">
289
  {% for item in items %}
290
+ <div class="item">
291
+ {% if item.type=='dir' %}
292
+ <span class="item-icon">📂</span>
293
+ <div class="item-name" onclick="nav('{{ item.path }}')">{{ item.name }}</div>
294
+ {% else %}
295
+ <span class="item-icon">📄</span>
296
+ <div class="item-name">{{ item.name }}</div>
297
+ {% endif %}
298
+
299
+ <div class="item-actions">
300
+ <button class="kebab-button" onclick="toggleKebabMenu(event, this)">⋮</button>
301
+ <div class="kebab-menu">
302
+ {% if item.type!='dir' %}
303
+ <a class="kebab-menu-item" href="/download?path={{ item.path | urlencode }}">Download</a>
304
+ {% endif %}
305
+ <button class="kebab-menu-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">Rename</button>
306
+ <button class="kebab-menu-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">Delete</button>
 
 
 
 
 
 
 
307
  </div>
308
  </div>
309
  </div>
310
  {% endfor %}
311
+ {% if not items %}
312
+ <div style="color: var(--secondary-text); padding: 20px; text-align: center;">This folder is empty.</div>
313
+ {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  </div>
315
  </div>
316
 
317
  <script>
318
+ // --- Global State ---
319
+ let renameContext = { oldPath: '', oldName: '' };
320
+ let deleteContext = { path: '' };
321
+ let createFolderContext = { parentPath: '' };
322
 
323
+ // --- Navigation ---
324
+ function nav(path) {
325
+ window.location.href = '/?path=' + encodeURIComponent(path);
326
  }
327
 
328
+ // --- Kebab Menu Logic ---
329
+ function toggleKebabMenu(event, button) {
330
+ event.stopPropagation();
331
+ closeAllKebabMenus(button);
332
+ const menu = button.nextElementSibling;
333
+ menu.classList.toggle('show');
334
  }
335
 
336
+ function closeAllKebabMenus(exceptButton = null) {
337
+ document.querySelectorAll('.kebab-menu.show').forEach(openMenu => {
338
+ if (!exceptButton || !openMenu.previousElementSibling.isSameNode(exceptButton)) {
339
+ openMenu.classList.remove('show');
340
+ }
341
+ });
 
 
 
 
342
  }
343
 
344
+ document.addEventListener('click', () => closeAllKebabMenus());
 
 
 
 
 
 
345
 
346
+ // --- Modal Handling ---
347
+ const overlay = document.getElementById('overlay');
348
+ const modals = document.querySelectorAll('.modal');
349
+
350
+ function hideModals() {
351
+ overlay.classList.remove('show');
352
+ modals.forEach(m => m.classList.remove('show'));
353
  }
354
 
355
  function showModal(modalId) {
356
+ closeAllKebabMenus();
357
+ hideModals(); // Hide any other open modal first
358
+ document.getElementById(modalId).classList.add('show');
359
+ overlay.classList.add('show');
360
  }
361
 
362
+ // --- Create Folder ---
363
+ function showCreateFolderModal(parentPath) {
364
+ createFolderContext.parentPath = parentPath;
365
+ document.getElementById('folderNameInput').value = '';
366
+ showModal('createFolderModal');
367
+ document.getElementById('folderNameInput').focus();
368
  }
369
 
370
+ async function submitCreateFolder() {
371
+ const folderName = document.getElementById('folderNameInput').value.trim();
372
+ if (!folderName) return;
373
+
374
+ const fullPath = createFolderContext.parentPath ? `${createFolderContext.parentPath}/${folderName}` : folderName;
375
+
376
+ await fetch('/create_folder', {
377
+ method: 'POST',
378
+ headers: { 'Content-Type': 'application/json' },
379
+ body: JSON.stringify({ path: fullPath })
380
  });
381
+ location.reload();
382
  }
383
 
384
+ // --- Rename ---
385
+ function showRenameModal(oldPath, oldName) {
386
+ renameContext = { oldPath, oldName };
387
+ document.getElementById('renameInput').value = oldName;
388
+ showModal('renameModal');
389
+ document.getElementById('renameInput').focus();
390
+ }
391
+
392
+ async function submitRename() {
393
  const newName = document.getElementById('renameInput').value.trim();
394
+ if (!newName || newName === renameContext.oldName) {
395
+ hideModals();
396
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  }
398
+
399
+ const pathParts = renameContext.oldPath.split('/');
400
+ pathParts.pop(); // remove old name
401
+ const parentPath = pathParts.join('/');
402
+ const newPath = parentPath ? `${parentPath}/${newName}` : newName;
403
+
404
+ await fetch('/rename', {
405
+ method: 'POST',
406
+ headers: { 'Content-Type': 'application/json' },
407
+ body: JSON.stringify({ old_path: renameContext.oldPath, new_path: newPath })
408
+ });
409
+ location.reload();
410
  }
411
 
412
+ // --- Delete ---
413
+ function showDeleteModal(path, name) {
414
+ deleteContext.path = path;
415
+ document.getElementById('deleteItemName').textContent = name;
416
+ showModal('deleteModal');
 
 
 
 
 
 
 
 
 
 
417
  }
418
 
419
+ async function submitDelete() {
420
+ await fetch('/delete', {
421
+ method: 'POST',
422
+ headers: { 'Content-Type': 'application/json' },
423
+ body: JSON.stringify({ path: deleteContext.path })
424
+ });
425
+ location.reload();
 
 
 
 
 
 
 
 
 
 
 
426
  }
427
 
428
+ // Add keyboard shortcuts for modals
429
+ document.addEventListener('keydown', (e) => {
430
+ if (e.key === 'Escape') {
431
+ hideModals();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  }
 
433
  });
434
 
 
 
 
 
 
 
435
  </script>
436
  </body>
437
  </html>
438
  """
439
 
440
+ # --- Helper Functions ---
441
+
442
  def list_folder(path=""):
443
+ """Lists files and directories in a given path of the repository."""
444
+ try:
445
+ # Normalize path to not have leading/trailing slashes, and add a trailing slash if not root
446
+ prefix = path.strip("/") + ("/" if path else "")
447
+ all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset")
448
+
449
+ seen_dirs = set()
450
+ items = []
451
+
452
+ for f in all_files:
453
+ if not f.startswith(prefix):
454
+ continue
455
+
456
+ # Get the part of the path relative to the current directory
457
+ relative_path = f[len(prefix):]
458
+
459
+ if "/" in relative_path:
460
+ # It's in a subdirectory
461
+ dir_name = relative_path.split("/")[0]
462
+ dir_path = (prefix + dir_name).strip("/")
463
+ if dir_path not in seen_dirs:
464
+ seen_dirs.add(dir_path)
465
+ items.append({"type": "dir", "name": dir_name, "path": dir_path})
466
+ else:
467
+ # It's a file in the current directory
468
+ if relative_path: # Exclude the .keep file placeholder if it's the only thing
469
+ items.append({"type": "file", "name": relative_path, "path": f})
470
+
471
+ # Sort directories first, then files, all alphabetically
472
+ items.sort(key=lambda x: (x["type"] != "dir", x["name"].lower()))
473
+ return items
474
+ except Exception as e:
475
+ print(f"Error listing folder: {e}")
476
+ return []
477
+
478
+ # --- Flask Routes ---
479
 
480
  @app.route("/", methods=["GET"])
481
  def index():
482
+ if not REPO_ID or not HF_TOKEN:
483
+ return "<h1>Configuration Error</h1><p>Please set REPO_ID and HF_TOKEN environment variables.</p>", 500
484
+
485
+ path = unquote(request.args.get("path", "")).strip("/")
486
+
487
+ # Build breadcrumbs
488
+ breadcrumbs = [{"name": "Root", "path": ""}]
489
+ if path:
490
+ current_path = ""
491
+ for part in path.split("/"):
492
+ current_path = f"{current_path}/{part}".strip("/")
493
+ breadcrumbs.append({"name": part, "path": current_path})
494
+
495
+ items = list_folder(path)
496
+ return render_template_string(TEMPLATE, items=items, path=path, breadcrumbs=breadcrumbs)
497
 
498
  @app.route("/download", methods=["GET"])
499
  def download():
500
+ path = request.args.get("path", "")
501
+ if not path:
502
+ return "File path is required.", 400
503
+
504
+ try:
505
+ local_file = hf_hub_download(
506
+ repo_id=REPO_ID,
507
+ filename=path,
508
+ repo_type="dataset",
509
+ cache_dir=tempfile.gettempdir()
510
+ )
511
+ return send_file(local_file, as_attachment=True, download_name=os.path.basename(path))
512
+ except Exception as e:
513
+ return f"Error downloading file: {e}", 500
514
 
515
  @app.route("/upload", methods=["POST"])
516
  def upload():
517
+ if 'file' not in request.files:
518
+ return redirect(request.referrer)
519
+
520
  file = request.files["file"]
521
+ if file.filename == '':
522
+ return redirect(request.referrer)
523
+
524
+ path_in_repo = request.form.get("path", "").strip("/")
525
+ destination_path = f"{path_in_repo}/{file.filename}".strip("/")
526
+
527
+ # Use a temporary file to handle the upload
528
+ with tempfile.NamedTemporaryFile(delete=False) as tmp:
529
+ file.save(tmp.name)
530
+ tmp_path = tmp.name
531
+
532
+ try:
533
+ upload_file(
534
+ path_or_fileobj=tmp_path,
535
+ path_in_repo=destination_path,
536
+ repo_id=REPO_ID,
537
+ repo_type="dataset"
538
+ )
539
+ finally:
540
+ os.remove(tmp_path)
541
+
542
+ return redirect(f"/?path={path_in_repo}")
543
 
544
  @app.route("/delete", methods=["POST"])
545
  def delete():
546
+ path_to_delete = request.get_json()["path"].strip("/")
547
+ all_repo_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset")
548
+
549
+ # This will be a list of operations for a single commit
550
+ operations = []
551
+ is_folder = not any(f['path'] == path_to_delete for f in list_folder('/'.join(path_to_delete.split('/')[:-1])))
552
+
553
+ if is_folder: # It's a directory
554
+ files_to_delete = [f for f in all_repo_files if f == path_to_delete or f.startswith(path_to_delete + "/")]
555
+ for f in files_to_delete:
556
+ delete_file(repo_id=REPO_ID, path_in_repo=f, repo_type="dataset")
557
+ else: # It's a single file
558
+ delete_file(repo_id=REPO_ID, path_in_repo=path_to_delete, repo_type="dataset")
559
+
560
  return jsonify(status="ok")
561
 
562
  @app.route("/create_folder", methods=["POST"])
563
  def create_folder():
564
+ folder_path = request.get_json()["path"].strip("/")
565
+ # Create a .keep file to make the directory exist in the repo
566
+ keep_file_path = f"{folder_path}/.gitkeep"
567
+
568
+ with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
569
+ tmp.write("") # empty file
570
+ tmp_path = tmp.name
571
+
572
+ try:
573
+ upload_file(
574
+ path_or_fileobj=tmp_path,
575
+ path_in_repo=keep_file_path,
576
+ repo_id=REPO_ID,
577
+ repo_type="dataset"
578
+ )
579
+ finally:
580
+ os.remove(tmp_path)
581
+
582
  return jsonify(status="ok")
583
 
584
  @app.route("/rename", methods=["POST"])
585
  def rename():
586
  data = request.get_json()
587
+ old_path = data["old_path"].strip("/")
588
+ new_path = data["new_path"].strip("/")
589
+
590
+ all_repo_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset")
591
+
592
+ is_folder = not any(f == old_path for f in all_repo_files)
593
+
594
+ files_to_move = []
595
+ if is_folder:
596
+ files_to_move = [f for f in all_repo_files if f.startswith(old_path + "/")]
597
+ if not files_to_move: # Empty folder, might have a .gitkeep
598
+ try:
599
+ # Check if a .gitkeep file exists and move it
600
+ keep_file = f"{old_path}/.gitkeep"
601
+ hf_hub_download(repo_id=REPO_ID, filename=keep_file, repo_type="dataset", token=HF_TOKEN, cache_dir=tempfile.gettempdir())
602
+ files_to_move.append(keep_file)
603
+ except Exception: # Not found, it's ok
604
+ pass
605
+ else:
606
+ files_to_move.append(old_path)
607
+
608
+ for f_path in files_to_move:
609
+ # Download file to a temporary location
610
+ local_file = hf_hub_download(repo_id=REPO_ID, filename=f_path, repo_type="dataset", cache_dir=tempfile.gettempdir())
611
+
612
+ # Calculate new path in repo
613
+ relative_path = os.path.relpath(f_path, old_path) if is_folder else ''
614
+ new_file_path = os.path.join(new_path, relative_path).replace("\\", "/")
615
+
616
+ # Upload to new location
617
+ upload_file(path_or_fileobj=local_file, path_in_repo=new_file_path, repo_id=REPO_ID, repo_type="dataset")
618
+
619
+ # Delete old file
620
+ delete_file(repo_id=REPO_ID, path_in_repo=f_path, repo_type="dataset")
621
+
622
  return jsonify(status="ok")
623
 
624
+
625
  if __name__ == "__main__":
626
+ if not REPO_ID or not HF_TOKEN:
627
+ print("FATAL: REPO_ID and HF_TOKEN environment variables must be set.")
628
+ else:
629
+ print(f"✅ Server starting for repository: {REPO_ID}")
630
+ print("✅ Access the UI at http://127.0.0.1:7860")
631
+ app.run(debug=False, host="0.0.0.0", port=7860)