testdeep123 commited on
Commit
4709525
·
verified ·
1 Parent(s): 5e61cf8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +826 -136
app.py CHANGED
@@ -10,149 +10,826 @@ HF_TOKEN = os.getenv("HF_TOKEN")
10
  app = Flask(__name__)
11
  api = HfApi()
12
 
13
- # HTML template with responsive layout and custom modals
14
  TEMPLATE = """
15
  <!DOCTYPE html>
16
- <html lang="en">
17
  <head>
18
  <meta charset="UTF-8">
19
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
  <title>HuggingFace Drive - {{ path or 'Root' }}</title>
 
21
  <style>
22
- /* Global Reset */
23
- * { margin: 0; padding: 0; box-sizing: border-box; }
24
- body { background: #1a1a2e; color: #e2e8f0; font-family: sans-serif; overflow-x: hidden; }
25
- .container { max-width: 1200px; margin: auto; padding: 20px; }
26
- .header { background: rgba(255,255,255,0.05); padding: 20px; border-radius: 12px; margin-bottom: 20px; }
27
- .title { font-size: 2rem; background: linear-gradient(135deg,#667eea,#764ba2); -webkit-background-clip: text; color: transparent; }
28
- .breadcrumb { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
29
- .breadcrumb-item { background: rgba(255,255,255,0.1); padding: 6px 12px; border-radius: 16px; font-size: 0.9rem; cursor: pointer; }
30
- .breadcrumb-item.active { background: linear-gradient(135deg,#667eea,#764ba2); color: #fff; }
31
- .actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
32
- .btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 0.9rem; }
33
- .btn-primary { background: linear-gradient(135deg,#667eea,#764ba2); color: #fff; }
34
- .btn-secondary { background: rgba(255,255,255,0.1); color: #e2e8f0; }
35
- .file-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(200px,1fr)); gap: 16px; }
36
- .file-item { background: rgba(255,255,255,0.05); padding: 16px; border-radius: 12px; position: relative; overflow: hidden; }
37
- .file-info { display: flex; align-items: center; gap: 8px; cursor: pointer; }
38
- .file-icon { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; background: rgba(255,255,255,0.1); }
39
- .dropdown { position: absolute; top: 10px; right: 10px; }
40
- .dropdown-menu { display: none; position: absolute; top: 100%; right: 0; background: rgba(15,15,35,0.95); border-radius: 8px; padding: 8px; margin-top: 4px; }
41
- .dropdown.active .dropdown-menu { display: block; }
42
- .dropdown-item { padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 0.9rem; }
43
- .dropdown-item:hover { background: rgba(255,255,255,0.1); }
44
- .dropdown-item.danger { color: #f87171; }
45
- /* Modals */
46
- .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; }
47
- .modal-overlay.active { display: flex; }
48
- .modal { background: rgba(15,15,35,0.95); padding: 24px; border-radius: 12px; width: 90%; max-width: 400px; }
49
- .modal h3 { margin-bottom: 12px; font-size: 1.2rem; }
50
- .modal input { width: 100%; padding: 10px; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; margin-bottom: 16px; background: rgba(255,255,255,0.1); color: #e2e8f0; }
51
- .modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
52
- @media(max-width:768px) { .file-grid { grid-template-columns: 1fr; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </style>
54
  </head>
55
  <body>
56
  <div class="container">
57
  <div class="header">
58
- <div class="title">HuggingFace Drive</div>
 
 
59
  <div class="breadcrumb">
60
- <div class="breadcrumb-item" onclick="nav('')">Home</div>
 
 
61
  {% if path %}
62
- {% set parts=path.split('/') %}
63
  {% for i in range(parts|length) %}
64
- <div class="breadcrumb-item{% if parts[:i+1]|join('/')==path %} active{% endif %}" onclick="nav('{{ parts[:i+1]|join('/') }}')">{{ parts[i] }}</div>
 
 
 
 
 
65
  {% endfor %}
66
  {% endif %}
67
  </div>
 
 
68
  <div class="actions">
69
- <form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
70
- <input type="file" name="file" id="fileInput" style="display:none;" onchange="document.getElementById('uploadForm').submit()">
71
  <input type="hidden" name="path" value="{{ path }}">
72
- <button type="button" class="btn btn-primary" onclick="document.getElementById('fileInput').click()">Upload File</button>
 
 
73
  </form>
74
- <button class="btn btn-secondary" onclick="openCreateModal()">New Folder</button>
 
 
75
  </div>
76
  </div>
 
 
77
  <div class="file-grid">
78
  {% for item in items %}
79
- <div class="file-item">
80
- <div class="file-info" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% endif %}">
81
- <div class="file-icon">{{ '📁' if item.type=='dir' else '📄' }}</div>
82
- <div>{{ item.name }}</div>
83
- </div>
84
- <div class="dropdown">
85
- <button onclick="toggleDropdown(this)">⋮</button>
86
- <div class="dropdown-menu">
87
- {% if item.type=='file' %}<div class="dropdown-item" onclick="download('{{ item.path }}')">Download</div>{% endif %}
88
- <div class="dropdown-item" onclick="openRenameModal('{{ item.path }}','{{ item.name }}')">Rename</div>
89
- <div class="dropdown-item danger" onclick="openDeleteModal('{{ item.path }}')">Delete</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  </div>
91
  </div>
92
- </div>
93
  {% endfor %}
94
  </div>
95
  </div>
96
 
97
- <!-- Modals -->
98
- <div class="modal-overlay" id="createModal">
99
  <div class="modal">
100
- <h3>Create New Folder</h3>
101
- <input type="text" id="createName" placeholder="Folder name">
 
 
102
  <div class="modal-actions">
103
- <button class="btn btn-secondary" onclick="closeModal('createModal')">Cancel</button>
104
- <button class="btn btn-primary" onclick="confirmCreate()">Create</button>
 
 
 
105
  </div>
106
  </div>
107
  </div>
108
- <div class="modal-overlay" id="renameModal">
 
 
109
  <div class="modal">
110
- <h3>Rename Item</h3>
111
- <input type="text" id="renameInput">
 
 
112
  <div class="modal-actions">
113
- <button class="btn btn-secondary" onclick="closeModal('renameModal')">Cancel</button>
114
- <button class="btn btn-primary" onclick="confirmRename()">Rename</button>
 
 
 
115
  </div>
116
  </div>
117
  </div>
118
- <div class="modal-overlay" id="deleteModal">
 
 
119
  <div class="modal">
120
- <h3>Confirm Deletion</h3>
121
- <p>Are you sure you want to delete this item?</p>
 
 
122
  <div class="modal-actions">
123
- <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
124
- <button class="btn btn-primary danger" onclick="confirmDelete()">Delete</button>
 
 
 
125
  </div>
126
  </div>
127
  </div>
128
 
129
  <script>
130
- let currentPath = '';
131
- function nav(p) { window.location = '/?path='+encodeURIComponent(p); }
132
- function download(p) { window.open('/download?path='+encodeURIComponent(p)); }
133
- function toggleDropdown(btn) {
134
- const d = btn.parentElement; d.classList.toggle('active');
135
- }
136
- function openCreateModal() { document.getElementById('createModal').classList.add('active'); }
137
- function openRenameModal(path,name) { currentPath=path; document.getElementById('renameInput').value=name; document.getElementById('renameModal').classList.add('active'); }
138
- function openDeleteModal(path) { currentPath=path; document.getElementById('deleteModal').classList.add('active'); }
139
- function closeModal(id) { document.getElementById(id).classList.remove('active'); }
140
- function confirmCreate() {
141
- const name = document.getElementById('createName').value.trim(); if(!name) return;
142
- fetch('/create_folder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path: name})})
143
- .then(r=>{closeModal('createModal');window.location.reload();});
144
- }
145
- function confirmRename() {
146
- const newName = document.getElementById('renameInput').value.trim(); if(!newName) return;
147
- fetch('/rename',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({old_path:currentPath,new_path:newName})})
148
- .then(r=>{closeModal('renameModal');window.location.reload();});
149
- }
150
- function confirmDelete() {
151
- fetch('/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path:currentPath})})
152
- .then(r=>{closeModal('deleteModal');window.location.reload();});
153
- }
154
- // Close modals on overlay click
155
- document.querySelectorAll('.modal-overlay').forEach(o=>o.addEventListener('click',e=>{if(e.target===o)o.classList.remove('active');}));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  </script>
157
  </body>
158
  </html>
@@ -161,65 +838,78 @@ TEMPLATE = """
161
  def list_folder(path=""):
