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
+ }
+}