commit ff2c63c0ce17666b49c1778a8bbaf1eea1bceae8 Author: dvs-dvsxt Date: Fri Aug 28 10:22:37 2026 +0800 Initial commit: DVS Cloud Drive v1.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a50f328 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.venv/ +venv/ +env/ + +# Database & uploads +*.db +*.sqlite3 +uploads/ +user_data/ +data/ + +# System files +Thumbs.db +.DS_Store +desktop.ini diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fddd2dd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DVS (dvs-dvsxt) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..26a5aca --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# ☁️ DVS Cloud Drive — Web File Management Platform + +> A full-featured **web-based file management (cloud drive)** platform built with Flask. Upload, download, preview, share, search, and manage your files — with robust security. + +**DVS Cloud Drive** is a self-hosted web file manager. It provides user authentication, file upload/download, folder management, batch operations, in-browser preview (PDF / audio / video), file search, and shareable links — all protected by security controls that block access to system-critical directories. + +--- + +## ✨ Features + +| Feature | Description | +|---------|-------------| +| 👤 **User System** | Register / login / logout / change password (hashed with `werkzeug`) | +| 📤 **File Upload** | Upload files with secure filename handling | +| 📁 **Folder Management** | Create / rename / delete folders & files | +| 📥 **Download** | Single & batch download (ZIP archive) | +| 👁️ **In-Browser Preview** | PDF / video / audio preview | +| 🔍 **Search** | Search files across the drive | +| 🔗 **File Sharing** | Generate shareable links (`/s/`) | +| 🗑️ **Batch Delete** | Delete multiple items at once | +| 📊 **Storage Usage** | View storage usage via API | +| 🛡️ **Security** | Forbidden system directories (Linux & Windows) blocked | + +--- + +## 🔌 Routes + +| Route | Method | Description | +|-------|--------|-------------| +| `/` | GET | Dashboard / file list | +| `/login` | GET/POST | User login | +| `/register` | GET/POST | User registration | +| `/change_password` | GET/POST | Change password | +| `/logout` | GET | Log out | +| `/upload` | POST | Upload files | +| `/create_folder` | POST | Create a folder | +| `/rename` | POST | Rename item | +| `/delete` | POST | Delete item | +| `/download/` | GET | Download file/folder | +| `/preview/` | GET | Preview file (PDF/video/audio) | +| `/search` | GET | Search files | +| `/batch_download` | POST | Batch download as ZIP | +| `/batch_delete` | POST | Batch delete | +| `/share` | POST | Create share link | +| `/unshare` | POST | Remove share | +| `/shared_files` | GET | List shared files | +| `/s/` | GET | Access a shared file | +| `/api/storage_usage` | GET | Storage usage (JSON) | + +--- + +## 🚀 Quick Start + +### Prerequisites +- Python 3.8+ +- `flask`, `flask-cors`, `werkzeug` + +### Install & Run + +```bash +# Install dependencies +pip install flask flask-cors + +# Run the platform +python app.py +``` + +Then open `http://localhost:5000` in your browser. + +### Default Flow +1. **Register** an account +2. **Login** +3. Upload / create folders / share / preview files from the dashboard + +--- + +## 🛡️ Security + +- Passwords hashed with `werkzeug.security` (PBKDF2) +- Secure filename normalization via `werkzeug.utils.secure_filename` +- **Forbidden directories** blocked (path traversal protection): + - Linux: `/root`, `/etc`, `/usr`, `/var`, `/proc`, `/sys`, etc. + - Windows: `C:\Windows`, `C:\Program Files`, System32, etc. +- Session-based authentication & CSRF protection + +--- + +## 📁 Project Structure + +``` +DVS-Cloud-Drive/ +├── app.py # Main Flask application +├── templates/ # HTML templates (Jinja2) +│ ├── base.html # Layout +│ ├── index.html # Dashboard / file list +│ ├── login.html / register.html / change_password.html +│ ├── preview_pdf.html / preview_video.html / preview_audio.html +│ ├── search_results.html / shared_files.html +│ ├── 404.html / 500.html +├── favicon.ico # Site icon +└── README.md # This document +``` + +--- + +## 📄 License + +This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details. diff --git a/app.py b/app.py new file mode 100644 index 0000000..ec5cb05 --- /dev/null +++ b/app.py @@ -0,0 +1,1282 @@ +import os +import time +import uuid +import hashlib +import sqlite3 +from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, jsonify, send_file, g, abort +from werkzeug.security import generate_password_hash, check_password_hash +from werkzeug.utils import secure_filename +from datetime import datetime, timedelta +import json +import zipfile +from io import BytesIO +import re +from flask_cors import CORS +import unicodedata + +# ==================== SECURITY CONFIGURATION ==================== +# Forbidden system directories (Linux & Windows) +FORBIDDEN_DIRS = [ + # Linux system directories + '/', '/root', '/etc', '/bin', '/sbin', '/usr', '/var', '/tmp', + '/proc', '/sys', '/dev', '/boot', '/lib', '/lib64', '/opt', + '/home', '/mnt', '/media', '/srv', '/run', '/lost+found', + '/usr/bin', '/usr/sbin', '/usr/local', '/usr/lib', '/usr/lib64', + '/var/log', '/var/run', '/var/tmp', '/var/cache', '/var/spool', + '/etc/passwd', '/etc/shadow', '/etc/hosts', '/etc/fstab', + '/etc/sudoers', '/etc/ssh', '/etc/nginx', '/etc/apache2', + '/etc/mysql', '/etc/postgresql', '/etc/redis', '/etc/mongodb', + '/root/.ssh', '/root/.bash_history', '/root/.bashrc', + # Windows system directories + 'C:\\', 'D:\\', 'E:\\', 'F:\\', 'C:/', 'D:/', 'E:/', 'F:/', + 'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)', + 'C:\\Windows\\System32', 'C:\\Windows\\SysWOW64', + 'C:\\Users', 'C:\\Documents and Settings', + 'C:\\ProgramData', 'C:\\Boot', 'C:\\System Volume Information', + # Path traversal patterns + '..', '../', '..\\', './../', '.\\..\\', + # Special patterns + '~', '~/', '~\\', +] + +# Hidden/system files to block +FORBIDDEN_FILES = [ + '.htaccess', '.htpasswd', '.git', '.svn', '.env', + 'config.php', 'wp-config.php', 'settings.py', + 'passwd', 'shadow', 'sudoers', 'fstab', + '.bash_history', '.bashrc', '.profile', '.ssh', + 'id_rsa', 'id_dsa', 'known_hosts', + '.npmrc', '.pypirc', '.dockercfg', '.kube', +] + +# Path traversal patterns +PATH_TRAVERSAL_PATTERNS = [ + '..', '../', '..\\', './../', '.\\..\\', + '%2e%2e', '%2e%2e%2f', '%2e%2e%5c', + '%252e%252e', '%252e%252e%252f', + '%c0%ae%c0%ae', '%c0%ae%c0%ae%c0%af', +] + +def safe_filename(filename): + """ + Safe filename handling with Chinese character support + Preserves Chinese, letters, numbers, dots, underscores, hyphens + """ + if '.' in filename: + name, ext = filename.rsplit('.', 1) + ext = '.' + ext + else: + name = filename + ext = '' + + # Remove path traversal characters + name = name.replace('..', '').replace('/', '').replace('\\', '') + + # Keep only safe characters + name = re.sub(r'[^\u4e00-\u9fff\u3400-\u4dbfa-zA-Z0-9\s\-_]', '', name) + name = name.strip() + + if not name: + name = '未命名文件' + + if len(name) > 200: + name = name[:200] + + return name + ext + +def is_path_traversal(path): + """ + Check if path contains traversal attempts + """ + if not path: + return False + + path_lower = path.lower() + for pattern in PATH_TRAVERSAL_PATTERNS: + if pattern in path_lower: + return True + + # Check for encoded traversal + decoded_path = path + for _ in range(3): # Try to decode multiple times + decoded_path = decoded_path.replace('%2e', '.').replace('%2f', '/').replace('%5c', '\\') + + for pattern in PATH_TRAVERSAL_PATTERNS: + if pattern in decoded_path.lower(): + return True + + return False + +def is_system_path(path): + """ + Check if path points to system directory + """ + if not path: + return False + + # Normalize path + normalized = os.path.normpath(path).replace('\\', '/') + + for forbidden in FORBIDDEN_DIRS: + forbidden_norm = os.path.normpath(forbidden).replace('\\', '/') + if normalized == forbidden_norm or normalized.startswith(forbidden_norm + '/'): + return True + + return False + +def is_forbidden_filename(filename): + """ + Check if filename is forbidden + """ + if not filename: + return False + + filename_lower = filename.lower() + for forbidden in FORBIDDEN_FILES: + if forbidden in filename_lower or filename_lower == forbidden: + return True + + return False + +def validate_user_path(user_path, username): + """ + Validate that path is within user's directory + Returns the validated absolute path or None if invalid + """ + if not username: + return None + + # Get user's base directory + user_base = os.path.abspath(os.path.join(app.config['UPLOAD_FOLDER'], username)) + + # Construct full path + if user_path: + full_path = os.path.abspath(os.path.join(user_base, user_path)) + else: + full_path = user_base + + # Check path traversal + if is_path_traversal(user_path): + print(f"[SECURITY] Path traversal detected: {user_path} by user {username}") + return None + + # Check if trying to access system directory + if is_system_path(full_path): + print(f"[SECURITY] System directory access blocked: {full_path} by user {username}") + return None + + # Check if path is within user's directory + if not full_path.startswith(user_base): + print(f"[SECURITY] User {username} tried to access outside directory: {full_path}") + return None + + return full_path + +def get_user_base_path(username): + """ + Get user's base directory path + """ + return os.path.abspath(os.path.join(app.config['UPLOAD_FOLDER'], username)) + +# ==================== APP CONFIGURATION ==================== +app = Flask(__name__) +CORS(app) + +app.config['SECRET_KEY'] = 'dvs-cloud-drive-secret-key-2025' + +UPLOAD_BASE = os.path.join(os.getcwd(), 'uploads') +app.config['UPLOAD_FOLDER'] = UPLOAD_BASE +app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024 * 100 # 100GB +app.config['DATABASE'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cloud_storage.db') + +# Invitation code +INVITATION_CODE = "Test" + +# Ensure upload folder exists +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + +# Log directory +os.makedirs('log', exist_ok=True) + +# ==================== DATABASE FUNCTIONS ==================== +def get_db(): + """Get database connection""" + if 'db' not in g: + g.db = sqlite3.connect(app.config['DATABASE']) + g.db.row_factory = sqlite3.Row + return g.db + +@app.teardown_appcontext +def close_db(error): + """Close database connection""" + if hasattr(g, 'db'): + g.db.close() + +def init_db(): + """Initialize database""" + with app.app_context(): + db = get_db() + + db.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL COLLATE NOCASE, + password_hash TEXT NOT NULL, + is_vip INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP + ) + ''') + + db.execute(''' + CREATE TABLE IF NOT EXISTS files ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL, + user_id INTEGER NOT NULL, + size INTEGER, + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_shared INTEGER DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES users (id) + ) + ''') + + db.execute(''' + CREATE TABLE IF NOT EXISTS shares ( + share_id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + original_path TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + downloads INTEGER DEFAULT 0, + is_dir INTEGER DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES users (id) + ) + ''') + + db.execute(''' + CREATE TABLE IF NOT EXISTS access_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + ip TEXT, + method TEXT, + path TEXT, + user_agent TEXT, + referer TEXT, + username TEXT + ) + ''') + + db.commit() + + # Default admin user + admin_password_hash = hashlib.sha256("admin123456".encode()).hexdigest() + try: + db.execute( + 'INSERT OR IGNORE INTO users (username, password_hash, is_vip) VALUES (?, ?, ?)', + ('admin', admin_password_hash, 1) + ) + db.commit() + except: + pass + +def get_user_by_username(username): + """Get user by username""" + db = get_db() + user = db.execute( + 'SELECT * FROM users WHERE username = ? COLLATE NOCASE', + (username,) + ).fetchone() + return dict(user) if user else None + +def add_user(username, password_hash, is_vip=False): + """Add user""" + db = get_db() + try: + db.execute( + 'INSERT INTO users (username, password_hash, is_vip) VALUES (?, ?, ?)', + (username, password_hash, 1 if is_vip else 0) + ) + db.commit() + return True + except sqlite3.IntegrityError: + return False + +def update_user_password(username, password_hash): + """Update user password""" + db = get_db() + db.execute( + 'UPDATE users SET password_hash = ? WHERE username = ? COLLATE NOCASE', + (password_hash, username) + ) + db.commit() + +def is_vip_user(username): + """Check if user is VIP""" + user = get_user_by_username(username) + return user and user['is_vip'] == 1 + +def sha256_hash(password): + """Calculate SHA256 hash""" + return hashlib.sha256(password.encode()).hexdigest() + +# ==================== LOGGING FUNCTION ==================== +def log_access(ip=None, username=None): + """Log access to database""" + try: + if ip is None: + if request.headers.get('X-Forwarded-For'): + ip = request.headers.get('X-Forwarded-For').split(',')[0].strip() + elif request.headers.get('X-Real-IP'): + ip = request.headers.get('X-Real-IP') + else: + ip = request.remote_addr + + if username is None: + username = session.get('username', '未登录') + + db = get_db() + db.execute( + '''INSERT INTO access_logs + (ip, method, path, user_agent, referer, username) + VALUES (?, ?, ?, ?, ?, ?)''', + (ip, request.method, request.path, + request.headers.get('User-Agent', ''), + request.headers.get('Referer', ''), + username) + ) + db.commit() + return True + except Exception as e: + print(f"Log error: {e}") + return False + +# ==================== SECURITY FILTER ==================== +@app.before_request +def security_filter(): + """Global security filter - executed before each request""" + # Skip login/register pages + if request.path in ['/login', '/register']: + return + + # Check if user is logged in for protected routes + if 'username' not in session: + return + + # Check GET parameters + for key, values in request.args.lists(): + for value in values: + if is_path_traversal(value): + print(f"[SECURITY] Blocked traversal in GET: {request.url}") + abort(403, "Access denied") + + # Check POST parameters + if request.method == 'POST': + for key, values in request.form.lists(): + for value in values: + if is_path_traversal(value): + print(f"[SECURITY] Blocked traversal in POST: {key}={value}") + abort(403, "Access denied") + + # Check for forbidden filenames in uploads + if key == 'file' and is_forbidden_filename(value): + print(f"[SECURITY] Blocked forbidden filename: {value}") + abort(403, "File name not allowed") + +# ==================== HELPER FUNCTIONS ==================== +def format_size(bytes): + """Format file size for display""" + if bytes is None: + return '0 B' + units = ['B', 'KB', 'MB', 'GB', 'TB'] + i = 0 + while bytes >= 1024 and i < len(units) - 1: + bytes /= 1024 + i += 1 + return f'{bytes:.2f} {units[i]}' + +def format_time(timestamp): + """Format timestamp for display""" + dt = datetime.fromtimestamp(timestamp) + return dt.strftime('%Y-%m-%d %H:%M:%S') + +def login_required(f): + """Decorator to require login""" + def decorated_function(*args, **kwargs): + if 'username' not in session: + flash('Please login first', 'danger') + return redirect(url_for('login')) + return f(*args, **kwargs) + decorated_function.__name__ = f.__name__ + return decorated_function + +# ==================== ROUTES ==================== + +# Main index route +@app.route('/') +@login_required +def index(): + username = session['username'] + path = request.args.get('path', '') + + # Validate path + full_path = validate_user_path(path, username) + if full_path is None: + flash('Invalid path', 'danger') + return redirect(url_for('index')) + + log_access() + + # Ensure user directory exists + user_base = get_user_base_path(username) + os.makedirs(user_base, exist_ok=True) + + # Get directory contents + items = [] + if os.path.exists(full_path) and os.path.isdir(full_path): + for item in os.listdir(full_path): + item_path = os.path.join(full_path, item) + item_rel_path = os.path.join(path, item) if path else item + + # Skip hidden files starting with . + if item.startswith('.'): + continue + + if os.path.isdir(item_path): + items.append({ + 'name': item, + 'path': item_rel_path, + 'is_dir': True, + 'size': '-', + 'modified': format_time(os.path.getmtime(item_path)) + }) + else: + file_ext = item.rsplit('.', 1)[1].lower() if '.' in item else '' + items.append({ + 'name': item, + 'path': item_rel_path, + 'is_dir': False, + 'size': format_size(os.path.getsize(item_path)), + 'modified': format_time(os.path.getmtime(item_path)), + 'ext': file_ext, + 'previewable': file_ext in {'png', 'jpg', 'jpeg', 'gif', 'mp4', 'mp3', 'pdf', 'txt'} + }) + + # Sort: folders first, then by name + items.sort(key=lambda x: (0 if x['is_dir'] else 1, x['name'].lower())) + + # Build breadcrumbs + breadcrumbs = [] + parts = path.split(os.sep) if path else [] + current_path = '' + breadcrumbs.append({'name': 'Root', 'path': ''}) + + for part in parts: + current_path = os.path.join(current_path, part) if current_path else part + breadcrumbs.append({'name': part, 'path': current_path}) + + # Calculate storage usage + total_size = 0 + if os.path.exists(user_base): + for dirpath, dirnames, filenames in os.walk(user_base): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + try: + total_size += os.path.getsize(filepath) + except OSError: + continue + + storage_usage = { + 'used': total_size, + 'used_formatted': format_size(total_size), + 'total_formatted': 'Unlimited', + 'free_formatted': 'Unlimited' + } + + vip_user = is_vip_user(username) + + return render_template('index.html', + items=items, + breadcrumbs=breadcrumbs, + path=path, + vip_user=vip_user, + storage_usage=storage_usage, + username=username) + +# Login route +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form.get('username', '').strip() + password = request.form.get('password', '') + + if not username or not password: + flash('Please enter username and password', 'danger') + return render_template('login.html') + + # Block system usernames + if username in ['root', 'admin', 'administrator', 'system', 'user']: + pass # Allow admin login with correct password + + password_hash = sha256_hash(password) + user = get_user_by_username(username) + + if user and user['password_hash'] == password_hash: + session['username'] = username + session['vip'] = bool(user['is_vip']) + + db = get_db() + db.execute( + 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE username = ? COLLATE NOCASE', + (username,) + ) + db.commit() + + log_access() + flash('Login successful', 'success') + return redirect(url_for('index')) + else: + flash('Invalid username or password', 'danger') + + return render_template('login.html') + +# Register route +@app.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + username = request.form.get('username', '').strip() + password = request.form.get('password', '') + confirm_password = request.form.get('confirm-password', '') + invitation_code = request.form.get('invitation_code', '') + + # Validate username + if not username or len(username) < 3: + flash('Username must be at least 3 characters', 'danger') + return redirect(url_for('register')) + + # Block system usernames + if username.lower() in ['root', 'admin', 'administrator', 'system', 'bin', 'daemon', 'nobody']: + flash('Username not allowed', 'danger') + return redirect(url_for('register')) + + if get_user_by_username(username): + flash('Username already exists', 'danger') + return redirect(url_for('register')) + + if password != confirm_password: + flash('Passwords do not match', 'danger') + return redirect(url_for('register')) + + if len(password) < 6: + flash('Password must be at least 6 characters', 'danger') + return redirect(url_for('register')) + + if invitation_code != INVITATION_CODE: + flash('Invalid invitation code', 'danger') + return redirect(url_for('register')) + + password_hash = sha256_hash(password) + + if add_user(username, password_hash, is_vip=False): + # Create user directory + user_base = get_user_base_path(username) + os.makedirs(user_base, exist_ok=True) + + log_access() + flash('Registration successful, please login', 'success') + return redirect(url_for('login')) + else: + flash('Registration failed', 'danger') + + return render_template('register.html') + +# Change password route +@app.route('/change_password', methods=['GET', 'POST']) +@login_required +def change_password(): + username = session['username'] + log_access() + + if request.method == 'POST': + current_password = request.form.get('current_password', '') + new_password = request.form.get('new_password', '') + confirm_password = request.form.get('confirm_password', '') + + user = get_user_by_username(username) + if not user or user['password_hash'] != sha256_hash(current_password): + flash('Current password is incorrect', 'danger') + return redirect(url_for('change_password')) + + if len(new_password) < 6: + flash('New password must be at least 6 characters', 'danger') + return redirect(url_for('change_password')) + + if new_password != confirm_password: + flash('Passwords do not match', 'danger') + return redirect(url_for('change_password')) + + update_user_password(username, sha256_hash(new_password)) + flash('Password changed successfully', 'success') + return redirect(url_for('index')) + + return render_template('change_password.html') + +# Logout route +@app.route('/logout') +def logout(): + log_access() + session.pop('username', None) + session.pop('vip', None) + flash('Logged out', 'info') + return redirect(url_for('login')) + +# File upload route +@app.route('/upload', methods=['POST']) +@login_required +def upload(): + username = session['username'] + path = request.form.get('path', '') + + # Validate path + full_path = validate_user_path(path, username) + if full_path is None: + flash('Invalid path', 'danger') + return redirect(url_for('index')) + + log_access() + + if 'file' not in request.files: + flash('No file uploaded', 'danger') + return redirect(url_for('index', path=path)) + + files = request.files.getlist('file') + + if not files or files[0].filename == '': + flash('No file selected', 'danger') + return redirect(url_for('index', path=path)) + + success_count = 0 + error_count = 0 + + for file in files: + if file: + # Check forbidden filename + if is_forbidden_filename(file.filename): + error_count += 1 + continue + + filename = safe_filename(file.filename) + + # Validate filename doesn't contain path traversal + if is_path_traversal(filename): + error_count += 1 + continue + + file_path = os.path.join(full_path, filename) + + # Handle duplicate files + if os.path.exists(file_path): + name_without_ext, ext = os.path.splitext(filename) + i = 1 + while os.path.exists(os.path.join(full_path, f"{name_without_ext}({i}){ext}")): + i += 1 + filename = f"{name_without_ext}({i}){ext}" + file_path = os.path.join(full_path, filename) + + try: + file.save(file_path) + + user = get_user_by_username(username) + user_id = user['id'] if user else None + + if user_id: + file_id = str(uuid.uuid4()) + db = get_db() + db.execute( + 'INSERT INTO files (id, name, path, user_id, size) VALUES (?, ?, ?, ?, ?)', + (file_id, filename, os.path.join(path, filename), user_id, os.path.getsize(file_path)) + ) + db.commit() + + success_count += 1 + except Exception as e: + error_count += 1 + print(f"Upload failed: {str(e)}") + + if success_count > 0: + flash(f'Successfully uploaded {success_count} file(s)', 'success') + if error_count > 0: + flash(f'Failed to upload {error_count} file(s)', 'danger') + + return redirect(url_for('index', path=path)) + +# Create folder route +@app.route('/create_folder', methods=['POST']) +@login_required +def create_folder(): + username = session['username'] + path = request.form.get('path', '') + folder_name = request.form.get('folder_name', '').strip() + + # Validate path + full_path = validate_user_path(path, username) + if full_path is None: + flash('Invalid path', 'danger') + return redirect(url_for('index')) + + log_access() + + if not folder_name: + flash('Folder name cannot be empty', 'danger') + return redirect(url_for('index', path=path)) + + # Check forbidden folder name + if is_forbidden_filename(folder_name) or is_path_traversal(folder_name): + flash('Invalid folder name', 'danger') + return redirect(url_for('index', path=path)) + + folder_path = os.path.join(full_path, folder_name) + + if os.path.exists(folder_path): + flash('Folder already exists', 'danger') + return redirect(url_for('index', path=path)) + + try: + os.makedirs(folder_path) + flash(f'Folder "{folder_name}" created successfully', 'success') + except Exception as e: + flash(f'Failed to create folder: {str(e)}', 'danger') + + return redirect(url_for('index', path=path)) + +# Rename route +@app.route('/rename', methods=['POST']) +@login_required +def rename(): + username = session['username'] + path = request.form.get('path', '') + item_path = request.form.get('item_path', '') + new_name = request.form.get('new_name', '').strip() + + # Validate paths + full_parent_path = validate_user_path(path, username) + if full_parent_path is None: + flash('Invalid path', 'danger') + return redirect(url_for('index')) + + full_item_path = validate_user_path(item_path, username) + if full_item_path is None: + flash('Invalid item path', 'danger') + return redirect(url_for('index', path=path)) + + log_access() + + if not item_path or not new_name: + flash('Invalid request', 'danger') + return redirect(url_for('index', path=path)) + + if is_forbidden_filename(new_name) or is_path_traversal(new_name): + flash('Invalid name', 'danger') + return redirect(url_for('index', path=path)) + + old_full_path = full_item_path + new_full_path = os.path.join(os.path.dirname(old_full_path), new_name) + + if not os.path.exists(old_full_path): + flash('File or folder does not exist', 'danger') + return redirect(url_for('index', path=path)) + + if os.path.exists(new_full_path): + flash('Target name already exists', 'danger') + return redirect(url_for('index', path=path)) + + # Verify new path is still within user directory + new_validated = validate_user_path(os.path.join(path, new_name), username) + if new_validated is None: + flash('Invalid target path', 'danger') + return redirect(url_for('index', path=path)) + + try: + os.rename(old_full_path, new_full_path) + + user = get_user_by_username(username) + user_id = user['id'] if user else None + + if user_id and os.path.isfile(new_full_path): + db = get_db() + new_db_path = os.path.join(os.path.dirname(item_path), new_name) + db.execute( + 'UPDATE files SET name = ?, path = ? WHERE user_id = ? AND path = ?', + (new_name, new_db_path, user_id, item_path) + ) + db.commit() + + flash('Renamed successfully', 'success') + except Exception as e: + flash(f'Rename failed: {str(e)}', 'danger') + + return redirect(url_for('index', path=path)) + +# Delete route +@app.route('/delete', methods=['POST']) +@login_required +def delete(): + username = session['username'] + path = request.form.get('path', '') + item_path = request.form.get('item_path', '') + + # Validate paths + full_item_path = validate_user_path(item_path, username) + if full_item_path is None: + flash('Invalid item path', 'danger') + return redirect(url_for('index', path=path)) + + log_access() + + if not item_path: + flash('Invalid path', 'danger') + return redirect(url_for('index', path=path)) + + try: + if os.path.isdir(full_item_path): + # Delete folder and its contents + for root, dirs, files in os.walk(full_item_path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(full_item_path) + flash(f'Folder "{os.path.basename(item_path)}" deleted', 'success') + else: + os.remove(full_item_path) + + user = get_user_by_username(username) + user_id = user['id'] if user else None + + if user_id: + db = get_db() + db.execute( + 'DELETE FROM files WHERE user_id = ? AND path = ?', + (user_id, item_path) + ) + db.commit() + + flash(f'File "{os.path.basename(item_path)}" deleted', 'success') + except Exception as e: + flash(f'Delete failed: {str(e)}', 'danger') + + return redirect(url_for('index', path=path)) + +# Download route +@app.route('/download/') +@login_required +def download(file_path): + username = session['username'] + + # Validate path + full_path = validate_user_path(file_path, username) + if full_path is None: + flash('Invalid file path', 'danger') + return redirect(url_for('index')) + + log_access() + + if not os.path.exists(full_path) or os.path.isdir(full_path): + flash('File not found', 'danger') + return redirect(url_for('index', path=os.path.dirname(file_path))) + + try: + return send_from_directory( + os.path.dirname(full_path), + os.path.basename(full_path), + as_attachment=True + ) + except Exception as e: + flash(f'Download failed: {str(e)}', 'danger') + return redirect(url_for('index', path=os.path.dirname(file_path))) + +# Preview route +@app.route('/preview/') +@login_required +def preview(file_path): + username = session['username'] + + # Validate path + full_path = validate_user_path(file_path, username) + if full_path is None: + flash('Invalid file path', 'danger') + return redirect(url_for('index')) + + log_access() + + if not os.path.exists(full_path) or os.path.isdir(full_path): + flash('File not found', 'danger') + return redirect(url_for('index', path=os.path.dirname(file_path))) + + file_ext = os.path.splitext(file_path)[1].lower() + + if file_ext in {'.png', '.jpg', '.jpeg', '.gif', '.txt'}: + return send_from_directory(os.path.dirname(full_path), os.path.basename(full_path)) + elif file_ext == '.pdf': + return render_template('preview_pdf.html', file_path=url_for('download', file_path=file_path)) + elif file_ext == '.mp4': + return render_template('preview_video.html', file_path=url_for('download', file_path=file_path)) + elif file_ext == '.mp3': + return render_template('preview_audio.html', file_path=url_for('download', file_path=file_path)) + + flash('Preview not supported', 'warning') + return redirect(url_for('index', path=os.path.dirname(file_path))) + +# Search route +@app.route('/search') +@login_required +def search(): + username = session['username'] + query = request.args.get('query', '').lower() + path = request.args.get('path', '') + + # Validate path + full_path = validate_user_path(path, username) + if full_path is None: + flash('Invalid path', 'danger') + return redirect(url_for('index')) + + log_access() + + results = [] + + if os.path.exists(full_path) and os.path.isdir(full_path): + for root, dirs, files in os.walk(full_path): + for dir_name in dirs: + if query in dir_name.lower(): + rel_dir = os.path.relpath(os.path.join(root, dir_name), full_path) + results.append({ + 'name': dir_name, + 'path': os.path.join(path, rel_dir), + 'is_dir': True, + 'size': '-', + 'modified': format_time(os.path.getmtime(os.path.join(root, dir_name))) + }) + + for file_name in files: + if query in file_name.lower(): + rel_file = os.path.relpath(os.path.join(root, file_name), full_path) + file_path = os.path.join(path, rel_file) + file_ext = file_name.rsplit('.', 1)[1].lower() if '.' in file_name else '' + results.append({ + 'name': file_name, + 'path': file_path, + 'is_dir': False, + 'size': format_size(os.path.getsize(os.path.join(root, file_name))), + 'modified': format_time(os.path.getmtime(os.path.join(root, file_name))), + 'ext': file_ext, + 'previewable': file_ext in {'png', 'jpg', 'jpeg', 'gif', 'mp4', 'mp3', 'pdf', 'txt'} + }) + + results.sort(key=lambda x: (0 if x['is_dir'] else 1, x['name'].lower())) + + breadcrumbs = [] + parts = path.split(os.sep) if path else [] + current_path = '' + breadcrumbs.append({'name': 'Root', 'path': ''}) + + for part in parts: + current_path = os.path.join(current_path, part) if current_path else part + breadcrumbs.append({'name': part, 'path': current_path}) + + breadcrumbs.append({'name': f'Search: {query}', 'path': ''}) + + vip_user = is_vip_user(username) + + return render_template('search_results.html', results=results, breadcrumbs=breadcrumbs, path=path, query=query, vip_user=vip_user) + +# Batch download route +@app.route('/batch_download', methods=['POST']) +@login_required +def batch_download(): + username = session['username'] + selected_items = request.form.getlist('selected_items') + path = request.form.get('path', '') + + log_access() + + if not selected_items: + flash('Please select files to download', 'warning') + return redirect(url_for('index', path=path)) + + buffer = BytesIO() + with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zipf: + for item_path in selected_items: + # Validate each item + full_path = validate_user_path(item_path, username) + if full_path is None: + continue + + if os.path.isdir(full_path): + for root, dirs, files in os.walk(full_path): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, get_user_base_path(username)) + zipf.write(file_path, arcname) + else: + arcname = os.path.relpath(full_path, get_user_base_path(username)) + zipf.write(full_path, arcname) + + buffer.seek(0) + return send_file( + buffer, + as_attachment=True, + download_name=f'dvs_download_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip', + mimetype='application/zip' + ) + +# Batch delete route +@app.route('/batch_delete', methods=['POST']) +@login_required +def batch_delete(): + username = session['username'] + selected_items = request.form.getlist('selected_items') + path = request.form.get('path', '') + + log_access() + + if not selected_items: + flash('Please select items to delete', 'warning') + return redirect(url_for('index', path=path)) + + success_count = 0 + error_count = 0 + + for item_path in selected_items: + full_path = validate_user_path(item_path, username) + if full_path is None: + error_count += 1 + continue + + try: + if os.path.isdir(full_path): + for root, dirs, files in os.walk(full_path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(full_path) + else: + os.remove(full_path) + + user = get_user_by_username(username) + user_id = user['id'] if user else None + + if user_id: + db = get_db() + db.execute( + 'DELETE FROM files WHERE user_id = ? AND path = ?', + (user_id, item_path) + ) + db.commit() + + success_count += 1 + except Exception as e: + error_count += 1 + print(f"Delete failed: {str(e)}") + + if success_count > 0: + flash(f'Successfully deleted {success_count} item(s)', 'success') + if error_count > 0: + flash(f'Failed to delete {error_count} item(s)', 'danger') + + return redirect(url_for('index', path=path)) + +# Share route +@app.route('/share', methods=['POST']) +@login_required +def share(): + username = session['username'] + item_path = request.form.get('item_path', '') + + # Validate path + full_path = validate_user_path(item_path, username) + if full_path is None: + flash('Invalid item path', 'danger') + return redirect(url_for('index')) + + log_access() + + if not item_path or not os.path.exists(full_path): + flash('Invalid file/folder', 'danger') + return redirect(url_for('index', path=os.path.dirname(item_path))) + + user = get_user_by_username(username) + if not user: + flash('User not found', 'danger') + return redirect(url_for('index', path=os.path.dirname(item_path))) + + share_id = hashlib.sha256(f"{username}{item_path}{time.time()}".encode()).hexdigest()[:10] + + db = get_db() + db.execute( + 'INSERT INTO shares (share_id, user_id, original_path, is_dir) VALUES (?, ?, ?, ?)', + (share_id, user['id'], item_path, 1 if os.path.isdir(full_path) else 0) + ) + db.commit() + + share_url = url_for('download_shared', share_id=share_id, _external=True) + flash(f'Share link created: {share_url}', 'success') + + return redirect(url_for('index', path=os.path.dirname(item_path))) + +# Unshare route +@app.route('/unshare', methods=['POST']) +@login_required +def unshare(): + username = session['username'] + share_id = request.form.get('share_id', '') + + log_access() + + user = get_user_by_username(username) + if not user: + flash('User not found', 'danger') + return redirect(url_for('shared_files')) + + db = get_db() + share = db.execute( + 'SELECT * FROM shares WHERE share_id = ? AND user_id = ?', + (share_id, user['id']) + ).fetchone() + + if share: + db.execute('DELETE FROM shares WHERE share_id = ?', (share_id,)) + db.commit() + flash('Share cancelled', 'success') + else: + flash('Invalid share ID or no permission', 'danger') + + return redirect(url_for('shared_files')) + +# Shared files list route +@app.route('/shared_files') +@login_required +def shared_files(): + username = session['username'] + log_access() + + user = get_user_by_username(username) + if not user: + flash('User not found', 'danger') + return redirect(url_for('index')) + + db = get_db() + user_shares = db.execute( + 'SELECT * FROM shares WHERE user_id = ? ORDER BY created_at DESC', + (user['id'],) + ).fetchall() + + shares_list = [] + for share in user_shares: + shares_list.append({ + 'share_id': share['share_id'], + 'name': os.path.basename(share['original_path']), + 'path': share['original_path'], + 'created_at': share['created_at'], + 'downloads': share['downloads'], + 'is_dir': bool(share['is_dir']) + }) + + return render_template('shared_files.html', shared_items=shares_list, vip_user=is_vip_user(username)) + +# Download shared file route (public, no login required) +@app.route('/s/') +def download_shared(share_id): + log_access() + + db = get_db() + share_info = db.execute( + 'SELECT s.*, u.username FROM shares s JOIN users u ON s.user_id = u.id WHERE s.share_id = ?', + (share_id,) + ).fetchone() + + if not share_info: + flash('Invalid share link', 'danger') + return redirect(url_for('index')) + + # Validate path for the original user + username = share_info['username'] + original_path = share_info['original_path'] + full_path = validate_user_path(original_path, username) + + if full_path is None or not os.path.exists(full_path): + flash('File has been deleted', 'danger') + return redirect(url_for('index')) + + # Update download count + db.execute( + 'UPDATE shares SET downloads = downloads + 1 WHERE share_id = ?', + (share_id,) + ) + db.commit() + + if os.path.isdir(full_path): + # If folder, create ZIP + buffer = BytesIO() + with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zipf: + for root, dirs, files in os.walk(full_path): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, os.path.dirname(full_path)) + zipf.write(file_path, arcname) + + buffer.seek(0) + folder_name = os.path.basename(full_path) + return send_file( + buffer, + as_attachment=True, + download_name=f'{folder_name}.zip', + mimetype='application/zip' + ) + + # If file, direct download + return send_from_directory( + os.path.dirname(full_path), + os.path.basename(full_path), + as_attachment=True + ) + +# API: Storage usage +@app.route('/api/storage_usage') +@login_required +def get_storage_usage(): + username = session['username'] + user_base = get_user_base_path(username) + + total_size = 0 + if os.path.exists(user_base): + for dirpath, dirnames, filenames in os.walk(user_base): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + try: + total_size += os.path.getsize(filepath) + except OSError: + continue + + return jsonify({ + 'used': total_size, + 'used_formatted': format_size(total_size), + 'free': '∞', + 'free_formatted': '∞', + 'total': '∞', + 'total_formatted': '∞' + }) + +# ==================== MAIN ==================== +if __name__ == '__main__': + init_db() + + # Ensure all user directories exist + with app.app_context(): + db = get_db() + users = db.execute('SELECT username FROM users').fetchall() + for user in users: + user_base = get_user_base_path(user['username']) + os.makedirs(user_base, exist_ok=True) + + app.run(debug=True, host='0.0.0.0', port=8080) diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..56d1959 Binary files /dev/null and b/favicon.ico differ diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..38daacd --- /dev/null +++ b/templates/404.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ +
+

