├── 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 | [](https://github.com/rsyncOSX/RsyncUI/blob/main/Licence.MD)
4 | 
5 | [](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 | 
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 |
--------------------------------------------------------------------------------