1604 lines
61 KiB
Lua
1604 lines
61 KiB
Lua
|
|
--[[
|
|||
|
|
@module exremotefile
|
|||
|
|
@summary exremotefile 远程文件管理系统扩展库,提供AP热点创建、SD卡挂载、SERVER文件管理服务器等功能,支持文件浏览、上传、下载和删除操作。
|
|||
|
|
@version 1.0
|
|||
|
|
@date 2025.09.10
|
|||
|
|
@author 拓毅恒
|
|||
|
|
@usage
|
|||
|
|
注:在使用exremotefile 扩展库时,需要将同一目录下的explorer.html文件烧录进模组中,否则无法启动server服务器来创建文件管理系统!!!
|
|||
|
|
|
|||
|
|
注:如果使用Air8000开发板测试,必须自定义配置is_8000_development_board = true
|
|||
|
|
因为Air8000开发板上TF和以太网是同一个SPI,使用开发板时必须要将以太网拉高
|
|||
|
|
如果使用其他硬件,需要根据硬件原理图来决定是否需要此操作
|
|||
|
|
|
|||
|
|
本文件的对外接口有2个:
|
|||
|
|
1、exremotefile.open(ap_opts, sdcard_opts, server_opts):启动远程文件管理系统,可配置AP参数、SD卡参数和服务器参数
|
|||
|
|
-- 启动后连接AP热点,直接使用luatools日志中默认的地址"http://192.168.4.1:80/explorer.html"来访问文件管理服务器。
|
|||
|
|
-- 如果使用自定义配置,则需要根据配置中的server_addr和server_port参数来访问文件管理服务器。
|
|||
|
|
|
|||
|
|
2、exremotefile.close():关闭远程文件管理系统,停止AP热点、卸载SD卡和关闭HTTP服务器
|
|||
|
|
]]
|
|||
|
|
|
|||
|
|
-- 导入必要的模块
|
|||
|
|
dnsproxy = require("dnsproxy")
|
|||
|
|
dhcpsrv = require("dhcpsrv")
|
|||
|
|
|
|||
|
|
local exremotefile = {}
|
|||
|
|
local is_initialized = false
|
|||
|
|
local user_server_opts = {}
|
|||
|
|
local user_sdcard_opts = {}
|
|||
|
|
local ETH3V3_EN = 140 -- Air8000开发板以太网供电
|
|||
|
|
local SPI_ETH_CS = 12 -- Air8000开发板以太网片选
|
|||
|
|
|
|||
|
|
-- AP默认配置
|
|||
|
|
local default_ap_opts = {
|
|||
|
|
ap_ssid = "LuatOS_FileHub",
|
|||
|
|
ap_pwd = "12345678"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
-- SPI默认配置
|
|||
|
|
local default_sdcard_opts = {
|
|||
|
|
spi_id = 1,
|
|||
|
|
spi_cs = 20,
|
|||
|
|
is_8000_development_board = false,
|
|||
|
|
is_sdio = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
-- server默认配置
|
|||
|
|
local default_server_opts = {
|
|||
|
|
user_name = "admin",
|
|||
|
|
user_pwd = "123456",
|
|||
|
|
server_addr = "192.168.4.1",
|
|||
|
|
server_port = 80
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
-- 保存模块引用,用于后续关闭操作
|
|||
|
|
local modules = {
|
|||
|
|
ap = nil,
|
|||
|
|
http_server = nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
-- 创建AP热点
|
|||
|
|
local function create_ap(ap_opts, server_opts)
|
|||
|
|
log.info("WIFI", "创建AP热点: " .. ap_opts.ap_ssid)
|
|||
|
|
log.info("WIFI", "AP密码: " .. ap_opts.ap_pwd)
|
|||
|
|
|
|||
|
|
-- 初始化WiFi
|
|||
|
|
wlan.init()
|
|||
|
|
sys.wait(100)
|
|||
|
|
|
|||
|
|
-- 创建AP
|
|||
|
|
wlan.createAP(ap_opts.ap_ssid, ap_opts.ap_pwd)
|
|||
|
|
|
|||
|
|
-- 配置IP
|
|||
|
|
netdrv.ipv4(socket.LWIP_AP, server_opts.server_addr, "255.255.255.0", "0.0.0.0")
|
|||
|
|
|
|||
|
|
-- 等待网络准备就绪
|
|||
|
|
while netdrv.ready(socket.LWIP_AP) ~= true do
|
|||
|
|
sys.wait(100)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 设置DNS代理
|
|||
|
|
dnsproxy.setup(socket.LWIP_AP, socket.LWIP_GP)
|
|||
|
|
|
|||
|
|
-- 创建DHCP服务器
|
|||
|
|
dhcpsrv.create({adapter=socket.LWIP_AP})
|
|||
|
|
|
|||
|
|
-- 发布AP创建完成事件
|
|||
|
|
sys.publish("AP_CREATE_OK")
|
|||
|
|
|
|||
|
|
log.info("WIFI", "AP热点创建成功")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 初始化SD卡
|
|||
|
|
local function init_sdcard(sdcard_opts)
|
|||
|
|
log.info("SDCARD", "开始初始化SD卡")
|
|||
|
|
|
|||
|
|
-- 双重验证,确认使用的是Air8000开发板
|
|||
|
|
if sdcard_opts.is_8000_development_board == true then
|
|||
|
|
if sdcard_opts.spi_cs == 20 then
|
|||
|
|
if sdcard_opts.spi_id == 1 then
|
|||
|
|
-- 注:Air8000开发板上TF和以太网是同一个SPI,使用开发板时必须要将以太网拉高
|
|||
|
|
-- 如果使用其他硬件,需要根据硬件原理图来决定是否需要此操作
|
|||
|
|
-- 配置以太网供电引脚,设置为输出模式,并启用上拉电阻
|
|||
|
|
gpio.setup(ETH3V3_EN, 1, gpio.PULLUP)
|
|||
|
|
-- 配置以太网片选引脚,设置为输出模式,并启用上拉电阻
|
|||
|
|
gpio.setup(SPI_ETH_CS, 1, gpio.PULLUP)
|
|||
|
|
log.info("sdcard_init", "使用的是开发板,开始将以太网拉高")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local mount_result = nil
|
|||
|
|
|
|||
|
|
if not sdcard_opts.is_sdio then
|
|||
|
|
-- 配置SPI,设置spi_id,波特率为400000,用于SD卡初始化
|
|||
|
|
local result = spi.setup(sdcard_opts.spi_id, nil, 0, 0, 8, 400 * 1000)
|
|||
|
|
log.info("sdcard_init", "open spi", result)
|
|||
|
|
-- 配置SD卡片选引脚,设置为输出模式,并启用上拉电阻
|
|||
|
|
gpio.setup(sdcard_opts.spi_cs, 1, gpio.PULLUP)
|
|||
|
|
-- 挂载SD卡到文件系统,指定挂载点为"/sd"
|
|||
|
|
mount_result = fatfs.mount(fatfs.SPI, "/sd", sdcard_opts.spi_id, sdcard_opts.spi_cs, 24 * 1000 * 1000)
|
|||
|
|
else
|
|||
|
|
-- gpio13为8101TF卡的供电控制引脚,在挂载前需要设置为高电平,不能省略
|
|||
|
|
gpio.setup(13, 1)
|
|||
|
|
mount_result = fatfs.mount(fatfs.SDIO, "/sd", 24 * 1000 * 1000)
|
|||
|
|
end
|
|||
|
|
log.info("SDCARD", "挂载SD卡结果:", mount_result)
|
|||
|
|
|
|||
|
|
-- 获取SD卡的可用空间信息
|
|||
|
|
local data, err = fatfs.getfree("/sd")
|
|||
|
|
if data then
|
|||
|
|
log.info("SDCARD", "SD卡可用空间信息:", json.encode(data))
|
|||
|
|
else
|
|||
|
|
log.info("SDCARD", "获取SD卡空间失败:", err)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return mount_result
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 会话管理
|
|||
|
|
local authenticated_sessions = {}
|
|||
|
|
|
|||
|
|
-- 获取文件信息
|
|||
|
|
local function get_file_info(path)
|
|||
|
|
log.info("FILE_INFO", "获取文件信息: " .. path)
|
|||
|
|
|
|||
|
|
-- 获取文件名
|
|||
|
|
local filename = path:match("([^/]+)$") or ""
|
|||
|
|
|
|||
|
|
-- 获取大小
|
|||
|
|
local direct_size = io.fileSize(path)
|
|||
|
|
if direct_size and direct_size > 0 then
|
|||
|
|
log.info("FILE_INFO", "获取文件大小成功: " .. direct_size .. " 字节")
|
|||
|
|
return {
|
|||
|
|
name = filename,
|
|||
|
|
size = direct_size,
|
|||
|
|
isDirectory = false,
|
|||
|
|
path = path
|
|||
|
|
}
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 检查文件是否存在,避免对文件进行错误的目录判断
|
|||
|
|
if not io.exists(path) then
|
|||
|
|
log.info("FILE_INFO", "文件不存在: " .. path)
|
|||
|
|
return {
|
|||
|
|
name = filename,
|
|||
|
|
size = 0,
|
|||
|
|
isDirectory = false,
|
|||
|
|
path = path
|
|||
|
|
}
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 尝试判断是否为目录
|
|||
|
|
local ret, data = io.lsdir(path, 1, 0)
|
|||
|
|
if ret and data and type(data) == "table" and #data > 0 then
|
|||
|
|
log.info("FILE_INFO", "路径是一个目录: " .. path)
|
|||
|
|
return {
|
|||
|
|
name = filename,
|
|||
|
|
size = 0,
|
|||
|
|
isDirectory = true,
|
|||
|
|
path = path
|
|||
|
|
}
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 检查文件是否存在
|
|||
|
|
if not io.exists(path) then
|
|||
|
|
log.info("FILE_INFO", "文件不存在: " .. path)
|
|||
|
|
return {
|
|||
|
|
name = filename,
|
|||
|
|
size = 0,
|
|||
|
|
isDirectory = false,
|
|||
|
|
path = path
|
|||
|
|
}
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 尝试打开文件获取大小
|
|||
|
|
local file = io.open(path, "rb")
|
|||
|
|
if file then
|
|||
|
|
-- 尝试获取文件大小
|
|||
|
|
local file_size = io.fileSize(path)
|
|||
|
|
|
|||
|
|
-- 如果返回0或nil,尝试通过读取文件内容获取大小
|
|||
|
|
if not file_size or file_size == 0 then
|
|||
|
|
log.info("FILE_INFO", "获取文件大小,尝试读取文件内容")
|
|||
|
|
local content = file:read("*a")
|
|||
|
|
file_size = #content
|
|||
|
|
log.info("FILE_INFO", "使用文件内容长度获取大小: " .. file_size .. " 字节")
|
|||
|
|
else
|
|||
|
|
log.info("FILE_INFO", "获取文件大小成功: " .. file_size .. " 字节")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
file:close()
|
|||
|
|
log.info("FILE_INFO", "成功获取文件信息: " .. filename .. ", 大小: " .. file_size .. " 字节")
|
|||
|
|
return {
|
|||
|
|
name = filename,
|
|||
|
|
size = file_size,
|
|||
|
|
isDirectory = false,
|
|||
|
|
path = path
|
|||
|
|
}
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 定义系统文件的规则(系统文件不显示)
|
|||
|
|
local function is_system_file(filename)
|
|||
|
|
-- 系统文件扩展名列表
|
|||
|
|
local system_extensions = {".luac", ".html", ".md"}
|
|||
|
|
-- 特殊系统文件名
|
|||
|
|
local special_system_files = {".airm2m_all_crc#.bin"}
|
|||
|
|
|
|||
|
|
-- 检查文件名是否匹配特殊系统文件名
|
|||
|
|
for _, sys_file in ipairs(special_system_files) do
|
|||
|
|
if filename == sys_file then
|
|||
|
|
return true
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 检查文件扩展名是否为系统文件扩展名
|
|||
|
|
for _, ext in ipairs(system_extensions) do
|
|||
|
|
if filename:sub(-#ext) == ext then
|
|||
|
|
return true
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 扫描目录
|
|||
|
|
local function scan_with_lsdir(path, files)
|
|||
|
|
log.info("LIST_DIR", "开始扫描目录")
|
|||
|
|
-- 确保路径格式正确,处理多层目录和编码问题
|
|||
|
|
local scan_path = path
|
|||
|
|
log.info("LIST_DIR", "原始路径: " .. scan_path)
|
|||
|
|
|
|||
|
|
-- 规范化路径,处理URL编码残留问题
|
|||
|
|
scan_path = scan_path:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
log.info("LIST_DIR", "解码后路径: " .. scan_path)
|
|||
|
|
|
|||
|
|
-- 移除多余的斜杠
|
|||
|
|
scan_path = scan_path:gsub("//+", "/")
|
|||
|
|
log.info("LIST_DIR", "去重斜杠后路径: " .. scan_path)
|
|||
|
|
|
|||
|
|
-- 规范化路径,移除可能的尾部斜杠
|
|||
|
|
scan_path = scan_path:gsub("/*$", "")
|
|||
|
|
log.info("LIST_DIR", "移除尾部斜杠后路径: " .. scan_path)
|
|||
|
|
|
|||
|
|
-- 确保路径以/开头
|
|||
|
|
if not scan_path:match("^/") then
|
|||
|
|
scan_path = "/" .. scan_path
|
|||
|
|
end
|
|||
|
|
log.info("LIST_DIR", "确保以/开头后路径: " .. scan_path)
|
|||
|
|
|
|||
|
|
-- 确保路径以/结尾
|
|||
|
|
scan_path = scan_path .. (scan_path == "" and "" or "/")
|
|||
|
|
|
|||
|
|
log.info("LIST_DIR", "开始扫描路径: " .. scan_path)
|
|||
|
|
|
|||
|
|
-- 扫描目录,最多列出50个文件,从第0个开始
|
|||
|
|
local ret, data = io.lsdir(scan_path, 50, 0)
|
|||
|
|
|
|||
|
|
if ret then
|
|||
|
|
log.info("LIST_DIR", "成功获取目录内容,文件数量: " .. #data)
|
|||
|
|
log.info("LIST_DIR", "目录内容: " .. json.encode(data))
|
|||
|
|
|
|||
|
|
-- 遍历目录内容
|
|||
|
|
for i = 1, #data do
|
|||
|
|
local entry = data[i]
|
|||
|
|
local is_dir = (entry.type ~= 0)
|
|||
|
|
local entry_type = is_dir and "目录" or "文件"
|
|||
|
|
log.info("LIST_DIR", "找到条目: " .. entry.name .. ", 类型: " .. entry_type)
|
|||
|
|
|
|||
|
|
local full_path = scan_path .. entry.name
|
|||
|
|
|
|||
|
|
-- 处理目录和文件的不同逻辑
|
|||
|
|
if is_dir then
|
|||
|
|
-- 对于目录,直接构造信息
|
|||
|
|
local dir_info = {
|
|||
|
|
name = entry.name,
|
|||
|
|
size = 0,
|
|||
|
|
isDirectory = true,
|
|||
|
|
path = full_path
|
|||
|
|
}
|
|||
|
|
-- 过滤sd卡系统文件夹目录
|
|||
|
|
if entry.name ~= "System Volume Information" then
|
|||
|
|
table.insert(files, dir_info)
|
|||
|
|
log.info("LIST_DIR", "添加目录: " .. entry.name .. ", 路径: " .. full_path)
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
-- 检查是否为用户文件
|
|||
|
|
local is_user_file = not is_system_file(entry.name)
|
|||
|
|
|
|||
|
|
-- 只有用户文件才会被添加到列表中
|
|||
|
|
if is_user_file then
|
|||
|
|
-- 对于文件,调用get_file_info获取详细信息
|
|||
|
|
local file_info = get_file_info(full_path)
|
|||
|
|
if file_info and file_info.size ~= nil then
|
|||
|
|
file_info.isDirectory = false
|
|||
|
|
table.insert(files, file_info)
|
|||
|
|
log.info("LIST_DIR", "添加文件: " .. entry.name .. ", 大小: " .. file_info.size ..
|
|||
|
|
" 字节, 路径: " .. file_info.path)
|
|||
|
|
else
|
|||
|
|
-- 如果get_file_info失败,使用默认值
|
|||
|
|
local default_info = {
|
|||
|
|
name = entry.name,
|
|||
|
|
size = entry.size or 0,
|
|||
|
|
isDirectory = false,
|
|||
|
|
path = full_path
|
|||
|
|
}
|
|||
|
|
table.insert(files, default_info)
|
|||
|
|
log.info("LIST_DIR", "添加文件(默认信息): " .. entry.name .. ", 大小: " ..
|
|||
|
|
(entry.size or 0) .. " 字节")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
return true
|
|||
|
|
else
|
|||
|
|
log.info("LIST_DIR", "扫描失败: " .. (data or "未知错误"))
|
|||
|
|
end
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 列出目录
|
|||
|
|
local function list_directory(path)
|
|||
|
|
-- 初始化文件列表
|
|||
|
|
local files = {}
|
|||
|
|
|
|||
|
|
log.info("LIST_DIR", "开始处理目录请求: " .. path)
|
|||
|
|
|
|||
|
|
-- 扫描方法表
|
|||
|
|
local scan_success = scan_with_lsdir(path, files)
|
|||
|
|
|
|||
|
|
-- 记录扫描结果
|
|||
|
|
if scan_success then
|
|||
|
|
log.info("LIST_DIR", "扫描方法成功")
|
|||
|
|
else
|
|||
|
|
log.info("LIST_DIR", "扫描方法失败")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("LIST_DIR", "目录扫描完成,总共找到文件数量: " .. #files)
|
|||
|
|
return files
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 会话验证
|
|||
|
|
local function validate_session(headers)
|
|||
|
|
-- 获取Cookie中的session_id
|
|||
|
|
local cookies = headers['Cookie'] or ''
|
|||
|
|
local session_id = nil
|
|||
|
|
if cookies then
|
|||
|
|
session_id = cookies:match('session_id=([^;]+)')
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 检查会话ID是否已认证
|
|||
|
|
if session_id and authenticated_sessions[session_id] then
|
|||
|
|
return true
|
|||
|
|
else
|
|||
|
|
return false
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 生成会话ID
|
|||
|
|
local function generate_session_id()
|
|||
|
|
local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|||
|
|
local id = ""
|
|||
|
|
for i = 1, 32 do
|
|||
|
|
local rand = math.random(1, #chars)
|
|||
|
|
id = id .. chars:sub(rand, rand)
|
|||
|
|
end
|
|||
|
|
return id
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 检查字符串是否以指定前缀开头
|
|||
|
|
local function string_starts_with(str, prefix)
|
|||
|
|
return string.sub(str, 1, string.len(prefix)) == prefix
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 解析文件上传数据
|
|||
|
|
local function parse_multipart_data(body, boundary)
|
|||
|
|
log.info("UPLOAD", "开始解析数据,body大小: " .. #body .. " 字节")
|
|||
|
|
|
|||
|
|
local result = {}
|
|||
|
|
local parts = {}
|
|||
|
|
local boundary_pattern = "--" .. boundary
|
|||
|
|
|
|||
|
|
-- 开始解析
|
|||
|
|
if #body > 0 then
|
|||
|
|
log.info("UPLOAD", "使用简化解析方法处理上传数据")
|
|||
|
|
|
|||
|
|
-- 首先尝试从body中提取文件名
|
|||
|
|
local filename_match = string.match(body, 'filename="([^"]+)"')
|
|||
|
|
if filename_match then
|
|||
|
|
result.filename = filename_match
|
|||
|
|
log.info("UPLOAD", "成功提取文件名: " .. filename_match)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 查找内容开始位置
|
|||
|
|
local content_start = string.find(body, "\r\n\r\n")
|
|||
|
|
if content_start then
|
|||
|
|
-- 提取内容部分
|
|||
|
|
local content = string.sub(body, content_start + 4)
|
|||
|
|
|
|||
|
|
-- 移除末尾可能的boundary
|
|||
|
|
local end_pos = string.find(content, "\r\n--" .. boundary, 1, true)
|
|||
|
|
if end_pos then
|
|||
|
|
content = string.sub(content, 1, end_pos - 1)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 清理内容
|
|||
|
|
content = string.gsub(content, "\r\n$", "")
|
|||
|
|
content = string.gsub(content, "\n$", "")
|
|||
|
|
|
|||
|
|
if #content > 0 then
|
|||
|
|
result.content = content
|
|||
|
|
result.size = #content
|
|||
|
|
log.info("UPLOAD", "解析成功,获取内容大小: " .. #content .. " 字节")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("UPLOAD", "multipart数据解析完成," .. (result.content and (result.filename and "成功获取文件: " .. result.filename or "成功获取文件内容") or "未找到有效文件内容"))
|
|||
|
|
return result
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 写入文件,支持分包写入
|
|||
|
|
local function write_file_with_chunks(file_path, content)
|
|||
|
|
-- 检查路径前缀
|
|||
|
|
local storage_type = "内存" -- 默认内存存储
|
|||
|
|
if string.sub(file_path, 1, 4) == "/sd/" then
|
|||
|
|
storage_type = "sdcard"
|
|||
|
|
-- 获取SD卡可用空间
|
|||
|
|
local data, err = fatfs.getfree("/sd")
|
|||
|
|
if not data then
|
|||
|
|
log.error("UPLOAD", "SD卡未挂载或不可用")
|
|||
|
|
return false, "SD卡未挂载或不可用"
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 设置为sd卡可用空闲内存空间
|
|||
|
|
local free_space = tonumber(data.free_kb)
|
|||
|
|
|
|||
|
|
-- 如果无法获取有效空间值,设置默认值以跳过空间检查
|
|||
|
|
if not free_space or free_space <= 0 then
|
|||
|
|
log.warn("UPLOAD", "无法获取准确的SD卡可用空间,跳过空间检查")
|
|||
|
|
free_space = #content + 1
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 确保free_space是数字类型
|
|||
|
|
if type(free_space) ~= "number" then
|
|||
|
|
log.error("UPLOAD", "无法获取有效的SD卡空间大小")
|
|||
|
|
-- 这里可以选择跳过空间检查继续执行,或者返回错误
|
|||
|
|
-- 为了避免崩溃,我们跳过空间检查
|
|||
|
|
else
|
|||
|
|
-- 检查SD卡空间是否足够
|
|||
|
|
if free_space < #content then
|
|||
|
|
log.error("UPLOAD", "SD卡空间不足,需要 " .. #content .. " 字节,可用 " .. free_space .. " 字节")
|
|||
|
|
return false, "SD卡空间不足"
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("UPLOAD", "开始写入文件到" .. storage_type .. ": " .. file_path)
|
|||
|
|
|
|||
|
|
-- 保留完整路径,不要只提取文件名
|
|||
|
|
-- 只有当路径不是绝对路径(不以/开头)时才需要特殊处理
|
|||
|
|
if not file_path:match("^/") then
|
|||
|
|
-- 如果不是绝对路径,可能需要获取文件名,但保留相对路径
|
|||
|
|
local filename = file_path:match("[^/]+$")
|
|||
|
|
-- 保持原始路径不变,确保写入到正确位置
|
|||
|
|
log.info("UPLOAD", "使用相对路径: " .. file_path)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 根据目标存储类型调整分块大小
|
|||
|
|
local chunk_size
|
|||
|
|
if storage_type == "sdcard" then
|
|||
|
|
-- SD卡写入使用较大分块以提高性能
|
|||
|
|
chunk_size = 32 * 1024 -- 32KB
|
|||
|
|
else
|
|||
|
|
-- 内存写入使用较小分块以避免内存峰值
|
|||
|
|
chunk_size = 16 * 1024 -- 16KB
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 安全地打开文件进行写入,使用更健壮的错误处理
|
|||
|
|
local file, err
|
|||
|
|
|
|||
|
|
-- 尝试不同的文件打开模式
|
|||
|
|
local modes = {"wb", "w"}
|
|||
|
|
|
|||
|
|
for _, mode in ipairs(modes) do
|
|||
|
|
-- 先尝试删除可能存在的同名文件(忽略错误)
|
|||
|
|
pcall(os.remove, file_path)
|
|||
|
|
|
|||
|
|
file, err = io.open(file_path, mode)
|
|||
|
|
if file then
|
|||
|
|
log.info("UPLOAD", "成功以模式" .. mode .. "打开文件: " .. file_path)
|
|||
|
|
break
|
|||
|
|
else
|
|||
|
|
log.warn("UPLOAD", "无法以模式" .. mode .. "打开文件: " .. file_path .. ", 错误: " .. (err or "未知错误"))
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
if not file then
|
|||
|
|
-- 尝试提取原始文件名,确保使用原始名称
|
|||
|
|
local original_filename = file_path:match("([^/]+)$") or "upload_file"
|
|||
|
|
|
|||
|
|
-- 对于内存存储
|
|||
|
|
if storage_type == "内存" then
|
|||
|
|
log.info("UPLOAD", "尝试使用根目录路径: /" .. original_filename)
|
|||
|
|
file, err = io.open("/" .. original_filename, "w")
|
|||
|
|
if file then
|
|||
|
|
file_path = "/" .. original_filename
|
|||
|
|
else
|
|||
|
|
-- 如果web还是不能显示,使用随机临时文件名
|
|||
|
|
-- 先使用原始文件名,不添加时间戳和随机数
|
|||
|
|
local simple_filename = original_filename
|
|||
|
|
log.info("UPLOAD", "尝试使用原始文件名: " .. simple_filename)
|
|||
|
|
file, err = io.open(simple_filename, "w")
|
|||
|
|
if not file then
|
|||
|
|
-- 添加时间戳方式显示文件名,排除因为文件名导致的无法显示
|
|||
|
|
simple_filename = original_filename .. "_" .. os.time()
|
|||
|
|
log.info("UPLOAD", "尝试使用带时间戳的文件名: " .. simple_filename)
|
|||
|
|
file, err = io.open(simple_filename, "w")
|
|||
|
|
file_path = simple_filename
|
|||
|
|
else
|
|||
|
|
file_path = simple_filename
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
-- SD卡存储时的处理
|
|||
|
|
log.info("UPLOAD", "尝试使用文件名: " .. original_filename)
|
|||
|
|
file, err = io.open(original_filename, "w")
|
|||
|
|
file_path = original_filename
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
if not file then
|
|||
|
|
log.error("UPLOAD", "最终无法创建文件: " .. file_path .. ", 错误: " .. (err or "未知错误"))
|
|||
|
|
return false, "无法创建文件: " .. (err or "未知错误")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 使用分块写入
|
|||
|
|
local total_size = #content
|
|||
|
|
local pos = 1
|
|||
|
|
local chunks_written = 0
|
|||
|
|
|
|||
|
|
-- 优化文件写入过程
|
|||
|
|
while pos <= total_size do
|
|||
|
|
local chunk_end = math.min(pos + chunk_size - 1, total_size)
|
|||
|
|
local chunk = string.sub(content, pos, chunk_end)
|
|||
|
|
local chunk_len = #chunk
|
|||
|
|
|
|||
|
|
local success, write_err = file:write(chunk)
|
|||
|
|
if not success then
|
|||
|
|
file:close()
|
|||
|
|
log.error("UPLOAD", "写入文件失败(块" .. chunks_written .. "): " .. file_path .. ", 错误: " .. (write_err or "未知错误"))
|
|||
|
|
return false, "写入文件失败: " .. (write_err or "未知错误")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 在SD卡写入时,每写入一个块就刷新缓冲区,避免数据丢失
|
|||
|
|
if storage_type == "sdcard" then
|
|||
|
|
file:flush()
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
chunks_written = chunks_written + 1
|
|||
|
|
pos = pos + chunk_size
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 确保所有数据都写入存储介质
|
|||
|
|
file:flush()
|
|||
|
|
file:close()
|
|||
|
|
|
|||
|
|
-- 验证写入是否成功(尝试读取文件大小)
|
|||
|
|
local file_info = get_file_info(file_path)
|
|||
|
|
if file_info and file_info.size == total_size then
|
|||
|
|
log.info("UPLOAD", "文件写入成功(" .. chunks_written .. "块): " .. file_path .. ", 大小: " .. total_size .. " 字节, 存储类型: " .. storage_type)
|
|||
|
|
return true, nil, file_path -- 返回实际使用的文件路径
|
|||
|
|
else
|
|||
|
|
log.warn("UPLOAD", "文件写入可能不完整: " .. file_path .. ", 期望大小: " .. total_size .. ", 实际大小: " .. (file_info and file_info.size or "未知"))
|
|||
|
|
return true, "文件写入可能不完整", file_path -- 返回实际使用的文件路径
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- server请求处理
|
|||
|
|
local function handle_http_request(fd, method, uri, headers, body)
|
|||
|
|
log.info("HTTP", method, uri)
|
|||
|
|
|
|||
|
|
-- 登录
|
|||
|
|
if uri == "/login" and method == "POST" then
|
|||
|
|
local data = json.decode(body or "{}")
|
|||
|
|
log.info("LOGIN", "收到登录请求,用户名: " .. (data and data.username or "空"))
|
|||
|
|
if data and data.username == user_server_opts.user_name and data.password == user_server_opts.user_pwd then
|
|||
|
|
local session_id = generate_session_id()
|
|||
|
|
authenticated_sessions[session_id] = os.time()
|
|||
|
|
|
|||
|
|
-- 计算已认证会话数量
|
|||
|
|
local session_count = 0
|
|||
|
|
for _ in pairs(authenticated_sessions) do
|
|||
|
|
session_count = session_count + 1
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("LOGIN", "登录成功!用户名: " .. data.username)
|
|||
|
|
log.info("LOGIN", "生成SessionID: " .. session_id)
|
|||
|
|
log.info("LOGIN", "当前已认证会话数量: " .. session_count)
|
|||
|
|
|
|||
|
|
-- 设置Cookie
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json",
|
|||
|
|
["Set-Cookie"] = "session_id=" .. session_id .. "; Path=/; Max-Age=3600"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = true,
|
|||
|
|
session_id = session_id
|
|||
|
|
})
|
|||
|
|
else
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "用户名或密码错误"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 登出
|
|||
|
|
if uri == "/logout" and method == "POST" then
|
|||
|
|
local cookie = headers["Cookie"] or ""
|
|||
|
|
for session_id in cookie:gmatch("session_id=([^;]+)") do
|
|||
|
|
authenticated_sessions[session_id] = nil
|
|||
|
|
end
|
|||
|
|
return 200, {
|
|||
|
|
["Set-Cookie"] = "session_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|||
|
|
}, ""
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 检查认证
|
|||
|
|
if uri == "/check-auth" then
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
authenticated = validate_session(headers)
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 扫描文件接口
|
|||
|
|
if string_starts_with(uri, "/scan-files") then
|
|||
|
|
log.info("SCAN", "收到文件扫描请求")
|
|||
|
|
|
|||
|
|
-- 检查传统认证方式
|
|||
|
|
local is_authenticated = validate_session(headers)
|
|||
|
|
|
|||
|
|
-- 如果传统认证失败,尝试从URL参数中获取用户名和密码
|
|||
|
|
if not is_authenticated then
|
|||
|
|
local url_username = uri:match("username=([^&]+)")
|
|||
|
|
local url_password = uri:match("password=([^&]+)")
|
|||
|
|
if url_username and url_password then
|
|||
|
|
url_username = url_username:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
url_password = url_password:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
|
|||
|
|
log.info("AUTH", "扫描请求通过URL参数认证成功")
|
|||
|
|
is_authenticated = true
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "扫描请求URL参数认证失败: 用户名或密码错误")
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "扫描请求URL中没有找到用户名和密码参数")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 如果认证仍然失败,返回未授权访问
|
|||
|
|
if not is_authenticated then
|
|||
|
|
log.info("HTTP", "未授权访问文件扫描功能")
|
|||
|
|
return 401, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "未授权访问"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 执行文件扫描
|
|||
|
|
log.info("SCAN", "开始扫描内部文件系统和TF卡...")
|
|||
|
|
|
|||
|
|
-- 定义要扫描的挂载点,包括SD卡挂载点
|
|||
|
|
local mount_points = {"/", "/luadb/", "/sd/"}
|
|||
|
|
local found_files = {}
|
|||
|
|
|
|||
|
|
-- 对每个挂载点执行扫描
|
|||
|
|
for _, mount_point in ipairs(mount_points) do
|
|||
|
|
log.info("SCAN", "开始扫描挂载点: " .. mount_point)
|
|||
|
|
|
|||
|
|
-- 如果路径不以/结尾,添加/确保路径格式正确
|
|||
|
|
local scan_path = mount_point
|
|||
|
|
if not scan_path:match("/$") then
|
|||
|
|
scan_path = scan_path .. (scan_path == "" and "" or "/")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 扫描目录
|
|||
|
|
log.info("SCAN", "开始扫描路径: " .. scan_path)
|
|||
|
|
-- 尝试列出目录内容,最多列出50个文件
|
|||
|
|
local ret, data = io.lsdir(scan_path, 50, 0)
|
|||
|
|
|
|||
|
|
if ret then
|
|||
|
|
log.info("SCAN", "成功获取目录内容,文件数量: " .. #data)
|
|||
|
|
log.info("SCAN", "目录内容: " .. json.encode(data))
|
|||
|
|
|
|||
|
|
-- 遍历目录内容
|
|||
|
|
for i = 1, #data do
|
|||
|
|
local entry = data[i]
|
|||
|
|
local full_path = scan_path .. entry.name
|
|||
|
|
|
|||
|
|
-- 如果是文件(type == 0),添加到文件列表
|
|||
|
|
if entry.type == 0 then
|
|||
|
|
local info = get_file_info(full_path)
|
|||
|
|
if info then
|
|||
|
|
table.insert(found_files, {
|
|||
|
|
name = entry.name,
|
|||
|
|
size = info.size,
|
|||
|
|
path = full_path
|
|||
|
|
})
|
|||
|
|
log.info("SCAN", "找到文件: " .. entry.name .. ", 大小: " .. info.size ..
|
|||
|
|
" 字节, 路径: " .. full_path)
|
|||
|
|
else
|
|||
|
|
-- 如果get_file_info失败,使用io.lsdir返回的大小
|
|||
|
|
table.insert(found_files, {
|
|||
|
|
name = entry.name,
|
|||
|
|
size = entry.size or 0,
|
|||
|
|
path = full_path
|
|||
|
|
})
|
|||
|
|
log.info("SCAN", "找到文件: " .. entry.name .. ", 大小: " .. (entry.size or 0) ..
|
|||
|
|
" 字节, 路径: " .. full_path)
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
-- 如果是目录,记录但不添加到文件列表
|
|||
|
|
log.info("SCAN", "找到目录: " .. entry.name .. ", 路径: " .. full_path)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
log.info("SCAN", "扫描失败: " .. (data or "未知错误"))
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
local list_files = list_directory(mount_point)
|
|||
|
|
if list_files then
|
|||
|
|
for _, file in ipairs(list_files) do
|
|||
|
|
-- 只记录非目录文件
|
|||
|
|
if not file.isDirectory then
|
|||
|
|
-- 确保文件路径正确
|
|||
|
|
local file_path = file.path or (mount_point .. (mount_point == "/" and "" or "/") .. file.name)
|
|||
|
|
|
|||
|
|
-- 检查文件是否已添加
|
|||
|
|
local is_exists = false
|
|||
|
|
for _, f in ipairs(found_files) do
|
|||
|
|
if f.name == file.name and f.path == file_path then
|
|||
|
|
is_exists = true
|
|||
|
|
break
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
if not is_exists then
|
|||
|
|
table.insert(found_files, {
|
|||
|
|
name = file.name,
|
|||
|
|
size = file.size,
|
|||
|
|
path = file_path
|
|||
|
|
})
|
|||
|
|
log.info("SCAN",
|
|||
|
|
"从list_directory添加文件: " .. file.name .. ", 大小: " .. file.size ..
|
|||
|
|
" 字节, 路径: " .. file_path)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("SCAN", "挂载点扫描完成: " .. mount_point .. ", 找到文件: " .. #found_files .. " 个")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 扫描完成后,打印详细的文件列表
|
|||
|
|
log.info("SCAN", "文件扫描完成,总共找到文件数量: " .. #found_files)
|
|||
|
|
for i, file in ipairs(found_files) do
|
|||
|
|
log.info("SCAN", "文件[" .. i .. "]: " .. file.name .. ", 大小: " .. file.size .. " 字节, 路径: " ..
|
|||
|
|
file.path)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 返回扫描结果
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = true,
|
|||
|
|
foundFiles = #found_files,
|
|||
|
|
files = found_files,
|
|||
|
|
message = "文件扫描完成"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 文件列表
|
|||
|
|
if string_starts_with(uri, "/list") then
|
|||
|
|
|
|||
|
|
-- 检查传统认证方式
|
|||
|
|
local is_authenticated = validate_session(headers)
|
|||
|
|
|
|||
|
|
-- 如果传统认证失败,尝试从URL参数中获取用户名和密码
|
|||
|
|
if not is_authenticated then
|
|||
|
|
local url_username = uri:match("username=([^&]+)")
|
|||
|
|
local url_password = uri:match("password=([^&]+)")
|
|||
|
|
if url_username and url_password then
|
|||
|
|
url_username = url_username:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
url_password = url_password:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
|
|||
|
|
log.info("AUTH", "通过URL参数认证成功")
|
|||
|
|
is_authenticated = true
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "URL参数认证失败: 用户名或密码错误")
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "URL中没有找到用户名和密码参数")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 如果认证仍然失败,返回未授权访问
|
|||
|
|
if not is_authenticated then
|
|||
|
|
log.info("HTTP", "未授权访问文件列表")
|
|||
|
|
return 401, {
|
|||
|
|
["Content-Type"] = "text/plain"
|
|||
|
|
}, "未授权访问"
|
|||
|
|
end
|
|||
|
|
-- 若没有获取到URI中path参数,则默认使用/luadb目录,防止文件无法上传
|
|||
|
|
local path = uri:match("path=([^&]+)") or "/luadb"
|
|||
|
|
-- 确保路径不会被错误识别为空或根路径
|
|||
|
|
if path == "" or path == "/" then
|
|||
|
|
path = "/luadb"
|
|||
|
|
log.info("HTTP", "修正默认路径为: " .. path)
|
|||
|
|
end
|
|||
|
|
log.info("HTTP", "请求的文件列表路径: " .. path)
|
|||
|
|
-- 将%xx格式的十六进制转义序列还原为对应字符
|
|||
|
|
path = path:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
log.info("HTTP", "解码后的文件列表路径: " .. path)
|
|||
|
|
|
|||
|
|
-- 调用list_directory函数扫描目录
|
|||
|
|
log.info("HTTP", "开始扫描目录")
|
|||
|
|
local files = list_directory(path)
|
|||
|
|
|
|||
|
|
-- 请求根路径时,过滤系统文件
|
|||
|
|
if path == "/" then
|
|||
|
|
log.info("HTTP", "过滤根路径中的系统文件")
|
|||
|
|
local filtered_files = {}
|
|||
|
|
for _, file in ipairs(files) do
|
|||
|
|
-- 过滤.nvm系统文件和系统配置文件
|
|||
|
|
if not file.isDirectory and not file.name:match("%.nvm$") and file.name ~= "plat_config" then
|
|||
|
|
table.insert(filtered_files, file)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
files = filtered_files
|
|||
|
|
-- 如果是/luadb路径请求,添加根目录下的上传文件,并确保过滤系统文件
|
|||
|
|
elseif path == "/luadb" then
|
|||
|
|
log.info("HTTP", "扫描根目录下的上传文件")
|
|||
|
|
local root_files = list_directory("/")
|
|||
|
|
for _, file in ipairs(root_files) do
|
|||
|
|
-- 只添加文件,不添加目录,过滤系统文件
|
|||
|
|
if not file.isDirectory and not file.name:match("%.nvm$") and file.name ~= "plat_config" then
|
|||
|
|
table.insert(files, file)
|
|||
|
|
log.info("HTTP", "添加上传文件: " .. file.name .. ", 大小: " .. file.size)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 记录传给页面的文件数据
|
|||
|
|
log.info("HTTP", "准备返回文件列表,数量: " .. #files)
|
|||
|
|
for i, file in ipairs(files) do
|
|||
|
|
log.info("HTTP", "文件[" .. i .. "]: " .. file.name .. ", 大小: " .. file.size)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = true,
|
|||
|
|
files = files
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 文件下载
|
|||
|
|
if string_starts_with(uri, "/download") then
|
|||
|
|
log.info("DOWNLOAD", "收到下载请求: " .. uri)
|
|||
|
|
|
|||
|
|
-- 检查传统认证方式
|
|||
|
|
local is_authenticated = validate_session(headers)
|
|||
|
|
|
|||
|
|
-- 如果传统认证失败,尝试从URL参数中获取用户名和密码
|
|||
|
|
if not is_authenticated then
|
|||
|
|
local url_username = uri:match("username=([^&]+)")
|
|||
|
|
local url_password = uri:match("password=([^&]+)")
|
|||
|
|
if url_username and url_password then
|
|||
|
|
url_username = url_username:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
url_password = url_password:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
|
|||
|
|
log.info("AUTH", "下载请求通过URL参数认证成功")
|
|||
|
|
is_authenticated = true
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "下载请求URL参数认证失败: 用户名或密码错误")
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "下载请求URL中没有找到用户名和密码参数")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 如果认证仍然失败,返回未授权访问
|
|||
|
|
if not is_authenticated then
|
|||
|
|
log.info("HTTP", "未授权访问文件下载")
|
|||
|
|
return 401, {
|
|||
|
|
["Content-Type"] = "text/plain"
|
|||
|
|
}, "未授权访问"
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 获取请求的文件路径
|
|||
|
|
local path = uri:match("path=([^&]+)") or ""
|
|||
|
|
path = path:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
-- 检查文件是否存在
|
|||
|
|
if not io.exists(path) then
|
|||
|
|
log.info("DOWNLOAD", "文件不存在: " .. path)
|
|||
|
|
return 404, {
|
|||
|
|
["Content-Type"] = "text/plain"
|
|||
|
|
}, "文件不存在"
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 尝试打开文件以确认可访问性并获取文件信息
|
|||
|
|
local file = io.open(path, "rb")
|
|||
|
|
if not file then
|
|||
|
|
log.info("DOWNLOAD", "文件无法打开: " .. path)
|
|||
|
|
return 404, {
|
|||
|
|
["Content-Type"] = "text/plain"
|
|||
|
|
}, "文件无法打开"
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 获取文件名
|
|||
|
|
local filename = path:match("([^/]+)$")
|
|||
|
|
|
|||
|
|
-- 获取文件大小
|
|||
|
|
local file_size = io.fileSize(path)
|
|||
|
|
|
|||
|
|
-- 关闭文件
|
|||
|
|
file:close()
|
|||
|
|
|
|||
|
|
log.info("DOWNLOAD", "确认文件信息: " .. filename .. ", 大小: " .. file_size .. " 字节")
|
|||
|
|
|
|||
|
|
-- 使用httpsrv下载,直接重定向URL
|
|||
|
|
-- 如需要下载文件系统中123.mp3,直接重定向到URL:http://192.168.4.1/123.mp3
|
|||
|
|
-- 如果路径以/sd/开头,则保留完整的sd路径
|
|||
|
|
local redirect_url = "/" .. filename
|
|||
|
|
if string_starts_with(path, "/sd/") then
|
|||
|
|
-- 保留完整的sd路径,以便直接访问sd卡文件及其子目录
|
|||
|
|
redirect_url = path
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("DOWNLOAD", "开始下载文件:" .. redirect_url)
|
|||
|
|
|
|||
|
|
-- 返回HTTP 302重定向响应
|
|||
|
|
return 302, {
|
|||
|
|
["Location"] = redirect_url,
|
|||
|
|
["Content-Type"] = "text/html"
|
|||
|
|
}, [[
|
|||
|
|
<html>
|
|||
|
|
<head><title>重定向下载</title></head>
|
|||
|
|
<body>
|
|||
|
|
<p>正在重定向到文件下载...</p>
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
]]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 文件上传
|
|||
|
|
if string_starts_with(uri, "/upload") and method == "POST" then
|
|||
|
|
log.info("UPLOAD", "收到文件上传请求")
|
|||
|
|
|
|||
|
|
-- 检查传统认证方式
|
|||
|
|
local is_authenticated = validate_session(headers)
|
|||
|
|
|
|||
|
|
-- 如果传统认证失败,尝试从URL参数中获取用户名和密码
|
|||
|
|
if not is_authenticated then
|
|||
|
|
local url_username = uri:match("username=([^&]+)")
|
|||
|
|
local url_password = uri:match("password=([^&]+)")
|
|||
|
|
if url_username and url_password then
|
|||
|
|
url_username = url_username:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
url_password = url_password:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
|
|||
|
|
log.info("AUTH", "上传请求通过URL参数认证成功")
|
|||
|
|
is_authenticated = true
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "上传请求URL参数认证失败: 用户名或密码错误")
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "上传请求URL中没有找到用户名和密码参数")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 如果认证仍然失败,返回未授权访问
|
|||
|
|
if not is_authenticated then
|
|||
|
|
log.info("HTTP", "未授权访问文件上传")
|
|||
|
|
return 401, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "未授权访问"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 获取上传参数
|
|||
|
|
local target_path = uri:match("path=([^&]+)") or "/luadb"
|
|||
|
|
local filename = uri:match("filename=([^&]+)")
|
|||
|
|
|
|||
|
|
-- URL解码
|
|||
|
|
target_path = target_path:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
|
|||
|
|
if filename then
|
|||
|
|
filename = filename:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
else
|
|||
|
|
log.error("UPLOAD", "未提供文件名")
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "未提供文件名"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 验证目标路径
|
|||
|
|
if target_path ~= "/luadb" and target_path ~= "/sd" then
|
|||
|
|
log.error("UPLOAD", "无效的上传目标路径: " .. target_path)
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "无效的上传目标路径"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 检查SD卡是否挂载(如果目标是SD卡)
|
|||
|
|
if target_path == "/sd" then
|
|||
|
|
local free_space = fatfs.getfree("/sd")
|
|||
|
|
if not free_space then
|
|||
|
|
log.error("UPLOAD", "SD卡未挂载")
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "SD卡未挂载"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 构建完整的文件路径
|
|||
|
|
local file_path = target_path .. "/" .. filename
|
|||
|
|
|
|||
|
|
-- 输出headers的完整内容,帮助诊断问题
|
|||
|
|
if headers then
|
|||
|
|
log.info("UPLOAD", "headers表类型: " .. type(headers))
|
|||
|
|
local headers_str = "{ "
|
|||
|
|
for k, v in pairs(headers) do
|
|||
|
|
headers_str = headers_str .. k .. "=" .. tostring(v) .. ", "
|
|||
|
|
end
|
|||
|
|
headers_str = headers_str .. "}"
|
|||
|
|
log.info("UPLOAD", "所有请求头: " .. headers_str)
|
|||
|
|
else
|
|||
|
|
log.warn("UPLOAD", "headers参数为nil")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 获取Content-Type头部,尝试多种可能的键名
|
|||
|
|
local content_type = ""
|
|||
|
|
if headers then
|
|||
|
|
-- 尝试标准的Content-Type键
|
|||
|
|
content_type = headers["Content-Type"] or headers["content-type"] or headers["Content-type"] or ""
|
|||
|
|
log.info("UPLOAD", "接收到的Content-Type: '" .. content_type .. "'")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 采用正则表达式处理各种格式的boundary参数
|
|||
|
|
-- 尝试多种格式的匹配
|
|||
|
|
local boundary = nil
|
|||
|
|
if content_type and content_type ~= "" then
|
|||
|
|
boundary =
|
|||
|
|
-- 匹配不带引号的boundary: boundary=abc123
|
|||
|
|
content_type:match("boundary=([^; ]+)") or
|
|||
|
|
-- 匹配带引号的boundary: boundary="abc123"
|
|||
|
|
content_type:match('boundary="([^"]+)"') or
|
|||
|
|
-- 匹配带单引号的boundary: boundary='abc123'
|
|||
|
|
content_type:match("boundary='([^']+)'")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
if not boundary then
|
|||
|
|
log.warn("UPLOAD", "Content-Type中未找到boundary,尝试从请求体中提取")
|
|||
|
|
|
|||
|
|
-- 直接从请求体中提取boundary
|
|||
|
|
if body and body ~= "" then
|
|||
|
|
-- 尝试匹配请求体中的第一个boundary行,通常格式为 "--xxxxxxx"
|
|||
|
|
local body_boundary = body:match("^%-%-(.+)")
|
|||
|
|
if body_boundary then
|
|||
|
|
boundary = body_boundary
|
|||
|
|
log.info("UPLOAD", "成功从请求体中提取boundary: ")
|
|||
|
|
else
|
|||
|
|
-- 尝试匹配可能的Content-Type行
|
|||
|
|
local body_content_type = body:match("Content%-Type: multipart/form%-data; boundary=(.+)")
|
|||
|
|
if body_content_type then
|
|||
|
|
boundary = body_content_type
|
|||
|
|
log.info("UPLOAD", "成功从请求体中的Content-Type提取boundary: ")
|
|||
|
|
else
|
|||
|
|
log.error("UPLOAD", "无法解析multipart边界,Content-Type为空,请求体中也未找到")
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "无法解析上传数据格式"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
log.error("UPLOAD", "请求体为空,无法提取boundary")
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "请求体为空,无法解析上传数据"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("UPLOAD", "成功解析boundary: " .. boundary)
|
|||
|
|
|
|||
|
|
log.info("UPLOAD", "上传参数: 目标路径=" .. target_path .. ", 文件名=" .. filename .. ", 完整路径=" .. file_path)
|
|||
|
|
|
|||
|
|
-- 解析multipart数据
|
|||
|
|
local upload_data = parse_multipart_data(body or "", boundary)
|
|||
|
|
|
|||
|
|
if not upload_data.content then
|
|||
|
|
log.error("UPLOAD", "无法解析上传文件数据")
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "无法解析上传文件数据"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 检查文件大小(200KB限制)
|
|||
|
|
if #upload_data.content > 200 * 1024 then
|
|||
|
|
log.error("UPLOAD", "文件大小超过限制: " .. #upload_data.content .. " 字节")
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "文件大小超过200KB限制"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 检查sd容量和内存容量
|
|||
|
|
local available_space
|
|||
|
|
if target_path == "/luadb" then
|
|||
|
|
-- 检查系统内存容量
|
|||
|
|
local total_mem, used_mem, max_used_mem = rtos.meminfo("sys")
|
|||
|
|
if total_mem and used_mem then
|
|||
|
|
local free_mem = total_mem - used_mem
|
|||
|
|
log.info("UPLOAD", "系统内存信息 - 总内存:", total_mem, "已用:", used_mem, "可用:", free_mem)
|
|||
|
|
-- 设置可用空闲内存空间
|
|||
|
|
available_space = free_mem
|
|||
|
|
else
|
|||
|
|
log.info("UPLOAD", "获取系统内存信息失败,无法进行上传")
|
|||
|
|
return 200, {["Content-Type"] = "application/json"},
|
|||
|
|
json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "获取系统内存失败,无法进行上传"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
elseif target_path == "/sd" then
|
|||
|
|
-- 获取SD卡可用空间
|
|||
|
|
local data, err = fatfs.getfree("/sd")
|
|||
|
|
if data then
|
|||
|
|
log.info("UPLOAD", "SD卡可用空间信息:", json.encode(data))
|
|||
|
|
-- 设置为sd卡可用空闲内存空间
|
|||
|
|
available_space = tonumber(data.free_kb)
|
|||
|
|
else
|
|||
|
|
log.info("UPLOAD", "获取SD卡空间失败:", err)
|
|||
|
|
available_space = 1024 * 1024 -- 默认1MB
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
if available_space and available_space < #upload_data.content * 2 then -- 预留足够的空间
|
|||
|
|
log.error("UPLOAD", "存储空间不足: 需要 " .. #upload_data.content * 2 .. " 字节, 可用 " .. available_space .. " 字节")
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "存储空间不足,需要至少 " .. (#upload_data.content * 2) .. " 字节"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 写入文件,保存原始请求路径
|
|||
|
|
local original_requested_path = file_path
|
|||
|
|
local success, err, actual_path = write_file_with_chunks(file_path, upload_data.content)
|
|||
|
|
|
|||
|
|
if success then
|
|||
|
|
-- 日志记录
|
|||
|
|
log.info("UPLOAD", "文件上传成功: " .. filename .. ", 大小: " .. #upload_data.content .. " 字节, 实际保存路径: " .. actual_path)
|
|||
|
|
-- 上传成功后再次收集垃圾
|
|||
|
|
collectgarbage()
|
|||
|
|
|
|||
|
|
-- 生成响应信息
|
|||
|
|
local message = "文件上传成功"
|
|||
|
|
if actual_path ~= original_requested_path then
|
|||
|
|
message = message .. ", 由于目录限制,已保存到: " .. actual_path
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = true,
|
|||
|
|
message = message,
|
|||
|
|
filename = filename,
|
|||
|
|
size = #upload_data.content,
|
|||
|
|
path = actual_path,
|
|||
|
|
original_path = original_requested_path
|
|||
|
|
})
|
|||
|
|
else
|
|||
|
|
log.error("UPLOAD", "文件上传失败: " .. (err or "未知错误"))
|
|||
|
|
-- 即使失败也尝试收集垃圾
|
|||
|
|
collectgarbage()
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "文件上传失败: " .. (err or "未知错误")
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 文件删除
|
|||
|
|
if string_starts_with(uri, "/delete") and method == "POST" then
|
|||
|
|
-- 检查传统认证方式
|
|||
|
|
local is_authenticated = validate_session(headers)
|
|||
|
|
|
|||
|
|
-- 如果传统认证失败,尝试从URL参数中获取用户名和密码
|
|||
|
|
if not is_authenticated then
|
|||
|
|
local url_username = uri:match("username=([^&]+)")
|
|||
|
|
local url_password = uri:match("password=([^&]+)")
|
|||
|
|
if url_username and url_password then
|
|||
|
|
url_username = url_username:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
url_password = url_password:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
|
|||
|
|
log.info("AUTH", "通过URL参数认证成功")
|
|||
|
|
is_authenticated = true
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "URL参数认证失败: 用户名或密码错误")
|
|||
|
|
end
|
|||
|
|
else
|
|||
|
|
log.info("AUTH", "URL中没有找到用户名和密码参数")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 如果认证仍然失败,返回未授权访问
|
|||
|
|
if not is_authenticated then
|
|||
|
|
log.info("HTTP", "未授权访问文件删除")
|
|||
|
|
return 401, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "未授权访问"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
local path = uri:match("path=([^&]+)") or ""
|
|||
|
|
path = path:gsub("%%(%x%x)", function(hex)
|
|||
|
|
return string.char(tonumber(hex, 16))
|
|||
|
|
end)
|
|||
|
|
if not io.exists(path) then
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "文件不存在"
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
-- 尝试删除文件
|
|||
|
|
local ok, err = os.remove(path)
|
|||
|
|
if ok then
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = true,
|
|||
|
|
message = "文件删除成功"
|
|||
|
|
})
|
|||
|
|
else
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "application/json"
|
|||
|
|
}, json.encode({
|
|||
|
|
success = false,
|
|||
|
|
message = "删除失败: " .. (err or "未知错误")
|
|||
|
|
})
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 首页
|
|||
|
|
if uri == "/" then
|
|||
|
|
local html_file = io.open("/index.html", "r")
|
|||
|
|
if html_file then
|
|||
|
|
local content = html_file:read("*a")
|
|||
|
|
html_file:close()
|
|||
|
|
return 200, {
|
|||
|
|
["Content-Type"] = "text/html"
|
|||
|
|
}, content
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 直接文件路径访问
|
|||
|
|
-- 检查是否是API路径,如果不是,则尝试作为文件路径访问
|
|||
|
|
local is_api_path = string_starts_with(uri, "/login") or string_starts_with(uri, "/logout") or
|
|||
|
|
string_starts_with(uri, "/check-auth") or string_starts_with(uri, "/scan-files") or
|
|||
|
|
string_starts_with(uri, "/list") or string_starts_with(uri, "/download") or
|
|||
|
|
string_starts_with(uri, "/delete") or uri == "/"
|
|||
|
|
|
|||
|
|
if not is_api_path then
|
|||
|
|
log.info("DIRECT_ACCESS", "尝试直接访问文件: " .. uri)
|
|||
|
|
|
|||
|
|
-- 确定实际文件路径
|
|||
|
|
local file_path = uri
|
|||
|
|
|
|||
|
|
-- 如果路径不是以/sd/开头,则默认在/luadb/目录下查找
|
|||
|
|
if not string_starts_with(file_path, "/sd/") then
|
|||
|
|
-- 移除开头的斜杠
|
|||
|
|
if file_path:sub(1, 1) == "/" then
|
|||
|
|
file_path = file_path:sub(2)
|
|||
|
|
end
|
|||
|
|
-- 添加/luadb/前缀
|
|||
|
|
file_path = "/luadb/" .. file_path
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("DIRECT_ACCESS", "解析后的实际文件路径: " .. file_path)
|
|||
|
|
|
|||
|
|
-- 检查文件是否存在
|
|||
|
|
if not io.exists(file_path) then
|
|||
|
|
log.info("DIRECT_ACCESS", "文件不存在: " .. file_path)
|
|||
|
|
return 404, {
|
|||
|
|
["Content-Type"] = "text/plain"
|
|||
|
|
}, "文件不存在"
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 尝试打开文件
|
|||
|
|
local file = io.open(file_path, "rb")
|
|||
|
|
if not file then
|
|||
|
|
log.info("DIRECT_ACCESS", "文件无法打开: " .. file_path)
|
|||
|
|
return 404, {
|
|||
|
|
["Content-Type"] = "text/plain"
|
|||
|
|
}, "文件无法打开"
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 获取文件名
|
|||
|
|
local filename = file_path:match("([^/]+)$")
|
|||
|
|
|
|||
|
|
-- 读取文件内容
|
|||
|
|
local content = file:read("*a")
|
|||
|
|
|
|||
|
|
-- 关闭文件
|
|||
|
|
file:close()
|
|||
|
|
|
|||
|
|
log.info("DIRECT_ACCESS", "文件读取完成: " .. filename .. ", 大小: " .. #content .. " 字节")
|
|||
|
|
|
|||
|
|
-- 设置HTTP头部
|
|||
|
|
local response_headers = {
|
|||
|
|
["Content-Type"] = "application/octet-stream",
|
|||
|
|
["Content-Disposition"] = "attachment; filename=\"" .. filename .. "\""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return 200, response_headers, content
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return 404, {
|
|||
|
|
["Content-Type"] = "text/plain"
|
|||
|
|
}, "页面未找到"
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- server服务器启动任务
|
|||
|
|
local function http_server_start_task(server_opts, ap_opts)
|
|||
|
|
-- 等待AP初始化完成
|
|||
|
|
sys.waitUntil("AP_CREATE_OK")
|
|||
|
|
|
|||
|
|
-- 确认SD卡是否挂载成功
|
|||
|
|
local retry_count = 0
|
|||
|
|
local max_retries = 3
|
|||
|
|
|
|||
|
|
while retry_count < max_retries do
|
|||
|
|
local free_space, err = fatfs.getfree("/sd")
|
|||
|
|
if free_space then
|
|||
|
|
log.info("HTTP", "SD卡挂载成功,可用空间: " .. json.encode(free_space))
|
|||
|
|
break
|
|||
|
|
else
|
|||
|
|
retry_count = retry_count + 1
|
|||
|
|
log.warn("HTTP", "SD卡挂载检查失败 (" .. retry_count .. "): " .. (err or "未知错误"))
|
|||
|
|
if retry_count < max_retries then
|
|||
|
|
sys.wait(1000)
|
|||
|
|
else
|
|||
|
|
log.error("HTTP", "SD卡挂载失败,将继续启动但可能无法访问SD卡内容")
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 启动HTTP服务器
|
|||
|
|
httpsrv.start(server_opts.server_port, handle_http_request, socket.LWIP_AP)
|
|||
|
|
|
|||
|
|
log.info("HTTP", "文件服务器已启动")
|
|||
|
|
log.info("HTTP", "请连接WiFi: " .. ap_opts.ap_ssid .. ",密码: " .. ap_opts.ap_pwd)
|
|||
|
|
log.info("HTTP", "然后访问: http://" .. server_opts.server_addr.. ":" .. server_opts.server_port .. "/explorer.html")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
|
|||
|
|
--[[
|
|||
|
|
启动文件管理系统,包括创建AP热点、挂载TF/SD卡和启动SERVER文件管理服务器功能
|
|||
|
|
@api exremotefile.open(ap_opts, sdcard_opts, server_opts)
|
|||
|
|
@table ap_opts 可选,AP配置选项表
|
|||
|
|
@table sdcard_opts 可选,TF/SD卡挂载配置选项表
|
|||
|
|
@table server_opts 可选,服务器配置选项表
|
|||
|
|
@return 无 无返回值
|
|||
|
|
@usage
|
|||
|
|
-- 一、使用默认参数创建server服务器
|
|||
|
|
-- 启动后连接默认AP热点,直接使用日志中默认的地址"http://192.168.4.1:80/explorer.html"来访问文件管理服务器。
|
|||
|
|
exremotefile.open()
|
|||
|
|
|
|||
|
|
|
|||
|
|
-- 二、自定义参数启动
|
|||
|
|
-- 启动后连接自定义AP热点,访问日志中自定义的地址"http://"server_addr":"server_port"/explorer.html"来访问文件管理服务器。
|
|||
|
|
exremotefile.open({
|
|||
|
|
ap_ssid = "LuatOS_FileHub", -- WiFi名称
|
|||
|
|
ap_pwd = "12345678" -- WiFi密码
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
spi_id = 1, -- SPI编号
|
|||
|
|
spi_cs = 12, -- CS片选引脚
|
|||
|
|
is_8000_development_board = false, -- 是否使用8000开发板
|
|||
|
|
is_sdio = false -- 是否使用sdio挂载
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
server_addr = "192.168.4.1", -- 服务器地址
|
|||
|
|
server_port = 80, -- 服务器端口
|
|||
|
|
user_name = "admin", -- 用户名
|
|||
|
|
user_pwd = "123456" -- 密码
|
|||
|
|
})
|
|||
|
|
]]
|
|||
|
|
function exremotefile.open(ap_opts, sdcard_opts, server_opts)
|
|||
|
|
if is_initialized then
|
|||
|
|
log.warn("exremotefile", "文件管理系统已经在运行中")
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("exremotefile", "启动文件管理系统")
|
|||
|
|
|
|||
|
|
-- 合并配置
|
|||
|
|
if ap_opts then
|
|||
|
|
log.info("check_config", "开始检查AP参数")
|
|||
|
|
if not ap_opts.ap_ssid then
|
|||
|
|
ap_opts.ap_ssid = default_ap_opts.ap_ssid
|
|||
|
|
log.info("check_config", "AP没有设置ssid,用默认配置",ap_opts.ap_ssid)
|
|||
|
|
end
|
|||
|
|
if not ap_opts.ap_pwd then
|
|||
|
|
ap_opts.ap_pwd = default_ap_opts.ap_pwd
|
|||
|
|
log.info("check_config", "AP没有设置pwd,用默认配置",ap_opts.ap_pwd)
|
|||
|
|
end
|
|||
|
|
log.info("check_config", "AP参数配置完毕")
|
|||
|
|
else
|
|||
|
|
ap_opts = default_ap_opts
|
|||
|
|
log.info("check_config", "没有AP参数,用默认配置")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
if sdcard_opts then
|
|||
|
|
log.info("check_config", "开始检查TF/SD挂载参数")
|
|||
|
|
if not sdcard_opts.spi_id then
|
|||
|
|
sdcard_opts.spi_id = default_sdcard_opts.spi_id
|
|||
|
|
log.info("check_config", "TF/SD挂载没有设置spi号,用默认配置",sdcard_opts.spi_id)
|
|||
|
|
end
|
|||
|
|
if not sdcard_opts.spi_cs then
|
|||
|
|
sdcard_opts.spi_cs = default_sdcard_opts.spi_cs
|
|||
|
|
log.info("check_config", "TF/SD挂载没有设置cs片选脚,用默认配置",sdcard_opts.spi_cs)
|
|||
|
|
end
|
|||
|
|
log.info("check_config", "TF/SD挂载参数配置完毕")
|
|||
|
|
else
|
|||
|
|
sdcard_opts = default_sdcard_opts
|
|||
|
|
log.info("check_config", "没有TF/SD挂载参数,用默认配置")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
if server_opts then
|
|||
|
|
log.info("check_config", "开始检查SERVER参数")
|
|||
|
|
if not server_opts.server_addr then
|
|||
|
|
server_opts.server_addr = default_server_opts.server_addr
|
|||
|
|
log.info("check_config", "SERVER没有设置addr,用默认配置",server_opts.server_addr)
|
|||
|
|
end
|
|||
|
|
if not server_opts.server_port then
|
|||
|
|
server_opts.server_port = default_server_opts.server_port
|
|||
|
|
log.info("check_config", "SERVER没有设置port,用默认配置",server_opts.server_port)
|
|||
|
|
end
|
|||
|
|
if not server_opts.user_name then
|
|||
|
|
server_opts.user_name = default_server_opts.user_name
|
|||
|
|
log.info("check_config", "SERVER没有设置user_name,用默认配置",server_opts.user_name)
|
|||
|
|
end
|
|||
|
|
if not server_opts.user_pwd then
|
|||
|
|
server_opts.user_pwd = default_server_opts.user_pwd
|
|||
|
|
log.info("check_config", "SERVER没有设置user_pwd,用默认配置",server_opts.user_pwd)
|
|||
|
|
end
|
|||
|
|
log.info("check_config", "SERVER参数配置完毕")
|
|||
|
|
else
|
|||
|
|
server_opts = default_server_opts
|
|||
|
|
log.info("check_config", "没有SERVER参数,用默认配置")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
user_sdcard_opts = sdcard_opts
|
|||
|
|
user_server_opts = server_opts
|
|||
|
|
-- 创建AP热点
|
|||
|
|
create_ap(ap_opts, server_opts)
|
|||
|
|
|
|||
|
|
-- 初始化SD卡
|
|||
|
|
local mount_result = init_sdcard(sdcard_opts)
|
|||
|
|
if not mount_result then
|
|||
|
|
log.error("exremotefile", "SD卡初始化失败")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
-- 启动HTTP服务器
|
|||
|
|
sys.taskInit(http_server_start_task, server_opts, ap_opts)
|
|||
|
|
|
|||
|
|
is_initialized = true
|
|||
|
|
log.info("exremotefile", "文件管理系统启动完成")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
--[[
|
|||
|
|
关闭文件管理系统,包括停止HTTP文件服务器、取消TF/SD卡挂载和停止AP热点
|
|||
|
|
@api exremotefile.close()
|
|||
|
|
@return 无 无返回值
|
|||
|
|
@usage
|
|||
|
|
-- 关闭文件管理系统
|
|||
|
|
-- exremotefile.close()
|
|||
|
|
]]
|
|||
|
|
function exremotefile.close()
|
|||
|
|
if not is_initialized then
|
|||
|
|
log.warn("exremotefile", "文件管理系统尚未启动")
|
|||
|
|
return
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
log.info("exremotefile", "关闭文件管理系统")
|
|||
|
|
|
|||
|
|
-- 停止HTTP服务器
|
|||
|
|
httpsrv.stop(user_server_opts.server_port, nil, socket.LWIP_AP)
|
|||
|
|
-- 取消挂载SD卡
|
|||
|
|
fatfs.unmount("/sd")
|
|||
|
|
-- 停止AP热点
|
|||
|
|
wlan.stopAP()
|
|||
|
|
|
|||
|
|
-- 关闭所用SPI
|
|||
|
|
spi.close(user_sdcard_opts.spi_id)
|
|||
|
|
|
|||
|
|
-- 关闭所用IO
|
|||
|
|
if user_sdcard_opts.is_8000_development_board == true then
|
|||
|
|
gpio.close(ETH3V3_EN)
|
|||
|
|
gpio.close(SPI_ETH_CS)
|
|||
|
|
end
|
|||
|
|
gpio.close(user_sdcard_opts.spi_cs)
|
|||
|
|
|
|||
|
|
is_initialized = false
|
|||
|
|
log.info("exremotefile", "文件管理系统已关闭")
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
return exremotefile
|