testdeep123 commited on
Commit
97b70cf
·
verified ·
1 Parent(s): eee6269

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +314 -81
app.py CHANGED
@@ -1,95 +1,185 @@
1
  import os
2
  import tempfile
3
  import requests
4
- from flask import Flask, request, send_file, redirect, url_for, render_template_string
5
- from huggingface_hub import HfApi, upload_file, hf_hub_url
 
 
6
 
 
 
7
  REPO_ID = os.getenv("REPO_ID")
8
  TOKEN = os.getenv("HF_TOKEN")
 
 
 
9
 
 
 
 
 
 
10
  app = Flask(__name__)
 
11
  api = HfApi()
12
 
 
13
  HTML_TEMPLATE = """
14
  <!DOCTYPE html>
15
  <html lang="en">
16
  <head>
17
  <meta charset="UTF-8">
 
18
  <title>{{ repo_id }} - Drive</title>
19
  <style>
 
 
 
 
 
 
20
  body {
21
- font-family: 'Segoe UI', sans-serif;
22
- background-color: #121212;
23
- color: #eee;
24
  margin: 0;
 
25
  }
26
  header {
27
- background-color: #1f1f1f;
28
- padding: 16px;
29
- font-size: 20px;
30
  font-weight: bold;
31
- color: #fff;
 
32
  }
33
- .container {
34
- padding: 20px;
 
 
 
 
 
 
35
  }
36
- .folder, .file {
37
- margin: 4px 0;
 
 
 
 
 
 
38
  }
39
- .folder {
40
- color: #8ab4f8;
41
  cursor: pointer;
42
  user-select: none;
 
43
  }
44
- .file a {
45
- color: #9cdcfe;
46
- text-decoration: none;
47
- margin-left: 10px;
 
 
 
48
  }
49
- .file a:hover {
50
- text-decoration: underline;
 
 
 
 
 
51
  }
52
- .upload-box {
 
 
 
 
53
  margin-top: 30px;
54
- background: #222;
55
- padding: 20px;
56
- border-radius: 6px;
 
57
  }
58
- input[type="file"], input[type="text"], input[type="submit"] {
59
- padding: 8px;
 
60
  margin-top: 10px;
61
  font-size: 14px;
62
  background: #333;
63
- color: #fff;
64
- border: none;
65
  border-radius: 4px;
 
66
  }
67
- input[type="submit"] {
68
- background: #4caf50;
 
69
  cursor: pointer;
 
 
 
70
  }
71
- .indent {
72
- margin-left: 20px;
73
- }
74
  </style>
75
  <script>
76
  function toggle(id) {
77
- let el = document.getElementById(id);
78
- el.style.display = (el.style.display === 'none') ? 'block' : 'none';
 
 
 
 
 
 
 
 
 
79
  }
80
  </script>
81
  </head>
82
  <body>
83
  <header>📁 Hugging Face Drive — {{ repo_id }}</header>
84
  <div class="container">
 
 
 
 
 
 
 
 
 
 
85
  {{ folder_html|safe }}
86
 
87
- <div class="upload-box">
88
  <h3>Upload File</h3>
89
- <form method="POST" action="/upload" enctype="multipart/form-data">
90
- Folder path (optional): <input type="text" name="folder" placeholder="e.g. world_the_end/DIM1/data"><br>
91
- File: <input type="file" name="file"><br>
92
- <input type="submit" value="Upload">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  </form>
94
  </div>
95
  </div>
@@ -97,70 +187,213 @@ HTML_TEMPLATE = """
97
  </html>
