testdeep123 commited on
Commit
62c5efa
·
verified ·
1 Parent(s): a6ae3e4

Update app.py

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