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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +620 -506
app.py CHANGED
@@ -1,631 +1,745 @@
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)
 
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)