lexlepty commited on
Commit
0828366
·
verified ·
1 Parent(s): fa5280a

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +324 -0
  2. config.py +25 -0
  3. models.py +45 -0
  4. utils.py +28 -0
app.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import (
2
+ Flask,
3
+ json,
4
+ render_template,
5
+ request,
6
+ jsonify,
7
+ redirect,
8
+ url_for,
9
+ send_file,
10
+ Response
11
+ )
12
+ from io import BytesIO
13
+ import urllib.parse
14
+ from functools import wraps
15
+ import requests
16
+ import hashlib
17
+ import os
18
+ from config import Config
19
+ from models import Database
20
+ from utils import get_file_type, format_file_size
21
+ from huggingface_hub import HfApi
22
+
23
+ # 初始化 HuggingFace API
24
+ api = HfApi(token=Config.HF_TOKEN)
25
+ app = Flask(__name__)
26
+ app.config['SECRET_KEY'] = Config.SECRET_KEY
27
+ db = Database()
28
+
29
+ def require_auth(f):
30
+ @wraps(f)
31
+ def decorated(*args, **kwargs):
32
+ if not Config.REQUIRE_LOGIN:
33
+ return f(*args, **kwargs)
34
+ if not request.cookies.get('authenticated'):
35
+ if request.is_json:
36
+ return jsonify({'error': 'Unauthorized'}), 401
37
+ return redirect(url_for('login'))
38
+ return f(*args, **kwargs)
39
+ return decorated
40
+
41
+ @app.route('/login', methods=['GET', 'POST'])
42
+ def login():
43
+ if request.method == 'POST':
44
+ if request.form.get('password') == Config.ACCESS_PASSWORD:
45
+ response = jsonify({'success': True})
46
+ response.set_cookie('authenticated', 'true', secure=True, httponly=True)
47
+ return response
48
+ return jsonify({'error': 'Invalid password'}), 401
49
+ return render_template('login.html')
50
+
51
+ @app.route('/logout')
52
+ def logout():
53
+ response = redirect(url_for('login'))
54
+ response.delete_cookie('authenticated')
55
+ return response
56
+
57
+ @app.route('/')
58
+ @require_auth
59
+ def index():
60
+ return render_template('index.html')
61
+
62
+ @app.route('/api/files/list/')
63
+ @app.route('/api/files/list/<path:directory>')
64
+ @require_auth
65
+ def list_files(directory=''):
66
+ try:
67
+ url = f"https://huggingface.co/api/datasets/{Config.HF_DATASET_ID}/tree/{Config.HF_BRANCH}"
68
+ if directory:
69
+ url = f"{url}/{directory}"
70
+
71
+ response = requests.get(
72
+ url,
73
+ headers={'Authorization': f'Bearer {Config.HF_TOKEN}'}
74
+ )
75
+ if not response.ok:
76
+ return jsonify({'error': 'Failed to fetch files', 'details': response.text}), response.status_code
77
+
78
+ files = response.json()
79
+ for file in files:
80
+ if file['type'] == 'file':
81
+ file['file_type'] = get_file_type(file['path'])
82
+ file['size_formatted'] = format_file_size(file['size'])
83
+ # 添加预览和下载URL
84
+ file['preview_url'] = f"/api/files/preview/{file['path']}"
85
+ file['download_url'] = f"/api/files/download/{file['path']}"
86
+
87
+ return jsonify(files)
88
+ except Exception as e:
89
+ return jsonify({'error': str(e)}), 500
90
+
91
+ @app.route('/api/files/preview/<path:filepath>')
92
+ @require_auth
93
+ def preview_file(filepath):
94
+ try:
95
+ file_type = get_file_type(filepath)
96
+ if file_type not in ['image', 'video', 'document']:
97
+ return jsonify({'error': 'File type not supported for preview'}), 400
98
+
99
+ url = f"https://{Config.PROXY_DOMAIN}/datasets/{Config.HF_DATASET_ID}/resolve/{Config.HF_BRANCH}/{filepath}"
100
+ response = requests.get(
101
+ url,
102
+ headers={'Authorization': f'Bearer {Config.HF_TOKEN}'},
103
+ stream=True
104
+ )
105
+
106
+ if response.ok:
107
+ def generate():
108
+ for chunk in response.iter_content(chunk_size=8192):
109
+ yield chunk
110
+
111
+ return Response(
112
+ generate(),
113
+ mimetype=response.headers.get('content-type', 'application/octet-stream'),
114
+ direct_passthrough=True
115
+ )
116
+
117
+ return jsonify({'error': 'Failed to fetch file'}), response.status_code
118
+ except Exception as e:
119
+ return jsonify({'error': str(e)}), 500
120
+
121
+ @app.route('/api/files/download/<path:filepath>')
122
+ @require_auth
123
+ def download_file(filepath):
124
+ try:
125
+ url = f"https://{Config.PROXY_DOMAIN}/datasets/{Config.HF_DATASET_ID}/resolve/{Config.HF_BRANCH}/{filepath}"
126
+
127
+ # 先发送 HEAD 请求获取文件信息
128
+ head_response = requests.head(
129
+ url,
130
+ headers={'Authorization': f'Bearer {Config.HF_TOKEN}'},
131
+ allow_redirects=True
132
+ )
133
+
134
+ if head_response.ok:
135
+ # 获取文件基本信息
136
+ content_type = head_response.headers.get('content-type', 'application/octet-stream')
137
+ content_length = head_response.headers.get('content-length')
138
+ last_modified = head_response.headers.get('last-modified')
139
+ etag = head_response.headers.get('etag')
140
+
141
+ # 如果是txt文件但没有指定字符集,设置为text/plain
142
+ if filepath.lower().endswith('.txt') and 'charset' not in content_type:
143
+ content_type = 'text/plain'
144
+
145
+ # 获取文件内容
146
+ response = requests.get(
147
+ url,
148
+ headers={'Authorization': f'Bearer {Config.HF_TOKEN}'},
149
+ stream=True
150
+ )
151
+
152
+ if response.ok:
153
+ filename = os.path.basename(filepath)
154
+ encoded_filename = urllib.parse.quote(filename.encode('utf-8'))
155
+
156
+ headers = {
157
+ 'Content-Disposition': f'attachment; filename*=UTF-8\'\'{encoded_filename}',
158
+ 'Content-Type': content_type,
159
+ 'Content-Length': content_length,
160
+ 'Accept-Ranges': 'bytes',
161
+ 'Cache-Control': 'no-cache',
162
+ 'Last-Modified': last_modified,
163
+ 'ETag': etag
164
+ }
165
+
166
+ # 移除为None的header
167
+ headers = {k: v for k, v in headers.items() if v is not None}
168
+
169
+ return Response(
170
+ response.iter_content(chunk_size=1048576),
171
+ headers=headers
172
+ )
173
+
174
+ return jsonify({'error': 'File not found'}), 404
175
+
176
+ except Exception as e:
177
+ print(f"Download error for {filepath}: {str(e)}")
178
+ return jsonify({'error': str(e)}), 500
179
+
180
+ @app.route('/api/files/upload', methods=['POST'])
181
+ @require_auth
182
+ def upload_file():
183
+ if 'file' not in request.files:
184
+ return jsonify({'error': 'No file provided'}), 400
185
+
186
+ file = request.files['file']
187
+ current_path = request.form.get('path', '').strip('/')
188
+
189
+ try:
190
+ file_content = file.read()
191
+ file.seek(0)
192
+
193
+ original_name = file.filename
194
+ stored_name = original_name
195
+ full_path = os.path.join(current_path, stored_name).replace("\\", "/")
196
+
197
+ response = api.upload_file(
198
+ path_or_fileobj=file_content,
199
+ path_in_repo=full_path,
200
+ repo_id=Config.HF_DATASET_ID,
201
+ repo_type="dataset",
202
+ token=Config.HF_TOKEN
203
+ )
204
+
205
+ if response:
206
+ with db.conn.cursor() as cursor:
207
+ cursor.execute("""
208
+ INSERT INTO files (
209
+ original_name, stored_name, file_path,
210
+ file_type, file_size
211
+ ) VALUES (%s, %s, %s, %s, %s)
212
+ """, (
213
+ original_name,
214
+ stored_name,
215
+ full_path,
216
+ get_file_type(original_name),
217
+ len(file_content)
218
+ ))
219
+ db.conn.commit()
220
+
221
+ return jsonify({'success': True})
222
+
223
+ return jsonify({'error': 'Upload failed'}), 500
224
+
225
+ except Exception as e:
226
+ return jsonify({'error': str(e)}), 500
227
+
228
+ @app.route('/api/files/search')
229
+ @require_auth
230
+ def search_files():
231
+ keyword = request.args.get('keyword', '')
232
+ if not keyword:
233
+ return jsonify([])
234
+
235
+ try:
236
+ files = db.search_files(keyword)
237
+ return jsonify([{
238
+ 'type': 'file',
239
+ 'path': f['file_path'],
240
+ 'file_type': get_file_type(f['file_path']),
241
+ 'size': f['file_size'],
242
+ 'size_formatted': format_file_size(f['file_size']),
243
+ 'preview_url': f'/api/files/preview/{f["file_path"]}',
244
+ 'download_url': f'/api/files/download/{f["file_path"]}'
245
+ } for f in files])
246
+ except Exception as e:
247
+ return jsonify({'error': str(e)}), 500
248
+
249
+ @app.route('/api/files/delete/<path:filepath>', methods=['DELETE'])
250
+ @require_auth
251
+ def delete_file(filepath):
252
+ try:
253
+ # Initialize HuggingFace API
254
+ api = HfApi(token=Config.HF_TOKEN)
255
+
256
+ # Delete file from HuggingFace Hub
257
+ api.delete_file(
258
+ path_in_repo=filepath,
259
+ repo_id=Config.HF_DATASET_ID,
260
+ repo_type="dataset"
261
+ )
262
+
263
+ # Delete file record from database
264
+ with db.conn.cursor() as cursor:
265
+ cursor.execute(
266
+ "DELETE FROM files WHERE file_path = %s",
267
+ [filepath]
268
+ )
269
+ db.conn.commit()
270
+
271
+ return jsonify({'success': True})
272
+
273
+ except Exception as e:
274
+ return jsonify({'error': str(e)}), 500
275
+
276
+ @app.route('/api/files/create_folder', methods=['POST'])
277
+ @require_auth
278
+ def create_folder():
279
+ try:
280
+ data = request.json
281
+ path = data.get('path', '/')
282
+ name = data.get('name')
283
+
284
+ if not name:
285
+ return jsonify({'error': 'Folder name is required'}), 400
286
+
287
+ full_path = os.path.join(path, name, '.keep').replace("\\", "/")
288
+
289
+ # 使用 HuggingFace API 创建文件夹
290
+ api = HfApi(token=Config.HF_TOKEN)
291
+ response = api.upload_file(
292
+ path_or_fileobj=b'', # 空内容
293
+ path_in_repo=full_path,
294
+ repo_id=Config.HF_DATASET_ID,
295
+ repo_type="dataset",
296
+ token=Config.HF_TOKEN
297
+ )
298
+
299
+ if response:
300
+ # 记录到数据库
301
+ with db.conn.cursor() as cursor:
302
+ cursor.execute("""
303
+ INSERT INTO files (
304
+ original_name, stored_name, file_path,
305
+ file_type, file_size
306
+ ) VALUES (%s, %s, %s, %s, %s)
307
+ """, (
308
+ '.keep',
309
+ '.keep',
310
+ full_path,
311
+ 'directory',
312
+ 0
313
+ ))
314
+ db.conn.commit()
315
+
316
+ return jsonify({'message': 'Folder created successfully'})
317
+
318
+ return jsonify({'error': 'Failed to create folder'}), 500
319
+
320
+ except Exception as e:
321
+ return jsonify({'error': str(e)}), 500
322
+
323
+ if __name__ == '__main__':
324
+ app.run(host='0.0.0.0', port=5000, debug=True)
config.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ class Config:
7
+ # HuggingFace配置
8
+ HF_DATASET_ID = os.getenv('HF_DATASET_ID')
9
+ HF_TOKEN = os.getenv('HF_TOKEN')
10
+ HF_BRANCH = os.getenv('HF_BRANCH', 'main')
11
+ PROXY_DOMAIN = os.getenv('PROXY_DOMAIN', 'huggingface.co')
12
+
13
+ # 系统配置
14
+ REQUIRE_LOGIN = os.getenv('REQUIRE_LOGIN', 'true').lower() == 'true'
15
+ ACCESS_PASSWORD = os.getenv('ACCESS_PASSWORD')
16
+ SECRET_KEY = os.getenv('SECRET_KEY')
17
+
18
+ # MySQL配置
19
+ MYSQL_CONFIG = {
20
+ 'host': os.getenv('MYSQL_HOST', 'localhost'),
21
+ 'port': int(os.getenv('MYSQL_PORT', 3306)),
22
+ 'user': os.getenv('MYSQL_USER'),
23
+ 'password': os.getenv('MYSQL_PASSWORD'),
24
+ 'database': os.getenv('MYSQL_DATABASE')
25
+ }
models.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pymysql
2
+ from config import Config
3
+
4
+ class Database:
5
+ def __init__(self):
6
+ self.conn = pymysql.connect(
7
+ host=Config.MYSQL_CONFIG['host'],
8
+ port=Config.MYSQL_CONFIG['port'],
9
+ user=Config.MYSQL_CONFIG['user'],
10
+ password=Config.MYSQL_CONFIG['password'],
11
+ charset='utf8mb4',
12
+ cursorclass=pymysql.cursors.DictCursor
13
+ )
14
+ self.init_db()
15
+
16
+ def init_db(self):
17
+ with self.conn.cursor() as cursor:
18
+ # 创建数据库
19
+ cursor.execute(f"CREATE DATABASE IF NOT EXISTS {Config.MYSQL_CONFIG['database']}")
20
+ cursor.execute(f"USE {Config.MYSQL_CONFIG['database']}")
21
+
22
+ # 创建文件表
23
+ cursor.execute("""
24
+ CREATE TABLE IF NOT EXISTS files (
25
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
26
+ original_name VARCHAR(255) NOT NULL,
27
+ stored_name VARCHAR(255) NOT NULL,
28
+ file_path VARCHAR(500) NOT NULL,
29
+ file_type VARCHAR(50),
30
+ file_size BIGINT,
31
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
32
+ UNIQUE KEY idx_path (file_path)
33
+ ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
34
+ """)
35
+ self.conn.commit()
36
+
37
+ def search_files(self, keyword):
38
+ with self.conn.cursor() as cursor:
39
+ cursor.execute("""
40
+ SELECT * FROM files
41
+ WHERE original_name LIKE %s
42
+ OR file_path LIKE %s
43
+ ORDER BY created_at DESC
44
+ """, (f"%{keyword}%", f"%{keyword}%"))
45
+ return cursor.fetchall()
utils.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ def get_file_type(filename):
4
+ """根据文件扩展名判断文件类型"""
5
+ ext = os.path.splitext(filename)[1].lower()
6
+
7
+ # 文件类型映射
8
+ type_map = {
9
+ 'image': ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'],
10
+ 'video': ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'],
11
+ 'document': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt'],
12
+ 'audio': ['.mp3', '.wav', '.ogg', '.m4a'],
13
+ 'archive': ['.zip', '.rar', '.7z', '.tar', '.gz'],
14
+ 'code': ['.py', '.js', '.html', '.css', '.json', '.xml']
15
+ }
16
+
17
+ for file_type, extensions in type_map.items():
18
+ if ext in extensions:
19
+ return file_type
20
+ return 'other'
21
+
22
+ def format_file_size(size):
23
+ """格式化文件大小"""
24
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
25
+ if size < 1024:
26
+ return f"{size:.2f} {unit}"
27
+ size /= 1024
28
+ return f"{size:.2f} PB"