This commit is contained in:
nnwang
2025-12-01 17:24:01 +08:00
parent a635b91e66
commit 5a3ce18193
25 changed files with 6665 additions and 0 deletions

445
Client/main.lua Normal file
View File

@@ -0,0 +1,445 @@
-- simple_file_client.lua
-- 支持创建房间、WebSocket 连接、异步文件操作、心跳保活
-- 使用 Basalt 实现非阻塞异步任务
local args = {...}
local httpServer = args[1] or "http://192.168.2.200:8080"
local roomId = args[2] -- 可选的房间ID参数
local wsPort = 8081
local heartbeatInterval = 1 -- 心跳间隔(秒)
-- ========== 加载 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
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)
if string.sub(path, 1, 9) == "computer/" then
return string.sub(path, 10)
elseif string.sub(path, 1, 9) == "computer\\" then
return string.sub(path, 10)
end
return path
end
local function sendJson(ws, obj)
local payload = table_to_json(obj)
ws.send(payload)
end
-- ========== 文件系统操作(纯逻辑,无网络)==========
local function isLikelyText(data, maxCheck)
if not data then return false end
maxCheck = maxCheck or math.min(#data, 1024)
for i = 1, maxCheck do
local b = data:byte(i)
if b < 32 and not (b == 9 or b == 10 or b == 13) then
return false
end
end
return true
end
local function getFiles(currentPath, result, prefix)
local fullPrefix
if currentPath == "" then
fullPrefix = prefix:sub(1, -2) -- 移除末尾的斜杠,将 "computer/" 变为 "computer"
else
fullPrefix = prefix .. currentPath
end
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
local nextPath = currentPath == "" and entry or (currentPath .. "/" .. entry)
getFiles(nextPath, result, prefix)
end
end
else
local content = "[binary]"
local ok, handle = pcall(fs.open, absPath, "rb")
if ok and handle then
local data = handle.readAll()
handle.close()
log(absPath.." File size: " .. #data)
if data and isLikelyText(data) then
content = data
log(absPath .. " is text file")
end
if content == "[binary]" then print("binary file: " .. absPath) end
end
result[fullPrefix] = { isFile = true, content = content }
end
end
local function fetchFiles()
local files = {}
getFiles("", files, "computer/")
return files
end
local function saveFile(path, content)
path = cleanPath(path)
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)
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)
fs.makeDir(path)
end
local function renameFile(oldPath, newPath)
oldPath = cleanPath(oldPath)
newPath = cleanPath(newPath)
fs.move(oldPath, newPath)
end
local function deleteFile(path)
path = cleanPath(path)
if fs.exists(path) then
fs.delete(path)
end
end
-- ========== 异步任务处理器 ==========
local function handleFetchFiles(ws, reqId, sender)
local success, result = pcall(fetchFiles)
sendJson(ws, {
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
})
log("Async fetch_files completed: " .. (success and "OK" or "FAILED"))
end
local function handleSaveFile(ws, data, reqId, sender)
local success, err = pcall(function()
assert(data.path, "Missing path")
saveFile(data.path, data.content)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async save_file completed: " .. (success and "OK" or "FAILED"))
end
local function handleCreateFile(ws, data, reqId, sender)
local success, err = pcall(function()
assert(data.path, "Missing path")
createFile(data.path)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async create_file completed: " .. (success and "OK" or "FAILED"))
end
local function handleCreateFolder(ws, data, reqId, sender)
local success, err = pcall(function()
assert(data.path, "Missing path")
createFolder(data.path)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async create_folder completed: " .. (success and "OK" or "FAILED"))
end
local function handleRename(ws, data, reqId, sender)
local success, err = pcall(function()
assert(data.path and data.newPath, "Missing path or newPath")
renameFile(data.path, data.newPath)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async rename completed: " .. (success and "OK" or "FAILED"))
end
local function handleDelete(ws, data, reqId, sender)
log("delete:"..data.path)
local success, err = pcall(function()
assert(data.path, "Missing path")
deleteFile(data.path)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async delete completed: " .. (success and "OK" or "FAILED"))
end
-- ========== 房间管理函数 ==========
local function createRoom()
log("正在创建房间...")
local roomUrl = httpServer .. "/api/room"
local roomResp = http.post(roomUrl, "{}")
if not roomResp then
error("无法连接到 HTTP 服务器创建房间")
end
local body = roomResp.readAll()
roomResp.close()
local roomData = textutils.unserialiseJSON(body)
if not roomData or not roomData.room_id or not roomData.ws_url then
error("无效的房间响应: " .. tostring(body))
end
log("房间创建成功,房间 ID: " .. roomData.room_id)
return roomData
end
local function joinRoom(roomId)
log("正在加入房间: " .. roomId)
-- 构建 WebSocket URL
local wsUrl = httpServer:gsub("^http", "ws") .. "/ws?room_id=" .. roomId
log("连接 WebSocket: " .. wsUrl)
local ws = http.websocket(wsUrl)
if not ws then
error("无法打开 WebSocket 连接")
end
-- 加入房间
sendJson(ws, {
type = "join_room",
room_id = roomId,
client_type = "file_client"
})
return ws, roomId
end
-- ========== 主函数 ==========
local function main()
local ws, finalRoomId
if roomId then
-- 如果提供了房间ID直接加入
ws, finalRoomId = joinRoom(roomId)
else
-- 否则创建新房间
local roomData = createRoom()
ws, finalRoomId = joinRoom(roomData.room_id)
end
-- 启动心跳
local heartbeatTimer = os.startTimer(heartbeatInterval)
mainFrame:addProgram():execute(function ()
shell.run("shell")
end):setSize("parent.w","parent.h")
-- 消息循环
parallel.waitForAll(
function ()
sleep(0.5)
os.queueEvent("mouse_click",1,1,1)
while true do
local payload = ws.receive()
if payload then
local ok, msg = pcall(textutils.unserialiseJSON, payload)
if ok and type(msg) == "table" then
local msgType = msg.type
log("收到消息: " .. tostring(msgType))
if msgType == "connected" then
log("分配的客户端 ID: " .. tostring(msg.client_id))
elseif msgType == "file_operation" or msgType == "file_operation_request" then
op = msg.operation_type or msg.type
data = msg.data or {}
reqId = msg.requestId or msg.request_id
sender = msg.sender_id
-- 派发到后台线程
if op == "fetch_files" then
mainFrame:addThread():start(function () handleFetchFiles(ws, reqId, sender) end )
elseif op == "create_or_save_file" then
mainFrame:addThread():start(function () handleSaveFile(ws, data, reqId, sender) end)
elseif op == "new_file" then
mainFrame:addThread():start(function () handleCreateFile(ws, data, reqId, sender) end)
elseif op == "new_folder" then
mainFrame:addThread():start(function () handleCreateFolder(ws, data, reqId, sender) end)
elseif op == "rename" then
mainFrame:addThread():start(function () handleRename(ws, data, reqId, sender) end)
elseif op == "delete_file" then
mainFrame:addThread():start(function () handleDelete(ws, data, reqId, sender) end)
else
-- 同步返回未知操作错误(轻量)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = false,
error = "Unknown operation: " .. tostring(op),
target_client_id = sender
})
end
end
else
log("无效 JSON: " .. tostring(payload))
end
end
end
end,
function ()
while true do
local event, param1, param2, param3 = os.pullEvent()
if event == "timer" and param1 == heartbeatTimer then
sendJson(ws, { type = "ping" })
heartbeatTimer = os.startTimer(heartbeatInterval)
elseif event == "websocket_closed" and param1 == ws then
log("WebSocket 连接已关闭")
break
elseif event == "key" and param1 == keys.q then
log("用户按 Q 退出")
break
end
end
end)
ws.close()
log("客户端已停止。房间ID: " .. finalRoomId)
end
-- 启动主逻辑和Basalt事件循环
parallel.waitForAll(basalt.autoUpdate, main)