new updates for paddel
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class SyncRun {
|
||||
var id: UUID
|
||||
var jobID: UUID
|
||||
var jobName: String
|
||||
var startedAt: Date
|
||||
var finishedAt: Date?
|
||||
var command: String
|
||||
var output: String
|
||||
var errorOutput: String
|
||||
var exitCode: Int?
|
||||
var wasDryRun: Bool
|
||||
var wasCancelled: Bool
|
||||
var triggerRaw: String = SyncRunTrigger.manual.rawValue
|
||||
|
||||
init(job: SyncJob, command: String, wasDryRun: Bool, trigger: SyncRunTrigger = .manual) {
|
||||
self.id = UUID()
|
||||
self.jobID = job.id
|
||||
self.jobName = job.name
|
||||
self.startedAt = Date()
|
||||
self.command = command
|
||||
self.output = ""
|
||||
self.errorOutput = ""
|
||||
self.exitCode = nil
|
||||
self.wasDryRun = wasDryRun
|
||||
self.wasCancelled = false
|
||||
self.triggerRaw = trigger.rawValue
|
||||
}
|
||||
|
||||
var trigger: SyncRunTrigger { SyncRunTrigger(rawValue: triggerRaw) ?? .manual }
|
||||
var succeeded: Bool { exitCode == 0 && !wasCancelled }
|
||||
var isRunning: Bool { finishedAt == nil }
|
||||
|
||||
var duration: TimeInterval? {
|
||||
guard let finishedAt else { return nil }
|
||||
return finishedAt.timeIntervalSince(startedAt)
|
||||
}
|
||||
|
||||
var summary: RsyncSummary {
|
||||
RsyncSummary.parse(output + "\n" + errorOutput)
|
||||
}
|
||||
|
||||
|
||||
static func sanitizedLog(_ value: String, maxBytes: Int = 2_000_000) -> String {
|
||||
let stripped = stripANSI(value)
|
||||
let redacted = stripped
|
||||
.replacingOccurrences(of: #"(?i)--password-file(?:=|\s+)[^\s]+"#, with: "--password-file=[REDACTED]", options: .regularExpression)
|
||||
.replacingOccurrences(of: #"(?i)(password|passwd|token|secret)=([^\s]+)"#, with: "$1=[REDACTED]", options: .regularExpression)
|
||||
|
||||
let data = Data(redacted.utf8)
|
||||
guard data.count > maxBytes else { return redacted }
|
||||
let prefix = data.prefix(maxBytes)
|
||||
return String(decoding: prefix, as: UTF8.self) + "\n\n[Log truncated by Rsync Studio for safety and storage limits.]\n"
|
||||
}
|
||||
|
||||
private static func stripANSI(_ value: String) -> String {
|
||||
value.replacingOccurrences(
|
||||
of: #"\u001B\[[0-9;?]*[ -/]*[@-~]"#,
|
||||
with: "",
|
||||
options: .regularExpression
|
||||
)
|
||||
}
|
||||
|
||||
var exportText: String {
|
||||
var text = "Rsync Studio Run Log\n"
|
||||
text += "Job: \(jobName)\n"
|
||||
text += "Started: \(startedAt.formatted(date: .complete, time: .standard))\n"
|
||||
if let finishedAt { text += "Finished: \(finishedAt.formatted(date: .complete, time: .standard))\n" }
|
||||
text += "Trigger: \(trigger.title)\n"
|
||||
text += "Dry run: \(wasDryRun ? "Yes" : "No")\n"
|
||||
text += "\nCommand\n-------\n$ \(command)\n\nOutput\n------\n"
|
||||
if !output.isEmpty { text += output }
|
||||
if !errorOutput.isEmpty {
|
||||
if !text.hasSuffix("\n") { text += "\n" }
|
||||
text += "\n--- stderr ---\n\(errorOutput)"
|
||||
}
|
||||
if let exitCode {
|
||||
if !text.hasSuffix("\n") { text += "\n" }
|
||||
text += "\nExit code: \(exitCode)\n"
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
struct RsyncSummary: Equatable {
|
||||
var numberOfFiles: String?
|
||||
var numberOfFilesTransferred: String?
|
||||
var totalFileSize: String?
|
||||
var totalTransferredFileSize: String?
|
||||
var unmatchedData: String?
|
||||
var matchedData: String?
|
||||
var fileListSize: String?
|
||||
var fileListGenerationTime: String?
|
||||
var fileListTransferTime: String?
|
||||
var totalSent: String?
|
||||
var totalReceived: String?
|
||||
var transferRate: String?
|
||||
var totalSizeAndSpeedup: String?
|
||||
|
||||
var hasStatistics: Bool {
|
||||
[
|
||||
numberOfFiles,
|
||||
numberOfFilesTransferred,
|
||||
totalFileSize,
|
||||
totalTransferredFileSize,
|
||||
totalSent,
|
||||
totalReceived
|
||||
].contains { $0 != nil }
|
||||
}
|
||||
|
||||
static func parse(_ text: String) -> RsyncSummary {
|
||||
var result = RsyncSummary()
|
||||
|
||||
for rawLine in text.components(separatedBy: .newlines) {
|
||||
let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !line.isEmpty else { continue }
|
||||
|
||||
func value(after prefix: String) -> String? {
|
||||
guard line.hasPrefix(prefix) else { return nil }
|
||||
return String(line.dropFirst(prefix.count)).trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
if let value = value(after: "Number of files:") { result.numberOfFiles = value; continue }
|
||||
if let value = value(after: "Number of files transferred:") { result.numberOfFilesTransferred = value; continue }
|
||||
if let value = value(after: "Total file size:") { result.totalFileSize = value; continue }
|
||||
if let value = value(after: "Total transferred file size:") { result.totalTransferredFileSize = value; continue }
|
||||
if let value = value(after: "Unmatched data:") { result.unmatchedData = value; continue }
|
||||
if let value = value(after: "Matched data:") { result.matchedData = value; continue }
|
||||
if let value = value(after: "File list size:") { result.fileListSize = value; continue }
|
||||
if let value = value(after: "File list generation time:") { result.fileListGenerationTime = value; continue }
|
||||
if let value = value(after: "File list transfer time:") { result.fileListTransferTime = value; continue }
|
||||
if let value = value(after: "Total sent:") { result.totalSent = value; continue }
|
||||
if let value = value(after: "Total received:") { result.totalReceived = value; continue }
|
||||
|
||||
if line.hasPrefix("sent "), line.contains(" bytes/sec") {
|
||||
result.transferRate = line
|
||||
continue
|
||||
}
|
||||
|
||||
if line.hasPrefix("total size is "), line.contains("speedup is") {
|
||||
result.totalSizeAndSpeedup = line
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user