exremotefile.lua 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097
  1. --[[
  2. @module exremotefile
  3. @summary exremotefile 远程文件管理系统扩展库,提供AP热点创建、SD卡挂载、SERVER文件管理服务器等功能,支持文件浏览、上传、下载和删除操作。
  4. @version 1.0
  5. @date 2025.09.10
  6. @author 拓毅恒
  7. @usage
  8. 注:在使用exremotefile 扩展库时,需要将同一目录下的explorer.html文件烧录进模组中,否则无法启动server服务器来创建文件管理系统!!!
  9. 注:如果使用Air8000开发板测试,必须自定义配置is_8000_development_board = true
  10. 因为Air8000开发板上TF和以太网是同一个SPI,使用开发板时必须要将以太网拉高
  11. 如果使用其他硬件,需要根据硬件原理图来决定是否需要此操作
  12. 本文件的对外接口有2个:
  13. 1、exremotefile.open(ap_opts, sdcard_opts, server_opts):启动远程文件管理系统,可配置AP参数、SD卡参数和服务器参数
  14. -- 启动后连接AP热点,直接使用luatools日志中默认的地址"http://192.168.4.1:80/explorer.html"来访问文件管理服务器。
  15. -- 如果使用自定义配置,则需要根据配置中的server_addr和server_port参数来访问文件管理服务器。
  16. 2、exremotefile.close():关闭远程文件管理系统,停止AP热点、卸载SD卡和关闭HTTP服务器
  17. ]]
  18. -- 导入必要的模块
  19. dnsproxy = require("dnsproxy")
  20. dhcpsrv = require("dhcpsrv")
  21. local exremotefile = {}
  22. local is_initialized = false
  23. local user_server_opts = {}
  24. local user_sdcard_opts = {}
  25. local ETH3V3_EN = 140 -- Air8000开发板以太网供电
  26. local SPI_ETH_CS = 12 -- Air8000开发板以太网片选
  27. -- AP默认配置
  28. local default_ap_opts = {
  29. ap_ssid = "LuatOS_FileHub",
  30. ap_pwd = "12345678"
  31. }
  32. -- SPI默认配置
  33. local default_sdcard_opts = {
  34. spi_id = 1,
  35. spi_cs = 20,
  36. is_8000_development_board = false,
  37. is_sdio = false
  38. }
  39. -- server默认配置
  40. local default_server_opts = {
  41. user_name = "admin",
  42. user_pwd = "123456",
  43. server_addr = "192.168.4.1",
  44. server_port = 80
  45. }
  46. -- 保存模块引用,用于后续关闭操作
  47. local modules = {
  48. ap = nil,
  49. http_server = nil
  50. }
  51. -- 创建AP热点
  52. local function create_ap(ap_opts, server_opts)
  53. log.info("WIFI", "创建AP热点: " .. ap_opts.ap_ssid)
  54. log.info("WIFI", "AP密码: " .. ap_opts.ap_pwd)
  55. -- 初始化WiFi
  56. wlan.init()
  57. sys.wait(100)
  58. -- 创建AP
  59. wlan.createAP(ap_opts.ap_ssid, ap_opts.ap_pwd)
  60. -- 配置IP
  61. netdrv.ipv4(socket.LWIP_AP, server_opts.server_addr, "255.255.255.0", "0.0.0.0")
  62. -- 等待网络准备就绪
  63. while netdrv.ready(socket.LWIP_AP) ~= true do
  64. sys.wait(100)
  65. end
  66. -- 设置DNS代理
  67. dnsproxy.setup(socket.LWIP_AP, socket.LWIP_GP)
  68. -- 创建DHCP服务器
  69. dhcpsrv.create({adapter=socket.LWIP_AP})
  70. -- 发布AP创建完成事件
  71. sys.publish("AP_CREATE_OK")
  72. log.info("WIFI", "AP热点创建成功")
  73. end
  74. -- 初始化SD卡
  75. local function init_sdcard(sdcard_opts)
  76. log.info("SDCARD", "开始初始化SD卡")
  77. -- 双重验证,确认使用的是Air8000开发板
  78. if sdcard_opts.is_8000_development_board == true then
  79. if sdcard_opts.spi_cs == 20 then
  80. if sdcard_opts.spi_id == 1 then
  81. -- 注:Air8000开发板上TF和以太网是同一个SPI,使用开发板时必须要将以太网拉高
  82. -- 如果使用其他硬件,需要根据硬件原理图来决定是否需要此操作
  83. -- 配置以太网供电引脚,设置为输出模式,并启用上拉电阻
  84. gpio.setup(ETH3V3_EN, 1, gpio.PULLUP)
  85. -- 配置以太网片选引脚,设置为输出模式,并启用上拉电阻
  86. gpio.setup(SPI_ETH_CS, 1, gpio.PULLUP)
  87. log.info("sdcard_init", "使用的是开发板,开始将以太网拉高")
  88. end
  89. end
  90. end
  91. local mount_result = nil
  92. if not sdcard_opts.is_sdio then
  93. -- 配置SPI,设置spi_id,波特率为400000,用于SD卡初始化
  94. local result = spi.setup(sdcard_opts.spi_id, nil, 0, 0, 8, 400 * 1000)
  95. log.info("sdcard_init", "open spi", result)
  96. -- 配置SD卡片选引脚,设置为输出模式,并启用上拉电阻
  97. gpio.setup(sdcard_opts.spi_cs, 1, gpio.PULLUP)
  98. -- 挂载SD卡到文件系统,指定挂载点为"/sd"
  99. mount_result = fatfs.mount(fatfs.SPI, "/sd", sdcard_opts.spi_id, sdcard_opts.spi_cs, 24 * 1000 * 1000)
  100. else
  101. mount_result = fatfs.mount(fatfs.SDIO, "/sd", 24 * 1000 * 1000)
  102. end
  103. log.info("SDCARD", "挂载SD卡结果:", mount_result)
  104. -- 获取SD卡的可用空间信息
  105. local data, err = fatfs.getfree("/sd")
  106. if data then
  107. log.info("SDCARD", "SD卡可用空间信息:", json.encode(data))
  108. else
  109. log.info("SDCARD", "获取SD卡空间失败:", err)
  110. end
  111. return mount_result
  112. end
  113. -- 会话管理
  114. local authenticated_sessions = {}
  115. -- 获取文件信息
  116. local function get_file_info(path)
  117. log.info("FILE_INFO", "获取文件信息: " .. path)
  118. -- 获取文件名
  119. local filename = path:match("([^/]+)$") or ""
  120. -- 获取大小
  121. local direct_size = io.fileSize(path)
  122. if direct_size and direct_size > 0 then
  123. log.info("FILE_INFO", "获取文件大小成功: " .. direct_size .. " 字节")
  124. return {
  125. name = filename,
  126. size = direct_size,
  127. isDirectory = false,
  128. path = path
  129. }
  130. end
  131. -- 检查文件是否存在,避免对文件进行错误的目录判断
  132. if not io.exists(path) then
  133. log.info("FILE_INFO", "文件不存在: " .. path)
  134. return {
  135. name = filename,
  136. size = 0,
  137. isDirectory = false,
  138. path = path
  139. }
  140. end
  141. -- 尝试判断是否为目录
  142. local ret, data = io.lsdir(path, 1, 0)
  143. if ret and data and type(data) == "table" and #data > 0 then
  144. log.info("FILE_INFO", "路径是一个目录: " .. path)
  145. return {
  146. name = filename,
  147. size = 0,
  148. isDirectory = true,
  149. path = path
  150. }
  151. end
  152. -- 检查文件是否存在
  153. if not io.exists(path) then
  154. log.info("FILE_INFO", "文件不存在: " .. path)
  155. return {
  156. name = filename,
  157. size = 0,
  158. isDirectory = false,
  159. path = path
  160. }
  161. end
  162. -- 尝试打开文件获取大小
  163. local file = io.open(path, "rb")
  164. if file then
  165. -- 尝试获取文件大小
  166. local file_size = io.fileSize(path)
  167. -- 如果返回0或nil,尝试通过读取文件内容获取大小
  168. if not file_size or file_size == 0 then
  169. log.info("FILE_INFO", "获取文件大小,尝试读取文件内容")
  170. local content = file:read("*a")
  171. file_size = #content
  172. log.info("FILE_INFO", "使用文件内容长度获取大小: " .. file_size .. " 字节")
  173. else
  174. log.info("FILE_INFO", "获取文件大小成功: " .. file_size .. " 字节")
  175. end
  176. file:close()
  177. log.info("FILE_INFO", "成功获取文件信息: " .. filename .. ", 大小: " .. file_size .. " 字节")
  178. return {
  179. name = filename,
  180. size = file_size,
  181. isDirectory = false,
  182. path = path
  183. }
  184. end
  185. end
  186. -- 定义系统文件的规则(系统文件不显示)
  187. local function is_system_file(filename)
  188. -- 系统文件扩展名列表
  189. local system_extensions = {".luac", ".html", ".md"}
  190. -- 特殊系统文件名
  191. local special_system_files = {".airm2m_all_crc#.bin"}
  192. -- 检查文件名是否匹配特殊系统文件名
  193. for _, sys_file in ipairs(special_system_files) do
  194. if filename == sys_file then
  195. return true
  196. end
  197. end
  198. -- 检查文件扩展名是否为系统文件扩展名
  199. for _, ext in ipairs(system_extensions) do
  200. if filename:sub(-#ext) == ext then
  201. return true
  202. end
  203. end
  204. return false
  205. end
  206. -- 扫描目录
  207. local function scan_with_lsdir(path, files)
  208. log.info("LIST_DIR", "开始扫描目录")
  209. -- 确保路径格式正确,处理多层目录和编码问题
  210. local scan_path = path
  211. log.info("LIST_DIR", "原始路径: " .. scan_path)
  212. -- 规范化路径,处理URL编码残留问题
  213. scan_path = scan_path:gsub("%%(%x%x)", function(hex)
  214. return string.char(tonumber(hex, 16))
  215. end)
  216. log.info("LIST_DIR", "解码后路径: " .. scan_path)
  217. -- 移除多余的斜杠
  218. scan_path = scan_path:gsub("//+", "/")
  219. log.info("LIST_DIR", "去重斜杠后路径: " .. scan_path)
  220. -- 规范化路径,移除可能的尾部斜杠
  221. scan_path = scan_path:gsub("/*$", "")
  222. log.info("LIST_DIR", "移除尾部斜杠后路径: " .. scan_path)
  223. -- 确保路径以/开头
  224. if not scan_path:match("^/") then
  225. scan_path = "/" .. scan_path
  226. end
  227. log.info("LIST_DIR", "确保以/开头后路径: " .. scan_path)
  228. -- 确保路径以/结尾
  229. scan_path = scan_path .. (scan_path == "" and "" or "/")
  230. log.info("LIST_DIR", "开始扫描路径: " .. scan_path)
  231. -- 扫描目录,最多列出50个文件,从第0个开始
  232. local ret, data = io.lsdir(scan_path, 50, 0)
  233. if ret then
  234. log.info("LIST_DIR", "成功获取目录内容,文件数量: " .. #data)
  235. log.info("LIST_DIR", "目录内容: " .. json.encode(data))
  236. -- 遍历目录内容
  237. for i = 1, #data do
  238. local entry = data[i]
  239. local is_dir = (entry.type ~= 0)
  240. local entry_type = is_dir and "目录" or "文件"
  241. log.info("LIST_DIR", "找到条目: " .. entry.name .. ", 类型: " .. entry_type)
  242. local full_path = scan_path .. entry.name
  243. -- 处理目录和文件的不同逻辑
  244. if is_dir then
  245. -- 对于目录,直接构造信息
  246. local dir_info = {
  247. name = entry.name,
  248. size = 0,
  249. isDirectory = true,
  250. path = full_path
  251. }
  252. -- 过滤sd卡系统文件夹目录
  253. if entry.name ~= "System Volume Information" then
  254. table.insert(files, dir_info)
  255. log.info("LIST_DIR", "添加目录: " .. entry.name .. ", 路径: " .. full_path)
  256. end
  257. else
  258. -- 检查是否为用户文件
  259. local is_user_file = not is_system_file(entry.name)
  260. -- 只有用户文件才会被添加到列表中
  261. if is_user_file then
  262. -- 对于文件,调用get_file_info获取详细信息
  263. local file_info = get_file_info(full_path)
  264. if file_info and file_info.size ~= nil then
  265. file_info.isDirectory = false
  266. table.insert(files, file_info)
  267. log.info("LIST_DIR", "添加文件: " .. entry.name .. ", 大小: " .. file_info.size ..
  268. " 字节, 路径: " .. file_info.path)
  269. else
  270. -- 如果get_file_info失败,使用默认值
  271. local default_info = {
  272. name = entry.name,
  273. size = entry.size or 0,
  274. isDirectory = false,
  275. path = full_path
  276. }
  277. table.insert(files, default_info)
  278. log.info("LIST_DIR", "添加文件(默认信息): " .. entry.name .. ", 大小: " ..
  279. (entry.size or 0) .. " 字节")
  280. end
  281. end
  282. end
  283. end
  284. return true
  285. else
  286. log.info("LIST_DIR", "扫描失败: " .. (data or "未知错误"))
  287. end
  288. return false
  289. end
  290. -- 列出目录
  291. local function list_directory(path)
  292. -- 初始化文件列表
  293. local files = {}
  294. log.info("LIST_DIR", "开始处理目录请求: " .. path)
  295. -- 扫描方法表
  296. local scan_success = scan_with_lsdir(path, files)
  297. -- 记录扫描结果
  298. if scan_success then
  299. log.info("LIST_DIR", "扫描方法成功")
  300. else
  301. log.info("LIST_DIR", "扫描方法失败")
  302. end
  303. log.info("LIST_DIR", "目录扫描完成,总共找到文件数量: " .. #files)
  304. return files
  305. end
  306. -- 会话验证
  307. local function validate_session(headers)
  308. -- 获取Cookie中的session_id
  309. local cookies = headers['Cookie'] or ''
  310. local session_id = nil
  311. if cookies then
  312. session_id = cookies:match('session_id=([^;]+)')
  313. end
  314. -- 检查会话ID是否已认证
  315. if session_id and authenticated_sessions[session_id] then
  316. return true
  317. else
  318. return false
  319. end
  320. end
  321. -- 生成会话ID
  322. local function generate_session_id()
  323. local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
  324. local id = ""
  325. for i = 1, 32 do
  326. local rand = math.random(1, #chars)
  327. id = id .. chars:sub(rand, rand)
  328. end
  329. return id
  330. end
  331. -- 检查字符串是否以指定前缀开头
  332. local function string_starts_with(str, prefix)
  333. return string.sub(str, 1, string.len(prefix)) == prefix
  334. end
  335. -- server请求处理
  336. local function handle_http_request(fd, method, uri, headers, body)
  337. log.info("HTTP", method, uri)
  338. -- 登录
  339. if uri == "/login" and method == "POST" then
  340. local data = json.decode(body or "{}")
  341. log.info("LOGIN", "收到登录请求,用户名: " .. (data and data.username or "空"))
  342. if data and data.username == user_server_opts.user_name and data.password == user_server_opts.user_pwd then
  343. local session_id = generate_session_id()
  344. authenticated_sessions[session_id] = os.time()
  345. -- 计算已认证会话数量
  346. local session_count = 0
  347. for _ in pairs(authenticated_sessions) do
  348. session_count = session_count + 1
  349. end
  350. log.info("LOGIN", "登录成功!用户名: " .. data.username)
  351. log.info("LOGIN", "生成SessionID: " .. session_id)
  352. log.info("LOGIN", "当前已认证会话数量: " .. session_count)
  353. -- 设置Cookie
  354. return 200, {
  355. ["Content-Type"] = "application/json",
  356. ["Set-Cookie"] = "session_id=" .. session_id .. "; Path=/; Max-Age=3600"
  357. }, json.encode({
  358. success = true,
  359. session_id = session_id
  360. })
  361. else
  362. return 200, {
  363. ["Content-Type"] = "application/json"
  364. }, json.encode({
  365. success = false,
  366. message = "用户名或密码错误"
  367. })
  368. end
  369. end
  370. -- 登出
  371. if uri == "/logout" and method == "POST" then
  372. local cookie = headers["Cookie"] or ""
  373. for session_id in cookie:gmatch("session_id=([^;]+)") do
  374. authenticated_sessions[session_id] = nil
  375. end
  376. return 200, {
  377. ["Set-Cookie"] = "session_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
  378. }, ""
  379. end
  380. -- 检查认证
  381. if uri == "/check-auth" then
  382. return 200, {
  383. ["Content-Type"] = "application/json"
  384. }, json.encode({
  385. authenticated = validate_session(headers)
  386. })
  387. end
  388. -- 扫描文件接口
  389. if string_starts_with(uri, "/scan-files") then
  390. log.info("SCAN", "收到文件扫描请求")
  391. -- 检查传统认证方式
  392. local is_authenticated = validate_session(headers)
  393. -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
  394. if not is_authenticated then
  395. local url_username = uri:match("username=([^&]+)")
  396. local url_password = uri:match("password=([^&]+)")
  397. if url_username and url_password then
  398. url_username = url_username:gsub("%%(%x%x)", function(hex)
  399. return string.char(tonumber(hex, 16))
  400. end)
  401. url_password = url_password:gsub("%%(%x%x)", function(hex)
  402. return string.char(tonumber(hex, 16))
  403. end)
  404. if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
  405. log.info("AUTH", "扫描请求通过URL参数认证成功")
  406. is_authenticated = true
  407. else
  408. log.info("AUTH", "扫描请求URL参数认证失败: 用户名或密码错误")
  409. end
  410. else
  411. log.info("AUTH", "扫描请求URL中没有找到用户名和密码参数")
  412. end
  413. end
  414. -- 如果认证仍然失败,返回未授权访问
  415. if not is_authenticated then
  416. log.info("HTTP", "未授权访问文件扫描功能")
  417. return 401, {
  418. ["Content-Type"] = "application/json"
  419. }, json.encode({
  420. success = false,
  421. message = "未授权访问"
  422. })
  423. end
  424. -- 执行文件扫描
  425. log.info("SCAN", "开始扫描内部文件系统和TF卡...")
  426. -- 定义要扫描的挂载点,包括SD卡挂载点
  427. local mount_points = {"/", "/luadb/", "/sd/"}
  428. local found_files = {}
  429. -- 对每个挂载点执行扫描
  430. for _, mount_point in ipairs(mount_points) do
  431. log.info("SCAN", "开始扫描挂载点: " .. mount_point)
  432. -- 如果路径不以/结尾,添加/确保路径格式正确
  433. local scan_path = mount_point
  434. if not scan_path:match("/$") then
  435. scan_path = scan_path .. (scan_path == "" and "" or "/")
  436. end
  437. -- 扫描目录
  438. log.info("SCAN", "开始扫描路径: " .. scan_path)
  439. -- 尝试列出目录内容,最多列出50个文件
  440. local ret, data = io.lsdir(scan_path, 50, 0)
  441. if ret then
  442. log.info("SCAN", "成功获取目录内容,文件数量: " .. #data)
  443. log.info("SCAN", "目录内容: " .. json.encode(data))
  444. -- 遍历目录内容
  445. for i = 1, #data do
  446. local entry = data[i]
  447. local full_path = scan_path .. entry.name
  448. -- 如果是文件(type == 0),添加到文件列表
  449. if entry.type == 0 then
  450. local info = get_file_info(full_path)
  451. if info then
  452. table.insert(found_files, {
  453. name = entry.name,
  454. size = info.size,
  455. path = full_path
  456. })
  457. log.info("SCAN", "找到文件: " .. entry.name .. ", 大小: " .. info.size ..
  458. " 字节, 路径: " .. full_path)
  459. else
  460. -- 如果get_file_info失败,使用io.lsdir返回的大小
  461. table.insert(found_files, {
  462. name = entry.name,
  463. size = entry.size or 0,
  464. path = full_path
  465. })
  466. log.info("SCAN", "找到文件: " .. entry.name .. ", 大小: " .. (entry.size or 0) ..
  467. " 字节, 路径: " .. full_path)
  468. end
  469. else
  470. -- 如果是目录,记录但不添加到文件列表
  471. log.info("SCAN", "找到目录: " .. entry.name .. ", 路径: " .. full_path)
  472. end
  473. end
  474. else
  475. log.info("SCAN", "扫描失败: " .. (data or "未知错误"))
  476. end
  477. local list_files = list_directory(mount_point)
  478. if list_files then
  479. for _, file in ipairs(list_files) do
  480. -- 只记录非目录文件
  481. if not file.isDirectory then
  482. -- 确保文件路径正确
  483. local file_path = file.path or (mount_point .. (mount_point == "/" and "" or "/") .. file.name)
  484. -- 检查文件是否已添加
  485. local is_exists = false
  486. for _, f in ipairs(found_files) do
  487. if f.name == file.name and f.path == file_path then
  488. is_exists = true
  489. break
  490. end
  491. end
  492. if not is_exists then
  493. table.insert(found_files, {
  494. name = file.name,
  495. size = file.size,
  496. path = file_path
  497. })
  498. log.info("SCAN",
  499. "从list_directory添加文件: " .. file.name .. ", 大小: " .. file.size ..
  500. " 字节, 路径: " .. file_path)
  501. end
  502. end
  503. end
  504. end
  505. log.info("SCAN", "挂载点扫描完成: " .. mount_point .. ", 找到文件: " .. #found_files .. " 个")
  506. end
  507. -- 扫描完成后,打印详细的文件列表
  508. log.info("SCAN", "文件扫描完成,总共找到文件数量: " .. #found_files)
  509. for i, file in ipairs(found_files) do
  510. log.info("SCAN", "文件[" .. i .. "]: " .. file.name .. ", 大小: " .. file.size .. " 字节, 路径: " ..
  511. file.path)
  512. end
  513. -- 返回扫描结果
  514. return 200, {
  515. ["Content-Type"] = "application/json"
  516. }, json.encode({
  517. success = true,
  518. foundFiles = #found_files,
  519. files = found_files,
  520. message = "文件扫描完成"
  521. })
  522. end
  523. -- 文件列表
  524. if string_starts_with(uri, "/list") then
  525. -- 检查传统认证方式
  526. local is_authenticated = validate_session(headers)
  527. -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
  528. if not is_authenticated then
  529. local url_username = uri:match("username=([^&]+)")
  530. local url_password = uri:match("password=([^&]+)")
  531. if url_username and url_password then
  532. url_username = url_username:gsub("%%(%x%x)", function(hex)
  533. return string.char(tonumber(hex, 16))
  534. end)
  535. url_password = url_password:gsub("%%(%x%x)", function(hex)
  536. return string.char(tonumber(hex, 16))
  537. end)
  538. if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
  539. log.info("AUTH", "通过URL参数认证成功")
  540. is_authenticated = true
  541. else
  542. log.info("AUTH", "URL参数认证失败: 用户名或密码错误")
  543. end
  544. else
  545. log.info("AUTH", "URL中没有找到用户名和密码参数")
  546. end
  547. end
  548. -- 如果认证仍然失败,返回未授权访问
  549. if not is_authenticated then
  550. log.info("HTTP", "未授权访问文件列表")
  551. return 401, {
  552. ["Content-Type"] = "text/plain"
  553. }, "未授权访问"
  554. end
  555. local path = uri:match("path=([^&]+)") or "/luadb"
  556. log.info("HTTP", "请求的文件列表路径: " .. path)
  557. path = path:gsub("%%(%x%x)", function(hex)
  558. return string.char(tonumber(hex, 16))
  559. end)
  560. log.info("HTTP", "解码后的文件列表路径: " .. path)
  561. -- 调用list_directory函数扫描目录
  562. log.info("HTTP", "开始扫描目录")
  563. local files = list_directory(path)
  564. -- 记录传给页面的文件数据
  565. log.info("HTTP", "准备返回文件列表,数量: " .. #files)
  566. for i, file in ipairs(files) do
  567. log.info("HTTP", "文件[" .. i .. "]: " .. file.name .. ", 大小: " .. file.size)
  568. end
  569. return 200, {
  570. ["Content-Type"] = "application/json"
  571. }, json.encode({
  572. success = true,
  573. files = files
  574. })
  575. end
  576. -- 文件下载
  577. if string_starts_with(uri, "/download") then
  578. log.info("DOWNLOAD", "收到下载请求: " .. uri)
  579. -- 检查传统认证方式
  580. local is_authenticated = validate_session(headers)
  581. -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
  582. if not is_authenticated then
  583. local url_username = uri:match("username=([^&]+)")
  584. local url_password = uri:match("password=([^&]+)")
  585. if url_username and url_password then
  586. url_username = url_username:gsub("%%(%x%x)", function(hex)
  587. return string.char(tonumber(hex, 16))
  588. end)
  589. url_password = url_password:gsub("%%(%x%x)", function(hex)
  590. return string.char(tonumber(hex, 16))
  591. end)
  592. if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
  593. log.info("AUTH", "下载请求通过URL参数认证成功")
  594. is_authenticated = true
  595. else
  596. log.info("AUTH", "下载请求URL参数认证失败: 用户名或密码错误")
  597. end
  598. else
  599. log.info("AUTH", "下载请求URL中没有找到用户名和密码参数")
  600. end
  601. end
  602. -- 如果认证仍然失败,返回未授权访问
  603. if not is_authenticated then
  604. log.info("HTTP", "未授权访问文件下载")
  605. return 401, {
  606. ["Content-Type"] = "text/plain"
  607. }, "未授权访问"
  608. end
  609. -- 获取请求的文件路径
  610. local path = uri:match("path=([^&]+)") or ""
  611. path = path:gsub("%%(%x%x)", function(hex)
  612. return string.char(tonumber(hex, 16))
  613. end)
  614. -- 检查文件是否存在
  615. if not io.exists(path) then
  616. log.info("DOWNLOAD", "文件不存在: " .. path)
  617. return 404, {
  618. ["Content-Type"] = "text/plain"
  619. }, "文件不存在"
  620. end
  621. -- 尝试打开文件以确认可访问性并获取文件信息
  622. local file = io.open(path, "rb")
  623. if not file then
  624. log.info("DOWNLOAD", "文件无法打开: " .. path)
  625. return 404, {
  626. ["Content-Type"] = "text/plain"
  627. }, "文件无法打开"
  628. end
  629. -- 获取文件名
  630. local filename = path:match("([^/]+)$")
  631. -- 获取文件大小
  632. local file_size = io.fileSize(path)
  633. -- 关闭文件
  634. file:close()
  635. log.info("DOWNLOAD", "确认文件信息: " .. filename .. ", 大小: " .. file_size .. " 字节")
  636. -- 使用httpsrv下载,直接重定向URL
  637. -- 如需要下载文件系统中123.mp3,直接重定向到URL:http://192.168.4.1/123.mp3
  638. -- 如果路径以/sd/开头,则保留完整的sd路径
  639. local redirect_url = "/" .. filename
  640. if string_starts_with(path, "/sd/") then
  641. -- 保留完整的sd路径,以便直接访问sd卡文件及其子目录
  642. redirect_url = path
  643. end
  644. log.info("DOWNLOAD", "开始下载文件:" .. redirect_url)
  645. -- 返回HTTP 302重定向响应
  646. return 302, {
  647. ["Location"] = redirect_url,
  648. ["Content-Type"] = "text/html"
  649. }, [[
  650. <html>
  651. <head><title>重定向下载</title></head>
  652. <body>
  653. <p>正在重定向到文件下载...</p>
  654. </body>
  655. </html>
  656. ]]
  657. end
  658. -- 文件删除
  659. if string_starts_with(uri, "/delete") and method == "POST" then
  660. -- 检查传统认证方式
  661. local is_authenticated = validate_session(headers)
  662. -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
  663. if not is_authenticated then
  664. local url_username = uri:match("username=([^&]+)")
  665. local url_password = uri:match("password=([^&]+)")
  666. if url_username and url_password then
  667. url_username = url_username:gsub("%%(%x%x)", function(hex)
  668. return string.char(tonumber(hex, 16))
  669. end)
  670. url_password = url_password:gsub("%%(%x%x)", function(hex)
  671. return string.char(tonumber(hex, 16))
  672. end)
  673. if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
  674. log.info("AUTH", "通过URL参数认证成功")
  675. is_authenticated = true
  676. else
  677. log.info("AUTH", "URL参数认证失败: 用户名或密码错误")
  678. end
  679. else
  680. log.info("AUTH", "URL中没有找到用户名和密码参数")
  681. end
  682. end
  683. -- 如果认证仍然失败,返回未授权访问
  684. if not is_authenticated then
  685. log.info("HTTP", "未授权访问文件删除")
  686. return 401, {
  687. ["Content-Type"] = "application/json"
  688. }, json.encode({
  689. success = false,
  690. message = "未授权访问"
  691. })
  692. end
  693. local path = uri:match("path=([^&]+)") or ""
  694. path = path:gsub("%%(%x%x)", function(hex)
  695. return string.char(tonumber(hex, 16))
  696. end)
  697. if not io.exists(path) then
  698. return 200, {
  699. ["Content-Type"] = "application/json"
  700. }, json.encode({
  701. success = false,
  702. message = "文件不存在"
  703. })
  704. end
  705. -- 尝试删除文件
  706. local ok, err = os.remove(path)
  707. if ok then
  708. return 200, {
  709. ["Content-Type"] = "application/json"
  710. }, json.encode({
  711. success = true,
  712. message = "文件删除成功"
  713. })
  714. else
  715. return 200, {
  716. ["Content-Type"] = "application/json"
  717. }, json.encode({
  718. success = false,
  719. message = "删除失败: " .. (err or "未知错误")
  720. })
  721. end
  722. end
  723. -- 首页
  724. if uri == "/" then
  725. local html_file = io.open("/index.html", "r")
  726. if html_file then
  727. local content = html_file:read("*a")
  728. html_file:close()
  729. return 200, {
  730. ["Content-Type"] = "text/html"
  731. }, content
  732. end
  733. end
  734. -- 直接文件路径访问
  735. -- 检查是否是API路径,如果不是,则尝试作为文件路径访问
  736. local is_api_path = string_starts_with(uri, "/login") or string_starts_with(uri, "/logout") or
  737. string_starts_with(uri, "/check-auth") or string_starts_with(uri, "/scan-files") or
  738. string_starts_with(uri, "/list") or string_starts_with(uri, "/download") or
  739. string_starts_with(uri, "/delete") or uri == "/"
  740. if not is_api_path then
  741. log.info("DIRECT_ACCESS", "尝试直接访问文件: " .. uri)
  742. -- 确定实际文件路径
  743. local file_path = uri
  744. -- 如果路径不是以/sd/开头,则默认在/luadb/目录下查找
  745. if not string_starts_with(file_path, "/sd/") then
  746. -- 移除开头的斜杠
  747. if file_path:sub(1, 1) == "/" then
  748. file_path = file_path:sub(2)
  749. end
  750. -- 添加/luadb/前缀
  751. file_path = "/luadb/" .. file_path
  752. end
  753. log.info("DIRECT_ACCESS", "解析后的实际文件路径: " .. file_path)
  754. -- 检查文件是否存在
  755. if not io.exists(file_path) then
  756. log.info("DIRECT_ACCESS", "文件不存在: " .. file_path)
  757. return 404, {
  758. ["Content-Type"] = "text/plain"
  759. }, "文件不存在"
  760. end
  761. -- 尝试打开文件
  762. local file = io.open(file_path, "rb")
  763. if not file then
  764. log.info("DIRECT_ACCESS", "文件无法打开: " .. file_path)
  765. return 404, {
  766. ["Content-Type"] = "text/plain"
  767. }, "文件无法打开"
  768. end
  769. -- 获取文件名
  770. local filename = file_path:match("([^/]+)$")
  771. -- 读取文件内容
  772. local content = file:read("*a")
  773. -- 关闭文件
  774. file:close()
  775. log.info("DIRECT_ACCESS", "文件读取完成: " .. filename .. ", 大小: " .. #content .. " 字节")
  776. -- 设置HTTP头部
  777. local response_headers = {
  778. ["Content-Type"] = "application/octet-stream",
  779. ["Content-Disposition"] = "attachment; filename=\"" .. filename .. "\""
  780. }
  781. return 200, response_headers, content
  782. end
  783. return 404, {
  784. ["Content-Type"] = "text/plain"
  785. }, "页面未找到"
  786. end
  787. -- server服务器启动任务
  788. local function http_server_start_task(server_opts, ap_opts)
  789. -- 等待AP初始化完成
  790. sys.waitUntil("AP_CREATE_OK")
  791. -- 确认SD卡是否挂载成功
  792. local retry_count = 0
  793. local max_retries = 3
  794. while retry_count < max_retries do
  795. local free_space, err = fatfs.getfree("/sd")
  796. if free_space then
  797. log.info("HTTP", "SD卡挂载成功,可用空间: " .. json.encode(free_space))
  798. break
  799. else
  800. retry_count = retry_count + 1
  801. log.warn("HTTP", "SD卡挂载检查失败 (" .. retry_count .. "): " .. (err or "未知错误"))
  802. if retry_count < max_retries then
  803. sys.wait(1000)
  804. else
  805. log.error("HTTP", "SD卡挂载失败,将继续启动但可能无法访问SD卡内容")
  806. end
  807. end
  808. end
  809. -- 启动HTTP服务器
  810. httpsrv.start(server_opts.server_port, handle_http_request, socket.LWIP_AP)
  811. log.info("HTTP", "文件服务器已启动")
  812. log.info("HTTP", "请连接WiFi: " .. ap_opts.ap_ssid .. ",密码: " .. ap_opts.ap_pwd)
  813. log.info("HTTP", "然后访问: http://" .. server_opts.server_addr.. ":" .. server_opts.server_port .. "/explorer.html")
  814. end
  815. --[[
  816. 启动文件管理系统,包括创建AP热点、挂载TF/SD卡和启动SERVER文件管理服务器功能
  817. @api exremotefile.open(ap_opts, sdcard_opts, server_opts)
  818. @table ap_opts 可选,AP配置选项表
  819. @table sdcard_opts 可选,TF/SD卡挂载配置选项表
  820. @table server_opts 可选,服务器配置选项表
  821. @return 无 无返回值
  822. @usage
  823. -- 一、使用默认参数创建server服务器
  824. -- 启动后连接默认AP热点,直接访问日志中默认的地址"http://192.168.4.1:80/explorer.html"来访问文件管理服务器。
  825. exremotefile.open()
  826. -- 二、自定义参数启动
  827. -- 启动后连接自定义AP热点,访问日志中自定义的地址"http://"server_addr":"server_port"/explorer.html"来访问文件管理服务器。
  828. exremotefile.open({
  829. ap_ssid = "LuatOS_FileHub", -- WiFi名称
  830. ap_pwd = "12345678" -- WiFi密码
  831. },
  832. {
  833. spi_id = 1, -- SPI编号
  834. spi_cs = 12, -- CS片选引脚
  835. is_8000_development_board = false, -- 是否使用8000开发板
  836. is_sdio = false -- 是否使用sdio挂载
  837. },
  838. {
  839. server_addr = "192.168.4.1", -- 服务器地址
  840. server_port = 80, -- 服务器端口
  841. user_name = "admin", -- 用户名
  842. user_pwd = "123456" -- 密码
  843. })
  844. ]]
  845. function exremotefile.open(ap_opts, sdcard_opts, server_opts)
  846. if is_initialized then
  847. log.warn("exremotefile", "文件管理系统已经在运行中")
  848. return
  849. end
  850. log.info("exremotefile", "启动文件管理系统")
  851. -- 合并配置
  852. if ap_opts then
  853. log.info("check_config", "开始检查AP参数")
  854. if not ap_opts.ap_ssid then
  855. ap_opts.ap_ssid = default_ap_opts.ap_ssid
  856. log.info("check_config", "AP没有设置ssid,用默认配置",ap_opts.ap_ssid)
  857. end
  858. if not ap_opts.ap_pwd then
  859. ap_opts.ap_pwd = default_ap_opts.ap_pwd
  860. log.info("check_config", "AP没有设置pwd,用默认配置",ap_opts.ap_pwd)
  861. end
  862. log.info("check_config", "AP参数配置完毕")
  863. else
  864. ap_opts = default_ap_opts
  865. log.info("check_config", "没有AP参数,用默认配置")
  866. end
  867. if sdcard_opts then
  868. log.info("check_config", "开始检查TF/SD挂载参数")
  869. if not sdcard_opts.spi_id then
  870. sdcard_opts.spi_id = default_sdcard_opts.spi_id
  871. log.info("check_config", "TF/SD挂载没有设置spi号,用默认配置",sdcard_opts.spi_id)
  872. end
  873. if not sdcard_opts.spi_cs then
  874. sdcard_opts.spi_cs = default_sdcard_opts.spi_cs
  875. log.info("check_config", "TF/SD挂载没有设置cs片选脚,用默认配置",sdcard_opts.spi_cs)
  876. end
  877. log.info("check_config", "TF/SD挂载参数配置完毕")
  878. else
  879. sdcard_opts = default_sdcard_opts
  880. log.info("check_config", "没有TF/SD挂载参数,用默认配置")
  881. end
  882. if server_opts then
  883. log.info("check_config", "开始检查SERVER参数")
  884. if not server_opts.server_addr then
  885. server_opts.server_addr = default_server_opts.server_addr
  886. log.info("check_config", "SERVER没有设置addr,用默认配置",server_opts.server_addr)
  887. end
  888. if not server_opts.server_port then
  889. server_opts.server_port = default_server_opts.server_port
  890. log.info("check_config", "SERVER没有设置port,用默认配置",server_opts.server_port)
  891. end
  892. if not server_opts.user_name then
  893. server_opts.user_name = default_server_opts.user_name
  894. log.info("check_config", "SERVER没有设置user_name,用默认配置",server_opts.user_name)
  895. end
  896. if not server_opts.user_pwd then
  897. server_opts.user_pwd = default_server_opts.user_pwd
  898. log.info("check_config", "SERVER没有设置user_pwd,用默认配置",server_opts.user_pwd)
  899. end
  900. log.info("check_config", "SERVER参数配置完毕")
  901. else
  902. server_opts = default_server_opts
  903. log.info("check_config", "没有SERVER参数,用默认配置")
  904. end
  905. user_sdcard_opts = sdcard_opts
  906. user_server_opts = server_opts
  907. -- 创建AP热点
  908. create_ap(ap_opts, server_opts)
  909. -- 初始化SD卡
  910. local mount_result = init_sdcard(sdcard_opts)
  911. if not mount_result then
  912. log.error("exremotefile", "SD卡初始化失败")
  913. end
  914. -- 启动HTTP服务器
  915. sys.taskInit(http_server_start_task, server_opts, ap_opts)
  916. is_initialized = true
  917. log.info("exremotefile", "文件管理系统启动完成")
  918. end
  919. --[[
  920. 关闭文件管理系统,包括停止HTTP文件服务器、取消TF/SD卡挂载和停止AP热点
  921. @api exremotefile.close()
  922. @return 无 无返回值
  923. @usage
  924. -- 关闭文件管理系统
  925. -- exremotefile.close()
  926. ]]
  927. function exremotefile.close()
  928. if not is_initialized then
  929. log.warn("exremotefile", "文件管理系统尚未启动")
  930. return
  931. end
  932. log.info("exremotefile", "关闭文件管理系统")
  933. -- 停止HTTP服务器
  934. httpsrv.stop(user_server_opts.server_port, nil, socket.LWIP_AP)
  935. -- 取消挂载SD卡
  936. fatfs.unmount("/sd")
  937. -- 停止AP热点
  938. wlan.stopAP()
  939. -- 关闭所用SPI
  940. spi.close(user_sdcard_opts.spi_id)
  941. -- 关闭所用IO
  942. if user_sdcard_opts.is_8000_development_board == true then
  943. gpio.close(ETH3V3_EN)
  944. gpio.close(SPI_ETH_CS)
  945. end
  946. gpio.close(user_sdcard_opts.spi_cs)
  947. is_initialized = false
  948. log.info("exremotefile", "文件管理系统已关闭")
  949. end
  950. return exremotefile