更改ws为http
This commit is contained in:
511
Client/main.lua
511
Client/main.lua
@@ -1,18 +1,15 @@
|
||||
-- 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 -- 心跳间隔(秒)
|
||||
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 框架,请检查网络或使用本地缓存")
|
||||
error("无法下载 Basalt 框架")
|
||||
end
|
||||
local basalt = load(basaltResp.readAll())()
|
||||
basaltResp.close()
|
||||
@@ -110,336 +107,282 @@ function table_to_json(t, indent)
|
||||
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
|
||||
return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
|
||||
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
|
||||
local function httpPost(path, data)
|
||||
local jsonData = table_to_json(data)
|
||||
local url = httpServer .. path
|
||||
|
||||
local response = http.post(url, jsonData, {
|
||||
["Content-Type"] = "application/json"
|
||||
})
|
||||
|
||||
if not response then
|
||||
return nil, "无法连接到服务器"
|
||||
end
|
||||
end
|
||||
return true
|
||||
|
||||
local responseBody = response.readAll()
|
||||
response.close()
|
||||
|
||||
local ok, result = pcall(textutils.unserialiseJSON, responseBody)
|
||||
if ok then
|
||||
return result
|
||||
else
|
||||
return nil, "无效的JSON响应"
|
||||
end
|
||||
end
|
||||
|
||||
-- ========== 文件系统操作 ==========
|
||||
local function isLikelyText(data)
|
||||
for i = 1, math.min(#data, 1024) do
|
||||
local b = data:byte(i)
|
||||
if b < 32 and b ~= 9 and b ~= 10 and 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
|
||||
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]"
|
||||
local ok, handle = pcall(fs.open, absPath, "rb")
|
||||
if ok and handle then
|
||||
local data = handle.readAll()
|
||||
handle.close()
|
||||
if data and isLikelyText(data) then
|
||||
content = data
|
||||
end
|
||||
end
|
||||
result[fullPrefix] = { isFile = true, content = content }
|
||||
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
|
||||
local files = {}
|
||||
getFiles("", files, "computer_" .. computerID .. "/")
|
||||
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()
|
||||
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)
|
||||
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
|
||||
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)
|
||||
fs.makeDir(path)
|
||||
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
|
||||
fs.makeDir(path)
|
||||
end
|
||||
|
||||
local function renameFile(oldPath, newPath)
|
||||
oldPath = cleanPath(oldPath)
|
||||
newPath = cleanPath(newPath)
|
||||
fs.move(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)
|
||||
if fs.exists(path) then
|
||||
fs.delete(path)
|
||||
end
|
||||
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
|
||||
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"))
|
||||
-- ========== 消息处理函数 ==========
|
||||
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(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"))
|
||||
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(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"))
|
||||
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(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"))
|
||||
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(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"))
|
||||
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(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"))
|
||||
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()
|
||||
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
|
||||
local result, err = httpPost("/api/room", {})
|
||||
if not result then
|
||||
error("无法创建房间: " .. tostring(err))
|
||||
end
|
||||
return result.room_id
|
||||
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
|
||||
local function sendResponse(response)
|
||||
if response and roomId then
|
||||
httpPost("/api/client/send", {
|
||||
room_id = roomId,
|
||||
message = response
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- 加入房间
|
||||
sendJson(ws, {
|
||||
type = "join_room",
|
||||
room_id = roomId,
|
||||
client_type = "file_client"
|
||||
})
|
||||
|
||||
return ws, roomId
|
||||
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 and result.message then
|
||||
local msg = result.message
|
||||
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
|
||||
elseif err then
|
||||
log("轮询错误: " .. tostring(err))
|
||||
end
|
||||
|
||||
sleep(pollInterval)
|
||||
end
|
||||
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
|
||||
if not roomId then
|
||||
roomId = createRoom()
|
||||
log("创建新房间: " .. roomId)
|
||||
else
|
||||
log("使用现有房间: " .. roomId)
|
||||
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)
|
||||
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 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))
|
||||
local event, param1 = os.pullEvent()
|
||||
if event == "key" and param1 == keys.q then
|
||||
log("用户按 Q 退出")
|
||||
break
|
||||
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)
|
||||
parallel.waitForAll(basalt.autoUpdate, main)
|
||||
|
||||
Reference in New Issue
Block a user