162
  prefix = path.strip("/") + ("/" if path else "")
163
  all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
164
- seen = set(); items = []
 
165
  for f in all_files:
166
  if not f.startswith(prefix): continue
167
  rest = f[len(prefix):]
168
  if "/" in rest:
169
  dir_name = rest.split("/")[0]
170
- dir_path = (prefix+dir_name).strip("/")
171
  if dir_path not in seen:
172
- seen.add(dir_path); items.append({"type":"dir","name":dir_name,"path":dir_path})
 
173
  else:
174
- items.append({"type":"file","name":rest,"path":(prefix+rest).strip("/")})
175
- items.sort(key=lambda x:(x['type']!='dir',x['name'].lower()))
 
176
  return items
177
 
178
- @app.route('/',methods=['GET'])
179
  def index():
180
- path = request.args.get('path','').strip('/')
181
  return render_template_string(TEMPLATE, items=list_folder(path), path=path)
182
 
183
- @app.route('/download',methods=['GET'])
184
  def download():
185
- p=request.args.get('path','')
186
- local=hf_hub_download(repo_id=REPO_ID,filename=p,repo_type='dataset',token=HF_TOKEN,cache_dir=tempfile.gettempdir())
187
- return send_file(local,as_attachment=True,download_name=os.path.basename(p))
 
188
 
189
- @app.route('/upload',methods=['POST'])
190
  def upload():
191
- file=request.files['file']; path=request.form.get('path','').strip('/')
192
- dest=f"{path}/{file.filename}".strip('/')
193
- tmp=tempfile.NamedTemporaryFile(delete=False); file.save(tmp.name)
194
- upload_file(path_or_fileobj=tmp.name,path_in_repo=dest,repo_id=REPO_ID,repo_type='dataset',token=HF_TOKEN)
 
 
195
  return redirect(f"/?path={path}")
196
 
197
- @app.route('/delete',methods=['POST'])
198
  def delete():
199
- d=request.get_json()['path']
200
- for f in api.list_repo_files(repo_id=REPO_ID,repo_type='dataset',token=HF_TOKEN):
201
- if f==d or f.startswith(d.rstrip('/')+'/'):
202
- delete_file(repo_id=REPO_ID,path_in_repo=f,repo_type='dataset',token=HF_TOKEN)
203
- return jsonify(status='ok')
 
