Add client_read, client_write, client_edit and client_download
This commit is contained in:
@@ -8,6 +8,8 @@ typealias ClientReply = Components.Schemas.ClientReply
|
||||
|
||||
private let retryAfter = Duration.seconds(2)
|
||||
private let maxOutput = 64 << 10
|
||||
private let maxMessage = 256 << 20
|
||||
private let maxRead = 64 << 20
|
||||
private let copyTimeoutMs = 10 * 60 * 1000
|
||||
|
||||
@MainActor
|
||||
@@ -26,6 +28,7 @@ final class Remote {
|
||||
return
|
||||
}
|
||||
let task = api.session.webSocketTask(with: api.url("/api/clients/connect", scheme: "wss"))
|
||||
task.maximumMessageSize = maxMessage
|
||||
ws = task
|
||||
task.resume()
|
||||
receive(task)
|
||||
@@ -93,8 +96,24 @@ final class Remote {
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
let (code, output) = try await spawn(["/bin/zsh", "-l", "-c", req.command ?? ""], in: home, timeoutMs: req.timeoutMs ?? 120_000)
|
||||
return ClientReply(id: req.id, exitCode: code, output: output)
|
||||
case .copy:
|
||||
try await copy(transfer: req.transfer ?? "", to: req.dest ?? "")
|
||||
case .upload:
|
||||
try await upload(transfer: req.transfer ?? "", to: req.dest ?? "")
|
||||
return ClientReply(id: req.id)
|
||||
case .download:
|
||||
try await download(req.path ?? "", transfer: req.transfer ?? "")
|
||||
return ClientReply(id: req.id)
|
||||
case .readFile:
|
||||
let path = req.path ?? ""
|
||||
let data = try await Task.detached {
|
||||
try readFile(path)
|
||||
}.value
|
||||
return ClientReply(id: req.id, data: Base64EncodedData(data))
|
||||
case .writeFile:
|
||||
let path = req.path ?? ""
|
||||
let data = Data(req.data?.data ?? [])
|
||||
try await Task.detached {
|
||||
try writeFile(path, data)
|
||||
}.value
|
||||
return ClientReply(id: req.id)
|
||||
case .hands:
|
||||
let result = try await hands.call(req.tool ?? "", req.arguments?.additionalProperties ?? OpenAPIObjectContainer())
|
||||
@@ -102,8 +121,30 @@ final class Remote {
|
||||
}
|
||||
}
|
||||
|
||||
private func copy(transfer: String, to dest: String) async throws {
|
||||
let target = URL(filePath: (dest as NSString).expandingTildeInPath)
|
||||
private func download(_ path: String, transfer: String) async throws {
|
||||
let source = expand(path)
|
||||
let archive = FileManager.default.temporaryDirectory.appending(path: "byre-download-\(UUID().uuidString).tgz")
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: archive)
|
||||
}
|
||||
let parent = source.deletingLastPathComponent().path
|
||||
let (status, output) = try await spawn(["/usr/bin/tar", "-czf", archive.path, "-C", parent, "--", source.lastPathComponent], in: parent, timeoutMs: copyTimeoutMs)
|
||||
if status != 0 {
|
||||
throw ApiError(status: 0, message: "pack: tar exited \(status): \(output)")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: api.url("/api/clients/transfers/\(transfer)"))
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("application/gzip", forHTTPHeaderField: "Content-Type")
|
||||
let (body, response) = try await api.session.upload(for: request, fromFile: archive)
|
||||
let code = (response as! HTTPURLResponse).statusCode
|
||||
if code != 204 {
|
||||
throw ApiError(status: code, message: "upload transfer: \(code) \(String(decoding: body, as: UTF8.self))")
|
||||
}
|
||||
}
|
||||
|
||||
private func upload(transfer: String, to dest: String) async throws {
|
||||
let target = expand(dest)
|
||||
let parent = target.deletingLastPathComponent()
|
||||
let fm = FileManager.default
|
||||
try fm.createDirectory(at: parent, withIntermediateDirectories: true)
|
||||
@@ -117,7 +158,7 @@ final class Remote {
|
||||
throw ApiError(status: code, message: "download transfer: \(code) \(String(decoding: (try? Data(contentsOf: archive)) ?? Data(), as: UTF8.self))")
|
||||
}
|
||||
|
||||
let tmp = parent.appending(path: ".byre-copy-\(UUID().uuidString)", directoryHint: .isDirectory)
|
||||
let tmp = parent.appending(path: ".byre-upload-\(UUID().uuidString)", directoryHint: .isDirectory)
|
||||
try fm.createDirectory(at: tmp, withIntermediateDirectories: false)
|
||||
defer {
|
||||
try? fm.removeItem(at: tmp)
|
||||
@@ -137,6 +178,37 @@ final class Remote {
|
||||
}
|
||||
}
|
||||
|
||||
private func expand(_ path: String) -> URL {
|
||||
URL(filePath: (path as NSString).expandingTildeInPath)
|
||||
}
|
||||
|
||||
private func readFile(_ path: String) throws -> Data {
|
||||
let source = expand(path).resolvingSymlinksInPath()
|
||||
let type = try FileManager.default.attributesOfItem(atPath: source.path)[.type] as? FileAttributeType
|
||||
guard type == .typeRegular else {
|
||||
throw ApiError(status: 0, message: "\(path) isn't a regular file")
|
||||
}
|
||||
let h = try FileHandle(forReadingFrom: source)
|
||||
defer {
|
||||
try? h.close()
|
||||
}
|
||||
let data = try h.read(upToCount: maxRead + 1) ?? Data()
|
||||
if data.count > maxRead {
|
||||
throw ApiError(status: 0, message: "\(path) is over the \(maxRead >> 20) MiB read limit; use client_download")
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private func writeFile(_ path: String, _ data: Data) throws {
|
||||
let target = expand(path).resolvingSymlinksInPath()
|
||||
let fm = FileManager.default
|
||||
if let attrs = try? fm.attributesOfItem(atPath: target.path), attrs[.type] as? FileAttributeType != .typeRegular {
|
||||
throw ApiError(status: 0, message: "\(path) isn't a regular file")
|
||||
}
|
||||
try fm.createDirectory(at: target.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
try data.write(to: target)
|
||||
}
|
||||
|
||||
private final class Flag: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var value = false
|
||||
|
||||
Reference in New Issue
Block a user