471 lines
14 KiB
Lua
471 lines
14 KiB
Lua
-- simple_file_client.lua
|
||
local args = {...}
|
||
local httpServer = args[1] or "http://192.168.2.200:8080"
|
||
local roomId = args[2]
|
||
local pollInterval = 1
|
||
local computerID = tostring(os.computerID() or "unknown")
|
||
|
||
-- ========== 加载 Basalt ==========
|
||
local basaltUrl = "https://git.liulikeji.cn/GitHub/Basalt/releases/download/v1.7/basalt.lua"
|
||
local basaltResp = http.get(basaltUrl)
|
||
if not basaltResp then
|
||
error("无法下载 Basalt 框架")
|
||
end
|
||
local basalt = load(basaltResp.readAll())()
|
||
basaltResp.close()
|
||
|
||
local mainFrame = basalt.createFrame()
|
||
|
||
-- ========== 工具函数 ==========
|
||
local function log(msg)
|
||
--basalt.debug("[FileClient] " .. tostring(msg))
|
||
end
|
||
|
||
-- 改进的二进制检测函数,增加对DFPWM等特定格式的支持
|
||
local function isBinaryFile(path)
|
||
local extension = string.lower(string.match(path, "%.([^%.%s]+)$") or "")
|
||
local binaryExtensions = {
|
||
["wav"] = true,
|
||
["mp3"] = true,
|
||
["ogg"] = true,
|
||
["flac"] = true,
|
||
["dfpwm"] = true,
|
||
["png"] = true,
|
||
["jpg"] = true,
|
||
["jpeg"] = true,
|
||
["gif"] = true,
|
||
["bmp"] = true,
|
||
["ico"] = true,
|
||
["exe"] = true,
|
||
["dll"] = true,
|
||
["so"] = true,
|
||
["bin"] = true,
|
||
["dat"] = true,
|
||
["zip"] = true,
|
||
["rar"] = true,
|
||
["tar"] = true,
|
||
["gz"] = true,
|
||
["pdf"] = true,
|
||
["doc"] = true,
|
||
["docx"] = true,
|
||
["xls"] = true,
|
||
["xlsx"] = true,
|
||
["ppt"] = true,
|
||
["pptx"] = true
|
||
}
|
||
|
||
if binaryExtensions[extension] then
|
||
return true
|
||
end
|
||
|
||
-- 对于没有扩展名的文件,检查内容
|
||
local absPath = path
|
||
if not fs.exists(absPath) then
|
||
return false
|
||
end
|
||
|
||
local ok, handle = pcall(fs.open, absPath, "rb")
|
||
if not ok or not handle then
|
||
return false
|
||
end
|
||
|
||
local data = handle.read(math.min(1024, fs.getSize(absPath)))
|
||
handle.close()
|
||
|
||
if not data then
|
||
return false
|
||
end
|
||
|
||
-- 检查是否存在控制字符(除常见的空白字符外)
|
||
for i = 1, #data do
|
||
local b = data:byte(i)
|
||
-- 控制字符范围是 0-8, 11-12, 14-31, 127
|
||
if (b >= 0 and b <= 8) or (b == 11) or (b == 12) or (b >= 14 and b <= 31) or (b == 127) then
|
||
-- 如果控制字符过多(超过5%),则认为是二进制文件
|
||
local controlCount = 0
|
||
for j = 1, #data do
|
||
local byte = data:byte(j)
|
||
if (byte >= 0 and byte <= 8) or (byte == 11) or (byte == 12) or (byte >= 14 and byte <= 31) or (byte == 127) then
|
||
controlCount = controlCount + 1
|
||
end
|
||
end
|
||
|
||
if controlCount / #data > 0.05 then
|
||
return true
|
||
end
|
||
end
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
function table_to_json(t, indent)
|
||
indent = indent or 0
|
||
local spaces = string.rep(" ", indent)
|
||
local result = {}
|
||
|
||
if type(t) ~= "table" then
|
||
if type(t) == "string" then
|
||
-- 正确转义所有特殊字符
|
||
local escaped = t:gsub("[\\\"\b\f\n\r\t]", function(c)
|
||
local replacements = {
|
||
['\\'] = '\\\\',
|
||
['"'] = '\\"',
|
||
['\b'] = '\\b',
|
||
['\f'] = '\\f',
|
||
['\n'] = '\\n',
|
||
['\r'] = '\\r',
|
||
['\t'] = '\\t'
|
||
}
|
||
return replacements[c]
|
||
end)
|
||
return '"' .. escaped .. '"'
|
||
elseif type(t) == "number" or type(t) == "boolean" then
|
||
return tostring(t)
|
||
else
|
||
return '"' .. tostring(t) .. '"'
|
||
end
|
||
end
|
||
|
||
-- 检查是否是数组
|
||
local is_array = true
|
||
local max_index = 0
|
||
local count = 0
|
||
for k, v in pairs(t) do
|
||
count = count + 1
|
||
if type(k) ~= "number" or k <= 0 or math.floor(k) ~= k then
|
||
is_array = false
|
||
end
|
||
if type(k) == "number" and k > max_index then
|
||
max_index = k
|
||
end
|
||
end
|
||
|
||
-- 空表当作对象处理
|
||
if count == 0 then
|
||
is_array = false
|
||
end
|
||
|
||
if is_array then
|
||
-- 处理数组
|
||
table.insert(result, "[")
|
||
local items = {}
|
||
for i = 1, max_index do
|
||
if t[i] ~= nil then
|
||
table.insert(items, table_to_json(t[i], indent + 2))
|
||
else
|
||
table.insert(items, "null")
|
||
end
|
||
end
|
||
table.insert(result, table.concat(items, ", "))
|
||
table.insert(result, "]")
|
||
else
|
||
-- 处理对象
|
||
table.insert(result, "{")
|
||
local items = {}
|
||
for k, v in pairs(t) do
|
||
local key = '"' .. tostring(k) .. '"'
|
||
local value = table_to_json(v, indent + 2)
|
||
if indent > 0 then
|
||
table.insert(items, spaces .. " " .. key .. ": " .. value)
|
||
else
|
||
table.insert(items, key .. ":" .. value)
|
||
end
|
||
end
|
||
if indent > 0 then
|
||
table.insert(result, table.concat(items, ",\n"))
|
||
table.insert(result, "\n" .. spaces .. "}")
|
||
else
|
||
table.insert(result, table.concat(items, ","))
|
||
table.insert(result, "}")
|
||
end
|
||
end
|
||
|
||
return table.concat(result, indent > 0 and "\n" .. spaces or "")
|
||
end
|
||
|
||
local function cleanPath(path)
|
||
return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
|
||
end
|
||
|
||
local function httpPost(path, data)
|
||
local jsonData = table_to_json(data)
|
||
local url = httpServer .. path
|
||
|
||
-- 使用长轮询
|
||
local response,err = http.post({
|
||
url = url,
|
||
body = jsonData,
|
||
method = "POST",
|
||
headers = {
|
||
["Content-Type"] = "application/json"
|
||
},
|
||
timeout = 60
|
||
})
|
||
|
||
if not response then
|
||
return nil, "0 "..err
|
||
end
|
||
|
||
local responseBody = response.readAll()
|
||
response.close()
|
||
|
||
local ok, result = pcall(textutils.unserialiseJSON, responseBody)
|
||
if ok then
|
||
return result
|
||
else
|
||
return nil, "1无效的JSON响应: " .. responseBody
|
||
end
|
||
end
|
||
|
||
-- ========== 文件系统操作 ==========
|
||
local function getFiles(currentPath, result, prefix)
|
||
local computerPrefix = "computer_" .. computerID
|
||
local fullPrefix = currentPath == "" and prefix:sub(1, -2) or prefix .. currentPath
|
||
local absPath = "/" .. (currentPath == "" and "" or currentPath)
|
||
|
||
if fs.isDir(absPath) then
|
||
result[fullPrefix] = { isFolder = true }
|
||
for _, entry in ipairs(fs.list(absPath)) do
|
||
if entry ~= "rom" then
|
||
getFiles(currentPath == "" and entry or (currentPath .. "/" .. entry), result, prefix)
|
||
end
|
||
end
|
||
else
|
||
local content = "[binary]"
|
||
if not isBinaryFile(absPath) then
|
||
local ok, handle = pcall(fs.open, absPath, "r")
|
||
if ok and handle then
|
||
local data = handle.readAll()
|
||
handle.close()
|
||
content = data or ""
|
||
end
|
||
end
|
||
|
||
result[fullPrefix] = { isFile = true, content = content, isBinary = isBinaryFile(absPath) }
|
||
end
|
||
end
|
||
|
||
local function fetchFiles()
|
||
local files = {}
|
||
getFiles("", files, "computer_" .. computerID .. "/")
|
||
return files
|
||
end
|
||
|
||
local function saveFile(path, content)
|
||
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
|
||
local dir = fs.getDir(path)
|
||
if not fs.exists(dir) then fs.makeDir(dir) end
|
||
local f = fs.open(path, "w")
|
||
f.write(content or "")
|
||
f.close()
|
||
end
|
||
|
||
local function createFile(path)
|
||
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
|
||
local dir = fs.getDir(path)
|
||
if not fs.exists(dir) then fs.makeDir(dir) end
|
||
if not fs.exists(path) then
|
||
local f = fs.open(path, "w")
|
||
f.close()
|
||
end
|
||
end
|
||
|
||
local function createFolder(path)
|
||
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
|
||
fs.makeDir(path)
|
||
end
|
||
|
||
local function renameFile(oldPath, newPath)
|
||
oldPath = cleanPath(oldPath):gsub("^computer[" .. computerID .. "_]*/", "")
|
||
newPath = cleanPath(newPath):gsub("^computer[" .. computerID .. "_]*/", "")
|
||
fs.move(oldPath, newPath)
|
||
end
|
||
|
||
local function deleteFile(path)
|
||
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
|
||
if fs.exists(path) then
|
||
fs.delete(path)
|
||
end
|
||
end
|
||
|
||
-- ========== 消息处理函数 ==========
|
||
local function handleFetchFiles(reqId, sender)
|
||
local success, result = pcall(fetchFiles)
|
||
return {
|
||
type = "file_operation_response",
|
||
requestId = reqId,
|
||
success = success,
|
||
data = success and result or nil,
|
||
error = success and nil or tostring(result),
|
||
target_client_id = sender
|
||
}
|
||
end
|
||
|
||
local function handleSaveFile(data, reqId, sender)
|
||
local success, err = pcall(saveFile, data.path, data.content)
|
||
return {
|
||
type = "file_operation_response",
|
||
requestId = reqId,
|
||
success = success,
|
||
error = success and nil or tostring(err),
|
||
target_client_id = sender
|
||
}
|
||
end
|
||
|
||
local function handleCreateFile(data, reqId, sender)
|
||
local success, err = pcall(createFile, data.path)
|
||
return {
|
||
type = "file_operation_response",
|
||
requestId = reqId,
|
||
success = success,
|
||
error = success and nil or tostring(err),
|
||
target_client_id = sender
|
||
}
|
||
end
|
||
|
||
local function handleCreateFolder(data, reqId, sender)
|
||
local success, err = pcall(createFolder, data.path)
|
||
return {
|
||
type = "file_operation_response",
|
||
requestId = reqId,
|
||
success = success,
|
||
error = success and nil or tostring(err),
|
||
target_client_id = sender
|
||
}
|
||
end
|
||
|
||
local function handleRename(data, reqId, sender)
|
||
local success, err = pcall(renameFile, data.path, data.newPath)
|
||
return {
|
||
type = "file_operation_response",
|
||
requestId = reqId,
|
||
success = success,
|
||
error = success and nil or tostring(err),
|
||
target_client_id = sender
|
||
}
|
||
end
|
||
|
||
local function handleDelete(data, reqId, sender)
|
||
local success, err = pcall(deleteFile, data.path)
|
||
return {
|
||
type = "file_operation_response",
|
||
requestId = reqId,
|
||
success = success,
|
||
error = success and nil or tostring(err),
|
||
target_client_id = sender
|
||
}
|
||
end
|
||
|
||
-- ========== 房间管理函数 ==========
|
||
local function createRoom()
|
||
local result, err = httpPost("/api/room", {})
|
||
if not result then
|
||
error("无法创建房间: " .. tostring(err))
|
||
end
|
||
return result.room_id
|
||
end
|
||
|
||
local function sendResponse(response)
|
||
if response and roomId then
|
||
local result, err = httpPost("/api/client/send", {
|
||
room_id = roomId,
|
||
message = response
|
||
})
|
||
if not result then
|
||
log("3 发送响应失败: " .. tostring(err))
|
||
end
|
||
end
|
||
end
|
||
|
||
local function pollMessages()
|
||
while true do
|
||
if not roomId then
|
||
sleep(pollInterval)
|
||
break
|
||
end
|
||
|
||
local result, err = httpPost("/api/client/receive", {
|
||
room_id = roomId
|
||
})
|
||
|
||
if result and result.success then
|
||
local msg = result.message
|
||
log(msg)
|
||
if msg then
|
||
local msgType = msg.type
|
||
|
||
if msgType == "file_operation" or msgType == "file_operation_request" then
|
||
local op = msg.operation_type or msg.type
|
||
local data = msg.data or {}
|
||
local reqId = msg.requestId or msg.request_id
|
||
local sender = msg.sender_id
|
||
|
||
local response
|
||
|
||
if op == "fetch_files" then
|
||
response = handleFetchFiles(reqId, sender)
|
||
elseif op == "create_or_save_file" then
|
||
response = handleSaveFile(data, reqId, sender)
|
||
elseif op == "new_file" then
|
||
response = handleCreateFile(data, reqId, sender)
|
||
elseif op == "new_folder" then
|
||
response = handleCreateFolder(data, reqId, sender)
|
||
elseif op == "rename" then
|
||
response = handleRename(data, reqId, sender)
|
||
elseif op == "delete_file" then
|
||
response = handleDelete(data, reqId, sender)
|
||
else
|
||
response = {
|
||
type = "file_operation_response",
|
||
requestId = reqId,
|
||
success = false,
|
||
error = "Unknown operation: " .. tostring(op),
|
||
target_client_id = sender
|
||
}
|
||
end
|
||
|
||
sendResponse(response)
|
||
end
|
||
end
|
||
elseif err then
|
||
log("2 轮询错误: " .. tostring(err))
|
||
-- 如果是连接错误,稍后再试
|
||
sleep(5)
|
||
end
|
||
|
||
end
|
||
end
|
||
|
||
-- ========== 主函数 ==========
|
||
local function main()
|
||
if not roomId then
|
||
roomId = createRoom()
|
||
log("创建新房间: " .. roomId)
|
||
else
|
||
log("使用现有房间: " .. roomId)
|
||
end
|
||
|
||
mainFrame:addProgram():execute(function()
|
||
shell.run("shell")
|
||
end):setSize("parent.w","parent.h")
|
||
os.queueEvent("mouse_click",1,1,1)
|
||
-- 启动消息轮询
|
||
mainFrame:addThread():start(pollMessages)
|
||
|
||
log("客户端已启动。房间ID: " .. roomId)
|
||
log("计算机ID: " .. computerID)
|
||
log("按 Q 退出")
|
||
|
||
-- 主循环
|
||
while true do
|
||
local event, param1 = os.pullEvent()
|
||
if event == "key" and param1 == keys.q then
|
||
log("用户按 Q 退出")
|
||
break
|
||
end
|
||
end
|
||
end
|
||
|
||
-- 启动主逻辑和Basalt事件循环
|
||
parallel.waitForAll(basalt.autoUpdate, main) |