204
 
205
- @app.route('/create_folder',methods=['POST'])
206
  def create_folder():
207
- folder=request.get_json()['path'].strip('/')
208
- keep=f"{folder}/.keep"
209
- tmp=tempfile.NamedTemporaryFile(delete=False); tmp.write(b""); tmp.flush()
210
- upload_file(path_or_fileobj=tmp.name,path_in_repo=keep,repo_id=REPO_ID,repo_type='dataset',token=HF_TOKEN)
211
- return jsonify(status='ok')
 
 
212
 
213
- @app.route('/rename',methods=['POST'])
214
  def rename():
215
- data=request.get_json(); old,new=data['old_path'].strip('/'),data['new_path'].strip('/')
216
- for f in api.list_repo_files(repo_id=REPO_ID,repo_type='dataset',token=HF_TOKEN):
217
- if f==old or f.startswith(old+'/'):
218
- rel=f[len(old):].lstrip('/'); newp=(new+'/'+rel).strip('/')
219
- local=hf_hub_download(repo_id=REPO_ID,filename=f,repo_type='dataset',token=HF_TOKEN,cache_dir=tempfile.gettempdir())
220
- upload_file(path_or_fileobj=local,path_in_repo=newp,repo_id=REPO_ID,repo_type='dataset',token=HF_TOKEN)
221
- delete_file(repo_id=REPO_ID,path_in_repo=f,repo_type='dataset',token=HF_TOKEN)
222
- return jsonify(status='ok')
223
-
224
- if __name__=='__main__':
225
- app.run(host='0.0.0.0',port=7860,debug=True)
 
 
 
 
 
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
+ :root {
22
+ --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
23
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
24
+ --warning-gradient: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
25
+ --bg-dark: #0f0f23;
26
+ --bg-darker: #0a0a18;
27
+ --border-light: rgba(255, 255, 255, 0.1);
28
+ --text-light: #e2e8f0;
29
+ --text-lighter: #f1f5f9;
30
+ --text-muted: #94a3b8;
31
+ --shadow-primary: 0 8px 32px rgba(102, 126, 234, 0.3);
32
+ --shadow-danger: 0 8px 32px rgba(239, 68, 68, 0.3);
33
+ --shadow-large: 0 20px 60px rgba(0, 0, 0, 0.6);
34
+ --radius-lg: 16px;
35
+ --radius-md: 12px;
36
+ --radius-sm: 8px;
37
+ }
38
+
39
+ * {
40
+ margin: 0;
41
+ padding: 0;
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ body {
46
+ background: linear-gradient(135deg, var(--bg-dark) 0%, var(--bg-darker) 100%);
47
+ color: var(--text-light);
48
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
49
+ min-height: 100vh;
50
+ line-height: 1.5;
51
+ }
52
+
53
+ .container {
54
+ max-width: 1200px;
55
+ margin: 0 auto;
56
+ padding: 20px;
57
+ }
58
+
59
+ .header {
60
+ background: rgba(255, 255, 255, 0.05);
61
+ backdrop-filter: blur(10px);
62
+ border-radius: var(--radius-lg);
63
+ padding: 24px;
64
+ margin-bottom: 24px;
65
+ border: 1px solid var(--border-light);
66
+ box-shadow: var(--shadow-large);
67
+ }
68
+
69
+ .title {
70
+ font-size: 2rem;
71
+ font-weight: 700;
72
+ margin-bottom: 16px;
73
+ background: var(--primary-gradient);
74
+ -webkit-background-clip: text;
75
+ -webkit-text-fill-color: transparent;
76
+ background-clip: text;
77
+ }
78
+
79
+ .breadcrumb {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 8px;
83
+ margin-bottom: 20px;
84
+ flex-wrap: wrap;
85
+ }
86
+
87
+ .breadcrumb-item {
88
+ background: rgba(255, 255, 255, 0.1);
89
+ color: var(--text-muted);
90
+ padding: 6px 12px;
91
+ border-radius: 20px;
92
+ font-size: 0.875rem;
93
+ cursor: pointer;
94
+ transition: all 0.3s ease;
95
+ border: 1px solid var(--border-light);
96
+ white-space: nowrap;
97
+ max-width: 200px;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ }
101
+
102
+ .breadcrumb-item:hover {
103
+ background: rgba(102, 126, 234, 0.2);
104
+ color: var(--text-lighter);
105
+ transform: translateY(-1px);
106
+ }
107
+
108
+ .breadcrumb-item.active {
109
+ background: var(--primary-gradient);
110
+ color: white;
111
+ }
112
+
113
+ .breadcrumb-separator {
114
+ color: var(--text-muted);
115
+ font-size: 0.875rem;
116
+ }
117
+
118
+ .actions {
119
+ display: flex;
120
+ gap: 12px;
121
+ margin-bottom: 24px;
122
+ flex-wrap: wrap;
123
+ }
124
+
125
+ .upload-container {
126
+ position: relative;
127
+ overflow: hidden;
128
+ display: inline-block;
129
+ }
130
+
131
+ .file-input {
132
+ position: absolute;
133
+ left: -9999px;
134
+ opacity: 0;
135
+ }
136
+
137
+ .btn {
138
+ background: var(--primary-gradient);
139
+ color: white;
140
+ border: none;
141
+ padding: 12px 24px;
142
+ border-radius: var(--radius-md);
143
+ cursor: pointer;
144
+ font-weight: 600;
145
+ font-size: 0.875rem;
146
+ transition: all 0.3s ease;
147
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
148
+ border: 1px solid var(--border-light);
149
+ display: inline-flex;
150
+ align-items: center;
151
+ gap: 8px;
152
+ }
153
+
154
+ .btn:hover {
155
+ transform: translateY(-2px);
156
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
157
+ }
158
+
159
+ .btn-secondary {
160
+ background: rgba(255, 255, 255, 0.1);
161
+ color: var(--text-lighter);
162
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
163
+ }
164
+
165
+ .btn-secondary:hover {
166
+ background: rgba(255, 255, 255, 0.15);
167
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
168
+ }
169
+
170
+ .file-grid {
171
+ display: grid;
172
+ gap: 16px;
173
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
174
+ }
175
+
176
+ .file-item {
177
+ background: rgba(255, 255, 255, 0.05);
178
+ backdrop-filter: blur(10px);
179
+ border: 1px solid var(--border-light);
180
+ border-radius: var(--radius-lg);
181
+ padding: 16px;
182
+ transition: all 0.3s ease;
183
+ cursor: pointer;
184
+ position: relative;
185
+ overflow: hidden;
186
+ }
187
+
188
+ .file-item:hover {
189
+ transform: translateY(-4px);
190
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
191
+ border-color: rgba(102, 126, 234, 0.3);
192
+ }
193
+
194
+ .file-content {
195
+ display: flex;
196
+ align-items: center;
197
+ justify-content: space-between;
198
+ position: relative;
199
+ z-index: 2;
200
+ gap: 12px;
201
+ }
202
+
203
+ .file-info {
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 12px;
207
+ flex: 1;
208
+ min-width: 0;
209
+ }
210
+
211
+ .file-icon {
212
+ font-size: 1.5rem;
213
+ width: 40px;
214
+ height: 40px;
215
+ display: flex;
216
+ align-items: center;
217
+ justify-content: center;
218
+ border-radius: var(--radius-sm);
219
+ background: rgba(255, 255, 255, 0.1);
220
+ flex-shrink: 0;
221
+ }
222
+
223
+ .folder-icon {
224
+ background: var(--warning-gradient);
225
+ }
226
+
227
+ .file-name {
228
+ font-weight: 500;
229
+ font-size: 0.95rem;
230
+ color: var(--text-lighter);
231
+ white-space: nowrap;
232
+ overflow: hidden;
233
+ text-overflow: ellipsis;
234
+ }
235
+
236
+ .dropdown {
237
+ position: relative;
238
+ flex-shrink: 0;
239
+ }
240
+
241
+ .dropdown-toggle {
242
+ background: rgba(255, 255, 255, 0.1);
243
+ border: none;
244
+ color: var(--text-muted);
245
+ padding: 8px;
246
+ border-radius: var(--radius-sm);
247
+ cursor: pointer;
248
+ transition: all 0.3s ease;
249
+ font-size: 1.25rem;
250
+ width: 36px;
251
+ height: 36px;
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ }
256
+
257
+ .dropdown-toggle:hover {
258
+ background: rgba(255, 255, 255, 0.2);
259
+ color: var(--text-lighter);
260
+ }
261
+
262
+ .dropdown-menu {
263
+ position: absolute;
264
+ top: 100%;
265
+ right: 0;
266
+ background: rgba(15, 15, 35, 0.98);
267
+ backdrop-filter: blur(20px);
268
+ border: 1px solid var(--border-light);
269
+ border-radius: var(--radius-md);
270
+ padding: 8px;
271
+ min-width: 160px;
272
+ opacity: 0;
273
+ visibility: hidden;
274
+ transform: translateY(-10px);
275
+ transition: all 0.3s ease;
276
+ z-index: 1000;
277
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
278
+ }
279
+
280
+ .dropdown.active .dropdown-menu {
281
+ opacity: 1;
282
+ visibility: visible;
283
+ transform: translateY(0);
284
+ }
285
+
286
+ .dropdown-item {
287
+ display: flex;
288
+ align-items: center;
289
+ gap: 8px;
290
+ padding: 10px 12px;
291
+ border-radius: var(--radius-sm);
292
+ cursor: pointer;
293
+ transition: all 0.2s ease;
294
+ font-size: 0.875rem;
295
+ color: var(--text-muted);
296
+ }
297
+
298
+ .dropdown-item:hover {
299
+ background: rgba(255, 255, 255, 0.1);
300
+ color: var(--text-lighter);
301
+ }
302
+
303
+ .dropdown-item.danger:hover {
304
+ background: rgba(239, 68, 68, 0.2);
305
+ color: #fca5a5;
306
+ }
307
+
308
+ /* Modal Styles */
309
+ .modal-overlay {
310
+ position: fixed;
311
+ top: 0;
312
+ left: 0;
313
+ width: 100%;
314
+ height: 100%;
315
+ background: rgba(0, 0, 0, 0.7);
316
+ backdrop-filter: blur(5px);
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: center;
320
+ z-index: 2000;
321
+ opacity: 0;
322
+ visibility: hidden;
323
+ transition: all 0.3s ease;
324
+ }
325
+
326
+ .modal-overlay.active {
327
+ opacity: 1;
328
+ visibility: visible;
329
+ }
330
+
331
+ .modal {
332
+ background: rgba(15, 15, 35, 0.98);
333
+ backdrop-filter: blur(20px);
334
+ border: 1px solid var(--border-light);
335
+ border-radius: var(--radius-lg);
336
+ padding: 24px;
337
+ max-width: 450px;
338
+ width: 90%;
339
+ transform: scale(0.9);
340
+ transition: all 0.3s ease;
341
+ box-shadow: var(--shadow-large);
342
+ }
343
+
344
+ .modal-overlay.active .modal {
345
+ transform: scale(1);
346
+ }
347
+
348
+ .modal-title {
349
+ font-size: 1.25rem;
350
+ font-weight: 600;
351
+ margin-bottom: 16px;
352
+ color: var(--text-lighter);
353
+ }
354
+
355
+ .modal-body {
356
+ margin-bottom: 24px;
357
+ color: var(--text-muted);
358
+ }
359
+
360
+ .modal-input {
361
+ width: 100%;
362
+ background: rgba(255, 255, 255, 0.1);
363
+ border: 1px solid rgba(255, 255, 255, 0.2);
364
+ border-radius: var(--radius-md);
365
+ padding: 12px 16px;
366
+ color: var(--text-lighter);
367
+ font-size: 1rem;
368
+ margin-bottom: 20px;
369
+ transition: all 0.3s ease;
370
+ }
371
+
372
+ .modal-input:focus {
373
+ outline: none;
374
+ border-color: #667eea;
375
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
376
+ }
377
+
378
+ .modal-actions {
379
+ display: flex;
380
+ gap: 12px;
381
+ justify-content: flex-end;
382
+ }
383
+
384
+ .btn-danger {
385
+ background: var(--danger-gradient);
386
+ box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
387
+ }
388
+
389
+ .btn-danger:hover {
390
+ box-shadow: 0 8px 25px rgba(239, 68, 68, 0.4);
391
+ }
392
+
393
+ /* New Folder Modal */
394
+ #folderModal .modal {
395
+ max-width: 400px;
396
+ }
397
+
398
+ /* Loading Animation */
399
+ .loading {
400
+ display: inline-block;
401
+ width: 20px;
402
+ height: 20px;
403
+ border: 3px solid rgba(255, 255, 255, 0.3);
404
+ border-radius: 50%;
405
+ border-top-color: #667eea;
406
+ animation: spin 1s ease-in-out infinite;
407
+ }
408
+
409
+ @keyframes spin {
410
+ to { transform: rotate(360deg); }
411
+ }
412
+
413
+ /* Responsive Design */
414
+ @media (max-width: 768px) {
415
+ .container {
416
+ padding: 16px;
417
+ }
418
+
419
+ .file-grid {
420
+ grid-template-columns: 1fr;
421
+ }
422
+
423
+ .actions {
424
+ flex-direction: column;
425
+ align-items: flex-start;
426
+ }
427
+
428
+ .breadcrumb {
429
+ gap: 6px;
430
+ }
431
+
432
+ .breadcrumb-separator {
433
+ display: none;
434
+ }
435
+
436
+ .breadcrumb-item {
437
+ max-width: 150px;
438
+ }
439
+
440
+ .modal {
441
+ padding: 20px;
442
+ }
443
+
444
+ .modal-actions {
445
+ flex-direction: column-reverse;
446
+ gap: 8px;
447
+ }
448
+
449
+ .modal-actions .btn {
450
+ width: 100%;
451
+ }
452
+ }
453
+
454
+ /* Custom Scrollbar */
455
+ ::-webkit-scrollbar {
456
+ width: 8px;
457
+ height: 8px;
458
+ }
459
+
460
+ ::-webkit-scrollbar-track {
461
+ background: rgba(255, 255, 255, 0.1);
462
+ }
463
+
464
+ ::-webkit-scrollbar-thumb {
465
+ background: rgba(102, 126, 234, 0.5);
466
+ border-radius: 4px;
467
+ }
468
+
469
+ ::-webkit-scrollbar-thumb:hover {
470
+ background: rgba(102, 126, 234, 0.7);
471
+ }
472
  </style>
