<think>我们遇到了两个主要问题:
1. 在FileListHandler中,处理目录项时使用了未导入的`time`模块,导致`NameError: name 'time' is not defined`
2. 在渲染模板file_list.html时,模板中使用了变量`files`,但我们在修改后的FileListHandler中传递的是`file_tree`,导致模板中找不到`files`变量
解决方案:
问题1:在handlers.py中导入time模块
问题2:修改模板file_list.html,将原来使用`files`的地方改为使用`file_tree`,并调整模板结构以支持目录和文件的混合显示。
具体步骤:
1. 在handlers.py中导入time模块:
```python
import time
```
2. 修改FileListHandler中的代码,确保在获取文件修改时间时使用`time`模块。
3. 修改file_list.html模板:
- 将原来使用`files`循环的部分改为使用`file_tree`
- 根据每个项的类型(目录或文件)显示不同的图标和操作按钮
我们已经在前面的回答中修改了file_list.html,但错误日志显示模板中仍然引用了`files`变量。因此,我们需要确保模板中不再使用`files`,而是使用`file_tree`。
下面我们重新检查并修复模板:
在file_list.html中,原来显示文件列表的部分是:
```html
{% if len(files) == 0 %}
...显示空状态...
{% else %}
<div class="file-grid">
{% for file in files %}
...显示文件卡片...
{% end %}
</div>
{% end %}
```
修改后应该使用`file_tree`,并且空状态判断也要改为`len(file_tree)==0`。
同时,我们还需要在模板中添加返回上一级目录的按钮(当当前路径非空时)。
以下是修复后的file_list.html相关部分:
```html
<!-- 在文件列表标题下面添加路径导航 -->
<div class="path-navigation mb-4">
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="javascript:void(0)" onclick="openDir('')">根目录</a></li>
{% if current_path %}
{% set parts = current_path.split('/') %}
{% set current = '' %}
{% for part in parts %}
{% set current = current + '/' + part if current else part %}
<li class="breadcrumb-item"><a href="javascript:void(0)" onclick="openDir('{{ current }}')">{{ part }}</a></li>
{% end %}
{% end %}
</ol>
</nav>
</div>
{% if len(file_tree) == 0 %}
<div class="empty-state">
<div class="empty-icon">
<i class="far fa-folder-open"></i>
</div>
<h4>此目录为空</h4>
<p class="text-muted">没有找到任何文件或子目录</p>
</div>
{% else %}
<div class="file-grid">
{% for item in file_tree %}
<!-- 根据item的类型显示目录或文件 -->
{% if item['type'] == "dir" %}
<div class="file-card">
<div class="d-flex align-items-center p-3">
<div class="file-icon">
<i class="fas fa-folder"></i>
</div>
<div class="file-info">
<div class="d-flex align-items-center">
<div class="file-name">{{ item['name'] }}</div>
<span class="file-type-badge">
目录
</span>
</div>
<div class="file-meta">
<span title="修改时间">
<i class="far fa-clock"></i> {{ item['modified'] }}
</span>
</div>
</div>
</div>
<div class="file-actions">
<button class="btn btn-sm btn-primary btn-action"
onclick="openDir('{{ item['name'] }}')">
<i class="fas fa-folder-open me-1"></i> 打开
</button>
</div>
</div>
{% else %}
<div class="file-card">
<div class="d-flex align-items-center p-3">
<!-- 文件图标根据类型显示 -->
{% if item['type'] == 'audio' or item['name'].lower().endswith(('.mp3','.wav','.flac','.ogg','.aac')) %}
<div class="file-icon">
<i class="fas fa-music"></i>
</div>
{% elif item['type'] == 'image' or item['name'].lower().endswith(('.jpg','.jpeg','.png','.gif','.bmp')) %}
<div class="file-icon">
<i class="fas fa-image"></i>
</div>
{% elif item['type'] == 'video' or item['name'].lower().endswith(('.mp4','.mov','.avi','.mkv','.webm')) %}
<div class="file-icon">
<i class="fas fa-video"></i>
</div>
{% elif item['type'] == 'text' or item['name'].lower().endswith(('.txt','.md')) %}
<div class="file-icon">
<i class="fas fa-file-alt"></i>
</div>
{% elif item['name'].lower().endswith('.pdf') %}
<div class="file-icon">
<i class="fas fa-file-pdf"></i>
</div>
{% else %}
<div class="file-icon">
<i class="fas fa-file"></i>
</div>
{% end %}
<div class="file-info">
<div class="d-flex align-items-center">
<div class="file-name">{{ item['name'] }}</div>
<span class="file-type-badge">
{% if item['type'] == 'audio' or item['name'].lower().endswith(('.mp3','.wav','.flac','.ogg','.aac')) %}
音乐
{% else %}
{{ item['type'] if item['type'] != "application" else "文档" }}
{% end %}
</span>
</div>
<div class="file-meta">
<span title="文件大小">
<i class="fas fa-weight-hanging"></i> {{ item['formatted_size'] }}
</span>
<span class="ms-3" title="修改时间">
<i class="far fa-clock"></i> {{ item['modified'] }}
</span>
</div>
</div>
</div>
<div class="file-actions">
{% if item['type'] == 'audio' or item['name'].lower().endswith(('.mp3','.wav','.flac','.ogg','.aac')) %}
<button class="btn btn-sm btn-preview btn-action"
onclick="playMusic('{{ item['name'] }}')">
<i class="fas fa-play me-1"></i> 播放
</button>
{% elif item['type'] in ['image','video','text','application'] or item['name'].lower().endswith(('.txt','.pdf','.jpg','.png','.mp4')) %}
<button class="btn btn-sm btn-preview btn-action"
onclick="previewFile('{{ item['name'] }}')">
<i class="fas fa-eye me-1"></i> 预览
</button>
{% end %}
<button class="btn btn-sm btn-download btn-action"
onclick="downloadFile('{{ item['name'] }}')">
<i class="fas fa-download me-1"></i> 下载
</button>
</div>
</div>
{% end %}
{% end %}
</div>
{% end %}
```
4. 修复FileListHandler中的时间格式化函数:
在FileListHandler中,我们使用了`time.strftime`,但需要确保导入了`time`模块。在handlers.py顶部添加:
```python
import time
```
5. 确保在FileListHandler中传递了正确的变量给模板:
```python
self.render("file_list.html",
pool_name=pool_name,
current_path=current_path,
file_tree=file_tree, # 注意这里传递的是file_tree
...其他参数...
)
```
6. 在模板中,我们使用了`current_path`来构建面包屑导航,因此需要确保在FileListHandler中获取了`current_path`并传递给模板。
7. 在模板的JavaScript部分,我们添加了`openDir`函数,用于打开子目录,以及`playMusic`函数用于播放音乐。
8. 修复路径导航中的链接:由于我们使用了`current_path`,在打开子目录时需要将当前路径与子目录名组合。
9. 注意:在打开目录时,我们需要将当前路径传递给服务器,服务器会扫描该路径下的内容。
经过以上修复,应该可以解决时间模块未定义和模板变量错误的问题。
另外,我们还需要在handlers.py中确保FileListHandler的顶部导入了time模块。
如果问题仍然存在,请检查日志中是否有其他错误信息。
由于我们修改了模板,请确保重新启动服务器以应用更改。
如果遇到其他错误,请根据错误日志进一步调整。
注意:在FileListHandler中,我们使用了`get_file_type`函数,这个函数应该在file_utils.py中定义。如果还没有,请确保定义:
```python
# file_utils.py
import mimetypes
def get_file_type(file_path):
"""根据文件路径获取文件类型(如:image, video, audio, text)"""
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type:
return mime_type.split('/')[0]
return 'other'
```
并确保在handlers.py中导入:
```python
from file_utils import get_file_type, scan_pools # 或其他需要的函数
```
最后,确保在FileListHandler中处理文件时,对于目录项,我们并没有调用`get_file_type`,因为目录项的类型是固定的"dir";对于文件项,我们调用`get_file_type`来获取类型。
如果一切都正确,那么文件列表应该能够正常显示,并且可以递归进入目录,同时音乐文件可以跳转到播放器。
如果还有问题,请提供新的错误日志。</think>根据错误日志,我发现了几个关键问题需要修复:
### 问题分析
1. **`time` 模块未导入**:在 `FileListHandler` 中使用了 `time` 模块但没有导入
2. **模板变量名不一致**:模板中使用了 `files` 变量,但处理程序中传递的是 `file_tree`
3. **目录处理逻辑错误**:尝试处理目录时出现了错误
4. **文件类型识别问题**:`get_file_type` 函数可能没有正确处理所有文件类型
### 完整修复方案
#### 1. 修复 handlers.py
```python
# handlers.py 完整修复
import logging
import time # 添加 time 模块导入
import os
import urllib.parse
from file_utils import get_file_type # 确保导入 get_file_type
# ... 其他代码保持不变 ...
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)
abs_pool_path = os.path.abspath(pool_path)
if not os.path.abspath(full_path).startswith(abs_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 = []
for entry in os.scandir(full_path):
try:
if entry.is_dir():
file_tree.append({
"name": entry.name,
"type": "dir",
"modified": time.strftime("%Y-%m-%d %H:%M", time.localtime(entry.stat().st_mtime))
})
elif entry.is_file():
stat = entry.stat()
file_tree.append({
"name": entry.name,
"type": get_file_type(entry.path),
"size": stat.st_size,
"formatted_size": self.format_size(stat.st_size),
"modified": time.strftime("%Y-%m-%d %H:%M", time.localtime(stat.st_mtime))
})
except Exception as e:
logging.error(f"Error processing {entry.path}: {str(e)}")
# 排序:目录在前,文件在后
file_tree.sort(key=lambda x: (x['type'] != 'dir', x['name'].lower()))
# 渲染模板 - 使用 file_tree 变量
self.render("file_list.html",
pool_name=pool_name,
current_path=current_path,
file_tree=file_tree, # 使用 file_tree 而不是 files
pool_info=RESOURCE_POOLS[pool_name],
is_admin=is_admin(self),
urllib=urllib.parse,
os=os)
def format_size(self, 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"
```
#### 2. 修复 file_utils.py
```python
# file_utils.py 完整修复
import os
import mimetypes
import time
def get_file_type(file_path):
"""获取文件类型"""
# 先根据扩展名判断
ext = os.path.splitext(file_path)[1].lower()
# 音频文件
if ext in ['.mp3', '.wav', '.flac', '.ogg', '.aac']:
return 'audio'
# 视频文件
elif ext in ['.mp4', '.mov', '.avi', '.mkv', '.webm']:
return 'video'
# 图片文件
elif ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']:
return 'image'
# 文档文件
elif ext in ['.txt', '.md']:
return 'text'
elif ext in ['.pdf']:
return 'application'
# 其他情况使用 MIME 类型
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type:
return mime_type.split('/')[0]
return 'other'
# 其他函数保持不变...
```
#### 3. 修复 file_list.html
```html
<!-- file_list.html 完整修复 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<!-- head 部分保持不变 -->
</head>
<body>
<!-- header 部分保持不变 -->
<div class="container">
<!-- 资源池信息部分保持不变 -->
<!-- 添加路径导航 -->
<div class="path-navigation mb-4">
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="javascript:void(0)" onclick="openDir('')">根目录</a></li>
{% if current_path %}
{% set parts = current_path.split('/') %}
{% set current = '' %}
{% for part in parts %}
{% set current = current + '/' + part if current else part %}
<li class="breadcrumb-item"><a href="javascript:void(0)" onclick="openDir('{{ current }}')">{{ part }}</a></li>
{% end %}
{% end %}
</ol>
</nav>
</div>
<h3 class="mb-4"><i class="fas fa-file me-3"></i>文件列表</h3>
<!-- 使用 file_tree 变量 -->
{% if len(file_tree) == 0 %}
<div class="empty-state">
<div class="empty-icon">
<i class="far fa-folder-open"></i>
</div>
<h4>此目录为空</h4>
<p class="text-muted">没有找到任何文件或子目录</p>
</div>
{% else %}
<div class="file-grid">
{% for item in file_tree %}
<div class="file-card">
<div class="d-flex align-items-center p-3">
{% if item['type'] == "dir" %}
<div class="file-icon">
<i class="fas fa-folder"></i>
</div>
{% elif item['type'] == "audio" or item['name'].lower().endswith(('.mp3', '.wav', '.flac', '.ogg', '.aac')) %}
<div class="file-icon">
<i class="fas fa-music"></i>
</div>
{% elif item['type'] == "image" or item['name'].lower().endswith(('.jpg', '.jpeg', '.png', '.gif')) %}
<div class="file-icon">
<i class="fas fa-image"></i>
</div>
{% elif item['type'] == "video" or item['name'].lower().endswith(('.mp4', '.mov', '.avi', '.mkv')) %}
<div class="file-icon">
<i class="fas fa-video"></i>
</div>
{% elif item['type'] == "text" or item['name'].lower().endswith(('.txt', '.md')) %}
<div class="file-icon">
<i class="fas fa-file-alt"></i>
</div>
{% elif item['name'].lower().endswith(('.pdf')) %}
<div class="file-icon">
<i class="fas fa-file-pdf"></i>
</div>
{% else %}
<div class="file-icon">
<i class="fas fa-file"></i>
</div>
{% end %}
<div class="file-info">
<div class="d-flex align-items-center">
<div class="file-name">{{ item['name'] }}</div>
<span class="file-type-badge">
{% if item['type'] == "dir" %}
目录
{% elif item['type'] == "audio" or item['name'].lower().endswith(('.mp3', '.wav', '.flac', '.ogg', '.aac')) %}
音乐
{% else %}
{{ item['type'] if item['type'] != "application" else "文档" }}
{% end %}
</span>
</div>
<div class="file-meta">
{% if item['type'] != "dir" %}
<span title="文件大小">
<i class="fas fa-weight-hanging"></i> {{ item['formatted_size'] }}
</span>
<span class="ms-3" title="修改时间">
<i class="far fa-clock"></i> {{ item['modified'] }}
</span>
{% else %}
<span title="修改时间">
<i class="far fa-clock"></i> {{ item['modified'] }}
</span>
{% end %}
</div>
</div>
</div>
<div class="file-actions">
{% if item['type'] == "dir" %}
<button class="btn btn-sm btn-primary btn-action"
onclick="openDir('{{ item['name'] }}')">
<i class="fas fa-folder-open me-1"></i> 打开
</button>
{% elif item['type'] == "audio" or item['name'].lower().endswith(('.mp3', '.wav', '.flac', '.ogg', '.aac')) %}
<button class="btn btn-sm btn-preview btn-action"
onclick="playMusic('{{ item['name'] }}')">
<i class="fas fa-play me-1"></i> 播放
</button>
{% elif item['type'] in ['image', 'video', 'text', 'application'] %}
<button class="btn btn-sm btn-preview btn-action"
onclick="previewFile('{{ item['name'] }}')">
<i class="fas fa-eye me-1"></i> 预览
</button>
{% end %}
<button class="btn btn-sm btn-download btn-action"
onclick="downloadFile('{{ item['name'] }}')">
<i class="fas fa-download me-1"></i> 下载
</button>
</div>
</div>
{% end %}
</div>
{% end %}
<!-- 返回按钮部分保持不变 -->
</div>
<script>
// 打开目录
function openDir(dirName) {
const currentPath = "{{ current_path }}";
const newPath = currentPath ? `${currentPath}/${dirName}` : dirName;
window.location.href = `/files/{{ pool_name }}?path=${encodeURIComponent(newPath)}`;
}
// 播放音乐
function playMusic(fileName) {
const currentPath = "{{ current_path }}";
const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName;
const url = `/music_player?pool={{ pool_name }}&file=${encodeURIComponent(fullPath)}`;
window.location.href = url;
}
// 预览文件
function previewFile(fileName) {
const currentPath = "{{ current_path }}";
const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName;
const url = `/preview?pool={{ pool_name }}&file=${encodeURIComponent(fullPath)}`;
// 根据文件类型决定打开方式
if (fileName.toLowerCase().match(/\.(mp4|mov|avi|mkv|webm)$/)) {
window.open(url, '_blank', 'width=1000,height=600');
} else if (fileName.toLowerCase().match(/\.(jpg|jpeg|png|gif|bmp)$/)) {
window.open(url, '_blank', 'width=800,height=600');
} else if (fileName.toLowerCase().match(/\.(pdf)$/)) {
window.open(url, '_blank', 'width=1000,height=700');
} else {
window.open(url, '_blank');
}
}
// 下载文件
function downloadFile(fileName) {
const currentPath = "{{ current_path }}";
const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName;
const url = `/download?pool={{ pool_name }}&file=${encodeURIComponent(fullPath)}`;
// 创建隐藏的iframe触发下载
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
// 10秒后移除iframe
setTimeout(() => {
document.body.removeChild(iframe);
}, 10000);
}
// 其他脚本保持不变...
</script>
</body>
</html>
```
#### 4. 修复 FilePreviewHandler
```python
# handlers.py 修复 FilePreviewHandler
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={urllib.parse.quote(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; charset=UTF-8',
'.md': 'text/markdown; charset=UTF-8',
'.html': 'text/html; charset=UTF-8',
'.css': 'text/css; charset=UTF-8',
'.js': 'application/javascript; charset=UTF-8',
'.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')
# 设置内容类型
self.set_header('Content-Type', mime_type)
# 对于文本文件,设置字符集
if ext in ['.txt', '.md', '.html', '.css', '.js']:
self.set_header('Content-Type', f'{mime_type}; charset=UTF-8')
# 读取文件内容
async with aiofiles.open(file_path, 'rb') as f:
content = await f.read()
self.write(content)
self.finish()
```
### 关键修复点
1. **导入缺失模块**:
- 添加 `import time` 到 handlers.py
- 添加 `import mimetypes` 到 file_utils.py
2. **变量名一致性**:
- 在模板中使用 `file_tree` 替代 `files`
- 在 FileListHandler 中传递 `file_tree` 变量
3. **文件类型识别**:
- 增强 `get_file_type` 函数,优先根据扩展名判断文件类型
- 为各种文件类型添加明确的处理逻辑
4. **路径处理**:
- 在 JavaScript 函数中正确处理当前路径
- 在打开目录和播放音乐时拼接完整路径
5. **错误处理**:
- 添加更详细的错误日志
- 确保所有路径操作都进行安全检查
### 验证步骤
1. 确保所有文件都已保存
2. 重启服务器
3. 访问文件列表页面,验证:
- 目录可以正常打开
- 文件可以预览
- 音乐可以播放
- 没有出现 500 错误
###