Просмотр исходного кода

add: exremotefile扩展库新增上传功能

tuoyiheng 2 месяцев назад
Родитель
Сommit
1484edda41
2 измененных файлов с 626 добавлено и 2 удалено
  1. 120 1
      script/libs/explorer.html
  2. 506 1
      script/libs/exremotefile.lua

+ 120 - 1
script/libs/explorer.html

@@ -148,6 +148,11 @@
             <div class="header">
                 <h1>LuatOS 文件管理系统</h1>
                 <div>
+                    <label for="uploadTarget" style="margin-right: 10px; color: white;">上传目标:</label>
+                    <select id="uploadTarget" style="margin-right: 10px; padding: 5px;">
+                        <option value="/luadb">内存</option>
+                        <option value="/sd">SD卡</option>
+                    </select>
                     <button onclick="scanFiles()" style="margin-right: 10px;">扫描文件</button>
                     <button onclick="logout()">退出登录</button>
                 </div>
@@ -158,6 +163,22 @@
                 <span> | </span>
                 <a onclick="navigateTo('/sd')">TF/SD目录</a>
             </div>
+            
+            <!-- 文件上传区域 -->
+            <div style="margin-bottom: 20px; padding: 15px; background-color: #f9f9f9; border-radius: 5px;">
+                <div class="form-group">
+                    <label for="fileUpload">选择文件 (最大200KB):</label>
+                    <input type="file" id="fileUpload" accept="*/*">
+                </div>
+                <button onclick="uploadFile()" id="uploadBtn">上传文件</button>
+                <div id="uploadError" class="error hidden"></div>
+                <div id="uploadProgress" style="display: none; margin-top: 10px;">
+                    <div style="width: 100%; background-color: #ddd; border-radius: 4px;">
+                        <div id="progressBar" style="width: 0%; height: 20px; background-color: #4CAF50; border-radius: 4px; transition: width 0.3s;"></div>
+                    </div>
+                    <div id="progressText" style="text-align: center; margin-top: 5px;">0%</div>
+                </div>
+            </div>
 
             <table class="file-list">
                 <thead>
@@ -450,7 +471,105 @@
             return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
         }
 
