--[[ @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 mount_result = fatfs.mount(fatfs.SDIO, "/sd", sdcard_opts.spi_id, sdcard_opts.spi_cs, 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 -- 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 local path = uri:match("path=([^&]+)") or "/luadb" log.info("HTTP", "请求的文件列表路径: " .. path) 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) -- 记录传给页面的文件数据 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" }, [[
正在重定向到文件下载...
]] 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