98
  """
99
 
100
- def build_tree(paths):
 
 
 
101
  tree = {}
102
- for path in paths:
103
- parts = path.strip("/").split("/")
104
- current = tree
105
  for part in parts[:-1]:
106
- current = current.setdefault(part, {})
107
- current.setdefault("_files", []).append(parts[-1])
 
 
 
 
 
 
 
 
108
  return tree
109
 
110
- def render_tree(tree, prefix="", depth=0, idx=0):
 
 
 
 
 
 
 
 
 
 
 
111
  html = ""
112
- for name, content in tree.items():
 
 
 
 
113
  if name == "_files":
114
- for file in content:
115
- full_path = f"{prefix}/{file}".strip("/")
116
- html += f'<div class="file indent" style="margin-left:{depth * 20}px;">📄 {file} <a href="/download/{full_path}">Download</a></div>'
117
- else:
118
- folder_id = f"f{idx}"
119
- html += f'<div class="folder" onclick="toggle(\'{folder_id}\')" style="margin-left:{depth * 20}px;">📂 {name}</div>'
120
- html += f'<div id="{folder_id}" style="display:none;">'
121
- html += render_tree(content, f"{prefix}/{name}".strip("/"), depth + 1, idx + 1)
122
- html += '</div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  return html
124
 
 
 
125
  @app.route("/")
126
  def index():
127
- files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=TOKEN)
128
- tree = build_tree(files)
129
- folder_html = render_tree(tree)
 
 
 
 
 
 
 
 
 
 
 
130
  return render_template_string(HTML_TEMPLATE, repo_id=REPO_ID, folder_html=folder_html)
131
 
132
- @app.route("/download/<path:filename>")
133
- def download_file(filename):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  try:
135
- url = hf_hub_url(REPO_ID, filename, repo_type="dataset")
136
- headers = {"Authorization": f"Bearer {TOKEN}"}
137
- response = requests.get(url, headers=headers)
138
- if response.status_code != 200:
139
- return f"Download failed: {response.status_code}", 500
140
- tmp = tempfile.NamedTemporaryFile(delete=False)
141
- tmp.write(response.content)
142
- tmp.close()
143
- return send_file(tmp.name, as_attachment=True, download_name=os.path.basename(filename))
 
144
  except Exception as e:
145
- return str(e), 500
 
146
 
147
  @app.route("/upload", methods=["POST"])
148
- def upload():
 
 
 
 
149
  file = request.files["file"]
150
  folder = request.form.get("folder", "").strip().strip("/")
151
- if file:
152
- filename = file.filename
153
- path_in_repo = f"{folder}/{filename}" if folder else filename
154
- temp_path = os.path.join(tempfile.gettempdir(), filename)
155
- file.save(temp_path)
 
 
 
 
 
156
  upload_file(
157
  path_or_fileobj=temp_path,
158
  path_in_repo=path_in_repo,
159
  repo_id=REPO_ID,
160
  repo_type="dataset",
161
- token=TOKEN
 
162
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  return redirect(url_for("index"))
164
 
165
  if __name__ == "__main__":
166
- app.run(debug=True, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
1
  import os
2
  import tempfile
3
  import requests
4
+ from datetime import datetime
5
+ from flask import Flask, request, send_file, redirect, url_for, render_template_string, flash, Response
6
+ from huggingface_hub import HfApi, HfFolder, hf_hub_download, delete_file, upload_file
7
+ from huggingface_hub.utils import HfHubHTTPError
8
 
9
+ # --- CONFIGURATION ---
10
+ # Load from environment variables
11
  REPO_ID = os.getenv("REPO_ID")
12
  TOKEN = os.getenv("HF_TOKEN")
13
+ # A secret key is required for Flask flashing system (user feedback)
14
+ # You can set this as an environment variable or generate a random one
15
+ APP_SECRET_KEY = os.getenv("APP_SECRET_KEY", "your-default-secret-key")
16
 
17
+ # --- VALIDATION ---
18
+ if not REPO_ID or not TOKEN:
19
+ raise ValueError("Please set the REPO_ID and HF_TOKEN environment variables.")
20
+
21
+ # --- INITIALIZATION ---
22
  app = Flask(__name__)
23
+ app.secret_key = APP_SECRET_KEY
24
  api = HfApi()
25
 
26
+ # --- HTML & CSS TEMPLATE ---
27
  HTML_TEMPLATE = """