473
  </head>
474
  <body>
475
  <div class="container">
476
  <div class="header">
477
+ <h1 class="title">🚀 HuggingFace Drive</h1>
478
+
479
+ <!-- Breadcrumb Navigation -->
480
  <div class="breadcrumb">
481
+ <span class="breadcrumb-item {{ 'active' if not path else '' }}" onclick="nav('')">
482
+ 🏠 Home
483
+ </span>
484
  {% if path %}
485
+ {% set parts = path.split('/') %}
486
  {% for i in range(parts|length) %}
487
+ <span class="breadcrumb-separator">›</span>
488
+ {% set current_path = parts[:i+1]|join('/') %}
489
+ <span class="breadcrumb-item {{ 'active' if current_path == path else '' }}"
490
+ onclick="nav('{{ current_path }}')">
491
+ 📁 {{ parts[i] }}
492
+ </span>
493
  {% endfor %}
494
  {% endif %}
495
  </div>
496
+
497
+ <!-- Actions -->
498
  <div class="actions">
499
+ <form action="/upload" method="post" enctype="multipart/form-data" class="upload-container">
500
+ <input type="file" name="file" required class="file-input" id="fileInput">
501
  <input type="hidden" name="path" value="{{ path }}">
502
+ <label for="fileInput" class="btn">
503
+ 📤 Upload File
504
+ </label>
505
  </form>
