手机上运行的网盘代码

精品源码1周前更新 小编
6 0

除了文件移动没有外,基础功能都是有的,应该没有bug吧。在Termux上运行,附带Termux的安卓安装包。
具体信息请看视频:https://b23.tv/Zm823vt
代码地址:
手机上运行的网盘代码

[Python]

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import os
  4. import sys
  5. import json
  6. import asyncio
  7. import socket
  8. import hashlib
  9. import mimetypes
  10. import uuid
  11. import shutil
  12. import re
  13. import time
  14. import subprocess
  15. from datetime import datetime, timedelta
  16. from pathlib import Path
  17. from urllib.parse import unquote, quote
  18. from typing import Dict, List, Optional, Tuple
  19. # -------------------- 自动安装依赖 --------------------
  20. def ensure_dependencies():
  21.     required = {
  22.         'aiohttp': 'aiohttp',
  23.         'aiofiles': 'aiofiles',
  24.         'aiosqlite': 'aiosqlite',
  25.         'netifaces': 'netifaces',
  26.     }
  27.     for module, package in required.items():
  28.         try:
  29.             __import__(module)
  30.         except ImportError:
  31.             print(f"[*] 缺少依赖: {package},正在安装...")
  32.             try:
  33.                 subprocess.check_call(
  34.                     [sys.executable, "-m", "pip", "install", package]
  35.                 )
  36.                 print(f"[+] {package} 安装成功")
  37.             except Exception as e:
  38.                 print(f"[-] 安装失败: {e},请手动安装 {package}")
  39.                 sys.exit(1)
  40. ensure_dependencies()
  41. import aiohttp
  42. from aiohttp import web
  43. try:
  44.     from aiohttp.client_exceptions import ClientDisconnectedError
  45. except ImportError:
  46.     try:
  47.         from aiohttp import ClientDisconnectedError
  48.     except ImportError:
  49.         from aiohttp import ClientError as ClientDisconnectedError
  50. import aiofiles
  51. import aiofiles.os
  52. import aiosqlite
  53. mimetypes.init()
  54. # -------------------- 日志记录器 --------------------
  55. class Logger:
  56.     def __init__(self, log_file='cloud_drive.log'):
  57.         self.log_file = log_file
  58.         log_dir = os.path.dirname(log_file)
  59.         if log_dir and not os.path.exists(log_dir):
  60.             os.makedirs(log_dir, exist_ok=True)
  61.         self._write_startup_marker()
  62.         self.download_requests: Dict[str, set] = {}
  63.     def _write_startup_marker(self):
  64.         timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  65.         marker = f"\n{'='*60}\n程序启动时间: {timestamp}\n{'='*60}\n"
  66.         try:
  67.             with open(self.log_file, 'a', encoding='utf-8') as f:
  68.                 f.write(marker)
  69.         except Exception:
  70.             pass
  71.     def _log(self, ip: str, user: str, action: str, details: str = "", status: str = ""):
  72.         timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  73.         parts = [f"[{timestamp}]"]
  74.         if ip:
  75.             parts.append(ip)
  76.         if user:
  77.             parts.append(f"{user} -")
  78.         parts.append(action)
  79.         if details:
  80.             parts.append(details)
  81.         if status:
  82.             parts.append(f"状态{status}")
  83.         log_entry = " ".join(parts)
  84.         print(log_entry)
  85.         try:
  86.             with open(self.log_file, 'a', encoding='utf-8') as f:
  87.                 f.write(log_entry + "\n")
  88.         except Exception:
  89.             pass
  90.     def log_access(self, ip, user, path, status):
  91.         if not path:
  92.             path = "/"
  93.         action = "访问成功"
  94.         details = path if path != "/" else "/"
  95.         self._log(ip, user, action, details, str(status))
  96.     def log_download(self, ip, user, path, force_download, status, is_range_request=False, request_id=None):
  97.         if is_range_request:
  98.             return
  99.         key = f"{user}:{path}"
  100.         if request_id:
  101.             if key not in self.download_requests:
  102.                 self.download_requests[key] = set()
  103.             if request_id in self.download_requests[key]:
  104.                 return
  105.             self.download_requests[key].add(request_id)
  106.         download_type = "下载" if force_download else "预览"
  107.         action = "文件下载"
  108.         details = f"{path} ({download_type})"
  109.         self._log(ip, user, action, details, str(status))
  110.     def log_upload(self, ip, user, file_count, path="", status=200):
  111.         action = "文件上传"
  112.         details = f"{file_count} 个文件"
  113.         if path:
  114.             details += f" {path}"
  115.         self._log(ip, user, action, details, str(status))
  116.     def log_create_folder(self, ip, user, path, status=200):
  117.         self._log(ip, user, "创建文件夹成功", path, str(status))
  118.     def log_rename(self, ip, user, old_path, new_name, status=200):
  119.         self._log(ip, user, "重命名成功", f"{old_path} -> {new_name}", str(status))
  120.     def log_delete(self, ip, user, path, status=200):
  121.         self._log(ip, user, "删除成功", path, str(status))
  122.     def log_server_event(self, msg):
  123.         timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  124.         entry = f"[{timestamp}] {msg}"
  125.         print(entry)
  126.         try:
  127.             with open(self.log_file, 'a', encoding='utf-8') as f:
  128.                 f.write(entry + "\n")
  129.         except Exception:
  130.             pass
  131.     def log_error(self, ip="", user="", action="", details="", error="", is_range_request=False):
  132.         if is_range_request and error in ["Cannot write to closing transport", "Connection lost"]:
  133.             return
  134.         timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  135.         parts = [f"[{timestamp}]"]
  136.         if ip:
  137.             parts.append(ip)
  138.         if user:
  139.             parts.append(f"{user} -")
  140.         if action:
  141.             parts.append(f"{action}失败")
  142.         if details:
  143.             parts.append(details)
  144.         if error:
  145.             parts.append(f"错误: {error}")
  146.         entry = " ".join(parts)
  147.         print(entry)
  148.         try:
  149.             with open(self.log_file, 'a', encoding='utf-8') as f:
  150.                 f.write(entry + "\n")
  151.         except Exception:
  152.             pass
  153.     def log_login_failed(self, ip, username, reason=""):
  154.         self._log(ip, "游客", "登录失败", f"用户名: {username} {reason}".strip(), "401")
  155.     def log_register_success(self, ip, username):
  156.         self._log(ip, "游客", "注册成功", f"新用户: {username}", "200")
  157.     def log_delete_user_success(self, ip, username, user_id):
  158.         self._log(ip, username, "删除用户成功", f"用户: {username} (ID: {user_id})", "200")
  159.     def log_visitor_login_page(self, ip):
  160.         self._log(ip, "游客", "访问登录页面")
  161.     def log_visitor_register_page(self, ip):
  162.         self._log(ip, "游客", "访问注册页面")
  163.     def clear_download_cache(self):
  164.         if len(self.download_requests) > 1000:
  165.             keys = list(self.download_requests.keys())
  166.             for key in keys[:500]:
  167.                 del self.download_requests[key]
  168. # -------------------- 云盘服务器 --------------------
  169. class CloudDriveServer:
  170.     def __init__(self, config: dict):
  171.         self.port = config.get('port', 8000)
  172.         self.allow_register = config.get('allow_register', True)
  173.         self.base_dir = Path(__file__).parent.resolve()
  174.         self.uploads_dir = self.base_dir / 'uploads'
  175.         self.db_path = self.base_dir / 'cloud_drive.db'
  176.         self.logger = Logger()
  177.         self.active_connections = set()
  178.         self.is_running = False
  179.         self.site = None
  180.         self.runner = None
  181.         self.app = None
  182.         self.uploads_dir.mkdir(exist_ok=True)
  183.     # ---------- 工具方法 ----------
  184.     def hash_password(self, password: str, salt: bytes = None) -> Tuple[str, str]:
  185.         if salt is None:
  186.             salt = hashlib.sha256(os.urandom(60)).digest()
  187.         pwdhash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
  188.         return pwdhash.hex(), salt.hex()
  189.     def normalize_path(self, path: str) -> str:
  190.         if not path:
  191.             return ''
  192.         path = path.replace('\\', '/')
  193.         path = re.sub(r'/+', '/', path)
  194.         if path.startswith('/'):
  195.             path = path[1:]
  196.         return path.rstrip('/')
  197.     def sanitize_path(self, path: str) -> str:
  198.         if not path:
  199.             return path
  200.         cleaned = re.sub(r'[\x00-\x1f\x7f]', '', path)
  201.         cleaned = re.sub(r'[<>:"|?*]', '', cleaned)
  202.         cleaned = re.sub(r'[/\\]{2,}', '/', cleaned)
  203.         cleaned = re.sub(r'\.{2,}', '.', cleaned)
  204.         cleaned = cleaned.strip(' .')
  205.         return cleaned
  206.     def preprocess_path(self, raw_path: str) -> str:
  207.         if not raw_path:
  208.             return ''
  209.         try:
  210.             decoded = unquote(raw_path)
  211.         except Exception:
  212.             decoded = raw_path
  213.         normalized = self.normalize_path(decoded)
  214.         cleaned = self.sanitize_path(normalized)
  215.         return cleaned
  216.     def is_safe_path(self, base_path: Path, target_path: str) -> Tuple[bool, str]:
  217.         try:
  218.             base = base_path.absolute()
  219.             processed = self.preprocess_path(target_path)
  220.             target = (base / processed).resolve() if processed else base
  221.             safe = target.is_relative_to(base)
  222.             if not safe:
  223.                 self.logger.log_error(action="路径安全检查", details=f"越权: {target_path}")
  224.             return safe, processed
  225.         except Exception as e:
  226.             self.logger.log_error(action="路径安全检查", error=str(e))
  227.             return False, ""
  228.     def format_file_size(self, size_bytes: int) -> str:
  229.         if size_bytes == 0:
  230.             return "0 B"
  231.         units = ['B', 'KB', 'MB', 'GB', 'TB']
  232.         unit_index = 0
  233.         while size_bytes >= 1024 and unit_index < len(units) - 1:
  234.             size_bytes /= 1024.0
  235.             unit_index += 1
  236.         if unit_index == 0:
  237.             return f"{int(size_bytes)} {units[unit_index]}"
  238.         else:
  239.             return f"{size_bytes:.1f} {units[unit_index]}"
  240.     def get_user_dir(self, username: str) -> Path:
  241.         return self.uploads_dir / username
  242.     def read_template(self, template_name: str, context: Dict = None) -> str:
  243.         template_path = self.base_dir / 'templates' / template_name
  244.         try:
  245.             with open(template_path, 'r', encoding='utf-8') as f:
  246.                 content = f.read()
  247.             if context:
  248.                 for key, value in context.items():
  249.                     content = content.replace(f'{{{{ {key} }}}}', str(value))
  250.             return content
  251.         except Exception as e:
  252.             self.logger.log_error(action="读取模板", details=template_name, error=str(e))
  253.             return ''
  254.     # -------------------- 网络地址获取 --------------------
  255.     @staticmethod
  256.     def _get_ipv4_by_udp():
  257.         try:
  258.             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  259.             s.settimeout(1)
  260.             s.connect(('8.8.8.8', 80))
  261.             ip = s.getsockname()[0]
  262.             s.close()
  263.             return ip
  264.         except Exception:
  265.             return None
  266.     @staticmethod
  267.     def _get_ipv6_by_udp():
  268.         try:
  269.             s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
  270.             s.settimeout(1)
  271.             s.connect(('2001:4860:4860::8888', 80))
  272.             ip = s.getsockname()[0]
  273.             s.close()
  274.             if ip == '::1' or ip.startswith('fe80:'):
  275.                 return None
  276.             return ip
  277.         except Exception:
  278.             return None
  279.     @staticmethod
  280.     def _get_all_ipv6_from_proc():
  281.         ipv6s = set()
  282.         proc_path = '/proc/net/if_inet6'
  283.         if not os.path.isfile(proc_path):
  284.             return ipv6s
  285.         try:
  286.             with open(proc_path, 'r') as f:
  287.                 for line in f:
  288.                     parts = line.strip().split()
  289.                     if len(parts) < 1:
  290.                         continue
  291.                     hex_str = parts[0]
  292.                     ipv6 = ':'.join(hex_str[i:i+4] for i in range(0, 32, 4))
  293.                     if ipv6 == '::1' or ipv6.startswith('fe80:'):
  294.                         continue
  295.                     ipv6s.add(ipv6)
  296.         except Exception:
  297.             pass
  298.         return ipv6s
  299.     @staticmethod
  300.     def _get_all_ipv6_via_command():
  301.         ipv6s = set()
  302.         try:
  303.             result = subprocess.run(['ip', '-6', 'addr', 'show'], capture_output=True, text=True, timeout=5)
  304.             if result.returncode != 0:
  305.                 result = subprocess.run(['ip', 'addr', 'show'], capture_output=True, text=True, timeout=5)
  306.             output = result.stdout
  307.         except Exception:
  308.             try:
  309.                 result = subprocess.run(['ifconfig'], capture_output=True, text=True, timeout=5)
  310.                 output = result.stdout
  311.             except Exception:
  312.                 output = ""
  313.         if output:
  314.             for line in output.splitlines():
  315.                 if 'inet6' not in line:
  316.                     continue
  317.                 m = re.search(r'inet6\s+([0-9a-fA-F:]+)', line)
  318.                 if not m:
  319.                     continue
  320.                 ip = m.group(1).split('/')[0]
  321.                 if (ip == '::1' or ip.startswith('fe80:') or
  322.                     ip.startswith('ff') or ip.startswith('::ffff:')):
  323.                     continue
  324.                 if 'scope host' in line or 'scope link' in line:
  325.                     continue
  326.                 ipv6s.add(ip)
  327.         return ipv6s
  328.     @staticmethod
  329.     def _get_all_ipv6_via_netifaces():
  330.         ipv6s = set()
  331.         try:
  332.             import netifaces
  333.             for interface in netifaces.interfaces():
  334.                 addrs = netifaces.ifaddresses(interface).get(netifaces.AF_INET6, [])
  335.                 for addr_info in addrs:
  336.                     ip = addr_info.get('addr', '')
  337.                     if not ip:
  338.                         continue
  339.                     if (ip == '::1' or ip.startswith('fe80:') or
  340.                         ip.startswith('ff') or ip.startswith('::ffff:')):
  341.                         continue
  342.                     ipv6s.add(ip)
  343.         except ImportError:
  344.             pass
  345.         except Exception:
  346.             pass
  347.         return ipv6s
  348.     def get_network_addresses(self):
  349.         ipv4_set = set()
  350.         ipv6_set = set()
  351.         # IPv4
  352.         udp_ip = self._get_ipv4_by_udp()
  353.         if udp_ip and not udp_ip.startswith('127.'):
  354.             ipv4_set.add(udp_ip)
  355.         try:
  356.             result = subprocess.run(['ip', 'addr', 'show'], capture_output=True, text=True, timeout=5)
  357.             if result.returncode != 0:
  358.                 result = subprocess.run(['ifconfig'], capture_output=True, text=True, timeout=5)
  359.             output = result.stdout
  360.         except Exception:
  361.             try:
  362.                 result = subprocess.run(['ifconfig'], capture_output=True, text=True, timeout=5)
  363.                 output = result.stdout
  364.             except Exception:
  365.                 output = ""
  366.         for m in re.finditer(r'inet\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', output):
  367.             ip = m.group(1)
  368.             if not ip.startswith('127.'):
  369.                 ipv4_set.add(ip)
  370.         if not ipv4_set:
  371.             try:
  372.                 hostname = socket.gethostname()
  373.                 for addr in socket.gethostbyname_ex(hostname)[2]:
  374.                     if not addr.startswith('127.'):
  375.                         ipv4_set.add(addr)
  376.             except:
  377.                 pass
  378.         # IPv6
  379.         udp_ipv6 = self._get_ipv6_by_udp()
  380.         if udp_ipv6:
  381.             ipv6_set.add(udp_ipv6)
  382.         ipv6_set |= self._get_all_ipv6_from_proc()
  383.         ipv6_set |= self._get_all_ipv6_via_command()
  384.         ipv6_set |= self._get_all_ipv6_via_netifaces()
  385.         try:
  386.             hostname = socket.gethostname()
  387.             for info in socket.getaddrinfo(hostname, None, socket.AF_INET6):
  388.                 ip = info[4][0]
  389.                 if (ip == '::1' or ip.startswith('fe80:') or
  390.                     ip.startswith('ff') or ip.startswith('::ffff:')):
  391.                     continue
  392.                 ipv6_set.add(ip)
  393.         except:
  394.             pass
  395.         ipv4_list = sorted(list(ipv4_set), key=lambda x: [int(n) for n in x.split('.')])
  396.         ipv6_list = sorted(list(ipv6_set))
  397.         return ipv4_list, ipv6_list
  398.     # ---------- 数据库操作 ----------
  399.     async def init_db(self):
  400.         async with aiosqlite.connect(self.db_path) as db:
  401.             await db.execute('''
  402.                 CREATE TABLE IF NOT EXISTS users (
  403.                     id INTEGER PRIMARY KEY AUTOINCREMENT,
  404.                     username TEXT UNIQUE NOT NULL,
  405.                     password_hash TEXT NOT NULL,
  406.                     salt TEXT NOT NULL,
  407.                     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  408.                     storage_used INTEGER DEFAULT 0
  409.                 )
  410.             ''')
  411.             await db.execute('''
  412.                 CREATE TABLE IF NOT EXISTS sessions (
  413.                     session_id TEXT PRIMARY KEY,
  414.                     user_id INTEGER NOT NULL,
  415.                     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  416.                     expires_at TIMESTAMP NOT NULL,
  417.                     FOREIGN KEY (user_id) REFERENCES users (id)
  418.                 )
  419.             ''')
  420.             await db.commit()
  421.         self.logger.log_server_event("数据库初始化完成")
  422.     async def get_user_info(self, user_id: int) -> Optional[Dict]:
  423.         async with aiosqlite.connect(self.db_path) as db:
  424.             cursor = await db.execute(
  425.                 'SELECT username, storage_used FROM users WHERE id = ?', (user_id,)
  426.             )
  427.             result = await cursor.fetchone()
  428.             if result:
  429.                 username, used = result
  430.                 return {
  431.                     'username': username,
  432.                     'used': used,
  433.                     'used_formatted': self.format_file_size(used),
  434.                     'percentage': min(100, (used / (1024**3)) * 100) if used > 0 else 0
  435.                 }
  436.         return None
  437.     async def verify_user(self, username: str, password: str) -> Optional[int]:
  438.         async with aiosqlite.connect(self.db_path) as db:
  439.             cursor = await db.execute(
  440.                 'SELECT id, password_hash, salt FROM users WHERE username = ?', (username,)
  441.             )
  442.             user = await cursor.fetchone()
  443.             if user:
  444.                 user_id, stored_hash, salt = user
  445.                 hashed, _ = self.hash_password(password, bytes.fromhex(salt))
  446.                 if hashed == stored_hash:
  447.                     return user_id
  448.         return None
  449.     async def create_user(self, username: str, password: str) -> bool:
  450.         if not self.allow_register:
  451.             return False
  452.         try:
  453.             password_hash, salt = self.hash_password(password)
  454.             async with aiosqlite.connect(self.db_path) as db:
  455.                 cursor = await db.execute('SELECT id FROM users WHERE username = ?', (username,))
  456.                 if await cursor.fetchone():
  457.                     return False
  458.                 await db.execute(
  459.                     'INSERT INTO users (username, password_hash, salt) VALUES (?, ?, ?)',
  460.                     (username, password_hash, salt)
  461.                 )
  462.                 user_dir = self.get_user_dir(username)
  463.                 await aiofiles.os.makedirs(str(user_dir), exist_ok=True)
  464.                 await db.commit()
  465.                 return True
  466.         except Exception as e:
  467.             self.logger.log_error(action="创建用户", error=str(e))
  468.             return False
  469.     async def create_session(self, user_id: int) -> str:
  470.         session_id = str(uuid.uuid4())
  471.         expires_at = datetime.now() + timedelta(days=1)
  472.         async with aiosqlite.connect(self.db_path) as db:
  473.             await db.execute(
  474.                 'INSERT INTO sessions (session_id, user_id, expires_at) VALUES (?, ?, ?)',
  475.                 (session_id, user_id, expires_at)
  476.             )
  477.             await db.commit()
  478.         return session_id
  479.     async def validate_session(self, session_id: str) -> Optional[int]:
  480.         async with aiosqlite.connect(self.db_path) as db:
  481.             cursor = await db.execute(
  482.                 'SELECT user_id FROM sessions WHERE session_id = ? AND expires_at > ?',
  483.                 (session_id, datetime.now())
  484.             )
  485.             result = await cursor.fetchone()
  486.             return result[0] if result else None
  487.     async def delete_session(self, session_id: str):
  488.         async with aiosqlite.connect(self.db_path) as db:
  489.             await db.execute('DELETE FROM sessions WHERE session_id = ?', (session_id,))
  490.             await db.commit()
  491.     async def delete_user(self, user_id: int):
  492.         async with aiosqlite.connect(self.db_path) as db:
  493.             cursor = await db.execute('SELECT username FROM users WHERE id = ?', (user_id,))
  494.             result = await cursor.fetchone()
  495.             if not result:
  496.                 return
  497.             username = result[0]
  498.             user_dir = self.get_user_dir(username)
  499.             if user_dir.exists():
  500.                 try:
  501.                     shutil.rmtree(str(user_dir))
  502.                 except Exception as e:
  503.                     self.logger.log_error(action="删除用户目录", error=str(e))
  504.             self.logger.log_delete_user_success("127.0.0.1", username, user_id)
  505.             await db.execute('DELETE FROM users WHERE id = ?', (user_id,))
  506.             await db.execute('DELETE FROM sessions WHERE user_id = ?', (user_id,))
  507.             await db.commit()
  508.     # ---------- 网络请求处理 ----------
  509.     async def check_auth(self, request):
  510.         session_id = request.cookies.get('session_id')
  511.         if not session_id:
  512.             return None
  513.         user_id = await self.validate_session(session_id)
  514.         if not user_id:
  515.             return None
  516.         async with aiosqlite.connect(self.db_path) as db:
  517.             expires_at = datetime.now() + timedelta(days=1)
  518.             await db.execute(
  519.                 'UPDATE sessions SET expires_at = ? WHERE session_id = ?',
  520.                 (expires_at, session_id)
  521.             )
  522.             await db.commit()
  523.         return user_id
  524.     async def handle_index(self, request):
  525.         session_id = request.cookies.get('session_id')
  526.         if session_id and await self.validate_session(session_id):
  527.             return web.HTTPFound('/cloud')
  528.         return web.HTTPFound('/login')
  529.     async def handle_login(self, request):
  530.         client_ip = request.remote
  531.         if request.method == 'GET':
  532.             self.logger.log_visitor_login_page(client_ip)
  533.             html = self.read_template('login.html')
  534.             if not html:
  535.                 return web.Response(text='模板缺失', status=500)
  536.             return web.Response(text=html, content_type='text/html')
  537.         data = await request.post()
  538.         username = data.get('username', '').strip()
  539.         password = data.get('password', '').strip()
  540.         if not username or not password:
  541.             self.logger.log_login_failed(client_ip, username, "用户名或密码为空")
  542.             html = self.read_template('login.html', {'error': '用户名或密码不能为空'})
  543.             return web.Response(text=html, content_type='text/html')
  544.         user_id = await self.verify_user(username, password)
  545.         if user_id:
  546.             session_id = await self.create_session(user_id)
  547.             resp = web.HTTPFound('/cloud')
  548.             resp.set_cookie('session_id', session_id, httponly=True, max_age=86400)
  549.             self.logger.log_access(client_ip, username, '/', 200)
  550.             return resp
  551.         else:
  552.             self.logger.log_login_failed(client_ip, username, "密码错误")
  553.             html = self.read_template('login.html', {'error': '用户名或密码错误'})
  554.             return web.Response(text=html, content_type='text/html')
  555.     async def handle_register(self, request):
  556.         client_ip = request.remote
  557.         if request.method == 'GET':
  558.             self.logger.log_visitor_register_page(client_ip)
  559.             if not self.allow_register:
  560.                 html = '''
  561.                 <!DOCTYPE html>
  562.                 <html><head><meta charset="UTF-8"><title>注册关闭</title>
  563.                 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  564.                 <style>.error-container{max-width:500px;margin:100px auto;padding:40px;text-align:center;}
  565.                 .error-icon{font-size:64px;color:#dc3545;margin-bottom:20px;}
  566.                 .error-title{font-size:24px;color:#333;margin-bottom:15px;}
  567.                 .error-message{color:#666;margin-bottom:30px;}
  568.                 .error-back-link{color:#007bff;text-decoration:none;}
  569.                 </style></head><body class="bg-gray-100">
  570.                 <div class="error-container"><div class="error-icon"><i class="fas fa-lock"></i></div>
  571.                 <h1 class="error-title">注册功能已关闭</h1>
  572.                 <p class="error-message">请联系管理员获取权限</p>
  573.                 <a href="/" class="error-back-link"><i class="fas fa-arrow-left"></i>返回登录</a></div>
  574.                 </body></html>'''
  575.                 return web.Response(text=html, content_type='text/html')
  576.             return web.Response(text=self.read_template('register.html'), content_type='text/html')
  577.         data = await request.post()
  578.         username = data.get('username', '').strip()
  579.         password = data.get('password', '').strip()
  580.         if not username or not password:
  581.             return web.Response(text=self.read_template('register.html', {'error': '用户名或密码不能为空'}), content_type='text/html')
  582.         if len(username) < 3 or len(username) > 20:
  583.             return web.Response(text=self.read_template('register.html', {'error': '用户名长度3-20字符'}), content_type='text/html')
  584.         success = await self.create_user(username, password)
  585.         if success:
  586.             user_id = await self.verify_user(username, password)
  587.             session_id = await self.create_session(user_id)
  588.             resp = web.HTTPFound('/cloud')
  589.             resp.set_cookie('session_id', session_id, httponly=True, max_age=86400)
  590.             self.logger.log_register_success(client_ip, username)
  591.             return resp
  592.         else:
  593.             return web.Response(text=self.read_template('register.html', {'error': '用户名已存在'}), content_type='text/html')
  594.     async def handle_logout(self, request):
  595.         session_id = request.cookies.get('session_id')
  596.         if session_id:
  597.             await self.delete_session(session_id)
  598.         resp = web.HTTPFound('/login')
  599.         resp.del_cookie('session_id')
  600.         return resp
  601.     async def handle_cloud(self, request):
  602.         user_id = await self.check_auth(request)
  603.         if not user_id:
  604.             return web.HTTPFound('/login')
  605.         user_info = await self.get_user_info(user_id)
  606.         if not user_info:
  607.             return web.HTTPFound('/logout')
  608.         path_param = request.query.get('path', '')
  609.         search_query = request.query.get('q', '')
  610.         current_path = self.normalize_path(path_param)
  611.         user_dir = self.get_user_dir(user_info['username'])
  612.         if current_path and not self.is_safe_path(user_dir, current_path):
  613.             return web.Response(text='路径不安全', status=403)
  614.         current_dir = user_dir / current_path if current_path else user_dir
  615.         items = []
  616.         if search_query:
  617.             search_query = search_query.lower()
  618.             for root, dirs, files in os.walk(str(user_dir)):
  619.                 rel_root = Path(root).relative_to(user_dir)
  620.                 rel_root_posix = rel_root.as_posix() if str(rel_root) != '.' else ''
  621.                 for d in dirs:
  622.                     if search_query in d.lower():
  623.                         dir_path = rel_root / d
  624.                         try:
  625.                             stat = (user_dir / dir_path).stat()
  626.                             items.append({
  627.                                 'name': d,
  628.                                 'path': dir_path.as_posix(),
  629.                                 'is_dir': True,
  630.                                 'size': '-',
  631.                                 'mtime': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M'),
  632.                                 'search_result': True,
  633.                                 'dir_path': rel_root_posix
  634.                             })
  635.                         except: pass
  636.                 for f in files:
  637.                     if search_query in f.lower():
  638.                         file_path = rel_root / f
  639.                         try:
  640.                             stat = (user_dir / file_path).stat()
  641.                             items.append({
  642.                                 'name': f,
  643.                                 'path': file_path.as_posix(),
  644.                                 'is_dir': False,
  645.                                 'size': self.format_file_size(stat.st_size),
  646.                                 'mtime': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M'),
  647.                                 'search_result': True,
  648.                                 'dir_path': rel_root_posix
  649.                             })
  650.                         except: pass
  651.         else:
  652.             try:
  653.                 if current_dir.exists() and current_dir.is_dir():
  654.                     for item in current_dir.iterdir():
  655.                         try:
  656.                             stat = item.stat()
  657.                             rel = item.relative_to(user_dir)
  658.                             if item.is_dir():
  659.                                 items.append({
  660.                                     'name': item.name,
  661.                                     'path': rel.as_posix(),
  662.                                     'is_dir': True,
  663.                                     'size': '-',
  664.                                     'mtime': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M')
  665.                                 })
  666.                             else:
  667.                                 items.append({
  668.                                     'name': item.name,
  669.                                     'path': rel.as_posix(),
  670.                                     'is_dir': False,
  671.                                     'size': self.format_file_size(stat.st_size),
  672.                                     'mtime': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M')
  673.                                 })
  674.                         except: pass
  675.             except Exception as e:
  676.                 self.logger.log_error(action="读取目录", error=str(e))
  677.         items.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
  678.         breadcrumbs = [('根目录', '')]
  679.         if current_path:
  680.             parts = current_path.split('/')
  681.             current = ''
  682.             for part in parts:
  683.                 if part:
  684.                     current += f'{part}/'
  685.                     breadcrumbs.append((part, current.rstrip('/')))
  686.         def make_breadcrumbs_html(bcs):
  687.             html = []
  688.             for i, (name, path) in enumerate(bcs):
  689.                 if i > 0:
  690.                     html.append('<li class="separator"><i class="fas fa-chevron-right"></i></li>')
  691.                 if i == len(bcs) - 1:
  692.                     html.append(f'<li><span>{name}</span></li>')
  693.                 else:
  694.                     html.append(f'<li><a href="/cloud?path={quote(path)}">{name}</a></li>')
  695.             return ''.join(html)
  696.         def make_file_list_html(items):
  697.             if not items:
  698.                 return '<div class="empty-state"><i class="fas fa-folder-open"></i><h3>文件夹为空</h3></div>'
  699.             html = ['<ul class="file-list">']
  700.             for item in items:
  701.                 url_path = quote(item['path'])
  702.                 raw_path = item['path'].replace("'", "\\'")
  703.                 name_escaped = item['name'].replace("'", "\\'")
  704.                 icon = 'fa-folder' if item['is_dir'] else 'fa-file'
  705.                 link = f'/cloud?path={url_path}' if item['is_dir'] else f'/download/{url_path}'
  706.                 html.append(f'<li class="file-item">')
  707.                 html.append(f'<div class="file-icon"><i class="fas {icon}"></i></div>')
  708.                 html.append(f'<div class="file-info"><div class="file-name"><a href="{link}">{item["name"]}</a></div>')
  709.                 html.append(f'<div class="file-meta"><div class="file-size">{item["size"]}</div><div class="file-time">{item["mtime"]}</div></div></div>')
  710.                 html.append(f'<div class="file-actions">')
  711.                 if not item['is_dir']:
  712.                     html.append(f'<button class="file-action-btn download-btn" onclick="downloadFile(\'{url_path}\')"><i class="fas fa-download"></i></button>')
  713.                 html.append(f'<button class="file-action-btn rename-btn" onclick="showRenameModal(\'{raw_path}\', \'{name_escaped}\')"><i class="fas fa-edit"></i></button>')
  714.                 html.append(f'<button class="file-action-btn delete-btn" onclick="showDeleteConfirm(\'{raw_path}\', \'{name_escaped}\')"><i class="fas fa-trash-alt"></i></button>')
  715.                 html.append('</div></li>')
  716.             html.append('</ul>')
  717.             return ''.join(html)
  718.         context = {
  719.             'username': user_info['username'],
  720.             'current_path': current_path,
  721.             'search_query': search_query,
  722.             'used_formatted': user_info['used_formatted'],
  723.             'percentage': user_info['percentage'],
  724.             'breadcrumbs_html': make_breadcrumbs_html(breadcrumbs),
  725.             'file_list_html': make_file_list_html(items)
  726.         }
  727.         html = self.read_template('cloud.html', context)
  728.         if not html:
  729.             return web.Response(text='模板缺失', status=500)
  730.         self.logger.log_access(request.remote, user_info['username'], f'/{current_path}' if current_path else '/', 200)
  731.         return web.Response(text=html, content_type='text/html')
  732.     # -------------------- 文件操作接口 --------------------
  733.     async def handle_download(self, request):
  734.         user_id = await self.check_auth(request)
  735.         if not user_id:
  736.             return web.Response(text='未授权', status=401)
  737.         user_info = await self.get_user_info(user_id)
  738.         if not user_info:
  739.             return web.Response(text='用户不存在', status=404)
  740.         filepath = unquote(request.match_info['path'])
  741.         user_dir = self.get_user_dir(user_info['username'])
  742.         file_path = user_dir / filepath
  743.         safe, _ = self.is_safe_path(user_dir, filepath)
  744.         if not safe or not file_path.exists() or not file_path.is_file():
  745.             return web.Response(text='文件不存在', status=404)
  746.         force_download = request.query.get('force', '').lower() == 'true'
  747.         file_size = file_path.stat().st_size
  748.         mime_type, _ = mimetypes.guess_type(str(file_path))
  749.         mime_type = mime_type or 'application/octet-stream'
  750.         client_ip = request.remote
  751.         range_header = request.headers.get('Range')
  752.         is_range_request = range_header is not None
  753.         request_id = f"{client_ip}:{filepath}:{int(time.time())}"
  754.         try:
  755.             response = web.StreamResponse()
  756.             if force_download:
  757.                 filename = file_path.name
  758.                 import urllib.parse
  759.                 encoded_filename = urllib.parse.quote(filename, encoding='utf-8')
  760.                 response.headers['Content-Disposition'] = f'attachment; filename="{encoded_filename}"'
  761.                 response.headers['Content-Type'] = 'application/octet-stream'
  762.             else:
  763.                 response.headers['Content-Type'] = mime_type
  764.             response.headers['Accept-Ranges'] = 'bytes'
  765.             if range_header:
  766.                 match = re.match(r'bytes=(\d+)-(\d*)', range_header)
  767.                 if match:
  768.                     start = int(match.group(1))
  769.                     end = int(match.group(2)) if match.group(2) else file_size - 1
  770.                     if start >= file_size or end >= file_size or start > end:
  771.                         return web.Response(status=416)
  772.                     response.set_status(206)
  773.                     response.headers['Content-Range'] = f'bytes {start}-{end}/{file_size}'
  774.                     response.headers['Content-Length'] = str(end - start + 1)
  775.                     await response.prepare(request)
  776.                     chunk_size = 64 * 1024
  777.                     async with aiofiles.open(str(file_path), 'rb') as f:
  778.                         await f.seek(start)
  779.                         remain = end - start + 1
  780.                         while remain > 0:
  781.                             chunk = await f.read(min(chunk_size, remain))
  782.                             if not chunk:
  783.                                 break
  784.                             try:
  785.                                 await response.write(chunk)
  786.                             except (ClientDisconnectedError, ConnectionResetError, asyncio.CancelledError, ConnectionError):
  787.                                 return response
  788.                             remain -= len(chunk)
  789.                     await response.write_eof()
  790.                     return response
  791.             response.headers['Content-Length'] = str(file_size)
  792.             await response.prepare(request)
  793.             self.logger.log_download(client_ip, user_info['username'], filepath, force_download, 200, is_range_request, request_id)
  794.             chunk_size = 64 * 1024
  795.             async with aiofiles.open(str(file_path), 'rb') as f:
  796.                 while True:
  797.                     chunk = await f.read(chunk_size)
  798.                     if not chunk:
  799.                         break
  800.                     try:
  801.                         await response.write(chunk)
  802.                     except (ClientDisconnectedError, ConnectionResetError, asyncio.CancelledError, ConnectionError):
  803.                         return response
  804.             await response.write_eof()
  805.             return response
  806.         except (ClientDisconnectedError, ConnectionResetError, asyncio.CancelledError, ConnectionError):
  807.             return web.Response(status=204)
  808.         except Exception as e:
  809.             self.logger.log_error(client_ip, user_info['username'], "文件下载", filepath, str(e), is_range_request)
  810.             return web.Response(text='下载失败', status=500)
  811.     async def handle_upload(self, request):
  812.         user_id = await self.check_auth(request)
  813.         if not user_id:
  814.             return web.Response(text='未授权', status=401)
  815.         user_info = await self.get_user_info(user_id)
  816.         if not user_info:
  817.             return web.Response(text='用户不存在', status=404)
  818.         client_ip = request.remote
  819.         try:
  820.             reader = await request.multipart()
  821.         except Exception as e:
  822.             self.logger.log_error(client_ip, user_info['username'], "上传", "解析请求失败", str(e))
  823.             return web.Response(text='请求解析失败', status=400)
  824.         user_dir = self.get_user_dir(user_info['username'])
  825.         upload_path = ''
  826.         target_dir = None
  827.         uploaded_files = []
  828.         total_size = 0
  829.         while True:
  830.             part = await reader.next()
  831.             if part is None:
  832.                 break
  833.             if part.name == 'path':
  834.                 text = await part.text()
  835.                 upload_path = self.normalize_path(text.strip())
  836.                 target_dir = user_dir / upload_path if upload_path else user_dir
  837.                 safe, _ = self.is_safe_path(user_dir, upload_path)
  838.                 if not safe:
  839.                     return web.Response(text='路径不安全', status=403)
  840.                 try:
  841.                     target_dir.mkdir(parents=True, exist_ok=True)
  842.                 except Exception as e:
  843.                     self.logger.log_error(client_ip, user_info['username'], "创建上传目录", str(e))
  844.                     return web.Response(text='无法创建目录', status=500)
  845.             elif part.filename:
  846.                 filename = self.sanitize_path(part.filename)
  847.                 if not filename:
  848.                     continue
  849.                 if target_dir is None:
  850.                     target_dir = user_dir
  851.                     target_dir.mkdir(parents=True, exist_ok=True)
  852.                 file_path = target_dir / filename
  853.                 counter = 1
  854.                 name, ext = os.path.splitext(filename)
  855.                 while file_path.exists():
  856.                     filename = f"{name}_{counter}{ext}"
  857.                     file_path = target_dir / filename
  858.                     counter += 1
  859.                 try:
  860.                     async with aiofiles.open(str(file_path), 'wb') as f:
  861.                         while True:
  862.                             chunk = await part.read_chunk(65536)
  863.                             if not chunk:
  864.                                 break
  865.                             await f.write(chunk)
  866.                             total_size += len(chunk)
  867.                     uploaded_files.append(filename)
  868.                 except Exception as e:
  869.                     self.logger.log_error(client_ip, user_info['username'], "保存文件", filename, str(e))
  870.                     continue
  871.         if uploaded_files:
  872.             async with aiosqlite.connect(self.db_path) as db:
  873.                 await db.execute(
  874.                     'UPDATE users SET storage_used = storage_used + ? WHERE id = ?',
  875.                     (total_size, user_id)
  876.                 )
  877.                 await db.commit()
  878.             self.logger.log_upload(client_ip, user_info['username'], len(uploaded_files), upload_path, 200)
  879.         return web.json_response({'success': True, 'uploaded_files': uploaded_files})
  880.     async def handle_create_folder(self, request):
  881.         user_id = await self.check_auth(request)
  882.         if not user_id:
  883.             return web.json_response({'status': 'error', 'message': '未授权'}, status=401)
  884.         try:
  885.             data = await request.json()
  886.         except:
  887.             return web.json_response({'status': 'error', 'message': '请求格式错误'}, status=400)
  888.         folder_name = data.get('name', '').strip()
  889.         raw_path = data.get('path', '').strip()
  890.         if not folder_name:
  891.             return web.json_response({'status': 'error', 'message': '文件夹名称不能为空'})
  892.         folder_name = self.sanitize_path(folder_name)
  893.         if not folder_name:
  894.             return web.json_response({'status': 'error', 'message': '文件夹名称包含非法字符'})
  895.         user_info = await self.get_user_info(user_id)
  896.         if not user_info:
  897.             return web.json_response({'status': 'error', 'message': '用户不存在'}, status=404)
  898.         user_dir = self.get_user_dir(user_info['username'])
  899.         safe, parent_path = self.is_safe_path(user_dir, raw_path)
  900.         if not safe:
  901.             return web.json_response({'status': 'error', 'message': '路径不安全'})
  902.         target_dir = user_dir / parent_path if parent_path else user_dir
  903.         folder_path = target_dir / folder_name
  904.         try:
  905.             folder_path.mkdir(exist_ok=False)
  906.             rel_path = folder_path.relative_to(user_dir).as_posix()
  907.             full_path = f"/{rel_path}" if rel_path else "/"
  908.             self.logger.log_create_folder(request.remote, user_info['username'], full_path, 200)
  909.             return web.json_response({'status': 'success'})
  910.         except FileExistsError:
  911.             return web.json_response({'status': 'error', 'message': '文件夹已存在'})
  912.         except Exception as e:
  913.             self.logger.log_error(request.remote, user_info['username'], "创建文件夹", str(e))
  914.             return web.json_response({'status': 'error', 'message': str(e)})
  915.     async def handle_delete(self, request):
  916.         user_id = await self.check_auth(request)
  917.         if not user_id:
  918.             return web.json_response({'status': 'error', 'message': '未授权'}, status=401)
  919.         try:
  920.             data = await request.json()
  921.         except:
  922.             return web.json_response({'status': 'error', 'message': '请求格式错误'}, status=400)
  923.         raw_path = data.get('path', '').strip()
  924.         user_info = await self.get_user_info(user_id)
  925.         if not user_info:
  926.             return web.json_response({'status': 'error', 'message': '用户不存在'}, status=404)
  927.         user_dir = self.get_user_dir(user_info['username'])
  928.         safe, processed_path = self.is_safe_path(user_dir, raw_path)
  929.         if not safe:
  930.             return web.json_response({'status': 'error', 'message': '路径不安全'})
  931.         full_path = user_dir / processed_path if processed_path else user_dir
  932.         if not full_path.exists():
  933.             return web.json_response({'status': 'error', 'message': '文件/文件夹不存在'})
  934.         try:
  935.             total_size = 0
  936.             if full_path.is_file():
  937.                 total_size = full_path.stat().st_size
  938.                 full_path.unlink()
  939.             else:
  940.                 for file in full_path.rglob('*'):
  941.                     if file.is_file():
  942.                         total_size += file.stat().st_size
  943.                 shutil.rmtree(str(full_path))
  944.             async with aiosqlite.connect(self.db_path) as db:
  945.                 await db.execute(
  946.                     'UPDATE users SET storage_used = MAX(0, storage_used - ?) WHERE id = ?',
  947.                     (total_size, user_id)
  948.                 )
  949.                 await db.commit()
  950.             self.logger.log_delete(request.remote, user_info['username'], f"/{processed_path}" if processed_path else "/", 200)
  951.             return web.json_response({'status': 'success'})
  952.         except Exception as e:
  953.             self.logger.log_error(request.remote, user_info['username'], "删除", str(e))
  954.             return web.json_response({'status': 'error', 'message': str(e)})
  955.     async def handle_rename(self, request):
  956.         user_id = await self.check_auth(request)
  957.         if not user_id:
  958.             return web.json_response({'status': 'error', 'message': '未授权'}, status=401)
  959.         try:
  960.             data = await request.json()
  961.         except:
  962.             return web.json_response({'status': 'error', 'message': '请求格式错误'}, status=400)
  963.         old_path = data.get('old_path', '').strip()
  964.         new_name = data.get('new_name', '').strip()
  965.         if not old_path or not new_name:
  966.             return web.json_response({'status': 'error', 'message': '参数不完整'})
  967.         new_name = self.sanitize_path(new_name)
  968.         if not new_name:
  969.             return web.json_response({'status': 'error', 'message': '新名称包含非法字符'})
  970.         user_info = await self.get_user_info(user_id)
  971.         if not user_info:
  972.             return web.json_response({'status': 'error', 'message': '用户不存在'}, status=404)
  973.         user_dir = self.get_user_dir(user_info['username'])
  974.         safe, processed_old = self.is_safe_path(user_dir, old_path)
  975.         if not safe:
  976.             return web.json_response({'status': 'error', 'message': '路径不安全'})
  977.         old_full = user_dir / processed_old if processed_old else user_dir
  978.         new_full = old_full.parent / new_name
  979.         if not old_full.exists():
  980.             return web.json_response({'status': 'error', 'message': '目标不存在'})
  981.         try:
  982.             old_full.rename(new_full)
  983.             self.logger.log_rename(request.remote, user_info['username'],
  984.                                    f"/{processed_old}" if processed_old else "/", new_name, 200)
  985.             return web.json_response({'status': 'success'})
  986.         except Exception as e:
  987.             self.logger.log_error(request.remote, user_info['username'], "重命名", str(e))
  988.             return web.json_response({'status': 'error', 'message': str(e)})
  989.     async def handle_storage_info(self, request):
  990.         user_id = await self.check_auth(request)
  991.         if not user_id:
  992.             return web.Response(text='未授权', status=401)
  993.         info = await self.get_user_info(user_id)
  994.         if not info:
  995.             return web.Response(text='用户不存在', status=404)
  996.         return web.json_response(info)
  997.     async def handle_delete_account(self, request):
  998.         user_id = await self.check_auth(request)
  999.         if not user_id:
  1000.             return web.Response(text='未授权', status=401)
  1001.         try:
  1002.             data = await request.json()
  1003.         except:
  1004.             return web.json_response({'status': 'error', 'message': '请求格式错误'}, status=400)
  1005.         password = data.get('password', '').strip()
  1006.         if not password:
  1007.             return web.json_response({'status': 'error', 'message': '请输入密码'})
  1008.         async with aiosqlite.connect(self.db_path) as db:
  1009.             cursor = await db.execute('SELECT username, password_hash, salt FROM users WHERE id = ?', (user_id,))
  1010.             user = await cursor.fetchone()
  1011.             if not user:
  1012.                 return web.json_response({'status': 'error', 'message': '用户不存在'})
  1013.             username, stored_hash, salt = user
  1014.             hashed, _ = self.hash_password(password, bytes.fromhex(salt))
  1015.             if hashed != stored_hash:
  1016.                 return web.json_response({'status': 'error', 'message': '密码错误'})
  1017.         await self.delete_user(user_id)
  1018.         resp = web.json_response({'status': 'success'})
  1019.         resp.del_cookie('session_id')
  1020.         return resp
  1021.     @web.middleware
  1022.     async def connection_tracker(self, request, handler):
  1023.         self.active_connections.add(id(request))
  1024.         try:
  1025.             return await handler(request)
  1026.         finally:
  1027.             self.active_connections.discard(id(request))
  1028.     # ---------- 启动/停止 ----------
  1029.     async def start_server(self):
  1030.         await self.init_db()
  1031.         self.app = web.Application(middlewares=[self.connection_tracker])
  1032.         self.app.router.add_get('/', self.handle_index)
  1033.         self.app.router.add_get('/login', self.handle_login)
  1034.         self.app.router.add_post('/login', self.handle_login)
  1035.         self.app.router.add_get('/register', self.handle_register)
  1036.         self.app.router.add_post('/register', self.handle_register)
  1037.         self.app.router.add_get('/logout', self.handle_logout)
  1038.         self.app.router.add_get('/cloud', self.handle_cloud)
  1039.         self.app.router.add_post('/upload', self.handle_upload)
  1040.         self.app.router.add_post('/create_folder', self.handle_create_folder)
  1041.         self.app.router.add_post('/delete', self.handle_delete)
  1042.         self.app.router.add_post('/rename', self.handle_rename)
  1043.         self.app.router.add_get('/storage_info', self.handle_storage_info)
  1044.         self.app.router.add_post('/delete_account', self.handle_delete_account)
  1045.         self.app.router.add_get('/download/{path:.+}', self.handle_download)
  1046.         self.app.router.add_static('/static', str(self.base_dir / 'static'))
  1047.         try:
  1048.             sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  1049.             sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  1050.             sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
  1051.             sock.bind(('::', self.port))
  1052.             sock.listen(100)
  1053.             self.logger.log_server_event("使用IPv6 socket(兼容IPv4)")
  1054.         except Exception:
  1055.             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  1056.             sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  1057.             sock.bind(('0.0.0.0', self.port))
  1058.             sock.listen(100)
  1059.         self.runner = web.AppRunner(self.app)
  1060.         await self.runner.setup()
  1061.         self.site = web.SockSite(self.runner, sock)
  1062.         await self.site.start()
  1063.         ipv4_addresses, ipv6_addresses = self.get_network_addresses()
  1064.         self.logger.log_server_event("=" * 50)
  1065.         self.logger.log_server_event("云盘服务器启动成功!")
  1066.         self.logger.log_server_event(f"端口: {self.port}")
  1067.         self.logger.log_server_event(f"允许注册: {'是' if self.allow_register else '否'}")
  1068.         self.logger.log_server_event("访问地址:")
  1069.         if ipv4_addresses:
  1070.             self.logger.log_server_event("[IPv4]")
  1071.             for ip in ipv4_addresses:
  1072.                 self.logger.log_server_event(f"  http://{ip}:{self.port}/")
  1073.         if ipv6_addresses:
  1074.             self.logger.log_server_event("[IPv6]")
  1075.             for ip in ipv6_addresses:
  1076.                 self.logger.log_server_event(f"  http://[{ip}]:{self.port}/")
  1077.         self.logger.log_server_event("[本地]")
  1078.         self.logger.log_server_event(f"  http://127.0.0.1:{self.port}/")
  1079.         self.logger.log_server_event("=" * 50)
  1080.         self.is_running = True
  1081.         async def periodic_cleanup():
  1082.             while self.is_running:
  1083.                 await asyncio.sleep(300)
  1084.                 self.logger.clear_download_cache()
  1085.         asyncio.ensure_future(periodic_cleanup())
  1086.         while self.is_running:
  1087.             await asyncio.sleep(1)
  1088.     async def stop_server(self):
  1089.         self.is_running = False
  1090.         if self.active_connections:
  1091.             self.logger.log_server_event(f'等待 {len(self.active_connections)} 个连接关闭...')
  1092.             wait = 30
  1093.             for _ in range(wait):
  1094.                 if not self.active_connections:
  1095.                     break
  1096.                 await asyncio.sleep(1)
  1097.         if hasattr(self, 'site') and self.site:
  1098.             await self.site.stop()
  1099.             self.site = None
  1100.         if hasattr(self, 'runner') and self.runner:
  1101.             await self.runner.cleanup()
  1102.             self.runner = None
  1103.         if hasattr(self, 'app') and self.app:
  1104.             await self.app.cleanup()
  1105.             self.app = None
  1106.         self.logger.log_server_event('服务器已停止')
  1107. # ---------- 配置加载 ----------
  1108. def load_config():
  1109.     config_path = Path(__file__).parent / 'config.json'
  1110.     defaults = {
  1111.         'port': 8000,
  1112.         'allow_register': 'yes'
  1113.     }
  1114.     config = {}
  1115.     if config_path.exists():
  1116.         try:
  1117.             with open(config_path, 'r', encoding='utf-8') as f:
  1118.                 config = json.load(f)
  1119.         except Exception as e:
  1120.             print(f"配置文件读取失败,使用默认配置: {e}")
  1121.     else:
  1122.         with open(config_path, 'w', encoding='utf-8') as f:
  1123.             json.dump(defaults, f, indent=2, ensure_ascii=False)
  1124.         print(f"已生成配置文件: {config_path}")
  1125.         return defaults
  1126.     for key, val in defaults.items():
  1127.         if key not in config:
  1128.             config[key] = val
  1129.     allow = config.get('allow_register', 'yes')
  1130.     if isinstance(allow, bool):
  1131.         config['allow_register'] = allow
  1132.     elif isinstance(allow, str):
  1133.         config['allow_register'] = allow.lower() in ('yes', 'true', '1', 'y')
  1134.     else:
  1135.         config['allow_register'] = True
  1136.     return config
  1137. async def main():
  1138.     config = load_config()
  1139.     server = CloudDriveServer(config)
  1140.     try:
  1141.         await server.start_server()
  1142.     except asyncio.CancelledError:
  1143.         pass
  1144.     except KeyboardInterrupt:
  1145.         pass
  1146.     except Exception as e:
  1147.         server.logger.log_error(action="服务器运行", error=str(e))
  1148.     finally:
  1149.         await server.stop_server()
  1150. if __name__ == "__main__":
  1151.     try:
  1152.         asyncio.run(main())
  1153.     except KeyboardInterrupt:
  1154.         print("\n服务器已手动停止")
复制代码

下载地址

https://wwamp.lanzouu.com/izuJN3o4ogre

© 版权声明

相关文章