-        // 格式化日期函数已移除
+        // 文件上传函数
+        function uploadFile() {
+            const fileInput = document.getElementById('fileUpload');
+            const uploadTarget = document.getElementById('uploadTarget').value;
+            const uploadBtn = document.getElementById('uploadBtn');
+            const uploadError = document.getElementById('uploadError');
+            const uploadProgress = document.getElementById('uploadProgress');
+            const progressBar = document.getElementById('progressBar');
+            const progressText = document.getElementById('progressText');
+
+            // 重置错误信息
+            uploadError.textContent = '';
+            uploadError.classList.add('hidden');
+
+            // 检查是否选择了文件
+            if (!fileInput.files || fileInput.files.length === 0) {
+                uploadError.textContent = '请先选择要上传的文件';
+                uploadError.classList.remove('hidden');
+                return;
+            }
+
+            const file = fileInput.files[0];
+            
+            // 检查文件大小(200KB限制)
+            if (file.size > 200 * 1024) {
+                uploadError.textContent = '文件大小超过200KB限制';
+                uploadError.classList.remove('hidden');
+                return;
+            }
+
+            // 获取用户名和密码用于URL参数认证
+            const username = document.getElementById('username')?.value || 'admin';
+            const password = document.getElementById('password')?.value || '123456';
+
+            // 构建上传URL
+            const url = '/upload?path=' + encodeURIComponent(uploadTarget) +
+                        '&filename=' + encodeURIComponent(file.name) +
+                        '&username=' + encodeURIComponent(username) +
+                        '&password=' + encodeURIComponent(password);
+
+            // 显示进度条
+            uploadProgress.style.display = 'block';
+            progressBar.style.width = '0%';
+            progressText.textContent = '0%';
+            uploadBtn.disabled = true;
+
+            // 创建FormData对象
+            const formData = new FormData();
+            formData.append('file', file);
+
+            // 使用XMLHttpRequest以便监控上传进度
+            const xhr = new XMLHttpRequest();
+
+            // 监听上传进度
+            xhr.upload.addEventListener('progress', function(event) {
+                if (event.lengthComputable) {
+                    const percentComplete = Math.round((event.loaded / event.total) * 100);
+                    progressBar.style.width = percentComplete + '%';
+                    progressText.textContent = percentComplete + '%';
+                }
+            });
+
+            // 监听上传完成
+            xhr.addEventListener('load', function() {
+                if (xhr.status === 200) {
+                    try {
+                        const response = JSON.parse(xhr.responseText);
+                        if (response.success) {
+                            alert('文件上传成功!');
+                            // 重新加载当前路径的文件列表
+                            loadFiles(uploadTarget);
+                            // 重置文件选择
+                            fileInput.value = '';
+                        } else {
+                            uploadError.textContent = '上传失败: ' + (response.message || '未知错误');
+                            uploadError.classList.remove('hidden');
+                        }
+                    } catch (e) {
+                        uploadError.textContent = '上传成功但解析响应失败';
+                        uploadError.classList.remove('hidden');
+                    }
+                } else {
+                    uploadError.textContent = '上传失败: 服务器响应错误';
+                    uploadError.classList.remove('hidden');
+                }
+                uploadBtn.disabled = false;
+            });
+
+            // 监听上传错误
+            xhr.addEventListener('error', function() {
+                uploadError.textContent = '上传失败: 网络错误';
+                uploadError.classList.remove('hidden');
+                uploadBtn.disabled = false;
+            });
+
+            // 发送请求
+            xhr.open('POST', url);
+            xhr.send(formData);
+        }
 
         // 启动后检查认证状态
         window.onload = function() {

+ 506 - 1
script/libs/exremotefile.lua

@@ -393,6 +393,210 @@ local function string_starts_with(str, prefix)
     return string.sub(str, 1, string.len(prefix)) == prefix
 end
 
+-- 解析文件上传数据
+local function parse_multipart_data(body, boundary)
+    log.info("UPLOAD", "开始解析数据,body大小: " .. #body .. " 字节")
+    
+    local result = {}
+    local parts = {}
+    local boundary_pattern = "--" .. boundary
+    
+    -- 开始解析
+    if #body > 0 then
+        log.info("UPLOAD", "使用简化解析方法处理上传数据")
+        
+        -- 首先尝试从body中提取文件名
+        local filename_match = string.match(body, 'filename="([^"]+)"')
+        if filename_match then
+            result.filename = filename_match
+            log.info("UPLOAD", "成功提取文件名: " .. filename_match)
+        end
+        
+        -- 查找内容开始位置
+        local content_start = string.find(body, "\r\n\r\n")
+        if content_start then
+            -- 提取内容部分
+            local content = string.sub(body, content_start + 4)
+            
+            -- 移除末尾可能的boundary
+            local end_pos = string.find(content, "\r\n--" .. boundary, 1, true)
+            if end_pos then
+                content = string.sub(content, 1, end_pos - 1)
+            end
+            
+            -- 清理内容
+            content = string.gsub(content, "\r\n$", "")
+            content = string.gsub(content, "\n$", "")
+            
+            if #content > 0 then
+                result.content = content
+                result.size = #content
+                log.info("UPLOAD", "解析成功,获取内容大小: " .. #content .. " 字节")
+            end
+        end
+    end
+    
+    log.info("UPLOAD", "multipart数据解析完成," .. (result.content and (result.filename and "成功获取文件: " .. result.filename or "成功获取文件内容") or "未找到有效文件内容"))
+    return result
+end
+
+-- 写入文件,支持分包写入
+local function write_file_with_chunks(file_path, content)
+    -- 检查路径前缀
+    local storage_type = "内存"  -- 默认内存存储
+    if string.sub(file_path, 1, 4) == "/sd/" then
+        storage_type = "sdcard"
+        -- 获取SD卡可用空间
+        local data, err = fatfs.getfree("/sd")
+        if not data then
+            log.error("UPLOAD", "SD卡未挂载或不可用")
+            return false, "SD卡未挂载或不可用"
+        end
+        
+        -- 设置为sd卡可用空闲内存空间
+        local free_space = tonumber(data.free_kb)
+        
+        -- 如果无法获取有效空间值,设置默认值以跳过空间检查
+        if not free_space or free_space <= 0 then
+            log.warn("UPLOAD", "无法获取准确的SD卡可用空间,跳过空间检查")
+            free_space = #content + 1
+        end
+        
+        -- 确保free_space是数字类型
+        if type(free_space) ~= "number" then
+            log.error("UPLOAD", "无法获取有效的SD卡空间大小")
+            -- 这里可以选择跳过空间检查继续执行,或者返回错误
+            -- 为了避免崩溃,我们跳过空间检查
+        else
+            -- 检查SD卡空间是否足够
+            if free_space < #content then
+                log.error("UPLOAD", "SD卡空间不足,需要 " .. #content .. " 字节,可用 " .. free_space .. " 字节")
+                return false, "SD卡空间不足"
+            end
+        end
+    end
+    
+    log.info("UPLOAD", "开始写入文件到" .. storage_type .. ": " .. file_path)
+
+    -- 保留完整路径,不要只提取文件名
+    -- 只有当路径不是绝对路径(不以/开头)时才需要特殊处理
+    if not file_path:match("^/") then
+        -- 如果不是绝对路径,可能需要获取文件名,但保留相对路径
+        local filename = file_path:match("[^/]+$")
+        -- 保持原始路径不变,确保写入到正确位置
+        log.info("UPLOAD", "使用相对路径: " .. file_path)
+    end
+    
+    -- 根据目标存储类型调整分块大小
+    local chunk_size
+    if storage_type == "sdcard" then
+        -- SD卡写入使用较大分块以提高性能
+        chunk_size = 32 * 1024  -- 32KB
+    else
+        -- 内存写入使用较小分块以避免内存峰值
+        chunk_size = 16 * 1024  -- 16KB
+    end
+    
+    -- 安全地打开文件进行写入,使用更健壮的错误处理
+    local file, err
+    
+    -- 尝试不同的文件打开模式
+    local modes = {"wb", "w"}
+    
+    for _, mode in ipairs(modes) do
+        -- 先尝试删除可能存在的同名文件(忽略错误)
+        pcall(os.remove, file_path)
+        
+        file, err = io.open(file_path, mode)
+        if file then
+            log.info("UPLOAD", "成功以模式" .. mode .. "打开文件: " .. file_path)
+            break
+        else
+            log.warn("UPLOAD", "无法以模式" .. mode .. "打开文件: " .. file_path .. ", 错误: " .. (err or "未知错误"))
+        end
+    end
+    
+    if not file then
+        -- 尝试提取原始文件名,确保使用原始名称
+        local original_filename = file_path:match("([^/]+)$") or "upload_file"
+        
+        -- 对于内存存储
+        if storage_type == "内存" then
+            log.info("UPLOAD", "尝试使用根目录路径: /" .. original_filename)
+            file, err = io.open("/" .. original_filename, "w")
+            if file then
+                file_path = "/" .. original_filename
+            else
+                -- 如果web还是不能显示,使用随机临时文件名
+                -- 先使用原始文件名,不添加时间戳和随机数
+                local simple_filename = original_filename
+                log.info("UPLOAD", "尝试使用原始文件名: " .. simple_filename)
+                file, err = io.open(simple_filename, "w")
+                if not file then
+                    -- 添加时间戳方式显示文件名,排除因为文件名导致的无法显示
+                    simple_filename = original_filename .. "_" .. os.time()
+                    log.info("UPLOAD", "尝试使用带时间戳的文件名: " .. simple_filename)
+                    file, err = io.open(simple_filename, "w")
+                    file_path = simple_filename
+                else
+                    file_path = simple_filename
+                end
+            end
+        else
+            -- SD卡存储时的处理
+            log.info("UPLOAD", "尝试使用文件名: " .. original_filename)
+            file, err = io.open(original_filename, "w")
+            file_path = original_filename
+        end
+    end
+    
+    if not file then
+        log.error("UPLOAD", "最终无法创建文件: " .. file_path .. ", 错误: " .. (err or "未知错误"))
+        return false, "无法创建文件: " .. (err or "未知错误")
+    end
+    
+    -- 使用分块写入
+    local total_size = #content
+    local pos = 1
+    local chunks_written = 0
+    
+    -- 优化文件写入过程
+    while pos <= total_size do
+        local chunk_end = math.min(pos + chunk_size - 1, total_size)
+        local chunk = string.sub(content, pos, chunk_end)
+        local chunk_len = #chunk
+        
+        local success, write_err = file:write(chunk)
+        if not success then
+            file:close()
+            log.error("UPLOAD", "写入文件失败(块" .. chunks_written .. "): " .. file_path .. ", 错误: " .. (write_err or "未知错误"))
+            return false, "写入文件失败: " .. (write_err or "未知错误")
+        end
+        
+        -- 在SD卡写入时,每写入一个块就刷新缓冲区,避免数据丢失
+        if storage_type == "sdcard" then
+            file:flush()
+        end
+        
+        chunks_written = chunks_written + 1
+        pos = pos + chunk_size
+    end
+    
+    -- 确保所有数据都写入存储介质
+    file:flush()
+    file:close()
+    
+    -- 验证写入是否成功(尝试读取文件大小)
+    local file_info = get_file_info(file_path)
+    if file_info and file_info.size == total_size then
+        log.info("UPLOAD", "文件写入成功(" .. chunks_written .. "块): " .. file_path .. ", 大小: " .. total_size .. " 字节, 存储类型: " .. storage_type)
+        return true, nil, file_path  -- 返回实际使用的文件路径
+    else
+        log.warn("UPLOAD", "文件写入可能不完整: " .. file_path .. ", 期望大小: " .. total_size .. ", 实际大小: " .. (file_info and file_info.size or "未知"))
+        return true, "文件写入可能不完整", file_path  -- 返回实际使用的文件路径
+    end
+end
+
 -- server请求处理
 local function handle_http_request(fd, method, uri, headers, body)
     log.info("HTTP", method, uri)
@@ -640,8 +844,15 @@ local function handle_http_request(fd, method, uri, headers, body)
                 ["Content-Type"] = "text/plain"
             }, "未授权访问"
         end
+        -- 若没有获取到URI中path参数,则默认使用/luadb目录,防止文件无法上传
         local path = uri:match("path=([^&]+)") or "/luadb"
+        -- 确保路径不会被错误识别为空或根路径
+        if path == "" or path == "/" then
+            path = "/luadb"
+            log.info("HTTP", "修正默认路径为: " .. path)
+        end
         log.info("HTTP", "请求的文件列表路径: " .. path)
+        -- 将%xx格式的十六进制转义序列还原为对应字符
         path = path:gsub("%%(%x%x)", function(hex)
             return string.char(tonumber(hex, 16))
         end)
@@ -650,6 +861,30 @@ local function handle_http_request(fd, method, uri, headers, body)
         -- 调用list_directory函数扫描目录
         log.info("HTTP", "开始扫描目录")
         local files = list_directory(path)
+        
+        -- 请求根路径时,过滤系统文件
+        if path == "/" then
+            log.info("HTTP", "过滤根路径中的系统文件")
+            local filtered_files = {}
+            for _, file in ipairs(files) do
+                -- 过滤.nvm系统文件和系统配置文件
+                if not file.isDirectory and not file.name:match("%.nvm$") and file.name ~= "plat_config" then
+                    table.insert(filtered_files, file)
+                end
+            end
+            files = filtered_files
+        -- 如果是/luadb路径请求,添加根目录下的上传文件,并确保过滤系统文件
+        elseif path == "/luadb" then
+            log.info("HTTP", "扫描根目录下的上传文件")
+            local root_files = list_directory("/")
+            for _, file in ipairs(root_files) do
+                -- 只添加文件,不添加目录,过滤系统文件
+                if not file.isDirectory and not file.name:match("%.nvm$") and file.name ~= "plat_config" then
+                    table.insert(files, file)
+                    log.info("HTTP", "添加上传文件: " .. file.name .. ", 大小: " .. file.size)
+                end
+            end
+        end
 
         -- 记录传给页面的文件数据
         log.info("HTTP", "准备返回文件列表,数量: " .. #files)
@@ -761,6 +996,276 @@ local function handle_http_request(fd, method, uri, headers, body)
         ]]
     end
 
+    -- 文件上传
+    if string_starts_with(uri, "/upload") and method == "POST" then
+        log.info("UPLOAD", "收到文件上传请求")
+        
+        -- 检查传统认证方式
+        local is_authenticated = validate_session(headers)
+        
+        -- 如果传统认证失败,尝试从URL参数中获取用户名和密码
+        if not is_authenticated then
+            local url_username = uri:match("username=([^&]+)")
+            local url_password = uri:match("password=([^&]+)")
+            if url_username and url_password then
+                url_username = url_username:gsub("%%(%x%x)", function(hex)
+                    return string.char(tonumber(hex, 16))
+                end)
+                url_password = url_password:gsub("%%(%x%x)", function(hex)
+                    return string.char(tonumber(hex, 16))
+                end)
+                if url_username == user_server_opts.user_name and url_password == user_server_opts.user_pwd then
+                    log.info("AUTH", "上传请求通过URL参数认证成功")
+                    is_authenticated = true
+                else
+                    log.info("AUTH", "上传请求URL参数认证失败: 用户名或密码错误")
+                end
+            else
+                log.info("AUTH", "上传请求URL中没有找到用户名和密码参数")
+            end
+        end
+        
+        -- 如果认证仍然失败,返回未授权访问
+        if not is_authenticated then
+            log.info("HTTP", "未授权访问文件上传")
+            return 401, {
+                ["Content-Type"] = "application/json"
+            }, json.encode({
+                success = false,
+                message = "未授权访问"
+            })
+        end
+        
+        -- 获取上传参数
+        local target_path = uri:match("path=([^&]+)") or "/luadb"
+        local filename = uri:match("filename=([^&]+)")
+        
+        -- URL解码
+        target_path = target_path:gsub("%%(%x%x)", function(hex)
+            return string.char(tonumber(hex, 16))
+        end)
+        
+        if filename then
+            filename = filename:gsub("%%(%x%x)", function(hex)
+                return string.char(tonumber(hex, 16))
+            end)
+        else
+            log.error("UPLOAD", "未提供文件名")
+            return 200, {
+                ["Content-Type"] = "application/json"
+            }, json.encode({
+                success = false,
+                message = "未提供文件名"
+            })
+        end
+        
+        -- 验证目标路径
+        if target_path ~= "/luadb" and target_path ~= "/sd" then
+            log.error("UPLOAD", "无效的上传目标路径: " .. target_path)
+            return 200, {
+                ["Content-Type"] = "application/json"
+            }, json.encode({
+                success = false,
+                message = "无效的上传目标路径"
+            })
+        end
+        
+        -- 检查SD卡是否挂载(如果目标是SD卡)
+        if target_path == "/sd" then
+            local free_space = fatfs.getfree("/sd")
+            if not free_space then
+                log.error("UPLOAD", "SD卡未挂载")
+                return 200, {
+                    ["Content-Type"] = "application/json"
+                }, json.encode({
+                    success = false,
+                    message = "SD卡未挂载"
+                })
+            end
+        end
+        
+        -- 构建完整的文件路径
+        local file_path = target_path .. "/" .. filename
+        
+        -- 输出headers的完整内容,帮助诊断问题
+        if headers then
+            log.info("UPLOAD", "headers表类型: " .. type(headers))
+            local headers_str = "{ "
+            for k, v in pairs(headers) do
+                headers_str = headers_str .. k .. "=" .. tostring(v) .. ", "
+            end
+            headers_str = headers_str .. "}"
+            log.info("UPLOAD", "所有请求头: " .. headers_str)
+        else
+            log.warn("UPLOAD", "headers参数为nil")
+        end
+        
+        -- 获取Content-Type头部,尝试多种可能的键名
+        local content_type = ""
+        if headers then
+            -- 尝试标准的Content-Type键
+            content_type = headers["Content-Type"] or headers["content-type"] or headers["Content-type"] or ""
+            log.info("UPLOAD", "接收到的Content-Type: '" .. content_type .. "'")
+        end
+        
+        -- 采用正则表达式处理各种格式的boundary参数
+        -- 尝试多种格式的匹配
+        local boundary = nil
+        if content_type and content_type ~= "" then
+            boundary = 
+                -- 匹配不带引号的boundary: boundary=abc123
+                content_type:match("boundary=([^; ]+)") or
+                -- 匹配带引号的boundary: boundary="abc123"
+                content_type:match('boundary="([^"]+)"') or
+                -- 匹配带单引号的boundary: boundary='abc123'
+                content_type:match("boundary='([^']+)'")
+        end
+        
+        if not boundary then
+            log.warn("UPLOAD", "Content-Type中未找到boundary,尝试从请求体中提取")
+            
+            -- 直接从请求体中提取boundary
+            if body and body ~= "" then
+                -- 尝试匹配请求体中的第一个boundary行,通常格式为 "--xxxxxxx"
+                local body_boundary = body:match("^%-%-(.+)")
+                if body_boundary then
+                    boundary = body_boundary
+                    log.info("UPLOAD", "成功从请求体中提取boundary: ")
+                else
+                    -- 尝试匹配可能的Content-Type行
+                    local body_content_type = body:match("Content%-Type: multipart/form%-data; boundary=(.+)")
+                    if body_content_type then
+                        boundary = body_content_type
+                        log.info("UPLOAD", "成功从请求体中的Content-Type提取boundary: ")
+                    else
+                        log.error("UPLOAD", "无法解析multipart边界,Content-Type为空,请求体中也未找到")
+                        return 200, {
+                            ["Content-Type"] = "application/json"
+                        }, json.encode({
+                            success = false,
+                            message = "无法解析上传数据格式"
+                        })
+                    end
+                end
+            else
+                log.error("UPLOAD", "请求体为空,无法提取boundary")
+                return 200, {
+                    ["Content-Type"] = "application/json"
+                }, json.encode({
+                    success = false,
+                    message = "请求体为空,无法解析上传数据"
+                })
+            end
+        end
+        
+        log.info("UPLOAD", "成功解析boundary: " .. boundary)
+        
+        log.info("UPLOAD", "上传参数: 目标路径=" .. target_path .. ", 文件名=" .. filename .. ", 完整路径=" .. file_path)
+        
+        -- 解析multipart数据
+        local upload_data = parse_multipart_data(body or "", boundary)
+        
+        if not upload_data.content then
+            log.error("UPLOAD", "无法解析上传文件数据")
+            return 200, {
+                ["Content-Type"] = "application/json"
+            }, json.encode({
+                success = false,
+                message = "无法解析上传文件数据"
+            })
+        end
+        
+        -- 检查文件大小(200KB限制)
+        if #upload_data.content > 200 * 1024 then
+            log.error("UPLOAD", "文件大小超过限制: " .. #upload_data.content .. " 字节")
+            return 200, {
+                ["Content-Type"] = "application/json"
+            }, json.encode({
+                success = false,
+                message = "文件大小超过200KB限制"
+            })
+        end
+        
+        -- 检查sd容量和内存容量
+        local available_space
+        if target_path == "/luadb" then
+            -- 检查系统内存容量
+            local total_mem, used_mem, max_used_mem = rtos.meminfo("sys")
+            if total_mem and used_mem then
+                local free_mem = total_mem - used_mem
+                log.info("UPLOAD", "系统内存信息 - 总内存:", total_mem, "已用:", used_mem, "可用:", free_mem)
+                -- 设置可用空闲内存空间
+                available_space = free_mem
+            else
+                log.info("UPLOAD", "获取系统内存信息失败,无法进行上传")
+                return 200, {["Content-Type"] = "application/json"},
+                json.encode({
+                    success = false,
+                    message = "获取系统内存失败,无法进行上传"
+                })
+            end
+        elseif target_path == "/sd" then
+            -- 获取SD卡可用空间
+            local data, err = fatfs.getfree("/sd")
+            if data then
+                log.info("UPLOAD", "SD卡可用空间信息:", json.encode(data))
+                -- 设置为sd卡可用空闲内存空间
+                available_space = tonumber(data.free_kb)
+            else
+                log.info("UPLOAD", "获取SD卡空间失败:", err)
+                available_space = 1024 * 1024  -- 默认1MB
+            end
+        end
+        
+        if available_space and available_space < #upload_data.content * 2 then  -- 预留足够的空间
+            log.error("UPLOAD", "存储空间不足: 需要 " .. #upload_data.content * 2 .. " 字节, 可用 " .. available_space .. " 字节")
+            return 200, {
+                ["Content-Type"] = "application/json"
+            }, json.encode({
+                success = false,
+                message = "存储空间不足,需要至少 " .. (#upload_data.content * 2) .. " 字节"
+            })
+        end
+        
+        -- 写入文件,保存原始请求路径
+        local original_requested_path = file_path
+        local success, err, actual_path = write_file_with_chunks(file_path, upload_data.content)
+        
+        if success then
+            -- 日志记录
+            log.info("UPLOAD", "文件上传成功: " .. filename .. ", 大小: " .. #upload_data.content .. " 字节, 实际保存路径: " .. actual_path)
+            -- 上传成功后再次收集垃圾
+            collectgarbage()
+            
+            -- 生成响应信息
+            local message = "文件上传成功"
+            if actual_path ~= original_requested_path then
+                message = message .. ", 由于目录限制,已保存到: " .. actual_path
+            end
+            
+            return 200, {
+                ["Content-Type"] = "application/json"
+            }, json.encode({
+                success = true,
+                message = message,
+                filename = filename,
+                size = #upload_data.content,
+                path = actual_path,
+                original_path = original_requested_path
+            })
+        else
+            log.error("UPLOAD", "文件上传失败: " .. (err or "未知错误"))
+            -- 即使失败也尝试收集垃圾
+            collectgarbage()
+            return 200, {
+                ["Content-Type"] = "application/json"
+            }, json.encode({
+                success = false,
+                message = "文件上传失败: " .. (err or "未知错误")
+            })
+        end
+    end
+    
     -- 文件删除
     if string_starts_with(uri, "/delete") and method == "POST" then
         -- 检查传统认证方式
@@ -951,7 +1456,7 @@ end
 @return 无 无返回值
 @usage
 -- 一、使用默认参数创建server服务器
--- 启动后连接默认AP热点,直接访问日志中默认的地址"http://192.168.4.1:80/explorer.html"来访问文件管理服务器。
+-- 启动后连接默认AP热点,直接使用日志中默认的地址"http://192.168.4.1:80/explorer.html"来访问文件管理服务器。
 exremotefile.open()