WACATW commited on
Commit
05c2deb
·
verified ·
1 Parent(s): 2e99c73

Upload 10 files

Browse files
public/cloud-disk/.env.example ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 应用配置
2
+ SECRET_KEY=your-secret-key-here
3
+
4
+ # 存储配置
5
+ # Hugging Face Storage Buckets 路径(推荐,数据持久化)
6
+ STORAGE_PATH=/data
7
+ # 本地存储路径(重启后数据会丢失)
8
+ # STORAGE_PATH=./storage
9
+
10
+ # 服务配置
11
+ PORT=7860
public/cloud-disk/Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy application
10
+ COPY . .
11
+
12
+ # Create storage directory
13
+ RUN mkdir -p /app/storage
14
+
15
+ # Environment variables
16
+ ENV PORT=7860
17
+ ENV USE_LOCAL_STORAGE=false
18
+
19
+ # Expose port
20
+ EXPOSE 7860
21
+
22
+ # Run the application
23
+ CMD ["python", "app.py"]
public/cloud-disk/README.md ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: 云盘 - Cloud Disk
3
+ emoji: 💾
4
+ colorFrom: purple
5
+ colorTo: pink
6
+ sdk: docker
7
+ app_file: app.py
8
+ pinned: false
9
+ ---
10
+
11
+ # 云盘 - Cloud Disk v2.0
12
+
13
+ 一个轻量级的个人云盘应用,支持用户注册登录、独立存储空间、文件上传下载、文件夹管理等功能。
14
+
15
+ ## ✨ 功能特性
16
+
17
+ ### v2.0 新功能
18
+ - 👤 **用户系统**:支持注册、登录、退出
19
+ - 🔒 **空间隔离**:每个用户独立存储空间,数据安全隔离
20
+ - 💾 **存储配额**:每个用户 1GB 存储空间
21
+ - 📊 **实时显示**:存储空间使用情况实时展示
22
+
23
+ ### 核心功能
24
+ - 📁 **文件管理**:上传、下载、删除文件
25
+ - 📂 **文件夹管理**:创建、浏览、删除文件夹
26
+ - 🎨 **现代化界面**:美观的 UI 设计,支持响应式布局
27
+ - 🖱️ **拖拽上传**:支持拖拽文件到上传区域
28
+ - 🔐 **密码安全**:密码哈希存储,保护用户隐私
29
+
30
+ ## 🚀 部署到 Hugging Face Spaces
31
+
32
+ ### Docker 部署(推荐)
33
+
34
+ 1. 在 Hugging Face 创建一个新的 Space,选择 **Docker** 作为 SDK
35
+ 2. 将本项目的所有文件上传到 Space 仓库
36
+ 3. 等待部署完成后即可访问
37
+
38
+ > 💡 **注意**:v2.0 版本默认使用本地存储,无需配置 HF_TOKEN 和 DATASET_REPO。但 Hugging Face Spaces 重启后数据可能会丢失。
39
+
40
+ ### 📦 使用 Storage Buckets 持久化存储(推荐)
41
+
42
+ 为了防止数据丢失,建议使用 Hugging Face 的 Storage Buckets 功能:
43
+
44
+ 1. 在你的 Space 设置中找到 **Storage Buckets** 部分
45
+ 2. 点击 **Mount a bucket** 挂载一个存储桶
46
+ 3. 挂载路径设置为 `/data`
47
+ 4. 重新部署 Space,数据就会自动保存到存储桶中
48
+
49
+ > ✅ 使用 Storage Buckets 后,即使 Space 重启,数据也不会丢失!
50
+
51
+ ### 本地运行
52
+
53
+ ```bash
54
+ # 安装依赖
55
+ pip install -r requirements.txt
56
+
57
+ # 运行
58
+ python app.py
59
+ ```
60
+
61
+ 访问 http://localhost:7860 即可使用。
62
+
63
+ ## ⚙️ 环境变量配置
64
+
65
+ | 变量名 | 说明 | 默认值 |
66
+ |--------|------|--------|
67
+ | `SECRET_KEY` | Session 加密密钥 | cloud-disk-secret-key-2024 |
68
+ | `STORAGE_PATH` | 存储文件路径(Storage Buckets 设为 `/data`) | /data |
69
+ | `PORT` | 服务端口 | 7860 |
70
+
71
+ ## 📝 使用说明
72
+
73
+ ### 注册账号
74
+ 1. 打开应用后点击"立即注册"
75
+ 2. 输入用户名(3-20位,支持字母、数字、下划线、中文)
76
+ 3. 设置密码(至少6位)并确认
77
+ 4. 点击"注册账号"完成注册
78
+
79
+ ### 登录账号
80
+ 1. 输入用户名和密码
81
+ 2. 点击"登录"进入云盘
82
+
83
+ ### 上传文件
84
+ - 点击"上传文件"按钮选择文件
85
+ - 或直接拖拽文件到上传区域
86
+ - 支持批量上传多个文件
87
+ - 上传前会检查剩余存储空间
88
+
89
+ ### 创建文件夹
90
+ - 点击"新建文件夹"按钮
91
+ - 输入文件夹名称并确认
92
+
93
+ ### 浏览文件
94
+ - 点击文件夹进入
95
+ - 使用面包屑导航返回上级目录
96
+ - 点击刷新按钮重新加载文件列表
97
+
98
+ ### 下载/删除文件
99
+ - 点击下载图标下载文件
100
+ - 点击删除图标删除文件或文件夹(删除前会确认)
101
+
102
+ ### 查看存储空间
103
+ - 顶部显示存储空间使用进度条
104
+ - 上传、删除文件后会自动更新
105
+
106
+ ### 退出登录
107
+ - 点击右上角的退出图标
108
+ - 确认后返回登录页面
109
+
110
+ ## 🏗️ 技术栈
111
+
112
+ - **后端**:Flask (Python)
113
+ - **前端**:HTML + CSS + JavaScript + Font Awesome
114
+ - **用户认证**:Werkzeug Security + Flask Session
115
+ - **存储**:本地文件系统
116
+ - **部署**:Docker / Hugging Face Spaces
117
+
118
+ ## 📁 项目结构
119
+
120
+ ```
121
+ cloud-disk/
122
+ ├── app.py # Flask 后端应用
123
+ ├── requirements.txt # Python 依赖
124
+ ├── Dockerfile # Docker 部署配置
125
+ ├── README.md # 说明文档
126
+ ├── .env.example # 环境变量示例
127
+ ├── storage/ # 存储目录(运行时自动创建)
128
+ │ ├── users.json # 用户数据文件
129
+ │ └── users/ # 用户存储目录
130
+ │ └── {username}/ # 每个用户独立目录
131
+ ├── templates/
132
+ │ ├── index.html # 主页面
133
+ │ ├── login.html # 登录页面
134
+ │ └── register.html # 注册页面
135
+ └── static/
136
+ ├── css/style.css # 样式文件
137
+ └── js/app.js # 前端交互逻辑
138
+ ```
139
+
140
+ ## 🔒 安全特性
141
+
142
+ - **密码哈希**:使用 werkzeug.security 进行密码哈希存储
143
+ - **路径安全**:防止路径遍历攻击,用户只能访问自己的文件
144
+ - **空间隔离**:每个用户独立存储目录,数据互不干扰
145
+ - **配额限制**:每个用户最多 1GB 存储空间,防止滥用
146
+
147
+ ## ⚠️ 注意事项
148
+
149
+ 1. **数据持久化**:建议使用 Storage Buckets 存储(路径 `/data`),这样 Space 重启后数据不会丢失
150
+ 2. **并发限制**:单用户演示用途,高并发场景可能需要优化
151
+ 3. **存储配额**:每个用户 1GB 空间,超出后无法上传新文件
152
+
153
+ ## 📄 License
154
+
155
+ MIT License
public/cloud-disk/app.py ADDED
@@ -0,0 +1,561 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import json
4
+ import mimetypes
5
+ from datetime import datetime
6
+ from flask import Flask, render_template, request, jsonify, send_file, abort, session, redirect, url_for
7
+ from werkzeug.security import generate_password_hash, check_password_hash
8
+ import re
9
+
10
+ def safe_filename(filename):
11
+ """安全的文件名处理,支持中文"""
12
+ if not filename:
13
+ return 'unnamed'
14
+
15
+ # 移除路径分隔符和危险字符
16
+ filename = re.sub(r'[\\/:*?"<>|\x00-\x1f]', '_', filename)
17
+ # 移除开头的点和空格
18
+ filename = filename.lstrip('. ')
19
+ # 确保文件名不为空
20
+ if not filename:
21
+ filename = 'unnamed'
22
+ return filename
23
+
24
+ def safe_username(username):
25
+ """安全的用户名处理"""
26
+ if not username:
27
+ return None
28
+ # 只允许字母、数字、下划线、中文
29
+ if not re.match(r'^[a-zA-Z0-9_\u4e00-\u9fa5]{3,20}$', username):
30
+ return None
31
+ return username
32
+
33
+ app = Flask(__name__)
34
+ app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB max upload
35
+ app.secret_key = os.environ.get('SECRET_KEY', 'cloud-disk-secret-key-2024')
36
+
37
+ # Session cookie 配置(修复移动端登录跳转问题)
38
+ app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
39
+ app.config['SESSION_COOKIE_HTTPONLY'] = True
40
+ app.config['SESSION_PERMANENT'] = True
41
+ app.config['PERMANENT_SESSION_LIFETIME'] = 30 * 24 * 60 * 60 # 30天
42
+
43
+ # 如果是 HTTPS 环境,取消下面两行的注释
44
+ # app.config['SESSION_COOKIE_SAMESITE'] = 'None'
45
+ # app.config['SESSION_COOKIE_SECURE'] = True
46
+
47
+ # Configuration
48
+ STORAGE_BASE_PATH = os.environ.get('STORAGE_PATH', '/data')
49
+ USERS_FILE = os.path.join(STORAGE_BASE_PATH, 'users.json')
50
+ MAX_STORAGE_PER_USER = 1 * 1024 * 1024 * 1024 # 1GB per user
51
+
52
+ # Ensure storage directory exists
53
+ os.makedirs(STORAGE_BASE_PATH, exist_ok=True)
54
+
55
+ # Initialize users file
56
+ if not os.path.exists(USERS_FILE):
57
+ with open(USERS_FILE, 'w', encoding='utf-8') as f:
58
+ json.dump({}, f)
59
+
60
+ def load_users():
61
+ """加载用户数据"""
62
+ try:
63
+ with open(USERS_FILE, 'r', encoding='utf-8') as f:
64
+ return json.load(f)
65
+ except:
66
+ return {}
67
+
68
+ def save_users(users):
69
+ """保存用户数据"""
70
+ with open(USERS_FILE, 'w', encoding='utf-8') as f:
71
+ json.dump(users, f, ensure_ascii=False, indent=2)
72
+
73
+ def get_user_storage_path(username):
74
+ """获取用户的存储目录"""
75
+ return os.path.join(STORAGE_BASE_PATH, 'users', username)
76
+
77
+ def ensure_user_storage(username):
78
+ """确保用户存储目录存在"""
79
+ path = get_user_storage_path(username)
80
+ os.makedirs(path, exist_ok=True)
81
+ return path
82
+
83
+ def calculate_user_storage(username):
84
+ """计算用户已使用的存储空间"""
85
+ user_path = get_user_storage_path(username)
86
+ if not os.path.exists(user_path):
87
+ return 0
88
+
89
+ total_size = 0
90
+ for dirpath, dirnames, filenames in os.walk(user_path):
91
+ for filename in filenames:
92
+ filepath = os.path.join(dirpath, filename)
93
+ try:
94
+ total_size += os.path.getsize(filepath)
95
+ except:
96
+ pass
97
+ return total_size
98
+
99
+ def get_current_user():
100
+ """获取当前登录用户"""
101
+ return session.get('username')
102
+
103
+ def login_required_api():
104
+ """API 登录检查装饰器辅助函数"""
105
+ username = get_current_user()
106
+ if not username:
107
+ return False, jsonify({'success': False, 'error': '请先登录', 'need_login': True})
108
+ return True, username
109
+
110
+ def get_full_path(username, path=''):
111
+ """获取用户目录下的完整路径"""
112
+ user_path = get_user_storage_path(username)
113
+ if path and path != '/':
114
+ # 规范化路径,防止路径遍历
115
+ path = path.strip('/')
116
+ full_path = os.path.normpath(os.path.join(user_path, path))
117
+ # 确保路径在用户目录内
118
+ if not full_path.startswith(os.path.normpath(user_path)):
119
+ return None
120
+ return full_path
121
+ return user_path
122
+
123
+ def list_files(username, path=''):
124
+ """列出用户目录下的文件"""
125
+ full_path = get_full_path(username, path)
126
+ if not full_path or not os.path.exists(full_path):
127
+ return []
128
+
129
+ result = []
130
+ try:
131
+ for item in os.listdir(full_path):
132
+ item_path = os.path.join(full_path, item)
133
+ is_dir = os.path.isdir(item_path)
134
+ stat = os.stat(item_path)
135
+
136
+ result.append({
137
+ 'name': item,
138
+ 'type': 'directory' if is_dir else 'file',
139
+ 'size': stat.st_size if not is_dir else 0,
140
+ 'modified': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
141
+ })
142
+
143
+ # Sort: directories first, then files
144
+ result.sort(key=lambda x: (x['type'] != 'directory', x['name'].lower()))
145
+ return result
146
+ except Exception as e:
147
+ print(f"Error listing files: {e}")
148
+ return []
149
+
150
+ def upload_file(username, file_storage, path=''):
151
+ """上��文件到用户目录"""
152
+ # 检查配额
153
+ used_storage = calculate_user_storage(username)
154
+ file_content = file_storage.read()
155
+ file_size = len(file_content)
156
+
157
+ if used_storage + file_size > MAX_STORAGE_PER_USER:
158
+ return False, f"存储空间不足,您已使用 {format_size(used_storage)},还剩 {format_size(MAX_STORAGE_PER_USER - used_storage)}"
159
+
160
+ full_path = get_full_path(username, path)
161
+ if not full_path:
162
+ return False, "路径无效"
163
+
164
+ try:
165
+ filename = safe_filename(file_storage.filename)
166
+ file_path = os.path.join(full_path, filename)
167
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
168
+
169
+ with open(file_path, 'wb') as f:
170
+ f.write(file_content)
171
+
172
+ return True, "上传成功"
173
+ except Exception as e:
174
+ print(f"Error uploading file: {e}")
175
+ return False, str(e)
176
+
177
+ def download_file(username, filename, path=''):
178
+ """下载用户文件"""
179
+ full_path = get_full_path(username, path)
180
+ if not full_path:
181
+ return None
182
+
183
+ file_path = os.path.join(full_path, filename)
184
+ if os.path.exists(file_path) and os.path.isfile(file_path):
185
+ return file_path
186
+ return None
187
+
188
+ def create_directory(username, dirname, path=''):
189
+ """创建文件夹"""
190
+ full_path = get_full_path(username, path)
191
+ if not full_path:
192
+ return False, "路径无效"
193
+
194
+ try:
195
+ dirname = safe_filename(dirname)
196
+ dir_path = os.path.join(full_path, dirname)
197
+ os.makedirs(dir_path, exist_ok=True)
198
+ return True, "文件夹创建成功"
199
+ except Exception as e:
200
+ print(f"Error creating directory: {e}")
201
+ return False, str(e)
202
+
203
+ def delete_item(username, filename, path=''):
204
+ """删除文件或文件夹"""
205
+ full_path = get_full_path(username, path)
206
+ if not full_path:
207
+ return False, "路径无效"
208
+
209
+ try:
210
+ item_path = os.path.join(full_path, filename)
211
+ if os.path.isdir(item_path):
212
+ import shutil
213
+ shutil.rmtree(item_path)
214
+ else:
215
+ os.remove(item_path)
216
+ return True, "删除成功"
217
+ except Exception as e:
218
+ print(f"Error deleting item: {e}")
219
+ return False, str(e)
220
+
221
+ def format_size(size_bytes):
222
+ """格式化文件大小"""
223
+ if size_bytes == 0 or size_bytes == '-':
224
+ return '0 B'
225
+ try:
226
+ size_bytes = int(size_bytes)
227
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
228
+ if size_bytes < 1024.0:
229
+ return f"{size_bytes:.2f} {unit}"
230
+ size_bytes /= 1024.0
231
+ return f"{size_bytes:.2f} PB"
232
+ except:
233
+ return '-'
234
+
235
+ # Template filter
236
+ app.jinja_env.filters['format_size'] = format_size
237
+
238
+ @app.route('/')
239
+ def index():
240
+ """主页面"""
241
+ username = get_current_user()
242
+ if not username:
243
+ return redirect(url_for('login_page'))
244
+
245
+ current_path = request.args.get('path', '')
246
+ files = list_files(username, current_path)
247
+
248
+ # Build breadcrumb
249
+ breadcrumb = []
250
+ if current_path:
251
+ parts = current_path.strip('/').split('/')
252
+ acc = ''
253
+ for part in parts:
254
+ acc += '/' + part if acc else part
255
+ breadcrumb.append({'name': part, 'path': acc})
256
+
257
+ # Calculate storage
258
+ used_storage = calculate_user_storage(username)
259
+ storage_percent = (used_storage / MAX_STORAGE_PER_USER) * 100
260
+
261
+ return render_template('index.html',
262
+ username=username,
263
+ files=files,
264
+ current_path=current_path,
265
+ breadcrumb=breadcrumb,
266
+ used_storage=used_storage,
267
+ max_storage=MAX_STORAGE_PER_USER,
268
+ storage_percent=storage_percent,
269
+ used_storage_formatted=format_size(used_storage),
270
+ max_storage_formatted=format_size(MAX_STORAGE_PER_USER))
271
+
272
+ @app.route('/login', methods=['GET', 'POST'])
273
+ def login_page():
274
+ """登录页面(支持表单提交)"""
275
+ if get_current_user():
276
+ return redirect(url_for('index'))
277
+
278
+ error = None
279
+
280
+ if request.method == 'POST':
281
+ # 表单提交方式
282
+ username = request.form.get('username', '').strip()
283
+ password = request.form.get('password', '')
284
+
285
+ if not username or not password:
286
+ error = '请输入用户名和密码'
287
+ else:
288
+ users = load_users()
289
+ user = users.get(username)
290
+
291
+ if not user or not check_password_hash(user['password_hash'], password):
292
+ error = '用户名或密码错误'
293
+ else:
294
+ # 登录成功
295
+ ensure_user_storage(username)
296
+ session['username'] = username
297
+ session.permanent = True
298
+ return redirect(url_for('index'))
299
+
300
+ return render_template('login.html', error=error)
301
+
302
+ @app.route('/register', methods=['GET', 'POST'])
303
+ def register_page():
304
+ """注册页面(支持表单提交)"""
305
+ if get_current_user():
306
+ return redirect(url_for('index'))
307
+
308
+ error = None
309
+
310
+ if request.method == 'POST':
311
+ # 表单提交方式
312
+ username = request.form.get('username', '').strip()
313
+ password = request.form.get('password', '')
314
+ confirm_password = request.form.get('confirm_password', '')
315
+
316
+ # 验证用户名
317
+ if not safe_username(username):
318
+ error = '用户名格式不正确,3-20位字母、数字、下划线或中文'
319
+ # 验证密码
320
+ elif len(password) < 6:
321
+ error = '密码至少6位'
322
+ elif password != confirm_password:
323
+ error = '两次密码不一致'
324
+ else:
325
+ # 检查用户是否存在
326
+ users = load_users()
327
+ if username in users:
328
+ error = '用户名已存在'
329
+ else:
330
+ # 创建用户
331
+ users[username] = {
332
+ 'password_hash': generate_password_hash(password),
333
+ 'created_at': datetime.now().isoformat(),
334
+ 'max_storage': MAX_STORAGE_PER_USER
335
+ }
336
+ save_users(users)
337
+
338
+ # 创建用户目录
339
+ ensure_user_storage(username)
340
+
341
+ # 自动登录
342
+ session['username'] = username
343
+ session.permanent = True
344
+ return redirect(url_for('index'))
345
+
346
+ return render_template('register.html', error=error)
347
+
348
+ # ========== Auth APIs ==========
349
+
350
+ @app.route('/api/register', methods=['POST'])
351
+ def api_register():
352
+ """注册 API"""
353
+ data = request.get_json()
354
+ username = data.get('username', '').strip()
355
+ password = data.get('password', '')
356
+ confirm_password = data.get('confirm_password', '')
357
+
358
+ # 验证用户名
359
+ if not safe_username(username):
360
+ return jsonify({'success': False, 'error': '用户名格式不正确,3-20位字母、数字、下划线或中文'})
361
+
362
+ # 验证密码
363
+ if len(password) < 6:
364
+ return jsonify({'success': False, 'error': '密码至少6位'})
365
+
366
+ if password != confirm_password:
367
+ return jsonify({'success': False, 'error': '两次密码不一致'})
368
+
369
+ # 检查用户是否存在
370
+ users = load_users()
371
+ if username in users:
372
+ return jsonify({'success': False, 'error': '用户名已存在'})
373
+
374
+ # 创建用户
375
+ users[username] = {
376
+ 'password_hash': generate_password_hash(password),
377
+ 'created_at': datetime.now().isoformat(),
378
+ 'max_storage': MAX_STORAGE_PER_USER
379
+ }
380
+ save_users(users)
381
+
382
+ # 创建用户目录
383
+ ensure_user_storage(username)
384
+
385
+ # 自动登录
386
+ session['username'] = username
387
+
388
+ return jsonify({'success': True, 'message': '注册成功', 'username': username})
389
+
390
+ @app.route('/api/login', methods=['POST'])
391
+ def api_login():
392
+ """登录 API"""
393
+ data = request.get_json()
394
+ username = data.get('username', '').strip()
395
+ password = data.get('password', '')
396
+
397
+ if not username or not password:
398
+ return jsonify({'success': False, 'error': '请输入用户名和密码'})
399
+
400
+ users = load_users()
401
+ user = users.get(username)
402
+
403
+ if not user or not check_password_hash(user['password_hash'], password):
404
+ return jsonify({'success': False, 'error': '用户名或密码错误'})
405
+
406
+ # 确保用户目录存在
407
+ ensure_user_storage(username)
408
+
409
+ session['username'] = username
410
+ return jsonify({'success': True, 'message': '登录成功', 'username': username})
411
+
412
+ @app.route('/api/logout', methods=['POST'])
413
+ def api_logout():
414
+ """退出登录 API"""
415
+ session.pop('username', None)
416
+ return jsonify({'success': True, 'message': '已退出登录'})
417
+
418
+ @app.route('/api/user-info')
419
+ def api_user_info():
420
+ """获取用户信息 API"""
421
+ is_logged_in, result = login_required_api()
422
+ if not is_logged_in:
423
+ return result
424
+
425
+ username = result
426
+ used_storage = calculate_user_storage(username)
427
+
428
+ return jsonify({
429
+ 'success': True,
430
+ 'username': username,
431
+ 'used_storage': used_storage,
432
+ 'max_storage': MAX_STORAGE_PER_USER,
433
+ 'used_storage_formatted': format_size(used_storage),
434
+ 'max_storage_formatted': format_size(MAX_STORAGE_PER_USER),
435
+ 'storage_percent': (used_storage / MAX_STORAGE_PER_USER) * 100
436
+ })
437
+
438
+ # ========== File APIs ==========
439
+
440
+ @app.route('/api/list')
441
+ def api_list():
442
+ """API: 列出文件"""
443
+ is_logged_in, result = login_required_api()
444
+ if not is_logged_in:
445
+ return result
446
+
447
+ username = result
448
+ path = request.args.get('path', '')
449
+ files = list_files(username, path)
450
+ return jsonify({'success': True, 'files': files, 'path': path})
451
+
452
+ @app.route('/api/upload', methods=['POST'])
453
+ def api_upload():
454
+ """API: 上传文件"""
455
+ is_logged_in, result = login_required_api()
456
+ if not is_logged_in:
457
+ return result
458
+
459
+ username = result
460
+
461
+ if 'file' not in request.files:
462
+ return jsonify({'success': False, 'error': '没有选择文件'})
463
+
464
+ file = request.files['file']
465
+ if file.filename == '':
466
+ return jsonify({'success': False, 'error': '没有选择文件'})
467
+
468
+ path = request.form.get('path', '')
469
+ success, message = upload_file(username, file, path)
470
+
471
+ return jsonify({'success': success, 'message': message})
472
+
473
+ @app.route('/api/download')
474
+ def api_download():
475
+ """API: 下载文件"""
476
+ is_logged_in, result = login_required_api()
477
+ if not is_logged_in:
478
+ return redirect(url_for('login_page'))
479
+
480
+ username = result
481
+ filename = request.args.get('filename', '')
482
+ path = request.args.get('path', '')
483
+
484
+ if not filename:
485
+ abort(400)
486
+
487
+ file_path = download_file(username, filename, path)
488
+ if not file_path:
489
+ abort(404)
490
+
491
+ # Determine MIME type
492
+ mime_type, _ = mimetypes.guess_type(filename)
493
+ if not mime_type:
494
+ mime_type = 'application/octet-stream'
495
+
496
+ return send_file(
497
+ file_path,
498
+ mimetype=mime_type,
499
+ as_attachment=True,
500
+ download_name=filename
501
+ )
502
+
503
+ @app.route('/api/mkdir', methods=['POST'])
504
+ def api_mkdir():
505
+ """API: 创建文件夹"""
506
+ is_logged_in, result = login_required_api()
507
+ if not is_logged_in:
508
+ return result
509
+
510
+ username = result
511
+ data = request.get_json()
512
+ dirname = data.get('dirname', '')
513
+ path = data.get('path', '')
514
+
515
+ if not dirname:
516
+ return jsonify({'success': False, 'error': '请输入文件夹名称'})
517
+
518
+ success, message = create_directory(username, dirname, path)
519
+ return jsonify({'success': success, 'message': message})
520
+
521
+ @app.route('/api/delete', methods=['POST'])
522
+ def api_delete():
523
+ """API: 删除文件/文件夹"""
524
+ is_logged_in, result = login_required_api()
525
+ if not is_logged_in:
526
+ return result
527
+
528
+ username = result
529
+ data = request.get_json()
530
+ filename = data.get('filename', '')
531
+ path = data.get('path', '')
532
+
533
+ if not filename:
534
+ return jsonify({'success': False, 'error': '请选择要删除的文件'})
535
+
536
+ success, message = delete_item(username, filename, path)
537
+ return jsonify({'success': success, 'message': message})
538
+
539
+ @app.route('/api/storage-info')
540
+ def api_storage_info():
541
+ """API: 获取存储信息"""
542
+ is_logged_in, result = login_required_api()
543
+ if not is_logged_in:
544
+ return result
545
+
546
+ username = result
547
+ used_storage = calculate_user_storage(username)
548
+
549
+ return jsonify({
550
+ 'success': True,
551
+ 'storage_type': 'local',
552
+ 'used_storage': used_storage,
553
+ 'max_storage': MAX_STORAGE_PER_USER,
554
+ 'used_storage_formatted': format_size(used_storage),
555
+ 'max_storage_formatted': format_size(MAX_STORAGE_PER_USER),
556
+ 'storage_percent': (used_storage / MAX_STORAGE_PER_USER) * 100
557
+ })
558
+
559
+ if __name__ == '__main__':
560
+ port = int(os.environ.get('PORT', 7860))
561
+ app.run(host='0.0.0.0', port=port, debug=False)
public/cloud-disk/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Flask==3.0.0
2
+ python-dotenv==1.0.0
3
+ Werkzeug==3.0.1
public/cloud-disk/static/css/style.css ADDED
@@ -0,0 +1,853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ color: #333;
12
+ }
13
+
14
+ .container {
15
+ max-width: 1200px;
16
+ margin: 0 auto;
17
+ padding: 20px;
18
+ min-height: 100vh;
19
+ }
20
+
21
+ /* Header */
22
+ .header {
23
+ background: white;
24
+ border-radius: 16px;
25
+ padding: 20px 24px;
26
+ display: flex;
27
+ justify-content: space-between;
28
+ align-items: center;
29
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
30
+ margin-bottom: 20px;
31
+ }
32
+
33
+ .header-left {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 12px;
37
+ }
38
+
39
+ .header-icon {
40
+ font-size: 32px;
41
+ color: #667eea;
42
+ }
43
+
44
+ .header-title {
45
+ font-size: 24px;
46
+ font-weight: 700;
47
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
48
+ -webkit-background-clip: text;
49
+ -webkit-text-fill-color: transparent;
50
+ background-clip: text;
51
+ }
52
+
53
+ .storage-badge {
54
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
55
+ color: white;
56
+ padding: 6px 12px;
57
+ border-radius: 20px;
58
+ font-size: 12px;
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 6px;
62
+ }
63
+
64
+ .header-right {
65
+ display: flex;
66
+ gap: 12px;
67
+ align-items: center;
68
+ }
69
+
70
+ /* Buttons */
71
+ .btn {
72
+ padding: 10px 20px;
73
+ border: none;
74
+ border-radius: 10px;
75
+ font-size: 14px;
76
+ font-weight: 500;
77
+ cursor: pointer;
78
+ transition: all 0.3s ease;
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 8px;
82
+ }
83
+
84
+ .btn:hover {
85
+ transform: translateY(-2px);
86
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
87
+ }
88
+
89
+ .btn:active {
90
+ transform: translateY(0);
91
+ }
92
+
93
+ .btn-primary {
94
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
95
+ color: white;
96
+ }
97
+
98
+ .btn-secondary {
99
+ background: #f0f0f0;
100
+ color: #333;
101
+ }
102
+
103
+ .btn-secondary:hover {
104
+ background: #e0e0e0;
105
+ }
106
+
107
+ .btn-danger {
108
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
109
+ color: white;
110
+ }
111
+
112
+ .btn-icon {
113
+ padding: 10px;
114
+ background: #f0f0f0;
115
+ color: #666;
116
+ border-radius: 10px;
117
+ }
118
+
119
+ .btn-icon:hover {
120
+ background: #e0e0e0;
121
+ color: #333;
122
+ }
123
+
124
+ /* Breadcrumb */
125
+ .breadcrumb {
126
+ background: white;
127
+ border-radius: 12px;
128
+ padding: 14px 20px;
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 8px;
132
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
133
+ margin-bottom: 20px;
134
+ flex-wrap: wrap;
135
+ }
136
+
137
+ .breadcrumb-item {
138
+ color: #667eea;
139
+ text-decoration: none;
140
+ font-size: 14px;
141
+ font-weight: 500;
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 6px;
145
+ transition: color 0.3s;
146
+ }
147
+
148
+ .breadcrumb-item:hover {
149
+ color: #764ba2;
150
+ }
151
+
152
+ .breadcrumb-separator {
153
+ color: #ccc;
154
+ font-size: 12px;
155
+ }
156
+
157
+ /* Upload Zone */
158
+ .upload-zone {
159
+ background: white;
160
+ border: 2px dashed #d0d0d0;
161
+ border-radius: 16px;
162
+ padding: 40px;
163
+ text-align: center;
164
+ margin-bottom: 20px;
165
+ transition: all 0.3s ease;
166
+ cursor: pointer;
167
+ }
168
+
169
+ .upload-zone:hover,
170
+ .upload-zone.drag-over {
171
+ border-color: #667eea;
172
+ background: #f8f9ff;
173
+ }
174
+
175
+ .upload-zone-content {
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: center;
179
+ gap: 10px;
180
+ }
181
+
182
+ .upload-icon {
183
+ font-size: 48px;
184
+ color: #667eea;
185
+ margin-bottom: 10px;
186
+ }
187
+
188
+ .upload-text {
189
+ font-size: 16px;
190
+ font-weight: 500;
191
+ color: #333;
192
+ }
193
+
194
+ .upload-subtext {
195
+ font-size: 13px;
196
+ color: #999;
197
+ }
198
+
199
+ /* File List */
200
+ .file-list-container {
201
+ background: white;
202
+ border-radius: 16px;
203
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
204
+ overflow: hidden;
205
+ }
206
+
207
+ .file-table {
208
+ width: 100%;
209
+ border-collapse: collapse;
210
+ }
211
+
212
+ .file-table th {
213
+ background: #f8f9fa;
214
+ padding: 14px 20px;
215
+ text-align: left;
216
+ font-size: 13px;
217
+ font-weight: 600;
218
+ color: #666;
219
+ border-bottom: 1px solid #eee;
220
+ }
221
+
222
+ .file-table td {
223
+ padding: 14px 20px;
224
+ border-bottom: 1px solid #f5f5f5;
225
+ font-size: 14px;
226
+ }
227
+
228
+ .file-item:hover {
229
+ background: #f8f9ff;
230
+ }
231
+
232
+ .file-item:last-child td {
233
+ border-bottom: none;
234
+ }
235
+
236
+ .file-name-cell {
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 12px;
240
+ }
241
+
242
+ .folder-icon {
243
+ color: #ffc107;
244
+ font-size: 20px;
245
+ }
246
+
247
+ .file-icon {
248
+ color: #667eea;
249
+ font-size: 20px;
250
+ }
251
+
252
+ .file-name {
253
+ color: #333;
254
+ text-decoration: none;
255
+ font-weight: 500;
256
+ }
257
+
258
+ .file-name:hover {
259
+ color: #667eea;
260
+ }
261
+
262
+ .file-size {
263
+ color: #999;
264
+ font-size: 13px;
265
+ }
266
+
267
+ .file-modified {
268
+ color: #999;
269
+ font-size: 13px;
270
+ }
271
+
272
+ .file-actions {
273
+ display: flex;
274
+ gap: 8px;
275
+ }
276
+
277
+ .action-btn {
278
+ padding: 8px;
279
+ border: none;
280
+ background: transparent;
281
+ color: #999;
282
+ cursor: pointer;
283
+ border-radius: 8px;
284
+ transition: all 0.3s;
285
+ }
286
+
287
+ .action-btn:hover {
288
+ background: #f0f0f0;
289
+ color: #333;
290
+ }
291
+
292
+ .download-btn:hover {
293
+ color: #667eea;
294
+ background: #eef0ff;
295
+ }
296
+
297
+ .delete-btn:hover {
298
+ color: #f5576c;
299
+ background: #ffeef0;
300
+ }
301
+
302
+ /* Empty State */
303
+ .empty-state {
304
+ padding: 80px 20px;
305
+ text-align: center;
306
+ }
307
+
308
+ .empty-icon {
309
+ font-size: 64px;
310
+ color: #ddd;
311
+ margin-bottom: 20px;
312
+ }
313
+
314
+ .empty-text {
315
+ font-size: 18px;
316
+ color: #999;
317
+ margin-bottom: 8px;
318
+ }
319
+
320
+ .empty-subtext {
321
+ font-size: 14px;
322
+ color: #ccc;
323
+ }
324
+
325
+ /* Modal */
326
+ .modal {
327
+ display: none;
328
+ position: fixed;
329
+ top: 0;
330
+ left: 0;
331
+ width: 100%;
332
+ height: 100%;
333
+ background: rgba(0, 0, 0, 0.5);
334
+ z-index: 1000;
335
+ align-items: center;
336
+ justify-content: center;
337
+ }
338
+
339
+ .modal.show {
340
+ display: flex;
341
+ }
342
+
343
+ .modal-content {
344
+ background: white;
345
+ border-radius: 16px;
346
+ width: 90%;
347
+ max-width: 400px;
348
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
349
+ animation: modalSlideIn 0.3s ease;
350
+ }
351
+
352
+ @keyframes modalSlideIn {
353
+ from {
354
+ opacity: 0;
355
+ transform: translateY(-20px);
356
+ }
357
+ to {
358
+ opacity: 1;
359
+ transform: translateY(0);
360
+ }
361
+ }
362
+
363
+ .modal-header {
364
+ padding: 20px 24px;
365
+ border-bottom: 1px solid #eee;
366
+ display: flex;
367
+ justify-content: space-between;
368
+ align-items: center;
369
+ }
370
+
371
+ .modal-header h3 {
372
+ font-size: 18px;
373
+ font-weight: 600;
374
+ }
375
+
376
+ .modal-close {
377
+ background: none;
378
+ border: none;
379
+ font-size: 24px;
380
+ color: #999;
381
+ cursor: pointer;
382
+ transition: color 0.3s;
383
+ }
384
+
385
+ .modal-close:hover {
386
+ color: #333;
387
+ }
388
+
389
+ .modal-body {
390
+ padding: 24px;
391
+ }
392
+
393
+ .modal-footer {
394
+ padding: 16px 24px;
395
+ border-top: 1px solid #eee;
396
+ display: flex;
397
+ justify-content: flex-end;
398
+ gap: 12px;
399
+ }
400
+
401
+ .form-input {
402
+ width: 100%;
403
+ padding: 12px 16px;
404
+ border: 2px solid #e0e0e0;
405
+ border-radius: 10px;
406
+ font-size: 14px;
407
+ transition: border-color 0.3s;
408
+ outline: none;
409
+ }
410
+
411
+ .form-input:focus {
412
+ border-color: #667eea;
413
+ }
414
+
415
+ .text-warning {
416
+ color: #f5576c;
417
+ font-size: 13px;
418
+ margin-top: 8px;
419
+ }
420
+
421
+ /* Upload Progress */
422
+ .upload-progress {
423
+ display: none;
424
+ position: fixed;
425
+ bottom: 20px;
426
+ right: 20px;
427
+ background: white;
428
+ border-radius: 12px;
429
+ padding: 16px 20px;
430
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
431
+ z-index: 100;
432
+ min-width: 300px;
433
+ }
434
+
435
+ .upload-progress.show {
436
+ display: block;
437
+ animation: slideUp 0.3s ease;
438
+ }
439
+
440
+ @keyframes slideUp {
441
+ from {
442
+ opacity: 0;
443
+ transform: translateY(20px);
444
+ }
445
+ to {
446
+ opacity: 1;
447
+ transform: translateY(0);
448
+ }
449
+ }
450
+
451
+ .progress-content {
452
+ display: flex;
453
+ align-items: center;
454
+ gap: 10px;
455
+ margin-bottom: 10px;
456
+ font-size: 14px;
457
+ font-weight: 500;
458
+ }
459
+
460
+ .progress-content i {
461
+ color: #667eea;
462
+ }
463
+
464
+ .progress-bar {
465
+ height: 6px;
466
+ background: #f0f0f0;
467
+ border-radius: 3px;
468
+ overflow: hidden;
469
+ }
470
+
471
+ .progress-fill {
472
+ height: 100%;
473
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
474
+ border-radius: 3px;
475
+ width: 0%;
476
+ transition: width 0.3s ease;
477
+ }
478
+
479
+ /* Toast */
480
+ .toast {
481
+ position: fixed;
482
+ top: 20px;
483
+ right: 20px;
484
+ z-index: 2000;
485
+ transform: translateX(120%);
486
+ transition: transform 0.3s ease;
487
+ }
488
+
489
+ .toast.show {
490
+ transform: translateX(0);
491
+ }
492
+
493
+ .toast-content {
494
+ background: white;
495
+ padding: 14px 20px;
496
+ border-radius: 12px;
497
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
498
+ display: flex;
499
+ align-items: center;
500
+ gap: 10px;
501
+ font-size: 14px;
502
+ font-weight: 500;
503
+ }
504
+
505
+ .toast-icon.success {
506
+ color: #4caf50;
507
+ }
508
+
509
+ .toast-icon.error {
510
+ color: #f5576c;
511
+ }
512
+
513
+ .toast-icon.info {
514
+ color: #667eea;
515
+ }
516
+
517
+ /* Responsive */
518
+ @media (max-width: 768px) {
519
+ .container {
520
+ padding: 10px;
521
+ }
522
+
523
+ .header {
524
+ flex-direction: column;
525
+ gap: 16px;
526
+ padding: 16px;
527
+ }
528
+
529
+ .header-right {
530
+ width: 100%;
531
+ justify-content: center;
532
+ flex-wrap: wrap;
533
+ }
534
+
535
+ .file-table th:nth-child(2),
536
+ .file-table th:nth-child(3),
537
+ .file-table td:nth-child(2),
538
+ .file-table td:nth-child(3) {
539
+ display: none;
540
+ }
541
+
542
+ .upload-zone {
543
+ padding: 30px 20px;
544
+ }
545
+ }
546
+
547
+ /* Auth Pages */
548
+ .auth-body {
549
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
550
+ min-height: 100vh;
551
+ display: flex;
552
+ align-items: center;
553
+ justify-content: center;
554
+ padding: 20px;
555
+ }
556
+
557
+ .auth-container {
558
+ display: flex;
559
+ gap: 40px;
560
+ max-width: 900px;
561
+ width: 100%;
562
+ align-items: center;
563
+ }
564
+
565
+ .auth-card {
566
+ background: white;
567
+ border-radius: 20px;
568
+ padding: 40px;
569
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
570
+ flex: 1;
571
+ max-width: 400px;
572
+ }
573
+
574
+ .auth-header {
575
+ text-align: center;
576
+ margin-bottom: 30px;
577
+ }
578
+
579
+ .auth-icon {
580
+ font-size: 48px;
581
+ color: #667eea;
582
+ margin-bottom: 10px;
583
+ }
584
+
585
+ .auth-header h1 {
586
+ font-size: 28px;
587
+ font-weight: 700;
588
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
589
+ -webkit-background-clip: text;
590
+ -webkit-text-fill-color: transparent;
591
+ background-clip: text;
592
+ margin-bottom: 8px;
593
+ }
594
+
595
+ .auth-subtitle {
596
+ color: #999;
597
+ font-size: 14px;
598
+ }
599
+
600
+ .auth-form {
601
+ display: flex;
602
+ flex-direction: column;
603
+ gap: 20px;
604
+ }
605
+
606
+ .form-group {
607
+ display: flex;
608
+ flex-direction: column;
609
+ gap: 8px;
610
+ }
611
+
612
+ .form-group label {
613
+ font-size: 14px;
614
+ font-weight: 500;
615
+ color: #555;
616
+ display: flex;
617
+ align-items: center;
618
+ gap: 8px;
619
+ }
620
+
621
+ .form-group label i {
622
+ color: #667eea;
623
+ }
624
+
625
+ .form-hint {
626
+ font-size: 12px;
627
+ color: #999;
628
+ margin-top: -4px;
629
+ }
630
+
631
+ .btn-block {
632
+ width: 100%;
633
+ justify-content: center;
634
+ padding: 14px;
635
+ font-size: 15px;
636
+ }
637
+
638
+ .auth-footer {
639
+ text-align: center;
640
+ margin-top: 24px;
641
+ padding-top: 20px;
642
+ border-top: 1px solid #f0f0f0;
643
+ }
644
+
645
+ .auth-footer p {
646
+ font-size: 14px;
647
+ color: #666;
648
+ }
649
+
650
+ .auth-link {
651
+ color: #667eea;
652
+ text-decoration: none;
653
+ font-weight: 500;
654
+ transition: color 0.3s;
655
+ }
656
+
657
+ .auth-link:hover {
658
+ color: #764ba2;
659
+ }
660
+
661
+ .auth-features {
662
+ flex: 1;
663
+ display: flex;
664
+ flex-direction: column;
665
+ gap: 30px;
666
+ color: white;
667
+ }
668
+
669
+ .feature-item {
670
+ display: flex;
671
+ flex-direction: column;
672
+ gap: 8px;
673
+ }
674
+
675
+ .feature-icon {
676
+ font-size: 32px;
677
+ margin-bottom: 4px;
678
+ opacity: 0.9;
679
+ }
680
+
681
+ .feature-item h3 {
682
+ font-size: 18px;
683
+ font-weight: 600;
684
+ }
685
+
686
+ .feature-item p {
687
+ font-size: 14px;
688
+ opacity: 0.85;
689
+ line-height: 1.6;
690
+ }
691
+
692
+ /* User Info in Header */
693
+ .user-info {
694
+ display: flex;
695
+ align-items: center;
696
+ gap: 16px;
697
+ padding-right: 16px;
698
+ border-right: 1px solid #eee;
699
+ margin-right: 4px;
700
+ }
701
+
702
+ .storage-info {
703
+ display: flex;
704
+ flex-direction: column;
705
+ gap: 6px;
706
+ min-width: 120px;
707
+ }
708
+
709
+ .storage-bar {
710
+ height: 6px;
711
+ background: #f0f0f0;
712
+ border-radius: 3px;
713
+ overflow: hidden;
714
+ }
715
+
716
+ .storage-fill {
717
+ height: 100%;
718
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
719
+ border-radius: 3px;
720
+ transition: width 0.3s ease;
721
+ }
722
+
723
+ .storage-text {
724
+ font-size: 12px;
725
+ color: #999;
726
+ text-align: center;
727
+ }
728
+
729
+ .user-profile {
730
+ display: flex;
731
+ align-items: center;
732
+ gap: 8px;
733
+ }
734
+
735
+ .user-avatar {
736
+ font-size: 28px;
737
+ color: #667eea;
738
+ }
739
+
740
+ .username {
741
+ font-size: 14px;
742
+ font-weight: 500;
743
+ color: #333;
744
+ max-width: 100px;
745
+ overflow: hidden;
746
+ text-overflow: ellipsis;
747
+ white-space: nowrap;
748
+ }
749
+
750
+ .logout-btn {
751
+ color: #999;
752
+ }
753
+
754
+ .logout-btn:hover {
755
+ color: #f5576c;
756
+ background: #ffeef0;
757
+ }
758
+
759
+ .header-actions {
760
+ display: flex;
761
+ gap: 12px;
762
+ align-items: center;
763
+ }
764
+
765
+ /* Toast Colors */
766
+ .toast-success .toast-icon {
767
+ color: #4caf50;
768
+ }
769
+
770
+ .toast-error .toast-icon {
771
+ color: #f5576c;
772
+ }
773
+
774
+ .toast-info .toast-icon {
775
+ color: #667eea;
776
+ }
777
+
778
+ /* Responsive for Auth */
779
+ @media (max-width: 768px) {
780
+ .auth-container {
781
+ flex-direction: column;
782
+ gap: 30px;
783
+ }
784
+
785
+ .auth-card {
786
+ max-width: 100%;
787
+ padding: 30px 24px;
788
+ }
789
+
790
+ .auth-features {
791
+ flex-direction: row;
792
+ flex-wrap: wrap;
793
+ justify-content: center;
794
+ gap: 20px;
795
+ text-align: center;
796
+ }
797
+
798
+ .feature-item {
799
+ flex: 1;
800
+ min-width: 120px;
801
+ }
802
+
803
+ .feature-icon {
804
+ font-size: 24px;
805
+ }
806
+
807
+ .feature-item h3 {
808
+ font-size: 14px;
809
+ }
810
+
811
+ .feature-item p {
812
+ font-size: 12px;
813
+ }
814
+
815
+ .user-info {
816
+ flex-direction: column;
817
+ gap: 8px;
818
+ padding-right: 0;
819
+ border-right: none;
820
+ margin-right: 0;
821
+ padding-bottom: 12px;
822
+ border-bottom: 1px solid #eee;
823
+ width: 100%;
824
+ }
825
+
826
+ .storage-info {
827
+ min-width: 100%;
828
+ }
829
+
830
+ .header-actions {
831
+ width: 100%;
832
+ justify-content: center;
833
+ flex-wrap: wrap;
834
+ }
835
+ }
836
+
837
+ /* 错误提示样式 */
838
+ .auth-error {
839
+ background: #fff0f0;
840
+ border: 1px solid #ffcccc;
841
+ color: #f5576c;
842
+ padding: 12px 16px;
843
+ border-radius: 8px;
844
+ margin-bottom: 20px;
845
+ font-size: 14px;
846
+ display: flex;
847
+ align-items: center;
848
+ gap: 8px;
849
+ }
850
+
851
+ .auth-error i {
852
+ font-size: 16px;
853
+ }
public/cloud-disk/static/js/app.js ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // DOM Elements
2
+ const uploadBtn = document.getElementById('uploadBtn');
3
+ const mkdirBtn = document.getElementById('mkdirBtn');
4
+ const refreshBtn = document.getElementById('refreshBtn');
5
+ const fileInput = document.getElementById('fileInput');
6
+ const uploadZone = document.getElementById('uploadZone');
7
+ const fileList = document.getElementById('fileList');
8
+
9
+ // Modals
10
+ const mkdirModal = document.getElementById('mkdirModal');
11
+ const deleteModal = document.getElementById('deleteModal');
12
+ const folderNameInput = document.getElementById('folderNameInput');
13
+ const deleteFileName = document.getElementById('deleteFileName');
14
+
15
+ // Progress
16
+ const uploadProgress = document.getElementById('uploadProgress');
17
+ const progressText = document.getElementById('progressText');
18
+ const progressFill = document.getElementById('progressFill');
19
+
20
+ // Toast
21
+ const toast = document.getElementById('toast');
22
+ const toastIcon = document.getElementById('toastIcon');
23
+ const toastMessage = document.getElementById('toastMessage');
24
+
25
+ // State
26
+ let deleteTarget = null;
27
+ let deleteType = null;
28
+
29
+ // Toast function
30
+ function showToast(message, type = 'success') {
31
+ toastMessage.textContent = message;
32
+ toastIcon.className = 'toast-icon fas ';
33
+
34
+ if (type === 'success') {
35
+ toastIcon.classList.add('fa-check-circle', 'success');
36
+ } else if (type === 'error') {
37
+ toastIcon.classList.add('fa-exclamation-circle', 'error');
38
+ } else {
39
+ toastIcon.classList.add('fa-info-circle', 'info');
40
+ }
41
+
42
+ toast.classList.add('show');
43
+
44
+ setTimeout(() => {
45
+ toast.classList.remove('show');
46
+ }, 3000);
47
+ }
48
+
49
+ // Upload functions
50
+ function uploadFiles(files) {
51
+ if (files.length === 0) return;
52
+
53
+ uploadProgress.classList.add('show');
54
+ progressText.textContent = `正在上传 0/${files.length}...`;
55
+ progressFill.style.width = '0%';
56
+
57
+ let uploaded = 0;
58
+ let failed = 0;
59
+
60
+ const uploadNext = (index) => {
61
+ if (index >= files.length) {
62
+ setTimeout(() => {
63
+ uploadProgress.classList.remove('show');
64
+ if (failed > 0) {
65
+ showToast(`${uploaded} 个文件上传成功,${failed} 个失败`, 'error');
66
+ } else {
67
+ showToast(`${uploaded} 个文件上传成功!`, 'success');
68
+ }
69
+ refreshFileList();
70
+ }, 500);
71
+ return;
72
+ }
73
+
74
+ const file = files[index];
75
+ progressText.textContent = `正在上传: ${file.name} (${index + 1}/${files.length})`;
76
+ progressFill.style.width = `${(index / files.length) * 100}%`;
77
+
78
+ const formData = new FormData();
79
+ formData.append('file', file);
80
+ formData.append('path', currentPath);
81
+
82
+ fetch('/api/upload', {
83
+ method: 'POST',
84
+ body: formData
85
+ })
86
+ .then(response => response.json())
87
+ .then(data => {
88
+ if (data.success) {
89
+ uploaded++;
90
+ } else {
91
+ failed++;
92
+ console.error('Upload failed:', data.error);
93
+ }
94
+ progressFill.style.width = `${((index + 1) / files.length) * 100}%`;
95
+ uploadNext(index + 1);
96
+ })
97
+ .catch(error => {
98
+ failed++;
99
+ console.error('Upload error:', error);
100
+ uploadNext(index + 1);
101
+ });
102
+ };
103
+
104
+ uploadNext(0);
105
+ }
106
+
107
+ // Refresh file list
108
+ function refreshFileList() {
109
+ fetch(`/api/list?path=${encodeURIComponent(currentPath)}`)
110
+ .then(response => response.json())
111
+ .then(data => {
112
+ if (data.success) {
113
+ renderFileList(data.files);
114
+ }
115
+ })
116
+ .catch(error => {
117
+ console.error('Refresh error:', error);
118
+ showToast('刷新失败', 'error');
119
+ });
120
+ }
121
+
122
+ // Render file list
123
+ function renderFileList(files) {
124
+ const container = document.querySelector('.file-list-container');
125
+
126
+ if (!files || files.length === 0) {
127
+ container.innerHTML = `
128
+ <div class="empty-state">
129
+ <i class="fas fa-folder-open empty-icon"></i>
130
+ <p class="empty-text">此文件夹为空</p>
131
+ <p class="empty-subtext">上传文件或创建文件夹开始使用</p>
132
+ </div>
133
+ `;
134
+ return;
135
+ }
136
+
137
+ // Sort: directories first
138
+ files.sort((a, b) => {
139
+ if (a.type === b.type) {
140
+ return a.name.localeCompare(b.name);
141
+ }
142
+ return a.type === 'directory' ? -1 : 1;
143
+ });
144
+
145
+ const tableHtml = `
146
+ <table class="file-table">
147
+ <thead>
148
+ <tr>
149
+ <th>名称</th>
150
+ <th>大小</th>
151
+ <th>修改时间</th>
152
+ <th>操作</th>
153
+ </tr>
154
+ </thead>
155
+ <tbody id="fileList">
156
+ ${files.map(file => `
157
+ <tr class="file-item" data-name="${file.name}" data-type="${file.type}">
158
+ <td class="file-name-cell">
159
+ ${file.type === 'directory'
160
+ ? `<i class="fas fa-folder folder-icon"></i>
161
+ <a href="/?path=${encodeURIComponent(currentPath ? currentPath + '/' + file.name : file.name)}" class="file-name">${file.name}</a>`
162
+ : `<i class="fas fa-file file-icon"></i>
163
+ <span class="file-name">${file.name}</span>`
164
+ }
165
+ </td>
166
+ <td class="file-size">${formatSize(file.size)}</td>
167
+ <td class="file-modified">${file.modified || '-'}</td>
168
+ <td class="file-actions">
169
+ ${file.type === 'file'
170
+ ? `<button class="action-btn download-btn" title="下载" data-filename="${file.name}">
171
+ <i class="fas fa-download"></i>
172
+ </button>`
173
+ : ''
174
+ }
175
+ <button class="action-btn delete-btn" title="删除" data-filename="${file.name}" data-type="${file.type}">
176
+ <i class="fas fa-trash"></i>
177
+ </button>
178
+ </td>
179
+ </tr>
180
+ `).join('')}
181
+ </tbody>
182
+ </table>
183
+ `;
184
+
185
+ container.innerHTML = tableHtml;
186
+
187
+ // Re-attach event listeners
188
+ attachFileEventListeners();
189
+ }
190
+
191
+ // Format file size
192
+ function formatSize(sizeBytes) {
193
+ if (!sizeBytes || sizeBytes === 0 || sizeBytes === '-') {
194
+ return '-';
195
+ }
196
+ try {
197
+ const size = parseInt(sizeBytes);
198
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
199
+ let i = 0;
200
+ let s = size;
201
+ while (s >= 1024 && i < units.length - 1) {
202
+ s /= 1024;
203
+ i++;
204
+ }
205
+ return s.toFixed(2) + ' ' + units[i];
206
+ } catch {
207
+ return '-';
208
+ }
209
+ }
210
+
211
+ // Attach event listeners to file items
212
+ function attachFileEventListeners() {
213
+ // Download buttons
214
+ document.querySelectorAll('.download-btn').forEach(btn => {
215
+ btn.addEventListener('click', (e) => {
216
+ e.stopPropagation();
217
+ const filename = btn.dataset.filename;
218
+ downloadFile(filename);
219
+ });
220
+ });
221
+
222
+ // Delete buttons
223
+ document.querySelectorAll('.delete-btn').forEach(btn => {
224
+ btn.addEventListener('click', (e) => {
225
+ e.stopPropagation();
226
+ const filename = btn.dataset.filename;
227
+ const type = btn.dataset.type;
228
+ showDeleteModal(filename, type);
229
+ });
230
+ });
231
+ }
232
+
233
+ // Download file
234
+ function downloadFile(filename) {
235
+ const url = `/api/download?filename=${encodeURIComponent(filename)}&path=${encodeURIComponent(currentPath)}`;
236
+ window.location.href = url;
237
+ }
238
+
239
+ // Create directory
240
+ function createDirectory(dirname) {
241
+ fetch('/api/mkdir', {
242
+ method: 'POST',
243
+ headers: {
244
+ 'Content-Type': 'application/json'
245
+ },
246
+ body: JSON.stringify({
247
+ dirname: dirname,
248
+ path: currentPath
249
+ })
250
+ })
251
+ .then(response => response.json())
252
+ .then(data => {
253
+ if (data.success) {
254
+ showToast('文件夹创建成功!', 'success');
255
+ closeMkdirModal();
256
+ refreshFileList();
257
+ } else {
258
+ showToast(data.error || '创建失败', 'error');
259
+ }
260
+ })
261
+ .catch(error => {
262
+ console.error('Mkdir error:', error);
263
+ showToast('创建失败', 'error');
264
+ });
265
+ }
266
+
267
+ // Delete file/directory
268
+ function deleteItem(filename) {
269
+ fetch('/api/delete', {
270
+ method: 'POST',
271
+ headers: {
272
+ 'Content-Type': 'application/json'
273
+ },
274
+ body: JSON.stringify({
275
+ filename: filename,
276
+ path: currentPath
277
+ })
278
+ })
279
+ .then(response => response.json())
280
+ .then(data => {
281
+ if (data.success) {
282
+ showToast('删除成功!', 'success');
283
+ closeDeleteModal();
284
+ refreshFileList();
285
+ } else {
286
+ showToast(data.error || '删除失败', 'error');
287
+ }
288
+ })
289
+ .catch(error => {
290
+ console.error('Delete error:', error);
291
+ showToast('删除失败', 'error');
292
+ });
293
+ }
294
+
295
+ // Modal functions
296
+ function showMkdirModal() {
297
+ mkdirModal.classList.add('show');
298
+ folderNameInput.value = '';
299
+ setTimeout(() => folderNameInput.focus(), 100);
300
+ }
301
+
302
+ function closeMkdirModal() {
303
+ mkdirModal.classList.remove('show');
304
+ }
305
+
306
+ function showDeleteModal(filename, type) {
307
+ deleteTarget = filename;
308
+ deleteType = type;
309
+ deleteFileName.textContent = filename;
310
+ deleteModal.classList.add('show');
311
+ }
312
+
313
+ function closeDeleteModal() {
314
+ deleteModal.classList.remove('show');
315
+ deleteTarget = null;
316
+ deleteType = null;
317
+ }
318
+
319
+ // Event Listeners
320
+
321
+ // Upload button
322
+ uploadBtn.addEventListener('click', () => {
323
+ fileInput.click();
324
+ });
325
+
326
+ fileInput.addEventListener('change', (e) => {
327
+ if (e.target.files.length > 0) {
328
+ uploadFiles(e.target.files);
329
+ fileInput.value = '';
330
+ }
331
+ });
332
+
333
+ // Upload zone click
334
+ uploadZone.addEventListener('click', () => {
335
+ fileInput.click();
336
+ });
337
+
338
+ // Drag and drop
339
+ uploadZone.addEventListener('dragover', (e) => {
340
+ e.preventDefault();
341
+ uploadZone.classList.add('drag-over');
342
+ });
343
+
344
+ uploadZone.addEventListener('dragleave', (e) => {
345
+ e.preventDefault();
346
+ uploadZone.classList.remove('drag-over');
347
+ });
348
+
349
+ uploadZone.addEventListener('drop', (e) => {
350
+ e.preventDefault();
351
+ uploadZone.classList.remove('drag-over');
352
+
353
+ if (e.dataTransfer.files.length > 0) {
354
+ uploadFiles(e.dataTransfer.files);
355
+ }
356
+ });
357
+
358
+ // Mkdir button
359
+ mkdirBtn.addEventListener('click', showMkdirModal);
360
+
361
+ document.getElementById('closeMkdirModal').addEventListener('click', closeMkdirModal);
362
+ document.getElementById('cancelMkdir').addEventListener('click', closeMkdirModal);
363
+
364
+ document.getElementById('confirmMkdir').addEventListener('click', () => {
365
+ const dirname = folderNameInput.value.trim();
366
+ if (dirname) {
367
+ createDirectory(dirname);
368
+ } else {
369
+ showToast('请输入文件夹名称', 'error');
370
+ }
371
+ });
372
+
373
+ folderNameInput.addEventListener('keypress', (e) => {
374
+ if (e.key === 'Enter') {
375
+ const dirname = folderNameInput.value.trim();
376
+ if (dirname) {
377
+ createDirectory(dirname);
378
+ }
379
+ }
380
+ });
381
+
382
+ // Delete modal
383
+ document.getElementById('closeDeleteModal').addEventListener('click', closeDeleteModal);
384
+ document.getElementById('cancelDelete').addEventListener('click', closeDeleteModal);
385
+
386
+ document.getElementById('confirmDelete').addEventListener('click', () => {
387
+ if (deleteTarget) {
388
+ deleteItem(deleteTarget);
389
+ }
390
+ });
391
+
392
+ // Refresh button
393
+ refreshBtn.addEventListener('click', () => {
394
+ refreshBtn.querySelector('i').classList.add('fa-spin');
395
+ refreshFileList();
396
+ setTimeout(() => {
397
+ refreshBtn.querySelector('i').classList.remove('fa-spin');
398
+ }, 500);
399
+ });
400
+
401
+ // Close modals on outside click
402
+ mkdirModal.addEventListener('click', (e) => {
403
+ if (e.target === mkdirModal) {
404
+ closeMkdirModal();
405
+ }
406
+ });
407
+
408
+ deleteModal.addEventListener('click', (e) => {
409
+ if (e.target === deleteModal) {
410
+ closeDeleteModal();
411
+ }
412
+ });
413
+
414
+ // Keyboard shortcuts
415
+ document.addEventListener('keydown', (e) => {
416
+ // Escape to close modals
417
+ if (e.key === 'Escape') {
418
+ closeMkdirModal();
419
+ closeDeleteModal();
420
+ }
421
+ });
422
+
423
+ // Initialize
424
+ document.addEventListener('DOMContentLoaded', () => {
425
+ attachFileEventListeners();
426
+ initUserFeatures();
427
+ });
428
+
429
+ // User features
430
+ function initUserFeatures() {
431
+ // Logout button
432
+ const logoutBtn = document.getElementById('logoutBtn');
433
+ if (logoutBtn) {
434
+ logoutBtn.addEventListener('click', logout);
435
+ }
436
+ }
437
+
438
+ // Logout function
439
+ function logout() {
440
+ fetch('/api/logout', {
441
+ method: 'POST',
442
+ headers: {
443
+ 'Content-Type': 'application/json'
444
+ }
445
+ })
446
+ .then(response => response.json())
447
+ .then(data => {
448
+ if (data.success) {
449
+ showToast('已退出登录', 'success');
450
+ setTimeout(() => {
451
+ window.location.href = '/login';
452
+ }, 500);
453
+ } else {
454
+ showToast(data.error || '退出失败', 'error');
455
+ }
456
+ })
457
+ .catch(error => {
458
+ console.error('Logout error:', error);
459
+ showToast('退出失败', 'error');
460
+ });
461
+ }
462
+
463
+ // Update storage info
464
+ function updateStorageInfo() {
465
+ fetch('/api/storage-info')
466
+ .then(response => response.json())
467
+ .then(data => {
468
+ if (data.success) {
469
+ const storageFill = document.querySelector('.storage-fill');
470
+ const storageText = document.querySelector('.storage-text');
471
+
472
+ if (storageFill) {
473
+ storageFill.style.width = data.storage_percent + '%';
474
+ }
475
+ if (storageText) {
476
+ storageText.textContent = `${data.used_storage_formatted} / ${data.max_storage_formatted}`;
477
+ }
478
+ }
479
+ })
480
+ .catch(error => {
481
+ console.error('Storage info error:', error);
482
+ });
483
+ }
484
+
485
+ // Override uploadFiles to update storage after
486
+ const originalUploadFiles = uploadFiles;
487
+ uploadFiles = function(files) {
488
+ originalUploadFiles(files);
489
+ // Update storage after upload completes (polling)
490
+ setTimeout(() => {
491
+ updateStorageInfo();
492
+ }, 2000);
493
+ };
494
+
495
+ // Override deleteItem to update storage after
496
+ const originalDeleteItem = deleteItem;
497
+ deleteItem = function(filename) {
498
+ originalDeleteItem(filename);
499
+ // Update storage after delete
500
+ setTimeout(() => {
501
+ updateStorageInfo();
502
+ }, 1000);
503
+ };
504
+
505
+ // Override createDirectory to update storage after
506
+ const originalCreateDirectory = createDirectory;
507
+ createDirectory = function(dirname) {
508
+ originalCreateDirectory(dirname);
509
+ // Update storage after mkdir
510
+ setTimeout(() => {
511
+ updateStorageInfo();
512
+ }, 1000);
513
+ };
public/cloud-disk/templates/index.html ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>云盘 - Cloud Disk</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <!-- Header -->
13
+ <header class="header">
14
+ <div class="header-left">
15
+ <i class="fas fa-cloud header-icon"></i>
16
+ <h1 class="header-title">云盘</h1>
17
+ </div>
18
+ <div class="header-right">
19
+ <div class="user-info">
20
+ <div class="storage-info">
21
+ <div class="storage-bar">
22
+ <div class="storage-fill" style="width: {{ storage_percent }}%"></div>
23
+ </div>
24
+ <span class="storage-text">{{ used_storage_formatted }} / {{ max_storage_formatted }}</span>
25
+ </div>
26
+ <div class="user-profile">
27
+ <i class="fas fa-user-circle user-avatar"></i>
28
+ <span class="username">{{ username }}</span>
29
+ </div>
30
+ <button class="btn btn-icon logout-btn" id="logoutBtn" title="退出登录">
31
+ <i class="fas fa-sign-out-alt"></i>
32
+ </button>
33
+ </div>
34
+ <div class="header-actions">
35
+ <button class="btn btn-primary" id="uploadBtn">
36
+ <i class="fas fa-upload"></i>
37
+ 上传文件
38
+ </button>
39
+ <button class="btn btn-secondary" id="mkdirBtn">
40
+ <i class="fas fa-folder-plus"></i>
41
+ 新建文件夹
42
+ </button>
43
+ <button class="btn btn-icon" id="refreshBtn" title="刷新">
44
+ <i class="fas fa-sync-alt"></i>
45
+ </button>
46
+ </div>
47
+ </div>
48
+ </header>
49
+
50
+ <!-- Breadcrumb -->
51
+ <nav class="breadcrumb">
52
+ <a href="/" class="breadcrumb-item">
53
+ <i class="fas fa-home"></i>
54
+ 根目录
55
+ </a>
56
+ {% for item in breadcrumb %}
57
+ <span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
58
+ <a href="/?path={{ item.path }}" class="breadcrumb-item">{{ item.name }}</a>
59
+ {% endfor %}
60
+ </nav>
61
+
62
+ <!-- Upload Drop Zone -->
63
+ <div class="upload-zone" id="uploadZone">
64
+ <div class="upload-zone-content">
65
+ <i class="fas fa-cloud-upload-alt upload-icon"></i>
66
+ <p class="upload-text">拖拽文件到此处上传</p>
67
+ <p class="upload-subtext">或点击上方"上传文件"按钮</p>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- File List -->
72
+ <div class="file-list-container">
73
+ {% if files %}
74
+ <table class="file-table">
75
+ <thead>
76
+ <tr>
77
+ <th>名称</th>
78
+ <th>大小</th>
79
+ <th>修改时间</th>
80
+ <th>操作</th>
81
+ </tr>
82
+ </thead>
83
+ <tbody id="fileList">
84
+ {% for file in files %}
85
+ <tr class="file-item" data-name="{{ file.name }}" data-type="{{ file.type }}">
86
+ <td class="file-name-cell">
87
+ {% if file.type == 'directory' %}
88
+ <i class="fas fa-folder folder-icon"></i>
89
+ <a href="/?path={{ (current_path + '/' + file.name) if current_path else file.name }}" class="file-name">
90
+ {{ file.name }}
91
+ </a>
92
+ {% else %}
93
+ <i class="fas fa-file file-icon"></i>
94
+ <span class="file-name">{{ file.name }}</span>
95
+ {% endif %}
96
+ </td>
97
+ <td class="file-size">{{ file.size | format_size }}</td>
98
+ <td class="file-modified">{{ file.modified }}</td>
99
+ <td class="file-actions">
100
+ {% if file.type == 'file' %}
101
+ <button class="action-btn download-btn" title="下载" data-filename="{{ file.name }}">
102
+ <i class="fas fa-download"></i>
103
+ </button>
104
+ {% endif %}
105
+ <button class="action-btn delete-btn" title="删除" data-filename="{{ file.name }}" data-type="{{ file.type }}">
106
+ <i class="fas fa-trash"></i>
107
+ </button>
108
+ </td>
109
+ </tr>
110
+ {% endfor %}
111
+ </tbody>
112
+ </table>
113
+ {% else %}
114
+ <div class="empty-state">
115
+ <i class="fas fa-folder-open empty-icon"></i>
116
+ <p class="empty-text">此文件夹为空</p>
117
+ <p class="empty-subtext">上传文件或创建文件夹开始使用</p>
118
+ </div>
119
+ {% endif %}
120
+ </div>
121
+ </div>
122
+
123
+ <!-- Hidden file input -->
124
+ <input type="file" id="fileInput" style="display: none;" multiple>
125
+
126
+ <!-- Modal: New Folder -->
127
+ <div class="modal" id="mkdirModal">
128
+ <div class="modal-content">
129
+ <div class="modal-header">
130
+ <h3>新建文件夹</h3>
131
+ <button class="modal-close" id="closeMkdirModal">&times;</button>
132
+ </div>
133
+ <div class="modal-body">
134
+ <input type="text" id="folderNameInput" class="form-input" placeholder="请输入文件夹名称">
135
+ </div>
136
+ <div class="modal-footer">
137
+ <button class="btn btn-secondary" id="cancelMkdir">取消</button>
138
+ <button class="btn btn-primary" id="confirmMkdir">创建</button>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
143
+ <!-- Modal: Confirm Delete -->
144
+ <div class="modal" id="deleteModal">
145
+ <div class="modal-content">
146
+ <div class="modal-header">
147
+ <h3>确认删除</h3>
148
+ <button class="modal-close" id="closeDeleteModal">&times;</button>
149
+ </div>
150
+ <div class="modal-body">
151
+ <p>确定要删除 <strong id="deleteFileName"></strong> 吗?</p>
152
+ <p class="text-warning">此操作不可撤销!</p>
153
+ </div>
154
+ <div class="modal-footer">
155
+ <button class="btn btn-secondary" id="cancelDelete">取消</button>
156
+ <button class="btn btn-danger" id="confirmDelete">删除</button>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- Upload Progress -->
162
+ <div class="upload-progress" id="uploadProgress">
163
+ <div class="progress-content">
164
+ <i class="fas fa-spinner fa-spin"></i>
165
+ <span id="progressText">正在上传...</span>
166
+ </div>
167
+ <div class="progress-bar">
168
+ <div class="progress-fill" id="progressFill"></div>
169
+ </div>
170
+ </div>
171
+
172
+ <!-- Toast -->
173
+ <div class="toast" id="toast">
174
+ <div class="toast-content">
175
+ <i class="toast-icon" id="toastIcon"></i>
176
+ <span id="toastMessage"></span>
177
+ </div>
178
+ </div>
179
+
180
+ <script>
181
+ const currentPath = "{{ current_path }}";
182
+ </script>
183
+ <script src="{{ url_for('static', filename='js/app.js') }}"></script>
184
+ </body>
185
+ </html>
public/cloud-disk/templates/login.html ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>登录 - 云盘</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ </head>
10
+ <body class="auth-body">
11
+ <div class="auth-container">
12
+ <div class="auth-card">
13
+ <div class="auth-header">
14
+ <i class="fas fa-cloud auth-icon"></i>
15
+ <h1>云盘</h1>
16
+ <p class="auth-subtitle">安全、便捷的云端存储</p>
17
+ </div>
18
+
19
+ {% if error %}
20
+ <div class="auth-error">
21
+ <i class="fas fa-exclamation-circle"></i>
22
+ {{ error }}
23
+ </div>
24
+ {% endif %}
25
+
26
+ <form class="auth-form" method="POST" action="/login">
27
+ <div class="form-group">
28
+ <label for="username">
29
+ <i class="fas fa-user"></i>
30
+ 用户名
31
+ </label>
32
+ <input type="text" id="username" name="username" class="form-input" placeholder="请输入用户名" required>
33
+ </div>
34
+
35
+ <div class="form-group">
36
+ <label for="password">
37
+ <i class="fas fa-lock"></i>
38
+ 密码
39
+ </label>
40
+ <input type="password" id="password" name="password" class="form-input" placeholder="请输入密码" required>
41
+ </div>
42
+
43
+ <button type="submit" class="btn btn-primary btn-block">
44
+ <i class="fas fa-sign-in-alt"></i>
45
+ 登录
46
+ </button>
47
+ </form>
48
+
49
+ <div class="auth-footer">
50
+ <p>还没有账号?<a href="/register" class="auth-link">立即注册</a></p>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="auth-features">
55
+ <div class="feature-item">
56
+ <i class="fas fa-shield-alt feature-icon"></i>
57
+ <h3>安全存储</h3>
58
+ <p>每个用户独立空间,数据安全隔离</p>
59
+ </div>
60
+ <div class="feature-item">
61
+ <i class="fas fa-hdd feature-icon"></i>
62
+ <h3>1GB 空间</h3>
63
+ <p>免费享受 1GB 云端存储空间</p>
64
+ </div>
65
+ <div class="feature-item">
66
+ <i class="fas fa-bolt feature-icon"></i>
67
+ <h3>快速上传</h3>
68
+ <p>支持拖拽上传,大文件也能快速传输</p>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </body>
73
+ </html>
public/cloud-disk/templates/register.html ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>注册 - 云盘</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ </head>
10
+ <body class="auth-body">
11
+ <div class="auth-container">
12
+ <div class="auth-card">
13
+ <div class="auth-header">
14
+ <i class="fas fa-cloud auth-icon"></i>
15
+ <h1>注册账号</h1>
16
+ <p class="auth-subtitle">创建您的云盘账号,免费获得 1GB 空间</p>
17
+ </div>
18
+
19
+ {% if error %}
20
+ <div class="auth-error">
21
+ <i class="fas fa-exclamation-circle"></i>
22
+ {{ error }}
23
+ </div>
24
+ {% endif %}
25
+
26
+ <form class="auth-form" method="POST" action="/register">
27
+ <div class="form-group">
28
+ <label for="username">
29
+ <i class="fas fa-user"></i>
30
+ 用户名
31
+ </label>
32
+ <input type="text" id="username" name="username" class="form-input" placeholder="3-20位字母、数字、下划线或中文" required>
33
+ <small class="form-hint">支持字母、数字、下划线和中文</small>
34
+ </div>
35
+
36
+ <div class="form-group">
37
+ <label for="password">
38
+ <i class="fas fa-lock"></i>
39
+ 密码
40
+ </label>
41
+ <input type="password" id="password" name="password" class="form-input" placeholder="至少6位字符" required>
42
+ </div>
43
+
44
+ <div class="form-group">
45
+ <label for="confirm_password">
46
+ <i class="fas fa-lock"></i>
47
+ 确认密码
48
+ </label>
49
+ <input type="password" id="confirm_password" name="confirm_password" class="form-input" placeholder="再次输入密码" required>
50
+ </div>
51
+
52
+ <button type="submit" class="btn btn-primary btn-block">
53
+ <i class="fas fa-user-plus"></i>
54
+ 注册账号
55
+ </button>
56
+ </form>
57
+
58
+ <div class="auth-footer">
59
+ <p>已有账号?<a href="/login" class="auth-link">立即登录</a></p>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="auth-features">
64
+ <div class="feature-item">
65
+ <i class="fas fa-shield-alt feature-icon"></i>
66
+ <h3>安全存储</h3>
67
+ <p>每个用户独立空间,数据安全隔离</p>
68
+ </div>
69
+ <div class="feature-item">
70
+ <i class="fas fa-hdd feature-icon"></i>
71
+ <h3>1GB 空间</h3>
72
+ <p>免费享受 1GB 云端存储空间</p>
73
+ </div>
74
+ <div class="feature-item">
75
+ <i class="fas fa-bolt feature-icon"></i>
76
+ <h3>快速上传</h3>
77
+ <p>支持拖拽上传,大文件也能快速传输</p>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </body>
82
+ </html>