28
  <!DOCTYPE html>
29
  <html lang="en">
30
  <head>
31
  <meta charset="UTF-8">
32
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
33
  <title>{{ repo_id }} - Drive</title>
34
  <style>
35
+ :root {
36
+ --bg-color: #121212; --text-color: #e0e0e0; --primary-color: #8ab4f8;
37
+ --secondary-color: #9cdcfe; --header-bg: #1f1f1f; --box-bg: #2a2a2a;
38
+ --border-color: #444; --success-bg: #2e7d32; --error-bg: #c62828;
39
+ --delete-color: #e57373; --delete-hover: #d32f2f;
40
+ }
41
  body {
42
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
43
+ background-color: var(--bg-color);
44
+ color: var(--text-color);
45
  margin: 0;
46
+ line-height: 1.6;
47
  }
48
  header {
49
+ background-color: var(--header-bg);
50
+ padding: 1rem 1.5rem;
51
+ font-size: 1.5em;
52
  font-weight: bold;
53
+ color: var(--text-color);
54
+ border-bottom: 1px solid var(--border-color);
55
  }
56
+ .container { padding: 1.5rem; }
57
+ .flash-messages { list-style: none; padding: 0; margin-bottom: 1rem; }
58
+ .flash-messages li {
59
+ padding: 0.8rem 1.2rem;
60
+ margin-bottom: 0.5rem;
61
+ border-radius: 4px;
62
+ color: white;
63
+ font-weight: 500;
64
  }
65
+ .flash-messages .success { background-color: var(--success-bg); }
66
+ .flash-messages .error { background-color: var(--error-bg); }
67
+ .tree-item {
68
+ display: flex;
69
+ align-items: center;
70
+ padding: 4px 8px;
71
+ border-radius: 4px;
72
+ transition: background-color 0.2s ease;
73
  }
74
+ .tree-item:hover { background-color: var(--box-bg); }
75
+ .folder-name, .file-name {
76
  cursor: pointer;
77
  user-select: none;
78
+ flex-grow: 1;
79
  }
80
+ .folder-name { color: var(--primary-color); font-weight: 500; }
81
+ .file-info {
82
+ font-size: 0.8em;
83
+ color: #999;
84
+ margin-left: auto;
85
+ padding-left: 1rem;
86
+ white-space: nowrap;
87
  }
88
+ .file-info span { margin-left: 1rem; }
89
+ .actions a {
90
+ color: var(--secondary-color);
91
+ text-decoration: none;
92
+ margin-left: 1rem;
93
+ font-size: 0.85em;
94
+ font-weight: 500;
95
  }
96
+ .actions a:hover { text-decoration: underline; }
97
+ .actions .delete-link { color: var(--delete-color); }
98
+ .actions .delete-link:hover { color: var(--delete-hover); }
99
+ .indent { padding-left: 24px; }
100
+ .form-box {
101
  margin-top: 30px;
102
+ background: var(--box-bg);
103
+ padding: 1.5rem;
104
+ border-radius: 8px;
105
+ border: 1px solid var(--border-color);
106
  }
107
+ .form-box h3 { margin-top: 0; }
108
+ input[type="file"], input[type="text"], input[type="submit"], button {
109
+ padding: 10px 14px;
110
  margin-top: 10px;
111
  font-size: 14px;
112
  background: #333;
113
+ color: var(--text-color);
114
+ border: 1px solid var(--border-color);
115
  border-radius: 4px;
116
+ width: calc(100% - 30px);
117
  }
118
+ input[type="submit"], button {
119
+ background: var(--primary-color);
120
+ color: var(--bg-color);
121
  cursor: pointer;
122
+ font-weight: bold;
123
+ width: auto;
124
+ margin-right: 10px;
125
  }
126
+ .form-group { margin-bottom: 1rem; }
127
+ label { display: block; margin-bottom: 5px; font-weight: 500; }
 
128
  </style>
129
  <script>
