├── images ├── rsyncui.png └── background.png ├── .swiftformat ├── Assets.xcassets ├── Contents.json └── AccentColor.colorset │ └── Contents.json ├── RsyncUIicon.icon ├── Assets │ ├── cloud.png │ └── icon_512x512@2x_1_numbers.png └── icon.json ├── WidgetEstimate ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── WidgetBackground.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── WidgetEstimate.entitlements ├── WidgetEstimateBundle.swift └── Info.plist ├── RsyncUI ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Model │ ├── Utils │ │ ├── yudpsocket │ │ │ ├── RsyncSwiftUI-Bridging-Header.h │ │ │ ├── ResultYTP.swift │ │ │ └── Socket.swift │ │ ├── MyEnvironment.swift │ │ ├── TCPconnections.swift │ │ ├── Resources.swift │ │ ├── PrepareOutputFromRsync.swift │ │ ├── FileSize.swift │ │ ├── Backupconfigfiles.swift │ │ ├── Macserialnumber.swift │ │ ├── RsyncCommandtoDisplay.swift │ │ ├── PushPullCommandtoDisplay.swift │ │ ├── GetfullpathforRsync.swift │ │ └── SetandValidatepathforrsync.swift │ ├── Storage │ │ ├── Basic │ │ │ ├── WidgetURLstrings.swift │ │ │ ├── JSON │ │ │ │ └── DecodeLogRecords.swift │ │ │ └── LogRecords.swift │ │ ├── Userconfiguration │ │ │ ├── ReadUserConfigurationJSON.swift │ │ │ └── WriteUserConfigurationJSON.swift │ │ ├── WriteSchedule.swift │ │ ├── ExportImport │ │ │ ├── ReadImportConfigurationsJSON.swift │ │ │ └── WriteExportConfigurationsJSON.swift │ │ ├── ReadLogRecordsJSON.swift │ │ ├── Actors │ │ │ ├── ActorReadSchedule.swift │ │ │ └── ActorReadSynchronizeConfigurationJSON.swift │ │ ├── WriteSynchronizeConfigurationJSON.swift │ │ ├── WriteLogRecordsJSON.swift │ │ └── Widgets │ │ │ └── WriteWidgetsURLStringsJSON.swift │ ├── Global │ │ ├── ObservableOutputfromrsync.swift │ │ ├── SharedConstants.swift │ │ ├── ObservableProfiles.swift │ │ ├── ObservableChartData.swift │ │ ├── AlertError.swift │ │ ├── ObservableLogSettings.swift │ │ ├── ObservableVerifyRemotePushPull.swift │ │ ├── ObservableSnapshotData.swift │ │ ├── RsyncUIconfigurations.swift │ │ ├── ObservableSSH.swift │ │ └── ObservableRsyncPathSetting.swift │ ├── Process │ │ ├── InterruptProcess.swift │ │ └── Rsyncversion.swift │ ├── Output │ │ ├── TrimOutputForRestore.swift │ │ └── TrimOutputFromRsync.swift │ ├── ParametersRsync │ │ ├── SSHParams.swift │ │ ├── ArgumentsSnapshotRemoteCatalogs.swift │ │ ├── ArgumentsRemoteFileList.swift │ │ ├── ArgumentsPullRemote.swift │ │ ├── ArgumentsRestore.swift │ │ ├── Params.swift │ │ ├── ArgumentsVerify.swift │ │ └── ArgumentsSynchronize.swift │ ├── Newversion │ │ ├── CheckfornewversionofRsyncUI.swift │ │ └── ActorGetversionofRsyncUI.swift │ ├── FilesAndCatalogs │ │ ├── HomeCatalogsService.swift │ │ ├── AttachedVolumesService.swift │ │ └── CatalogForProfile.swift │ ├── Execution │ │ ├── CreateHandlers │ │ │ ├── CreateCommandHandlers.swift │ │ │ └── CreateHandlers.swift │ │ └── ProgressDetails │ │ │ └── NoEstProgressDetails.swift │ ├── ProcessArguments │ │ ├── ArgumentsSnapshotCreateCatalog.swift │ │ └── ArgumentsSnapshotDeleteCatalogs.swift │ ├── Snapshots │ │ ├── LogRecordSnapshot.swift │ │ ├── RecordsSnapshot.swift │ │ ├── SnapshotRemoteCatalogs.swift │ │ └── DeleteSnapshots.swift │ ├── Ssh │ │ └── SshKeys.swift │ ├── Schedules │ │ └── SchedulesConfigurations.swift │ └── Deeplink │ │ └── DeeplinkURL.swift ├── Views │ ├── Quicktask │ │ └── ObservableRsyncOutput.swift │ ├── OutputViews │ │ ├── URLExtensions.swift │ │ ├── OutputRsyncView.swift │ │ ├── DetailsView.swift │ │ ├── DetailsViewHeading.swift │ │ └── RsyncRealtimeView.swift │ ├── Restore │ │ └── RestoreFilesTableView.swift │ ├── TextValues │ │ ├── EditValueScheme.swift │ │ └── EditValueErrorScheme.swift │ ├── Tools │ │ └── Commands │ │ │ ├── SnapshotCommands.swift │ │ │ └── ImportExportCommands.swift │ ├── Tasks │ │ ├── CompletedView.swift │ │ └── ExecuteNoEstTasksView.swift │ ├── VerifyRemote │ │ ├── DetailsVerifyView.swift │ │ └── PushPullCommandView.swift │ ├── Add │ │ ├── VerifyDuplicates.swift │ │ └── OpencatalogView.swift │ ├── RsyncParameters │ │ ├── RsyncCommandView.swift │ │ ├── OtherRsyncCommandsView.swift │ │ └── ArgumentsView.swift │ ├── Configurations │ │ ├── ListofTasksAddView.swift │ │ └── ConfigurationsTableDataView.swift │ ├── ScheduleView │ │ ├── TableofSchedules.swift │ │ └── TableofNotExeSchedules.swift │ ├── HelpView │ │ └── HelpView.swift │ ├── ProgressView │ │ └── SynchronizeProgressView.swift │ ├── Detailsview │ │ ├── TimerView.swift │ │ ├── EstimateTableView.swift │ │ └── EstimationInProgressView.swift │ ├── Settings │ │ ├── Environmentsettings.swift │ │ ├── SidebarSettingsView.swift │ │ └── Sshsettings.swift │ ├── Profiles │ │ └── ProfilesToUpdateView.swift │ ├── LogView │ │ └── LogfileView.swift │ └── Snapshots │ │ └── SnapshotListView.swift ├── RsyncUI.entitlements ├── Info.plist └── Main │ └── RsyncUIView.swift ├── .periphery.yml ├── Credits.rtf ├── RsyncUI.xcodeproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── WorkspaceSettings.xcsettings │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── .swiftlint.yml ├── XPC ├── XPC.entitlements ├── Info.plist ├── XPC.swift ├── XPCProtocol.swift └── main.swift ├── exportOptions.plist ├── versionRsyncUI ├── README.md ├── versionRsyncUI.json └── versionRsyncUIsonoma.json ├── Licence.MD ├── VSC ├── setup-build-dirs-MBP.sh └── setup-build-dirs-M4.sh ├── .gitignore └── README.md /images/rsyncui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsyncOSX/RsyncUI/HEAD/images/rsyncui.png -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --swiftversion 6 2 | --allman false 3 | --disable wrapMultilineStatementBraces 4 | -------------------------------------------------------------------------------- /images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsyncOSX/RsyncUI/HEAD/images/background.png -------------------------------------------------------------------------------- /Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RsyncUIicon.icon/Assets/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsyncOSX/RsyncUI/HEAD/RsyncUIicon.icon/Assets/cloud.png -------------------------------------------------------------------------------- /WidgetEstimate/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RsyncUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /RsyncUIicon.icon/Assets/icon_512x512@2x_1_numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsyncOSX/RsyncUI/HEAD/RsyncUIicon.icon/Assets/icon_512x512@2x_1_numbers.png -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/yudpsocket/RsyncSwiftUI-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /.periphery.yml: -------------------------------------------------------------------------------- 1 | project: RsyncUI.xcodeproj 2 | retain_objc_accessible: true 3 | retain_public: true 4 | schemes: 5 | - RsyncUI 6 | index_exclude: 7 | - RsyncUI/Model/Utils/yudpsocket/ytcpsocket.swift 8 | -------------------------------------------------------------------------------- /Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2636 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | } -------------------------------------------------------------------------------- /RsyncUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WidgetEstimate/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WidgetEstimate/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - file_length 4 | - type_body_length 5 | - function_body_length 6 | - trailing_comma 7 | - function_parameter_count 8 | - identifier_name 9 | 10 | opt_in_rules: 11 | - force_unwrapping 12 | -------------------------------------------------------------------------------- /XPC/XPC.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /WidgetEstimate/WidgetEstimate.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RsyncUI.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RsyncUI/Views/Quicktask/ObservableRsyncOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableRsyncOutput.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 04/10/2024. 6 | // 7 | 8 | import Observation 9 | import OSLog 10 | 11 | @Observable 12 | final class ObservableRsyncOutput { 13 | var output: [RsyncOutputData]? 14 | } 15 | -------------------------------------------------------------------------------- /RsyncUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /XPC/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | XPCService 6 | 7 | ServiceType 8 | Application 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /RsyncUI/Views/OutputViews/URLExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLExtensions.swift 3 | // RsyncUImenu 4 | // 5 | // Created by Assistant on 11/24/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension URL { 11 | static var userHomeDirectoryURLPath: URL? { 12 | FileManager.default.homeDirectoryForCurrentUser 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /exportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | method 6 | developer-id 7 | signingStyle 8 | automatic 9 | 10 | -------------------------------------------------------------------------------- /WidgetEstimate/WidgetEstimateBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetEstimateBundle.swift 3 | // WidgetEstimate 4 | // 5 | // Created by Thomas Evensen on 15/01/2025. 6 | // 7 | 8 | import SwiftUI 9 | import WidgetKit 10 | 11 | @main 12 | struct WidgetEstimateBundle: WidgetBundle { 13 | var body: some Widget { 14 | WidgetEstimate() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /RsyncUI/RsyncUI.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /WidgetEstimate/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/Basic/WidgetURLstrings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetURLstrings.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 14/01/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | @MainActor 11 | struct WidgetURLstrings: @MainActor Codable { 12 | var urlstringestimate: String? 13 | 14 | @discardableResult 15 | init(urletimate: String) { 16 | urlstringestimate = urletimate 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/ObservableOutputfromrsync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableOutputfromrsync.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 15/05/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RsyncOutputData: Identifiable, Equatable, Hashable { 11 | let id = UUID() 12 | var record: String 13 | } 14 | 15 | @Observable @MainActor 16 | final class ObservableOutputfromrsync { 17 | var output: [RsyncOutputData]? 18 | } 19 | -------------------------------------------------------------------------------- /versionRsyncUI/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## versionRsyncUI.json 3 | 4 | The file `versionRsyncUI.json` is checked by RsyncUI at every startup. The [file](https://github.com/rsyncOSX/RsyncUI/tree/main/versionRsyncUI/versionRsyncUI.json) is hosted on Github. The content of URL is `version` and url to new `RsyncUI.dmg` file. At startup RsyncUI checks its version number compared to version numbers listed in URL. If there is a match between RsyncUI version number and URL there is released a newer version. 5 | 6 | -------------------------------------------------------------------------------- /RsyncUI/Views/OutputViews/OutputRsyncView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OutputRsyncView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 20/11/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OutputRsyncView: View { 11 | var output: [RsyncOutputData] 12 | 13 | var body: some View { 14 | Table(output) { 15 | TableColumn("Output from rsync" + ": \(output.count) rows") { data in 16 | Text(data.record) 17 | } 18 | } 19 | .padding() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/SharedConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SharedConstants.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 29/03/2025. 6 | // 7 | 8 | // Sendable 9 | struct SharedConstants: Sendable { 10 | // JSON names 11 | let filenamelogrecordsjson = "logrecords.json" 12 | let fileconfigurationsjson = "configurations.json" 13 | // Caldenarfile 14 | let caldenarfilejson: String = "schedule.json" 15 | // Filename logfile 16 | let logname: String = "rsyncui.txt" 17 | // filsize logfile warning 18 | // 1_000_000 Bytes = 1 MB 19 | let logfilesize: Int = 1_000_000 20 | } 21 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/yudpsocket/ResultYTP.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ResultYTP { 4 | case success 5 | case failure(Error) 6 | 7 | public var isSuccess: Bool { 8 | switch self { 9 | case .success: 10 | true 11 | case .failure: 12 | false 13 | } 14 | } 15 | 16 | public var isFailure: Bool { 17 | !isSuccess 18 | } 19 | 20 | public var error: Error? { 21 | switch self { 22 | case .success: 23 | nil 24 | case let .failure(error): 25 | error 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/MyEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyEnvironment.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 02/06/2019. 6 | // Copyright © 2019 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @MainActor 12 | struct MyEnvironment { 13 | var environment: [String: String]? 14 | 15 | init?() { 16 | if let environment = SharedReference.shared.environment { 17 | if let environmentvalue = SharedReference.shared.environmentvalue { 18 | self.environment = [environment: environmentvalue] 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/TCPconnections.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TCPconnections.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 24.08.2018. 6 | // Copyright © 2018 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import OSLog 11 | 12 | struct TCPconnections: Sendable { 13 | func verifyTCPconnection(_ host: String, port: Int, timeout: Int) -> Bool { 14 | let client = TCPClient(address: host, port: Int32(port)) 15 | switch client.connect(timeout: timeout) { 16 | case .success: 17 | return true 18 | default: 19 | return false 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RsyncUI/Model/Process/InterruptProcess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterruptProcess.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 18/06/2020. 6 | // Copyright © 2020 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @MainActor 12 | struct InterruptProcess { 13 | @discardableResult 14 | init() { 15 | Task { 16 | let string: [String] = ["Interrupted: " + Date().long_localized_string_from_date()] 17 | await ActorLogToFile("Interrupted", string) 18 | SharedReference.shared.process?.interrupt() 19 | SharedReference.shared.process = nil 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /XPC/XPC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPC.swift 3 | // XPC 4 | // 5 | // Created by Thomas Evensen on 12/01/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | /// This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. 11 | class XPC: NSObject, XPCProtocol { 12 | /// This implements the example protocol. Replace the body of this class with the implementation of this service's protocol. 13 | @objc func performCalculation(firstNumber: Int, secondNumber: Int, with reply: @escaping (Int) -> Void) { 14 | let response = firstNumber + secondNumber 15 | reply(response) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /RsyncUI/Views/Restore/RestoreFilesTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestoreFilesTableView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 05/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RestoreFilesTableView: View { 11 | @State private var selectedid: RsyncOutputData.ID? 12 | @Binding var filestorestore: String 13 | 14 | var datalist: [RsyncOutputData] 15 | 16 | var body: some View { 17 | Table(datalist, selection: $selectedid) { 18 | TableColumn("Files for restore: \(datalist.count) files", value: \.record) 19 | } 20 | .onChange(of: selectedid) { 21 | let record = datalist.filter { $0.id == selectedid } 22 | guard record.count > 0 else { return } 23 | filestorestore = record[0].record 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RsyncUI/Model/Output/TrimOutputForRestore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrimOutputForRestore.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 05/05/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | @MainActor 11 | final class TrimOutputForRestore { 12 | var trimmeddata: [String]? 13 | 14 | init(_ stringoutputfromrsync: [String]) { 15 | trimmeddata = stringoutputfromrsync.compactMap { line in 16 | let substr = line.dropFirst(10).trimmingCharacters(in: .whitespacesAndNewlines) 17 | let str = substr.components(separatedBy: " ").dropFirst(3).joined(separator: " ") 18 | return (str.isEmpty == false && 19 | str.contains(".DS_Store") == false && 20 | str.contains("bytes") == false && 21 | str.contains("speedup") == false) ? ("./" + str) : nil 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /RsyncUI/Model/ParametersRsync/SSHParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSHParams.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 20/11/2025. 6 | // 7 | 8 | import Foundation 9 | import RsyncArguments 10 | 11 | @MainActor 12 | struct SSHParams { 13 | func sshparams( 14 | config: SynchronizeConfiguration) -> SSHParameters { 15 | SSHParameters( 16 | offsiteServer: config.offsiteServer, 17 | offsiteUsername: config.offsiteUsername, 18 | sshport: String(config.sshport ?? -1), 19 | sshkeypathandidentityfile: config.sshkeypathandidentityfile ?? "", 20 | sharedsshport: String(SharedReference.shared.sshport ?? -1), 21 | sharedsshkeypathandidentityfile: SharedReference.shared.sshkeypathandidentityfile, 22 | rsyncversion3: SharedReference.shared.rsyncversion3 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /RsyncUI/Model/Newversion/CheckfornewversionofRsyncUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckfornewversionofRsyncUI.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 07/12/2022. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | struct VersionsofRsyncUI: Codable { 12 | let url: String? 13 | let version: String? 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case url 17 | case version 18 | } 19 | 20 | init(from decoder: Decoder) throws { 21 | let values = try decoder.container(keyedBy: CodingKeys.self) 22 | url = try values.decodeIfPresent(String.self, forKey: .url) 23 | version = try values.decodeIfPresent(String.self, forKey: .version) 24 | } 25 | } 26 | 27 | @Observable @MainActor 28 | final class CheckfornewversionofRsyncUI { 29 | var notifynewversion: Bool = false 30 | var downloadavaliable: Bool = false 31 | } 32 | -------------------------------------------------------------------------------- /RsyncUI/Views/TextValues/EditValueScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditValueScheme.swift 3 | // RsyncSwiftUI 4 | // 5 | // Created by Thomas Evensen on 25/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EditValueScheme: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | 13 | var myvalue: Binding 14 | var mywidth: CGFloat? 15 | var myprompt: Text? 16 | 17 | var body: some View { 18 | TextField("", text: myvalue, prompt: myprompt) 19 | .textFieldStyle(RoundedBorderTextFieldStyle()) 20 | .frame(width: mywidth, alignment: .trailing) 21 | .lineLimit(1) 22 | .foregroundColor(colorScheme == .dark ? .white : .black) 23 | } 24 | 25 | init(_ width: CGFloat, _ str: String?, _ value: Binding) { 26 | mywidth = width 27 | myvalue = value 28 | myprompt = Text(str ?? "") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /RsyncUI/Model/FilesAndCatalogs/HomeCatalogsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeCatalogsService.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 09/08/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Catalog: Identifiable, Hashable { 11 | var id: String { name } 12 | let name: String 13 | 14 | init(_ name: String) { 15 | self.name = name 16 | } 17 | } 18 | 19 | struct HomeCatalogsService { 20 | func homeCatalogs() -> [Catalog] { 21 | let fm = FileManager.default 22 | let homeURL = fm.homeDirectoryForCurrentUser 23 | 24 | do { 25 | return try fm.contentsOfDirectory( 26 | at: homeURL, 27 | includingPropertiesForKeys: [.isDirectoryKey] 28 | ) 29 | .filter(\.hasDirectoryPath) 30 | .map { Catalog($0.lastPathComponent) } 31 | } catch { 32 | return [] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /RsyncUI/Model/Execution/CreateHandlers/CreateCommandHandlers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateCommandHandlers.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 17/11/2025. 6 | // 7 | 8 | import Foundation 9 | import ProcessCommand 10 | 11 | @MainActor 12 | struct CreateCommandHandlers { 13 | func createcommandhandlers( 14 | processTermination: @escaping ([String]?, Bool) -> Void 15 | 16 | ) -> ProcessHandlersCommand { 17 | ProcessHandlersCommand( 18 | processtermination: processTermination, 19 | checklineforerror: TrimOutputFromRsync().checkForRsyncError(_:), 20 | updateprocess: SharedReference.shared.updateprocess, 21 | propogateerror: { error in 22 | SharedReference.shared.errorobject?.alert(error: error) 23 | }, 24 | logger: { command, output in 25 | _ = await ActorLogToFile(command, output) 26 | }, 27 | rsyncui: true 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/Resources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resources.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 20/12/2016. 6 | // Copyright © 2016 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Enumtype type of resource 12 | enum ResourceType { 13 | case changelog 14 | case documents 15 | case urlJSON 16 | } 17 | 18 | struct Resources { 19 | // Resource strings 20 | private var changelog: String = "https://rsyncui.netlify.app/blog/" 21 | private var documents: String = "https://rsyncui.netlify.app/docs/" 22 | private var urlJSON: String = "https://raw.githubusercontent.com/rsyncOSX/RsyncUI/master/versionRsyncUI/versionRsyncUI.json" 23 | // Get the resource. 24 | func getResource(resource: ResourceType) -> String { 25 | switch resource { 26 | case .changelog: 27 | changelog 28 | case .documents: 29 | documents 30 | case .urlJSON: 31 | urlJSON 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /RsyncUI/Model/Output/TrimOutputFromRsync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrimOutputFromRsync.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 05/05/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Rsyncerror: LocalizedError { 11 | case rsyncerror 12 | 13 | var errorDescription: String? { 14 | switch self { 15 | case .rsyncerror: 16 | "There are errors in output from rsync" 17 | } 18 | } 19 | } 20 | 21 | @MainActor 22 | final class TrimOutputFromRsync { 23 | var trimmeddata: [String]? 24 | 25 | // Check for error in output form rsync 26 | func checkForRsyncError(_ line: String) throws { 27 | let error = line.contains("rsync error:") 28 | if error { 29 | throw Rsyncerror.rsyncerror 30 | } 31 | } 32 | 33 | init(_ stringoutputfromrsync: [String]) { 34 | trimmeddata = stringoutputfromrsync.compactMap { line in 35 | line.hasSuffix("/") == false ? line : nil 36 | } 37 | } 38 | 39 | init() {} 40 | } 41 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/ObservableProfiles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableProfiles.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 06/09/2024. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @Observable @MainActor 12 | final class ObservableProfiles { 13 | func createProfile(_ newprofile: String) -> Bool { 14 | guard newprofile != "Default" else { return false } 15 | guard newprofile != "default" else { return false } 16 | guard newprofile.isEmpty == false else { return false } 17 | let catalogprofile = CatalogForProfile() 18 | if catalogprofile.createProfileCatalog(newprofile) { 19 | return true 20 | } else { 21 | return false 22 | } 23 | } 24 | 25 | func deleteProfile(_ profile: String?) -> Bool { 26 | guard profile != nil else { return false } 27 | if CatalogForProfile().deleteProfileCatalog(profile) { 28 | return true 29 | } else { 30 | return false 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /RsyncUI/Model/ParametersRsync/ArgumentsSnapshotRemoteCatalogs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgumentsSnapshotRemoteCatalogs.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 27/08/2024. 6 | // 7 | 8 | import Foundation 9 | import RsyncArguments 10 | 11 | @MainActor 12 | final class ArgumentsSnapshotRemoteCatalogs { 13 | var config: SynchronizeConfiguration? 14 | 15 | func remotefilelistarguments() -> [String]? { 16 | if let config { 17 | let params = Params().params(config: config) 18 | let rsyncparametersrestore = RsyncParametersRestore(parameters: params) 19 | do { 20 | try rsyncparametersrestore.remoteArgumentsSnapshotCatalogList() 21 | return rsyncparametersrestore.computedArguments 22 | } catch { 23 | return nil 24 | } 25 | } 26 | return nil 27 | } 28 | 29 | init(config: SynchronizeConfiguration) { 30 | guard config.task == SharedReference.shared.snapshot else { return } 31 | self.config = config 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/ObservableChartData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableChartData.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 04/09/2025. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | import OSLog 11 | 12 | @Observable @MainActor 13 | final class ObservableChartData { 14 | var parsedlogs: [LogEntry]? 15 | 16 | // Only read logrecords from store once 17 | func readandparselogs(profile: String?, validhiddenIDs: Set, hiddenID: Int) async { 18 | guard parsedlogs == nil else { return } 19 | // Read logrecords 20 | let actorreadlogs = ActorReadLogRecordsJSON() 21 | let logrecords = await actorreadlogs.readjsonfilelogrecords(profile, validhiddenIDs) 22 | let actorreadchartsdata = ActorLogChartsData() 23 | let alllogs = await actorreadlogs.updatelogsbyhiddenID(logrecords, hiddenID) ?? [] 24 | // LogEntry logs 25 | parsedlogs = await actorreadchartsdata.parselogrecords(from: alllogs) 26 | } 27 | 28 | deinit { 29 | Logger.process.debugMessageOnly("ObservableChartData: DEINIT") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RsyncUI/Views/Tools/Commands/SnapshotCommands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnapshotCommands.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 08/11/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct SnapshotCommands: Commands { 12 | @FocusedBinding(\.tagsnapshot) private var tagsnapshot 13 | 14 | var body: some Commands { 15 | CommandMenu("Snapshots") { 16 | Tagsnapshot(tagsnapshot: $tagsnapshot) 17 | } 18 | } 19 | } 20 | 21 | struct Tagsnapshot: View { 22 | @Binding var tagsnapshot: Bool? 23 | 24 | var body: some View { 25 | Button { 26 | tagsnapshot = true 27 | } label: { 28 | Text("Tag snapshot") 29 | } 30 | .keyboardShortcut("t", modifiers: [.command]) 31 | } 32 | } 33 | 34 | struct FocusedTagsnapshot: FocusedValueKey { 35 | typealias Value = Binding 36 | } 37 | 38 | extension FocusedValues { 39 | var tagsnapshot: FocusedTagsnapshot.Value? { 40 | get { self[FocusedTagsnapshot.self] } 41 | set { self[FocusedTagsnapshot.self] = newValue } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /RsyncUIicon.icon/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "fill" : { 3 | "linear-gradient" : [ 4 | "srgb:1.00000,0.23788,0.21885,1.00000", 5 | "srgb:1.00000,0.26063,0.42736,1.00000" 6 | ] 7 | }, 8 | "groups" : [ 9 | { 10 | "layers" : [ 11 | { 12 | "image-name" : "cloud.png", 13 | "name" : "cloud" 14 | } 15 | ], 16 | "shadow" : { 17 | "kind" : "neutral", 18 | "opacity" : 0.5 19 | }, 20 | "translucency" : { 21 | "enabled" : true, 22 | "value" : 0.5 23 | } 24 | }, 25 | { 26 | "layers" : [ 27 | { 28 | "blend-mode" : "normal", 29 | "image-name" : "icon_512x512@2x_1_numbers.png", 30 | "name" : "icon_512x512@2x_1_numbers" 31 | } 32 | ], 33 | "shadow" : { 34 | "kind" : "neutral", 35 | "opacity" : 0.5 36 | }, 37 | "translucency" : { 38 | "enabled" : true, 39 | "value" : 0.5 40 | } 41 | } 42 | ], 43 | "supported-platforms" : { 44 | "circles" : [ 45 | "watchOS" 46 | ], 47 | "squares" : "shared" 48 | } 49 | } -------------------------------------------------------------------------------- /RsyncUI/Model/FilesAndCatalogs/AttachedVolumesService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttachedVolumesService.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 09/08/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | struct AttachedVolume: Identifiable, Hashable { 11 | var id: URL { volumeURL } 12 | let volumeURL: URL 13 | 14 | init(_ url: URL) { 15 | volumeURL = url 16 | } 17 | } 18 | 19 | struct AttachedVolumesService: Sendable { 20 | func attachedVolumes() -> [AttachedVolume] { 21 | let keys: [URLResourceKey] = [ 22 | .volumeNameKey, 23 | .volumeIsRemovableKey, 24 | .volumeIsEjectableKey, 25 | ] 26 | 27 | guard let paths = FileManager.default.mountedVolumeURLs( 28 | includingResourceValuesForKeys: keys, 29 | options: [] 30 | ) else { 31 | return [] 32 | } 33 | 34 | return paths 35 | .filter { url in 36 | let components = url.pathComponents 37 | return components.count > 1 && components[1] == "Volumes" 38 | } 39 | .map { AttachedVolume($0) } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Licence.MD: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2020-2026, Thomas Evensen 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without restriction, 9 | including without limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of the Software, 11 | and to permit persons to whom the Software is furnished to do so, subject 12 | to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included 15 | in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 21 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /RsyncUI/Model/Execution/ProgressDetails/NoEstProgressDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoEstProgressDetails.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 06/01/2024. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @Observable 12 | final class NoEstProgressDetails { 13 | var executenoestimationcompleted: Bool = false 14 | var executelist: [RemoteDataNumbers]? 15 | // UUIDs with data to be transferred 16 | var uuidswithdatatosynchronize = Set() 17 | 18 | func executeAllTasksNoEstimationComplete() { 19 | executenoestimationcompleted = true 20 | } 21 | 22 | func startExecuteAllTasksNoEstimation() { 23 | executenoestimationcompleted = false 24 | } 25 | 26 | func appendRecordExecutedList(_ record: RemoteDataNumbers) { 27 | if executelist == nil { 28 | executelist = [RemoteDataNumbers]() 29 | } 30 | executelist?.append(record) 31 | } 32 | 33 | func appendUUIDWithDataToSynchronize(_ id: UUID) { 34 | uuidswithdatatosynchronize.insert(id) 35 | } 36 | 37 | func reset() { 38 | uuidswithdatatosynchronize.removeAll() 39 | executelist = nil 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /WidgetEstimate/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /RsyncUI/Views/Tasks/CompletedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompletedView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 20/05/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CompletedView: View { 11 | // Navigation path for executetasks 12 | @Binding var executetaskpath: [Tasks] 13 | @State var showtext: Bool = true 14 | 15 | var body: some View { 16 | VStack { 17 | if showtext { 18 | HStack { 19 | Image(systemName: "hand.thumbsup.fill") 20 | .font(.title) 21 | .imageScale(.large) 22 | .foregroundColor(.green) 23 | Text("Synchronize data is completed") 24 | .foregroundColor(.green) 25 | .font(.title) 26 | } 27 | .onAppear { 28 | Task { 29 | try await Task.sleep(seconds: 1) 30 | showtext = false 31 | } 32 | } 33 | .onDisappear { 34 | executetaskpath.removeAll() 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/PrepareOutputFromRsync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrepareOutputFromRsync.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 17/02/2025. 6 | // 7 | 8 | struct PrepareOutputFromRsync { 9 | func prepareOutputFromRsync(_ stringoutputfromrsync: [String]?) -> [String] { 10 | let numberoflines = 20 11 | // Trim output, remove all catalogs - keep files only in output 12 | // And then only keep the lst 20 lines, it is there the accumulated numbers are 13 | let trimmeddata = stringoutputfromrsync?.compactMap { line in 14 | (line.last != "/") ? line : nil 15 | } 16 | var resultarrayrsyncoutput: [String]? 17 | let count = trimmeddata?.count 18 | // Delete most of lines and keep only the last 20 lines of array, that is where the summarized data stay. 19 | if (count ?? 0) >= numberoflines { 20 | let firstindex = (count ?? 0) - numberoflines 21 | let lastindex = (count ?? 0) 22 | resultarrayrsyncoutput = Array(trimmeddata?[firstindex ..< lastindex] ?? []) 23 | } else { 24 | resultarrayrsyncoutput = trimmeddata 25 | } 26 | return resultarrayrsyncoutput ?? [] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RsyncUI/Model/ProcessArguments/ArgumentsSnapshotCreateCatalog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgumentsSnapshotCreateCatalog.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 17.01.2018. 6 | // Copyright © 2018 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RsyncArguments 11 | 12 | @MainActor 13 | final class ArgumentsSnapshotCreateCatalog { 14 | private var config: SynchronizeConfiguration? 15 | private var arguments: [String]? 16 | private var command: String? 17 | 18 | private func argumentssnapshotcreatecatalog() -> [String]? { 19 | if let config { 20 | let sshparameters = SSHParams().sshparams(config: config) 21 | let createcatalog = SnapshotCreateRootCatalog(sshParameters: sshparameters) 22 | command = createcatalog.remoteCommand 23 | return createcatalog.snapshotCreateRootCatalog(offsiteCatalog: config.offsiteCatalog) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func getArguments() -> [String]? { arguments } 30 | func getCommand() -> String? { command } 31 | 32 | init(config: SynchronizeConfiguration?) { 33 | self.config = config 34 | arguments = argumentssnapshotcreatecatalog() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /RsyncUI/Model/Snapshots/LogRecordSnapshot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogRecordSnapshot.swift 3 | // RsyncSwiftUI 4 | // 5 | // Created by Thomas Evensen on 23/02/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct LogRecordSnapshot: Identifiable { 11 | var id = UUID() 12 | // Pick up the id from the actual log record 13 | // The id is used to clear not used log records 14 | var idlogrecord: UUID 15 | var date: Date 16 | var dateExecuted: String 17 | var resultExecuted: String 18 | var period: String? 19 | var snapshotCatalog: String? 20 | var latest: String? 21 | } 22 | 23 | extension LogRecordSnapshot: Hashable, Equatable { 24 | static func == (lhs: LogRecordSnapshot, rhs: LogRecordSnapshot) -> Bool { 25 | lhs.dateExecuted == rhs.dateExecuted && 26 | lhs.resultExecuted == rhs.resultExecuted && 27 | lhs.snapshotCatalog == rhs.snapshotCatalog && 28 | lhs.period == rhs.period && 29 | lhs.id == rhs.id 30 | } 31 | 32 | func hash(into hasher: inout Hasher) { 33 | hasher.combine(dateExecuted) 34 | hasher.combine(resultExecuted) 35 | hasher.combine(id) 36 | hasher.combine(snapshotCatalog) 37 | hasher.combine(period) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/FileSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileSize.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 19/06/2025. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | @MainActor 12 | struct FileSize { 13 | // Only logfile is checked for size, URL-file for logfile is evaluated within function 14 | 15 | func filesize() throws -> NSNumber? { 16 | let path = Homepath() 17 | let fm = FileManager.default 18 | if let fullpathmacserial = path.fullpathmacserial { 19 | let logfileString = fullpathmacserial.appending("/") + SharedConstants().logname 20 | guard fm.locationExists(at: logfileString, kind: .file) == true else { return nil } 21 | 22 | let fullpathmacserialURL = URL(fileURLWithPath: fullpathmacserial) 23 | let logfileURL = fullpathmacserialURL.appendingPathComponent(SharedConstants().logname) 24 | 25 | do { 26 | // Return filesize 27 | if let filesize = try fm.attributesOfItem(atPath: logfileURL.path)[FileAttributeKey.size] as? NSNumber { 28 | return filesize 29 | } 30 | } catch { 31 | return nil 32 | } 33 | } 34 | return nil 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /VSC/setup-build-dirs-MBP.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Base directory for builds 4 | BUILD_BASE="/Users/thomas/tmp/vscodebuilds" 5 | 6 | # Array of project names 7 | PROJECTS=("DecodeEncodeGeneric" "ParseRsyncOutput" "ProcessCommand" "RsyncArguments" "RsyncProcess" "RsyncUIDeepLinks" "SSHCreateKey" "RsyncUI") 8 | 9 | # Create base directory if it doesn't exist 10 | mkdir -p "$BUILD_BASE" 11 | 12 | # Process each project 13 | for project in "${PROJECTS[@]}"; do 14 | echo "Processing $project..." 15 | 16 | # Create the build directory in the target location 17 | mkdir -p "$BUILD_BASE/$project" 18 | 19 | # Navigate to project directory 20 | if [ -d "$project" ]; then 21 | cd "$project" 22 | 23 | # Remove existing .build (file, directory, or symlink) 24 | if [ -e .build ] || [ -L .build ]; then 25 | rm -rf .build 26 | echo " Removed existing .build" 27 | fi 28 | 29 | # Create symlink 30 | ln -s "$BUILD_BASE/$project" .build 31 | echo " Created symlink: .build -> $BUILD_BASE/$project" 32 | 33 | cd .. 34 | else 35 | echo " Warning: Directory $project not found, skipping..." 36 | fi 37 | done 38 | 39 | echo "Done! All projects configured." 40 | -------------------------------------------------------------------------------- /VSC/setup-build-dirs-M4.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Base directory for builds 4 | BUILD_BASE="/Volumes/MacMini4/tmp/vscodebuilds" 5 | 6 | # Array of project names 7 | PROJECTS=("DecodeEncodeGeneric" "ParseRsyncOutput" "ProcessCommand" "RsyncArguments" "RsyncProcess" "RsyncUIDeepLinks" "SSHCreateKey" "RsyncUI") 8 | 9 | # Create base directory if it doesn't exist 10 | mkdir -p "$BUILD_BASE" 11 | 12 | # Process each project 13 | for project in "${PROJECTS[@]}"; do 14 | echo "Processing $project..." 15 | 16 | # Create the build directory in the target location 17 | mkdir -p "$BUILD_BASE/$project" 18 | 19 | # Navigate to project directory 20 | if [ -d "$project" ]; then 21 | cd "$project" 22 | 23 | # Remove existing .build (file, directory, or symlink) 24 | if [ -e .build ] || [ -L .build ]; then 25 | rm -rf .build 26 | echo " Removed existing .build" 27 | fi 28 | 29 | # Create symlink 30 | ln -s "$BUILD_BASE/$project" .build 31 | echo " Created symlink: .build -> $BUILD_BASE/$project" 32 | 33 | cd .. 34 | else 35 | echo " Warning: Directory $project not found, skipping..." 36 | fi 37 | done 38 | 39 | echo "Done! All projects configured." 40 | -------------------------------------------------------------------------------- /RsyncUI/Model/ParametersRsync/ArgumentsRemoteFileList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgumentsRemoteFileList.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 07/08/2024. 6 | // 7 | 8 | import Foundation 9 | import RsyncArguments 10 | 11 | @MainActor 12 | final class ArgumentsRemoteFileList { 13 | var config: SynchronizeConfiguration? 14 | 15 | func remotefilelistarguments() -> [String]? { 16 | if let config { 17 | let params = Params().params(config: config) 18 | let rsyncparametersremotelist = RsyncParametersRestore(parameters: params) 19 | if config.task == SharedReference.shared.synchronize { 20 | do { 21 | try rsyncparametersremotelist.remoteArgumentsFileList() 22 | return rsyncparametersremotelist.computedArguments 23 | } catch {} 24 | } else if config.task == SharedReference.shared.snapshot { 25 | do { 26 | try rsyncparametersremotelist.remoteArgumentsSnapshotFileList() 27 | return rsyncparametersremotelist.computedArguments 28 | } catch {} 29 | } 30 | } 31 | return nil 32 | } 33 | 34 | init(config: SynchronizeConfiguration) { 35 | self.config = config 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /RsyncUI/Views/VerifyRemote/DetailsVerifyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailsVerifyView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 20/11/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DetailsVerifyView: View { 11 | let remotedatanumbers: RemoteDataNumbers 12 | 13 | var body: some View { 14 | Table(remotedatanumbers.outputfromrsync ?? []) { 15 | TableColumn("Output from rsync" + ": \(remotedatanumbers.outputfromrsync?.count ?? 0) rows") { data in 16 | if data.record.contains("*deleting") { 17 | HStack { 18 | Text("delete").foregroundColor(.red) 19 | Text(data.record) 20 | } 21 | 22 | } else if data.record.contains("<") { 23 | HStack { 24 | Text("push").foregroundColor(.blue) 25 | Text(data.record) 26 | } 27 | 28 | } else if data.record.contains(">") { 29 | HStack { 30 | Text("pull").foregroundColor(.green) 31 | Text(data.record) 32 | } 33 | } else { 34 | Text(data.record) 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /RsyncUI/Model/ParametersRsync/ArgumentsPullRemote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgumentsPullRemote.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 13/10/2019. 6 | // Copyright © 2019 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RsyncArguments 11 | 12 | @MainActor 13 | final class ArgumentsPullRemote { 14 | var config: SynchronizeConfiguration? 15 | 16 | func argumentspullremotewithparameters(dryRun: Bool, forDisplay: Bool, keepdelete: Bool) -> [String]? { 17 | if let config { 18 | let params = Params().params(config: config) 19 | let rsyncparameterssynchronize = RsyncParametersPullRemote(parameters: params) 20 | do { 21 | try rsyncparameterssynchronize.argumentsPullRemoteWithParameters(forDisplay: forDisplay, 22 | verify: false, 23 | dryrun: dryRun, keepDelete: keepdelete) 24 | return rsyncparameterssynchronize.computedArguments 25 | } catch { 26 | return nil 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | init(config: SynchronizeConfiguration?) { 33 | self.config = config 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/Userconfiguration/ReadUserConfigurationJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadUserConfigurationJSON.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 12/02/2022. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | 12 | @MainActor 13 | struct ReadUserConfigurationJSON { 14 | let path = Homepath() 15 | 16 | func readuserconfiguration() { 17 | let decodeuserconfiguration = DecodeGeneric() 18 | var userconfigurationfile = "" 19 | if let fullpathmacserial = path.fullpathmacserial { 20 | userconfigurationfile = fullpathmacserial.appending("/") + SharedReference.shared.userconfigjson 21 | } 22 | do { 23 | let importeddata = try 24 | decodeuserconfiguration.decode(DecodeUserConfiguration.self, 25 | fromFile: userconfigurationfile) 26 | 27 | UserConfiguration(importeddata) 28 | Logger.process.debugThreadOnly("ReadUserConfigurationJSON: Reading user configurations") 29 | 30 | } catch let err { 31 | Logger.process.error("ReadUserConfigurationJSON: some ERROR reading user configurations from permanent storage") 32 | let error = err 33 | path.propagateError(error: error) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /RsyncUI/Views/Add/VerifyDuplicates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerifyDuplicates.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 24/10/2024. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | @MainActor 12 | struct VerifyDuplicates { 13 | private var arrayofhiddenIDs = [Int]() 14 | 15 | private func checkForDuplicates() throws { 16 | let uniqueIDs = Set(arrayofhiddenIDs) 17 | guard arrayofhiddenIDs.count == uniqueIDs.count else { 18 | throw DuplicateError.duplicate 19 | } 20 | } 21 | 22 | @discardableResult 23 | init(_ configurations: [SynchronizeConfiguration]) { 24 | for configuration in configurations { 25 | arrayofhiddenIDs.append(configuration.hiddenID) 26 | } 27 | do { 28 | try checkForDuplicates() 29 | } catch let err { 30 | let error = err 31 | propagateError(error: error) 32 | } 33 | } 34 | 35 | func propagateError(error: Error) { 36 | SharedReference.shared.errorobject?.alert(error: error) 37 | } 38 | } 39 | 40 | enum DuplicateError: LocalizedError { 41 | case duplicate 42 | 43 | var errorDescription: String? { 44 | switch self { 45 | case .duplicate: 46 | "Oh my, you've got a duplicate hiddenID!" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/Backupconfigfiles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Backupconfigfiles.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 09/10/2020. 6 | // Copyright © 2020 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | @MainActor 13 | final class Backupconfigfiles { 14 | let homepath = Homepath() 15 | var fullpathnomacserial: String? 16 | var backuppath: String? 17 | 18 | func backup() { 19 | let fm = FileManager.default 20 | if let backuppath, 21 | let fullpathnomacserial { 22 | let fullpathnomacserialURL = URL(fileURLWithPath: fullpathnomacserial) 23 | let targetpath = "RsyncUIcopy-" + Date().shortlocalized_string_from_date() 24 | let documentsURL = URL(fileURLWithPath: backuppath) 25 | let documentsbackuppathURL = documentsURL.appendingPathComponent(targetpath) 26 | do { 27 | try fm.copyItem(at: fullpathnomacserialURL, to: documentsbackuppathURL) 28 | } catch let err { 29 | let error = err 30 | homepath.propagateError(error: error) 31 | } 32 | } 33 | } 34 | 35 | init() { 36 | fullpathnomacserial = homepath.fullpathnomacserial 37 | backuppath = homepath.documentscatalog 38 | backup() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /RsyncUI/Views/TextValues/EditValueErrorScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditValueErrorScheme.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 08/07/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EditValueErrorScheme: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | 13 | var myvalue: Binding 14 | var mywidth: CGFloat? 15 | var myprompt: Text? 16 | var myerror: Bool 17 | 18 | var body: some View { 19 | TextField("", text: Binding( 20 | get: { String(myvalue.wrappedValue) }, 21 | set: { newValue in 22 | if let converted = T(newValue) { 23 | myvalue.wrappedValue = converted 24 | } 25 | } 26 | ), prompt: myprompt) 27 | .textFieldStyle(RoundedBorderTextFieldStyle()) 28 | .frame(width: mywidth, alignment: .trailing) 29 | .lineLimit(1) 30 | .foregroundColor(color(error: myerror)) 31 | } 32 | 33 | init(_ width: CGFloat, _ str: String?, _ value: Binding, _ error: Bool) { 34 | mywidth = width 35 | myvalue = value 36 | myprompt = Text(str ?? "") 37 | myerror = error 38 | } 39 | 40 | func color(error: Bool) -> Color { 41 | error == false ? .red : (colorScheme == .dark ? .white : .black) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /RsyncUI/Model/Execution/CreateHandlers/CreateHandlers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateHandlers.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 16/11/2025. 6 | // 7 | 8 | import Foundation 9 | import RsyncProcess 10 | 11 | @MainActor 12 | struct CreateHandlers { 13 | func createHandlers( 14 | fileHandler: @escaping (Int) -> Void, 15 | processTermination: @escaping ([String]?, Int?) -> Void 16 | 17 | ) -> ProcessHandlers { 18 | ProcessHandlers( 19 | processTermination: processTermination, 20 | fileHandler: fileHandler, 21 | rsyncPath: GetfullpathforRsync().rsyncpath, 22 | checkLineForError: TrimOutputFromRsync().checkForRsyncError(_:), 23 | updateProcess: SharedReference.shared.updateprocess, 24 | propagateError: { error in 25 | SharedReference.shared.errorobject?.alert(error: error) 26 | }, 27 | logger: { command, output in 28 | _ = await ActorLogToFile(command, output) 29 | }, 30 | checkForErrorInRsyncOutput: SharedReference.shared.checkforerrorinrsyncoutput, 31 | rsyncVersion3: SharedReference.shared.rsyncversion3, 32 | environment: MyEnvironment()?.environment, 33 | printLine: RsyncOutputCapture.shared.makePrintLinesClosure() 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /XPC/XPCProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XPCProtocol.swift 3 | // XPC 4 | // 5 | // Created by Thomas Evensen on 12/01/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The protocol that this service will vend as its API. This protocol will also need to be visible to the process hosting the service. 11 | @objc protocol XPCProtocol { 12 | /// Replace the API of this protocol with an API appropriate to the service you are vending. 13 | func performCalculation(firstNumber: Int, secondNumber: Int, with reply: @escaping (Int) -> Void) 14 | } 15 | 16 | /* 17 | To use the service from an application or other process, use NSXPCConnection to establish a connection to the service by doing something like this: 18 | 19 | connectionToService = NSXPCConnection(serviceName: "test.XPC") 20 | connectionToService.remoteObjectInterface = NSXPCInterface(with: XPCProtocol.self) 21 | connectionToService.resume() 22 | 23 | Once you have a connection to the service, you can use it like this: 24 | 25 | if let proxy = connectionToService.remoteObjectProxy as? XPCProtocol { 26 | proxy.performCalculation(firstNumber: 23, secondNumber: 19) { result in 27 | NSLog("Result of calculation is: \(result)") 28 | } 29 | } 30 | 31 | And, when you are finished with the service, clean up the connection like this: 32 | 33 | connectionToService.invalidate() 34 | */ 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | .build/ 7 | build/ 8 | Build/ 9 | DerivedData/ 10 | Logs/ 11 | ModuleCache.noindex/ 12 | Index.noindex/ 13 | *.dmg 14 | *.ipa 15 | 16 | ## Swift Package Manager 17 | .swiftpm/ 18 | .netrc 19 | Packages/ 20 | Package.pins 21 | Package.resolved 22 | SourcePackages/ 23 | 24 | ## Various settings 25 | *.pbxuser 26 | !default.pbxuser 27 | *.mode1v3 28 | !default.mode1v3 29 | *.mode2v3 30 | !default.mode2v3 31 | *.perspectivev3 32 | !default.perspectivev3 33 | xcuserdata/ 34 | *.xcuserstate 35 | *.xcbkptlist 36 | 37 | ## Other 38 | *.moved-aside 39 | *.xccheckout 40 | *.xcscmblueprint 41 | *.hmap 42 | *.dSYM.zip 43 | *.dSYM 44 | *.sdkstatcache 45 | 46 | ## Playgrounds 47 | timeline.xctimeline 48 | playground.xcworkspace 49 | 50 | # CocoaPods 51 | # 52 | # Uncomment if you want to ignore Pods directory 53 | # Pods/ 54 | 55 | # Carthage 56 | # 57 | # Uncomment if you want to ignore Carthage checkouts 58 | # Carthage/Checkouts 59 | Carthage/Build/ 60 | 61 | # fastlane 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots/**/*.png 65 | fastlane/test_output/ 66 | 67 | # macOS 68 | .DS_Store 69 | 70 | # Info.plist (if auto-generated) 71 | # Note: Be careful with this - only ignore if it's truly generated 72 | # info.plist 73 | .build 74 | -------------------------------------------------------------------------------- /RsyncUI/Views/RsyncParameters/RsyncCommandView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RsyncCommandView.swift 3 | // RsyncOSXSwiftUI 4 | // 5 | // Created by Thomas Evensen on 07/01/2021. 6 | // Copyright © 2021 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct RsyncCommandView: View { 12 | @State var selectedrsynccommand: RsyncCommand = .synchronize_data 13 | 14 | let config: SynchronizeConfiguration 15 | 16 | var body: some View { 17 | HStack(alignment: .bottom) { 18 | Picker("", selection: $selectedrsynccommand) { 19 | ForEach(RsyncCommand.allCases) { Text($0.description) 20 | .tag($0) 21 | } 22 | } 23 | .pickerStyle(RadioGroupPickerStyle()) 24 | 25 | showcommand 26 | } 27 | } 28 | 29 | var showcommand: some View { 30 | Text(commandstring ?? "") 31 | .textSelection(.enabled) 32 | .lineLimit(nil) 33 | .multilineTextAlignment(.leading) 34 | .frame(maxWidth: .infinity) 35 | .padding() 36 | .overlay( 37 | RoundedRectangle(cornerRadius: 8) 38 | .stroke(.blue, lineWidth: 1) 39 | ) 40 | } 41 | 42 | var commandstring: String? { 43 | RsyncCommandtoDisplay(display: selectedrsynccommand, 44 | config: config).rsynccommand 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RsyncUI/Model/ParametersRsync/ArgumentsRestore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgumentsRestore.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 13/10/2019. 6 | // Copyright © 2019 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RsyncArguments 11 | 12 | @MainActor 13 | final class ArgumentsRestore { 14 | var config: SynchronizeConfiguration? 15 | var restoresnapshotbyfiles: Bool = false 16 | 17 | func argumentsrestore(dryRun: Bool, forDisplay: Bool) -> [String]? { 18 | if let config { 19 | let params = Params().params(config: config) 20 | let rsyncparametersrestore = RsyncParametersRestore(parameters: params) 21 | do { 22 | try rsyncparametersrestore.argumentsRestore(forDisplay: forDisplay, 23 | verify: false, 24 | dryrun: dryRun, 25 | restoreSnapshotByFiles: restoresnapshotbyfiles) 26 | return rsyncparametersrestore.computedArguments 27 | } catch { 28 | return nil 29 | } 30 | } 31 | return nil 32 | } 33 | 34 | init(config: SynchronizeConfiguration?, restoresnapshotbyfiles: Bool) { 35 | self.config = config 36 | self.restoresnapshotbyfiles = restoresnapshotbyfiles 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/AlertError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertError.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 20/12/2023. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | import SwiftUI 11 | 12 | @Observable @MainActor 13 | final class AlertError { 14 | private(set) var activeError: Error? 15 | 16 | func alert(error: Error) { 17 | activeError = error 18 | } 19 | 20 | func clearError() { 21 | activeError = nil 22 | } 23 | 24 | var isPresentingAlert: Binding { 25 | Binding( 26 | get: { self.activeError != nil }, 27 | set: { value in 28 | if !value { 29 | self.activeError = nil 30 | } 31 | } 32 | ) 33 | } 34 | } 35 | 36 | extension Alert { 37 | init(localizedError: Error) { 38 | self = Alert(nsError: localizedError as NSError) 39 | } 40 | 41 | init(nsError: NSError) { 42 | let message: Text? = { 43 | let message = [nsError.localizedFailureReason, 44 | nsError.localizedRecoverySuggestion] 45 | .compactMap(\.self).joined(separator: "\n\n") 46 | return message.isEmpty ? nil : Text(message) 47 | }() 48 | self = Alert(title: Text(nsError.localizedDescription), 49 | message: message, 50 | dismissButton: .default(Text("OK"))) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/ObservableLogSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableLogSettings.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 13/09/2024. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @Observable @MainActor 12 | final class ObservableLogSettings { 13 | // Detailed logging 14 | var addsummarylogrecord: Bool = SharedReference.shared.addsummarylogrecord 15 | // Check for "error" in output from rsync 16 | var checkforerrorinrsyncoutput: Bool = SharedReference.shared.checkforerrorinrsyncoutput 17 | // Automatic execution of estimated tasks 18 | var confirmexecute: Bool = SharedReference.shared.confirmexecute 19 | // Synchronize without time delay URL actions 20 | var synchronizewithouttimedelay: Bool = SharedReference.shared.synchronizewithouttimedelay 21 | // Toggle sidebar hidden on/off 22 | var sidebarishidden: Bool = SharedReference.shared.sidebarishidden 23 | // Observe mounting local atteched discs 24 | var observemountedvolumes: Bool = SharedReference.shared.observemountedvolumes 25 | // Always show the summarized estimated view 26 | var alwaysshowestimateddetailsview: Bool = SharedReference.shared.alwaysshowestimateddetailsview 27 | // Hide Verify Remote view 28 | var hideverifyremotefunction: Bool = SharedReference.shared.hideverifyremotefunction 29 | // Silence missing stats 30 | var silencemissingstats: Bool = SharedReference.shared.silencemissingstats 31 | } 32 | -------------------------------------------------------------------------------- /RsyncUI/Model/ProcessArguments/ArgumentsSnapshotDeleteCatalogs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgumentsSnapshotDeleteCatalogs.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 26.01.2018. 6 | // Copyright © 2018 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RsyncArguments 11 | 12 | @MainActor 13 | final class ArgumentsSnapshotDeleteCatalogs { 14 | private var config: SynchronizeConfiguration? 15 | private var arguments: [String]? 16 | private var command: String? 17 | private var remotecatalog: String 18 | 19 | private func argumentssnapshotdeletecatalogs() -> [String]? { 20 | if let config { 21 | let sshparameters = SSHParams().sshparams(config: config) 22 | let sshargs = SnapshotDelete(sshParameters: sshparameters) 23 | if config.offsiteServer.isEmpty == false { 24 | command = sshargs.remoteCommand 25 | } else { 26 | command = sshargs.localCommand 27 | } 28 | return sshargs.snapshotDelete(remoteCatalog: remotecatalog) 29 | } 30 | return nil 31 | } 32 | 33 | func getArguments() -> [String]? { arguments } 34 | func getCommand() -> String? { command } 35 | 36 | init(config: SynchronizeConfiguration, remotecatalog: String) { 37 | self.config = config 38 | self.remotecatalog = remotecatalog 39 | arguments = argumentssnapshotdeletecatalogs() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /RsyncUI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Viewer 26 | CFBundleURLName 27 | no.blogspot.RsyncUI 28 | CFBundleURLSchemes 29 | 30 | rsyncuiapp 31 | 32 | 33 | 34 | CFBundleVersion 35 | $(CURRENT_PROJECT_VERSION) 36 | LSApplicationCategoryType 37 | public.app-category.utilities 38 | LSMinimumSystemVersion 39 | $(MACOSX_DEPLOYMENT_TARGET) 40 | NSPrincipalClass 41 | NSApplication 42 | 43 | 44 | -------------------------------------------------------------------------------- /RsyncUI/Views/Add/OpencatalogView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpencatalogView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 06/03/2025. 6 | // 7 | 8 | import SwiftUI 9 | import UniformTypeIdentifiers 10 | 11 | struct OpencatalogView: View { 12 | @Binding var selecteditem: String 13 | @State private var isImporting: Bool = false 14 | let catalogs: Bool 15 | 16 | var body: some View { 17 | Button(action: { 18 | isImporting = true 19 | }, label: { 20 | if catalogs { 21 | Image(systemName: "folder.fill") 22 | .foregroundColor(Color(.blue)) 23 | } else { 24 | Image(systemName: "text.document.fill") 25 | .foregroundColor(Color(.blue)) 26 | } 27 | }) 28 | .fileImporter(isPresented: $isImporting, 29 | allowedContentTypes: [uutype], 30 | onCompletion: { result in 31 | switch result { 32 | case let .success(url): 33 | selecteditem = url.relativePath 34 | case let .failure(error): 35 | SharedReference.shared.errorobject?.alert(error: error) 36 | } 37 | }) 38 | } 39 | 40 | var uutype: UTType { 41 | if catalogs { 42 | .directory 43 | } else { 44 | .item 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /RsyncUI/Views/VerifyRemote/PushPullCommandView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PushPullCommandView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 07/12/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PushPullCommandView: View { 11 | @Binding var pushpullcommand: PushPullCommand 12 | @Binding var dryrun: Bool 13 | @Binding var keepdelete: Bool 14 | 15 | let config: SynchronizeConfiguration 16 | 17 | var body: some View { 18 | HStack { 19 | pickerselectcommand 20 | 21 | Spacer() 22 | 23 | showcommand 24 | } 25 | } 26 | 27 | var pickerselectcommand: some View { 28 | Picker("", selection: $pushpullcommand) { 29 | ForEach(PushPullCommand.allCases) { Text($0.description) 30 | .tag($0) 31 | } 32 | } 33 | .pickerStyle(RadioGroupPickerStyle()) 34 | } 35 | 36 | var showcommand: some View { 37 | Text(commandstring ?? "") 38 | .textSelection(.enabled) 39 | .lineLimit(nil) 40 | .multilineTextAlignment(.leading) 41 | .frame(maxWidth: .infinity) 42 | .padding() 43 | .overlay( 44 | RoundedRectangle(cornerRadius: 8) 45 | .stroke(.blue, lineWidth: 1) 46 | ) 47 | } 48 | 49 | var commandstring: String? { 50 | PushPullCommandtoDisplay(display: pushpullcommand, 51 | config: config, dryRun: dryrun, keepdelete: keepdelete).command 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/Macserialnumber.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Macserialnumber.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 24.08.2018. 6 | // Copyright © 2018 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Macserialnumber { 12 | // private var macSerialNumber: String? 13 | 14 | // Function for computing MacSerialNumber 15 | func getMacSerialNumber() -> String { 16 | // Get the platform expert 17 | let platformExpert: io_service_t = IOServiceGetMatchingService(kIOMainPortDefault, 18 | IOServiceMatching("IOPlatformExpertDevice")) 19 | // Get the serial number as a CFString ( actually as Unmanaged! ) 20 | let serialNumberAsCFString = IORegistryEntryCreateCFProperty(platformExpert, 21 | kIOPlatformSerialNumberKey as CFString?, 22 | kCFAllocatorDefault, 0) 23 | // Release the platform expert (we're responsible) 24 | IOObjectRelease(platformExpert) 25 | // Take the unretained value of the unmanaged-any-object 26 | // (so we're not responsible for releasing it) 27 | // and pass it back as a String or, if it fails, an empty string 28 | // return (serialNumberAsCFString!.takeUnretainedValue() as? String) ?? "" 29 | return (serialNumberAsCFString?.takeRetainedValue() as? String) ?? "C00123456789" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /versionRsyncUI/versionRsyncUI.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "2.7.0", 4 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 5 | }, 6 | { 7 | "version": "2.7.1", 8 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 9 | }, 10 | { 11 | "version": "2.7.2", 12 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 13 | }, 14 | { 15 | "version": "2.7.3", 16 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 17 | }, 18 | { 19 | "version": "2.7.4", 20 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 21 | }, 22 | { 23 | "version": "2.7.5", 24 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 25 | }, 26 | { 27 | "version": "2.7.6", 28 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 29 | }, 30 | { 31 | "version": "2.7.7", 32 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 33 | }, 34 | { 35 | "version": "2.7.8", 36 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 37 | }, 38 | { 39 | "version": "2.7.9", 40 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 41 | }, 42 | { 43 | "version": "2.8.0", 44 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /RsyncUI/Views/Configurations/ListofTasksAddView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListofTasksAddView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 25/08/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ListofTasksAddView: View { 11 | @Bindable var rsyncUIdata: RsyncUIconfigurations 12 | @Binding var selecteduuids: Set 13 | 14 | @State private var confirmdelete: Bool = false 15 | 16 | var body: some View { 17 | ConfigurationsTableDataView(selecteduuids: $selecteduuids, 18 | configurations: rsyncUIdata.configurations) 19 | .confirmationDialog(selecteduuids.count == 1 ? "Delete 1 configuration" : 20 | "Delete \(selecteduuids.count) configurations", 21 | isPresented: $confirmdelete) { 22 | Button("Delete") { 23 | delete() 24 | confirmdelete = false 25 | } 26 | } 27 | .onDeleteCommand { 28 | confirmdelete = true 29 | } 30 | } 31 | 32 | func delete() { 33 | if let configurations = rsyncUIdata.configurations { 34 | let deleteconfigurations = 35 | UpdateConfigurations(profile: rsyncUIdata.profile, 36 | configurations: configurations) 37 | deleteconfigurations.deleteconfigurations(selecteduuids) 38 | selecteduuids.removeAll() 39 | rsyncUIdata.configurations = deleteconfigurations.configurations 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /versionRsyncUI/versionRsyncUIsonoma.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "2.7.0", 4 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 5 | }, 6 | { 7 | "version": "2.7.1", 8 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 9 | }, 10 | { 11 | "version": "2.7.2", 12 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 13 | }, 14 | { 15 | "version": "2.7.3", 16 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 17 | }, 18 | { 19 | "version": "2.7.4", 20 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 21 | }, 22 | { 23 | "version": "2.7.5", 24 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 25 | }, 26 | { 27 | "version": "2.7.6", 28 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 29 | }, 30 | { 31 | "version": "2.7.7", 32 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 33 | }, 34 | { 35 | "version": "2.7.8", 36 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 37 | }, 38 | { 39 | "version": "2.7.9", 40 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 41 | }, 42 | { 43 | "version": "2.8.0", 44 | "url": "https://github.com/rsyncOSX/RsyncUI/releases/download/v2.8.1/RsyncUI.2.8.1.dmg" 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/WriteSchedule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteSchedule.swift 3 | 4 | import DecodeEncodeGeneric 5 | import Foundation 6 | import OSLog 7 | 8 | @MainActor 9 | struct WriteSchedule { 10 | private func writeJSONToPersistentStore(jsonData: Data?) { 11 | let path = Homepath() 12 | 13 | if let fullpathmacserial = path.fullpathmacserial { 14 | var calendarfileURL: URL? 15 | let fullpathmacserialURL = URL(fileURLWithPath: fullpathmacserial) 16 | calendarfileURL = fullpathmacserialURL.appendingPathComponent(SharedConstants().caldenarfilejson) 17 | if let jsonData, let calendarfileURL { 18 | do { 19 | try jsonData.write(to: calendarfileURL) 20 | } catch let err { 21 | Logger.process.error("WriteSchedule: some ERROR write Calendar to permanent storage") 22 | let error = err 23 | path.propagateError(error: error) 24 | } 25 | } 26 | } 27 | } 28 | 29 | private func encodeJSONData(_ calendar: [SchedulesConfigurations]) { 30 | let encodejsondata = EncodeGeneric() 31 | do { 32 | let encodeddata = try encodejsondata.encode(calendar) 33 | writeJSONToPersistentStore(jsonData: encodeddata) 34 | 35 | } catch { 36 | Logger.process.error("WriteSchedule some ERROR writing") 37 | return 38 | } 39 | } 40 | 41 | @discardableResult 42 | init(_ calendar: [SchedulesConfigurations]) { 43 | encodeJSONData(calendar) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Hi there 👋 2 | 3 | [![GitHub license](https://img.shields.io/github/license/rsyncOSX/RsyncUI)](https://github.com/rsyncOSX/RsyncUI/blob/main/Licence.MD) 4 | ![GitHub Releases](https://img.shields.io/github/downloads/rsyncosx/RsyncUI/v2.8.1/total) 5 | [![GitHub issues](https://img.shields.io/github/issues/rsyncOSX/RsyncUI)](https://github.com/rsyncOSX/RsyncUI/issues) 6 | 7 | This is the repository for RsyncUI, a SwiftUI based macOS application. RsyncUI is released for *macOS Sonoma and later*. RsyncUI is a GUI on the Apple macOS platform for the command line tool [rsync](https://github.com/WayneD/rsync). It is `rsync` which executes the synchronize data tasks. The GUI is *only* for organizing tasks, setting parameters to `rsync` and make it easier to use `rsync`. 8 | 9 | | Homebrew | macOS versions | Latest version | 10 | | ----------- | ----------- | ----------- | 11 | | `brew install --cask rsyncui` | macOS Sonoma and later | v2.8.1 - [December 5, 2025](https://github.com/rsyncOSX/RsyncUI/releases) - in *active development* | 12 | | | | [documentation](https://rsyncui.netlify.app/docs/) and [changelog](https://rsyncui.netlify.app/blog/) | 13 | 14 | If you find RsyncUI useful, I would appreciate it if you could consider giving me a star. Each star serves as motivation for me to continue developing RsyncUI. 15 | 16 | The [user documentation](https://rsyncui.netlify.app/docs/)([repo](https://github.com/rsyncOSX/rsyncuidocs)) is based upon a fork of the excellent Hugo based theme [Docsy](https://github.com/google/docsy). RsyncUI might be installed by Homebrew or by direct download. It is *signed* and *notarized* by Apple. 17 | 18 | ![](images/rsyncui.png) 19 | 20 | -------------------------------------------------------------------------------- /XPC/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // XPC 4 | // 5 | // Created by Thomas Evensen on 12/01/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | class ServiceDelegate: NSObject, NSXPCListenerDelegate { 11 | /// This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. 12 | func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { 13 | // Configure the connection. 14 | // First, set the interface that the exported object implements. 15 | newConnection.exportedInterface = NSXPCInterface(with: XPCProtocol.self) 16 | 17 | // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. 18 | let exportedObject = XPC() 19 | newConnection.exportedObject = exportedObject 20 | 21 | // Resuming the connection allows the system to deliver more incoming messages. 22 | newConnection.resume() 23 | 24 | // Returning true from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call invalidate() on the connection and return false. 25 | return true 26 | } 27 | } 28 | 29 | // Create the delegate for the service. 30 | let delegate = ServiceDelegate() 31 | 32 | // Set up the one NSXPCListener for this service. It will handle all incoming connections. 33 | let listener = NSXPCListener.service() 34 | listener.delegate = delegate 35 | 36 | // Resuming the serviceListener starts this service. This method does not return. 37 | listener.resume() 38 | -------------------------------------------------------------------------------- /RsyncUI/Views/ScheduleView/TableofSchedules.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableofSchedules.swift 3 | // Calendar 4 | // 5 | // Created by Thomas Evensen on 25/03/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TableofSchedules: View { 11 | @Binding var selecteduuids: Set 12 | 13 | var body: some View { 14 | Table(GlobalTimer.shared.allSchedules, selection: $selecteduuids) { 15 | TableColumn("Profile") { data in 16 | Text(data.scheduledata?.profile ?? "Default") 17 | } 18 | .width(min: 100, max: 150) 19 | 20 | TableColumn("Schedule") { data in 21 | Text(data.scheduledata?.schedule ?? "") 22 | } 23 | .width(min: 50, max: 70) 24 | 25 | TableColumn("Run") { data in 26 | Text(data.scheduledata?.dateRun ?? "") 27 | } 28 | .width(max: 120) 29 | 30 | TableColumn("Added") { data in 31 | Text(data.scheduledata?.dateAdded ?? "") 32 | } 33 | .width(max: 120) 34 | 35 | TableColumn("Min/hour/day") { data in 36 | var seconds: Double { 37 | if let date = data.scheduledata?.dateRun { 38 | let lastbackup = date.en_date_from_string() 39 | return lastbackup.timeIntervalSinceNow 40 | } else { 41 | return 0 42 | } 43 | } 44 | 45 | Text(seconds.latest()) 46 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .trailing) 47 | } 48 | .width(max: 90) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/ExportImport/ReadImportConfigurationsJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadImportConfigurationsJSON.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 23/07/2024. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | 12 | @MainActor 13 | final class ReadImportConfigurationsJSON { 14 | var importconfigurations: [SynchronizeConfiguration]? 15 | var maxhiddenID: Int = -1 16 | 17 | private func importjsonfile(_ filenameimport: String) { 18 | let decodeimport = DecodeGeneric() 19 | do { 20 | let importeddata = try 21 | decodeimport.decodeArray(DecodeSynchronizeConfiguration.self, fromFile: filenameimport) 22 | 23 | importconfigurations = importeddata.map { importrecord in 24 | var element = SynchronizeConfiguration(importrecord) 25 | element.hiddenID = maxhiddenID + 1 26 | element.dateRun = nil 27 | element.backupID = "IMPORT: " + (importrecord.backupID ?? "") 28 | element.id = UUID() 29 | maxhiddenID += 1 30 | return element 31 | } 32 | Logger.process.debugMessageOnly("ReadImportConfigurationsJSON - \(filenameimport)read import configurations from permanent storage") 33 | 34 | } catch { 35 | Logger.process.error("ReadImportConfigurationsJSON - \(filenameimport, privacy: .public): some ERROR read import configurations from permanent storage") 36 | return 37 | } 38 | } 39 | 40 | init(_ filenameimport: String, maxhiddenId: Int) { 41 | maxhiddenID = maxhiddenId 42 | importjsonfile(filenameimport) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /RsyncUI/Views/ScheduleView/TableofNotExeSchedules.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableofNotExeSchedules.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 19/10/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TableofNotExeSchedules: View { 11 | @Binding var selecteduuids: Set 12 | 13 | var body: some View { 14 | Table(GlobalTimer.shared.notExecutedSchedulesafterWakeUp, selection: $selecteduuids) { 15 | TableColumn("Profile") { data in 16 | Text(data.scheduledata?.profile ?? "Default") 17 | } 18 | .width(min: 100, max: 150) 19 | 20 | TableColumn("Schedule") { data in 21 | Text(data.scheduledata?.schedule ?? "") 22 | } 23 | .width(min: 50, max: 70) 24 | 25 | TableColumn("Run") { data in 26 | Text(data.scheduledata?.dateRun ?? "") 27 | } 28 | .width(max: 120) 29 | 30 | TableColumn("Added") { data in 31 | Text(data.scheduledata?.dateAdded ?? "") 32 | } 33 | .width(max: 120) 34 | 35 | TableColumn("Min/hour/day") { data in 36 | var seconds: Double { 37 | if let date = data.scheduledata?.dateRun { 38 | let lastbackup = date.en_date_from_string() 39 | return -lastbackup.timeIntervalSinceNow 40 | } else { 41 | return 0 42 | } 43 | } 44 | 45 | Text(seconds.latest()) 46 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .trailing) 47 | } 48 | .width(max: 90) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/ReadLogRecordsJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadLogRecordsJSON.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 19/04/2021. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | 12 | @MainActor 13 | final class ReadLogRecordsJSON { 14 | func readjsonfilelogrecords(_ profile: String?, _ validhiddenIDs: Set) -> [LogRecords]? { 15 | var filename = "" 16 | let path = Homepath() 17 | 18 | if let profile, let fullpathmacserial = path.fullpathmacserial { 19 | filename = fullpathmacserial.appending("/") + profile.appending("/") + SharedConstants().filenamelogrecordsjson 20 | } else { 21 | if let fullpathmacserial = path.fullpathmacserial { 22 | filename = fullpathmacserial.appending("/") + SharedConstants().filenamelogrecordsjson 23 | } 24 | } 25 | let decodeimport = DecodeGeneric() 26 | do { 27 | let data = try 28 | decodeimport.decodeArray(DecodeLogRecords.self, fromFile: filename) 29 | 30 | Logger.process.debugMessageOnly("ReadLogRecordsJSON - \(profile ?? "default") read logrecords from permanent storage") 31 | return data.compactMap { element in 32 | let item = LogRecords(element) 33 | return validhiddenIDs.contains(item.hiddenID) ? item : nil 34 | } 35 | 36 | } catch { 37 | Logger.process.error("ReadLogRecordsJSON - \(profile ?? "default profile", privacy: .public): some ERROR reading logrecords from permanent storage") 38 | } 39 | return nil 40 | } 41 | 42 | deinit { 43 | Logger.process.debugMessageOnly("ReadLogRecordsJSON: DEINIT") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /RsyncUI/Views/RsyncParameters/OtherRsyncCommandsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtherRsyncCommandsView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 16/09/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OtherRsyncCommandsView: View { 11 | @Bindable var rsyncUIdata: RsyncUIconfigurations 12 | @Binding var config: SynchronizeConfiguration? 13 | @Binding var otherselectedrsynccommand: OtherRsyncCommand 14 | 15 | var body: some View { 16 | HStack { 17 | pickerselectcommand 18 | 19 | Spacer() 20 | 21 | if config != nil { 22 | showcommand 23 | } 24 | } 25 | } 26 | 27 | var pickerselectcommand: some View { 28 | Picker("", selection: $otherselectedrsynccommand) { 29 | ForEach(OtherRsyncCommand.allCases) { Text($0.description) 30 | .tag($0) 31 | } 32 | } 33 | .pickerStyle(RadioGroupPickerStyle()) 34 | } 35 | 36 | var showcommand: some View { 37 | Text(commandstring ?? "") 38 | .textSelection(.enabled) 39 | .lineLimit(nil) 40 | .multilineTextAlignment(.leading) 41 | .frame(maxWidth: .infinity) 42 | .padding() 43 | .overlay( 44 | RoundedRectangle(cornerRadius: 8) 45 | .stroke(.blue, lineWidth: 1) 46 | ) 47 | } 48 | 49 | var commandstring: String? { 50 | if let config { 51 | return OtherRsyncCommandtoDisplay(display: otherselectedrsynccommand, 52 | config: config, 53 | profile: rsyncUIdata.profile).command 54 | } 55 | return nil 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/Actors/ActorReadSchedule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActorReadSchedule.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 19/04/2021. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | 12 | actor ActorReadSchedule { 13 | func readjsonfilecalendar(_ validprofiles: [String]) async -> [SchedulesConfigurations]? { 14 | var filename = "" 15 | let path = await Homepath() 16 | Logger.process.debugThreadOnly("ActorReadSchedule: readjsonfilecalendar()") 17 | if let fullpathmacserial = path.fullpathmacserial { 18 | filename = fullpathmacserial.appending("/") + SharedConstants().caldenarfilejson 19 | } 20 | 21 | let decodeimport = DecodeGeneric() 22 | do { 23 | let data = try 24 | decodeimport.decodeArray(DecodeSchedules.self, 25 | fromFile: filename) 26 | 27 | return data.compactMap { element in 28 | let item = SchedulesConfigurations(element) 29 | if item.schedule == ScheduleType.once.rawValue, 30 | let daterun = item.dateRun, daterun.en_date_from_string() < Date.now { 31 | return nil 32 | } else { 33 | if let profile = item.profile { 34 | return validprofiles.contains(profile) ? item : nil 35 | } else { 36 | return item 37 | } 38 | } 39 | } 40 | 41 | } catch { 42 | Logger.process.debugMessageOnly("ActorReadSchedule - read Calendar from permanent storage \(filename) failed with error: some ERROR reading") 43 | } 44 | return nil 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/Basic/JSON/DecodeLogRecords.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DecodeLogRecords.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 18/10/2020. 6 | // Copyright © 2020 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct DecodeLogRecords: Codable { 12 | let dateStart: String? 13 | let hiddenID: Int? 14 | var logrecords: [DecodeLog]? 15 | let offsiteserver: String? 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case dateStart 19 | case hiddenID 20 | case logrecords 21 | case offsiteserver 22 | } 23 | 24 | init(from decoder: Decoder) throws { 25 | let values = try decoder.container(keyedBy: CodingKeys.self) 26 | dateStart = try values.decodeIfPresent(String.self, forKey: .dateStart) 27 | hiddenID = try values.decodeIfPresent(Int.self, forKey: .hiddenID) 28 | logrecords = try values.decodeIfPresent([DecodeLog].self, forKey: .logrecords) 29 | offsiteserver = try values.decodeIfPresent(String.self, forKey: .offsiteserver) 30 | } 31 | } 32 | 33 | struct DecodeLog: Codable, Hashable { 34 | var dateExecuted: String? 35 | var resultExecuted: String? 36 | 37 | enum CodingKeys: String, CodingKey { 38 | case dateExecuted 39 | case resultExecuted 40 | } 41 | 42 | init(from decoder: Decoder) throws { 43 | let values = try decoder.container(keyedBy: CodingKeys.self) 44 | dateExecuted = try values.decodeIfPresent(String.self, forKey: .dateExecuted) 45 | resultExecuted = try values.decodeIfPresent(String.self, forKey: .resultExecuted) 46 | } 47 | 48 | // This init is used in WriteConfigurationJSON 49 | init() { 50 | dateExecuted = nil 51 | resultExecuted = nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /RsyncUI/Views/HelpView/HelpView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelpView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 09/05/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HelpView: View { 11 | @Environment(\.dismiss) var dismiss 12 | 13 | let text: String 14 | let add: Bool 15 | let deleteparameterpresent: Bool 16 | 17 | var body: some View { 18 | VStack(spacing: 20) { 19 | Text(text) 20 | .font(.title2) 21 | .multilineTextAlignment(.center) 22 | .padding() 23 | 24 | if add { 25 | VStack(alignment: .leading) { 26 | Text("Add --delete parameter") 27 | .foregroundColor(deleteparameterpresent ? .red : .blue) 28 | .font(.title2) 29 | } 30 | } 31 | 32 | ScrollView { 33 | Text("As a safety precaution, the --delete parameter is *not* set as a default parameter when adding new tasks. To ensure that the source and destination are in complete synchronization, the --delete parameter must be *enabled*. If you are new to `rsync`, I strongly recommend reading the *Important* and *Limitations* sections in RsyncUI user documentation as a minimum. ") 34 | .padding() 35 | } 36 | 37 | if #available(macOS 26.0, *) { 38 | Button("Close", role: .close) { 39 | dismiss() 40 | } 41 | .buttonStyle(RefinedGlassButtonStyle()) 42 | 43 | } else { 44 | Button("Close") { 45 | dismiss() 46 | } 47 | .padding() 48 | .buttonStyle(.borderedProminent) 49 | } 50 | } 51 | .padding() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /RsyncUI/Views/Tools/Commands/ImportExportCommands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImportExportCommands.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 21/07/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImportExportCommands: Commands { 11 | @FocusedBinding(\.exporttasks) private var exporttasks 12 | @FocusedBinding(\.importtasks) private var importtasks 13 | 14 | var body: some Commands { 15 | CommandGroup(replacing: CommandGroupPlacement.importExport) { 16 | Menu("Export & Import") { 17 | ExporttasksButton(exporttasks: $exporttasks) 18 | ImporttasksButton(importtasks: $importtasks) 19 | } 20 | } 21 | } 22 | } 23 | 24 | struct ExporttasksButton: View { 25 | @Binding var exporttasks: Bool? 26 | 27 | var body: some View { 28 | Button { 29 | exporttasks = true 30 | } label: { 31 | Text("Export") 32 | } 33 | } 34 | } 35 | 36 | struct ImporttasksButton: View { 37 | @Binding var importtasks: Bool? 38 | 39 | var body: some View { 40 | Button { 41 | importtasks = true 42 | } label: { 43 | Text("Import") 44 | } 45 | } 46 | } 47 | 48 | struct FocusedExporttasksBinding: FocusedValueKey { 49 | typealias Value = Binding 50 | } 51 | 52 | struct FocusedImporttasksBinding: FocusedValueKey { 53 | typealias Value = Binding 54 | } 55 | 56 | extension FocusedValues { 57 | var exporttasks: FocusedExporttasksBinding.Value? { 58 | get { self[FocusedExporttasksBinding.self] } 59 | set { self[FocusedExporttasksBinding.self] = newValue } 60 | } 61 | 62 | var importtasks: FocusedImporttasksBinding.Value? { 63 | get { self[FocusedImporttasksBinding.self] } 64 | set { self[FocusedImporttasksBinding.self] = newValue } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/ObservableVerifyRemotePushPull.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableVerifyRemotePushPull.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 16/12/2024. 6 | // 7 | 8 | import OSLog 9 | 10 | @Observable 11 | final class ObservableVerifyRemotePushPull { 12 | @ObservationIgnored var adjustedpull: Set? 13 | @ObservationIgnored var adjustedpush: Set? 14 | 15 | @ObservationIgnored var rsyncpull: [String]? 16 | @ObservationIgnored var rsyncpush: [String]? 17 | 18 | @ObservationIgnored var rsyncpullmax: Int = 0 19 | @ObservationIgnored var rsyncpushmax: Int = 0 20 | 21 | func adjustoutput() { 22 | if var pullremote = rsyncpull, 23 | var pushremote = rsyncpush { 24 | guard pullremote.count > 17, pushremote.count > 17 else { return } 25 | 26 | pullremote.removeFirst() 27 | pushremote.removeFirst() 28 | 29 | pullremote.removeLast(17) 30 | pushremote.removeLast(17) 31 | 32 | // Pull data <<-- 33 | var setpullremote = Set(pullremote.compactMap { row in 34 | row.hasSuffix("/") == false ? row : nil 35 | }) 36 | setpullremote.subtract(pushremote.compactMap { row in 37 | row.hasSuffix("/") == false ? row : nil 38 | }) 39 | 40 | adjustedpull = setpullremote 41 | 42 | // Push data -->> 43 | var setpushremote = Set(pushremote.compactMap { row in 44 | row.hasSuffix("/") == false ? row : nil 45 | }) 46 | setpushremote.subtract(pullremote.compactMap { row in 47 | row.hasSuffix("/") == false ? row : nil 48 | }) 49 | 50 | adjustedpush = setpushremote 51 | } 52 | } 53 | 54 | deinit { 55 | Logger.process.debugMessageOnly("ObservablePushPull: DEINIT") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/Userconfiguration/WriteUserConfigurationJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteUserConfigurationJSON.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 12/02/2022. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | 12 | @MainActor 13 | struct WriteUserConfigurationJSON { 14 | let path = Homepath() 15 | 16 | private func writeJSONToPersistentStore(jsonData: Data?) { 17 | if let fullpathmacserial = path.fullpathmacserial { 18 | let fullpathmacserialURL = URL(fileURLWithPath: fullpathmacserial) 19 | let usercongigfileURL = fullpathmacserialURL.appendingPathComponent(SharedReference.shared.userconfigjson) 20 | if let jsonData { 21 | do { 22 | try jsonData.write(to: usercongigfileURL) 23 | } catch let err { 24 | let error = err 25 | path.propagateError(error: error) 26 | } 27 | } 28 | } 29 | } 30 | 31 | private func encodeJSONData(_ userconfiguration: UserConfiguration) { 32 | let encodejsondata = EncodeGeneric() 33 | do { 34 | let encodeddata = try encodejsondata.encode(userconfiguration) 35 | writeJSONToPersistentStore(jsonData: encodeddata) 36 | Logger.process.debugMessageOnly("WriteUserConfigurationJSON: Writing user configurations to permanent storage") 37 | 38 | } catch let err { 39 | Logger.process.error("WriteUserConfigurationJSON: some ERROR writing user configurations from permanent storage") 40 | let error = err 41 | path.propagateError(error: error) 42 | } 43 | } 44 | 45 | @discardableResult 46 | init(_ userconfiguration: UserConfiguration?) { 47 | if let userconfiguration { 48 | encodeJSONData(userconfiguration) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /RsyncUI/Model/Snapshots/RecordsSnapshot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordsSnapshot.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 27/08/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | final class RecordsSnapshot { 11 | var loggrecordssnapshots: [LogRecordSnapshot]? 12 | 13 | private func readandsortallloggdata(_ config: SynchronizeConfiguration, 14 | _ logrecords: [LogRecords]) { 15 | var data: [LogRecordSnapshot]? 16 | let localrecords = logrecords.filter { $0.hiddenID == config.hiddenID } 17 | guard localrecords.count == 1 else { return } 18 | if let logrecords = localrecords[0].logrecords { 19 | data = logrecords.map { record in 20 | var datestring: String? 21 | var date: Date? 22 | if let stringdate = record.dateExecuted { 23 | if stringdate.isEmpty == false { 24 | datestring = stringdate.en_date_from_string().localized_string_from_date() 25 | date = stringdate.en_date_from_string() 26 | } 27 | } 28 | return LogRecordSnapshot( 29 | // Pick up the id from the log record itself. 30 | idlogrecord: record.id, 31 | date: date ?? Date(), 32 | dateExecuted: datestring ?? "", 33 | resultExecuted: record.resultExecuted ?? "" 34 | ) 35 | } 36 | loggrecordssnapshots = data?.sorted { d1, d2 in 37 | d1.dateExecuted.en_date_from_string() < d2.dateExecuted.en_date_from_string() 38 | } 39 | } 40 | } 41 | 42 | init(config: SynchronizeConfiguration, 43 | logrecords: [LogRecords]) { 44 | if loggrecordssnapshots == nil { 45 | readandsortallloggdata(config, logrecords) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/ExportImport/WriteExportConfigurationsJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteExportConfigurationsJSON.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 22/07/2024. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | 12 | @MainActor 13 | final class WriteExportConfigurationsJSON { 14 | var exportpath: String? 15 | 16 | private func writeJSONToPersistentStore(jsonData: Data?) { 17 | if let exportpath { 18 | let exportconfigurationfileURL = URL(fileURLWithPath: exportpath) 19 | 20 | if let jsonData { 21 | do { 22 | try jsonData.write(to: exportconfigurationfileURL) 23 | Logger.process.debugMessageOnly("WriteExportConfigurationsJSON - \(exportpath) write export configurations to permanent storage") 24 | } catch let err { 25 | Logger.process.error("WriteExportConfigurationsJSON - \(exportpath) some ERROR write export configurations to permanent storage") 26 | let error = err 27 | propagateError(error: error) 28 | } 29 | } 30 | } 31 | } 32 | 33 | private func encodeJSONData(_ configurations: [SynchronizeConfiguration]) { 34 | let encodejsondata = EncodeGeneric() 35 | do { 36 | let encodeddata = try encodejsondata.encode(configurations) 37 | writeJSONToPersistentStore(jsonData: encodeddata) 38 | 39 | } catch let err { 40 | let error = err 41 | propagateError(error: error) 42 | } 43 | } 44 | 45 | func propagateError(error: Error) { 46 | SharedReference.shared.errorobject?.alert(error: error) 47 | } 48 | 49 | @discardableResult 50 | init(_ path: String?, _ configurations: [SynchronizeConfiguration]?) { 51 | exportpath = path 52 | if let configurations { 53 | encodeJSONData(configurations) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /RsyncUI/Views/RsyncParameters/ArgumentsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgumentsView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 16/09/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ArgumentsView: View { 11 | @Bindable var rsyncUIdata: RsyncUIconfigurations 12 | 13 | @State private var selectedconfig: SynchronizeConfiguration? 14 | @State private var otherselectedrsynccommand = OtherRsyncCommand.list_remote_files 15 | @State private var selecteduuids = Set() 16 | 17 | var body: some View { 18 | VStack { 19 | ConfigurationsTableDataView(selecteduuids: $selecteduuids, 20 | configurations: rsyncUIdata.configurations) 21 | .frame(maxWidth: .infinity) 22 | .onChange(of: selecteduuids) { 23 | if let configurations = rsyncUIdata.configurations { 24 | if let index = configurations.firstIndex(where: { $0.id == selecteduuids.first }) { 25 | selectedconfig = configurations[index] 26 | } else { 27 | selectedconfig = nil 28 | } 29 | } 30 | } 31 | .onChange(of: rsyncUIdata.profile) { 32 | selecteduuids.removeAll() 33 | selectedconfig = nil 34 | } 35 | 36 | VStack(alignment: .leading) { 37 | Text("Select a task") 38 | .font(.title3) 39 | .fontWeight(.bold) 40 | 41 | OtherRsyncCommandsView(rsyncUIdata: rsyncUIdata, 42 | config: $selectedconfig, 43 | otherselectedrsynccommand: $otherselectedrsynccommand) 44 | .disabled(selectedconfig == nil) 45 | .padding() 46 | } 47 | } 48 | .padding() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/RsyncCommandtoDisplay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RsyncCommandtoDisplay.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 22.07.2017. 6 | // Copyright © 2017 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum RsyncCommand: String, CaseIterable, Identifiable, CustomStringConvertible { 12 | case synchronize_data 13 | case restore_data 14 | case verify_synchronized_data 15 | 16 | var id: String { rawValue } 17 | var description: String { rawValue.localizedCapitalized.replacingOccurrences(of: "_", with: " ") } 18 | } 19 | 20 | @MainActor 21 | struct RsyncCommandtoDisplay { 22 | var rsynccommand: String 23 | 24 | init(display: RsyncCommand, 25 | config: SynchronizeConfiguration) { 26 | var str = "" 27 | switch display { 28 | case .synchronize_data: 29 | if config.task == SharedReference.shared.halted { 30 | str = "Task is halted" 31 | } else { 32 | if let arguments = ArgumentsSynchronize(config: config).argumentsSynchronize(dryRun: true, forDisplay: true) { 33 | str = (GetfullpathforRsync().rsyncpath()) + " " + arguments.joined() 34 | } 35 | } 36 | case .restore_data: 37 | if let arguments = ArgumentsRestore(config: config, restoresnapshotbyfiles: false).argumentsrestore(dryRun: true, forDisplay: true) { 38 | str = (GetfullpathforRsync().rsyncpath()) + " " + arguments.joined() 39 | } 40 | case .verify_synchronized_data: 41 | if config.task == SharedReference.shared.halted { 42 | str = "Task is halted" 43 | } else { 44 | if let arguments = ArgumentsVerify(config: config).argumentsverify(forDisplay: true) { 45 | str = (GetfullpathforRsync().rsyncpath()) + " " + arguments.joined() 46 | } 47 | } 48 | } 49 | rsynccommand = str 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/PushPullCommandtoDisplay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PushPullCommandtoDisplay.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 07/12/2024. 6 | // 7 | 8 | import Foundation 9 | import SSHCreateKey 10 | 11 | enum PushPullCommand: String, CaseIterable, Identifiable, CustomStringConvertible { 12 | case pull_remote 13 | case push_local 14 | case none 15 | 16 | var id: String { rawValue } 17 | var description: String { rawValue.localizedCapitalized.replacingOccurrences(of: "_", with: " ") } 18 | } 19 | 20 | @MainActor 21 | struct PushPullCommandtoDisplay { 22 | var command: String 23 | 24 | init(display: PushPullCommand, 25 | config: SynchronizeConfiguration, 26 | dryRun: Bool, 27 | keepdelete: Bool) { 28 | var str = "" 29 | switch display { 30 | case .pull_remote: 31 | if config.offsiteServer.isEmpty == false, config.task == SharedReference.shared.synchronize { 32 | if let arguments = ArgumentsPullRemote(config: config).argumentspullremotewithparameters(dryRun: dryRun, forDisplay: true, keepdelete: keepdelete) { 33 | str = (GetfullpathforRsync().rsyncpath()) + " " + arguments.joined() 34 | } 35 | } else { 36 | str = NSLocalizedString("Use macOS Finder", comment: "") 37 | } 38 | case .push_local: 39 | if config.offsiteServer.isEmpty == false, config.task == SharedReference.shared.synchronize { 40 | if let arguments = ArgumentsSynchronize(config: config).argumentsforpushlocaltoremotewithparameters(dryRun: dryRun, forDisplay: true, keepdelete: keepdelete) { 41 | str = (GetfullpathforRsync().rsyncpath()) + " " + arguments.joined() 42 | } 43 | } else { 44 | str = NSLocalizedString("Use macOS Finder", comment: "") 45 | } 46 | case .none: 47 | str = NSLocalizedString("Select either pull or push", comment: "") 48 | } 49 | command = str 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /RsyncUI/Views/ProgressView/SynchronizeProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SynchronizeProgressView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 01/12/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SynchronizeProgressView: View { 11 | let max: Double 12 | let progress: Double 13 | let statusText: String 14 | 15 | var body: some View { 16 | VStack(spacing: 20) { 17 | // Circular progress indicator 18 | ZStack { 19 | Circle() 20 | .stroke( 21 | Color.gray.opacity(0.2), 22 | lineWidth: 12 23 | ) 24 | 25 | if max > 0 { 26 | Circle() 27 | .trim(from: 0, to: min(progress / max, 1.0)) 28 | .stroke( 29 | LinearGradient( 30 | colors: [.blue, .cyan], 31 | startPoint: .topLeading, 32 | endPoint: .bottomTrailing 33 | ), 34 | style: StrokeStyle( 35 | lineWidth: 12, 36 | lineCap: .round 37 | ) 38 | ) 39 | .rotationEffect(.degrees(-90)) 40 | .animation(.spring(response: 0.6, dampingFraction: 0.8), value: progress) 41 | } 42 | 43 | VStack(spacing: 4) { 44 | Text("\(Int(progress))") 45 | .font(.system(size: 36, weight: .bold, design: .rounded)) 46 | .contentTransition(.numericText(countsDown: false)) 47 | } 48 | } 49 | .frame(width: 160, height: 160) 50 | 51 | Text(statusText) 52 | .font(.headline) 53 | .foregroundStyle(.secondary) 54 | } 55 | .padding(32) 56 | .animation(.default, value: progress) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/GetfullpathforRsync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetfullpathforRsync.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 06/06/2019. 6 | // Copyright © 2019 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import OSLog 11 | 12 | @MainActor 13 | struct GetfullpathforRsync { 14 | func rsyncpath() -> String { 15 | guard SharedReference.shared.norsync == false else { return "no rsync in path" } 16 | if SharedReference.shared.rsyncversion3 { 17 | if let localrsyncpath = SharedReference.shared.localrsyncpath, 18 | localrsyncpath.isEmpty == false { 19 | Logger.process.debugMessageOnly("GetfullpathforRsync OPTIONAL path: \(localrsyncpath)") 20 | 21 | if localrsyncpath.hasPrefix("/") { 22 | return localrsyncpath + SharedReference.shared.rsync 23 | } else { 24 | return localrsyncpath.appending("/") + SharedReference.shared.rsync 25 | } 26 | } else { 27 | if SharedReference.shared.macosarm { 28 | Logger.process.debugMessageOnly("GetfullpathforRsync HOMEBREW path ARM: \(SharedReference.shared.usrlocalbinarm.appending("/"))") 29 | } else { 30 | Logger.process.debugMessageOnly("GetfullpathforRsync HOMEBREW path INTEL: \(SharedReference.shared.usrlocalbin.appending("/"))") 31 | } 32 | if SharedReference.shared.macosarm { 33 | return SharedReference.shared.usrlocalbinarm.appending("/") + SharedReference.shared.rsync 34 | } else { 35 | return SharedReference.shared.usrlocalbin.appending("/") + SharedReference.shared.rsync 36 | } 37 | } 38 | } else { 39 | Logger.process.debugMessageOnly("GetfullpathforRsync DEFAULT path: \(SharedReference.shared.usrbin.appending("/"))") 40 | return SharedReference.shared.usrbin.appending("/") + SharedReference.shared.rsync 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/ObservableSnapshotData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableSnapshotData.swift 3 | // RsyncSwiftUI 4 | // 5 | // Created by Thomas Evensen on 23/02/2021. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | import OSLog 11 | 12 | @Observable 13 | final class ObservableSnapshotData { 14 | var maxnumbertodelete: Int = 0 15 | var remainingsnapshotstodelete: Int = 0 16 | // Deleteobject 17 | var delete: DeleteSnapshots? 18 | var inprogressofdelete: Bool = false 19 | // Show progress view when getting data 20 | var snapshotlist: Bool = false 21 | // UUIDs for DELETE snapshots 22 | var snapshotuuidsfordelete = Set() 23 | var snapshotfolders: [SnapshotFolder] = [] 24 | var logrecordssnapshot: [LogRecordSnapshot]? 25 | // Originally loaded logrecords, to be used if cleaning up logrecords 26 | // with no snapshot catalogs 27 | var readlogrecordsfromfile: [LogRecords]? 28 | // UUIDs for logrecords with no snapshot catalogs 29 | var notmappedloguuids: Set? 30 | 31 | func setsnapshotdata(_ data: [LogRecordSnapshot]?) { 32 | logrecordssnapshot = data 33 | inprogressofdelete = false 34 | snapshotuuidsfordelete.removeAll() 35 | maxnumbertodelete = 0 36 | remainingsnapshotstodelete = 0 37 | notmappedloguuids = nil 38 | } 39 | 40 | func getsnapshotdata() -> [LogRecordSnapshot]? { 41 | if let logrecordssnapshot { 42 | return logrecordssnapshot.sorted { cat1, cat2 -> Bool in 43 | if let cat1 = cat1.snapshotCatalog, 44 | let cat2 = cat2.snapshotCatalog { 45 | return (Int(cat1.dropFirst(2)) ?? 0) > (Int(cat2.dropFirst(2)) ?? 0) 46 | } 47 | return false 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | deinit { 54 | Logger.process.debugMessageOnly("ObservableSnapshotData: DEINIT") 55 | } 56 | } 57 | 58 | struct SnapshotFolder: Identifiable, Equatable { 59 | let id = UUID() 60 | var folder: String 61 | } 62 | -------------------------------------------------------------------------------- /RsyncUI/Views/Detailsview/TimerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 29/12/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TimerView: View { 11 | @Environment(\.dismiss) var dismiss 12 | // Navigation path for executetasks 13 | @Binding var executetaskpath: [Tasks] 14 | 15 | @State var startDate = Date.now 16 | @State var timetosynchronize: Int = 6 17 | @State var timeosynchronizestring: String = "6" 18 | 19 | let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 20 | 21 | var body: some View { 22 | if SharedReference.shared.synchronizewithouttimedelay { 23 | Image(systemName: "figure.run") 24 | .font(.title) 25 | .imageScale(.small) 26 | .foregroundColor(.blue) 27 | .onReceive(timer) { firedDate in 28 | timetosynchronize = 1 29 | timetosynchronize -= Int(firedDate.timeIntervalSince(startDate)) 30 | if timetosynchronize < 0 { 31 | executetaskpath.removeAll() 32 | executetaskpath.append(Tasks(task: .executestimatedview)) 33 | } 34 | } 35 | .onTapGesture { 36 | dismiss() 37 | } 38 | } else { 39 | Text(timeosynchronizestring) 40 | .fontWeight(.bold) 41 | .foregroundColor(.blue) 42 | .font(.title2) 43 | .onReceive(timer) { firedDate in 44 | timetosynchronize -= Int(firedDate.timeIntervalSince(startDate)) 45 | timeosynchronizestring = String(timetosynchronize) 46 | if timetosynchronize < 0 { 47 | executetaskpath.removeAll() 48 | executetaskpath.append(Tasks(task: .executestimatedview)) 49 | } 50 | } 51 | .onTapGesture { 52 | dismiss() 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/Basic/LogRecords.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogRecords.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 02/05/16. 6 | // Copyright © 2016 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Log: Identifiable, Codable { 12 | var id = UUID() 13 | var dateExecuted: String? 14 | var resultExecuted: String? 15 | var date: Date { 16 | dateExecuted?.en_date_from_string() ?? Date() 17 | } 18 | } 19 | 20 | struct LogRecords: Identifiable, Codable { 21 | var id = UUID() 22 | var hiddenID: Int 23 | var offsiteserver: String? 24 | var dateStart: String 25 | var logrecords: [Log]? 26 | 27 | // Used when reading JSON data from store 28 | init(_ data: DecodeLogRecords) { 29 | dateStart = data.dateStart ?? "" 30 | hiddenID = data.hiddenID ?? -1 31 | offsiteserver = data.offsiteserver 32 | logrecords = data.logrecords?.map { record in 33 | Log(dateExecuted: record.dateExecuted, resultExecuted: record.resultExecuted) 34 | } 35 | } 36 | 37 | // Create an empty record with no values 38 | init() { 39 | hiddenID = -1 40 | dateStart = "" 41 | } 42 | } 43 | 44 | extension LogRecords: Hashable, Equatable { 45 | static func == (lhs: LogRecords, rhs: LogRecords) -> Bool { 46 | lhs.hiddenID == rhs.hiddenID && 47 | lhs.dateStart == rhs.dateStart && 48 | lhs.offsiteserver == rhs.offsiteserver 49 | } 50 | 51 | func hash(into hasher: inout Hasher) { 52 | hasher.combine(String(hiddenID)) 53 | hasher.combine(dateStart) 54 | hasher.combine(offsiteserver) 55 | } 56 | } 57 | 58 | extension Log: Hashable, Equatable { 59 | static func == (lhs: Log, rhs: Log) -> Bool { 60 | lhs.dateExecuted == rhs.dateExecuted && 61 | lhs.resultExecuted == rhs.resultExecuted && 62 | lhs.id == rhs.id 63 | } 64 | 65 | func hash(into hasher: inout Hasher) { 66 | hasher.combine(dateExecuted) 67 | hasher.combine(resultExecuted) 68 | hasher.combine(id) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /RsyncUI/Views/Configurations/ConfigurationsTableDataView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationsTableDataView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 03/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ConfigurationsTableDataView: View { 11 | @Binding var selecteduuids: Set 12 | 13 | let configurations: [SynchronizeConfiguration]? 14 | 15 | var body: some View { 16 | Table(configurations ?? [], selection: $selecteduuids) { 17 | TableColumn("Synchronize ID") { data in 18 | if data.parameter4.isEmpty == false { 19 | if data.backupID.isEmpty == true { 20 | Text("Synchronize ID") 21 | .foregroundColor(.red) 22 | 23 | } else { 24 | Text(data.backupID) 25 | .foregroundColor(.red) 26 | } 27 | } else { 28 | if data.backupID.isEmpty == true { 29 | Text("Synchronize ID") 30 | 31 | } else { 32 | Text(data.backupID) 33 | } 34 | } 35 | } 36 | .width(min: 90, max: 200) 37 | 38 | TableColumn("Action") { data in 39 | if data.task == SharedReference.shared.halted { 40 | Image(systemName: "stop.fill") 41 | .foregroundColor(Color(.red)) 42 | } else { 43 | Text(data.task) 44 | } 45 | } 46 | .width(max: 80) 47 | TableColumn("Source folder", value: \.localCatalog) 48 | .width(min: 80, max: 300) 49 | TableColumn("Destination folder", value: \.offsiteCatalog) 50 | .width(min: 80, max: 300) 51 | TableColumn("Server") { data in 52 | if data.offsiteServer.count > 0 { 53 | Text(data.offsiteServer) 54 | } else { 55 | Text("localhost") 56 | } 57 | } 58 | .width(min: 50, max: 90) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /RsyncUI/Views/Settings/Environmentsettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environmentsettings.swift 3 | // RsyncSwiftUI 4 | // 5 | // Created by Thomas Evensen on 03/03/2021. 6 | // 7 | 8 | import OSLog 9 | import SwiftUI 10 | 11 | struct Environmentsettings: View { 12 | @State private var environmentvalue: String = "" 13 | @State private var environment: String = "" 14 | 15 | var body: some View { 16 | Form { 17 | Section(header: Text("Rsync environment") 18 | .font(.title3) 19 | .fontWeight(.bold)) { 20 | setenvironment 21 | 22 | setenvironmenvariable 23 | } 24 | 25 | Section(header: Text("Save userconfiguration") 26 | .font(.title3) 27 | .fontWeight(.bold)) { 28 | ConditionalGlassButton( 29 | systemImage: "square.and.arrow.down", 30 | text: "Save", 31 | helpText: "Save userconfiguration" 32 | ) { 33 | _ = WriteUserConfigurationJSON(UserConfiguration()) 34 | } 35 | } 36 | } 37 | .formStyle(.grouped) 38 | } 39 | 40 | var setenvironment: some View { 41 | EditValueScheme(400, NSLocalizedString("Environment", comment: ""), $environment) 42 | .onAppear { 43 | if let environmentstring = SharedReference.shared.environment { 44 | environment = environmentstring 45 | } 46 | } 47 | .onChange(of: environment) { 48 | SharedReference.shared.environment = environment 49 | } 50 | } 51 | 52 | var setenvironmenvariable: some View { 53 | EditValueScheme(400, NSLocalizedString("Environment variable", comment: ""), $environmentvalue) 54 | .onAppear { 55 | if let environmentvaluestring = SharedReference.shared.environmentvalue { 56 | environmentvalue = environmentvaluestring 57 | } 58 | } 59 | .onChange(of: environmentvalue) { 60 | SharedReference.shared.environmentvalue = environmentvalue 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /RsyncUI/Model/Newversion/ActorGetversionofRsyncUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActorGetversionofRsyncUI.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 02/07/2025. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import OSLog 10 | 11 | actor ActorGetversionofRsyncUI { 12 | @concurrent 13 | nonisolated func getversionsofrsyncui() async -> Bool { 14 | do { 15 | let versions = DecodeGeneric() 16 | let versionsofrsyncui = 17 | try await versions.decodeArray(VersionsofRsyncUI.self, 18 | fromURL: Resources().getResource(resource: .urlJSON)) 19 | 20 | Logger.process.debugMessageOnly("CheckfornewversionofRsyncUI: \(versionsofrsyncui)") 21 | let runningversion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" 22 | let check = versionsofrsyncui.filter { runningversion.isEmpty ? true : $0.version == runningversion } 23 | if check.count > 0 { 24 | return true 25 | } else { 26 | return false 27 | } 28 | 29 | } catch { 30 | Logger.process.warning("CheckfornewversionofRsyncUI: loading data failed)") 31 | return false 32 | } 33 | } 34 | 35 | @concurrent 36 | nonisolated func downloadlinkofrsyncui() async -> String? { 37 | do { 38 | let versions = DecodeGeneric() 39 | let versionsofrsyncui = 40 | try await versions.decodeArray(VersionsofRsyncUI.self, 41 | fromURL: Resources().getResource(resource: .urlJSON)) 42 | 43 | Logger.process.debugMessageOnly("CheckfornewversionofRsyncUI: \(versionsofrsyncui)") 44 | let runningversion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" 45 | let check = versionsofrsyncui.filter { runningversion.isEmpty ? true : $0.version == runningversion } 46 | if check.count > 0 { 47 | return check[0].url 48 | } else { 49 | return nil 50 | } 51 | 52 | } catch { 53 | Logger.process.warning("CheckfornewversionofRsyncUI: loading data failed)") 54 | return nil 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/yudpsocket/Socket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) <2014>, skysent 3 | // All rights reserved. 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are met: 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 2. Redistributions in binary form must reproduce the above copyright 10 | // notice, this list of conditions and the following disclaimer in the 11 | // documentation and/or other materials provided with the distribution. 12 | // 3. All advertising materials mentioning features or use of this software 13 | // must display the following acknowledgement: 14 | // This product includes software developed by skysent. 15 | // 4. Neither the name of the skysent nor the 16 | // names of its contributors may be used to endorse or promote products 17 | // derived from this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY skysent ''AS IS'' AND ANY 20 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | // DISCLAIMED. IN NO EVENT SHALL skysent BE LIABLE FOR ANY 23 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | 31 | import Foundation 32 | 33 | public typealias Byte = UInt8 34 | 35 | open class Socket { 36 | public let address: String 37 | public internal(set) var port: Int32 38 | public internal(set) var fd: Int32? 39 | 40 | public init(address: String, port: Int32) { 41 | self.address = address 42 | self.port = port 43 | } 44 | } 45 | 46 | public enum SocketError: Error { 47 | case queryFailed 48 | case connectionClosed 49 | case connectionTimeout 50 | case unknownError 51 | } 52 | -------------------------------------------------------------------------------- /RsyncUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "7e7a4861cde0580aabda714b8f1a09918bce76cfbeb93143eb8c7bf15cc21735", 3 | "pins" : [ 4 | { 5 | "identity" : "decodeencodegeneric", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/rsyncOSX/DecodeEncodeGeneric.git", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "b5ecbbbe1b244191efec1532a979f6ae342d6617" 11 | } 12 | }, 13 | { 14 | "identity" : "parsersyncoutput", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/rsyncOSX/ParseRsyncOutput", 17 | "state" : { 18 | "branch" : "main", 19 | "revision" : "ebfa08c37dc9d8e042f1940ea9757b7b6aea0bf6" 20 | } 21 | }, 22 | { 23 | "identity" : "processcommand", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/rsyncOSX/ProcessCommand", 26 | "state" : { 27 | "branch" : "main", 28 | "revision" : "32ef8674169d796bd3ccd40a6e4416ff595fa701" 29 | } 30 | }, 31 | { 32 | "identity" : "rsyncarguments", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/rsyncOSX/RsyncArguments", 35 | "state" : { 36 | "branch" : "main", 37 | "revision" : "7c3ce0fc29a83ca65135e7136b1a4a22187789ba" 38 | } 39 | }, 40 | { 41 | "identity" : "rsyncprocess", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/rsyncOSX/RsyncProcess", 44 | "state" : { 45 | "branch" : "main", 46 | "revision" : "8dbf3f1203639707ce3e2ee345ef74bcbae4bac5" 47 | } 48 | }, 49 | { 50 | "identity" : "rsyncuideeplinks", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/rsyncOSX/RsyncUIDeepLinks", 53 | "state" : { 54 | "branch" : "main", 55 | "revision" : "32df87aae399719264e06b547dbf3b3005fff54c" 56 | } 57 | }, 58 | { 59 | "identity" : "sshcreatekey", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/rsyncOSX/SSHCreateKey.git", 62 | "state" : { 63 | "branch" : "main", 64 | "revision" : "76dcd284ef2e3101732f1c4684a4e55db8445326" 65 | } 66 | } 67 | ], 68 | "version" : 3 69 | } 70 | -------------------------------------------------------------------------------- /RsyncUI/Views/Profiles/ProfilesToUpdateView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfilesToUpdateView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 11/10/2024. 6 | // 7 | 8 | import OSLog 9 | import SwiftUI 10 | 11 | struct ProfilesToUpdateView: View { 12 | let allconfigurations: [SynchronizeConfiguration] 13 | 14 | var body: some View { 15 | Table(allconfigurations) { 16 | TableColumn("Synchronize ID : profilename") { data in 17 | let split = data.backupID.split(separator: " : ") 18 | if split.count > 1 { 19 | Text(split[0]) + Text(" : ") + Text(split[1]).foregroundColor(.blue) 20 | } else { 21 | Text(data.backupID) 22 | } 23 | } 24 | .width(min: 150, max: 300) 25 | 26 | TableColumn("Task", value: \.task) 27 | .width(max: 80) 28 | 29 | TableColumn("Min/hour/day") { data in 30 | var seconds: Double { 31 | if let date = data.dateRun { 32 | let lastbackup = date.en_date_from_string() 33 | return lastbackup.timeIntervalSinceNow * -1 34 | } else { 35 | return 0 36 | } 37 | } 38 | let color: Color = markConfig(seconds) == true ? .red : .white 39 | 40 | Text(seconds.latest()) 41 | .foregroundColor(color) 42 | } 43 | .width(max: 90) 44 | TableColumn("Date last") { data in 45 | Text(data.dateRun ?? "") 46 | } 47 | .width(max: 120) 48 | } 49 | 50 | .overlay { 51 | if allconfigurations.count == 0 { 52 | ContentUnavailableView { 53 | Label("All tasks has been synchronized in the past \(SharedReference.shared.marknumberofdayssince) days", 54 | systemImage: "play.fill") 55 | } description: { 56 | Text("This is only due to Marknumberofdayssince set in the settings") 57 | } 58 | } 59 | } 60 | } 61 | 62 | private func markConfig(_ seconds: Double) -> Bool { 63 | seconds / (60 * 60 * 24) > Double(SharedReference.shared.marknumberofdayssince) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/RsyncUIconfigurations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RsyncUIconfigurations.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 28/12/2020. 6 | // Copyright © 2020 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Observation 10 | import SwiftUI 11 | 12 | struct ProfilesnamesRecord: Identifiable, Equatable, Hashable { 13 | var profilename: String 14 | let id = UUID() 15 | 16 | init(_ name: String) { 17 | profilename = name 18 | } 19 | } 20 | 21 | @Observable @MainActor 22 | final class RsyncUIconfigurations { 23 | var configurations: [SynchronizeConfiguration]? 24 | var profile: String? 25 | // This is observed when URL actions are initiated. 26 | // Before commence the real action must be sure that selected profile data is loaded from store 27 | @ObservationIgnored var validprofiles: [ProfilesnamesRecord] = [] 28 | // Toggle sidebar 29 | var columnVisibility = NavigationSplitViewVisibility.doubleColumn 30 | // .doubleColumn or .detailOnly 31 | 32 | @ObservationIgnored var oneormoretasksissnapshot: Bool { 33 | guard SharedReference.shared.rsyncversion3 else { return false } 34 | return (configurations?.contains { $0.task == SharedReference.shared.snapshot } ?? false) 35 | } 36 | 37 | @ObservationIgnored var oneormoresnapshottasksisremote: Bool { 38 | guard SharedReference.shared.rsyncversion3 else { return false } 39 | return configurations?.filter { $0.task == SharedReference.shared.snapshot && 40 | $0.offsiteServer.isEmpty == false 41 | }.count ?? 0 > 0 42 | } 43 | 44 | @ObservationIgnored var oneormoresynchronizetasksisremoteVer3x: Bool { 45 | guard SharedReference.shared.rsyncversion3 else { return false } 46 | return configurations?.filter { $0.task == SharedReference.shared.synchronize && 47 | $0.offsiteServer.isEmpty == false 48 | }.count ?? 0 > 0 49 | } 50 | 51 | @ObservationIgnored var oneormoresynchronizetasksisremoteOpenrsync: Bool { 52 | configurations?.filter { $0.task == SharedReference.shared.synchronize && 53 | $0.offsiteServer.isEmpty == false 54 | }.count ?? 0 > 0 55 | } 56 | 57 | @ObservationIgnored var externalurlrequestinprogress: Bool = false 58 | @ObservationIgnored var executetasksinprogress: Bool = false 59 | 60 | init() {} 61 | } 62 | -------------------------------------------------------------------------------- /RsyncUI/Model/Snapshots/SnapshotRemoteCatalogs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnapshotRemoteCatalogs.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 19/09/2023. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | import RsyncProcess 11 | 12 | @MainActor 13 | final class SnapshotRemoteCatalogs { 14 | var mysnapshotdata: ObservableSnapshotData? 15 | var catalogsanddates: [SnapshotFolder]? 16 | 17 | func getremotecataloginfo(_ config: SynchronizeConfiguration) { 18 | let handlers = CreateHandlers().createHandlers( 19 | fileHandler: { _ in }, 20 | processTermination: processTermination 21 | ) 22 | 23 | let arguments = ArgumentsSnapshotRemoteCatalogs(config: config).remotefilelistarguments() 24 | let process = RsyncProcess(arguments: arguments, 25 | handlers: handlers, 26 | fileHandler: false) 27 | do { 28 | try process.executeProcess() 29 | } catch let err { 30 | let error = err 31 | SharedReference.shared.errorobject?.alert(error: error) 32 | } 33 | } 34 | 35 | @discardableResult 36 | init(config: SynchronizeConfiguration, 37 | snapshotdata: ObservableSnapshotData) { 38 | guard config.task == SharedReference.shared.snapshot else { return } 39 | mysnapshotdata = snapshotdata 40 | getremotecataloginfo(config) 41 | } 42 | 43 | func processTermination(stringoutputfromrsync: [String]?, hiddenID _: Int?) { 44 | if let stringoutputfromrsync { 45 | let catalogs = TrimOutputForRestore(stringoutputfromrsync).trimmeddata 46 | catalogsanddates = catalogs?.compactMap { line in 47 | let item = SnapshotFolder(folder: line) 48 | return (line.contains("done") == false && line.contains("receiving") == false && 49 | line.contains("sent") == false && line.contains("total") == false && 50 | line.contains("./.") == false && line.isEmpty == false && 51 | line.contains("speedup") == false && line.contains("bytes") == false) ? item : nil 52 | }.sorted { cat1, cat2 in 53 | (Int(cat1.folder.dropFirst(2)) ?? 0) > (Int(cat2.folder.dropFirst(2)) ?? 0) 54 | } 55 | } 56 | mysnapshotdata?.snapshotfolders = catalogsanddates ?? [] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/WriteSynchronizeConfigurationJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteSynchronizeConfigurationJSON.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 27/04/2021. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | 12 | @MainActor 13 | final class WriteSynchronizeConfigurationJSON { 14 | let path = Homepath() 15 | 16 | private func writeJSONToPersistentStore(jsonData: Data?, _ profile: String?) { 17 | if let fullpathmacserial = path.fullpathmacserial { 18 | var configurationfileURL: URL? 19 | let fullpathmacserialURL = URL(fileURLWithPath: fullpathmacserial) 20 | if let profile { 21 | let tempURL = fullpathmacserialURL.appendingPathComponent(profile) 22 | configurationfileURL = tempURL.appendingPathComponent(SharedConstants().fileconfigurationsjson) 23 | 24 | } else { 25 | configurationfileURL = fullpathmacserialURL.appendingPathComponent(SharedConstants().fileconfigurationsjson) 26 | } 27 | if let configurationfileURL { 28 | Logger.process.debugMessageOnly("WriteSynchronizeConfigurationJSON: writeJSONToPersistentStore \(configurationfileURL)") 29 | } 30 | if let jsonData, let configurationfileURL { 31 | do { 32 | try jsonData.write(to: configurationfileURL) 33 | } catch let err { 34 | let error = err 35 | path.propagateError(error: error) 36 | } 37 | } 38 | } 39 | } 40 | 41 | private func encodeJSONData(_ configurations: [SynchronizeConfiguration], _ profile: String?) { 42 | let encodejsondata = EncodeGeneric() 43 | do { 44 | let encodeddata = try encodejsondata.encode(configurations) 45 | writeJSONToPersistentStore(jsonData: encodeddata, profile) 46 | 47 | } catch let err { 48 | let error = err 49 | path.propagateError(error: error) 50 | } 51 | } 52 | 53 | @discardableResult 54 | init(_ profile: String?, _ configurations: [SynchronizeConfiguration]?) { 55 | if let configurations { 56 | encodeJSONData(configurations, profile) 57 | } 58 | } 59 | 60 | deinit { 61 | Logger.process.debugMessageOnly("WriteSynchronizeConfigurationJSON DEINIT") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /RsyncUI/Views/OutputViews/DetailsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailsView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 07/06/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DetailsView: View { 11 | let remotedatanumbers: RemoteDataNumbers 12 | 13 | var body: some View { 14 | HStack { 15 | VStack(alignment: .leading) { 16 | DetailsViewHeading(remotedatanumbers: remotedatanumbers) 17 | 18 | Spacer() 19 | 20 | if remotedatanumbers.datatosynchronize { 21 | VStack(alignment: .leading) { 22 | if SharedReference.shared.rsyncversion3 { 23 | Text(remotedatanumbers.newfiles_Int == 1 ? "1 new file" : "\(remotedatanumbers.newfiles_Int) new files") 24 | Text(remotedatanumbers.deletefiles_Int == 1 ? "1 file for delete" : "\(remotedatanumbers.deletefiles_Int) files for delete") 25 | } 26 | Text(remotedatanumbers.filestransferred_Int == 1 ? "1 file changed" : "\(remotedatanumbers.filestransferred_Int) files changed") 27 | Text(remotedatanumbers.totaltransferredfilessize_Int == 1 ? "byte for transfer" : "\(remotedatanumbers.totaltransferredfilessize_Int) bytes for transfer") 28 | } 29 | .padding() 30 | .foregroundStyle(.white) 31 | .background { 32 | RoundedRectangle(cornerRadius: 8) 33 | .fill(.blue.gradient) 34 | } 35 | .padding() 36 | 37 | } else { 38 | Text("No data to synchronize") 39 | .font(.title2) 40 | .padding() 41 | .foregroundStyle(.white) 42 | .background { 43 | RoundedRectangle(cornerRadius: 8) 44 | .fill(.blue.gradient) 45 | } 46 | .padding() 47 | } 48 | } 49 | 50 | Table(remotedatanumbers.outputfromrsync ?? []) { 51 | TableColumn("Output from rsync" + ": \(remotedatanumbers.outputfromrsync?.count ?? 0) rows") { data in 52 | Text(data.record) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /RsyncUI/Model/ParametersRsync/Params.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Params.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 20/11/2025. 6 | // 7 | 8 | import Foundation 9 | import RsyncArguments 10 | 11 | @MainActor 12 | struct Params { 13 | func params( 14 | config: SynchronizeConfiguration) -> Parameters { 15 | var rsyncdaemon = false 16 | if config.rsyncdaemon == 1 { rsyncdaemon = true } 17 | return Parameters( 18 | task: config.task, 19 | basicParameters: BasicRsyncParameters( 20 | archiveMode: "--archive", 21 | verboseOutput: "--verbose", 22 | compressionEnabled: "--compress", 23 | deleteExtraneous: "--delete" 24 | ), 25 | optionalParameters: OptionalRsyncParameters(parameter8: config.parameter8, 26 | parameter9: config.parameter9, 27 | parameter10: config.parameter10, 28 | parameter11: config.parameter11, 29 | parameter12: config.parameter12, 30 | parameter13: config.parameter13, 31 | parameter14: config.parameter14), 32 | 33 | sshParameters: SSHParameters( 34 | offsiteServer: config.offsiteServer, 35 | offsiteUsername: config.offsiteUsername, 36 | sshport: String(config.sshport ?? -1), 37 | sshkeypathandidentityfile: config.sshkeypathandidentityfile ?? "", 38 | sharedsshport: String(SharedReference.shared.sshport ?? -1), 39 | sharedsshkeypathandidentityfile: SharedReference.shared.sshkeypathandidentityfile, 40 | rsyncversion3: SharedReference.shared.rsyncversion3 41 | ), 42 | paths: PathConfiguration( 43 | localCatalog: config.localCatalog, 44 | offsiteCatalog: config.offsiteCatalog, 45 | sharedPathForRestore: SharedReference.shared.pathforrestore ?? "" 46 | ), 47 | snapshotNumber: config.snapshotnum, 48 | isRsyncDaemon: rsyncdaemon, // Use Bool instead of -1/1 49 | rsyncVersion3: SharedReference.shared.rsyncversion3 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/WriteLogRecordsJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteLogRecordsJSON.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 27/04/2021. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | 12 | @MainActor 13 | final class WriteLogRecordsJSON { 14 | let path = Homepath() 15 | 16 | private func writeJSONToPersistentStore(jsonData: Data?, _ profile: String?) { 17 | if let fullpathmacserial = path.fullpathmacserial { 18 | var logrecordfileURL: URL? 19 | let fullpathmacserialURL = URL(fileURLWithPath: fullpathmacserial) 20 | if let profile { 21 | let tempURL = fullpathmacserialURL.appendingPathComponent(profile) 22 | logrecordfileURL = tempURL.appendingPathComponent(SharedConstants().filenamelogrecordsjson) 23 | } else { 24 | logrecordfileURL = fullpathmacserialURL.appendingPathComponent(SharedConstants().filenamelogrecordsjson) 25 | } 26 | if let logrecordfileURL { 27 | Logger.process.debugMessageOnly("WriteLogRecordsJSON: writeJSONToPersistentStore \(logrecordfileURL)") 28 | } 29 | if let jsonData, let logrecordfileURL { 30 | do { 31 | try jsonData.write(to: logrecordfileURL) 32 | } catch let err { 33 | Logger.process.error("WriteLogRecordsJSON - \(profile ?? "default profile", privacy: .public): some ERROR writing logrecords to permanent storage") 34 | let error = err 35 | path.propagateError(error: error) 36 | } 37 | } 38 | } 39 | } 40 | 41 | private func encodeJSONData(_ logrecords: [LogRecords], _ profile: String?) { 42 | let encodejsondata = EncodeGeneric() 43 | do { 44 | let encodeddata = try encodejsondata.encode(logrecords) 45 | writeJSONToPersistentStore(jsonData: encodeddata, profile) 46 | 47 | } catch let err { 48 | let error = err 49 | path.propagateError(error: error) 50 | } 51 | } 52 | 53 | @discardableResult 54 | init(_ profile: String?, _ logrecords: [LogRecords]?) { 55 | if let logrecords { 56 | encodeJSONData(logrecords, profile) 57 | } 58 | } 59 | 60 | deinit { 61 | Logger.process.debugMessageOnly("WriteLogRecordsJSON DEINIT") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /RsyncUI/Model/ParametersRsync/ArgumentsVerify.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgumentsVerify.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 13/10/2019. 6 | // Copyright © 2019 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RsyncArguments 11 | 12 | @MainActor 13 | final class ArgumentsVerify { 14 | var config: SynchronizeConfiguration? 15 | 16 | func argumentsverify(forDisplay: Bool) -> [String]? { 17 | if let config { 18 | let params = Params().params(config: config) 19 | let rsyncparameterssynchronize = RsyncParametersSynchronize(parameters: params) 20 | 21 | switch config.task { 22 | case SharedReference.shared.synchronize: 23 | do { 24 | try rsyncparameterssynchronize.argumentsForSynchronize(forDisplay: forDisplay, 25 | verify: true, 26 | dryrun: true) 27 | return rsyncparameterssynchronize.computedArguments 28 | } catch { 29 | return nil 30 | } 31 | case SharedReference.shared.snapshot: 32 | do { 33 | try rsyncparameterssynchronize.argumentsForSynchronizeSnapshot(forDisplay: forDisplay, 34 | verify: true, 35 | dryrun: true) 36 | return rsyncparameterssynchronize.computedArguments 37 | } catch { 38 | return nil 39 | } 40 | case SharedReference.shared.syncremote: 41 | do { 42 | try rsyncparameterssynchronize.argumentsForSynchronizeRemote(forDisplay: forDisplay, 43 | verify: true, 44 | dryrun: true) 45 | return rsyncparameterssynchronize.computedArguments 46 | } catch { 47 | return nil 48 | } 49 | default: 50 | break 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | init(config: SynchronizeConfiguration?) { 57 | self.config = config 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /RsyncUI/Model/Ssh/SshKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SshKeys.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 23.04.2017. 6 | // Copyright © 2017 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | import ProcessCommand 12 | import SSHCreateKey 13 | 14 | @MainActor 15 | final class SshKeys { 16 | var command: String? 17 | var arguments: [String]? 18 | 19 | var sshcreatekey: SSHCreateKey? 20 | 21 | // Create rsa keypair 22 | func createPublicPrivateRSAKeyPair() -> Bool { 23 | let present = sshcreatekey?.validatePublicKeyPresent() 24 | if present == false { 25 | do { 26 | // If new keypath is set create it 27 | try sshcreatekey?.createSSHKeyRootPath() 28 | // Create keys 29 | arguments = try sshcreatekey?.argumentsCreateKey() 30 | // command = "/usr/bin/ssh-keygen" 31 | command = sshcreatekey?.createKeyCommand 32 | executesshcreatekeys() 33 | return true 34 | } catch { 35 | return false 36 | } 37 | 38 | } else { 39 | return false 40 | } 41 | } 42 | 43 | func validatepublickeypresent() -> Bool { 44 | sshcreatekey?.validatePublicKeyPresent() ?? false 45 | } 46 | 47 | // Execute command 48 | func executesshcreatekeys() { 49 | guard arguments != nil else { return } 50 | 51 | let handlers = CreateCommandHandlers().createcommandhandlers( 52 | processTermination: processTermination) 53 | 54 | let process = ProcessCommand(command: command, 55 | arguments: arguments, 56 | handlers: handlers) 57 | do { 58 | try process.executeProcess() 59 | } catch let err { 60 | let error = err 61 | SharedReference.shared.errorobject?.alert(error: error) 62 | } 63 | } 64 | 65 | func processTermination(stringoutputfromrsync: [String]?, _: Bool) { 66 | Task { 67 | await ActorLogToFile(command ?? "", TrimOutputFromRsync(stringoutputfromrsync ?? []).trimmeddata) 68 | } 69 | } 70 | 71 | init() { 72 | sshcreatekey = SSHCreateKey(sharedSSHPort: String(SharedReference.shared.sshport ?? -1), 73 | sharedSSHKeyPathAndIdentityFile: SharedReference.shared.sshkeypathandidentityfile) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /RsyncUI/Model/FilesAndCatalogs/CatalogForProfile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CatalogForProfile.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 17/10/2016. 6 | // Copyright © 2016 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import OSLog 11 | 12 | @MainActor 13 | struct CatalogForProfile { 14 | let path = Homepath() 15 | 16 | func createProfileCatalog(_ profile: String?) -> Bool { 17 | let fm = FileManager.default 18 | // First check if profilecatalog exists, if yes bail out 19 | if let fullpathmacserial = path.fullpathmacserial, let profile { 20 | let fullpathprofileString = fullpathmacserial.appending("/") + profile 21 | guard fm.locationExists(at: fullpathprofileString, kind: .folder) == false else { 22 | Logger.process.debugMessageOnly("CatalogProfile: profile catalog exist: \(fullpathprofileString)") 23 | return false 24 | } 25 | 26 | let fullpathmacserialURL = URL(fileURLWithPath: fullpathmacserial) 27 | let profileURL = fullpathmacserialURL.appendingPathComponent(profile) 28 | 29 | do { 30 | Logger.process.debugMessageOnly("CatalogProfile creating: \(profileURL)") 31 | try fm.createDirectory(at: profileURL, withIntermediateDirectories: true, attributes: nil) 32 | } catch let err { 33 | let error = err 34 | path.propagateError(error: error) 35 | return false 36 | } 37 | } 38 | return true 39 | } 40 | 41 | // Function for deleting profile directory 42 | func deleteProfileCatalog(_ profile: String?) -> Bool { 43 | let fm = FileManager.default 44 | if let fullpathmacserial = path.fullpathmacserial, let profile { 45 | let fullpathprofileString = fullpathmacserial.appending("/") + profile 46 | let fullpathmacserialURL = URL(fileURLWithPath: fullpathmacserial) 47 | let profileURL = fullpathmacserialURL.appendingPathComponent(profile) 48 | 49 | guard fm.locationExists(at: fullpathprofileString, kind: .folder) == true else { 50 | return false 51 | } 52 | do { 53 | try fm.removeItem(at: profileURL) 54 | } catch let err { 55 | let error = err as NSError 56 | path.propagateError(error: error) 57 | return false 58 | } 59 | } 60 | return true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /RsyncUI/Model/Schedules/SchedulesConfigurations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchedulesConfigurations.swift 3 | // Calendar 4 | // 5 | // Created by Thomas Evensen on 25/03/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ScheduleType: String, CaseIterable, Identifiable, CustomStringConvertible { 11 | case once 12 | case daily 13 | case weekly 14 | 15 | var id: String { rawValue } 16 | var description: String { rawValue.localizedCapitalized } 17 | } 18 | 19 | struct SchedulesConfigurations: Identifiable, Codable { 20 | var id = UUID() 21 | var profile: String? 22 | var dateAdded: String? 23 | var dateRun: String? 24 | var schedule: String? 25 | 26 | init(_ data: DecodeSchedules) { 27 | dateRun = data.dateRun 28 | dateAdded = data.dateAdded 29 | schedule = data.schedule 30 | profile = data.profile 31 | } 32 | 33 | init(profile: String?, dateAdded: String?, dateRun: String?, schedule: String?) { 34 | self.profile = profile 35 | self.dateAdded = dateAdded 36 | self.dateRun = dateRun 37 | self.schedule = schedule 38 | } 39 | } 40 | 41 | extension SchedulesConfigurations: Hashable, Equatable { 42 | static func == (lhs: SchedulesConfigurations, rhs: SchedulesConfigurations) -> Bool { 43 | lhs.id == rhs.id && 44 | lhs.profile == rhs.profile && 45 | lhs.dateAdded == rhs.dateAdded && 46 | lhs.dateRun == rhs.dateRun && 47 | lhs.schedule == rhs.schedule 48 | } 49 | 50 | func hash(into hasher: inout Hasher) { 51 | hasher.combine(profile) 52 | hasher.combine(dateRun) 53 | hasher.combine(dateAdded) 54 | hasher.combine(id) 55 | hasher.combine(schedule) 56 | } 57 | } 58 | 59 | struct DecodeSchedules: Codable { 60 | let dateRun: String? 61 | let dateAdded: String? 62 | let schedule: String? 63 | let profile: String? 64 | 65 | enum CodingKeys: String, CodingKey { 66 | case dateRun 67 | case dateAdded 68 | case schedule 69 | case profile 70 | } 71 | 72 | init(from decoder: Decoder) throws { 73 | let values = try decoder.container(keyedBy: CodingKeys.self) 74 | dateRun = try values.decodeIfPresent(String.self, forKey: .dateRun) 75 | dateAdded = try values.decodeIfPresent(String.self, forKey: .dateAdded) 76 | schedule = try values.decodeIfPresent(String.self, forKey: .schedule) 77 | profile = try values.decodeIfPresent(String.self, forKey: .profile) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/ObservableSSH.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableSSH.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 14/03/2021. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | import SSHCreateKey 11 | 12 | @Observable @MainActor 13 | final class ObservableSSH { 14 | // Global SSH parameters 15 | // Have to convert String -> Int before saving 16 | // Set the current value as placeholder text 17 | var sshportnumber: String = .init(SharedReference.shared.sshport ?? 22) 18 | // SSH keypath and identityfile, the settings View is picking up the current value 19 | // Set the current value as placeholder text 20 | var sshkeypathandidentityfile: String = SharedReference.shared.sshkeypathandidentityfile ?? "" 21 | var sshcreatekey: SSHCreateKey? 22 | 23 | func sshkeypath(_ keypath: String) -> Bool { 24 | guard keypath.isEmpty == false else { 25 | SharedReference.shared.sshkeypathandidentityfile = nil 26 | return false 27 | } 28 | let verified = verifysshkeypath(keypath) 29 | if verified == true { 30 | SharedReference.shared.sshkeypathandidentityfile = keypath 31 | return true 32 | } else { 33 | return false 34 | } 35 | } 36 | 37 | func setsshport(_ port: String) -> Bool { 38 | guard port.isEmpty == false else { 39 | SharedReference.shared.sshport = nil 40 | return false 41 | } 42 | let verified = verifysshport(port) 43 | if verified == true { 44 | SharedReference.shared.sshport = Int(port) 45 | return true 46 | } else { 47 | return false 48 | } 49 | } 50 | 51 | // Verify SSH keypathidentityfile 52 | func verifysshkeypath(_ keypath: String) -> Bool { 53 | guard keypath.isEmpty == false else { return false } 54 | if keypath.first != "~" { return false } 55 | let number = keypath.filter { $0 == "/" }.count 56 | guard number == 2 else { return false } 57 | return true 58 | } 59 | 60 | // Verify SSH port is a valid INT 61 | func verifysshport(_ port: String) -> Bool { 62 | guard port.isEmpty == false else { return false } 63 | if Int(port) != nil { return true } 64 | return false 65 | } 66 | 67 | init() { 68 | sshcreatekey = SSHCreateKey(sharedSSHPort: String(SharedReference.shared.sshport ?? -1), 69 | sharedSSHKeyPathAndIdentityFile: SharedReference.shared.sshkeypathandidentityfile) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/Widgets/WriteWidgetsURLStringsJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteWidgetsURLStringsJSON.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 12/02/2022. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | import RsyncUIDeepLinks 12 | 13 | @MainActor 14 | struct WriteWidgetsURLStringsJSON { 15 | let deeplinks = RsyncUIDeepLinks() 16 | // They are Sandboxed and Documents catalog, to reade the URL-strings is in a Container 17 | let estimatestringsandboxcatalog = "Library/Containers/no.blogspot.RsyncUI.WidgetEstimate/Data/Documents" 18 | 19 | private func writeJSONToPersistentStore(jsonData: Data?) { 20 | if let userHomeDirectoryPath = URL.userHomeDirectoryURLPath?.path() { 21 | let pathestimate = userHomeDirectoryPath.appending("/" + estimatestringsandboxcatalog) 22 | let fullpathURL = URL(fileURLWithPath: pathestimate) 23 | let estimatefileURL = fullpathURL.appendingPathComponent(SharedReference.shared.userconfigjson) 24 | Logger.process.debugMessageOnly("WriteWidgetsURLStringsJSON: URL-string \(estimatefileURL)") 25 | if let jsonData { 26 | do { 27 | try jsonData.write(to: estimatefileURL) 28 | } catch let err { 29 | let error = err 30 | SharedReference.shared.errorobject?.alert(error: error) 31 | } 32 | } 33 | } 34 | } 35 | 36 | private func encodeJSONData(_ urlwidgetstrings: WidgetURLstrings) { 37 | let encodejsondata = EncodeGeneric() 38 | do { 39 | let encodeddata = try encodejsondata.encode(urlwidgetstrings) 40 | writeJSONToPersistentStore(jsonData: encodeddata) 41 | Logger.process.debugMessageOnly("WriteWidgetsURLStringsJSON: Writing URL-strings to permanent storage") 42 | 43 | } catch let err { 44 | Logger.process.error("WriteWidgetsURLStringsJSON: some ERROR writing user configurations from permanent storage") 45 | let error = err 46 | SharedReference.shared.errorobject?.alert(error: error) 47 | } 48 | } 49 | 50 | @discardableResult 51 | init(_ urlwidgetstrings: WidgetURLstrings?) { 52 | if let urlwidgetstrings { 53 | do { 54 | let valid = try deeplinks.validateURLstring(urlwidgetstrings.urlstringestimate ?? "") 55 | if valid { encodeJSONData(urlwidgetstrings) } 56 | } catch let err { 57 | let error = err 58 | SharedReference.shared.errorobject?.alert(error: error) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /RsyncUI/Model/Storage/Actors/ActorReadSynchronizeConfigurationJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActorReadSynchronizeConfigurationJSON.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 19/04/2021. 6 | // 7 | 8 | import DecodeEncodeGeneric 9 | import Foundation 10 | import OSLog 11 | 12 | actor ActorReadSynchronizeConfigurationJSON { 13 | @concurrent 14 | nonisolated func readjsonfilesynchronizeconfigurations(_ profile: String?, 15 | _ rsyncversion3: Bool) async -> [SynchronizeConfiguration]? { 16 | var filename = "" 17 | let path = await Homepath() 18 | Logger.process.debugThreadOnly("ActorReadSynchronizeConfigurationJSON: readjsonfilesynchronizeconfigurations()") 19 | if let profile, let fullpathmacserial = path.fullpathmacserial { 20 | filename = fullpathmacserial.appending("/") + profile.appending("/") + SharedConstants().fileconfigurationsjson 21 | } else { 22 | if let fullpathmacserial = path.fullpathmacserial { 23 | filename = fullpathmacserial.appending("/") + SharedConstants().fileconfigurationsjson 24 | } 25 | } 26 | Logger.process.debugMessageOnly("ActorReadSynchronizeConfigurationJSON: readjsonfilesynchronizeconfigurations \(filename)") 27 | let decodeimport = DecodeGeneric() 28 | do { 29 | let data = try 30 | decodeimport.decodeArray(DecodeSynchronizeConfiguration.self, fromFile: filename) 31 | 32 | Logger.process.debugThreadOnly("ActorReadSynchronizeConfigurationJSON - \(profile ?? "default") ?? DECODE") 33 | let tasks = data.compactMap { element in 34 | // snapshot and syncremote tasks requiere version3.x of rsync 35 | if element.task == "snapshot" || element.task == "syncremote" { 36 | if rsyncversion3 { 37 | return SynchronizeConfiguration(element) 38 | } 39 | } else { 40 | return SynchronizeConfiguration(element) 41 | } 42 | return nil 43 | } 44 | 45 | return tasks 46 | 47 | } catch { 48 | Logger.process.error("ActorReadSynchronizeConfigurationJSON - \(profile ?? "default profile", privacy: .public): some ERROR reading synchronize configurations from permanent storage") 49 | } 50 | return nil 51 | } 52 | 53 | deinit { 54 | Logger.process.debugMessageOnly("ActorReadSynchronizeConfigurationJSON: DEINIT") 55 | } 56 | } 57 | 58 | @MainActor 59 | struct ReportError { 60 | func propagateError(error: Error) { 61 | SharedReference.shared.errorobject?.alert(error: error) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /RsyncUI/Model/Deeplink/DeeplinkURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeeplinkURL.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 23/12/2024. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | import RsyncUIDeepLinks 11 | 12 | // URL code 13 | @MainActor 14 | struct DeeplinkURL { 15 | let deeplinks = RsyncUIDeepLinks() 16 | 17 | func handleURL(_ url: URL) -> DeeplinkQueryItem? { 18 | do { 19 | if let components = try deeplinks.validateScheme(url) { 20 | if let deepLinkQueryItem = deeplinks.handlevalidURL(components) { 21 | return deepLinkQueryItem 22 | } else { 23 | do { 24 | try deeplinks.thrownoaction() 25 | } catch let err { 26 | let error = err 27 | SharedReference.shared.errorobject?.alert(error: error) 28 | } 29 | } 30 | } 31 | 32 | } catch let err { 33 | let error = err 34 | SharedReference.shared.errorobject?.alert(error: error) 35 | } 36 | return nil 37 | } 38 | 39 | func validateProfile(_ profile: String?, _ validprofiles: [ProfilesnamesRecord]) -> Bool { 40 | if let profile { 41 | let profiles: [String] = validprofiles.map { record in 42 | record.profilename 43 | } 44 | 45 | do { 46 | try deeplinks.validateprofile(profile, profiles) 47 | return true 48 | } catch let err { 49 | let error = err 50 | SharedReference.shared.errorobject?.alert(error: error) 51 | return false 52 | } 53 | } else { 54 | // Default profile 55 | return true 56 | } 57 | } 58 | 59 | func validateNoAction(_ queryItem: URLQueryItem?) -> Bool { 60 | do { 61 | try deeplinks.validateNoOngoingURLAction(queryItem) 62 | return true 63 | } catch let err { 64 | let error = err 65 | SharedReference.shared.errorobject?.alert(error: error) 66 | return false 67 | } 68 | } 69 | 70 | func createURLestimateandsynchronize(valueprofile: String?) -> URL? { 71 | let host = Deeplinknavigation.loadprofileandestimate.rawValue 72 | var adjustedvalueprofile = valueprofile 73 | if valueprofile == nil { 74 | adjustedvalueprofile = "Default" 75 | } 76 | let queryitems: [URLQueryItem] = [URLQueryItem(name: "profile", value: adjustedvalueprofile)] 77 | if let url = deeplinks.createURL(host, queryitems) { 78 | return url 79 | } else { 80 | return nil 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /RsyncUI/Views/LogView/LogfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogfileView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 25/11/2023. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | import SwiftUI 11 | 12 | struct LogfileView: View { 13 | @State private var logfilerecords: [LogfileRecords]? 14 | @State private var whichlogfileispresented: LogfileToReset = .RsyncUIlogfile 15 | 16 | var body: some View { 17 | VStack { 18 | Table(logfilerecords ?? []) { 19 | TableColumn("Logfile") { data in 20 | Text(data.line) 21 | } 22 | } 23 | 24 | Spacer() 25 | 26 | HStack { 27 | ConditionalGlassButton( 28 | systemImage: "document", 29 | text: "Logfile", 30 | helpText: "View logfile" 31 | ) { 32 | Task { 33 | whichlogfileispresented = .RsyncUIlogfile 34 | logfilerecords = await ActorCreateOutputforView().createaoutputlogfileforview() 35 | } 36 | } 37 | 38 | ConditionalGlassButton( 39 | systemImage: "square.and.arrow.down.badge.checkmark", 40 | text: "rsync", 41 | helpText: "View rsync output" 42 | ) { 43 | Task { 44 | whichlogfileispresented = .RsyncOutputlogfile 45 | logfilerecords = await ActorCreateOutputforView().createaoutputrsynclogforview() 46 | } 47 | } 48 | 49 | Spacer() 50 | 51 | ConditionalGlassButton( 52 | systemImage: "trash", 53 | text: "Clear", 54 | helpText: "Reset logfile" 55 | ) { 56 | reset() 57 | } 58 | } 59 | } 60 | .padding() 61 | .task { 62 | logfilerecords = await ActorCreateOutputforView().createaoutputlogfileforview() 63 | } 64 | } 65 | 66 | func reset() { 67 | Task { 68 | await ActorLogToFile(whichlogfileispresented) 69 | switch whichlogfileispresented { 70 | case .RsyncOutputlogfile: 71 | logfilerecords = await ActorCreateOutputforView().createaoutputrsynclogforview() 72 | case .RsyncUIlogfile: 73 | logfilerecords = await ActorCreateOutputforView().createaoutputlogfileforview() 74 | } 75 | } 76 | } 77 | } 78 | 79 | struct LogfileRecords: Identifiable { 80 | let id = UUID() 81 | var line: String 82 | } 83 | 84 | @Observable @MainActor 85 | final class Logfileview { 86 | var output: [LogfileRecords]? 87 | } 88 | -------------------------------------------------------------------------------- /RsyncUI/Views/Detailsview/EstimateTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EstimateTableView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 01/11/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EstimateTableView: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | 13 | @Bindable var progressdetails: ProgressDetails 14 | let estimatinguuid: SynchronizeConfiguration.ID 15 | let configurations: [SynchronizeConfiguration] 16 | 17 | var body: some View { 18 | Table(configurations) { 19 | TableColumn("Synchronize ID") { data in 20 | if data.id == estimatinguuid { 21 | HStack { 22 | Image(systemName: "arrowshape.right.fill") 23 | .foregroundColor(Color(.blue)) 24 | 25 | if data.backupID.isEmpty == true { 26 | Text("Synchronize ID") 27 | .foregroundColor(color(uuid: data.id)) 28 | 29 | } else { 30 | Text(data.backupID) 31 | .foregroundColor(color(uuid: data.id)) 32 | } 33 | } 34 | } else { 35 | if data.backupID.isEmpty == true { 36 | Text("Synchronize ID") 37 | .foregroundColor(color(uuid: data.id)) 38 | 39 | } else { 40 | Text(data.backupID) 41 | .foregroundColor(color(uuid: data.id)) 42 | } 43 | } 44 | } 45 | .width(min: 50, max: 150) 46 | TableColumn("Action") { data in 47 | if data.task == SharedReference.shared.halted { 48 | Image(systemName: "stop.fill") 49 | .foregroundColor(Color(.red)) 50 | } else { 51 | Text(data.task) 52 | } 53 | } 54 | .width(max: 80) 55 | TableColumn("Source folder", value: \.localCatalog) 56 | .width(min: 80, max: 300) 57 | TableColumn("Destination folder", value: \.offsiteCatalog) 58 | .width(min: 80, max: 300) 59 | TableColumn("Server") { data in 60 | if data.offsiteServer.count > 0 { 61 | Text(data.offsiteServer) 62 | } else { 63 | Text("localhost") 64 | } 65 | } 66 | .width(min: 50, max: 90) 67 | } 68 | } 69 | 70 | func color(uuid: UUID) -> Color { 71 | let filter = progressdetails.estimatedlist?.filter { 72 | $0.id == uuid 73 | } 74 | return filter?.isEmpty == false ? .blue : (colorScheme == .dark ? .white : .black) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /RsyncUI/Model/Utils/SetandValidatepathforrsync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetandValidatepathforrsync.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 06/06/2019. 6 | // Copyright © 2019 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Validatedrsync: LocalizedError { 12 | case norsync 13 | case noversion3inusrbin 14 | 15 | var errorDescription: String? { 16 | switch self { 17 | case .norsync: 18 | "No rsync in path" 19 | case .noversion3inusrbin: 20 | "No ver3 of rsync in /usr/bin" 21 | } 22 | } 23 | } 24 | 25 | @MainActor 26 | struct SetandValidatepathforrsync { 27 | // Validate if LOCAL path for rsync is set 28 | func validateLocalPathForRsync() throws { 29 | let fm = FileManager.default 30 | SharedReference.shared.norsync = false 31 | var rsyncpath: String? 32 | // First check if a local path is set or use default values 33 | // Only validate path if rsyncversion is true, set default values else 34 | if let pathforrsync = SharedReference.shared.localrsyncpath { 35 | switch SharedReference.shared.rsyncversion3 { 36 | case true: 37 | rsyncpath = pathforrsync + SharedReference.shared.rsync 38 | // Check that version rsync 3 is not set to /usr/bin - throw if true 39 | guard SharedReference.shared.localrsyncpath != (SharedReference.shared.usrbin.appending("/")) else { 40 | throw Validatedrsync.noversion3inusrbin 41 | } 42 | if fm.isExecutableFile(atPath: rsyncpath ?? "") == false { 43 | SharedReference.shared.norsync = true 44 | // Throwing no valid rsync in path 45 | throw Validatedrsync.norsync 46 | } 47 | return 48 | case false: 49 | return 50 | } 51 | } else { 52 | return 53 | } 54 | } 55 | 56 | func setlocalrsyncpath(_ path: String) { 57 | var path = path 58 | if path.isEmpty == false { 59 | if path.hasSuffix("/") == false { 60 | path += "/" 61 | SharedReference.shared.localrsyncpath = path 62 | } else { 63 | SharedReference.shared.localrsyncpath = path 64 | } 65 | } else { 66 | SharedReference.shared.localrsyncpath = nil 67 | } 68 | } 69 | 70 | func getpathforrsync(_ rsyncversion3: Bool) -> String { 71 | if rsyncversion3 == true { 72 | if SharedReference.shared.macosarm { 73 | if SharedReference.shared.localrsyncpath == nil { 74 | return SharedReference.shared.usrlocalbinarm 75 | } 76 | } else if SharedReference.shared.localrsyncpath == nil { 77 | return SharedReference.shared.usrlocalbin 78 | } 79 | } 80 | return SharedReference.shared.usrbin 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /RsyncUI/Views/Settings/SidebarSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarSettingsView.swift 3 | // RsyncSwiftUI 4 | // 5 | // Created by Thomas Evensen on 01/02/2021. 6 | // 7 | 8 | import Observation 9 | import SwiftUI 10 | 11 | enum SideSettingsbaritems: String, Identifiable, CaseIterable { 12 | case rsync_and_path, log, ssh, environment, about 13 | var id: String { rawValue } 14 | } 15 | 16 | struct SidebarSettingsView: View { 17 | @State private var selectedsetting: SideSettingsbaritems = .rsync_and_path 18 | 19 | var body: some View { 20 | NavigationSplitView { 21 | Divider() 22 | 23 | List(SideSettingsbaritems.allCases, selection: $selectedsetting) { item in 24 | SettingsNavigationLinkWithHover(item: item, selectedview: $selectedsetting) 25 | } 26 | .listStyle(.sidebar) 27 | .toolbar(removing: .sidebarToggle) 28 | 29 | } detail: { 30 | settingsView(selectedsetting) 31 | } 32 | .frame(minWidth: 300, minHeight: 600) 33 | .navigationTitle("RsyncUI settings") 34 | } 35 | 36 | @MainActor @ViewBuilder 37 | func settingsView(_ view: SideSettingsbaritems) -> some View { 38 | switch view { 39 | case .rsync_and_path: 40 | RsyncandPathsettings() 41 | case .log: 42 | Logsettings() 43 | case .ssh: 44 | Sshsettings() 45 | case .environment: 46 | Environmentsettings() 47 | case .about: 48 | AboutView() 49 | } 50 | } 51 | } 52 | 53 | struct SidebarSettingsRow: View { 54 | var sidebaritem: SideSettingsbaritems 55 | 56 | var body: some View { 57 | Label(sidebaritem.rawValue.localizedCapitalized.replacingOccurrences(of: "_", with: " "), 58 | systemImage: systemImage(sidebaritem)) 59 | } 60 | 61 | func systemImage(_ view: SideSettingsbaritems) -> String { 62 | switch view { 63 | case .rsync_and_path: 64 | "gear" 65 | case .log: 66 | "network" 67 | case .ssh: 68 | "terminal" 69 | case .environment: 70 | "gear" 71 | case .about: 72 | "info.circle.fill" 73 | } 74 | } 75 | } 76 | 77 | struct SettingsNavigationLinkWithHover: View { 78 | let item: SideSettingsbaritems // Replace with your actual item type 79 | @Binding var selectedview: SideSettingsbaritems // Replace with your selection type 80 | @State private var isHovered = false 81 | 82 | var body: some View { 83 | NavigationLink(value: item) { 84 | SidebarSettingsRow(sidebaritem: item) 85 | } 86 | .listRowBackground( 87 | RoundedRectangle(cornerRadius: 10) 88 | .fill(isHovered ? Color.blue.opacity(0.2) : Color.clear) 89 | .padding(.horizontal, 10) 90 | ) 91 | .listRowInsets(EdgeInsets()) 92 | .onHover { hovering in 93 | withAnimation(.easeInOut(duration: 0.15)) { 94 | isHovered = hovering 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /RsyncUI/Model/Global/ObservableRsyncPathSetting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableRsyncPathSetting.swift 3 | // RsyncSwiftUI 4 | // 5 | // Created by Thomas Evensen on 16/02/2021. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | import OSLog 11 | 12 | @Observable @MainActor 13 | final class ObservableRsyncPathSetting { 14 | // True if version 3.1.2 or 3.1.3 of rsync in /usr/local/bin 15 | var rsyncversion3: Bool = SharedReference.shared.rsyncversion3 16 | // Optional path to rsync, the settings View is picking up the current value 17 | // Set the current value as placeholder text 18 | var localrsyncpath: String = "" 19 | // No valid rsyncPath - true if no valid rsync is found 20 | var norsync: Bool = false 21 | // Temporary path for restore, the settings View is picking up the current value 22 | // Set the current value as placeholder text 23 | var temporarypathforrestore: String = "" 24 | // Mark number of days since last backup 25 | var marknumberofdayssince = String(SharedReference.shared.marknumberofdayssince) 26 | // True if on ARM based Mac 27 | var macosarm: Bool = SharedReference.shared.macosarm 28 | 29 | // Used for mark local path red or white 30 | func verifypathforrsync(_ path: String) -> Bool { 31 | let fm = FileManager.default 32 | switch SharedReference.shared.rsyncversion3 { 33 | case true: 34 | let rsyncpath = path.appending("/") + SharedReference.shared.rsync 35 | if fm.isExecutableFile(atPath: rsyncpath) == false { 36 | return false 37 | } else { 38 | return true 39 | } 40 | case false: 41 | return false 42 | } 43 | } 44 | 45 | func verifyPathForRestore(_ path: String) -> Bool { 46 | let fm = FileManager.default 47 | return fm.fileExists(atPath: path, isDirectory: nil) 48 | } 49 | 50 | // Only validate path if rsyncver3 is true 51 | func setandvalidatepathforrsync(_ path: String) -> Bool { 52 | guard path.isEmpty == false, rsyncversion3 == true else { 53 | // Set rsync path = nil 54 | let validate = SetandValidatepathforrsync() 55 | validate.setlocalrsyncpath("") 56 | return false 57 | } 58 | let validate = SetandValidatepathforrsync() 59 | validate.setlocalrsyncpath(path) 60 | do { 61 | try validate.validateLocalPathForRsync() 62 | return true 63 | } catch { 64 | SharedReference.shared.rsyncversionshort = "No valid rsync detected" 65 | return false 66 | } 67 | } 68 | 69 | func verifystringtoint(_ days: String) -> Bool { 70 | if Int(days) != nil { 71 | true 72 | } else { 73 | false 74 | } 75 | } 76 | 77 | func markdays(days: String) { 78 | let verified = verifystringtoint(days) 79 | if verified { 80 | SharedReference.shared.marknumberofdayssince = Int(days) ?? 5 81 | } else { 82 | SharedReference.shared.marknumberofdayssince = 5 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /RsyncUI/Views/Detailsview/EstimationInProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EstimationInProgressView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 19/12/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EstimationInProgressView: View { 11 | @Bindable var progressdetails: ProgressDetails 12 | @Binding var selecteduuids: Set 13 | // Focus buttons from the menu 14 | @State private var focusaborttask: Bool = false 15 | 16 | let profile: String? 17 | let configurations: [SynchronizeConfiguration] 18 | 19 | var body: some View { 20 | ZStack { 21 | if let uuid = getUUID(uuid: progressdetails.configurationtobestimated) { 22 | EstimateTableView(progressdetails: progressdetails, 23 | estimatinguuid: uuid, 24 | configurations: configurations) 25 | } 26 | 27 | VStack { 28 | Spacer() 29 | 30 | if configurations.count == 1 || selecteduuids.count == 1 { 31 | progressviewonetaskonly 32 | } else { 33 | progressviewestimation 34 | } 35 | } 36 | 37 | if focusaborttask { labelaborttask } 38 | } 39 | .focusedSceneValue(\.aborttask, $focusaborttask) 40 | .frame(maxWidth: .infinity) 41 | } 42 | 43 | var progressviewestimation: some View { 44 | ProgressView("", 45 | value: progressdetails.numberofconfigurationsestimated, 46 | total: Double(progressdetails.numberofconfigurations)) 47 | .onAppear { 48 | // Either is there some selceted tasks or if not 49 | // the EstimateTasks selects all tasks to be estimated 50 | 51 | Estimate(profile: profile, 52 | configurations: configurations, 53 | selecteduuids: selecteduuids, 54 | progressdetails: progressdetails) 55 | } 56 | .progressViewStyle(.circular) 57 | } 58 | 59 | var progressviewonetaskonly: some View { 60 | ProgressView() 61 | .onAppear { 62 | // Either is there some selceted tasks or if not 63 | // the EstimateTasks selects all tasks to be estimated 64 | Estimate(profile: profile, 65 | configurations: configurations, 66 | selecteduuids: selecteduuids, 67 | progressdetails: progressdetails) 68 | } 69 | } 70 | 71 | var labelaborttask: some View { 72 | Label("", systemImage: "play.fill") 73 | .onAppear { 74 | focusaborttask = false 75 | abort() 76 | } 77 | } 78 | 79 | func getUUID(uuid: UUID?) -> SynchronizeConfiguration.ID? { 80 | if let index = configurations.firstIndex(where: { $0.id == uuid }) { 81 | return configurations[index].id 82 | } 83 | return nil 84 | } 85 | 86 | func abort() { 87 | InterruptProcess() 88 | progressdetails.resetCounts() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /RsyncUI/Model/Process/Rsyncversion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rsyncversion.swift 3 | // RsyncSwiftUI 4 | // 5 | // Created by Thomas Evensen on 18/01/2021. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | import OSLog 11 | import RsyncProcess 12 | 13 | @Observable @MainActor 14 | final class Rsyncversion { 15 | func getRsyncVersion() { 16 | let handlers = CreateHandlers().createHandlers( 17 | fileHandler: { _ in }, 18 | processTermination: processTermination 19 | ) 20 | 21 | do { 22 | try SetandValidatepathforrsync().validateLocalPathForRsync() 23 | } catch { 24 | SharedReference.shared.norsync = true 25 | SharedReference.shared.rsyncversionshort = "No valid rsync deteced" 26 | } 27 | if SharedReference.shared.norsync == false { 28 | let process = RsyncProcess(arguments: ["--version"], 29 | handlers: handlers, 30 | fileHandler: false) 31 | do { 32 | try process.executeProcess() 33 | } catch let err { 34 | let error = err 35 | SharedReference.shared.errorobject?.alert(error: error) 36 | } 37 | } 38 | } 39 | 40 | init() { 41 | let silicon = ProcessInfo().machineHardwareName?.contains("arm64") ?? false 42 | if silicon { 43 | SharedReference.shared.macosarm = true 44 | } else { 45 | SharedReference.shared.macosarm = false 46 | } 47 | } 48 | } 49 | 50 | extension Rsyncversion { 51 | func processTermination(stringoutputfromrsync: [String]?, hiddenID _: Int?) { 52 | guard stringoutputfromrsync?.count ?? 0 > 0 else { return } 53 | if let rsyncversionshort = stringoutputfromrsync?[0] { 54 | let s = rsyncversionshort.replacingOccurrences(of: "protocol", with: "\nprotocol") 55 | let result = s.replacingOccurrences(of: "(?s)Web site.*", with: "", options: .regularExpression) 56 | SharedReference.shared.rsyncversionshort = result 57 | 58 | if rsyncversionshort.contains("version 3.") { 59 | SharedReference.shared.rsyncversion3 = true 60 | Logger.process.debugMessageOnly("Rsyncversion: version 3.x of rsync discovered") 61 | } else { 62 | SharedReference.shared.rsyncversion3 = false 63 | Logger.process.debugMessageOnly("Rsyncversion: default openrsync discovered") 64 | } 65 | } 66 | } 67 | } 68 | 69 | extension ProcessInfo { 70 | /// Returns a `String` representing the machine hardware name or nil if there was an error invoking `uname(_:)` 71 | /// or decoding the response. Return value is the equivalent to running `$ uname -m` in shell. 72 | var machineHardwareName: String? { 73 | var sysinfo = utsname() 74 | let result = uname(&sysinfo) 75 | guard result == EXIT_SUCCESS else { return nil } 76 | let data = Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)) 77 | guard let identifier = String(bytes: data, encoding: .ascii) else { return nil } 78 | return identifier.trimmingCharacters(in: .controlCharacters) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /RsyncUI/Views/OutputViews/DetailsViewHeading.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailsViewHeading.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 20/11/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DetailsViewHeading: View { 11 | let remotedatanumbers: RemoteDataNumbers 12 | 13 | var body: some View { 14 | HStack { 15 | VStack(alignment: .leading) { 16 | VStack(alignment: .leading) { 17 | LabeledContent("Synchronize ID: ") { 18 | if remotedatanumbers.backupID.count == 0 { 19 | Text("Synchronize ID") 20 | .foregroundColor(.blue) 21 | } else { 22 | Text(remotedatanumbers.backupID) 23 | .foregroundColor(.blue) 24 | } 25 | } 26 | .padding(-3) 27 | 28 | LabeledContent("Task: ") { 29 | Text(remotedatanumbers.task) 30 | .foregroundColor(.blue) 31 | } 32 | .padding(-3) 33 | 34 | LabeledContent("Source folder: ") { 35 | Text(remotedatanumbers.localCatalog) 36 | .foregroundColor(.blue) 37 | } 38 | .padding(-3) 39 | 40 | LabeledContent("Destination folder: ") { 41 | Text(remotedatanumbers.offsiteCatalog) 42 | .foregroundColor(.blue) 43 | } 44 | .padding(-3) 45 | 46 | LabeledContent("Server: ") { 47 | if remotedatanumbers.offsiteServer.count == 0 { 48 | Text("localhost") 49 | .foregroundColor(.blue) 50 | } else { 51 | Text(remotedatanumbers.offsiteServer) 52 | .foregroundColor(.blue) 53 | } 54 | } 55 | .padding(-3) 56 | } 57 | .padding() 58 | 59 | VStack(alignment: .leading) { 60 | LabeledContent("Total number of files: ") { 61 | Text(remotedatanumbers.numberoffiles) 62 | .foregroundColor(.blue) 63 | } 64 | .padding(-3) 65 | 66 | LabeledContent("Total number of catalogs: ") { 67 | Text(remotedatanumbers.totaldirectories) 68 | .foregroundColor(.blue) 69 | } 70 | .padding(-3) 71 | 72 | LabeledContent("Total numbers: ") { 73 | Text(remotedatanumbers.totalnumbers) 74 | .foregroundColor(.blue) 75 | } 76 | .padding(-3) 77 | 78 | LabeledContent("Total bytes: ") { 79 | Text(remotedatanumbers.totalfilesize) 80 | .foregroundColor(.blue) 81 | } 82 | .padding(-3) 83 | } 84 | .padding() 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /RsyncUI/Views/Settings/Sshsettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sshsettings.swift 3 | // RsyncSwiftUI 4 | // 5 | // Created by Thomas Evensen on 10/02/2021. 6 | // 7 | 8 | import OSLog 9 | import SwiftUI 10 | 11 | struct Sshsettings: View { 12 | @State private var sshsettings = ObservableSSH() 13 | @State private var localsshkeys: Bool = SshKeys().validatepublickeypresent() 14 | // Show keys are created 15 | @State private var showsshkeyiscreated: Bool = false 16 | 17 | var body: some View { 18 | Form { 19 | Section(header: Text("Global ssh-keys") 20 | .font(.title3) 21 | .fontWeight(.bold)) { 22 | VStack(alignment: .leading) { 23 | ToggleViewDefault(text: NSLocalizedString("Public ssh-key is present", comment: ""), 24 | binding: $localsshkeys) 25 | .disabled(true) 26 | } 27 | } 28 | 29 | Section(header: Text("Global ssh-keypath and ssh-port") 30 | .font(.title3) 31 | .fontWeight(.bold)) { 32 | EditValueErrorScheme(400, NSLocalizedString("Global ssh-keypath and identityfile", comment: ""), $sshsettings.sshkeypathandidentityfile, 33 | sshsettings.sshkeypath(sshsettings.sshkeypathandidentityfile)) 34 | 35 | EditValueErrorScheme(400, NSLocalizedString("Global ssh-port", comment: ""), 36 | $sshsettings.sshportnumber, sshsettings.setsshport(sshsettings.sshportnumber)) 37 | } 38 | 39 | Section(header: Text("Save userconfiguration") 40 | .font(.title3) 41 | .fontWeight(.bold)) { 42 | ConditionalGlassButton( 43 | systemImage: "square.and.arrow.down", 44 | text: "Save", 45 | helpText: "Save userconfiguration" 46 | ) { 47 | _ = WriteUserConfigurationJSON(UserConfiguration()) 48 | } 49 | } 50 | 51 | if localsshkeys == false { 52 | Section(header: Text("SSH keys") 53 | .font(.title3) 54 | .fontWeight(.bold)) { 55 | HStack { 56 | Button { 57 | createKeys() 58 | } label: { 59 | Image(systemName: "key") 60 | } 61 | .help("Create keys") 62 | .buttonStyle(.borderedProminent) 63 | } 64 | } 65 | } 66 | 67 | if showsshkeyiscreated { DismissafterMessageView(dismissafter: 2, mytext: NSLocalizedString("ssh-key is created, see logfile.", comment: "")) } 68 | } 69 | .formStyle(.grouped) 70 | } 71 | } 72 | 73 | extension Sshsettings { 74 | func createKeys() { 75 | if SshKeys().createPublicPrivateRSAKeyPair() { 76 | Task { 77 | try await Task.sleep(seconds: 1) 78 | localsshkeys = SshKeys().validatepublickeypresent() 79 | showsshkeyiscreated = true 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /RsyncUI/Main/RsyncUIView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RsyncUIView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 17/06/2021. 6 | // 7 | 8 | import OSLog 9 | import RsyncProcess 10 | import SwiftUI 11 | 12 | struct RsyncUIView: View { 13 | // Selected profile 14 | @State private var selectedprofileID: ProfilesnamesRecord.ID? 15 | // Set version of rsync to use 16 | @State private var rsyncversion = Rsyncversion() 17 | @State private var start: Bool = true 18 | @State private var rsyncUIdata = RsyncUIconfigurations() 19 | 20 | var body: some View { 21 | VStack { 22 | if start { 23 | VStack { 24 | Text("RsyncUI a GUI for rsync") 25 | .font(.largeTitle) 26 | Text("https://rsyncui.netlify.app") 27 | .font(.title2) 28 | } 29 | .onAppear { 30 | Task { 31 | try await Task.sleep(seconds: 1) 32 | start = false 33 | } 34 | } 35 | } else { 36 | SidebarMainView(rsyncUIdata: rsyncUIdata, 37 | selectedprofileID: $selectedprofileID, 38 | errorhandling: errorhandling) 39 | } 40 | } 41 | .padding() 42 | .task { 43 | ReadUserConfigurationJSON().readuserconfiguration() 44 | // Get version of rsync 45 | rsyncversion.getRsyncVersion() 46 | rsyncUIdata.executetasksinprogress = false 47 | // Load valid profilenames 48 | let catalognames = Homepath().getFullPathMacSerialCatalogsAsStringNames() 49 | rsyncUIdata.validprofiles = catalognames.map { catalog in 50 | ProfilesnamesRecord(catalog) 51 | } 52 | } 53 | .task(id: selectedprofileID) { 54 | var profile: String? 55 | // Only for external URL 56 | guard rsyncUIdata.externalurlrequestinprogress == false else { 57 | rsyncUIdata.externalurlrequestinprogress = false 58 | return 59 | } 60 | 61 | if let index = rsyncUIdata.validprofiles.firstIndex(where: { $0.id == selectedprofileID }) { 62 | rsyncUIdata.profile = rsyncUIdata.validprofiles[index].profilename 63 | profile = rsyncUIdata.validprofiles[index].profilename 64 | } else { 65 | rsyncUIdata.profile = nil 66 | profile = nil 67 | } 68 | 69 | rsyncUIdata.profile = profile 70 | rsyncUIdata.executetasksinprogress = false 71 | 72 | rsyncUIdata.configurations = await ActorReadSynchronizeConfigurationJSON() 73 | .readjsonfilesynchronizeconfigurations(profile, 74 | SharedReference.shared.rsyncversion3) 75 | } 76 | } 77 | 78 | var errorhandling: AlertError { 79 | SharedReference.shared.errorobject = AlertError() 80 | return SharedReference.shared.errorobject ?? AlertError() 81 | } 82 | } 83 | 84 | extension Task where Success == Never, Failure == Never { 85 | static func sleep(seconds: Double) async throws { 86 | let duration = UInt64(seconds * 1_000_000_000) 87 | try await Task.sleep(nanoseconds: duration) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /RsyncUI/Model/Snapshots/DeleteSnapshots.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteSnapshots.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 11/05/2021. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | import ProcessCommand 11 | 12 | @MainActor 13 | final class DeleteSnapshots { 14 | var localeconfig: SynchronizeConfiguration? 15 | var snapshotcatalogstodelete: [String]? 16 | var mysnapshotdata: ObservableSnapshotData? 17 | 18 | private func preparesnapshotcatalogsfordelete(logrecordssnapshot: [LogRecordSnapshot]?) { 19 | if let uuidsfordelete = mysnapshotdata?.snapshotuuidsfordelete, let logrecordssnapshot { 20 | snapshotcatalogstodelete = logrecordssnapshot.compactMap { record in 21 | let snaproot = localeconfig?.offsiteCatalog 22 | let snapcatalog = record.snapshotCatalog 23 | let pathfordelete = (snaproot ?? "") + (snapcatalog ?? "").dropFirst(2) 24 | return (uuidsfordelete.contains(record.id)) ? pathfordelete : nil 25 | } 26 | } 27 | // Set maxnumber and remaining to delete 28 | mysnapshotdata?.maxnumbertodelete = snapshotcatalogstodelete?.count ?? 0 29 | mysnapshotdata?.remainingsnapshotstodelete = snapshotcatalogstodelete?.count ?? 0 30 | } 31 | 32 | func deletesnapshots() { 33 | guard (snapshotcatalogstodelete?.count ?? 0) > 0 else { 34 | mysnapshotdata?.inprogressofdelete = false 35 | return 36 | } 37 | if let remotecatalog = snapshotcatalogstodelete?[0] { 38 | Logger.process.debugMessageOnly("DeleteSnapshots: deleting snapshot catalog \(remotecatalog)") 39 | snapshotcatalogstodelete?.remove(at: 0) 40 | if (snapshotcatalogstodelete?.count ?? 0) == 0 { 41 | snapshotcatalogstodelete = nil 42 | } 43 | // Remaining number to delete 44 | let remaining = snapshotcatalogstodelete?.count ?? 0 45 | mysnapshotdata?.remainingsnapshotstodelete = (mysnapshotdata?.maxnumbertodelete ?? 0) - remaining 46 | if let config = localeconfig { 47 | let handlers = CreateCommandHandlers().createcommandhandlers( 48 | processTermination: processTermination) 49 | 50 | let delete = ArgumentsSnapshotDeleteCatalogs(config: config, remotecatalog: remotecatalog) 51 | let process = ProcessCommand(command: delete.getCommand(), 52 | arguments: delete.getArguments(), 53 | handlers: handlers) 54 | do { 55 | try process.executeProcess() 56 | } catch let err { 57 | let error = err 58 | SharedReference.shared.errorobject?.alert(error: error) 59 | } 60 | } 61 | } 62 | } 63 | 64 | init(config: SynchronizeConfiguration, 65 | snapshotdata: ObservableSnapshotData, 66 | logrecordssnapshot: [LogRecordSnapshot]?) { 67 | guard config.task == SharedReference.shared.snapshot else { return } 68 | localeconfig = config 69 | mysnapshotdata = snapshotdata 70 | preparesnapshotcatalogsfordelete(logrecordssnapshot: logrecordssnapshot) 71 | } 72 | } 73 | 74 | extension DeleteSnapshots { 75 | func processTermination(data _: [String]?, _: Bool) { 76 | deletesnapshots() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /RsyncUI/Views/Tasks/ExecuteNoEstTasksView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExecuteNoEstTasksView.swift 3 | // RsyncUI 4 | // 5 | // Created by Thomas Evensen on 11/11/2023. 6 | // 7 | 8 | import OSLog 9 | import SwiftUI 10 | 11 | struct ExecuteNoEstTasksView: View { 12 | @Bindable var rsyncUIdata: RsyncUIconfigurations 13 | @Binding var selecteduuids: Set 14 | // Navigation path for executetasks 15 | @Binding var executetaskpath: [Tasks] 16 | 17 | @State private var noestprogressdetails = NoEstProgressDetails() 18 | @State private var progressviewshowinfo: Bool = true 19 | @State private var focusaborttask: Bool = false 20 | 21 | @State private var progress: Int = 0 22 | 23 | var body: some View { 24 | ZStack { 25 | ConfigurationsTableDataView(selecteduuids: $selecteduuids, 26 | configurations: rsyncUIdata.configurations) 27 | 28 | if progressviewshowinfo { 29 | VStack { 30 | ProgressView() 31 | 32 | Text("\(Int(progress))") 33 | .font(.title2) 34 | .contentTransition(.numericText(countsDown: false)) 35 | .animation(.default, value: progress) 36 | } 37 | } 38 | if focusaborttask { labelaborttask } 39 | } 40 | .onAppear { 41 | executeAllNoEstimationTasks() 42 | } 43 | .onDisappear { 44 | if SharedReference.shared.process != nil { 45 | InterruptProcess() 46 | } 47 | } 48 | .focusedSceneValue(\.aborttask, $focusaborttask) 49 | .toolbar(content: { 50 | ToolbarItem { 51 | Button { 52 | abort() 53 | } label: { 54 | Image(systemName: "stop.fill") 55 | } 56 | .help("Abort (⌘K)") 57 | } 58 | }) 59 | } 60 | 61 | var labelaborttask: some View { 62 | Label("", systemImage: "play.fill") 63 | .onAppear { 64 | focusaborttask = false 65 | abort() 66 | } 67 | } 68 | } 69 | 70 | extension ExecuteNoEstTasksView { 71 | func fileHandler(count: Int) { 72 | progress = count 73 | } 74 | 75 | func abort() { 76 | selecteduuids.removeAll() 77 | InterruptProcess() 78 | progressviewshowinfo = false 79 | noestprogressdetails.reset() 80 | } 81 | 82 | func executeAllNoEstimationTasks() { 83 | noestprogressdetails.startExecuteAllTasksNoEstimation() 84 | if let configurations = rsyncUIdata.configurations { 85 | Execute(profile: rsyncUIdata.profile, 86 | configurations: configurations, 87 | selecteduuids: selecteduuids, 88 | noestprogressdetails: noestprogressdetails, 89 | fileHandler: fileHandler, 90 | updateconfigurations: updateConfigurations) 91 | } 92 | } 93 | 94 | func updateConfigurations(_ configurations: [SynchronizeConfiguration]) { 95 | rsyncUIdata.configurations = configurations 96 | progressviewshowinfo = false 97 | noestprogressdetails.reset() 98 | executetaskpath.append(Tasks(task: .completedview)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /RsyncUI/Views/OutputViews/RsyncRealtimeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RsyncRealtimeView.swift 3 | // RsyncUImenu 4 | // 5 | // Created by Thomas Evensen on 12/11/2025. 6 | // 7 | 8 | import Observation 9 | import RsyncProcess 10 | import SwiftUI 11 | 12 | struct RsyncRealtimeView: View { 13 | // The generated observable model from @Observable should be usable as an observable object here. 14 | // Using @ObservedObject to reference the shared singleton. 15 | @State private var model = PrintLines.shared 16 | @State private var isTappedfile = false 17 | @State private var isTappedview = false 18 | 19 | var body: some View { 20 | // NavigationView { 21 | VStack { 22 | List(model.output, id: \.self) { line in 23 | Text(line) 24 | .font(.system(.caption, design: .monospaced)) 25 | .lineLimit(1) 26 | } 27 | 28 | HStack { 29 | ConditionalGlassButton( 30 | systemImage: "eyes.inverse", 31 | text: "View", 32 | helpText: "Enable capture rsync output", 33 | textcolor: isTappedview 34 | ) { 35 | Task { 36 | isTappedview.toggle() 37 | guard isTappedview else { 38 | await RsyncOutputCapture.shared.disable() 39 | return 40 | } 41 | 42 | if await RsyncOutputCapture.shared.isCapturing() { 43 | isTappedfile = false 44 | await RsyncOutputCapture.shared.disable() 45 | } 46 | await RsyncOutputCapture.shared.enable() 47 | } 48 | } 49 | 50 | ConditionalGlassButton( 51 | systemImage: "square.and.arrow.down.badge.checkmark", 52 | text: "File", 53 | helpText: "Enable capture rsync output", 54 | textcolor: isTappedfile 55 | ) { 56 | Task { 57 | isTappedfile.toggle() 58 | guard isTappedfile else { 59 | await RsyncOutputCapture.shared.disable() 60 | return 61 | } 62 | if await RsyncOutputCapture.shared.isCapturing() { 63 | isTappedview = false 64 | await RsyncOutputCapture.shared.disable() 65 | } 66 | if let logURL = URL.userHomeDirectoryURLPath?.appendingPathComponent("rsync-output.log") { 67 | await RsyncOutputCapture.shared.enable(writeToFile: logURL) 68 | } 69 | } 70 | } 71 | 72 | Spacer() 73 | 74 | ConditionalGlassButton( 75 | systemImage: "trash", 76 | text: "Clear", 77 | helpText: "Clear output" 78 | ) { 79 | Task { @MainActor in 80 | model.clear() 81 | } 82 | } 83 | } 84 | } 85 | .padding() 86 | .onDisappear { 87 | Task { 88 | await RsyncOutputCapture.shared.disable() 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /RsyncUI/Model/ParametersRsync/ArgumentsSynchronize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgumentsSynchronize.swift 3 | // RsyncOSX 4 | // 5 | // Created by Thomas Evensen on 13/10/2019. 6 | // Copyright © 2019 Thomas Evensen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RsyncArguments 11 | 12 | @MainActor 13 | final class ArgumentsSynchronize { 14 | var config: SynchronizeConfiguration? 15 | 16 | func argumentsforpushlocaltoremotewithparameters(dryRun: Bool, forDisplay: Bool, keepdelete: Bool) -> [String]? { 17 | if let config { 18 | let params = Params().params(config: config) 19 | let rsyncparameterssynchronize = RsyncParametersSynchronize(parameters: params) 20 | do { 21 | try rsyncparameterssynchronize.argumentsForPushLocalToRemoteWithParameters(forDisplay: forDisplay, 22 | verify: false, 23 | dryrun: dryRun, 24 | keepDelete: keepdelete) 25 | return rsyncparameterssynchronize.computedArguments 26 | } catch { 27 | return nil 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | func argumentsSynchronize(dryRun: Bool, forDisplay: Bool) -> [String]? { 34 | if let config { 35 | let params = Params().params(config: config) 36 | let rsyncparameterssynchronize = RsyncParametersSynchronize(parameters: params) 37 | 38 | switch config.task { 39 | case SharedReference.shared.synchronize: 40 | do { 41 | try rsyncparameterssynchronize.argumentsForSynchronize(forDisplay: forDisplay, 42 | verify: false, 43 | dryrun: dryRun) 44 | return rsyncparameterssynchronize.computedArguments 45 | } catch { 46 | return nil 47 | } 48 | case SharedReference.shared.snapshot: 49 | do { 50 | try rsyncparameterssynchronize.argumentsForSynchronizeSnapshot(forDisplay: forDisplay, 51 | verify: false, 52 | dryrun: dryRun) 53 | return rsyncparameterssynchronize.computedArguments 54 | } catch { 55 | return nil 56 | } 57 | case SharedReference.shared.syncremote: 58 | do { 59 | try rsyncparameterssynchronize.argumentsForSynchronizeRemote(forDisplay: forDisplay, 60 | verify: false, 61 | dryrun: dryRun) 62 | return rsyncparameterssynchronize.computedArguments 63 | } catch { 64 | return nil 65 | } 66 | default: 67 | break 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | init(config: SynchronizeConfiguration?) { 74 | self.config = config 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /RsyncUI/Views/Snapshots/SnapshotListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnapshotListView.swift 3 | // RsyncSwiftUI 4 | // 5 | // Created by Thomas Evensen on 23/02/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SnapshotListView: View { 11 | @Binding var snapshotdata: ObservableSnapshotData 12 | @Binding var filterstring: String 13 | 14 | @Binding var selectedconfig: SynchronizeConfiguration? 15 | @State private var confirmdelete: Bool = false 16 | 17 | var body: some View { 18 | Table(logrecords, selection: $snapshotdata.snapshotuuidsfordelete) { 19 | TableColumn("Snap") { data in 20 | if let snapshotCatalog = data.snapshotCatalog { 21 | Text(snapshotCatalog) 22 | } 23 | } 24 | .width(max: 40) 25 | 26 | TableColumn("Date") { data in 27 | Text(data.dateExecuted) 28 | } 29 | .width(max: 150) 30 | TableColumn("Tag") { data in 31 | if let period = data.period { 32 | if period.contains("Delete") { 33 | Text(period) 34 | .foregroundColor(.red) 35 | } else { 36 | Text(period) 37 | } 38 | } 39 | } 40 | .width(max: 200) 41 | TableColumn("Last") { data in 42 | if let days = data.latest { 43 | Text(days) 44 | } 45 | } 46 | .width(max: 60) 47 | TableColumn("Result") { data in 48 | Text(data.resultExecuted) 49 | } 50 | .width(max: 250) 51 | } 52 | .confirmationDialog(snapshotdata.snapshotuuidsfordelete.count == 1 ? "Delete 1 snapshot" : 53 | "Delete \(snapshotdata.snapshotuuidsfordelete.count) snapshots", 54 | isPresented: $confirmdelete) { 55 | Button("Delete") { 56 | delete() 57 | confirmdelete = false 58 | } 59 | } 60 | .onDeleteCommand { 61 | confirmdelete = true 62 | } 63 | .overlay { 64 | if logrecords.count == 0, 65 | selectedconfig != nil, 66 | selectedconfig?.task == SharedReference.shared.snapshot, 67 | snapshotdata.snapshotlist == false { 68 | ContentUnavailableView { 69 | Label("There are no snapshot records by this search string in Date or Tag", 70 | systemImage: "doc.richtext.fill") 71 | } description: { 72 | Text("Change search string to filter records") 73 | } 74 | } 75 | } 76 | } 77 | 78 | var logrecords: [LogRecordSnapshot] { 79 | if filterstring.isEmpty { 80 | snapshotdata.getsnapshotdata() ?? [] 81 | } else { 82 | snapshotdata.getsnapshotdata()?.filter { ($0.dateExecuted).contains(filterstring) || 83 | ($0.period ?? "").contains(filterstring) 84 | } ?? [] 85 | } 86 | } 87 | 88 | func delete() { 89 | if let config = selectedconfig { 90 | snapshotdata.delete = DeleteSnapshots(config: config, 91 | snapshotdata: snapshotdata, 92 | logrecordssnapshot: snapshotdata.getsnapshotdata()) 93 | snapshotdata.inprogressofdelete = true 94 | snapshotdata.delete?.deletesnapshots() 95 | } 96 | } 97 | } 98 | --------------------------------------------------------------------------------