在部署好的Openwrt系统上实现文件列表展示并下载文件
按照Luci开发的标准模式:MVC方式编写
1. 新增controller,注册页面路由、列表函数、下载函数
lua
-- luci\lua\luci\controller\myapp\custom_file_downloader.lua
module("luci.controller.myapp.custom_file_downloader", package.seeall)
function index()
-- 在LuCI主菜单的"系统"下注册一个入口
entry({"admin", "system", "file_download"}, firstchild(), _("文件下载"), 60).index = true
-- 第一个子节点:显示文件列表页面
entry({"admin", "system", "file_download", "list"}, template("custom_file_downloader/file_list"), _("文件列表"),
1)
-- 第二个子节点:处理文件下载的实际请求(这是一个调用函数,而非页面)
entry({"admin", "system", "file_download", "download"}, call("download_file"))
end
-- 替换旧的 get_file_list 函数
function get_file_list(directory)
local fs = require "nixio.fs"
local files = {}
-- 使用 nixio.fs.dir 迭代目录
for filename in fs.dir(directory) do
if filename ~= "." and filename ~= ".." then
local filepath = directory .. "/" .. filename
local stat = fs.stat(filepath)
if stat then
table.insert(files, {
name = filename,
size = stat.size,
-- nixio.fs.stat 返回的 mtime 已经是时间戳
modtime = os.date("%Y-%m-%d %H:%M:%S", stat.mtime),
path = filepath
})
end
end
end
table.sort(files, function(a, b)
return a.name < b.name
end)
return files
end
-- 处理文件下载的Action函数
function download_file()
local fs = require "nixio.fs"
local http = require "luci.http"
local filepath = luci.http.formvalue("file") -- 从GET参数中获取文件路径
-- **安全检查:确保文件路径在允许的范围内,防止目录遍历攻击**
local allowed_dir = "/tmp" -- 只允许下载/tmp目录下的文件
if not filepath or not filepath:find("^" .. allowed_dir) then
http.status(403, "Forbidden")
http.write("Access denied.")
return
end
-- 检查文件是否存在且是普通文件
if not fs.stat(filepath) or fs.stat(filepath).type ~= "reg" then
http.status(404, "Not Found")
http.write("File not found.")
return
end
-- 获取文件名(去除路径)
local filename = filepath:match("([^/]+)$")
-- 设置HTTP响应头以触发下载
http.header('Content-Disposition', 'attachment; filename="' .. filename .. '"')
http.header('Content-Type', 'application/json') -- 这里设置为application/octet-stream也是可以的
http.header('Content-Length', tostring(fs.stat(filepath).size))
-- 以二进制块模式读取并发送文件内容
local chunk_size = 4096 -- 或调整为 8192, 16384 以获得更好性能
local fd = nixio.open(filepath, "r")
if fd then
local chunk = fd:read(chunk_size)
while chunk do
http.write(chunk)
chunk = fd:read(chunk_size)
if not chunk or #chunk == 0 then
break
end
end
-- 无论成功与否,都确保关闭文件描述符
fd:close()
end
这里的关键步骤是:http.header('Content-Type', 'application/json'),设置为application/octet-stream,也是可以的。
这里的关键步骤还有是:while循环中chunk 判断,bituck 读取文件时,每次读取的字节数可能会小于 chunk_size,这时需要判断 chunk 是否为空或者长度为0,如果为空或者长度为0,则说明文件已读完。
文件读取,除了用二进制块模式读取文件,也可以用其他方式,如 nixio.fs.readfile、readall。
以下是一个示例,使用 readall 读取文件全部内容并发送内容:
lua
-- 简洁可靠的读取方式
local fd = nixio.open(filepath, "r")
if fd then
-- 使用 pcall 捕获读取过程中可能发生的任何异常
local ok, data_or_error = pcall(fd.readall, fd)
nixio.syslog("info", "ok:" ..tostring(ok))
nixio.syslog("info", "data:" ..data_or_error)
-- 读取完成后立即关闭文件,释放资源
fd:close()
-- 根据读取结果处理
if ok and data_or_error then
-- 成功读取到数据
http.write(data_or_error)
nixio.syslog("info", "data:" ..data_or_error)
else
-- 读取失败:ok为false时,data_or_error是错误信息;data_or_error为nil时是EOF。
-- 可以选择记录日志或返回错误,但至少不会"卡住"
-- 例如:http.status(500, "Read Error")
nixio.syslog("info", "data:" .. data_or_error)
http.write("读取文件时发生错误")
end
end
-- 注意:这个函数不返回任何值,因为我们已经直接操作了HTTP响应流
另一个示例是基于nixio.fs.readfile 函数:
lua
local data = nixio.fs.readfile(filepath)
if data then
http.write(data)
nixio.syslog("info", "data:" .. data)
else
nixio.syslog("err", "读取文件失败: " .. filepath)
http.status(500, "Read Error")
http.write("读取文件时发生错误")
end
2. 新增view,实现文件列表页面,调用controller中的函数
lua
--luci\lua\luci\view\custom_file_downloader\file_list.htm
<%+header%>
<h2><a href="<%=url('admin/system/file_download')%>"><%:文件下载%></a> >> <%:文件列表%></h2>
<div class="cbi-map">
<fieldset class="cbi-section">
<table class="cbi-section-table" style="width: 100%">
<tr class="cbi-section-table-titles">
<th class="cbi-section-table-cell">文件名</th>
<th class="cbi-section-table-cell">大小(字节)</th>
<th class="cbi-section-table-cell">修改时间</th>
<th class="cbi-section-table-cell">操作</th>
</tr>
<%
local dir = "/tmp" -- 这里可以修改为你想要展示的目录
require "luci.controller.myapp.custom_file_downloader" -- 这里是关键的地方,否则报错
local files = luci.controller.myapp.custom_file_downloader.get_file_list(dir)
for i, file in ipairs(files) do
%>
<tr class="cbi-section-table-row cbi-rowstyle-<%=i%2+1%>">
<td><%=pcdata(file.name)%></td>
<td><%=file.size%></td>
<td><%=file.modtime%></td>
<td>
<!-- 下载链接:调用download动作,并传递文件路径作为参数 -->
<a href="<%=url('admin/system/file_download/download')%>?file=<%=luci.http.urlencode(file.path)%>">
<button class="cbi-button cbi-button-download">下载</button>
</a>
</td>
</tr>
<% end %>
</table>
</fieldset>
</div>
<%+footer%>
这里关键步骤是:
require "luci.controller.myapp.custom_file_downloader",否则刷新页面会报错:
shell
Sun Dec 28 21:23:57 2025 daemon.err uhttpd[4528]: /usr/lib/lua/luci/template.lua:97: Failed to execute template 'custom_file_downloader/file_list'.
Sun Dec 28 21:23:57 2025 daemon.err uhttpd[4528]: A runtime error occurred: [string "/usr/lib/lua/luci/view/custom_file_download..."]:3: attempt to index field 'controller' (a nil value)
这里引入了自定义的模块,这个模块在
/usr/lib/lua/luci/controller/myapp/custom_file_downloader.lua中定义,这个模块中定义了get_file_list函数,这个函数会返回一个文件列表,这个列表中包含文件名、大小、修改时间等信息。