Skip to content

Commit

Permalink
Merge pull request #38 from jamf/awake
Browse files Browse the repository at this point in the history
Awake and permission fixes (and a whole bunch more)
  • Loading branch information
HarryStrandJamf authored Oct 25, 2024
2 parents 8ded11c + 00bbb1f commit 42a0afd
Show file tree
Hide file tree
Showing 22 changed files with 172 additions and 35 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.0] - 2024-10-25
### Features
- Added a settings view and a setting for allowing deletions after synchronization. This defaults to off so it's less likely for someone to delete files unintentionally.
### Enhancements
- Added some more detail to the synchronization confirmation message about how much will be deleted if one of the deletion options is chosen and added a warning that deletion cannot be undone.
- Prevent macOS prompt for credentials from appearing if authentication fails on a file share and removes the keychain entry with the wrong credentials.
### Bug fixes
- Prevent the machine running Jamf Sync from going to sleep when copying files to an AFP/SMB share.
- Fix permission issues when syncing to a file share (GitHub issue #18 "Permissions issue with packages when using Jamf Sync")
- Fixed an issue where it could crash if the Jamf Pro instance didn't have any packages.

## [1.3.3] - 2024-10-21
### Bug fixes
- Added multipart uploads to solve an issue with >5GB uploads to JCDS distribution points. (GitHub issue #4 "Not able to upload huge files like 14GB.")
Expand Down
18 changes: 13 additions & 5 deletions Jamf Sync.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
84BC6E4D2AC38C6200CF6D39 /* FileShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC6E4A2AC38C6200CF6D39 /* FileShare.swift */; };
84BC6E552AC4933500CF6D39 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 84BC6E542AC4933500CF6D39 /* README.md */; };
84BF5E3A2CC15FD3008B07A1 /* TemporaryFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BF5E392CC15FB4008B07A1 /* TemporaryFiles.swift */; };
84BF61A92CC93DFB008B07A1 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BF61A82CC93DFB008B07A1 /* SettingsView.swift */; };
84BF61AB2CC94DD5008B07A1 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BF61AA2CC94DD5008B07A1 /* SettingsViewModel.swift */; };
84CAB0E52C25C47400582D59 /* FileManager+moveRetainingPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAB0E42C25C47400582D59 /* FileManager+moveRetainingPermissions.swift */; };
84CCB5D22B4C852F00328291 /* SetupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCB5D12B4C852F00328291 /* SetupViewModel.swift */; };
84CCB5D42B4C946200328291 /* LogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCB5D32B4C946200328291 /* LogViewModel.swift */; };
Expand Down Expand Up @@ -180,6 +182,8 @@
84BC6E4A2AC38C6200CF6D39 /* FileShare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileShare.swift; sourceTree = "<group>"; };
84BC6E542AC4933500CF6D39 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
84BF5E392CC15FB4008B07A1 /* TemporaryFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryFiles.swift; sourceTree = "<group>"; };
84BF61A82CC93DFB008B07A1 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
84BF61AA2CC94DD5008B07A1 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
84CAB0E42C25C47400582D59 /* FileManager+moveRetainingPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+moveRetainingPermissions.swift"; sourceTree = "<group>"; };
84CCB5D12B4C852F00328291 /* SetupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupViewModel.swift; sourceTree = "<group>"; };
84CCB5D32B4C946200328291 /* LogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -344,6 +348,7 @@
84E489962B59BBFD00FFFE59 /* PackageAnimationView.swift */,
84BC6E402AC3659600CF6D39 /* PackageListView.swift */,
840A79002ACB6E8200161D85 /* SaveableItemListView.swift */,
84BF61A82CC93DFB008B07A1 /* SettingsView.swift */,
84BC6E442AC37F4100CF6D39 /* SetupView.swift */,
84E489902B5867F800FFFE59 /* SourceDestinationView.swift */,
84FC415D2AD5A5C600DCB033 /* SynchronizeProgressView.swift */,
Expand Down Expand Up @@ -419,11 +424,12 @@
84CCB5CE2B4C83AC00328291 /* ViewModels */ = {
isa = PBXGroup;
children = (
844E40D62B50A26A003D1940 /* DpFilesViewModel.swift */,
844E40D42B50A07A003D1940 /* DpFileViewModel.swift */,
84CCB5D32B4C946200328291 /* LogViewModel.swift */,
84CCB5D12B4C852F00328291 /* SetupViewModel.swift */,
844E40D22B505404003D1940 /* PackageListViewModel.swift */,
844E40D42B50A07A003D1940 /* DpFileViewModel.swift */,
844E40D62B50A26A003D1940 /* DpFilesViewModel.swift */,
84BF61AA2CC94DD5008B07A1 /* SettingsViewModel.swift */,
84CCB5D12B4C852F00328291 /* SetupViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
Expand Down Expand Up @@ -662,11 +668,13 @@
84D568622BD0722B00C91686 /* AboutView.swift in Sources */,
844E40D32B505404003D1940 /* PackageListViewModel.swift in Sources */,
84FC41662AD895F400DCB033 /* KeychainHelper.swift in Sources */,
84BF61A92CC93DFB008B07A1 /* SettingsView.swift in Sources */,
840A79032ACB75FC00161D85 /* SavableItem.swift in Sources */,
84B967642B74157C00D73F75 /* HelpMenu.swift in Sources */,
84BC6E372AC3631400CF6D39 /* FileShareDp.swift in Sources */,
84BC6E362AC3631400CF6D39 /* DpFile.swift in Sources */,
848C2D262BC47B3300036999 /* CharacterSet_rfc3986Unreserved.swift in Sources */,
84BF61AB2CC94DD5008B07A1 /* SettingsViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -857,7 +865,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.3.3;
MARKETING_VERSION = 1.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.jamfsync;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down Expand Up @@ -889,7 +897,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.3.3;
MARKETING_VERSION = 1.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.jamfsync;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down
1 change: 1 addition & 0 deletions JamfSync/Model/DataModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum ReloadFiles {
class DataModel: ObservableObject {
static let noSelection = UUID()
static let shared = DataModel()
@Published var settingsViewModel = SettingsViewModel()
@Published var srcPackageListViewModel = PackageListViewModel(isSrc: true)
@Published var dstPackageListViewModel = PackageListViewModel(isSrc: false)
@Published var savableItems: SavableItems = SavableItems()
Expand Down
39 changes: 27 additions & 12 deletions JamfSync/Model/FileShareDp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class FileShareDp: DistributionPoint {
var mountPoint: String?
var localPath: String?

let keychainHelper = KeychainHelper()

init(jamfProId: Int, name: String, address: String, isMaster: Bool, connectionType: ConnectionType, shareName: String, workgroupOrDomain: String, sharePort: Int, readOnlyUsername: String?, readOnlyPassword: String?, readWriteUsername: String?, readWritePassword: String?) {
self.jamfProId = jamfProId
self.address = address
Expand Down Expand Up @@ -109,19 +111,32 @@ class FileShareDp: DistributionPoint {
Task { @MainActor in
DataModel.shared.showSpinner = true
}
fileShare = try await FileShares.shared.mountFileShare(type: connectionType, address: address, shareName: shareName, username: readWriteUsername, password: readWritePassword)
if let mountPoint = await fileShare?.mountPoint {
let mountPointUrl = URL(filePath: mountPoint)
let packagesUrl = mountPointUrl.appendingPathComponent("Packages", isDirectory: true)
localPath = packagesUrl.path().removingPercentEncoding
if let localPath, !fileManager.fileExists(atPath: localPath) {
do {
try fileManager.createDirectory(at: packagesUrl, withIntermediateDirectories: false)
} catch {
LogManager.shared.logMessage(message: "\"Packages\" directory does not exist on file share \(name) and it couldn't be created: \(error)", level: .error)
throw error
do {
fileShare = try await FileShares.shared.mountFileShare(type: connectionType, address: address, shareName: shareName, username: readWriteUsername, password: readWritePassword)
if let mountPoint = await fileShare?.mountPoint {
let mountPointUrl = URL(filePath: mountPoint)
let packagesUrl = mountPointUrl.appendingPathComponent("Packages", isDirectory: true)
localPath = packagesUrl.path().removingPercentEncoding
if let localPath, !fileManager.fileExists(atPath: localPath) {
do {
try fileManager.createDirectory(at: packagesUrl, withIntermediateDirectories: false)
} catch {
LogManager.shared.logMessage(message: "\"Packages\" directory does not exist on file share \(name) and it couldn't be created: \(error)", level: .error)
throw error
}
}
}
} catch {
let serviceName = keychainHelper.fileShareServiceName(username: readWriteUsername, urlString: address)
try await keychainHelper.deleteKeychainItem(serviceName: serviceName, key: readWriteUsername)
Task { @MainActor in
DataModel.shared.dpToPromptForPassword = self
DataModel.shared.shouldPromptForDpPassword = true
}
Task { @MainActor in
DataModel.shared.showSpinner = false
}
throw error
}
Task { @MainActor in
DataModel.shared.showSpinner = false
Expand All @@ -131,7 +146,7 @@ class FileShareDp: DistributionPoint {
private func loadKeychainData() {
guard let address, let readWriteUsername else { return }
let keychainHelper = KeychainHelper()
let serviceName = keychainHelper.fileShareServiceName(urlString: address)
let serviceName = keychainHelper.fileShareServiceName(username: readWriteUsername, urlString: address)
Task {
do {
let data = try await keychainHelper.getInformationFromKeychain(serviceName: serviceName, key: readWriteUsername)
Expand Down
8 changes: 5 additions & 3 deletions JamfSync/Model/JamfProPackageUApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ class JamfProPackageUApi: JamfProPackageApi {
var pageOfPackages = try await loadPageOfPackages(pageNbr: 0, pageSize: pageSize, jamfProInstance: jamfProInstance)
var packages = pageOfPackages.packages
let pages = (pageOfPackages.totalPackages + (pageSize - 1)) / pageSize
for pageNbr in 1..<pages {
pageOfPackages = try await loadPageOfPackages(pageNbr: pageNbr, pageSize: pageSize, jamfProInstance: jamfProInstance)
packages += pageOfPackages.packages
if pages > 0 {
for pageNbr in 1..<pages {
pageOfPackages = try await loadPageOfPackages(pageNbr: pageNbr, pageSize: pageSize, jamfProInstance: jamfProInstance)
packages += pageOfPackages.packages
}
}

return packages
Expand Down
4 changes: 0 additions & 4 deletions JamfSync/Model/Jcds2Dp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class Jcds2Dp: DistributionPoint, RenewTokenProtocol {
var urlSession: URLSession?
var downloadTask: URLSessionDownloadTask?
var dispatchGroup: DispatchGroup?
var keepAwake = KeepAwake()
var multipartUpload: MultipartUpload?

init(jamfProInstanceId: UUID? = nil, jamfProInstanceName: String? = nil) {
Expand Down Expand Up @@ -219,9 +218,6 @@ class Jcds2Dp: DistributionPoint, RenewTokenProtocol {

guard let fileUrl, let fileSize else { throw DistributionPointError.badFileUrl }

keepAwake.disableSleep(reason: "Starting upload")
defer { keepAwake.enableSleep() }

multipartUpload = MultipartUpload(initiateUploadData: initiateUploadData, renewTokenObject: self, progress: progress)
guard let multipartUpload else { throw DistributionPointError.programError }

Expand Down
9 changes: 9 additions & 0 deletions JamfSync/Model/SynchronizeTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Foundation

class SynchronizeTask {
var activeDp: DistributionPoint?
var keepAwake = KeepAwake()

/// Loops through the files to synchronize to calculate the total size of files to be transferred.
/// - Parameters:
Expand All @@ -19,6 +20,14 @@ class SynchronizeTask {
/// - progress: The progress object that should be updated as the synchronization progresses
/// - Returns: Returns true if the file lists need to reload files, otherwise false
func synchronize(srcDp: DistributionPoint, dstDp: DistributionPoint, selectedItems: [DpFile], jamfProInstance: JamfProInstance?, forceSync: Bool, deleteFiles: Bool, deletePackages: Bool, progress: SynchronizationProgress) async throws -> Bool {

keepAwake.disableSleep(reason: "Starting Sync")
keepAwake.disableDiskIdle(reason: "Starting Sync")
defer {
keepAwake.enableSleep()
keepAwake.enableIdleDisk()
}

activeDp = srcDp
try await srcDp.prepareDp()
try await srcDp.retrieveFileList()
Expand Down
22 changes: 22 additions & 0 deletions JamfSync/Model/ViewModels/SettingsViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Copyright 2024, Jamf
//

import Foundation

class SettingsViewModel: ObservableObject {
let userSettings = UserSettings()
@Published var allowDeletionsAfterSynchronization = false

init() {
loadSettings()
}

func loadSettings() {
allowDeletionsAfterSynchronization = userSettings.allowDeletionsAfterSynchronization
}

func saveSettings() {
userSettings.allowDeletionsAfterSynchronization = allowDeletionsAfterSynchronization
}
}
Binary file modified JamfSync/Resources/Jamf Sync User Guide.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion JamfSync/UI/FileSharePasswordView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ struct FileSharePasswordView: View {
if let address = fileShareDp.address, let username = fileShareDp.readWriteUsername, let password = fileShareDp.readWritePassword, !password.isEmpty, let data = password.data(using: String.Encoding.utf8) {
Task {
do {
let serviceName = keychainHelper.fileShareServiceName(urlString: address)
let serviceName = keychainHelper.fileShareServiceName(username: username, urlString: address)
try await keychainHelper.storeInformationToKeychain(serviceName: serviceName, key: username, data: data)
} catch {
LogManager.shared.logMessage(message: "Failed to save the password for \(address) to the keychain: \(error)", level: .error)
Expand Down
25 changes: 24 additions & 1 deletion JamfSync/UI/HeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ struct HeaderView: View {
dataModel.updateListViewModels()
}
}
.alert("Do you want to delete items from the destination that are not on the source?", isPresented: $promptForSynchronizationOptions) {
.alert(deletionMessage(), isPresented: $promptForSynchronizationOptions) {
HStack {
if dataModel.findDp(id: dataModel.selectedDstDpId)?.jamfProInstanceId == nil {
Button("Yes", role: .destructive) {
Expand Down Expand Up @@ -77,6 +77,25 @@ struct HeaderView: View {
}
}

func deletionMessage() -> String {
var message = "Do you want to delete items from the destination that are not on the source?"
var warning = " WARNING: Deletions cannot be undone!"
if let dstDp = dataModel.findDp(id: dataModel.selectedDstDpId), let srcDp = dataModel.findDp(id: dataModel.selectedSrcDpId) {
let filesToRemove = dstDp.filesToRemove(srcDp: srcDp)
message += " There are \(filesToRemove.count) files "
if filesToRemove.count == dstDp.dpFiles.files.count {
warning = " WARNING: This is all of the files on the destination! Deletions cannot be undone!"
}
if let jamfProInstance = DataModel.shared.findJamfProInstance(id: dstDp.jamfProInstanceId) {
let packagesToRemove = jamfProInstance.packagesToRemove(srcDp: srcDp)
message += "and \(packagesToRemove.count) package records "
}
message += "that can be removed.\(warning)"
}

return message
}

func startSynchronize(deleteFiles: Bool, deletePackages: Bool) async {
if let srcDp = dataModel.findDp(id: dataModel.selectedSrcDpId), let dstDp = dataModel.findDp(id: dataModel.selectedDstDpId) {
SynchronizeProgressView(srcDp: srcDp, dstDp: dstDp, deleteFiles: deleteFiles, deletePackages: deletePackages, processToExecute: { (synchronizeTask, deleteFiles, deletePackages, progress, synchronizationProgressView) in
Expand All @@ -94,6 +113,7 @@ struct HeaderView: View {
DataModel.shared.synchronizationInProgress = true
do {
guard let srcDp else { throw DistributionPointError.programError }

reloadFiles = try await synchronizeTask.synchronize(srcDp: srcDp, dstDp: dstDp, selectedItems: DataModel.shared.selectedDpFilesFromSelectionIds(packageListViewModel: DataModel.shared.srcPackageListViewModel), jamfProInstance: DataModel.shared.findJamfProInstance(id: dstDp.jamfProInstanceId), forceSync: DataModel.shared.forceSync, deleteFiles: deleteFiles, deletePackages: deletePackages, progress: progress)
} catch {
LogManager.shared.logMessage(message: "Failed to synchronize \(srcDp?.name ?? "nil") to \(dstDp.name): \(error)", level: .error)
Expand All @@ -114,6 +134,9 @@ struct HeaderView: View {
}

func promptForDeletion() -> Bool {
if !dataModel.settingsViewModel.allowDeletionsAfterSynchronization {
return false
}
if dataModel.srcPackageListViewModel.selectedDpFiles.count == 0 {
if let srcDp = dataModel.findDp(id: dataModel.selectedSrcDpId), let dstDp = dataModel.findDp(id: dataModel.selectedDstDpId) {
// If there are any packages on the destination Jamf Pro server that would be removed, then prompt
Expand Down
6 changes: 3 additions & 3 deletions JamfSync/UI/JamfProServerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct JamfProServerView: View {
Text("URL:")
.frame(height: 16)
.padding(.bottom)
Text("\(userNameClientIdPrompt()):")
Text("\(usernameClientIdPrompt()):")
.frame(height: 16)
.padding(.bottom)
Text("\(passwordClientSecretPrompt()):")
Expand All @@ -49,7 +49,7 @@ struct JamfProServerView: View {
TextField("https://jamfproserver.com", text: $urlString)
.frame(height: 16)
.padding(.bottom)
TextField("", text: $usernameOrClientId, prompt: Text(userNameClientIdPrompt()))
TextField("", text: $usernameOrClientId, prompt: Text(usernameClientIdPrompt()))
.frame(height: 16)
.padding(.bottom)
HStack {
Expand Down Expand Up @@ -143,7 +143,7 @@ struct JamfProServerView: View {
.frame(width: 600)
}

func userNameClientIdPrompt() -> String {
func usernameClientIdPrompt() -> String {
return useClientApi ? "Client Id" : "Username"
}

Expand Down
3 changes: 3 additions & 0 deletions JamfSync/UI/JamfSyncApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ struct JamfSyncApp: App {
}
}
}
Settings {
SettingsView(settingsViewModel: DataModel.shared.settingsViewModel)
}
}

func startingView() -> some View {
Expand Down
3 changes: 3 additions & 0 deletions JamfSync/UI/PackageListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ struct PackageListView: View {
colorWhenMismatched = nil
}
}
if !dataModel.settingsViewModel.allowDeletionsAfterSynchronization {
colorWhenDeleted = nil
}
switch fileItem.state {
case .undefined:
return nil
Expand Down
Loading

0 comments on commit 42a0afd

Please sign in to comment.