506
+ <button class="btn btn-secondary" onclick="showFolderModal('{{ path }}')">
507
+ 📁 New Folder
508
+ </button>
509
  </div>
510
  </div>
511
+
512
+ <!-- File Grid -->
513
  <div class="file-grid">
514
  {% for item in items %}
515
+ <div class="file-item">
516
+ <div class="file-content">
517
+ <div class="file-info" onclick="{% if item.type=='dir' %}nav('{{ item.path }}'){% else %}download('{{ item.path }}'){% endif %}">
518
+ <div class="file-icon {{ 'folder-icon' if item.type=='dir' else '' }}">
519
+ {{ '📁' if item.type=='dir' else '📄' }}
520
+ </div>
521
+ <div class="file-name" title="{{ item.name }}">{{ item.name }}</div>
522
+ </div>
523
+
524
+ <div class="dropdown">
525
+ <button class="dropdown-toggle" onclick="toggleDropdown(this)">⋮</button>
526
+ <div class="dropdown-menu">
527
+ {% if item.type == 'file' %}
528
+ <div class="dropdown-item" onclick="download('{{ item.path }}')">
529
+ 📥 Download
530
+ </div>
531
+ {% endif %}
532
+ <div class="dropdown-item" onclick="showRenameModal('{{ item.path }}', '{{ item.name }}')">
533
+ ✏️ Rename
534
+ </div>
535
+ <div class="dropdown-item danger" onclick="showDeleteModal('{{ item.path }}', '{{ item.name }}')">
536
+ 🗑️ Delete
537
+ </div>
538
+ </div>
539
+ </div>
540
  </div>
