exremotefile.lua 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604
  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. -- gpio13为8101TF卡的供电控制引脚,在挂载前需要设置为高电平,不能省略
  102. gpio.setup(13, 1)
  103. mount_result = fatfs.mount(fatfs.SDIO, "/sd", 24 * 1000 * 1000)
  104. end
  105. log.info("SDCARD", "挂载SD卡结果:", mount_result)
  106. -- 获取SD卡的可用空间信息
  107. local data, err = fatfs.getfree("/sd")
  108. if data then
  109. log.info("SDCARD", "SD卡可用空间信息:", json.encode(data))
  110. else
  111. log.info("SDCARD", "获取SD卡空间失败:", err)
  112. end
  113. return mount_result
  114. end
  115. -- 会话管理
  116. local authenticated_sessions = {}
  117. -- 获取文件信息
  118. local function get_file_info(path)
  119. log.info("FILE_INFO", "获取文件信息: " .. path)
  120. -- 获取文件名
  121. local filename = path:match("([^/]+)$") or ""
  122. -- 获取大小
  123. local direct_size = io.fileSize(path)
  124. if direct_size and direct_size > 0 then
  125. log.info("FILE_INFO", "获取文件大小成功: " .. direct_size .. " 字节")
  126. return {
  127. name = filename,
  128. size = direct_size,
  129. isDirectory = false,
  130. path = path
  131. }
  132. end
  133. -- 检查文件是否存在,避免对文件进行错误的目录判断
  134. if not io.exists(path) then
  135. log.info("FILE_INFO", "文件不存在: " .. path)
  136. return {
  137. name = filename,
  138. size = 0,
  139. isDirectory = false,
  140. path = path
  141. }
  142. end
  143. -- 尝试判断是否为目录
  144. local ret, data = io.lsdir(path, 1, 0)
  145. if ret and data and type(data) == "table" and #data > 0 then
  146. log.info("FILE_INFO", "路径是一个目录: " .. path)
  147. return {
  148. name = filename,
  149. size = 0,
  150. isDirectory = true,
  151. path = path
  152. }
  153. end
  154. -- 检查文件是否存在
  155. if not io.exists(path) then
  156. log.info("FILE_INFO", "文件不存在: " .. path)
  157. return {
  158. name = filename,
  159. size = 0,
  160. isDirectory = false,
  161. path = path
  162. }
  163. end
  164. -- 尝试打开文件获取大小
  165. local file = io.open(path, "rb")
  166. if file then
  167. -- 尝试获取文件大小
  168. local file_size = io.fileSize(path)
  169. -- 如果返回0或nil,尝试通过读取文件内容获取大小
  170. if not file_size or file_size == 0 then
  171. log.info("FILE_INFO", "获取文件大小,尝试读取文件内容")
  172. local content = file:read("*a")
  173. file_size = #content
  174. log.info("FILE_INFO", "使用文件内容长度获取大小: " .. file_size .. " 字节")
  175. else
  176. log.info("FILE_INFO", "获取文件大小成功: " .. file_size .. " 字节")
  177. end
  178. file:close()
  179. log.info("FILE_INFO", "成功获取文件信息: " .. filename .. ", 大小: " .. file_size .. " 字节")
  180. return {
  181. name = filename,
  182. size = file_size,
  183. isDirectory = false,
  184. path = path
  185. }
  186. end
  187. end
  188. -- 定义系统文件的规则(系统文件不显示)
  189. local function is_system_file(filename)
  190. -- 系统文件扩展名列表
  191. local system_extensions = {".luac", ".html", ".md"}
  192. -- 特殊系统文件名
  193. local special_system_files = {".airm2m_all_crc#.bin"}
  194. -- 检查文件名是否匹配特殊系统文件名
  195. for _, sys_file in ipairs(special_system_files) do
  196. if filename == sys_file then
  197. return true
  198. end
  199. end
  200. -- 检查文件扩展名是否为系统文件扩展名
  201. for _, ext in ipairs(system_extensions) do
  202. if filename:sub(-#ext) == ext then
  203. return true
  204. end
  205. end
  206. return false
  207. end
  208. -- 扫描目录
  209. local function scan_with_lsdir(path, files)
  210. log.info("LIST_DIR", "开始扫描目录")
  211. -- 确保路径格式正确,处理多层目录和编码问题
  212. local scan_path = path
  213. log.info("LIST_DIR", "原始路径: " .. scan_path)
  214. -- 规范化路径,处理URL编码残留问题
  215. scan_path = scan_path:gsub("%%(%x%x)", function(hex)
  216. return string.char(tonumber(hex, 16))
  217. end)
  218. log.info("LIST_DIR", "解码后路径: " .. scan_path)
  219. -- 移除多余的斜杠
  220. scan_path = scan_path:gsub("//+", "/")
  221. log.info("LIST_DIR", "去重斜杠后路径: " .. scan_path)
  222. -- 规范化路径,移除可能的尾部斜杠
  223. scan_path = scan_path:gsub("/*$", "")
  224. log.info("LIST_DIR", "移除尾部斜杠后路径: " .. scan_path)
  225. -- 确保路径以/开头
  226. if not scan_path:match("^/") then
  227. scan_path = "/" .. scan_path
  228. end
  229. log.info("LIST_DIR", "确保以/开头后路径: " .. scan_path)
  230. -- 确保路径以/结尾
  231. scan_path = scan_path .. (scan_path == "" and "" or "/")
  232. log.info("LIST_DIR", "开始扫描路径: " .. scan_path)
  233. -- 扫描目录,最多列出50个文件,从第0个开始
  234. local ret, data = io.lsdir(scan_path, 50, 0)
  235. if ret then
  236. log.info("LIST_DIR", "成功获取目录内容,文件数量: " .. #data)
  237. log.info("LIST_DIR", "目录内容: " .. json.encode(data))
  238. -- 遍历目录内容
  239. for i = 1, #data do
  240. local entry = data[i]
  241. local is_dir = (entry.type ~= 0)
  242. local entry_type = is_dir and "目录" or "文件"
  243. log.info("LIST_DIR", "找到条目: " .. entry.name .. ", 类型: " .. entry_type)
  244. local full_path = scan_path .. entry.name
  245. -- 处理目录和文件的不同逻辑
  246. if is_dir then
  247. -- 对于目录,直接构造信息
  248. local dir_info = {
  249. name = entry.name,
  250. size = 0,
  251. isDirectory = true,
  252. path = full_path
  253. }
  254. -- 过滤sd卡系统文件夹目录
  255. if entry.name ~= "System Volume Information" then
  256. table.insert(files, dir_info)
  257. log.info("LIST_DIR", "添加目录: " .. entry.name .. ", 路径: " .. full_path)
  258. end
  259. else
  260. -- 检查是否为用户文件
  261. local is_user_file = not is_system_file(entry.name)
  262. -- 只有用户文件才会被添加到列表中
  263. if is_user_file then
  264. -- 对于文件,调用get_file_info获取详细信息
  265. local file_info = get_file_info(full_path)
  266. if file_info and file_info.size ~= nil then
  267. file_info.isDirectory = false
  268. table.insert(files, file_info)
  269. log.info("LIST_DIR", "添加文件: " .. entry.name .. ", 大小: " .. file_info.size ..
  270. " 字节, 路径: " .. file_info.path)
  271. else
  272. -- 如果get_file_info失败,使用默认值
  273. local default_info = {
  274. name = entry.name,
  275. size = entry.size or 0,
  276. isDirectory = false,
  277. path = full_path
  278. }
  279. table.insert(files, default_info)
  280. log.info("LIST_DIR", "添加文件(默认信息): " .. entry.name .. ", 大小: " ..
  281. (entry.size or 0) .. " 字节")
  282. end
  283. end
  284. end
  285. end
  286. return true
  287. else
  288. log.info("LIST_DIR", "扫描失败: " .. (data or "未知错误"))
  289. end
  290. return false
  291. end
  292. -- 列出目录
  293. local function list_directory(path)
  294. -- 初始化文件列表
  295. local files = {}
  296. log.info("LIST_DIR", "开始处理目录请求: " .. path)
  297. -- 扫描方法表
  298. local scan_success = scan_with_lsdir(path, files)
  299. -- 记录扫描结果
  300. if scan_success then
  301. log.info("LIST_DIR", "扫描方法成功")
  302. else
  303. log.info("LIST_DIR", "扫描方法失败")
  304. end
  305. log.info("LIST_DIR", "目录扫描完成,总共找到文件数量: " .. #files)
  306. return files
  307. end
  308. -- 会话验证
  309. local function validate_session(headers)
  310. -- 获取Cookie中的session_id
  311. local cookies = headers['Cookie'] or ''
  312. local session_id = nil
  313. if cookies then
  314. session_id = cookies:match('session_id=([^;]+)')
  315. end
  316. -- 检查会话ID是否已认证
  317. if session_id and authenticated_sessions[session_id] then
  318. return true
  319. else
  320. return false
  321. end
  322. end
  323. -- 生成会话ID
  324. local function generate_session_id()
  325. local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
  326. local id = ""
  327. for i = 1, 32 do
  328. local rand = math.random(1, #chars)
  329. id = id .. chars:sub(rand, rand)
  330. end
  331. return id
  332. end
  333. -- 检查字符串是否以指定前缀开头
  334. local function string_starts_with(str, prefix)
  335. return string.sub(str, 1, string.len(prefix)) == prefix
  336. end
  337. -- 解析文件上传数据
  338. local function parse_multipart_data(body, boundary)
  339. log.info("UPLOAD", "开始解析数据,body大小: " .. #body .. " 字节")
  340. local result = {}
  341. local parts = {}
  342. local boundary_pattern = "--" .. boundary
  343. -- 开始解析
  344. if #body > 0 then
  345. log.info("UPLOAD", "使用简化解析方法处理上传数据")
  346. -- 首先尝试从body中提取文件名
  347. local filename_match = string.match(body, 'filename="([^"]+)"')
  348. if filename_match then
  349. result.filename = filename_match
  350. log.info("UPLOAD", "成功提取文件名: " .. filename_match)
  351. end
  352. -- 查找内容开始位置
  353. local content_start = string.find(body, "\r\n\r\n")
  354. if content_start then
  355. -- 提取内容部分
  356. local content = string.sub(body, content_start + 4)
  357. -- 移除末尾可能的boundary
  358. local end_pos = string.find(content, "\r\n--" .. boundary, 1, true)
  359. if end_pos then
  360. content = string.sub(content, 1, end_pos - 1)
  361. end
  362. -- 清理内容
  363. content = string.gsub(content, "\r\n$", "")
  364. content = string.gsub(content, "\n$", "")
  365. if #content > 0 then
  366. result.content = content
  367. result.size = #content
  368. log.info("UPLOAD", "解析成功,获取内容大小: " .. #content .. " 字节")
  369. end
  370. end
  371. end
  372. log.info("UPLOAD", "multipart数据解析完成," .. (result.content and (result.filename and "成功获取文件: " .. result.filename or "成功获取文件内容") or "未找到有效文件内容"))
  373. return result
  374. end
  375. -- 写入文件,支持分包写入
  376. local function write_file_with_chunks(file_path, content)
  377. -- 检查路径前缀
  378. local storage_type = "内存" -- 默认内存存储
  379. if string.sub(file_path, 1, 4) == "/sd/" then
  380. storage_type = "sdcard"
  381. -- 获取SD卡可用空间
  382. local data, err = fatfs.getfree("/sd")
  383. if not data then
  384. log.error("UPLOAD", "SD卡未挂载或不可用")
  385. return false, "SD卡未挂载或不可用"
  386. end
  387. -- 设置为sd卡可用空闲内存空间
  388. local free_space = tonumber(data.free_kb)
  389. -- 如果无法获取有效空间值,设置默认值以跳过空间检查
  390. if not free_space or free_space <= 0 then
  391. log.warn("UPLOAD", "无法获取准确的SD卡可用空间,跳过空间检查")
  392. free_space = #content + 1
  393. end
  394. -- 确保free_space是数字类型
  395. if type(free_space) ~= "number" then
  396. log.error("UPLOAD", "无法获取有效的SD卡空间大小")
  397. -- 这里可以选择跳过空间检查继续执行,或者返回错误
  398. -- 为了避免崩溃,我们跳过空间检查
  399. else
  400. -- 检查SD卡空间是否足够
  401. if free_space < #content then
  402. log.error("UPLOAD", "SD卡空间不足,需要 " .. #content .. " 字节,可用 " .. free_space .. " 字节")
  403. return false, "SD卡空间不足"
  404. end
  405. end
  406. end
  407. log.info("UPLOAD", "开始写入文件到" .. storage_type .. ": " .. file_path)
  408. -- 保留完整路径,不要只提取文件名
  409. -- 只有当路径不是绝对路径(不以/开头)时才需要特殊处理
  410. if not file_path:match("^/") then
  411. -- 如果不是绝对路径,可能需要获取文件名,但保留相对路径
  412. local filename = file_path:match("[^/]+$")
  413. -- 保持原始路径不变,确保写入到正确位置
  414. log.info("UPLOAD", "使用相对路径: " .. file_path)
  415. end
  416. -- 根据目标存储类型调整分块大小
  417. local chunk_size
  418. if storage_type == "sdcard" then
  419. -- SD卡写入使用较大分块以提高性能
  420. chunk_size = 32 * 1024 -- 32KB
  421. else
  422. -- 内存写入使用较小分块以避免内存峰值
  423. chunk_size = 16 * 1024 -- 16KB
  424. end
  425. -- 安全地打开文件进行写入,使用更健壮的错误处理
  426. local file, err
  427. -- 尝试不同的文件打开模式
  428. local modes = {"wb", "w"}
  429. for _, mode in ipairs(modes) do
  430. -- 先尝试删除可能存在的同名文件(忽略错误)
  431. pcall(os.remove, file_path)
  432. file, err = io.open(file_path, mode)
  433. if file then
  434. log.info("UPLOAD", "成功以模式" .. mode .. "打开文件: " .. file_path)
  435. break
  436. else
  437. log.warn("UPLOAD", "无法以模式" .. mode .. "打开文件: " .. file_path .. ", 错误: " .. (err or "未知错误"))
  438. end
  439. end
  440. if not file then
  441. -- 尝试提取原始文件名,确保使用原始名称
  442. local original_filename = file_path:match("([^/]+)$") or "upload_file"
  443. -- 对于内存存储
  444. if storage_type == "内存" then
  445. log.info("UPLOAD", "尝试使用根目录路径: /" .. original_filename)
  446. file, err = io.open("/" .. original_filename, "w")
  447. if file then
  448. file_path = "/" .. original_filename
  449. else
  450. -- 如果web还是不能显示,使用随机临时文件名
  451. -- 先使用原始文件名,不添加时间戳和随机数
  452. local simple_filename = original_filename
  453. log.info("UPLOAD", "尝试使用原始文件名: " .. simple_filename)
  454. file, err = io.open(simple_filename, "w")
  455. if not file then
  456. -- 添加时间戳方式显示文件名,排除因为文件名导致的无法显示
  457. simple_filename = original_filename .. "_" .. os.time()
  458. log.info("UPLOAD", "尝试使用带时间戳的文件名: " .. simple_filename)
  459. file, err = io.open(simple_filename, "w")
  460. file_path = simple_filename
  461. else
  462. file_path = simple_filename
  463. end
  464. end
  465. else
  466. -- SD卡存储时的处理
  467. log.info("UPLOAD", "尝试使用文件名: " .. original_filename)
  468. file, err = io.open(original_filename, "w")
  469. file_path = original_filename
  470. end
  471. end
  472. if not file then
  473. log.error("UPLOAD", "最终无法创建文件: " .. file_path .. ", 错误: " .. (err or "未知错误"))
  474. return false, "无法创建文件: " .. (err or "未知错误")
  475. end
  476. -- 使用分块写入
  477. local total_size = #content
  478. local pos = 1
  479. local chunks_written = 0
  480. -- 优化文件写入过程
  481. while pos <= total_size do
  482. local chunk_end = math.min(pos + chunk_size - 1, total_size)
  483. local chunk = string.sub(content, pos, chunk_end)
  484. local chunk_len = #chunk
  485. local success, write_err = file:write(chunk)
  486. if not success then
  487. file:close()
  488. log.error("UPLOAD", "写入文件失败(块" .. chunks_written .. "): " .. file_path .. ", 错误: " .. (write_err or "未知错误"))
  489. return false, "写入文件失败: " .. (write_err or "未知错误")
  490. end
  491. -- 在SD卡写入时,每写入一个块就刷新缓冲区,避免数据丢失
  492. if storage_type == "sdcard" then
  493. file:flush()
  494. end
  495. chunks_written = chunks_written + 1
  496. pos = pos + chunk_size
  497. end
  498. -- 确保所有数据都写入存储介质
  499. file:flush()
  500. file:close()
  501. -- 验证写入是否成功(尝试读取文件大小)
  502. local file_info = get_file_info(file_path)
  503. if file_info and file_info.size == total_size then
  504. log.info("UPLOAD", "文件写入成功(" .. chunks_written .. "块): " .. file_path .. ", 大小: " .. total_size .. " 字节, 存储类型: " .. storage_type)
  505. return true, nil, file_path -- 返回实际使用的文件路径
  506. else
  507. log.warn("UPLOAD", "文件写入可能不完整: " .. file_path .. ", 期望大小: " .. total_size .. ", 实际大小: " .. (file_info and file_info.size or "未知"))
  508. return true, "文件写入可能不完整", file_path -- 返回实际使用的文件路径
  509. end
  510. end
  511. -- server请求处理
  512. local function handle_http_request(fd, method, uri, headers, body)
  513. log.info("HTTP", method, uri)
  514. -- 登录
  515. if uri == "/login" and method == "POST" then
  516. local data = json.decode(body or "{}")
  517. log.info("LOGIN", "收到登录请求,用户名: " .. (data and data.username or "空"))
  518. if data and data.username == user_server_opts.user_name and data.password == user_server_opts.user_pwd then
  519. local session_id = generate_session_id()
  520. authenticated_sessions[session_id] = os.time()
  521. -- 计算已认证会话数量
  522. local session_count = 0
  523. for _ in pairs(authenticated_sessions) do
  524. session_count = session_count + 1
  525. end
  526. log.info("LOGIN", "登录成功!用户名: " .. data.username)
  527. log.info("LOGIN", "生成SessionID: " .. session_id)
  528. log.info("LOGIN", "当前已认证会话数量: " .. session_count)
  529. -- 设置Cookie
  530. return 200, {
  531. ["Content-Type"] = "application/json",
  532. ["Set-Cookie"] = "session_id=" .. session_id .. "; Path=/; Max-Age=3600"
  533. }, json.encode({
  534. success = true,
  535. session_id = session_id
  536. })
  537. else
  538. return 200, {
  539. ["Content-Type"] = "application/json"
  540. }, json.encode({
  541. success = false,
  542. message = "用户名或密码错误"
  543. })
  544. end
  545. end
  546. -- 登出
  547. if uri == "/logout" and method == "POST" then
  548. local cookie = headers["Cookie"] or ""
  549. for session_id in cookie:gmatch("session_id=([^;]+)") do
  550. authenticated_sessions[session_id] = nil
  551. end
  552. return 200, {
  553. ["Set-Cookie"] = "session_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
  554. }, ""
  555. end
  556. -- 检查认证
  557. if uri == "/check-auth" then
  558. return 200, {
  559. ["Content-Type"] = "application/json"
  560. }, json.encode({
  561. authenticated = validate_session(headers)
  562. })
  563. end
  564. -- 扫描文件接口
  565. if string_starts_with(uri, "/scan-files") then
  566. log.info("SCAN", "收到文件扫描请求")
  567. -- 检查传统认证方式
  568. local is_authenticated = validate_session(headers)
  569. -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
  570. if not is_authenticated then
  571. local url_username = uri:match("username=([^&]+)")
  572. local url_password = uri:match("password=([^&]+)")
  573. if url_username and url_password then
  574. url_username = url_username:gsub("%%(%x%x)", function(hex)
  575. return string.char(tonumber(hex, 16))
  576. end)
  577. url_password = url_password:gsub("%%(%x%x)", function(hex)
  578. return string.char(tonumber(hex, 16))
  579. end)
  580. if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
  581. log.info("AUTH", "扫描请求通过URL参数认证成功")
  582. is_authenticated = true
  583. else
  584. log.info("AUTH", "扫描请求URL参数认证失败: 用户名或密码错误")
  585. end
  586. else
  587. log.info("AUTH", "扫描请求URL中没有找到用户名和密码参数")
  588. end
  589. end
  590. -- 如果认证仍然失败,返回未授权访问
  591. if not is_authenticated then
  592. log.info("HTTP", "未授权访问文件扫描功能")
  593. return 401, {
  594. ["Content-Type"] = "application/json"
  595. }, json.encode({
  596. success = false,
  597. message = "未授权访问"
  598. })
  599. end
  600. -- 执行文件扫描
  601. log.info("SCAN", "开始扫描内部文件系统和TF卡...")
  602. -- 定义要扫描的挂载点,包括SD卡挂载点
  603. local mount_points = {"/", "/luadb/", "/sd/"}
  604. local found_files = {}
  605. -- 对每个挂载点执行扫描
  606. for _, mount_point in ipairs(mount_points) do
  607. log.info("SCAN", "开始扫描挂载点: " .. mount_point)
  608. -- 如果路径不以/结尾,添加/确保路径格式正确
  609. local scan_path = mount_point
  610. if not scan_path:match("/$") then
  611. scan_path = scan_path .. (scan_path == "" and "" or "/")
  612. end
  613. -- 扫描目录
  614. log.info("SCAN", "开始扫描路径: " .. scan_path)
  615. -- 尝试列出目录内容,最多列出50个文件
  616. local ret, data = io.lsdir(scan_path, 50, 0)
  617. if ret then
  618. log.info("SCAN", "成功获取目录内容,文件数量: " .. #data)
  619. log.info("SCAN", "目录内容: " .. json.encode(data))
  620. -- 遍历目录内容
  621. for i = 1, #data do
  622. local entry = data[i]
  623. local full_path = scan_path .. entry.name
  624. -- 如果是文件(type == 0),添加到文件列表
  625. if entry.type == 0 then
  626. local info = get_file_info(full_path)
  627. if info then
  628. table.insert(found_files, {
  629. name = entry.name,
  630. size = info.size,
  631. path = full_path
  632. })
  633. log.info("SCAN", "找到文件: " .. entry.name .. ", 大小: " .. info.size ..
  634. " 字节, 路径: " .. full_path)
  635. else
  636. -- 如果get_file_info失败,使用io.lsdir返回的大小
  637. table.insert(found_files, {
  638. name = entry.name,
  639. size = entry.size or 0,
  640. path = full_path
  641. })
  642. log.info("SCAN", "找到文件: " .. entry.name .. ", 大小: " .. (entry.size or 0) ..
  643. " 字节, 路径: " .. full_path)
  644. end
  645. else
  646. -- 如果是目录,记录但不添加到文件列表
  647. log.info("SCAN", "找到目录: " .. entry.name .. ", 路径: " .. full_path)
  648. end
  649. end
  650. else
  651. log.info("SCAN", "扫描失败: " .. (data or "未知错误"))
  652. end
  653. local list_files = list_directory(mount_point)
  654. if list_files then
  655. for _, file in ipairs(list_files) do
  656. -- 只记录非目录文件
  657. if not file.isDirectory then
  658. -- 确保文件路径正确
  659. local file_path = file.path or (mount_point .. (mount_point == "/" and "" or "/") .. file.name)
  660. -- 检查文件是否已添加
  661. local is_exists = false
  662. for _, f in ipairs(found_files) do
  663. if f.name == file.name and f.path == file_path then
  664. is_exists = true
  665. break
  666. end
  667. end
  668. if not is_exists then
  669. table.insert(found_files, {
  670. name = file.name,
  671. size = file.size,
  672. path = file_path
  673. })
  674. log.info("SCAN",
  675. "从list_directory添加文件: " .. file.name .. ", 大小: " .. file.size ..
  676. " 字节, 路径: " .. file_path)
  677. end
  678. end
  679. end
  680. end
  681. log.info("SCAN", "挂载点扫描完成: " .. mount_point .. ", 找到文件: " .. #found_files .. " 个")
  682. end
  683. -- 扫描完成后,打印详细的文件列表
  684. log.info("SCAN", "文件扫描完成,总共找到文件数量: " .. #found_files)
  685. for i, file in ipairs(found_files) do
  686. log.info("SCAN", "文件[" .. i .. "]: " .. file.name .. ", 大小: " .. file.size .. " 字节, 路径: " ..
  687. file.path)
  688. end
  689. -- 返回扫描结果
  690. return 200, {
  691. ["Content-Type"] = "application/json"
  692. }, json.encode({
  693. success = true,
  694. foundFiles = #found_files,
  695. files = found_files,
  696. message = "文件扫描完成"
  697. })
  698. end
  699. -- 文件列表
  700. if string_starts_with(uri, "/list") then
  701. -- 检查传统认证方式
  702. local is_authenticated = validate_session(headers)
  703. -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
  704. if not is_authenticated then
  705. local url_username = uri:match("username=([^&]+)")
  706. local url_password = uri:match("password=([^&]+)")
  707. if url_username and url_password then
  708. url_username = url_username:gsub("%%(%x%x)", function(hex)
  709. return string.char(tonumber(hex, 16))
  710. end)
  711. url_password = url_password:gsub("%%(%x%x)", function(hex)
  712. return string.char(tonumber(hex, 16))
  713. end)
  714. if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
  715. log.info("AUTH", "通过URL参数认证成功")
  716. is_authenticated = true
  717. else
  718. log.info("AUTH", "URL参数认证失败: 用户名或密码错误")
  719. end
  720. else
  721. log.info("AUTH", "URL中没有找到用户名和密码参数")
  722. end
  723. end
  724. -- 如果认证仍然失败,返回未授权访问
  725. if not is_authenticated then
  726. log.info("HTTP", "未授权访问文件列表")
  727. return 401, {
  728. ["Content-Type"] = "text/plain"
  729. }, "未授权访问"
  730. end
  731. -- 若没有获取到URI中path参数,则默认使用/luadb目录,防止文件无法上传
  732. local path = uri:match("path=([^&]+)") or "/luadb"
  733. -- 确保路径不会被错误识别为空或根路径
  734. if path == "" or path == "/" then
  735. path = "/luadb"
  736. log.info("HTTP", "修正默认路径为: " .. path)
  737. end
  738. log.info("HTTP", "请求的文件列表路径: " .. path)
  739. -- 将%xx格式的十六进制转义序列还原为对应字符
  740. path = path:gsub("%%(%x%x)", function(hex)
  741. return string.char(tonumber(hex, 16))
  742. end)
  743. log.info("HTTP", "解码后的文件列表路径: " .. path)
  744. -- 调用list_directory函数扫描目录
  745. log.info("HTTP", "开始扫描目录")
  746. local files = list_directory(path)
  747. -- 请求根路径时,过滤系统文件
  748. if path == "/" then
  749. log.info("HTTP", "过滤根路径中的系统文件")
  750. local filtered_files = {}
  751. for _, file in ipairs(files) do
  752. -- 过滤.nvm系统文件和系统配置文件
  753. if not file.isDirectory and not file.name:match("%.nvm$") and file.name ~= "plat_config" then
  754. table.insert(filtered_files, file)
  755. end
  756. end
  757. files = filtered_files
  758. -- 如果是/luadb路径请求,添加根目录下的上传文件,并确保过滤系统文件
  759. elseif path == "/luadb" then
  760. log.info("HTTP", "扫描根目录下的上传文件")
  761. local root_files = list_directory("/")
  762. for _, file in ipairs(root_files) do
  763. -- 只添加文件,不添加目录,过滤系统文件
  764. if not file.isDirectory and not file.name:match("%.nvm$") and file.name ~= "plat_config" then
  765. table.insert(files, file)
  766. log.info("HTTP", "添加上传文件: " .. file.name .. ", 大小: " .. file.size)
  767. end
  768. end
  769. end
  770. -- 记录传给页面的文件数据
  771. log.info("HTTP", "准备返回文件列表,数量: " .. #files)
  772. for i, file in ipairs(files) do
  773. log.info("HTTP", "文件[" .. i .. "]: " .. file.name .. ", 大小: " .. file.size)
  774. end
  775. return 200, {
  776. ["Content-Type"] = "application/json"
  777. }, json.encode({
  778. success = true,
  779. files = files
  780. })
  781. end
  782. -- 文件下载
  783. if string_starts_with(uri, "/download") then
  784. log.info("DOWNLOAD", "收到下载请求: " .. uri)
  785. -- 检查传统认证方式
  786. local is_authenticated = validate_session(headers)
  787. -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
  788. if not is_authenticated then
  789. local url_username = uri:match("username=([^&]+)")
  790. local url_password = uri:match("password=([^&]+)")
  791. if url_username and url_password then
  792. url_username = url_username:gsub("%%(%x%x)", function(hex)
  793. return string.char(tonumber(hex, 16))
  794. end)
  795. url_password = url_password:gsub("%%(%x%x)", function(hex)
  796. return string.char(tonumber(hex, 16))
  797. end)
  798. if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
  799. log.info("AUTH", "下载请求通过URL参数认证成功")
  800. is_authenticated = true
  801. else
  802. log.info("AUTH", "下载请求URL参数认证失败: 用户名或密码错误")
  803. end
  804. else
  805. log.info("AUTH", "下载请求URL中没有找到用户名和密码参数")
  806. end
  807. end
  808. -- 如果认证仍然失败,返回未授权访问
  809. if not is_authenticated then
  810. log.info("HTTP", "未授权访问文件下载")
  811. return 401, {
  812. ["Content-Type"] = "text/plain"
  813. }, "未授权访问"
  814. end
  815. -- 获取请求的文件路径
  816. local path = uri:match("path=([^&]+)") or ""
  817. path = path:gsub("%%(%x%x)", function(hex)
  818. return string.char(tonumber(hex, 16))
  819. end)
  820. -- 检查文件是否存在
  821. if not io.exists(path) then
  822. log.info("DOWNLOAD", "文件不存在: " .. path)
  823. return 404, {
  824. ["Content-Type"] = "text/plain"
  825. }, "文件不存在"
  826. end
  827. -- 尝试打开文件以确认可访问性并获取文件信息
  828. local file = io.open(path, "rb")
  829. if not file then
  830. log.info("DOWNLOAD", "文件无法打开: " .. path)
  831. return 404, {
  832. ["Content-Type"] = "text/plain"
  833. }, "文件无法打开"
  834. end
  835. -- 获取文件名
  836. local filename = path:match("([^/]+)$")
  837. -- 获取文件大小
  838. local file_size = io.fileSize(path)
  839. -- 关闭文件
  840. file:close()
  841. log.info("DOWNLOAD", "确认文件信息: " .. filename .. ", 大小: " .. file_size .. " 字节")
  842. -- 使用httpsrv下载,直接重定向URL
  843. -- 如需要下载文件系统中123.mp3,直接重定向到URL:http://192.168.4.1/123.mp3
  844. -- 如果路径以/sd/开头,则保留完整的sd路径
  845. local redirect_url = "/" .. filename
  846. if string_starts_with(path, "/sd/") then
  847. -- 保留完整的sd路径,以便直接访问sd卡文件及其子目录
  848. redirect_url = path
  849. end
  850. log.info("DOWNLOAD", "开始下载文件:" .. redirect_url)
  851. -- 返回HTTP 302重定向响应
  852. return 302, {
  853. ["Location"] = redirect_url,
  854. ["Content-Type"] = "text/html"
  855. }, [[
  856. <html>
  857. <head><title>重定向下载</title></head>
  858. <body>
  859. <p>正在重定向到文件下载...</p>
  860. </body>
  861. </html>
  862. ]]
  863. end
  864. -- 文件上传
  865. if string_starts_with(uri, "/upload") and method == "POST" then
  866. log.info("UPLOAD", "收到文件上传请求")
  867. -- 检查传统认证方式
  868. local is_authenticated = validate_session(headers)
  869. -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
  870. if not is_authenticated then
  871. local url_username = uri:match("username=([^&]+)")
  872. local url_password = uri:match("password=([^&]+)")
  873. if url_username and url_password then
  874. url_username = url_username:gsub("%%(%x%x)", function(hex)
  875. return string.char(tonumber(hex, 16))
  876. end)
  877. url_password = url_password:gsub("%%(%x%x)", function(hex)
  878. return string.char(tonumber(hex, 16))
  879. end)
  880. if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
  881. log.info("AUTH", "上传请求通过URL参数认证成功")
  882. is_authenticated = true
  883. else
  884. log.info("AUTH", "上传请求URL参数认证失败: 用户名或密码错误")
  885. end
  886. else
  887. log.info("AUTH", "上传请求URL中没有找到用户名和密码参数")
  888. end
  889. end
  890. -- 如果认证仍然失败,返回未授权访问
  891. if not is_authenticated then
  892. log.info("HTTP", "未授权访问文件上传")
  893. return 401, {
  894. ["Content-Type"] = "application/json"
  895. }, json.encode({
  896. success = false,
  897. message = "未授权访问"
  898. })
  899. end
  900. -- 获取上传参数
  901. local target_path = uri:match("path=([^&]+)") or "/luadb"
  902. local filename = uri:match("filename=([^&]+)")
  903. -- URL解码
  904. target_path = target_path:gsub("%%(%x%x)", function(hex)
  905. return string.char(tonumber(hex, 16))
  906. end)
  907. if filename then
  908. filename = filename:gsub("%%(%x%x)", function(hex)
  909. return string.char(tonumber(hex, 16))
  910. end)
  911. else
  912. log.error("UPLOAD", "未提供文件名")
  913. return 200, {
  914. ["Content-Type"] = "application/json"
  915. }, json.encode({
  916. success = false,
  917. message = "未提供文件名"
  918. })
  919. end
  920. -- 验证目标路径
  921. if target_path ~= "/luadb" and target_path ~= "/sd" then
  922. log.error("UPLOAD", "无效的上传目标路径: " .. target_path)
  923. return 200, {
  924. ["Content-Type"] = "application/json"
  925. }, json.encode({
  926. success = false,
  927. message = "无效的上传目标路径"
  928. })
  929. end
  930. -- 检查SD卡是否挂载(如果目标是SD卡)
  931. if target_path == "/sd" then
  932. local free_space = fatfs.getfree("/sd")
  933. if not free_space then
  934. log.error("UPLOAD", "SD卡未挂载")
  935. return 200, {
  936. ["Content-Type"] = "application/json"
  937. }, json.encode({
  938. success = false,
  939. message = "SD卡未挂载"
  940. })
  941. end
  942. end
  943. -- 构建完整的文件路径
  944. local file_path = target_path .. "/" .. filename
  945. -- 输出headers的完整内容,帮助诊断问题
  946. if headers then
  947. log.info("UPLOAD", "headers表类型: " .. type(headers))
  948. local headers_str = "{ "
  949. for k, v in pairs(headers) do
  950. headers_str = headers_str .. k .. "=" .. tostring(v) .. ", "
  951. end
  952. headers_str = headers_str .. "}"
  953. log.info("UPLOAD", "所有请求头: " .. headers_str)
  954. else
  955. log.warn("UPLOAD", "headers参数为nil")
  956. end
  957. -- 获取Content-Type头部,尝试多种可能的键名
  958. local content_type = ""
  959. if headers then
  960. -- 尝试标准的Content-Type键
  961. content_type = headers["Content-Type"] or headers["content-type"] or headers["Content-type"] or ""
  962. log.info("UPLOAD", "接收到的Content-Type: '" .. content_type .. "'")
  963. end
  964. -- 采用正则表达式处理各种格式的boundary参数
  965. -- 尝试多种格式的匹配
  966. local boundary = nil
  967. if content_type and content_type ~= "" then
  968. boundary =
  969. -- 匹配不带引号的boundary: boundary=abc123
  970. content_type:match("boundary=([^; ]+)") or
  971. -- 匹配带引号的boundary: boundary="abc123"
  972. content_type:match('boundary="([^"]+)"') or
  973. -- 匹配带单引号的boundary: boundary='abc123'
  974. content_type:match("boundary='([^']+)'")
  975. end
  976. if not boundary then
  977. log.warn("UPLOAD", "Content-Type中未找到boundary,尝试从请求体中提取")
  978. -- 直接从请求体中提取boundary
  979. if body and body ~= "" then
  980. -- 尝试匹配请求体中的第一个boundary行,通常格式为 "--xxxxxxx"
  981. local body_boundary = body:match("^%-%-(.+)")
  982. if body_boundary then
  983. boundary = body_boundary
  984. log.info("UPLOAD", "成功从请求体中提取boundary: ")
  985. else
  986. -- 尝试匹配可能的Content-Type行
  987. local body_content_type = body:match("Content%-Type: multipart/form%-data; boundary=(.+)")
  988. if body_content_type then
  989. boundary = body_content_type
  990. log.info("UPLOAD", "成功从请求体中的Content-Type提取boundary: ")
  991. else
  992. log.error("UPLOAD", "无法解析multipart边界,Content-Type为空,请求体中也未找到")
  993. return 200, {
  994. ["Content-Type"] = "application/json"
  995. }, json.encode({
  996. success = false,
  997. message = "无法解析上传数据格式"
  998. })
  999. end
  1000. end
  1001. else
  1002. log.error("UPLOAD", "请求体为空,无法提取boundary")
  1003. return 200, {
  1004. ["Content-Type"] = "application/json"
  1005. }, json.encode({
  1006. success = false,
  1007. message = "请求体为空,无法解析上传数据"
  1008. })
  1009. end
  1010. end
  1011. log.info("UPLOAD", "成功解析boundary: " .. boundary)
  1012. log.info("UPLOAD", "上传参数: 目标路径=" .. target_path .. ", 文件名=" .. filename .. ", 完整路径=" .. file_path)
  1013. -- 解析multipart数据
  1014. local upload_data = parse_multipart_data(body or "", boundary)
  1015. if not upload_data.content then
  1016. log.error("UPLOAD", "无法解析上传文件数据")
  1017. return 200, {
  1018. ["Content-Type"] = "application/json"
  1019. }, json.encode({
  1020. success = false,
  1021. message = "无法解析上传文件数据"
  1022. })
  1023. end
  1024. -- 检查文件大小(200KB限制)
  1025. if #upload_data.content > 200 * 1024 then
  1026. log.error("UPLOAD", "文件大小超过限制: " .. #upload_data.content .. " 字节")
  1027. return 200, {
  1028. ["Content-Type"] = "application/json"
  1029. }, json.encode({
  1030. success = false,
  1031. message = "文件大小超过200KB限制"
  1032. })
  1033. end
  1034. -- 检查sd容量和内存容量
  1035. local available_space
  1036. if target_path == "/luadb" then
  1037. -- 检查系统内存容量
  1038. local total_mem, used_mem, max_used_mem = rtos.meminfo("sys")
  1039. if total_mem and used_mem then
  1040. local free_mem = total_mem - used_mem
  1041. log.info("UPLOAD", "系统内存信息 - 总内存:", total_mem, "已用:", used_mem, "可用:", free_mem)
  1042. -- 设置可用空闲内存空间
  1043. available_space = free_mem
  1044. else
  1045. log.info("UPLOAD", "获取系统内存信息失败,无法进行上传")
  1046. return 200, {["Content-Type"] = "application/json"},
  1047. json.encode({
  1048. success = false,
  1049. message = "获取系统内存失败,无法进行上传"
  1050. })
  1051. end
  1052. elseif target_path == "/sd" then
  1053. -- 获取SD卡可用空间
  1054. local data, err = fatfs.getfree("/sd")
  1055. if data then
  1056. log.info("UPLOAD", "SD卡可用空间信息:", json.encode(data))
  1057. -- 设置为sd卡可用空闲内存空间
  1058. available_space = tonumber(data.free_kb)
  1059. else
  1060. log.info("UPLOAD", "获取SD卡空间失败:", err)
  1061. available_space = 1024 * 1024 -- 默认1MB
  1062. end
  1063. end
  1064. if available_space and available_space < #upload_data.content * 2 then -- 预留足够的空间
  1065. log.error("UPLOAD", "存储空间不足: 需要 " .. #upload_data.content * 2 .. " 字节, 可用 " .. available_space .. " 字节")
  1066. return 200, {
  1067. ["Content-Type"] = "application/json"
  1068. }, json.encode({
  1069. success = false,
  1070. message = "存储空间不足,需要至少 " .. (#upload_data.content * 2) .. " 字节"
  1071. })
  1072. end
  1073. -- 写入文件,保存原始请求路径
  1074. local original_requested_path = file_path
  1075. local success, err, actual_path = write_file_with_chunks(file_path, upload_data.content)
  1076. if success then
  1077. -- 日志记录
  1078. log.info("UPLOAD", "文件上传成功: " .. filename .. ", 大小: " .. #upload_data.content .. " 字节, 实际保存路径: " .. actual_path)
  1079. -- 上传成功后再次收集垃圾
  1080. collectgarbage()
  1081. -- 生成响应信息
  1082. local message = "文件上传成功"
  1083. if actual_path ~= original_requested_path then
  1084. message = message .. ", 由于目录限制,已保存到: " .. actual_path
  1085. end
  1086. return 200, {
  1087. ["Content-Type"] = "application/json"
  1088. }, json.encode({
  1089. success = true,
  1090. message = message,
  1091. filename = filename,
  1092. size = #upload_data.content,
  1093. path = actual_path,
  1094. original_path = original_requested_path
  1095. })
  1096. else
  1097. log.error("UPLOAD", "文件上传失败: " .. (err or "未知错误"))
  1098. -- 即使失败也尝试收集垃圾
  1099. collectgarbage()
  1100. return 200, {
  1101. ["Content-Type"] = "application/json"
  1102. }, json.encode({
  1103. success = false,
  1104. message = "文件上传失败: " .. (err or "未知错误")
  1105. })
  1106. end
  1107. end
  1108. -- 文件删除
  1109. if string_starts_with(uri, "/delete") and method == "POST" then
  1110. -- 检查传统认证方式
  1111. local is_authenticated = validate_session(headers)
  1112. -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
  1113. if not is_authenticated then
  1114. local url_username = uri:match("username=([^&]+)")
  1115. local url_password = uri:match("password=([^&]+)")
  1116. if url_username and url_password then
  1117. url_username = url_username:gsub("%%(%x%x)", function(hex)
  1118. return string.char(tonumber(hex, 16))
  1119. end)
  1120. url_password = url_password:gsub("%%(%x%x)", function(hex)
  1121. return string.char(tonumber(hex, 16))
  1122. end)
  1123. if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
  1124. log.info("AUTH", "通过URL参数认证成功")
  1125. is_authenticated = true
  1126. else
  1127. log.info("AUTH", "URL参数认证失败: 用户名或密码错误")
  1128. end
  1129. else
  1130. log.info("AUTH", "URL中没有找到用户名和密码参数")
  1131. end
  1132. end
  1133. -- 如果认证仍然失败,返回未授权访问
  1134. if not is_authenticated then
  1135. log.info("HTTP", "未授权访问文件删除")
  1136. return 401, {
  1137. ["Content-Type"] = "application/json"
  1138. }, json.encode({
  1139. success = false,
  1140. message = "未授权访问"
  1141. })
  1142. end
  1143. local path = uri:match("path=([^&]+)") or ""
  1144. path = path:gsub("%%(%x%x)", function(hex)
  1145. return string.char(tonumber(hex, 16))
  1146. end)
  1147. if not io.exists(path) then
  1148. return 200, {
  1149. ["Content-Type"] = "application/json"
  1150. }, json.encode({
  1151. success = false,
  1152. message = "文件不存在"
  1153. })
  1154. end
  1155. -- 尝试删除文件
  1156. local ok, err = os.remove(path)
  1157. if ok then
  1158. return 200, {
  1159. ["Content-Type"] = "application/json"
  1160. }, json.encode({
  1161. success = true,
  1162. message = "文件删除成功"
  1163. })
  1164. else
  1165. return 200, {
  1166. ["Content-Type"] = "application/json"
  1167. }, json.encode({
  1168. success = false,
  1169. message = "删除失败: " .. (err or "未知错误")
  1170. })
  1171. end
  1172. end
  1173. -- 首页
  1174. if uri == "/" then
  1175. local html_file = io.open("/index.html", "r")
  1176. if html_file then
  1177. local content = html_file:read("*a")
  1178. html_file:close()
  1179. return 200, {
  1180. ["Content-Type"] = "text/html"
  1181. }, content
  1182. end
  1183. end
  1184. -- 直接文件路径访问
  1185. -- 检查是否是API路径,如果不是,则尝试作为文件路径访问
  1186. local is_api_path = string_starts_with(uri, "/login") or string_starts_with(uri, "/logout") or
  1187. string_starts_with(uri, "/check-auth") or string_starts_with(uri, "/scan-files") or
  1188. string_starts_with(uri, "/list") or string_starts_with(uri, "/download") or
  1189. string_starts_with(uri, "/delete") or uri == "/"
  1190. if not is_api_path then
  1191. log.info("DIRECT_ACCESS", "尝试直接访问文件: " .. uri)
  1192. -- 确定实际文件路径
  1193. local file_path = uri
  1194. -- 如果路径不是以/sd/开头,则默认在/luadb/目录下查找
  1195. if not string_starts_with(file_path, "/sd/") then
  1196. -- 移除开头的斜杠
  1197. if file_path:sub(1, 1) == "/" then
  1198. file_path = file_path:sub(2)
  1199. end
  1200. -- 添加/luadb/前缀
  1201. file_path = "/luadb/" .. file_path
  1202. end
  1203. log.info("DIRECT_ACCESS", "解析后的实际文件路径: " .. file_path)
  1204. -- 检查文件是否存在
  1205. if not io.exists(file_path) then
  1206. log.info("DIRECT_ACCESS", "文件不存在: " .. file_path)
  1207. return 404, {
  1208. ["Content-Type"] = "text/plain"
  1209. }, "文件不存在"
  1210. end
  1211. -- 尝试打开文件
  1212. local file = io.open(file_path, "rb")
  1213. if not file then
  1214. log.info("DIRECT_ACCESS", "文件无法打开: " .. file_path)
  1215. return 404, {
  1216. ["Content-Type"] = "text/plain"
  1217. }, "文件无法打开"
  1218. end
  1219. -- 获取文件名
  1220. local filename = file_path:match("([^/]+)$")
  1221. -- 读取文件内容
  1222. local content = file:read("*a")
  1223. -- 关闭文件
  1224. file:close()
  1225. log.info("DIRECT_ACCESS", "文件读取完成: " .. filename .. ", 大小: " .. #content .. " 字节")
  1226. -- 设置HTTP头部
  1227. local response_headers = {
  1228. ["Content-Type"] = "application/octet-stream",
  1229. ["Content-Disposition"] = "attachment; filename=\"" .. filename .. "\""
  1230. }
  1231. return 200, response_headers, content
  1232. end
  1233. return 404, {
  1234. ["Content-Type"] = "text/plain"
  1235. }, "页面未找到"
  1236. end
  1237. -- server服务器启动任务
  1238. local function http_server_start_task(server_opts, ap_opts)
  1239. -- 等待AP初始化完成
  1240. sys.waitUntil("AP_CREATE_OK")
  1241. -- 确认SD卡是否挂载成功
  1242. local retry_count = 0
  1243. local max_retries = 3
  1244. while retry_count < max_retries do
  1245. local free_space, err = fatfs.getfree("/sd")
  1246. if free_space then
  1247. log.info("HTTP", "SD卡挂载成功,可用空间: " .. json.encode(free_space))
  1248. break
  1249. else
  1250. retry_count = retry_count + 1
  1251. log.warn("HTTP", "SD卡挂载检查失败 (" .. retry_count .. "): " .. (err or "未知错误"))
  1252. if retry_count < max_retries then
  1253. sys.wait(1000)
  1254. else
  1255. log.error("HTTP", "SD卡挂载失败,将继续启动但可能无法访问SD卡内容")
  1256. end
  1257. end
  1258. end
  1259. -- 启动HTTP服务器
  1260. httpsrv.start(server_opts.server_port, handle_http_request, socket.LWIP_AP)
  1261. log.info("HTTP", "文件服务器已启动")
  1262. log.info("HTTP", "请连接WiFi: " .. ap_opts.ap_ssid .. ",密码: " .. ap_opts.ap_pwd)
  1263. log.info("HTTP", "然后访问: http://" .. server_opts.server_addr.. ":" .. server_opts.server_port .. "/explorer.html")
  1264. end
  1265. --[[
  1266. 启动文件管理系统,包括创建AP热点、挂载TF/SD卡和启动SERVER文件管理服务器功能
  1267. @api exremotefile.open(ap_opts, sdcard_opts, server_opts)
  1268. @table ap_opts 可选,AP配置选项表
  1269. @table sdcard_opts 可选,TF/SD卡挂载配置选项表
  1270. @table server_opts 可选,服务器配置选项表
  1271. @return 无 无返回值
  1272. @usage
  1273. -- 一、使用默认参数创建server服务器
  1274. -- 启动后连接默认AP热点,直接使用日志中默认的地址"http://192.168.4.1:80/explorer.html"来访问文件管理服务器。
  1275. exremotefile.open()
  1276. -- 二、自定义参数启动
  1277. -- 启动后连接自定义AP热点,访问日志中自定义的地址"http://"server_addr":"server_port"/explorer.html"来访问文件管理服务器。
  1278. exremotefile.open({
  1279. ap_ssid = "LuatOS_FileHub", -- WiFi名称
  1280. ap_pwd = "12345678" -- WiFi密码
  1281. },
  1282. {
  1283. spi_id = 1, -- SPI编号
  1284. spi_cs = 12, -- CS片选引脚
  1285. is_8000_development_board = false, -- 是否使用8000开发板
  1286. is_sdio = false -- 是否使用sdio挂载
  1287. },
  1288. {
  1289. server_addr = "192.168.4.1", -- 服务器地址
  1290. server_port = 80, -- 服务器端口
  1291. user_name = "admin", -- 用户名
  1292. user_pwd = "123456" -- 密码
  1293. })
  1294. ]]
  1295. function exremotefile.open(ap_opts, sdcard_opts, server_opts)
  1296. if is_initialized then
  1297. log.warn("exremotefile", "文件管理系统已经在运行中")
  1298. return
  1299. end
  1300. log.info("exremotefile", "启动文件管理系统")
  1301. -- 合并配置
  1302. if ap_opts then
  1303. log.info("check_config", "开始检查AP参数")
  1304. if not ap_opts.ap_ssid then
  1305. ap_opts.ap_ssid = default_ap_opts.ap_ssid
  1306. log.info("check_config", "AP没有设置ssid,用默认配置",ap_opts.ap_ssid)
  1307. end
  1308. if not ap_opts.ap_pwd then
  1309. ap_opts.ap_pwd = default_ap_opts.ap_pwd
  1310. log.info("check_config", "AP没有设置pwd,用默认配置",ap_opts.ap_pwd)
  1311. end
  1312. log.info("check_config", "AP参数配置完毕")
  1313. else
  1314. ap_opts = default_ap_opts
  1315. log.info("check_config", "没有AP参数,用默认配置")
  1316. end
  1317. if sdcard_opts then
  1318. log.info("check_config", "开始检查TF/SD挂载参数")
  1319. if not sdcard_opts.spi_id then
  1320. sdcard_opts.spi_id = default_sdcard_opts.spi_id
  1321. log.info("check_config", "TF/SD挂载没有设置spi号,用默认配置",sdcard_opts.spi_id)
  1322. end
  1323. if not sdcard_opts.spi_cs then
  1324. sdcard_opts.spi_cs = default_sdcard_opts.spi_cs
  1325. log.info("check_config", "TF/SD挂载没有设置cs片选脚,用默认配置",sdcard_opts.spi_cs)
  1326. end
  1327. log.info("check_config", "TF/SD挂载参数配置完毕")
  1328. else
  1329. sdcard_opts = default_sdcard_opts
  1330. log.info("check_config", "没有TF/SD挂载参数,用默认配置")
  1331. end
  1332. if server_opts then
  1333. log.info("check_config", "开始检查SERVER参数")
  1334. if not server_opts.server_addr then
  1335. server_opts.server_addr = default_server_opts.server_addr
  1336. log.info("check_config", "SERVER没有设置addr,用默认配置",server_opts.server_addr)
  1337. end
  1338. if not server_opts.server_port then
  1339. server_opts.server_port = default_server_opts.server_port
  1340. log.info("check_config", "SERVER没有设置port,用默认配置",server_opts.server_port)
  1341. end
  1342. if not server_opts.user_name then
  1343. server_opts.user_name = default_server_opts.user_name
  1344. log.info("check_config", "SERVER没有设置user_name,用默认配置",server_opts.user_name)
  1345. end
  1346. if not server_opts.user_pwd then
  1347. server_opts.user_pwd = default_server_opts.user_pwd
  1348. log.info("check_config", "SERVER没有设置user_pwd,用默认配置",server_opts.user_pwd)
  1349. end
  1350. log.info("check_config", "SERVER参数配置完毕")
  1351. else
  1352. server_opts = default_server_opts
  1353. log.info("check_config", "没有SERVER参数,用默认配置")
  1354. end
  1355. user_sdcard_opts = sdcard_opts
  1356. user_server_opts = server_opts
  1357. -- 创建AP热点
  1358. create_ap(ap_opts, server_opts)
  1359. -- 初始化SD卡
  1360. local mount_result = init_sdcard(sdcard_opts)
  1361. if not mount_result then
  1362. log.error("exremotefile", "SD卡初始化失败")
  1363. end
  1364. -- 启动HTTP服务器
  1365. sys.taskInit(http_server_start_task, server_opts, ap_opts)
  1366. is_initialized = true
  1367. log.info("exremotefile", "文件管理系统启动完成")
  1368. end
  1369. --[[
  1370. 关闭文件管理系统,包括停止HTTP文件服务器、取消TF/SD卡挂载和停止AP热点
  1371. @api exremotefile.close()
  1372. @return 无 无返回值
  1373. @usage
  1374. -- 关闭文件管理系统
  1375. -- exremotefile.close()
  1376. ]]
  1377. function exremotefile.close()
  1378. if not is_initialized then
  1379. log.warn("exremotefile", "文件管理系统尚未启动")
  1380. return
  1381. end
  1382. log.info("exremotefile", "关闭文件管理系统")
  1383. -- 停止HTTP服务器
  1384. httpsrv.stop(user_server_opts.server_port, nil, socket.LWIP_AP)
  1385. -- 取消挂载SD卡
  1386. fatfs.unmount("/sd")
  1387. -- 停止AP热点
  1388. wlan.stopAP()
  1389. -- 关闭所用SPI
  1390. spi.close(user_sdcard_opts.spi_id)
  1391. -- 关闭所用IO
  1392. if user_sdcard_opts.is_8000_development_board == true then
  1393. gpio.close(ETH3V3_EN)
  1394. gpio.close(SPI_ETH_CS)
  1395. end
  1396. gpio.close(user_sdcard_opts.spi_cs)
  1397. is_initialized = false
  1398. log.info("exremotefile", "文件管理系统已关闭")
  1399. end
  1400. return exremotefile