我的handlers代码里没有filetreehandler方法,我给你提供我的代码,你在我的代码中改动吧:handlers:
import tornado.web
import tornado.websocket
import json
import os
import mimetypes
import urllib.parse
from config import RESOURCE_POOLS, save_config, ADMIN_IP, ADMIN_PASSWORD
from file_utils import get_file_list, scan_pools, get_recursive_file_list
import aiofiles
# 改进管理员检查
def is_admin(handler):
"""检查当前用户是否有管理员权限"""
client_ip = handler.request.remote_ip
# 支持IPv4和IPv6的本地地址
return client_ip in ["127.0.0.1", "::1", "localhost"] or \
client_ip == ADMIN_IP or \
handler.get_cookie("admin_auth") == ADMIN_PASSWORD
# 手动资源扫描接口
class ManualScanHandler(tornado.web.RequestHandler):
async def get(self):
if not is_admin(self):
self.set_status(403)
self.write("权限不足")
return
await tornado.ioloop.IOLoop.current().run_in_executor(None, scan_pools)
WSHandler.broadcast_update()
self.write("资源池已手动扫描完成")
# 主页面处理器
class MainHandler(tornado.web.RequestHandler):
def get(self):
try:
if self.request.path == "/admin":
if not is_admin(self):
self.redirect("/")
return
self.render("admin.html", pools=RESOURCE_POOLS)
else:
if is_admin(self):
self.render("admin.html", pools=RESOURCE_POOLS)
else:
self.render("user.html", pools=RESOURCE_POOLS)
except FileNotFoundError:
self.set_status(404)
self.render("404.html")
except Exception as e:
self.set_status(500)
self.render("500.html", error=str(e))
# WebSocket实时更新处理器
class WSHandler(tornado.websocket.WebSocketHandler):
clients = set()
def open(self):
self.clients.add(self)
self.send_update()
def on_message(self, message):
pass
def on_close(self):
self.clients.remove(self)
def send_update(self):
for client in self.clients:
client.write_message(json.dumps(RESOURCE_POOLS))
@classmethod
def broadcast_update(cls):
for client in cls.clients:
client.write_message(json.dumps(RESOURCE_POOLS))
# 资源池更新处理器(仅管理员)
class UpdatePoolHandler(tornado.web.RequestHandler):
def post(self):
if not is_admin(self):
self.write({"status": "error", "message": "无权限执行此操作"})
return
data = json.loads(self.request.body)
pool_name = data.get("pool")
new_path = data.get("path")
if pool_name and new_path and pool_name in RESOURCE_POOLS:
if not os.path.exists(new_path) or not os.path.isdir(new_path):
self.write({"status": "error", "message": "路径不存在或不是目录"})
return
RESOURCE_POOLS[pool_name]["path"] = new_path
try:
RESOURCE_POOLS[pool_name]["files"] = [
f for f in os.listdir(new_path)
if os.path.isfile(os.path.join(new_path, f))
]
except Exception as e:
RESOURCE_POOLS[pool_name]["files"] = [f"错误: {str(e)}"]
save_config()
WSHandler.broadcast_update()
self.write({"status": "success"})
else:
self.write({"status": "error", "message": "无效资源池或路径"})
# 文件列表处理器
class FileListHandler(tornado.web.RequestHandler):
async def get(self, pool_name):
if pool_name not in RESOURCE_POOLS:
self.set_status(404)
self.render("404.html")
return
current_path = self.get_argument("path", default="")
pool_path = RESOURCE_POOLS[pool_name]["path"]
full_path = os.path.join(pool_path, current_path)
full_path = os.path.normpath(full_path)
if not full_path.startswith(pool_path):
self.set_status(403)
self.write("非法路径访问")
return
if not os.path.exists(full_path) or not os.path.isdir(full_path):
self.set_status(404)
self.write("目录不存在")
return
file_tree = get_recursive_file_list(full_path)
self.render("file_tree.html",
pool_name=pool_name,
current_path=current_path,
file_tree=file_tree,
pool_info=RESOURCE_POOLS[pool_name],
is_admin=is_admin(self))
# 音乐播放处理器
class MusicPlayerHandler(tornado.web.RequestHandler):
def get(self):
pool_name = self.get_argument("pool")
file_name = self.get_argument("file")
if pool_name not in RESOURCE_POOLS:
self.set_status(404)
self.write("资源池不存在")
return
pool_path = RESOURCE_POOLS[pool_name]["path"]
file_path = os.path.join(pool_path, file_name)
real_path = os.path.realpath(file_path)
if not real_path.startswith(os.path.realpath(pool_path)):
self.set_status(403)
self.write("禁止访问此文件路径")
return
if not os.path.exists(file_path):
self.set_status(404)
self.write("文件不存在")
return
self.render("music_player.html", file_name=file_name, pool_name=pool_name)
# 文件下载处理器
class FileDownloadHandler(tornado.web.RequestHandler):
async def get(self):
pool_name = self.get_argument("pool")
file_name = self.get_argument("file")
if pool_name not in RESOURCE_POOLS:
self.set_status(404)
self.render("404.html")
return
pool_path = RESOURCE_POOLS[pool_name]["path"]
file_path = os.path.join(pool_path, file_name)
real_path = os.path.realpath(file_path)
if not real_path.startswith(os.path.realpath(pool_path)):
self.set_status(403)
self.write("禁止访问此文件路径")
return
if not os.path.exists(file_path):
self.set_status(404)
self.render("404.html")
return
file_size = os.path.getsize(file_path)
range_header = self.request.headers.get('Range')
start = 0
end = file_size - 1
if range_header:
self.set_status(206)
start_bytes, end_bytes = range_header.replace('bytes=', '').split('-')
start = int(start_bytes) if start_bytes else 0
end = int(end_bytes) if end_bytes else file_size - 1
self.set_header('Content-Range', f'bytes {start}-{end}/{file_size}')
self.set_header("Content-Length", str(end - start + 1))
encoded_filename = urllib.parse.quote(file_name.encode('utf-8'))
self.set_header("Content-Type", "application/octet-stream")
self.set_header("Content-Disposition", f'attachment; filename*=UTF-8\'\'{encoded_filename}')
self.set_header("Accept-Ranges", "bytes")
chunk_size = 1024 * 1024 * 4
try:
async with aiofiles.open(file_path, 'rb') as f:
await f.seek(start)
remaining = end - start + 1
while remaining > 0:
read_size = min(chunk_size, remaining)
chunk = await f.read(read_size)
if not chunk:
break
self.write(chunk)
await self.flush()
remaining -= len(chunk)
self.finish()
except tornado.iostream.StreamClosedError:
return
except Exception as e:
self.set_status(500)
self.write(f"Internal Server Error: {str(e)}")
return
# 文件预览处理器
class FilePreviewHandler(tornado.web.RequestHandler):
async def get(self):
pool_name = self.get_argument("pool")
file_name = self.get_argument("file")
if pool_name not in RESOURCE_POOLS:
self.set_status(404)
self.render("404.html")
return
pool_path = RESOURCE_POOLS[pool_name]["path"]
file_path = os.path.join(pool_path, file_name)
real_path = os.path.realpath(file_path)
if not real_path.startswith(os.path.realpath(pool_path)):
self.set_status(403)
self.write("禁止访问此文件路径")
return
if not os.path.exists(file_path):
self.set_status(404)
self.render("404.html")
return
ext = os.path.splitext(file_name)[1].lower()
# 判断是否为音乐文件,跳转到播放器
if ext in ['.mp3', '.wav', '.flac', '.ogg', '.aac']:
self.redirect(f"/music_player?pool={pool_name}&file={file_name}")
return
mime_type = {
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.md': 'text/markdown',
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.ogg': 'video/ogg',
}.get(ext, mimetypes.guess_type(file_name)[0] or 'application/octet-stream')
encoded_filename = urllib.parse.quote(file_name.encode('utf-8'))
self.set_header("Content-Disposition", f'inline; filename*=UTF-8\'\'{encoded_filename}')
file_size = os.path.getsize(file_path)
range_header = self.request.headers.get('Range')
is_download = not range_header and ext in ['.mp4', '.webm', '.ogg', '.pdf']
if is_download:
self.set_header('Content-Type', mime_type)
self.set_header('Content-Length', str(file_size))
self.set_status(200)
if os.name == 'posix':
fd = os.open(file_path, os.O_RDONLY)
try:
await tornado.ioloop.IOLoop.current().run_in_executor(
None,
os.sendfile,
self.request.connection.fileno(),
fd,
None,
file_size
)
finally:
os.close(fd)
else:
with open(file_path, 'rb') as f:
while True:
chunk = f.read(1024 * 1024 * 4)
if not chunk:
break
self.write(chunk)
await self.flush()
self.finish()
elif ext in ['.mp4', '.webm', '.ogg', '.pdf']:
self.set_header('Accept-Ranges', 'bytes')
start = 0
end = file_size - 1
if range_header:
self.set_status(206)
start_bytes, end_bytes = range_header.replace('bytes=', '').split('-')
start = int(start_bytes) if start_bytes else 0
end = int(end_bytes) if end_bytes else file_size - 1
self.set_header('Content-Range', f'bytes {start}-{end}/{file_size}')
self.set_header("Content-Length", str(end - start + 1))
self.set_header('Content-Type', mime_type)
chunk_size = 4096 * 16
async with aiofiles.open(file_path, 'rb') as f:
await f.seek(start)
remaining = end - start + 1
while remaining > 0:
chunk = await f.read(min(chunk_size, remaining))
if not chunk:
break
self.write(chunk)
await self.flush()
remaining -= len(chunk)
self.finish()
elif ext == '.txt':
self.set_header('Content-Type', 'text/plain; charset=UTF-8')
async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
content = await f.read()
self.write(content)
self.finish()
elif ext in ['.png', '.jpg', '.jpeg', '.gif', '.md', '.pdf', '.docx', '.xlsx', '.pptx']:
self.set_header('Content-Type', mime_type)
async with aiofiles.open(file_path, 'rb') as f:
data = await f.read()
self.write(data)
self.finish()
else:
self.set_status(400)
self.write("不支持预览此文件类型")
server:
import tornado.ioloop
import tornado.web
import threading
import time
import os
import logging
import asyncio
from concurrent.futures import ThreadPoolExecutor
from handlers import MainHandler, WSHandler, UpdatePoolHandler, FileListHandler, FileDownloadHandler, FilePreviewHandler, ManualScanHandler, MusicPlayerHandler
from file_utils import scan_pools
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("ResourceManager")
# 自定义线程池(用于异步执行资源池扫描等阻塞操作)
executor = ThreadPoolExecutor(4) # 根据 CPU 核心数调整线程池大小
# 设置 asyncio 默认事件循环的执行器为线程池
asyncio.get_event_loop().set_default_executor(executor)
# 修复模板路径和路由配置
def make_app():
base_dir = os.path.dirname(os.path.abspath(__file__))
template_path = os.path.join(base_dir, "templates")
static_path = os.path.join(base_dir, "static")
return tornado.web.Application(
[
(r"/", MainHandler),
(r"/ws", WSHandler),
(r"/update_pool", UpdatePoolHandler),
(r"/files/(.+)", FileListHandler),
(r"/download", FileDownloadHandler),
(r"/music_player", MusicPlayerHandler),
(r"/preview", FilePreviewHandler),
(r"/scan", ManualScanHandler),
(r"/static/(.*)", tornado.web.StaticFileHandler, {"path": static_path}),
(r"/admin", MainHandler),
],
template_path=template_path,
cookie_secret="YOUR_SECRET_KEY",
login_url="/login",
debug=False, # 生产环境建议关闭 debug
max_buffer_size=1024 * 1024 * 1024, # 最大缓冲区大小(1GB)
chunked_write_timeout=60000, # 分块写入超时时间(毫秒)
idle_connection_timeout=60000, # 空闲连接超时时间
executor=executor # 使用自定义线程池
)
if __name__ == "__main__":
try:
# 初始化资源池扫描
scan_pools()
logger.info("资源池扫描初始化完成")
except Exception as e:
logger.error(f"资源池扫描初始化失败: {str(e)}")
# 创建并启动应用
app = make_app()
app.listen(8888)
logger.info("服务器运行中: http://localhost:8888")
try:
# 启动事件循环
tornado.ioloop.IOLoop.current().start()
except KeyboardInterrupt:
logger.info("\n服务器正在关闭...")
except Exception as e:
logger.error(f"服务器异常停止: {str(e)}")
finally:
# 清理资源(可选)
executor.shutdown(wait=True)
#预览视频下载和下载按钮下载速度大幅度提升,txt文本实现在线预览,word等文档预览会下载而不是显示不支持预览,本地速度快,跨设备比较慢
file_utils:
import os
import time
import mimetypes
import logging
# 配置日志
logger = logging.getLogger("FileUtils")
logging.basicConfig(level=logging.INFO)
# 从 config 导入 RESOURCE_POOLS
from config import RESOURCE_POOLS
def get_file_list(pool_name):
if pool_name not in RESOURCE_POOLS:
return []
pool = RESOURCE_POOLS[pool_name]
files = []
path = pool["path"]
if not os.path.exists(path):
logger.error(f"路径不存在: {path}")
return [{"name": "错误: 路径不存在", "formatted_size": "0 B"}]
if not os.path.isdir(path):
logger.error(f"路径不是目录: {path}")
return [{"name": "错误: 路径不是目录", "formatted_size": "0 B"}]
for file_name in pool.get("files", []):
file_path = os.path.join(path, file_name)
if not os.path.exists(file_path):
logger.warning(f"文件不存在: {file_path}")
continue
try:
stat = os.stat(file_path)
file_type = get_file_type(file_path)
files.append({
"name": file_name,
"size": stat.st_size,
"formatted_size": _format_size(stat.st_size),
"modified": time.strftime("%Y-%m-%d %H:%M", time.localtime(stat.st_mtime)),
"path": file_path,
"type": file_type
})
except Exception as e:
logger.error(f"访问文件错误 {file_path}: {e}")
files.append({
"name": f"错误: {file_name}",
"formatted_size": "0 B",
"type": "error"
})
return files
def _format_size(size):
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
def get_file_type(file_path):
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type:
return mime_type.split('/')[0]
return 'other'
def scan_pools():
for pool_name, pool in RESOURCE_POOLS.items():
path = pool["path"]
if not os.path.exists(path) or not os.path.isdir(path):
pool["files"] = [f"错误: 路径无效 {path}"]
continue
try:
pool["files"] = [
f for f in os.listdir(path)
if os.path.isfile(os.path.join(path, f))
]
logger.info(f"扫描完成: {pool_name} ({len(pool['files'])} 文件)")
except Exception as e:
pool["files"] = [f"错误: {str(e)}"]
logger.error(f"扫描资源池错误 {pool_name}: {e}")
def get_recursive_file_list(path, base_path=None):
if base_path is None:
base_path = path
files = []
try:
for entry in os.scandir(path):
relative_path = os.path.relpath(entry.path, base_path)
if entry.is_dir():
files.append({
"name": entry.name,
"type": "dir",
"children": get_recursive_file_list(entry.path, base_path)
})
elif entry.is_file():
files.append({
"name": entry.name,
"type": "file",
"size": os.path.getsize(entry.path),
"modified": time.strftime("%Y-%m-%d %H:%M", time.localtime(os.path.getmtime(entry.path)))
})
except PermissionError:
logger.warning(f"无权访问目录: {path}")
return [{"name": "无权访问", "type": "error"}]
except Exception as e:
logger.error(f"扫描目录失败 {path}: {e}")
return [{"name": f"错误: {str(e)}", "type": "error"}]
return files
最新发布