541
  </div>
 
542
  {% endfor %}
543
  </div>
544
  </div>
545
 
546
+ <!-- Rename Modal -->
547
+ <div class="modal-overlay" id="renameModal">
548
  <div class="modal">
549
+ <h3 class="modal-title">Rename Item</h3>
550
+ <div class="modal-body">
551
+ <input type="text" class="modal-input" id="renameInput" placeholder="Enter new name" autocomplete="off">
552
+ </div>
553
  <div class="modal-actions">
554
+ <button class="btn btn-secondary" onclick="closeModal('renameModal')">Cancel</button>
555
+ <button class="btn" onclick="confirmRename()" id="renameConfirmBtn">
556
+ <span id="renameBtnText">Rename</span>
557
+ <span id="renameBtnSpinner" class="loading" style="display: none;"></span>
558
+ </button>
559
  </div>
560
  </div>
561
  </div>
562
+
563
+ <!-- Delete Confirmation Modal -->
564
+ <div class="modal-overlay" id="deleteModal">
565
  <div class="modal">
566
+ <h3 class="modal-title">Confirm Deletion</h3>
567
+ <div class="modal-body">
568
+ <p>Are you sure you want to delete <strong id="deleteItemName"></strong>? This action cannot be undone.</p>
569
+ </div>
570
  <div class="modal-actions">
571
+ <button class="btn btn-secondary" onclick="closeModal('deleteModal')">Cancel</button>
572
+ <button class="btn btn-danger" onclick="confirmDelete()" id="deleteConfirmBtn">
573
+ <span id="deleteBtnText">Delete</span>
574
+ <span id="deleteBtnSpinner" class="loading" style="display: none;"></span>
575
+ </button>
576
  </div>
577
  </div>
578
  </div>
579
+
580
+ <!-- New Folder Modal -->
581
+ <div class="modal-overlay" id="folderModal">
582
  <div class="modal">
583
+ <h3 class="modal-title">Create New Folder</h3>
584
+ <div class="modal-body">
585
+ <input type="text" class="modal-input" id="folderInput" placeholder="Enter folder name" autocomplete="off">
586
+ </div>
587
  <div class="modal-actions">
588
+ <button class="btn btn-secondary" onclick="closeModal('folderModal')">Cancel</button>
589
+ <button class="btn" onclick="confirmCreateFolder()" id="folderConfirmBtn">
590
+ <span id="folderBtnText">Create</span>
591
+ <span id="folderBtnSpinner" class="loading" style="display: none;"></span>
592
+ </button>
593
  </div>
594
  </div>
595
  </div>
596
 
597
  <script>