404 - 页面未找到

+

您要访问的页面不存在或已被移动。

+ +
+{% endblock %} diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..5c0c607 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ +
+

500 - 服务器内部错误

+

发生了一些意外情况,请稍后再试。

+ +
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..08cfdbd --- /dev/null +++ b/templates/base.html @@ -0,0 +1,742 @@ + + + + + + {% block title %}DVS云盘{% endblock %} + + + + + + + + + + + + + + + + + + {% block extra_css %}{% endblock %} + + + + + + +
+ {% block content %}{% endblock %} +
+ + + + + + + + + + + {% block extra_js %}{% endblock %} + + +
+
+

🚀 DVS云盘 · 某个开发者的作品

+

+ 安全的代码是优美的代码 · + 用户友好的设计是简单的设计 · + 每一次302重定向都是明了的防护 +

+

+ © 2026 DVS云盘 保留所有权利 +

+
+
+ + \ No newline at end of file diff --git a/templates/change_password.html b/templates/change_password.html new file mode 100644 index 0000000..acf72fe --- /dev/null +++ b/templates/change_password.html @@ -0,0 +1,86 @@ +{% extends "base.html" %} + +{% block title %}修改密码 - DVS云盘{% endblock %} + +{% block content %} +
+
+
+
+
+ +
修改密码
+
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ + {{ message|safe }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + +
+ +
+ + +
密码长度至少6个字符
+
+ +
+ + +
请再次输入新密码以确保一致性
+
+ +
+ + 返回 + + +
+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/favicon.ico b/templates/favicon.ico new file mode 100644 index 0000000..56d1959 Binary files /dev/null and b/templates/favicon.ico differ diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..71536ce --- /dev/null +++ b/templates/index.html @@ -0,0 +1,973 @@ +{% extends "base.html" %} +{% block title %}DVS云盘 - 首页{% endblock %} + +{% block content %} + +
+
+
+ 根目录 +
+ {% if breadcrumbs %} + {% for crumb in breadcrumbs[1:] %} +
/
+ + {% endfor %} + {% endif %} +
+
+ + +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+
+ {% if category == 'success' %} + + {% elif category == 'danger' %} + + {% elif category == 'warning' %} + + {% else %} + + {% endif %} +
+
+ {% if category == 'success' %}成功{% elif category == 'danger' %}错误{% elif category == 'warning' %}警告{% else %}提示{% endif %} +
+
{{ message|safe }}
+
+ +
+
+ {% endfor %} +
+ + {% endif %} +{% endwith %} + + +
+
+

我的文件

+

管理你的文件与文件夹

+
+
+ + +
+
+ + +
+
+
+ 文件列表 +
+ 共 {{ items|length if items else 0 }} 项 +
+
+ {% if items and items|length > 0 %} + {% for item in items %} +
+ +
+ + + {% if item.is_dir %} + + {% elif item.ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'] %} + + {% elif item.ext in ['mp4', 'avi', 'mov', 'mkv', 'flv', 'wmv', 'webm', 'm4v'] %} + + {% elif item.ext in ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a', 'wma'] %} + + {% elif item.ext == 'pdf' %} + + {% elif item.ext in ['doc', 'docx'] %} + + {% elif item.ext in ['xls', 'xlsx', 'csv'] %} + + {% elif item.ext in ['ppt', 'pptx'] %} + + {% elif item.ext in ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'] %} + + {% elif item.ext in ['py', 'js', 'java', 'c', 'cpp', 'h', 'html', 'css', 'php', 'rb', 'go', 'rs', 'swift', 'kt'] %} + + {% elif item.ext in ['txt', 'md', 'log', 'ini', 'cfg', 'conf'] %} + + {% elif item.ext in ['exe', 'msi', 'dmg', 'pkg', 'deb', 'rpm', 'appimage'] %} + + {% elif item.ext in ['iso', 'img', 'vhd', 'vmdk'] %} + + {% elif item.ext in ['psd', 'ai', 'eps'] %} + + {% elif item.ext in ['bin', 'dat', 'db'] %} + + {% else %} + + {% endif %} +
+ + +
+
+ +
+ {% if item.is_dir %} + {{ item.name }} + {% else %} + {{ item.name }} + {% endif %} + + + {% if not item.is_dir %} + + + {% if item.ext %} + {{ item.ext|upper }} - + {% if item.ext in ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a', 'wma'] %}音频文件 + {% elif item.ext in ['mp4', 'avi', 'mov', 'mkv', 'flv', 'wmv', 'webm', 'm4v'] %}视频文件 + {% elif item.ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'] %}图片文件 + {% elif item.ext == 'pdf' %}PDF文档 + {% elif item.ext in ['doc', 'docx'] %}Word文档 + {% elif item.ext in ['xls', 'xlsx', 'csv'] %}Excel表格 + {% elif item.ext in ['ppt', 'pptx'] %}演示文稿 + {% elif item.ext in ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'] %}压缩文件 + {% elif item.ext in ['py', 'js', 'java', 'c', 'cpp', 'h', 'html', 'css', 'php', 'rb', 'go', 'rs', 'swift', 'kt'] %}代码文件 + {% elif item.ext in ['txt', 'md', 'log', 'ini', 'cfg', 'conf'] %}文本文件 + {% elif item.ext in ['exe', 'msi', 'dmg', 'pkg', 'deb', 'rpm', 'appimage'] %}可执行文件 + {% elif item.ext in ['iso', 'img', 'vhd', 'vmdk'] %}磁盘镜像 + {% elif item.ext in ['psd', 'ai', 'eps'] %}设计文件 + {% elif item.ext in ['bin', 'dat', 'db'] %}二进制文件 + {% else %}其他文件 + {% endif %} + {% else %} + 未知类型 + {% endif %} + + {% else %} + + + 文件夹目录 + + {% endif %} + + {% if item.previewable %} + 可预览 + {% endif %} +
+ + +
+ {% if item.is_dir %} + 文件夹 + {% else %} + {{ item.size or "未知大小" }} + {% endif %} + {{ item.modified or "未知" }} + {% if not item.is_dir %} + 扩展名: {{ item.ext|upper if item.ext else '无' }} + {% endif %} +
+
+
+ + +
+ + {% if item.is_dir %} + + + + {% endif %} + + {% if not item.is_dir %} + + + + {% if item.previewable %} + + + + {% endif %} + {% endif %} + + + + +
+
+ {% endfor %} + {% else %} +
+ +
+
这里还没有文件
+
点击右上角上传文件或创建文件夹。
+
+
+ {% endif %} +
+
+ + + + + + + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..8fc4fae --- /dev/null +++ b/templates/login.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} +{% block title %}登录 - DVS云盘{% endblock %} + +{% block content %} +
+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ + {{ message|safe }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ +

DVS云盘

+

安全、高效的云端存储解决方案

+
+
+
+
+
+ + +
+
+ + +
+ +
+
+
+

还没有账户? 立即注册

+
+
+
数据加密
+
高速传输
+
多端同步
+
+
+
+ +
+

安全提示:请妥善保管您的登录凭证

+
+ + +
+
+
🚀 DVS云盘
+
+
安全的代码是优美的代码
+
用户友好的设计是简单的设计
+
每一次302重定向都是明了的防护
+
每一行日志都是错误的明确记录
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/preview_audio.html b/templates/preview_audio.html new file mode 100644 index 0000000..f3f1635 --- /dev/null +++ b/templates/preview_audio.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}音频预览 - DVS云盘{% endblock %} + +{% block content %} +
+
+
+
+
+ +
音频预览
+
+ + 返回 + +
+
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/preview_pdf.html b/templates/preview_pdf.html new file mode 100644 index 0000000..1ce1030 --- /dev/null +++ b/templates/preview_pdf.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %}PDF预览 - DVS云盘{% endblock %} + +{% block content %} +
+
+
+
+
+ +
PDF预览
+
+ + 返回 + +
+
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/preview_video.html b/templates/preview_video.html new file mode 100644 index 0000000..568f496 --- /dev/null +++ b/templates/preview_video.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}视频预览 - DVS云盘{% endblock %} + +{% block content %} +
+
+
+
+ +
视频预览
+
+ + 返回 + +
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..3b0f07c --- /dev/null +++ b/templates/register.html @@ -0,0 +1,179 @@ +{% extends "base.html" %} + +{% block title %}注册 - DVS云盘{% endblock %} + +{% block content %} +
+
+
+
+
+ +
注册账户
+
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ + {{ message|safe }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ + +
用户名长度3-20个字符,支持字母、数字和下划线
+
+ +
+ + +
密码长度至少6个字符
+
+ +
+ + +
请再次输入密码以确保一致性
+
+ +
+ + +
请联系管理员获取邀请码
+
+ +
+
+ + +
+
+ +
+ +
+
+ +
+

+ 已有账户? + + 立即登录 + +

+
+
+
+ +
+
+
+ +
VIP专属特权
+
+
+
+
+
+
+ +
+

超大存储

+
+
+
+ +
+

极速传输

+
+
+
+ +
+

高级安全

+
+
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/search_results.html b/templates/search_results.html new file mode 100644 index 0000000..3470b3d --- /dev/null +++ b/templates/search_results.html @@ -0,0 +1,128 @@ +{% extends "base.html" %} + +{% block title %}搜索结果 - DVS云盘{% endblock %} + +{% block content %} +
+ +
+
+ {% for crumb in breadcrumbs %} + {% if loop.last %} + {{ crumb.name }} + {% else %} + + {{ crumb.name }} + + + {% endif %} + {% endfor %} +
+
+ + +
+
+
+ +
搜索结果
+ {{ results|length }} 个匹配项 +
+
+
+

+ 搜索关键词: "{{ query }}" +

+
+
+ + +
+
+
+ +
搜索结果列表
+
+
+
+ {% if results %} +
+ {% for item in results %} +
+
+
+ {% if item.is_dir %} +
+ +
+ {% else %} + {% if item.ext in ['png', 'jpg', 'jpeg', 'gif'] %} +
+ +
+ {% elif item.ext in ['mp4', 'avi', 'mov'] %} +
+ +
+ {% elif item.ext in ['mp3', 'wav'] %} +
+ +
+ {% elif item.ext == 'pdf' %} +
+ +
+ {% else %} +
+ +
+ {% endif %} + {% endif %} +
+
+
+ {{ item.name }} +
+
+ {% if item.is_dir %} + 文件夹 • {{ item.path }} + {% else %} + 文件 • {{ item.size }} • {{ item.path }} + {% endif %} + • 修改时间: {{ item.modified }} +
+
+
+ {% if item.is_dir %} + + + + {% else %} + {% if item.previewable %} + + + + {% endif %} + + + + {% endif %} +
+
+
+ {% endfor %} +
+ {% else %} +
+ +
没有找到匹配的结果
+

请尝试使用其他关键词搜索

+ + 返回文件列表 + +
+ {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/shared_files.html b/templates/shared_files.html new file mode 100644 index 0000000..eb2e93c --- /dev/null +++ b/templates/shared_files.html @@ -0,0 +1,618 @@ +{% extends "base.html" %} +{% block title %}已分享文件 - DVS云盘{% endblock %} + +{% block content %} + +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+
+ {% if category == 'success' %} + + {% elif category == 'danger' %} + + {% elif category == 'warning' %} + + {% else %} + + {% endif %} +
+
+ {% if category == 'success' %}成功{% elif category == 'danger' %}错误{% elif category == 'warning' %}警告{% else %}提示{% endif %} +
+
{{ message|safe }}
+
+ +
+
+ {% endfor %} +
+ + {% endif %} +{% endwith %} + +
+
+

已分享文件

+

管理你分享的文件和文件夹

+
+ +
+ +
+
+
+ 分享列表 +
+ 共 {{ shared_items|length }} 个分享 +
+
+ {% if shared_items %} + {% for item in shared_items %} +
+ +
+ {% if item.is_dir %} + + {% else %} + + {% endif %} +
+ + +
+
+ +
+ {{ item.name }} + + + {% if not item.is_dir %} + + + 分享文件 + + {% else %} + + + 分享文件夹 + + {% endif %} +
+ + +
+ 分享ID: {{ item.share_id }} + 创建时间: {{ item.created_at }} + 下载次数: {{ item.downloads }} + 路径: {{ item.path }} +
+
+
+ + +
+ + + + +
+
+ {% endfor %} + {% else %} +
+ +
+
还没有分享任何文件
+
从文件列表中选择文件或文件夹并点击"分享"按钮来创建分享链接。
+
+
+ + {% endif %} +
+
+ + + + + + + +{% endblock %} \ No newline at end of file