Files
computer-craft-web-file/Client/main.lua
2025-12-01 17:24:01 +08:00

445 lines
13 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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)