598
+ let currentRenamePath = '';
599
+ let currentDeletePath = '';
600
+ let currentFolderPath = '';
601
+ let activeDropdown = null;
602
+
603
+ // Navigation
604
+ function nav(p) {
605
+ location.href = '/?path=' + encodeURIComponent(p);
606
+ }
607
+
608
+ // File download
609
+ function download(path) {
610
+ window.open('/download?path=' + encodeURIComponent(path), '_blank');
611
+ }
612
+
613
+ // Dropdown toggle
614
+ function toggleDropdown(button) {
615
+ event.stopPropagation();
616
+ const dropdown = button.closest('.dropdown');
617
+
618
+ // Close other dropdowns
619
+ if (activeDropdown && activeDropdown !== dropdown) {
620
+ activeDropdown.classList.remove('active');
621
+ }
622
+
623
+ dropdown.classList.toggle('active');
624
+ activeDropdown = dropdown.classList.contains('active') ? dropdown : null;
625
+ }
626
+
627
+ // Show rename modal
628
+ function showRenameModal(path, currentName) {
629
+ currentRenamePath = path;
630
+ document.getElementById('renameInput').value = currentName;
631
+ showModal('renameModal');
632
+ setTimeout(() => {
633
+ document.getElementById('renameInput').focus();
634
+ document.getElementById('renameInput').select();
635
+ }, 100);
636
+ closeAllDropdowns();
637
+ }
638
+
639
+ // Show delete modal
640
+ function showDeleteModal(path, name) {
641
+ currentDeletePath = path;
642
+ document.getElementById('deleteItemName').textContent = name;
643
+ showModal('deleteModal');
644
+ closeAllDropdowns();
645
+ }
646
+
647
+ // Show folder creation modal
648
+ function showFolderModal(path) {
649
+ currentFolderPath = path;
650
+ document.getElementById('folderInput').value = '';
651
+ showModal('folderModal');
652
+ setTimeout(() => document.getElementById('folderInput').focus(), 100);
653
+ }
654
+
655
+ // Show modal
656
+ function showModal(modalId) {
657
+ document.getElementById(modalId).classList.add('active');
658
+ document.body.style.overflow = 'hidden';
659
+ }
660
+
661
+ // Close modal
662
+ function closeModal(modalId) {
663
+ document.getElementById(modalId).classList.remove('active');
664
+ document.body.style.overflow = '';
665
+ }
666
+
667
+ // Close all dropdowns
668
+ function closeAllDropdowns() {
669
+ document.querySelectorAll('.dropdown.active').forEach(dropdown => {
670
+ dropdown.classList.remove('active');
671
+ });
672
+ activeDropdown = null;
673
+ }
674
+
675
+ // Confirm rename action
676
+ async function confirmRename() {
677
+ const newName = document.getElementById('renameInput').value.trim();
678
+ if (!newName) {
679
+ alert('Please enter a valid name');
680
+ return;
681
+ }
682
+
683
+ const btn = document.getElementById('renameConfirmBtn');
684
+ const btnText = document.getElementById('renameBtnText');
685
+ const spinner = document.getElementById('renameBtnSpinner');
686
+
687
+ btn.disabled = true;
688
+ btnText.style.display = 'none';
689
+ spinner.style.display = 'inline-block';
690
+
691
+ try {
692
+ const response = await fetch('/rename', {
693
+ method: 'POST',
694
+ headers: { 'Content-Type': 'application/json' },
695
+ body: JSON.stringify({
696
+ old_path: currentRenamePath,
697
+ new_path: newName
698
+ })
699
+ });
700
+
701
+ if (response.ok) {
702
+ closeModal('renameModal');
703
+ location.reload();
704
+ } else {
705
+ alert('Failed to rename item');
706
+ }
707
+ } catch (error) {
708
+ console.error('Rename failed:', error);
709
+ alert('An error occurred while renaming');
710
+ } finally {
711
+ btn.disabled = false;
712
+ btnText.style.display = 'inline-block';
713
+ spinner.style.display = 'none';
714
+ }
715
+ }
716
+
717
+ // Confirm delete action
718
+ async function confirmDelete() {
719
+ const btn = document.getElementById('deleteConfirmBtn');
720
+ const btnText = document.getElementById('deleteBtnText');
721
+ const spinner = document.getElementById('deleteBtnSpinner');
722
+
723
+ btn.disabled = true;
724
+ btnText.style.display = 'none';
725
+ spinner.style.display = 'inline-block';
726
+
727
+ try {
728
+ const response = await fetch('/delete', {
729
+ method: 'POST',
730
+ headers: { 'Content-Type': 'application/json' },
731
+ body: JSON.stringify({ path: currentDeletePath })
732
+ });
733
+
734
+ if (response.ok) {
735
+ closeModal('deleteModal');
736
+ location.reload();
737
+ } else {
738
+ alert('Failed to delete item');
739
+ }
740
+ } catch (error) {
741
+ console.error('Delete failed:', error);
742
+ alert('An error occurred while deleting');
743
+ } finally {
744
+ btn.disabled = false;
745
+ btnText.style.display = 'inline-block';
746
+ spinner.style.display = 'none';
747
+ }
748
+ }
749
+
750
+ // Confirm folder creation
751
+ async function confirmCreateFolder() {
752
+ const folderName = document.getElementById('folderInput').value.trim();
753
+ if (!folderName) {
754
+ alert('Please enter a folder name');
755
+ return;
756
+ }
757
+
758
+ const btn = document.getElementById('folderConfirmBtn');
759
+ const btnText = document.getElementById('folderBtnText');
760
+ const spinner = document.getElementById('folderBtnSpinner');
761
+
762
+ btn.disabled = true;
763
+ btnText.style.display = 'none';
764
+ spinner.style.display = 'inline-block';
765
+
766
+ try {
767
+ const folderPath = currentFolderPath ? `${currentFolderPath}/${folderName}` : folderName;
768
+ const response = await fetch('/create_folder', {
769
+ method: 'POST',
770
+ headers: { 'Content-Type': 'application/json' },
771
+ body: JSON.stringify({ path: folderPath })
772
+ });
773
+
774
+ if (response.ok) {
775
+ closeModal('folderModal');
776
+ location.reload();
777
+ } else {
778
+ alert('Failed to create folder');
779
+ }
780
+ } catch (error) {
781
+ console.error('Create folder failed:', error);
782
+ alert('An error occurred while creating folder');
783
+ } finally {
784
+ btn.disabled = false;
785
+ btnText.style.display = 'inline-block';
786
+ spinner.style.display = 'none';
787
+ }
788
+ }
789
+
790
+ // Close dropdowns when clicking outside
791
+ document.addEventListener('click', function(e) {
792
+ if (!e.target.closest('.dropdown')) {
793
+ closeAllDropdowns();
794
+ }
795
+
796
+ // Close modals when clicking on overlay
797
+ if (e.target.classList.contains('modal-overlay')) {
798
+ closeModal(e.target.id);
799
+ }
800
+ });
801
+
802
+ // Handle file input change for better UX
803
+ document.getElementById('fileInput').addEventListener('change', function(e) {
804
+ if (e.target.files.length > 0) {
805
+ e.target.closest('form').submit();
806
+ }
807
+ });
808
+
809
+ // Handle Enter key in modals
810
+ document.getElementById('renameInput').addEventListener('keypress', function(e) {
811
+ if (e.key === 'Enter') {
812
+ confirmRename();
813
+ }
814
+ });
815
+
816
+ document.getElementById('folderInput').addEventListener('keypress', function(e) {
817
+ if (e.key === 'Enter') {
818
+ confirmCreateFolder();
819
+ }
820
+ });
821
+
822
+ // Focus management for accessibility
823
+ document.addEventListener('keydown', function(e) {
824
+ if (e.key === 'Escape') {
825
+ const openModal = document.querySelector('.modal-overlay.active');
826
+ if (openModal) {
827
+ closeModal(openModal.id);
828
+ } else {
829
+ closeAllDropdowns();
830
+ }
831
+ }
832
+ });
833
  </script>