130
  function toggle(id) {
131
+ const el = document.getElementById(id);
132
+ if (el) el.style.display = (el.style.display === 'none') ? 'block' : 'none';
133
+ }
134
+ function setUploadFolder(folderPath) {
135
+ document.getElementById('upload-folder-input').value = folderPath;
136
+ document.getElementById('upload-form').scrollIntoView({ behavior: 'smooth' });
137
+ }
138
+ function confirmDelete(event, path) {
139
+ if (!confirm('Are you sure you want to delete "' + path + '"? This action cannot be undone.')) {
140
+ event.preventDefault();
141
+ }
142
  }
143
  </script>
144
  </head>
145
  <body>
146
  <header>📁 Hugging Face Drive — {{ repo_id }}</header>
147
  <div class="container">
148
+ {% with messages = get_flashed_messages(with_categories=true) %}
149
+ {% if messages %}
150
+ <ul class="flash-messages">
151
+ {% for category, message in messages %}
152
+ <li class="{{ category }}">{{ message }}</li>
153
+ {% endfor %}
154
+ </ul>
155
+ {% endif %}
156
+ {% endwith %}
157
+
158
  {{ folder_html|safe }}
159
 
160
+ <div id="upload-form" class="form-box">
161
  <h3>Upload File</h3>
162
+ <form method="POST" action="{{ url_for('upload_file_route') }}" enctype="multipart/form-data">
163
+ <div class="form-group">
164
+ <label for="upload-folder-input">Folder path (optional)</label>
165
+ <input type="text" id="upload-folder-input" name="folder" placeholder="e.g. data/images">
166
+ </div>
167
+ <div class="form-group">
168
+ <label for="file-input">File</label>
169
+ <input type="file" id="file-input" name="file" required>
170
+ </div>
171
+ <input type="submit" value="Upload File">
172
+ </form>
173
+ </div>
174
+
175
+ <div class="form-box">
176
+ <h3>Create New Folder</h3>
177
+ <form method="POST" action="{{ url_for('create_folder_route') }}">
178
+ <div class="form-group">
179
+ <label for="create-folder-input">New folder path</label>
180
+ <input type="text" id="create-folder-input" name="folder_path" placeholder="e.g. new_folder/sub_folder" required>
181
+ </div>
182
+ <input type="submit" value="Create Folder">
183
  </form>
184
  </div>
185
  </div>
 
187
  </html>
