diff --git a/external/libzip/libzip.xcodeproj/project.pbxproj b/external/libzip/libzip.xcodeproj/project.pbxproj index f03b4736f..246da5d96 100644 --- a/external/libzip/libzip.xcodeproj/project.pbxproj +++ b/external/libzip/libzip.xcodeproj/project.pbxproj @@ -741,6 +741,12 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "HAVE_CONFIG_H=1", + "HAVE_CRYPTO=1", + ); INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -765,6 +771,10 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "HAVE_CONFIG_H=1", + "HAVE_CRYPTO=1", + ); INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/ownCloud Intents/Info.plist b/ownCloud Intents/Info.plist index 228b254ed..0ad1dd895 100644 --- a/ownCloud Intents/Info.plist +++ b/ownCloud Intents/Info.plist @@ -30,6 +30,7 @@ IntentsSupported + CompressPathItemsIntent CreateFolderIntent DeletePathItemIntent GetAccountIntent diff --git a/ownCloud Intents/IntentHandler.swift b/ownCloud Intents/IntentHandler.swift index 384eed5d4..b2c0eaba8 100644 --- a/ownCloud Intents/IntentHandler.swift +++ b/ownCloud Intents/IntentHandler.swift @@ -40,6 +40,8 @@ class IntentHandler: INExtension { return PathExistsIntentHandler() } else if intent is DeletePathItemIntent { return DeletePathItemIntentHandler() + } else if intent is CompressPathItemsIntent { + return CompressPathItemsIntentHandler() } fatalError("Unhandled intent type: \(intent)") diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 20e999434..7a4958154 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -79,6 +79,8 @@ 3998F5D3224102FE00B66713 /* UITableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */; }; 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D422411EDF00B66713 /* BorderedLabel.swift */; }; 3998F5D72241486F00B66713 /* OCCertificate+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */; }; + 39999DF224629FA800880D45 /* OCCore+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A513AB22674E56002CF1AA /* OCCore+Extension.swift */; }; + 39999DF42462D57800880D45 /* UncompressAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39999DF32462D57800880D45 /* UncompressAction.swift */; }; 399A4C002317CC460027DDD6 /* AppLockHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4BFF2317CC460027DDD6 /* AppLockHelper.swift */; }; 399A4C032317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */; }; 399A4C1023190ADF0027DDD6 /* PathExistsIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A4C0F23190ADF0027DDD6 /* PathExistsIntentHandler.swift */; }; @@ -93,6 +95,8 @@ 39B289A8226F1EE000BE0E11 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B289A7226F1EE000BE0E11 /* MessageView.swift */; }; 39B9675022BE0FBA0074DB22 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 593A821320C7D4C5000E2A90 /* Localizable.strings */; }; 39BC9C3023DB831F0097C52D /* DocumentEditingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CD755123D787E400193950 /* DocumentEditingAction.swift */; }; + 39BCDD7F246006AD00FE3D23 /* CompressAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BCDD79246006AC00FE3D23 /* CompressAction.swift */; }; + 39BCDD812460858300FE3D23 /* CompressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BCDD802460858200FE3D23 /* CompressViewController.swift */; }; 39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BE385C23435AFE0062A2FE /* String+Extension.swift */; }; 39CC8AE6228C12100020253B /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8AE5228C12100020253B /* Array+Extension.swift */; }; 39CC8B01228C8A950020253B /* MediaUploadSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */; }; @@ -104,6 +108,7 @@ 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */; }; 39E2FE0021FF814A00F0117F /* ThemeRoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */; }; 39E42D1C2315288B00B82AC3 /* KeyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */; }; + 39E49DBF24619CB40069414A /* CompressPathItemsIntentHandler .swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E49DBE24619CB40069414A /* CompressPathItemsIntentHandler .swift */; }; 39E6DE84233CC39A008DAE04 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */; }; 39E6DE86233CDF1E008DAE04 /* OCItemTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */; }; 39E98B3E22797D1B009911F1 /* PublicLinkTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */; }; @@ -819,6 +824,7 @@ 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extension.swift"; sourceTree = ""; }; 3998F5D422411EDF00B66713 /* BorderedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedLabel.swift; sourceTree = ""; }; 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCCertificate+Extension.swift"; sourceTree = ""; }; + 39999DF32462D57800880D45 /* UncompressAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UncompressAction.swift; sourceTree = ""; }; 399A4BFF2317CC460027DDD6 /* AppLockHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockHelper.swift; sourceTree = ""; }; 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmarkManager+Extension.swift"; sourceTree = ""; }; 399A4C0F23190ADF0027DDD6 /* PathExistsIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathExistsIntentHandler.swift; sourceTree = ""; }; @@ -833,6 +839,8 @@ 39AFC3D0225E72FB00A6D3AE /* GroupSharingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSharingTableViewController.swift; sourceTree = ""; }; 39AFC3D7225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSharingEditTableViewController.swift; sourceTree = ""; }; 39B289A7226F1EE000BE0E11 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; + 39BCDD79246006AC00FE3D23 /* CompressAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompressAction.swift; sourceTree = ""; }; + 39BCDD802460858200FE3D23 /* CompressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompressViewController.swift; sourceTree = ""; }; 39BE385C23435AFE0062A2FE /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 39CC8AE5228C12100020253B /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaUploadSettingsSection.swift; sourceTree = ""; }; @@ -845,6 +853,7 @@ 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableHeaderView.swift; sourceTree = ""; }; 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeRoundedButton.swift; sourceTree = ""; }; 39E42D1B2315288B00B82AC3 /* KeyCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCommands.swift; sourceTree = ""; }; + 39E49DBE24619CB40069414A /* CompressPathItemsIntentHandler .swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CompressPathItemsIntentHandler .swift"; sourceTree = ""; }; 39E6DE85233CDF1E008DAE04 /* OCItemTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCItemTracker.swift; sourceTree = ""; }; 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkTableViewController.swift; sourceTree = ""; }; 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkEditTableViewController.swift; sourceTree = ""; }; @@ -1409,6 +1418,7 @@ 23D77FC6212BFBD100DE76F1 /* NamingViewController.swift */, 6E37F48A2188B27D00CF16CA /* Action.swift */, DC3393A322E0A75C00DD3DA4 /* ClientItemResolvingCell.swift */, + 39BCDD802460858200FE3D23 /* CompressViewController.swift */, 39CD755323D8392D00193950 /* EditDocumentViewController.swift */, ); path = Actions; @@ -1535,6 +1545,7 @@ 39A5C3A0231566D9009D9EE3 /* GetFileInfoIntentHandler.swift */, 399A4C0F23190ADF0027DDD6 /* PathExistsIntentHandler.swift */, 3984F56B2319202200DC2639 /* DeletePathItemIntentHandler.swift */, + 39E49DBE24619CB40069414A /* CompressPathItemsIntentHandler .swift */, ); path = Intent; sourceTree = ""; @@ -1758,6 +1769,8 @@ DC62514B225D254500736874 /* UploadBaseAction.swift */, DC625147225CEB2C00736874 /* UploadFileAction.swift */, DC625149225CEB4300736874 /* UploadMediaAction.swift */, + 39BCDD79246006AC00FE3D23 /* CompressAction.swift */, + 39999DF32462D57800880D45 /* UncompressAction.swift */, ); path = "Actions+Extensions"; sourceTree = ""; @@ -3269,6 +3282,7 @@ 4C464BF12187AF1500D30602 /* PDFTocTableViewCell.swift in Sources */, DC1B270C209CF34B004715E1 /* BookmarkViewController.swift in Sources */, DC63208321FCAC1E007EC0A8 /* ClientActivityViewController.swift in Sources */, + 39999DF42462D57800880D45 /* UncompressAction.swift in Sources */, DC3DEC7E22B03ACD00F3352D /* CardPresentationController.swift in Sources */, 4C464BF62187AF1500D30602 /* PDFTocItem.swift in Sources */, 6E3A103E219D5BBA00F90C96 /* RenameAction.swift in Sources */, @@ -3368,6 +3382,7 @@ 394804DA225CBDBA00AA8183 /* BreadCrumbTableViewController.swift in Sources */, 4C235CEE21F88C0300A989A8 /* UIViewController+Extension.swift in Sources */, DC27A19D20CAB602008ACB6C /* FileProviderInterfaceManager.swift in Sources */, + 39BCDD7F246006AD00FE3D23 /* CompressAction.swift in Sources */, 4C51727D22DE04BD001BC97F /* ScheduledTaskExtension.swift in Sources */, DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */, 23EC77582137F3DD0032D4E6 /* PDFViewerViewController.swift in Sources */, @@ -3439,6 +3454,7 @@ DC29F09022974AEA00F77349 /* QueryFileListTableViewController.swift in Sources */, 6E586D002199A78E00F680C4 /* DeleteAction.swift in Sources */, DC3317CE2084966700E36C8F /* ThemeTableViewCell.swift in Sources */, + 39BCDD812460858300FE3D23 /* CompressViewController.swift in Sources */, 3913213822946E5E00EF88F4 /* FileListTableViewController.swift in Sources */, DC0196AB20F7690C00C41B78 /* OCBookmark+FileProvider.m in Sources */, 4C6B78122226B86300C5F3DB /* PhotoAlbumTableViewCell.swift in Sources */, @@ -3485,7 +3501,9 @@ 397754E223279EED00119FCB /* OCItem+Extension.swift in Sources */, 39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */, 39E6DE86233CDF1E008DAE04 /* OCItemTracker.swift in Sources */, + 39999DF224629FA800880D45 /* OCCore+Extension.swift in Sources */, 393D2B3F23FEB6DC00ED4F8C /* DispatchQueueTools.swift in Sources */, + 39E49DBF24619CB40069414A /* CompressPathItemsIntentHandler .swift in Sources */, 3984F5712319202200DC2639 /* DeletePathItemIntentHandler.swift in Sources */, 39F689AC22F0206900E63429 /* OCBookmark+Extension.swift in Sources */, 395E16FF22F172C900DE89A1 /* CreateFolderIntentHandler.swift in Sources */, diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift index cfbf8ee03..634b96312 100644 --- a/ownCloud/AppDelegate.swift +++ b/ownCloud/AppDelegate.swift @@ -85,6 +85,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCExtensionManager.shared.addExtension(LinksAction.actionExtension) OCExtensionManager.shared.addExtension(FavoriteAction.actionExtension) OCExtensionManager.shared.addExtension(UnfavoriteAction.actionExtension) + OCExtensionManager.shared.addExtension(CompressAction.actionExtension) + OCExtensionManager.shared.addExtension(UncompressAction.actionExtension) if #available(iOS 13.0, *) { if UIDevice.current.isIpad() { // iPad & iOS 13+ only diff --git a/ownCloud/Client/Actions/Action.swift b/ownCloud/Client/Actions/Action.swift index aeec36382..48358c0f4 100644 --- a/ownCloud/Client/Actions/Action.swift +++ b/ownCloud/Client/Actions/Action.swift @@ -173,12 +173,19 @@ class Action : NSObject { // MARK: - Provide Card view controller - class func cardViewController(for item: OCItem, with context: ActionContext, progressHandler: ActionProgressHandler? = nil, completionHandler: ((Action, Error?) -> Void)? = nil) -> UIViewController? { + class func cardViewController(for items: [OCItem], with context: ActionContext, progressHandler: ActionProgressHandler? = nil, completionHandler: ((Action, Error?) -> Void)? = nil) -> UIViewController? { guard let core = context.core else { return nil } let tableViewController = MoreStaticTableViewController(style: .grouped) - let header = MoreViewHeader(for: item, with: core) - let moreViewController = MoreViewController(item: item, core: core, header: header, viewController: tableViewController) + + var moreViewController : MoreViewController! + + if items.count > 1 { + let header = MoreViewHeader(title: String(format: "%ld Items Selected".localized, items.count)) + moreViewController = MoreViewController(core: core, header: header, viewController: tableViewController) + } else if let item = items.first { + let header = MoreViewHeader(for: item, with: core) + moreViewController = MoreViewController(core: core, header: header, viewController: tableViewController) if core.connectionStatus == .online { if core.connection.capabilities?.sharingAPIEnabled == 1 { @@ -214,6 +221,7 @@ class Action : NSObject { } } } + } let title = NSAttributedString(string: "Actions".localized, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20, weight: .heavy)]) diff --git a/ownCloud/Client/Actions/Actions+Extensions/CompressAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CompressAction.swift new file mode 100644 index 000000000..70f74e72b --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/CompressAction.swift @@ -0,0 +1,179 @@ +// +// CompressAction.swift +// ownCloud +// +// Created by Matthias Hühne on 05/04/2020. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2020, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import ownCloudSDK +import ownCloudApp + +class CompressAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.compress") } + override class var category : ActionCategory? { return .normal } + override class var name : String { return "Compress".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar, .keyboardShortcut] } + override class var keyCommand : String? { return "Z" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } + + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + if forContext.items.count == 1, forContext.items.first?.mimeType == "application/zip" { + return .none + } + + return .afterMiddle + } + + let defaultZipName = "Archive.zip".localized + + override func run() { + guard context.items.count > 0, let hostViewController = context.viewController, let core = self.core else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + var fileItems = context.items.filter { (item) -> Bool in + if item.type == .collection { + return false + } + + return true + } + + var collectionItems = context.items.filter { (item) -> Bool in + if item.type == .file { + return false + } + + return true + } + + let dispatchGroup = DispatchGroup() + for collection in collectionItems { + + dispatchGroup.enter() + core.retrieveSubItems(for: collection) { (items) in + let subFileItems = items?.filter { (item) -> Bool in + if item.type == .collection { + return false + } + + return true + } + let subCollectionItems = items?.filter { (item) -> Bool in + if item.type == .file { + return false + } + + return true + } + fileItems.append(contentsOf: subFileItems!) + collectionItems.append(contentsOf: subCollectionItems!) + dispatchGroup.leave() + } + } + dispatchGroup.wait() + + let hudViewController = DownloadItemsHUDViewController(core: core, downloadItems: fileItems as [OCItem]) { [weak hostViewController] (error, downloadedItems) in + + var unifiedItems = downloadedItems + + unifiedItems?.append(contentsOf: + collectionItems.map { (item) -> DownloadItem in + return DownloadItem(file: OCFile(), item: item) + }) + + if let error = error { + if (error as NSError).isOCError(withCode: .cancelled) { + return + } + + let appName = OCAppIdentity.shared.appName ?? "ownCloud" + let alertController = ThemedAlertController(with: "Cannot connect to ".localized + appName, message: appName + " couldn't download file(s)".localized, okLabel: "OK".localized, action: nil) + + hostViewController?.present(alertController, animated: true) + } else { + guard let unifiedItems = unifiedItems, unifiedItems.count > 0, let viewController = hostViewController else { return } + + var zipName = self.defaultZipName + + if self.context.items.count == 1, let item = self.context.items.first { + zipName = String(format: "%@.zip", item.name ?? self.defaultZipName) + } + + let renameViewController = CompressViewController(with: nil, core: self.core, defaultName: zipName, stringValidator: { name in + if name.contains("/") || name.contains("\\") { + return (false, "File name cannot contain / or \\".localized) + } else { + return (true, nil) + } + }, completion: { newName, password, _ in + OnBackgroundQueue { + if let newName = newName, error == nil, let fileItem = self.context.items.first, let parentItem = fileItem.parentItem(from: core) { + let zipURL = FileManager.default.temporaryDirectory.appendingPathComponent(newName) + let error = ZIPArchive.compressContents(of: unifiedItems, fromBasePath: parentItem.path ?? "", asZipFile: zipURL, withPassword: password) + + if !self.upload(itemURL: zipURL, to: parentItem, name: zipURL.lastPathComponent) { + self.removeFiles(url: zipURL) + self.completed(with: NSError(ocError: .internal)) + return + } else { + self.removeFiles(url: zipURL) + self.completed() + } + } + } + }) + + renameViewController.navigationItem.title = "Compress".localized + + let navigationController = ThemeNavigationController(rootViewController: renameViewController) + navigationController.modalPresentationStyle = .overFullScreen + + viewController.present(navigationController, animated: true) + } + } + + hudViewController.presentHUDOn(viewController: hostViewController) + } + + private func removeFiles(url : URL) { + do { + try FileManager.default.removeItem(at: url) + } catch { + } + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem || location == .moreFolder { + if #available(iOS 13.0, *) { + return UIImage(systemName: "cube.box")?.tinted(with: Theme.shared.activeCollection.tintColor) + } else { + return UIImage(named: "cube")?.tinted(with: Theme.shared.activeCollection.tintColor) + } + } + + return nil + } + + internal func upload(itemURL: URL, to rootItem: OCItem, name: String) -> Bool { + if core != nil, let progress = itemURL.upload(with: core, at: rootItem) { + self.publish(progress: progress) + return true + } else { + Log.debug("Error setting up upload of \(Log.mask(name)) to \(Log.mask(rootItem.path))") + return false + } + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/DocumentEditingAction.swift b/ownCloud/Client/Actions/Actions+Extensions/DocumentEditingAction.swift index bbb766ffb..f326b051c 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/DocumentEditingAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/DocumentEditingAction.swift @@ -65,7 +65,7 @@ class DocumentEditingAction : Action { return } - let hudViewController = DownloadItemsHUDViewController(core: core, downloadItems: context.items) { [weak hostViewController] (error, files) in + let hudViewController = DownloadItemsHUDViewController(core: core, downloadItems: context.items) { [weak hostViewController] (error, downloadedItems) in if let error = error { if (error as NSError).isOCError(withCode: .cancelled) { return @@ -76,8 +76,8 @@ class DocumentEditingAction : Action { hostViewController?.present(alertController, animated: true) } else { - guard let files = files, files.count > 0, let viewController = hostViewController else { return } - if let fileURL = files.first?.url, let item = self.context.items.first { + guard let downloadedItems = downloadedItems, downloadedItems.count > 0, let viewController = hostViewController else { return } + if let fileURL = downloadedItems.first?.file.url, let item = self.context.items.first { let editDocumentViewController = EditDocumentViewController(with: fileURL, item: item, core: self.core) let navigationController = ThemeNavigationController(rootViewController: editDocumentViewController) navigationController.modalPresentationStyle = .overFullScreen diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift index 77a841c8c..7a84e2520 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift @@ -42,7 +42,7 @@ class OpenInAction: Action { return } - let hudViewController = DownloadItemsHUDViewController(core: core, downloadItems: context.items) { [weak hostViewController] (error, files) in + let hudViewController = DownloadItemsHUDViewController(core: core, downloadItems: context.items) { [weak hostViewController] (error, downloadedItems) in if let error = error { if (error as NSError).isOCError(withCode: .cancelled) { return @@ -53,10 +53,10 @@ class OpenInAction: Action { hostViewController?.present(alertController, animated: true) } else { - guard let files = files, files.count > 0, let viewController = hostViewController else { return } + guard let downloadedItems = downloadedItems, downloadedItems.count > 0, let viewController = hostViewController else { return } // UIDocumentInteractionController can only be used with a single file - if files.count == 1 { - if let fileURL = files.first?.url { + if downloadedItems.count == 1 { + if let fileURL = downloadedItems.first?.file.url { // Make sure self is around until interactionControllerDispatchGroup.leave() is called by the documentInteractionControllerDidDismissOptionsMenu delegate method implementation self.interactionControllerDispatchGroup = DispatchGroup() self.interactionControllerDispatchGroup?.enter() @@ -90,8 +90,8 @@ class OpenInAction: Action { } } else { // Handle multiple files with a fallback solution - let urls = files.map { (file) -> URL in - return file.url! + let urls = downloadedItems.map { (item) -> URL in + return item.file.url! } let activityController = UIActivityViewController(activityItems: urls, applicationActivities: nil) diff --git a/ownCloud/Client/Actions/Actions+Extensions/UncompressAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UncompressAction.swift new file mode 100644 index 000000000..c1fc92955 --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/UncompressAction.swift @@ -0,0 +1,174 @@ +// +// UncompressAction.swift +// ownCloud +// +// Created by Matthias Hühne on 05/06/2020. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2020, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import ownCloudSDK +import ownCloudApp + +class UncompressAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.uncompress") } + override class var category : ActionCategory? { return .normal } + override class var name : String { return "Uncompress".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar, .keyboardShortcut] } + override class var keyCommand : String? { return "Z" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } + class var supportedMimeTypes : [String] { return ["application/zip"] } + + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + if let core = forContext.core, forContext.items.count == 1, forContext.items.contains(where: {$0.type == .file && ($0.permissions.contains(.writable) || $0.parentItem(from: core)? .permissions.contains(.createFile) == true)}) { + if let item = forContext.items.first, let mimeType = item.mimeType { + if supportedMimeTypes.filter({ + if mimeType.contains($0) { + return true + } + + return false + }).count == 1 { + return .middle + } + } + } + + // Examine items in context + return .none + } + + override func run() { + guard context.items.count > 0, let hostViewController = context.viewController, let core = self.core else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + let hudViewController = DownloadItemsHUDViewController(core: core, downloadItems: context.items as [OCItem]) { [weak hostViewController] (error, downloadedItems) in + + if let downloadedItems = downloadedItems, let downloadedItem = downloadedItems.first, error == nil, let fileItem = self.context.items.first, let filename = fileItem.name, let parentItem = fileItem.parentItem(from: core), let fileURL = downloadedItem.file.url { + + if ZIPArchive.isZipFileEncrypted(fileURL) { + let alertController = UIAlertController(title: "Enter Password".localized, message: String(format: "The document \"%@\" is password protected.\nPlease enter the password to uncompress the document.".localized, filename), preferredStyle: .alert) + alertController.addTextField { textField in + textField.placeholder = "Password".localized + textField.isSecureTextEntry = true + } + let confirmAction = UIAlertAction(title: "OK".localized, style: .default) { [weak alertController] _ in + guard let alertController = alertController, let textField = alertController.textFields?.first, let password = textField.text else { return } + + if ZIPArchive.checkPassword(password, forZipFile: fileURL) == true { + self.uncompressContents(of: fileURL, fileItem: fileItem, parentItem: parentItem, password: password, core: core) + } else { + let alert = UIAlertController(title: "Wrong Password".localized, message: "The archive could not be uncompressed with the provided password.".localized, preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: { _ in + self.completed() + })) + + hostViewController?.present(alert, animated: true, completion: nil) + } + } + alertController.addAction(confirmAction) + let cancelAction = UIAlertAction(title: "Cancel".localized, style: .cancel, handler: { _ in + self.completed() + }) + alertController.addAction(cancelAction) + + hostViewController?.present(alertController, animated: true, completion: nil) + } else { + self.uncompressContents(of: fileURL, fileItem: fileItem, parentItem: parentItem, password: nil, core: core) + } + } + } + + hudViewController.presentHUDOn(viewController: hostViewController) + } + + func uncompressContents(of zipFile: URL, fileItem: OCItem, parentItem: OCItem, password: String?, core: OCCore) { + if let parentPath = parentItem.path, let fileName = fileItem.path { + let zipItems = ZIPArchive.uncompressContents(ofZipFile: zipFile, parentItem: parentItem, withPassword: nil, with: core) + /* + for item in zipItems { + print("--> \(item.filepath) \(item.isDirectory) \(item.absolutePath)") + }*/ + + let collectionItems = zipItems.filter { (item) -> Bool in + return item.isDirectory + } + + let fileItems = zipItems.filter { (item) -> Bool in + return !item.isDirectory + } + let fileName = ((fileName as NSString).lastPathComponent as NSString).deletingPathExtension + + let dispatchGroup = DispatchGroup() + // Todo: Should be repleaced by SDK function! + core.createFolder(fileName, inside: parentItem, options: [ + .returnImmediatelyIfOfflineOrUnavailable : true, + .addTemporaryClaimForPurpose : OCCoreClaimPurpose.view.rawValue + ]) { (error, subcore, containerItem, _) in + + guard let containerItem = containerItem else { return } + var lastItem = containerItem + for collectionItem in collectionItems { + OnMainThread { + dispatchGroup.enter() + let newFolderPath = (collectionItem.filepath as NSString).lastPathComponent + + var insideItem = containerItem + if let lastpath = lastItem.path, lastpath.hasSuffix(collectionItem.filepath) { + insideItem = lastItem + } + print("--> create folder \(newFolderPath) in \(insideItem.path) \(collectionItem.filepath)") + + subcore.createFolder(newFolderPath, inside: insideItem, options: [ + .returnImmediatelyIfOfflineOrUnavailable : true, + .addTemporaryClaimForPurpose : OCCoreClaimPurpose.view.rawValue + ]) { (error, core, item, _) in + print("create folder finished \(item?.path)") + if let item = item { + lastItem = item + } + dispatchGroup.leave() + } + dispatchGroup.wait() + self.completed() + } + } + } + } + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem || location == .moreFolder { + if #available(iOS 13.0, *) { + return UIImage(systemName: "cube.box")?.tinted(with: Theme.shared.activeCollection.tintColor) + } else { + return UIImage(named: "cube")?.tinted(with: Theme.shared.activeCollection.tintColor) + } + } + + return nil + } + + internal func upload(itemURL: URL, to rootItem: OCItem, name: String) -> Bool { + + if core != nil, let progress = itemURL.upload(with: core, at: rootItem) { + self.publish(progress: progress) + return true + } else { + Log.debug("Error setting up upload of \(Log.mask(name)) to \(Log.mask(rootItem.path))") + return false + } + } +} diff --git a/ownCloud/Client/Actions/CompressViewController.swift b/ownCloud/Client/Actions/CompressViewController.swift new file mode 100644 index 000000000..1f9ab0090 --- /dev/null +++ b/ownCloud/Client/Actions/CompressViewController.swift @@ -0,0 +1,138 @@ +// +// CompressViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 05/4/2020. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2020, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import UIKit +import ownCloudSDK + +class CompressViewController: NamingViewController { + + private var passwordTextField: UITextField + private var passwordSwitch: UISwitch + private var passwordLabel: UILabel + var completionHandler: (String?, String?, NamingViewController) -> Void + private let zipExtension = ".zip" + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public init(with item: OCItem? = nil, core: OCCore? = nil, defaultName: String? = nil, stringValidator: StringValidatorHandler? = nil, completion: @escaping (String?, String?, NamingViewController) -> Void) { + + passwordTextField = UITextField(frame: .zero) + passwordTextField.accessibilityIdentifier = "pasword-text-field" + passwordSwitch = UISwitch(frame: .zero) + passwordSwitch.accessibilityIdentifier = "password-switch" + passwordLabel = UILabel(frame: .zero) + completionHandler = completion + + super.init(with: item, core: core, defaultName: defaultName, stringValidator: stringValidator, completion: { _, _ in + }) + } + + override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + super.applyThemeCollection(theme: theme, collection: collection, event: event) + + passwordTextField.backgroundColor = collection.tableBackgroundColor + passwordTextField.textColor = collection.tableRowColors.labelColor + passwordTextField.keyboardAppearance = collection.keyboardAppearance + passwordLabel.textColor = collection.tableRowColors.labelColor + } + + override func viewDidLoad() { + super.viewDidLoad() + + completion = {name, controller in + self.passwordTextField.resignFirstResponder() + if self.passwordSwitch.isOn { + self.completionHandler(name, self.passwordTextField.text, controller) + } else { + self.completionHandler(name, nil, controller) + } + } + + // Password switch + passwordSwitch.translatesAutoresizingMaskIntoConstraints = false + nameContainer.addSubview(passwordSwitch) + + NSLayoutConstraint.activate([ + passwordSwitch.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 30), + passwordSwitch.leftAnchor.constraint(equalTo: nameContainer.leftAnchor, constant: 30) + ]) + + // Password label + passwordLabel.translatesAutoresizingMaskIntoConstraints = false + nameContainer.addSubview(passwordLabel) + + passwordLabel.text = "Protect file with password".localized + + NSLayoutConstraint.activate([ + passwordLabel.centerYAnchor.constraint(equalTo: passwordSwitch.centerYAnchor), + passwordLabel.heightAnchor.constraint(equalToConstant: 40), + passwordLabel.leftAnchor.constraint(equalTo: passwordSwitch.rightAnchor, constant: 15), + passwordLabel.rightAnchor.constraint(equalTo: nameContainer.rightAnchor, constant: -20) + ]) + + // Password textfield + passwordTextField.translatesAutoresizingMaskIntoConstraints = false + nameContainer.addSubview(passwordTextField) + NSLayoutConstraint.activate([ + passwordTextField.topAnchor.constraint(equalTo: passwordLabel.bottomAnchor, constant: 15), + passwordTextField.heightAnchor.constraint(equalToConstant: 40), + passwordTextField.leftAnchor.constraint(equalTo: nameContainer.leftAnchor, constant: 30), + passwordTextField.rightAnchor.constraint(equalTo: nameContainer.rightAnchor, constant: -20) + ]) + + passwordSwitch.isOn = false + + passwordTextField.delegate = self + passwordTextField.textAlignment = .center + passwordTextField.becomeFirstResponder() + passwordTextField.addTarget(self, action: #selector(textfieldDidChange(_:)), for: .editingChanged) + passwordTextField.enablesReturnKeyAutomatically = true + passwordTextField.autocorrectionType = .no + passwordTextField.isSecureTextEntry = true + passwordTextField.borderStyle = .roundedRect + passwordTextField.clearButtonMode = .always + passwordTextField.accessibilityLabel = "Password".localized + passwordTextField.placeholder = "Password".localized + } + + override func textfieldDidChange(_ sender: UITextField) { + if sender.isEqual(passwordTextField), sender.text?.count ?? 0 > 0 { + passwordSwitch.setOn(true, animated: true) + } + } +} + +extension CompressViewController { + + override func textFieldDidBeginEditing(_ textField: UITextField) { + + if textField.isEqual(nameTextField) { + if let name = nameTextField.text, + let range = name.range(of: zipExtension), + let position: UITextPosition = nameTextField.position(from: nameTextField.beginningOfDocument, offset: range.lowerBound.utf16Offset(in: name)) { + + textField.selectedTextRange = nameTextField.textRange(from: nameTextField.beginningOfDocument, to:position) + + } else { + textField.selectedTextRange = nameTextField.textRange(from: nameTextField.beginningOfDocument, to: nameTextField.endOfDocument) + } + } + } +} diff --git a/ownCloud/Client/Actions/MoreViewController.swift b/ownCloud/Client/Actions/MoreViewController.swift index f11a71635..da671d23d 100644 --- a/ownCloud/Client/Actions/MoreViewController.swift +++ b/ownCloud/Client/Actions/MoreViewController.swift @@ -21,7 +21,7 @@ import ownCloudSDK class MoreViewController: UIViewController, CardPresentationSizing { - private var item: OCItem? + //private var item: OCItem? private weak var core: OCCore? private var headerView: UIView @@ -35,8 +35,8 @@ class MoreViewController: UIViewController, CardPresentationSizing { } } - init(item: OCItem, core: OCCore, header: UIView, viewController: UIViewController) { - self.item = item + init(core: OCCore, header: UIView, viewController: UIViewController) { + //self.item = item self.core = core self.headerView = header self.viewController = viewController diff --git a/ownCloud/Client/Actions/MoreViewHeader.swift b/ownCloud/Client/Actions/MoreViewHeader.swift index 6d4e7c505..7db5ce7a8 100644 --- a/ownCloud/Client/Actions/MoreViewHeader.swift +++ b/ownCloud/Client/Actions/MoreViewHeader.swift @@ -38,6 +38,7 @@ class MoreViewHeader: UIView { var item: OCItem weak var core: OCCore? var url: URL? + var titleString: String? init(for item: OCItem, with core: OCCore, favorite: Bool = true, adaptBackgroundColor: Bool = false, showActivityIndicator: Bool = false) { self.item = item @@ -85,6 +86,30 @@ class MoreViewHeader: UIView { render() } + init(title : String) { + self.showFavoriteButton = false + self.showActivityIndicator = false + self.adaptBackgroundColor = false + self.item = OCItem() + self.url = nil + titleString = title + + iconView = UIImageView() + titleLabel = UILabel() + detailLabel = UILabel() + labelContainerView = UIView() + favoriteButton = UIButton() + activityIndicator = UIActivityIndicatorView(style: .white) + + super.init(frame: .zero) + + self.translatesAutoresizingMaskIntoConstraints = false + + Theme.shared.register(client: self) + + render() + } + deinit { Theme.shared.unregister(client: self) } @@ -181,6 +206,8 @@ class MoreViewHeader: UIView { } catch { print("Error: \(error)") } + } else if let titleString = titleString { + titleLabel.attributedText = NSAttributedString(string: titleString, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold)]) } else { titleLabel.attributedText = NSAttributedString(string: item.name ?? "", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold)]) diff --git a/ownCloud/Client/Actions/NamingViewController.swift b/ownCloud/Client/Actions/NamingViewController.swift index c9432b067..9617c3731 100644 --- a/ownCloud/Client/Actions/NamingViewController.swift +++ b/ownCloud/Client/Actions/NamingViewController.swift @@ -25,19 +25,20 @@ typealias StringValidatorHandler = (String) -> StringValidatorResult class NamingViewController: UIViewController, Themeable { weak var item: OCItem? weak var core: OCCore? - var completion: (String?, NamingViewController) -> Void + typealias NamingCompletionHandler = (String?, NamingViewController) -> Void + var completion: NamingCompletionHandler var stringValidator: StringValidatorHandler? var defaultName: String? private var blurView: UIVisualEffectView - private var stackView: UIStackView + var stackView: UIStackView private var thumbnailContainer: UIView private var thumbnailImageView: UIImageView - private var nameContainer: UIView - private var nameTextField: UITextField + var nameContainer: UIView + var nameTextField: UITextField private var textfieldTopAnchorConstraint: NSLayoutConstraint private var textfieldCenterYAnchorConstraint: NSLayoutConstraint @@ -52,7 +53,7 @@ class NamingViewController: UIViewController, Themeable { private let thumbnailSize = CGSize(width: 150.0, height: 150.0) - init(with item: OCItem? = nil, core: OCCore? = nil, defaultName: String? = nil, stringValidator: StringValidatorHandler? = nil, completion: @escaping (String?, NamingViewController) -> Void) { + public init(with item: OCItem? = nil, core: OCCore? = nil, defaultName: String? = nil, stringValidator: StringValidatorHandler? = nil, completion: @escaping (String?, NamingViewController) -> Void) { self.item = item self.core = core self.completion = completion diff --git a/ownCloud/Client/ClientQueryViewController.swift b/ownCloud/Client/ClientQueryViewController.swift index 9bc88a0ee..4d78b14a3 100644 --- a/ownCloud/Client/ClientQueryViewController.swift +++ b/ownCloud/Client/ClientQueryViewController.swift @@ -52,6 +52,7 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac var copyMultipleBarButtonItem: UIBarButtonItem? var openMultipleBarButtonItem: UIBarButtonItem? + var folderActionMultipleBarButton: UIBarButtonItem? var folderActionBarButton: UIBarButtonItem? var plusBarButton: UIBarButtonItem? var selectDeselectAllButtonItem: UIBarButtonItem? @@ -140,6 +141,7 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac self.tableView.dragInteractionEnabled = true self.tableView.allowsMultipleSelectionDuringEditing = true + folderActionMultipleBarButton = UIBarButtonItem(image: UIImage(named: "more-dots"), style: .plain, target: self, action: #selector(multipleMoreBarButtonPressed)) folderActionBarButton = UIBarButtonItem(image: UIImage(named: "more-dots"), style: .plain, target: self, action: #selector(moreBarButtonPressed)) folderActionBarButton?.accessibilityIdentifier = "client.folder-action" plusBarButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(plusBarButtonPressed)) @@ -151,6 +153,7 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac exitMultipleSelectionBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(exitMultipleSelection)) // Create bar button items for the toolbar + deleteMultipleBarButtonItem?.isEnabled = true deleteMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named:"trash"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: DeleteAction.identifier!) deleteMultipleBarButtonItem?.isEnabled = false @@ -381,6 +384,8 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac for item in toolbarItems { if self.actions?.contains(where: {type(of:$0).identifier == item.actionIdentifier}) ?? false { item.isEnabled = true + } else if item.isEqual(folderActionMultipleBarButton) { + item.isEnabled = true } else { item.isEnabled = false } @@ -444,9 +449,9 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac flexibleSpaceBarButton, copyMultipleBarButtonItem!, flexibleSpaceBarButton, - duplicateMultipleBarButtonItem!, + deleteMultipleBarButtonItem!, flexibleSpaceBarButton, - deleteMultipleBarButtonItem!]) + folderActionMultipleBarButton!]) } @objc func actOnMultipleItems(_ sender: UIButton) { @@ -566,7 +571,37 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreFolder) let actionContext = ActionContext(viewController: self, core: core, query: query, items: [rootItem], location: actionsLocation, sender: sender) - if let moreViewController = Action.cardViewController(for: rootItem, with: actionContext, progressHandler: makeActionProgressHandler()) { + if let moreViewController = Action.cardViewController(for: [rootItem], with: actionContext, progressHandler: makeActionProgressHandler()) { + self.present(asCard: moreViewController, animated: true) + } + } + + @objc func multipleMoreBarButtonPressed(_ sender: UIBarButtonItem) { + guard let core = core else { + return + } + + var selectedItems = [OCItem]() + + // Do we have selected items? + if let selectedIndexPaths = self.tableView.indexPathsForSelectedRows { + if selectedIndexPaths.count > 0 { + + // Get array of OCItems from selected table view index paths + for indexPath in selectedIndexPaths { + if let item = itemAt(indexPath: indexPath) { + selectedItems.append(item) + } + } + } + } + + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreFolder) + let actionContext = ActionContext(viewController: self, core: core, query: query, items: selectedItems, location: actionsLocation, sender: sender) + + print("--> multipleMoreBarButtonPressed \(selectedItems)") + + if let moreViewController = Action.cardViewController(for: selectedItems, with: actionContext, progressHandler: makeActionProgressHandler()) { self.present(asCard: moreViewController, animated: true) } } diff --git a/ownCloud/Client/Viewer/DisplayViewController.swift b/ownCloud/Client/Viewer/DisplayViewController.swift index 5c03412ed..249bdad01 100644 --- a/ownCloud/Client/Viewer/DisplayViewController.swift +++ b/ownCloud/Client/Viewer/DisplayViewController.swift @@ -396,7 +396,7 @@ class DisplayViewController: UIViewController, OCQueryDelegate { let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreItem) let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation, sender: sender) - if let moreViewController = Action.cardViewController(for: item, with: actionContext, completionHandler: nil) { + if let moreViewController = Action.cardViewController(for: [item], with: actionContext, completionHandler: nil) { self.present(asCard: moreViewController, animated: true) } } diff --git a/ownCloud/Client/Viewer/DownloadItemsHUDViewController.swift b/ownCloud/Client/Viewer/DownloadItemsHUDViewController.swift index c44e25bf5..23584b213 100644 --- a/ownCloud/Client/Viewer/DownloadItemsHUDViewController.swift +++ b/ownCloud/Client/Viewer/DownloadItemsHUDViewController.swift @@ -18,12 +18,13 @@ import UIKit import ownCloudSDK +import ownCloudApp -typealias DownloadItemsHUDViewControllerCompletionHandler = (Error?, [OCFile]?) -> Void +typealias DownloadItemsHUDViewControllerCompletionHandler = (Error?, [DownloadItem]?) -> Void class DownloadItemsHUDViewController: CardViewController { var items : [OCItem] - var downloadedFiles : [OCFile] = [OCFile]() + var downloadedItems : [DownloadItem] = [DownloadItem]() var downloadError : Error? var downloadProgress : [Progress] = [Progress]() var completion : DownloadItemsHUDViewControllerCompletionHandler? @@ -123,7 +124,7 @@ class DownloadItemsHUDViewController: CardViewController { if core.localCopy(of: item) != nil { if let file = item.file(with: core) { core.registerUsage(of: item, completionHandler: nil) - downloadedFiles.append(file) + downloadedItems.append(DownloadItem(file: file, item: item)) return false } @@ -145,7 +146,6 @@ class DownloadItemsHUDViewController: CardViewController { for item in items { downloadGroup.enter() - if let progress = core.downloadItem(item, options: [ .returnImmediatelyIfOfflineOrUnavailable : true, .addTemporaryClaimForPurpose : OCCoreClaimPurpose.view.rawValue @@ -155,7 +155,7 @@ class DownloadItemsHUDViewController: CardViewController { self.downloadError = error } else { if let file = file { - self.downloadedFiles.append(file) + self.downloadedItems.append(DownloadItem(file: file, item: item)) if let claim = file.claim { self.core?.remove(claim, on: item, afterDeallocationOf: [self]) @@ -180,7 +180,7 @@ class DownloadItemsHUDViewController: CardViewController { if let error = self.downloadError { self.completed(with: error) } else { - self.completed(files: self.downloadedFiles) + self.completed(items: self.downloadedItems) } }) } @@ -189,7 +189,7 @@ class DownloadItemsHUDViewController: CardViewController { }) } else { // Done - completed(files: downloadedFiles) + completed(items: downloadedItems) } } else { // No core @@ -197,9 +197,9 @@ class DownloadItemsHUDViewController: CardViewController { } } - func completed(with error: Error? = nil, files: [OCFile]? = nil) { + func completed(with error: Error? = nil, items: [DownloadItem]? = nil) { if let completion = completion { - completion(error, files) + completion(error, items) self.completion = nil } } diff --git a/ownCloud/FileLists/FileListTableViewController.swift b/ownCloud/FileLists/FileListTableViewController.swift index 7e32db816..4eac2919a 100644 --- a/ownCloud/FileLists/FileListTableViewController.swift +++ b/ownCloud/FileLists/FileListTableViewController.swift @@ -74,7 +74,7 @@ class FileListTableViewController: UITableViewController, ClientItemCellDelegate let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreItem) let actionContext = ActionContext(viewController: self, core: core, query: query, items: [item], location: actionsLocation, sender: cell) - if let moreViewController = Action.cardViewController(for: item, with: actionContext, progressHandler: makeActionProgressHandler()) { + if let moreViewController = Action.cardViewController(for: [item], with: actionContext, progressHandler: makeActionProgressHandler()) { self.present(asCard: moreViewController, animated: true) } } diff --git a/ownCloud/Resources/Assets.xcassets/cube.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/cube.imageset/Contents.json new file mode 100644 index 000000000..071e0c843 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/cube.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "cube.box.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ownCloud/Resources/Assets.xcassets/cube.imageset/cube.box.png b/ownCloud/Resources/Assets.xcassets/cube.imageset/cube.box.png new file mode 100644 index 000000000..17dfada08 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/cube.imageset/cube.box.png differ diff --git a/ownCloud/Resources/Info.plist b/ownCloud/Resources/Info.plist index e417984f6..84e9d0547 100644 --- a/ownCloud/Resources/Info.plist +++ b/ownCloud/Resources/Info.plist @@ -64,6 +64,7 @@ This permission is needed to upload photos and videos from your photo library. NSUserActivityTypes + CompressPathItemsIntent CreateFolderIntent DeletePathItemIntent GetAccountIntent diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index c207a7342..44aba024e 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -593,3 +593,12 @@ "trial" = "trial"; "purchase" = "purchase"; "subscription" = "subscription"; + +/* Compress / Uncompress */ +"Enter Password" = "Enter Password"; +"The document \"%@\" is password protected.\nPlease enter the password to uncompress the document." = "The document \"%@\" is password protected.\nPlease enter the password to uncompress the document."; +"Wrong Password" = "Wrong Password"; +"The archive could not be uncompressed with the provided password." = "The archive could not be uncompressed with the provided password."; +"Compress" = "Compress"; +"Uncompress" = "Uncompress"; +"Protect file with password" = "Protect file with password"; diff --git a/ownCloud/SDK Extensions/OCCore+Extension.swift b/ownCloud/SDK Extensions/OCCore+Extension.swift index 97abf44cb..02fdf6734 100644 --- a/ownCloud/SDK Extensions/OCCore+Extension.swift +++ b/ownCloud/SDK Extensions/OCCore+Extension.swift @@ -18,6 +18,7 @@ import Foundation import ownCloudSDK +import ownCloudApp extension OCCore { @@ -143,4 +144,37 @@ extension OCCore { return parentItems.reversed() } + + func retrieveSubItems(for item: OCItem, completionHandler: ((_ items: [OCItem]?) -> Void)? = nil) { + var newHandler = completionHandler + let subitemsQuery = OCQuery(condition: .require([ + .where(.path, startsWith: item.path!) + ]), inputFilter:nil) + + var items : [OCItem]? + + subitemsQuery.changesAvailableNotificationHandler = { [weak self] query in + items = query.queryResults + self?.stop(query) + newHandler?(items) + newHandler = nil + } + self.start(subitemsQuery) + } + + func localFile(for item: OCItem, completionHandler: @escaping (_ item: DownloadItem?) -> Void) { + if self.localCopy(of: item) == nil { + self.downloadItem(item, options: [ .returnImmediatelyIfOfflineOrUnavailable : true ], resultHandler: { (error, core, item, file) in + if error == nil, let item = item, let file = item.file(with: core) { + completionHandler(DownloadItem(file: file, item: item)) + } else { + completionHandler(nil) + } + }) + } else if let file = item.file(with: self) { + completionHandler(DownloadItem(file: file, item: item)) + } else { + completionHandler(nil) + } + } } diff --git a/ownCloudAppFramework/ZIP Archive/ZIPArchive.h b/ownCloudAppFramework/ZIP Archive/ZIPArchive.h index 55c701173..2a9383e2e 100644 --- a/ownCloudAppFramework/ZIP Archive/ZIPArchive.h +++ b/ownCloudAppFramework/ZIP Archive/ZIPArchive.h @@ -20,9 +20,41 @@ NS_ASSUME_NONNULL_BEGIN +@interface DownloadItem : NSObject +{ + OCFile *_file; + OCItem *_item; +} + +@property(strong,nonatomic) OCFile *file; +@property(strong,nonatomic) OCItem *item; + +- (instancetype)initWithFile:(OCFile *)file item:(OCItem *)item; + +@end + +@interface ZipFileItem : NSObject +{ + NSString *_filepath; + BOOL _isDirectory; + NSString *_absolutePath; +} + +@property(strong,nonatomic) NSString *filepath; +@property(assign,nonatomic) BOOL isDirectory; +@property(strong,nonatomic) NSString *absolutePath; + +- (instancetype)initWithFilepath:(NSString *)filepath isDirectory:(BOOL)isDirectory absolutePath:(NSString *)absolutePath; + +@end + @interface ZIPArchive : NSObject + (NSError *)compressContentsOf:(NSURL *)sourceDirectory asZipFile:(NSURL *)zipFileURL; ++ (nullable NSError *)compressContentsOfItems:(NSArray *)sourceDirectorie fromBasePath:(NSString *)basePath asZipFile:(NSURL *)zipFileURL withPassword:(nullable NSString *)password; ++ (NSArray *)uncompressContentsOfZipFile:(NSURL *)zipFileURL parentItem:(OCItem *)parentItem withPassword:(nullable NSString *)password withCore:(OCCore *)core; ++ (BOOL)isZipFileEncrypted:(NSURL *)zipFileURL; ++ (BOOL)checkPassword:(NSString *)password forZipFile:(NSURL *)zipFileURL; @end diff --git a/ownCloudAppFramework/ZIP Archive/ZIPArchive.m b/ownCloudAppFramework/ZIP Archive/ZIPArchive.m index e76476a27..7ef6ddc19 100644 --- a/ownCloudAppFramework/ZIP Archive/ZIPArchive.m +++ b/ownCloudAppFramework/ZIP Archive/ZIPArchive.m @@ -19,6 +19,39 @@ #import #import #import "ZIPArchive.h" +#import "OCCore+BundleImport.h" + + +@implementation DownloadItem + +- (instancetype)initWithFile:(OCFile *)file item:(OCItem *)item +{ + if ((self = [super init]) != nil) + { + self.file = file; + self.item = item; + } + + return(self); +} + +@end + +@implementation ZipFileItem + +- (instancetype)initWithFilepath:(NSString *)filepath isDirectory:(BOOL)isDirectory absolutePath:(NSString *)absolutePath +{ + if ((self = [super init]) != nil) + { + self.filepath = filepath; + self.isDirectory = isDirectory; + self.absolutePath = absolutePath; + } + + return(self); +} + +@end @implementation ZIPArchive @@ -96,6 +129,33 @@ + (NSError *)compressContentsOf:(NSURL *)sourceDirectory asZipFile:(NSURL *)zipF } } } + } else { + NSURL *addURL = sourceDirectory; + zip_source_t *fileSource = NULL; + NSNumber *isDirectory = nil, *size = nil; + + NSString *relativePath = addURL.path.lastPathComponent; + [addURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; + [addURL getResourceValue:&size forKey:NSURLFileSizeKey error:nil]; + + // Add file + if ((fileSource = zip_source_file(zipArchive, addURL.path.UTF8String, 0, (zip_int64_t)size.unsignedIntegerValue)) != NULL) + { + if (zip_file_add(zipArchive, relativePath.UTF8String, fileSource, ZIP_FL_ENC_UTF_8) < 0) + { + // Error + error = ErrorFromZipArchive(zipArchive); + OCLogError(@"Error compressing %@: %@", relativePath, error); + + zip_source_free(fileSource); + } + } + else + { + // Error + error = ErrorFromZipArchive(zipArchive); + OCLogError(@"Error adding directory %@: %@", relativePath, error); + } } if (zip_close(zipArchive) < 0) @@ -117,6 +177,235 @@ + (NSError *)compressContentsOf:(NSURL *)sourceDirectory asZipFile:(NSURL *)zipF return (error); } ++ (nullable NSError *)compressContentsOfItems:(NSArray *)sourceItems fromBasePath:(NSString *)basePath asZipFile:(NSURL *)zipFileURL withPassword:(nullable NSString *)password +{ + zip_t *zipArchive = NULL; + int zipError = ZIP_ER_OK; + NSError *error = nil; + NSError *(^ErrorFromZipArchive)(zip_t *zipArchive) = ^(zip_t *zipArchive) { + zip_error_t *zipError = zip_get_error(zipArchive); + + return ([NSError errorWithDomain:LibZipErrorDomain code:zipError->zip_err userInfo:nil]); + }; + + if ((zipArchive = zip_open(zipFileURL.path.UTF8String, ZIP_CREATE, &zipError)) != NULL) + { + zip_uint64_t index = 0; + for (DownloadItem *sourceItem in sourceItems) + { + NSURL *addURL = sourceItem.file.url; + zip_source_t *fileSource = NULL; + NSNumber *size = nil; + NSString *relativePath = [sourceItem.item.path stringByReplacingOccurrencesOfString:basePath withString:@"/"]; + + if (sourceItem.item.type == OCItemTypeCollection) + { + // Add directory + OCLogDebug(@"Adding directory %@ from %@", relativePath, addURL); + + if (zip_dir_add(zipArchive, relativePath.UTF8String, ZIP_FL_ENC_UTF_8) < 0) + { + // Error + error = ErrorFromZipArchive(zipArchive); + OCLogError(@"Error adding directory %@: %@", relativePath, error); + } + } + else + { + // Add file + OCLogDebug(@"Adding file %@ from %@", relativePath, addURL); + + if ((fileSource = zip_source_file(zipArchive, addURL.path.UTF8String, 0, (zip_int64_t)size.unsignedIntegerValue)) != NULL) + { + if (zip_file_add(zipArchive, relativePath.UTF8String, fileSource, ZIP_FL_ENC_UTF_8) < 0) + { + // Error + error = ErrorFromZipArchive(zipArchive); + OCLogError(@"Error compressing %@: %@", relativePath, error); + + zip_source_free(fileSource); + } else { + if (password != nil) { + zip_file_set_encryption(zipArchive, index, ZIP_EM_AES_256, (const char*)[password UTF8String]); + } + index ++; + } + } + else + { + // Error + error = ErrorFromZipArchive(zipArchive); + OCLogError(@"Error adding directory %@: %@", relativePath, error); + } + } + } + } + else + { + // Error + error = [NSError errorWithDomain:LibZipErrorDomain code:zipError userInfo:nil]; + OCLogError(@"Error opening zip archive %@: %@", zipFileURL.path, error); + } + + if (zip_close(zipArchive) < 0) + { + // Error + error = ErrorFromZipArchive(zipArchive); + OCLogError(@"Error closing zip archive %@: %@", zipFileURL.path, error); + + zip_discard(zipArchive); + } + + return (error); +} + ++ (NSArray *)uncompressContentsOfZipFile:(NSURL *)zipFileURL parentItem:(OCItem *)parentItem withPassword:(nullable NSString *)password withCore:(OCCore *)core +{ + NSMutableArray *zipItems = [NSMutableArray new]; + zip_t *zipArchive = NULL; + int zipError = ZIP_ER_OK; + struct zip_stat sb; + struct zip_file *zf; + + char buf[100]; + long long sum; + NSInteger len; + NSError *error = nil; + + NSError *(^ErrorFromZipArchive)(zip_t *zipArchive) = ^(zip_t *zipArchive) { + zip_error_t *zipError = zip_get_error(zipArchive); + + return ([NSError errorWithDomain:LibZipErrorDomain code:zipError->zip_err userInfo:nil]); + }; + + if ((zipArchive = zip_open(zipFileURL.path.UTF8String, ZIP_RDONLY, &zipError)) != NULL) + { + if (password != nil) { + zip_set_default_password(zipArchive, (const char*)[password UTF8String]); + } + NSURL *tmpURL = [zipFileURL URLByDeletingPathExtension]; + + [NSFileManager.defaultManager createDirectoryAtURL:tmpURL withIntermediateDirectories:NO attributes:nil error:nil]; + + for (NSInteger i = 0; i < zip_get_num_entries(zipArchive, 0); i++) { + if (zip_stat_index(zipArchive, i, 0, &sb) == 0) { + printf("==================/n"); + len = strlen(sb.name); + printf("Name: [%s], ", sb.name); + printf("Size: [%llu], ", sb.size); + printf("mtime: [%u]/n", (unsigned int)sb.mtime); + + + NSString *filePath = [tmpURL.path stringByAppendingPathComponent:[NSString stringWithUTF8String:sb.name]]; + + if (sb.name[len - 1] == '/') { + //safe_create_dir(sb.name); + ZipFileItem *item = [[ZipFileItem alloc] initWithFilepath:[NSString stringWithUTF8String:sb.name] isDirectory:YES absolutePath:filePath]; + [zipItems addObject:item]; + NSLog(@"--> create dir: %s", sb.name); + [NSFileManager.defaultManager createDirectoryAtURL:[tmpURL URLByAppendingPathComponent:[NSString stringWithUTF8String:sb.name]] withIntermediateDirectories:NO attributes:nil error:nil]; + } else { + NSLog(@"--> read file: %s", sb.name); + + zf = zip_fopen_index(zipArchive, i, 0); + if (!zf) { + error = ErrorFromZipArchive(zipArchive); + OCLogError(@"Error opening zip file %@", error); + } else { + NSMutableData *data = [NSMutableData new]; + sum = 0; + while (sum != sb.size) { + len = zip_fread(zf, buf, 100); + if (len < 0) { + error = ErrorFromZipArchive(zipArchive); + OCLogError(@"Error reading zip file %@", error); + } + [data appendBytes:buf length:len]; + sum += len; + } + + [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil]; + + ZipFileItem *item = [[ZipFileItem alloc] initWithFilepath:[NSString stringWithUTF8String:sb.name] isDirectory:NO absolutePath:filePath]; + [zipItems addObject:item]; + + NSLog(@"--> zipitems %@", zipItems); + + zip_fclose(zf); + } + } + } else { + printf("File[%s] Line[%d]/n", __FILE__, __LINE__); + } + } + + } + + if (zip_close(zipArchive) < 0) + { + // Error + error = ErrorFromZipArchive(zipArchive); + OCLogError(@"Error closing zip archive %@: %@", zipFileURL.path, error); + + zip_discard(zipArchive); + } + + return zipItems; + //return (error); +} + ++ (BOOL)isZipFileEncrypted:(NSURL *)zipFileURL +{ + zip_t *zipArchive = NULL; + int zipError = ZIP_ER_OK; + + if ((zipArchive = zip_open(zipFileURL.path.UTF8String, ZIP_RDONLY, &zipError)) != NULL) + { + struct zip_file *zf; + zf = zip_fopen_index(zipArchive, 0, 0); + if (!zf) { + return YES; + } + + if (zip_close(zipArchive) < 0) + { + // Error + zip_discard(zipArchive); + } + } + + return NO; +} + ++ (BOOL)checkPassword:(NSString *)password forZipFile:(NSURL *)zipFileURL +{ + zip_t *zipArchive = NULL; + int zipError = ZIP_ER_OK; + + if ((zipArchive = zip_open(zipFileURL.path.UTF8String, ZIP_RDONLY, &zipError)) != NULL) + { + struct zip_file *zf; + zf = zip_fopen_index_encrypted(zipArchive, 0, 0, (const char*)[password UTF8String]); + if (zf) { + if (zip_close(zipArchive) < 0) + { + // Error + zip_discard(zipArchive); + } + + return YES; + } + + if (zip_close(zipArchive) < 0) + { + // Error + zip_discard(zipArchive); + } + } + + return NO; +} + @end NSErrorDomain LibZipErrorDomain = @"LibZIPErrorDomain"; diff --git a/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition index dfb484931..e04c348b0 100644 --- a/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition +++ b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition @@ -126,11 +126,11 @@ INIntentDefinitionNamespace K5U8aR INIntentDefinitionSystemVersion - 19D76 + 19E266 INIntentDefinitionToolsBuildVersion - 11C504 + 11E146 INIntentDefinitionToolsVersion - 11.3.1 + 11.4 INIntents @@ -2506,6 +2506,323 @@ INIntentVerb Get + + INIntentCategory + generic + INIntentConfigurable + + INIntentDescription + Compress the given path items into a zip file + INIntentDescriptionID + hgRmHs + INIntentLastParameterTag + 9 + INIntentManagedParameterCombinations + + pathItems,account,filename,password + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Compress ${pathItems} of ${account} + INIntentParameterCombinationTitleID + oBIFKF + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + CompressPathItems + INIntentParameterCombinations + + pathItems,account,filename,password + + INIntentParameterCombinationIsLinked + + INIntentParameterCombinationIsPrimary + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Compress ${pathItems} of ${account} + INIntentParameterCombinationTitleID + x0Cl4V + + + INIntentParameters + + + INIntentParameterDisplayName + Path Items + INIntentParameterDisplayNameID + YnFYyb + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + pathItems + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Enter at minimum one path to which should be compressed + INIntentParameterPromptDialogFormatStringID + X1OVs4 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsMultipleValues + + INIntentParameterSupportsResolution + + INIntentParameterTag + 7 + INIntentParameterType + String + + + INIntentParameterCustomDisambiguation + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + gmaNlp + INIntentParameterDisplayPriority + 2 + INIntentParameterName + account + INIntentParameterObjectType + Account + INIntentParameterObjectTypeNamespace + K5U8aR + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + gMfROG + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogFormatString + Which one? + INIntentParameterPromptDialogFormatStringID + IWGvu6 + INIntentParameterPromptDialogType + DisambiguationSelection + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + Og0voM + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsResolution + + INIntentParameterTag + 5 + INIntentParameterType + Object + + + INIntentParameterDisplayName + Password (optional) + INIntentParameterDisplayNameID + n2gH1U + INIntentParameterDisplayPriority + 3 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + password + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Enter a password for the file, if needed + INIntentParameterPromptDialogFormatStringID + P4MF3Y + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 9 + INIntentParameterType + String + + + INIntentParameterDisplayName + Filename (optional) + INIntentParameterDisplayNameID + dPSUsQ + INIntentParameterDisplayPriority + 4 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + + INIntentParameterName + filename + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Enter a file name for the compressed file + INIntentParameterPromptDialogFormatStringID + nHQ237 + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 8 + INIntentParameterType + String + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeConciseFormatStringID + Pn1D1B + INIntentResponseCodeFormatString + App is currently locked by pass code. Please disable pass code to proceed. + INIntentResponseCodeFormatStringID + 25zMZv + INIntentResponseCodeName + authenticationRequired + + + INIntentResponseCodeConciseFormatString + The account with the given UUID does not exists + INIntentResponseCodeConciseFormatStringID + bDZ7q6 + INIntentResponseCodeFormatString + The account with the given UUID does not exists + INIntentResponseCodeFormatStringID + llOtTJ + INIntentResponseCodeName + accountFailure + + + INIntentResponseCodeConciseFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeConciseFormatStringID + h7Yg4L + INIntentResponseCodeFormatString + Shortcuts support is disabled. Please open the app and enable Shortcuts in Settings. + INIntentResponseCodeFormatStringID + XlVjYV + INIntentResponseCodeName + disabled + + + INIntentResponseCodeConciseFormatString + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. + INIntentResponseCodeConciseFormatStringID + Ra4dpr + INIntentResponseCodeFormatString + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. + INIntentResponseCodeFormatStringID + 1kJ8De + INIntentResponseCodeName + unlicensed + + + INIntentResponseCodeConciseFormatString + The given path does not exists + INIntentResponseCodeConciseFormatStringID + NlhMFB + INIntentResponseCodeFormatString + The given path does not exists + INIntentResponseCodeFormatStringID + GzUAMf + INIntentResponseCodeName + pathFailure + + + INIntentResponseLastParameterTag + 2 + INIntentResponseOutput + file + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + File + INIntentResponseParameterDisplayNameID + 7g5hLt + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + file + INIntentResponseParameterTag + 2 + INIntentResponseParameterType + File + + + + INIntentTitle + Compress Path Items + INIntentTitleID + 3CMRXn + INIntentType + Custom + INIntentVerb + Do + INTypes diff --git a/ownCloudAppShared/Intent/CompressPathItemsIntentHandler .swift b/ownCloudAppShared/Intent/CompressPathItemsIntentHandler .swift new file mode 100644 index 000000000..86d778415 --- /dev/null +++ b/ownCloudAppShared/Intent/CompressPathItemsIntentHandler .swift @@ -0,0 +1,168 @@ +// +// CompressPathItemsIntentHandler.swift +// ownCloudAppShared +// +// Created by Matthias Hühne on 30.08.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import Intents +import ownCloudSDK +import ownCloudApp + +@available(iOS 13.0, *) +public class CompressPathItemsIntentHandler: NSObject, CompressPathItemsIntentHandling { + + let defaultZipName = "Archive.zip".localized + + public func handle(intent: CompressPathItemsIntent, completion: @escaping (CompressPathItemsIntentResponse) -> Void) { + + guard IntentSettings.shared.isEnabled else { + completion(CompressPathItemsIntentResponse(code: .disabled, userActivity: nil)) + return + } + + guard !AppLockHelper().isPassCodeEnabled else { + completion(CompressPathItemsIntentResponse(code: .authenticationRequired, userActivity: nil)) + return + } + + guard let pathItems = intent.pathItems, let uuid = intent.account?.uuid else { + completion(CompressPathItemsIntentResponse(code: .failure, userActivity: nil)) + return + } + + guard let bookmark = OCBookmarkManager.shared.bookmark(for: uuid) else { + completion(CompressPathItemsIntentResponse(code: .accountFailure, userActivity: nil)) + return + } + + guard IntentSettings.shared.isLicensedFor(bookmark: bookmark) else { + completion(CompressPathItemsIntentResponse(code: .unlicensed, userActivity: nil)) + return + } + + var unifiedItems : [DownloadItem] = [] + let dispatchGroup = DispatchGroup() + + for path in pathItems { + dispatchGroup.enter() + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in + if error == nil, let item = item, let core = core { + if item.type == .file { + core.localFile(for: item) { (downloadItem) in + if let downloadItem = downloadItem { + unifiedItems.append(downloadItem) + } + dispatchGroup.leave() + } + } else { + unifiedItems.append(DownloadItem(file: OCFile(), item: item)) + + core.retrieveSubItems(for: item) { (items) in + for item in items! { + if item.type == .file { + dispatchGroup.enter() + core.localFile(for: item) { (downloadItem) in + if let downloadItem = downloadItem { + unifiedItems.append(downloadItem) + } + dispatchGroup.leave() + } + } else { + unifiedItems.append(DownloadItem(file: OCFile(), item: item)) + } + } + dispatchGroup.leave() + } + } + } else if core != nil { + completion(CompressPathItemsIntentResponse(code: .pathFailure, userActivity: nil)) + } else { + completion(CompressPathItemsIntentResponse(code: .failure, userActivity: nil)) + } + } + } + dispatchGroup.wait() + + if unifiedItems.count > 0 { + var zipFileName = defaultZipName + if let filename = intent.filename, filename.count > 0 { + zipFileName = filename + } else if unifiedItems.count == 1, let item = unifiedItems.first?.item { + zipFileName = String(format: "%@.zip", item.name ?? defaultZipName) + } + var password: String? + if let intentPassword = intent.password, intentPassword.count > 0 { + password = intentPassword + } + + let zipURL = FileManager.default.temporaryDirectory.appendingPathComponent(zipFileName) + let error = ZIPArchive.compressContents(of: unifiedItems, fromBasePath: "/", asZipFile: zipURL, withPassword: password) + + let file = INFile(fileURL: zipURL, filename: zipFileName, typeIdentifier: nil) + completion(CompressPathItemsIntentResponse.success(file: file)) + } else { + completion(CompressPathItemsIntentResponse(code: .failure, userActivity: nil)) + } + } + + public func resolveAccount(for intent: CompressPathItemsIntent, with completion: @escaping (AccountResolutionResult) -> Void) { + if let account = intent.account { + completion(AccountResolutionResult.success(with: account)) + } else { + completion(AccountResolutionResult.needsValue()) + } + } + + public func provideAccountOptions(for intent: CompressPathItemsIntent, with completion: @escaping ([Account]?, Error?) -> Void) { + completion(OCBookmarkManager.shared.accountList, nil) + } + + public func resolvePathItems(for intent: CompressPathItemsIntent, with completion: @escaping ([INStringResolutionResult]) -> Void) { + if let pathItems = intent.pathItems { + + var resolutionResults = [INStringResolutionResult]() + for pathItem in pathItems { + if pathItem.count > 0 { + resolutionResults.append(INStringResolutionResult.success(with: pathItem)) + } else { + resolutionResults.append(INStringResolutionResult.needsValue()) + } + } + completion(resolutionResults) + } else { + completion([INStringResolutionResult.needsValue()]) + } + } + + public func resolveFilename(for intent: CompressPathItemsIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + completion(INStringResolutionResult.success(with: intent.filename ?? "")) + } + + public func resolvePassword(for intent: CompressPathItemsIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + completion(INStringResolutionResult.success(with: intent.password ?? "")) + } + +} + +@available(iOS 13.0, *) +extension CompressPathItemsIntentResponse { + + public static func success(file: INFile) -> CompressPathItemsIntentResponse { + let intentResponse = CompressPathItemsIntentResponse(code: .success, userActivity: nil) + intentResponse.file = file + return intentResponse + } +}