834
  </body>
835
  </html>
 
838
  def list_folder(path=""):
839
  prefix = path.strip("/") + ("/" if path else "")
840
  all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
841
+ seen = set()
842
+ items = []
843
  for f in all_files:
844
  if not f.startswith(prefix): continue
845
  rest = f[len(prefix):]
846
  if "/" in rest:
847
  dir_name = rest.split("/")[0]
848
+ dir_path = (prefix + dir_name).strip("/")
849
  if dir_path not in seen:
850
+ seen.add(dir_path)
851
+ items.append({"type":"dir","name":dir_name,"path":dir_path})
852
  else:
853
+ items.append({"type":"file","name":rest,"path":(prefix + rest).strip("/")})
854
+ # sort dirs then files
855
+ items.sort(key=lambda x: (x["type"]!="dir", x["name"].lower()))
856
  return items
857
 
858
+ @app.route("/", methods=["GET"])
859
  def index():
860
+ path = request.args.get("path","").strip("/")
861
  return render_template_string(TEMPLATE, items=list_folder(path), path=path)
862
 
863
+ @app.route("/download", methods=["GET"])
864
  def download():
865
+ p = request.args.get("path","")
866
+ # download via hf_hub_download into /tmp
867
+ local = hf_hub_download(repo_id=REPO_ID, filename=p, repo_type="dataset", token=HF_TOKEN, cache_dir=tempfile.gettempdir())
868
+ return send_file(local, as_attachment=True, download_name=os.path.basename(p))
869
 
870
+ @app.route("/upload", methods=["POST"])
871
  def upload():
872
+ file = request.files["file"]
873
+ path = request.form.get("path","").strip("/")
874
+ dest = f"{path}/{file.filename}".strip("/")
875
+ tmp = tempfile.NamedTemporaryFile(delete=False)
876
+ file.save(tmp.name)
877
+ upload_file(path_or_fileobj=tmp.name, path_in_repo=dest, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
878
  return redirect(f"/?path={path}")
879
 
880
+ @app.route("/delete", methods=["POST"])
881
  def delete():
882
+ d = request.get_json()["path"]
883
+ all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
884
+ for f in all_files:
885
+ if f == d or f.startswith(d.rstrip("/")+"/"):
886
+ delete_file(repo_id=REPO_ID, path_in_repo=f, repo_type="dataset", token=HF_TOKEN)
887
+ return jsonify(status="ok")
888
 
889
+ @app.route("/create_folder", methods=["POST"])
890
  def create_folder():
891
+ folder = request.get_json()["path"].strip("/")
892
+ keep = f"{folder}/.keep"
893
+ tmp = tempfile.NamedTemporaryFile(delete=False)
894
+ tmp.write(b"")
895
+ tmp.flush()
896
+ upload_file(path_or_fileobj=tmp.name, path_in_repo=keep, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
897
+ return jsonify(status="ok")
898
 
899
+ @app.route("/rename", methods=["POST"])
900
  def rename():
901
+ data = request.get_json()
902
+ old, new = data["old_path"].strip("/"), data["new_path"].strip("/")
903
+ all_files = api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
904
+ for f in all_files:
905
+ if f == old or f.startswith(old+"/"):
906
+ rel = f[len(old):].lstrip("/")
907
+ newp = (new + "/" + rel).strip("/")
908
+ local = hf_hub_download(repo_id=REPO_ID, filename=f, repo_type="dataset",
909
+ token=HF_TOKEN, cache_dir=tempfile.gettempdir())
910
+ upload_file(path_or_fileobj=local, path_in_repo=newp, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)
911
+ delete_file(repo_id=REPO_ID, path_in_repo=f, repo_type="dataset", token=HF_TOKEN)
912
+ return jsonify(status="ok")
913
+
914
+ if __name__ == "__main__":
915
+ app.run(debug=True, host="0.0.0.0", port=7860)