188
  """
189
 
190
+ # --- HELPER FUNCTIONS ---
191
+
192
+ def build_tree(repo_files):
193
+ """Builds a hierarchical dictionary from a flat list of file paths."""
194
  tree = {}
195
+ for repo_file in repo_files:
196
+ parts = repo_file.path.split('/')
197
+ current_level = tree
198
  for part in parts[:-1]:
199
+ # setdefault returns the value for the key if it exists,
200
+ # otherwise it inserts the key with a default value and returns it.
201
+ if part not in current_level:
202
+ current_level[part] = {}
203
+ current_level = current_level[part]
204
+
205
+ # Store the full RepoFile object at the leaf
206
+ if "_files" not in current_level:
207
+ current_level["_files"] = []
208
+ current_level["_files"].append(repo_file)
209
  return tree
210
 
211
+ def format_size(size_bytes):
212
+ """Converts bytes to a human-readable format."""
213
+ if size_bytes == 0:
214
+ return "0B"
215
+ size_name = ("B", "KB", "MB", "GB", "TB")
216
+ i = int(size_bytes.bit_length() / 10)
217
+ p = 1024 ** i
218
+ s = round(size_bytes / p, 2)
219
+ return f"{s} {size_name[i]}"
220
+
221
+ def render_tree(tree, current_path="", depth=0):
222
+ """Recursively renders the file tree into HTML."""
223
  html = ""
224
+ # Sort items: folders first, then files, both alphabetically
225
+ sorted_items = sorted(tree.items(), key=lambda x: (x[0] != "_files", x[0].lower()))
226
+
227
+ for name, content in sorted_items:
228
+ full_path = f"{current_path}/{name}".strip("/")
229
  if name == "_files":
230
+ # Sort files alphabetically
231
+ sorted_files = sorted(content, key=lambda f: f.path.lower())
232
+ for file in sorted_files:
233
+ filename = os.path.basename(file.path)
234
+ file_size = format_size(file.size) if file.size is not None else "N/A"
235
+ last_modified = file.last_modified.strftime('%Y-%m-%d %H:%M') if file.last_modified else "N/A"
236
+
237
+ html += f"""
238
+ <div class="tree-item" style="padding-left: {depth * 24 + 8}px;">
239
+ <div class="file-name">📄 {filename}</div>
240
+ <div class="file-info">
241
+ <span>{file_size}</span>
242
+ <span>{last_modified}</span>
243
+ </div>
244
+ <div class="actions">
245
+ <a href="{url_for('download_file_route', path_in_repo=file.path)}">Download</a>
246
+ <a href="{url_for('delete_file_route', path_in_repo=file.path)}"
247
+ class="delete-link"
248
+ onclick="confirmDelete(event, '{file.path}')">Delete</a>
249
+ </div>
250
+ </div>
251
+ """
252
+ else: # It's a folder
253
+ # Sanitize path for HTML id attribute
254
+ folder_id = f"folder-{'--'.join(full_path.split('/'))}"
255
+ html += f"""
256
+ <div class="tree-item" style="padding-left: {depth * 24 + 8}px;">
257
+ <div class="folder-name" onclick="toggle('{folder_id}')">📂 {name}</div>
258
+ <div class="actions">
259
+ <a href="#" onclick="setUploadFolder('{full_path}'); return false;">Upload here</a>
260
+ </div>
261
+ </div>
262
+ <div id="{folder_id}" class="indent" style="display:none;">
263
+ {render_tree(content, full_path, depth + 1)}
264
+ </div>
265
+ """
266
  return html
267
 
268
+ # --- FLASK ROUTES ---
269
+
270
  @app.route("/")
271
  def index():
272
+ try:
273
+ # Use list_repo_tree to get detailed file info (size, last_modified)
274
+ repo_files = api.list_repo_tree(repo_id=REPO_ID, repo_type="dataset", token=TOKEN, recursive=True)
275
+ tree = build_tree(repo_files)
276
+ folder_html = render_tree(tree)
277
+ if not repo_files:
278
+ flash("This repository is empty. Upload a file or create a folder to get started.", "success")
279
+ except HfHubHTTPError as e:
280
+ flash(f"Error fetching repository files: {e}", "error")
281
+ folder_html = "<p>Could not load repository data. Please check your REPO_ID and TOKEN.</p>"
282
+ except Exception as e:
283
+ flash(f"An unexpected error occurred: {e}", "error")
284
+ folder_html = "<p>An unexpected error occurred.</p>"
285
+
286
  return render_template_string(HTML_TEMPLATE, repo_id=REPO_ID, folder_html=folder_html)
287
 
288
+ @app.route("/download/<path:path_in_repo>")
289
+ def download_file_route(path_in_repo):
290
+ try:
291
+ # hf_hub_download is the recommended way, it handles caching etc.
292
+ local_path = hf_hub_download(
293
+ repo_id=REPO_ID,
294
+ filename=path_in_repo,
295
+ repo_type="dataset",
296
+ token=TOKEN
297
+ )
298
+ return send_file(local_path, as_attachment=True, download_name=os.path.basename(path_in_repo))
299
+ except HfHubHTTPError as e:
300
+ flash(f"Download failed for '{path_in_repo}': {e}", "error")
301
+ return redirect(url_for('index'))
302
+ except Exception as e:
303
+ flash(f"An error occurred during download: {e}", "error")
304
+ return redirect(url_for('index'))
305
+
306
+ @app.route("/delete/<path:path_in_repo>")
307
+ def delete_file_route(path_in_repo):
308
+ # Note: For strict REST, a destructive action should be a DELETE request.
309
+ # For simplicity in this single-page app, GET with confirmation is used.
310
  try:
311
+ delete_file(
312
+ path_in_repo=path_in_repo,
313
+ repo_id=REPO_ID,
314
+ repo_type="dataset",
315
+ token=TOKEN,
316
+ commit_message=f"Delete file: {path_in_repo}"
317
+ )
318
+ flash(f'Successfully deleted "{path_in_repo}".', 'success')
319
+ except HfHubHTTPError as e:
320
+ flash(f'Error deleting "{path_in_repo}": {e}', 'error')
321
  except Exception as e:
322
+ flash(f'An unexpected error occurred: {e}', 'error')
323
+ return redirect(url_for('index'))
324
 
325
  @app.route("/upload", methods=["POST"])
326
+ def upload_file_route():
327
+ if 'file' not in request.files or not request.files['file'].filename:
328
+ flash("No file selected for upload.", "error")
329
+ return redirect(url_for('index'))
330
+
331
  file = request.files["file"]
332
  folder = request.form.get("folder", "").strip().strip("/")
333
+ filename = file.filename
334
+ path_in_repo = f"{folder}/{filename}" if folder else filename
335
+
336
+ # Use a temporary file to safely handle the upload
337
+ temp_path = None
338
+ try:
339
+ with tempfile.NamedTemporaryFile(delete=False) as tmp:
340
+ file.save(tmp.name)
341
+ temp_path = tmp.name
342
+
343
  upload_file(
344
  path_or_fileobj=temp_path,
345
  path_in_repo=path_in_repo,
346
  repo_id=REPO_ID,
347
  repo_type="dataset",
348
+ token=TOKEN,
349
+ commit_message=f"Upload file: {path_in_repo}"
350
  )
351
+ flash(f'Successfully uploaded "{filename}" to "{folder if folder else "root"}".', 'success')
352
+ except Exception as e:
353
+ flash(f"Upload failed: {e}", "error")
354
+ finally:
355
+ # Ensure the temporary file is deleted
356
+ if temp_path and os.path.exists(temp_path):
357
+ os.remove(temp_path)
358
+
359
+ return redirect(url_for("index"))
360
+
361
+ @app.route("/create_folder", methods=["POST"])
362
+ def create_folder_route():
363
+ folder_path = request.form.get("folder_path", "").strip().strip("/")
364
+ if not folder_path:
365
+ flash("Folder path cannot be empty.", "error")
366
+ return redirect(url_for('index'))
367
+
368
+ # To create a folder, we upload a placeholder file into it.
369
+ path_in_repo = f"{folder_path}/.gitkeep"
370
+
371
+ # Use an in-memory object to avoid creating a temp file
372
+ from io import BytesIO
373
+ dummy_file = BytesIO(b"") # Empty file content
374
+
375
+ try:
376
+ upload_file(
377
+ path_or_fileobj=dummy_file,
378
+ path_in_repo=path_in_repo,
379
+ repo_id=REPO_ID,
380
+ repo_type="dataset",
381
+ token=TOKEN,
382
+ commit_message=f"Create folder: {folder_path}"
383
+ )
384
+ flash(f'Successfully created folder "{folder_path}".', 'success')
385
+ except Exception as e:
386
+ flash(f"Failed to create folder: {e}", "error")
387
+
388
  return redirect(url_for("index"))
389
 
390
  if __name__ == "__main__":
391
+ print("--- Hugging Face Drive ---")
392
+ print(f"Serving repository: {REPO_ID}")
393
+ print("Ensure you have set the following environment variables:")
394
+ print(" - REPO_ID")
395
+ print(" - HF_TOKEN")
396
+ print(" - APP_SECRET_KEY (optional, but recommended for security)")
397
+ print("--------------------------")
398
+ # Setting debug=False is recommended for any non-local deployment
399
+ app.run(host="0.0.0.0", port=7860, debug=True)