diff --git a/Chronos.xcodeproj/project.pbxproj b/Chronos.xcodeproj/project.pbxproj index 5872c25..34ddb00 100644 --- a/Chronos.xcodeproj/project.pbxproj +++ b/Chronos.xcodeproj/project.pbxproj @@ -663,7 +663,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0; + MARKETING_VERSION = 4.0; PRODUCT_BUNDLE_IDENTIFIER = com.joeldavidw.ChronosDevDebug; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -709,7 +709,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0; + MARKETING_VERSION = 4.0; PRODUCT_BUNDLE_IDENTIFIER = com.joeldavidw.ChronosDevRelease; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -812,7 +812,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0; + MARKETING_VERSION = 4.0; PRODUCT_BUNDLE_IDENTIFIER = com.joeldavidw.Chronos; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportConfirmPasswordView.swift b/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportConfirmPasswordView.swift index d7cef32..76ff428 100644 --- a/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportConfirmPasswordView.swift +++ b/Chronos/App/Tabs/Settings/Export/EncryptedExport/EncryptedExportConfirmPasswordView.swift @@ -69,6 +69,7 @@ struct EncryptedExportConfirmPasswordView: View { .presentationDragIndicator(Visibility.hidden) .onDisappear { exportNav.showSheet = false + exportService.cleanupTemporaryDirectory() } } else { VStack { diff --git a/Chronos/App/Tabs/Settings/Export/ExportSelectionView.swift b/Chronos/App/Tabs/Settings/Export/ExportSelectionView.swift index 7406fcb..60f2596 100644 --- a/Chronos/App/Tabs/Settings/Export/ExportSelectionView.swift +++ b/Chronos/App/Tabs/Settings/Export/ExportSelectionView.swift @@ -61,6 +61,9 @@ struct ExportSelectionView: View { if let fileurl = exportService.exportToUnencryptedJson() { ActivityView(fileUrl: fileurl) .presentationDetents([.medium, .large]) + .onDisappear { + exportService.cleanupTemporaryDirectory() + } } else { VStack { Image(systemName: "xmark.circle") diff --git a/Chronos/Services/ExportService.swift b/Chronos/Services/ExportService.swift index 864fdfc..f33c072 100644 --- a/Chronos/Services/ExportService.swift +++ b/Chronos/Services/ExportService.swift @@ -16,15 +16,15 @@ public class ExportService { calendar: Calendar(identifier: .gregorian) ) - func exportToUnencryptedJson() -> URL? { + private func getExportVault() -> ExportVault? { let context = ModelContext(swiftDataService.getModelContainer()) - let vault = vaultService.getVault(context: context) - - let exportVault = ExportVault() + guard let vault = vaultService.getVault(context: context) else { + logger.error("Vault missing") + return nil + } var tokens: [Token] = [] - - for encToken in vault!.encryptedTokens! { + for encToken in vault.encryptedTokens ?? [] { guard let token = cryptoService.decryptToken(encryptedToken: encToken) else { logger.error("Unable to decode token") continue @@ -32,71 +32,75 @@ public class ExportService { tokens.append(token) } + let exportVault = ExportVault() exportVault.tokens = tokens + return exportVault + } - let url = FileManager.default.temporaryDirectory - .appendingPathComponent("Chronos_" + Date().formatted(verbatimStyle)) - .appendingPathExtension("json") + private func createUniqueDir() -> URL? { + let uniqueDir = FileManager.default.temporaryDirectory + .appendingPathComponent("export", isDirectory: true) + .appendingPathComponent(UUID().uuidString, isDirectory: true) + + do { + try FileManager.default.createDirectory(at: uniqueDir, withIntermediateDirectories: true) + } catch { + logger.error("Unable to create temporary directory: \(error.localizedDescription)") + return nil + } + return uniqueDir + } + private func writeJSON(to url: URL, from exportVault: ExportVault) -> Bool { guard let jsonData = try? JSONEncoder().encode(exportVault) else { logger.error("Unable to encode exportVault") - return nil + return false } do { try jsonData.write(to: url) + return true } catch { logger.error("Unable to write json file: \(error.localizedDescription)") - return nil + return false } - - return url } - func exportToEncryptedZip(password: String) -> URL? { - let context = ModelContext(swiftDataService.getModelContainer()) - let vault = vaultService.getVault(context: context) - - let exportVault = ExportVault() - - var tokens: [Token] = [] - - for encToken in vault!.encryptedTokens! { - guard let token = cryptoService.decryptToken(encryptedToken: encToken) else { - logger.error("Unable to decode token") - continue - } - tokens.append(token) + func exportToUnencryptedJson() -> URL? { + guard let exportVault = getExportVault(), let uniqueDir = createUniqueDir() else { + return nil } - exportVault.tokens = tokens - - let randomDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + let url = uniqueDir.appendingPathComponent("Chronos_" + Date().formatted(verbatimStyle)) + .appendingPathExtension("json") - try? FileManager.default.createDirectory(at: randomDir, withIntermediateDirectories: true) + if writeJSON(to: url, from: exportVault) { + return url + } - let jsonUrl = randomDir.appendingPathComponent("Chronos_" + Date().formatted(verbatimStyle)) - .appendingPathExtension("json") + return nil + } - guard let jsonData = try? JSONEncoder().encode(exportVault) else { - logger.error("Unable to encode exportVault") + func exportToEncryptedZip(password: String) -> URL? { + guard let exportVault = getExportVault(), let uniqueDir = createUniqueDir() else { return nil } - do { - try jsonData.write(to: jsonUrl) - } catch { - logger.error("Unable to write json file: \(error.localizedDescription)") + let jsonUrl = uniqueDir.appendingPathComponent("Chronos_" + Date().formatted(verbatimStyle)) + .appendingPathExtension("json") + + if !writeJSON(to: jsonUrl, from: exportVault) { return nil } let zipUrl = FileManager.default.temporaryDirectory - .appendingPathComponent("Chronos_Encrypted_" + Date().formatted(verbatimStyle)) + .appendingPathComponent("export", isDirectory: true) + .appendingPathComponent("Chronos_" + Date().formatted(verbatimStyle)) .appendingPathExtension("zip") let success = SSZipArchive.createZipFile( - atPath: zipUrl.path(), - withContentsOfDirectory: randomDir.path(), + atPath: zipUrl.path, + withContentsOfDirectory: uniqueDir.path, keepParentDirectory: false, compressionLevel: 0, password: password, @@ -107,6 +111,18 @@ public class ExportService { return zipUrl } + logger.error("Failed to create zip file") return nil } + + func cleanupTemporaryDirectory() { + do { + let tempExportDirUrl = FileManager.default.temporaryDirectory + .appendingPathComponent("export", isDirectory: true) + + try FileManager.default.removeItem(at: tempExportDirUrl) + } catch { + logger.error("Unable to delete temporary directory: \(error.localizedDescription)") + } + } }