diff --git a/README.ja.md b/README.ja.md index 06bf2b6..fb0a442 100644 --- a/README.ja.md +++ b/README.ja.md @@ -6,7 +6,7 @@ https://itunes.apple.com/jp/app/cryptcloudviewer/id1458528598?mt=8 ## 説明 暗号化したまま閲覧できるクラウドビューワです。 -クラウドストレージ: Google Drive, Dropbox, OneDrive, pCloud,ドキュメントフォルダ、 +クラウドストレージ: Google Drive, Dropbox, OneDrive, pCloud, WebDAV, ドキュメントフォルダ、 暗号化: rclone, CarotDAV, Cryptomatorに対応しています。 iOSで再生できるメディアファイルの他、ソフトウエアデコードによりmpeg2等の動画も再生できます。 クラウドストレージのファイルを編集することも可能です(アップロード、フォルダ作成、リネーム、移動、削除) diff --git a/README.md b/README.md index 8329ebf..b7e48eb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This repository is source code of iOS app "CryptCloudViewer" https://itunes.apple.com/us/app/cryptcloudviewer/id1458528598?mt=8 ## description -This app is iOS cloud viewer with keeping encrypted. Supported storages: Google Drive, Dropbox, OneDrive, pCloud and Document folder. Available encryption: rclone, CarotDAV. This app can play media files with keeping encrypted. In addition, this app can play non-native media files (ex. mpeg2) with software decoder. You can edit your cloud storages: upload, make folder, rename, move, delete items. +This app is iOS cloud viewer with keeping encrypted. Supported storages: Google Drive, Dropbox, OneDrive, pCloud WebDAV and Document folder. Available encryption: rclone, CarotDAV. This app can play media files with keeping encrypted. In addition, this app can play non-native media files (ex. mpeg2) with software decoder. You can edit your cloud storages: upload, make folder, rename, move, delete items. In version 1.4.0, Chromecast support added. Please keep the app foreground and not lock the device while casting to Chromecast. diff --git a/RemoteCloud/RemoteCloud.xcodeproj/project.pbxproj b/RemoteCloud/RemoteCloud.xcodeproj/project.pbxproj index ebc6116..9d7cfc2 100644 --- a/RemoteCloud/RemoteCloud.xcodeproj/project.pbxproj +++ b/RemoteCloud/RemoteCloud.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 3F987DEC2277608A0020A796 /* Cryptomator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F987DEB2277608A0020A796 /* Cryptomator.swift */; }; 3FA46CE223849615007ACC82 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA46CE123849615007ACC82 /* FileCache.swift */; }; 3FA46CE5238496B7007ACC82 /* cache.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3FA46CE3238496B7007ACC82 /* cache.xcdatamodeld */; }; + 3FAF14D32387131F0052AFAB /* WebDAVStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FAF14D22387131F0052AFAB /* WebDAVStorage.swift */; }; 3FE23B1F2340B96200916DA6 /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FE23B1E2340B96200916DA6 /* UploadManager.swift */; }; /* End PBXBuildFile section */ @@ -70,6 +71,7 @@ 3F987DEB2277608A0020A796 /* Cryptomator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cryptomator.swift; sourceTree = ""; }; 3FA46CE123849615007ACC82 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = ""; }; 3FA46CE4238496B7007ACC82 /* cache.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = cache.xcdatamodel; sourceTree = ""; }; + 3FAF14D22387131F0052AFAB /* WebDAVStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebDAVStorage.swift; sourceTree = ""; }; 3FE23B1E2340B96200916DA6 /* UploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -161,6 +163,7 @@ 3C14C453225DBB1A0044E4A7 /* LocalStorage.swift */, 3C400921224190920028A45A /* OneDriveStorage.swift */, 3F3ABABC2273119D002A8D16 /* pCloudStorage.swift */, + 3FAF14D22387131F0052AFAB /* WebDAVStorage.swift */, ); path = Storages; sourceTree = ""; @@ -277,6 +280,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3FAF14D32387131F0052AFAB /* WebDAVStorage.swift in Sources */, 3F987DEC2277608A0020A796 /* Cryptomator.swift in Sources */, 3CA2C95E2235569800481DB5 /* cloud.xcdatamodeld in Sources */, 3F3ABABB2272C707002A8D16 /* Secret.swift in Sources */, diff --git a/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/Contents.json b/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/Contents.json new file mode 100644 index 0000000..90f6d93 --- /dev/null +++ b/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "webdav3.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "webdav2.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "webdav1.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/webdav1.png b/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/webdav1.png new file mode 100644 index 0000000..1239dd3 Binary files /dev/null and b/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/webdav1.png differ diff --git a/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/webdav2.png b/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/webdav2.png new file mode 100644 index 0000000..b8db503 Binary files /dev/null and b/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/webdav2.png differ diff --git a/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/webdav3.png b/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/webdav3.png new file mode 100644 index 0000000..0781c16 Binary files /dev/null and b/RemoteCloud/RemoteCloud/Assets.xcassets/webdav.imageset/webdav3.png differ diff --git a/RemoteCloud/RemoteCloud/ChildStorage.swift b/RemoteCloud/RemoteCloud/ChildStorage.swift index fff6121..e9d5cc2 100644 --- a/RemoteCloud/RemoteCloud/ChildStorage.swift +++ b/RemoteCloud/RemoteCloud/ChildStorage.swift @@ -360,7 +360,6 @@ public class ChildStorage: RemoteStorageBase { return size } - override func ListChildren(fileId: String, path: String, onFinish: (() -> Void)?) { let fixFileId = (fileId == "") ? "\(baseRootStorage)\n\(baseRootFileId)" : fileId let array = fixFileId.components(separatedBy: .newlines) @@ -371,62 +370,60 @@ public class ChildStorage: RemoteStorageBase { return } - let group = DispatchGroup() - group.enter() DispatchQueue.global().async { s.list(fileId: baseFileId) { - group.leave() - } - } - - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + DispatchQueue.main.async { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", baseFileId, baseStorage) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - for item in items { - let newid = "\(item.storage!)\n\(item.id!)" - let newname = self.ConvertDecryptName(name: item.name!) - let newcdate = item.cdate - let newmdate = item.mdate - let newfolder = item.folder - let newsize = self.ConvertDecryptSize(size: item.size) - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { - for object in result { - viewContext.delete(object as! NSManagedObject) + fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", baseFileId, baseStorage) + if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + for item in items { + guard let storage = item.storage, let id = item.id, let name = item.name else { + continue + } + let newid = "\(storage)\n\(id)" + let newname = self.ConvertDecryptName(name: name) + let newcdate = item.cdate + let newmdate = item.mdate + let newfolder = item.folder + let newsize = self.ConvertDecryptSize(size: item.size) + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + for object in result { + viewContext.delete(object as! NSManagedObject) + } + } + + let newitem = RemoteData(context: viewContext) + newitem.storage = self.storageName + newitem.id = newid + newitem.name = newname + let comp = newname.components(separatedBy: ".") + if comp.count >= 1 { + newitem.ext = comp.last!.lowercased() + } + newitem.cdate = newcdate + newitem.mdate = newmdate + newitem.folder = newfolder + newitem.size = newsize + newitem.hashstr = "" + newitem.parent = fileId + if fileId == "" { + newitem.path = "\(self.storageName ?? ""):/\(newname)" + } + else { + newitem.path = "\(path)/\(newname)" + } } + try? viewContext.save() } - - let newitem = RemoteData(context: viewContext) - newitem.storage = self.storageName - newitem.id = newid - newitem.name = newname - let comp = newname.components(separatedBy: ".") - if comp.count >= 1 { - newitem.ext = comp.last!.lowercased() - } - newitem.cdate = newcdate - newitem.mdate = newmdate - newitem.folder = newfolder - newitem.size = newsize - newitem.hashstr = "" - newitem.parent = fileId - if fileId == "" { - newitem.path = "\(self.storageName ?? ""):/\(newname)" - } - else { - newitem.path = "\(path)/\(newname)" + DispatchQueue.global().async { + onFinish?() } } - try? viewContext.save() - } - - DispatchQueue.global().async { - onFinish?() } } } @@ -454,66 +451,65 @@ public class ChildStorage: RemoteStorageBase { } var newBaseId = "" - let group = DispatchGroup() - group.enter() DispatchQueue.global().async { s.mkdir(parentId: baseFileId, newname: self.ConvertEncryptName(name: newname, folder: true)) { id in if let id = id { newBaseId = id } - group.leave() - } - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - var ret: String? - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, baseStorage) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - if let item = items.first { - let newid = "\(item.storage!)\n\(item.id!)" - let newname = self.ConvertDecryptName(name: item.name!) - let newcdate = item.cdate - let newmdate = item.mdate - let newfolder = item.folder - let newsize = self.ConvertDecryptSize(size: item.size) - + + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + + backgroundContext.perform { + var ret: String? let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { - for object in result { - viewContext.delete(object as! NSManagedObject) + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, baseStorage) + if let result = try? backgroundContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let item = items.first { + let newid = "\(item.storage!)\n\(item.id!)" + let newname = self.ConvertDecryptName(name: item.name!) + let newcdate = item.cdate + let newmdate = item.mdate + let newfolder = item.folder + let newsize = self.ConvertDecryptSize(size: item.size) + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") + if let result = try? backgroundContext.fetch(fetchRequest) { + for object in result { + backgroundContext.delete(object as! NSManagedObject) + } + } + + let newitem = RemoteData(context: backgroundContext) + newitem.storage = self.storageName + newitem.id = newid + newitem.name = newname + let comp = newname.components(separatedBy: ".") + if comp.count >= 1 { + newitem.ext = comp.last!.lowercased() + } + newitem.cdate = newcdate + newitem.mdate = newmdate + newitem.folder = newfolder + newitem.size = newsize + newitem.hashstr = "" + newitem.parent = parentId + if parentId == "" { + newitem.path = "\(self.storageName ?? ""):/\(newname)" + } + else { + newitem.path = "\(parentPath)/\(newname)" + } + ret = newid + try? backgroundContext.save() } } - let newitem = RemoteData(context: viewContext) - newitem.storage = self.storageName - newitem.id = newid - newitem.name = newname - let comp = newname.components(separatedBy: ".") - if comp.count >= 1 { - newitem.ext = comp.last!.lowercased() - } - newitem.cdate = newcdate - newitem.mdate = newmdate - newitem.folder = newfolder - newitem.size = newsize - newitem.hashstr = "" - newitem.parent = parentId - if parentId == "" { - newitem.path = "\(self.storageName ?? ""):/\(newname)" - } - else { - newitem.path = "\(parentPath)/\(newname)" + DispatchQueue.global().async { + onFinish?(ret) } - ret = newid - try? viewContext.save() } } - - DispatchQueue.global().async { - onFinish?(ret) - } } } @@ -535,34 +531,29 @@ public class ChildStorage: RemoteStorageBase { return } - var done = false - let group = DispatchGroup() - group.enter() DispatchQueue.global().async { s.delete(fileId: baseFileId) { success in - done = success - group.leave() - } - } - group.notify(queue: .main) { - guard done else { - DispatchQueue.global().async { - onFinish?(false) + guard success else { + DispatchQueue.global().async { + onFinish?(false) + } + return } - return - } - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - for item in items { - viewContext.delete(item) + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) + if let result = try? backgroundContext.fetch(fetchRequest), let items = result as? [RemoteData] { + for item in items { + backgroundContext.delete(item) + } + try? backgroundContext.save() + } + + DispatchQueue.global().async { + onFinish?(true) + } } - try? viewContext.save() - } - - DispatchQueue.global().async { - onFinish?(true) } } } @@ -589,14 +580,10 @@ public class ChildStorage: RemoteStorageBase { return } - let group = DispatchGroup() var parentPath = "" - var parentId = c.parent + let parentId = c.parent if parentId != "" { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -607,75 +594,88 @@ public class ChildStorage: RemoteStorageBase { } } } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } + } + } + } } var newBaseId = "" - group.enter() DispatchQueue.global().async { b.rename(newname: self.ConvertEncryptName(name: newname, folder: b.isFolder)) { id in if let id = id { newBaseId = id } - group.leave() - } - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - let fetchRequest1 = NSFetchRequest(entityName: "RemoteData") - fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) - if let result = try? viewContext.fetch(fetchRequest1), let items1 = result as? [RemoteData] { - for item in items1 { - viewContext.delete(item) - } - } - var ret: String? - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, baseStorage) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - if let item = items.first { - let newid = "\(item.storage!)\n\(item.id!)" - let newname = self.ConvertDecryptName(name: item.name!) - let newcdate = item.cdate - let newmdate = item.mdate - let newfolder = item.folder - let newsize = self.ConvertDecryptSize(size: item.size) - - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { - for object in result { - viewContext.delete(object as! NSManagedObject) + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { + let fetchRequest1 = NSFetchRequest(entityName: "RemoteData") + fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) + if let result = try? backgroundContext.fetch(fetchRequest1), let items1 = result as? [RemoteData] { + for item in items1 { + backgroundContext.delete(item) } } - - let newitem = RemoteData(context: viewContext) - newitem.storage = self.storageName - newitem.id = newid - newitem.name = newname - let comp = newname.components(separatedBy: ".") - if comp.count >= 1 { - newitem.ext = comp.last!.lowercased() - } - newitem.cdate = newcdate - newitem.mdate = newmdate - newitem.folder = newfolder - newitem.size = newsize - newitem.hashstr = "" - newitem.parent = parentId - if parentId == "" { - newitem.path = "\(self.storageName ?? ""):/\(newname)" + } + backgroundContext.perform { + var ret: String? + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, baseStorage) + if let result = try? backgroundContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let item = items.first { + let newid = "\(item.storage!)\n\(item.id!)" + let newname = self.ConvertDecryptName(name: item.name!) + let newcdate = item.cdate + let newmdate = item.mdate + let newfolder = item.folder + let newsize = self.ConvertDecryptSize(size: item.size) + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") + if let result = try? backgroundContext.fetch(fetchRequest) { + for object in result { + backgroundContext.delete(object as! NSManagedObject) + } + } + + let newitem = RemoteData(context: backgroundContext) + newitem.storage = self.storageName + newitem.id = newid + newitem.name = newname + let comp = newname.components(separatedBy: ".") + if comp.count >= 1 { + newitem.ext = comp.last!.lowercased() + } + newitem.cdate = newcdate + newitem.mdate = newmdate + newitem.folder = newfolder + newitem.size = newsize + newitem.hashstr = "" + newitem.parent = parentId + if parentId == "" { + newitem.path = "\(self.storageName ?? ""):/\(newname)" + } + else { + newitem.path = "\(parentPath)/\(newname)" + } + ret = newid + } } - else { - newitem.path = "\(parentPath)/\(newname)" + try? backgroundContext.save() + + DispatchQueue.global().async { + onFinish?(ret) } - ret = newid } } - try? viewContext.save() - - DispatchQueue.global().async { - onFinish?(ret) - } } } @@ -701,14 +701,10 @@ public class ChildStorage: RemoteStorageBase { return } - let group = DispatchGroup() var parentPath = "" - var parentId = c.parent + let parentId = c.parent if parentId != "" { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -719,75 +715,87 @@ public class ChildStorage: RemoteStorageBase { } } } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } + } + } + } } var newBaseId = "" - group.enter() DispatchQueue.global().async { b.changetime(newdate: newdate) { id in if let id = id { newBaseId = id } - group.leave() - } - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - let fetchRequest1 = NSFetchRequest(entityName: "RemoteData") - fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) - if let result = try? viewContext.fetch(fetchRequest1), let items1 = result as? [RemoteData] { - for item in items1 { - viewContext.delete(item) + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { + let fetchRequest1 = NSFetchRequest(entityName: "RemoteData") + fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) + if let result = try? backgroundContext.fetch(fetchRequest1), let items1 = result as? [RemoteData] { + for item in items1 { + backgroundContext.delete(item) + } + } } - } - - var ret: String? - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, baseStorage) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - if let item = items.first { - let newid = "\(item.storage!)\n\(item.id!)" - let newname = self.ConvertDecryptName(name: item.name!) - let newcdate = item.cdate - let newmdate = item.mdate - let newfolder = item.folder - let newsize = self.ConvertDecryptSize(size: item.size) - + backgroundContext.perform { + var ret: String? let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { - for object in result { - viewContext.delete(object as! NSManagedObject) + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, baseStorage) + if let result = try? backgroundContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let item = items.first { + let newid = "\(item.storage!)\n\(item.id!)" + let newname = self.ConvertDecryptName(name: item.name!) + let newcdate = item.cdate + let newmdate = item.mdate + let newfolder = item.folder + let newsize = self.ConvertDecryptSize(size: item.size) + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") + if let result = try? backgroundContext.fetch(fetchRequest) { + for object in result { + backgroundContext.delete(object as! NSManagedObject) + } + } + + let newitem = RemoteData(context: backgroundContext) + newitem.storage = self.storageName + newitem.id = newid + newitem.name = newname + let comp = newname.components(separatedBy: ".") + if comp.count >= 1 { + newitem.ext = comp.last!.lowercased() + } + newitem.cdate = newcdate + newitem.mdate = newmdate + newitem.folder = newfolder + newitem.size = newsize + newitem.hashstr = "" + newitem.parent = parentId + if parentId == "" { + newitem.path = "\(self.storageName ?? ""):/\(newname)" + } + else { + newitem.path = "\(parentPath)/\(newname)" + } + ret = newid } } + try? backgroundContext.save() - let newitem = RemoteData(context: viewContext) - newitem.storage = self.storageName - newitem.id = newid - newitem.name = newname - let comp = newname.components(separatedBy: ".") - if comp.count >= 1 { - newitem.ext = comp.last!.lowercased() - } - newitem.cdate = newcdate - newitem.mdate = newmdate - newitem.folder = newfolder - newitem.size = newsize - newitem.hashstr = "" - newitem.parent = parentId - if parentId == "" { - newitem.path = "\(self.storageName ?? ""):/\(newname)" + DispatchQueue.global().async { + onFinish?(ret) } - else { - newitem.path = "\(parentPath)/\(newname)" - } - ret = newid } } - try? viewContext.save() - - DispatchQueue.global().async { - onFinish?(ret) - } } } @@ -824,13 +832,9 @@ public class ChildStorage: RemoteStorageBase { return } - let group = DispatchGroup() var toParentPath = "\(tobaseStorage):/" if toParentId != "" { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -841,77 +845,88 @@ public class ChildStorage: RemoteStorageBase { } } } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", toParentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + toParentPath = items.first?.path ?? "" + } + } + } + } } var newBaseId = "" - group.enter() DispatchQueue.global().async { b.move(toParentId: tobaseFileId) { id in if let id = id { newBaseId = id } - group.leave() - } - } - - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - let fetchRequest1 = NSFetchRequest(entityName: "RemoteData") - fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) - if let result = try? viewContext.fetch(fetchRequest1), let items1 = result as? [RemoteData] { - for item in items1 { - viewContext.delete(item) + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { + let fetchRequest1 = NSFetchRequest(entityName: "RemoteData") + fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) + if let result = try? backgroundContext.fetch(fetchRequest1), let items1 = result as? [RemoteData] { + for item in items1 { + backgroundContext.delete(item) + } + } } - } - - var ret: String? - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, baseStorage) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - if let item = items.first { - let newid = "\(item.storage!)\n\(item.id!)" - let newname = self.ConvertDecryptName(name: item.name!) - let newcdate = item.cdate - let newmdate = item.mdate - let newfolder = item.folder - let newsize = self.ConvertDecryptSize(size: item.size) - + backgroundContext.perform { + var ret: String? let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { - for object in result { - viewContext.delete(object as! NSManagedObject) + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, baseStorage) + if let result = try? backgroundContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let item = items.first { + let newid = "\(item.storage!)\n\(item.id!)" + let newname = self.ConvertDecryptName(name: item.name!) + let newcdate = item.cdate + let newmdate = item.mdate + let newfolder = item.folder + let newsize = self.ConvertDecryptSize(size: item.size) + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") + if let result = try? backgroundContext.fetch(fetchRequest) { + for object in result { + backgroundContext.delete(object as! NSManagedObject) + } + } + + let newitem = RemoteData(context: backgroundContext) + newitem.storage = self.storageName + newitem.id = newid + newitem.name = newname + let comp = newname.components(separatedBy: ".") + if comp.count >= 1 { + newitem.ext = comp.last!.lowercased() + } + newitem.cdate = newcdate + newitem.mdate = newmdate + newitem.folder = newfolder + newitem.size = newsize + newitem.hashstr = "" + newitem.parent = toParentId + if toParentId == "" { + newitem.path = "\(self.storageName ?? ""):/\(newname)" + } + else { + newitem.path = "\(toParentPath)/\(newname)" + } + ret = newid } } + try? backgroundContext.save() - let newitem = RemoteData(context: viewContext) - newitem.storage = self.storageName - newitem.id = newid - newitem.name = newname - let comp = newname.components(separatedBy: ".") - if comp.count >= 1 { - newitem.ext = comp.last!.lowercased() - } - newitem.cdate = newcdate - newitem.mdate = newmdate - newitem.folder = newfolder - newitem.size = newsize - newitem.hashstr = "" - newitem.parent = toParentId - if toParentId == "" { - newitem.path = "\(self.storageName ?? ""):/\(newname)" - } - else { - newitem.path = "\(toParentPath)/\(newname)" + DispatchQueue.global().async { + onFinish?(ret) } - ret = newid } } - try? viewContext.save() - - DispatchQueue.global().async { - onFinish?(ret) - } } } @@ -947,13 +962,12 @@ public class ChildStorage: RemoteStorageBase { onFinish?(nil) return } - - DispatchQueue.main.async { + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { var ret: String? = nil - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, baseStorage) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let result = try? backgroundContext.fetch(fetchRequest), let items = result as? [RemoteData] { if let item = items.first { let newid = "\(item.storage!)\n\(item.id!)" let newname = self.ConvertDecryptName(name: item.name!) @@ -964,13 +978,13 @@ public class ChildStorage: RemoteStorageBase { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: backgroundContext) newitem.storage = self.storageName newitem.id = newid newitem.name = newname @@ -993,7 +1007,7 @@ public class ChildStorage: RemoteStorageBase { ret = newid } } - try? viewContext.save() + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(ret) diff --git a/RemoteCloud/RemoteCloud/FileCache.swift b/RemoteCloud/RemoteCloud/FileCache.swift index 08dd0ab..079304f 100644 --- a/RemoteCloud/RemoteCloud/FileCache.swift +++ b/RemoteCloud/RemoteCloud/FileCache.swift @@ -10,6 +10,8 @@ import Foundation import CoreData public class FileCache { + public var diskQueue = DispatchQueue(label: "FileEnumerate") + public var cacheMaxSize: Int { get { return UserDefaults.standard.integer(forKey: "networkCacheSize") @@ -19,64 +21,205 @@ public class FileCache { } } + public func getPartialFile(storage: String, id: String, offset: Int64, size: Int64) -> Data? { + var ret: Data? + let group = DispatchGroup() + group.enter() + CloudFactory.shared.data.getData(storage: storage, fileId: id) { item1 in + guard let orgItem = item1 else { + group.leave() + return + } + var targetURL: URL? + self.persistentContainer.performBackgroundTask { context in + let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") + fetchrequest.predicate = NSPredicate(format: "storage == %@ && id == %@ && chunkOffset == 0", storage, id) + do{ + guard let item = try context.fetch(fetchrequest).first as? FileCacheItem else { + return + } + if orgItem.mdate != item.mdate || orgItem.size != item.orgSize { + if let target = item.filename?.uuidString { + let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) + self.diskQueue.async { + try? FileManager.default.removeItem(at: target_path) + } + } + context.delete(item) + try context.save() + return + } + if let target = item.filename?.uuidString, item.orgSize == item.chunkSize { + let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) + if FileManager.default.fileExists(atPath: target_path.path) { + targetURL = target_path + item.rdate = Date() + try context.save() + } + else { + context.delete(item) + try context.save() + } + } + } + catch{ + return + } + } + if let target = targetURL { + do { + let hFile = try FileHandle(forReadingFrom: target) + defer { + do { + if #available(iOS 13.0, *) { + try hFile.close() + } else { + hFile.closeFile() + } + } + catch { + print(error) + } + } + if #available(iOS 13.0, *) { + try hFile.seek(toOffset: UInt64(offset)) + } else { + hFile.seek(toFileOffset: UInt64(offset)) + } + if size < 0 { + ret = hFile.readDataToEndOfFile() + return + } + ret = hFile.readData(ofLength: Int(size)) + return + } + catch { + print(error) + } + } + } + let _ = group.wait(timeout: .now()+2) + return ret + } + public func getCache(storage: String, id: String, offset: Int64, size: Int64) -> URL? { var ret: URL? = nil guard cacheMaxSize > 0 else { return nil } - if Thread.isMainThread { - guard let orgItem = CloudFactory.shared.data.getData(storage: storage, fileId: id) else { - return nil + let group = DispatchGroup() + group.enter() + CloudFactory.shared.data.getData(storage: storage, fileId: id) { item1 in + guard let orgItem = item1 else { + group.leave() + return } - - let viewContext = self.persistentContainer.viewContext - - let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") - fetchrequest.predicate = NSPredicate(format: "storage == %@ && id == %@ && chunkOffset == %lld", storage, id, offset) - do{ - guard let item = try viewContext.fetch(fetchrequest).first as? FileCacheItem else { - return nil + self.persistentContainer.performBackgroundTask { context in + defer { + group.leave() } - if orgItem.mdate != item.mdate || orgItem.size != item.orgSize { - if let target = item.filename?.uuidString { - let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) - let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) - try FileManager.default.removeItem(at: target_path) + let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") + fetchrequest.predicate = NSPredicate(format: "storage == %@ && id == %@ && chunkOffset == %lld", storage, id, offset) + do{ + guard let item = try context.fetch(fetchrequest).first as? FileCacheItem else { + return } - viewContext.delete(item) - try viewContext.save() - return nil - } - if let target = item.filename?.uuidString, (size == item.chunkSize || size < 0) { - let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) - let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) - if FileManager.default.fileExists(atPath: target_path.path) { - ret = target_path - item.rdate = Date() - try? viewContext.save() + if orgItem.mdate != item.mdate || orgItem.size != item.orgSize { + if let target = item.filename?.uuidString { + let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) + self.diskQueue.async { + try? FileManager.default.removeItem(at: target_path) + } + } + context.delete(item) + try context.save() + return } - else { - viewContext.delete(item) - try viewContext.save() + if let target = item.filename?.uuidString, (size == item.chunkSize || size < 0) { + let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) + if FileManager.default.fileExists(atPath: target_path.path) { + ret = target_path + item.rdate = Date() + try context.save() + } + else { + context.delete(item) + try context.save() + } } } + catch{ + return + } } - catch{ - return nil - } - return ret + + } + let _ = group.wait(timeout: .now()+2) + return ret + } + + public func saveFile(storage: String, id: String, data: Data) { + if getCacheSize() > cacheMaxSize { + increseFreeSpace() } - else { - DispatchQueue.main.sync { - ret = getCache(storage: storage, id: id, offset: offset, size: size) + + do { + let size = Int64(data.count) + let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) + let newId = UUID() + let target = newId.uuidString + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true) + try FileManager.default.createDirectory(at: target_path, withIntermediateDirectories: true, attributes: nil) + try diskQueue.sync { + try data.write(to: target_path.appendingPathComponent(target)) } - return ret + + persistentContainer.performBackgroundTask { context in + let item1 = CloudFactory.shared.data.getData(storage: storage, fileId: id) + guard let orgItem = item1 else { + try? FileManager.default.removeItem(at: target_path.appendingPathComponent(target)) + return + } + + let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") + fetchrequest.predicate = NSPredicate(format: "storage == %@ && id == %@ && chunkOffset == 0 && chunkSize == %lld", storage, id, size) + do { + if let items = try context.fetch(fetchrequest) as? [FileCacheItem], items.count > 0 { + try FileManager.default.removeItem(at: target_path.appendingPathComponent(target)) + return + } + } + catch { + print(error) + } + + let newitem = FileCacheItem(context: context) + newitem.filename = newId + newitem.storage = storage + newitem.id = id + newitem.chunkSize = size + newitem.chunkOffset = 0 + newitem.rdate = Date() + newitem.mdate = orgItem.mdate + newitem.orgSize = orgItem.size + + try? context.save() + } + } + catch { + print(error) } } public func saveCache(storage: String, id: String, offset: Int64, data: Data) { guard cacheMaxSize > 0 else { - increseFreeSpace() + if getCacheSize() > 0 { + deleteAllCache() + } return } do { @@ -86,24 +229,25 @@ public class FileCache { let target = newId.uuidString let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true) try FileManager.default.createDirectory(at: target_path, withIntermediateDirectories: true, attributes: nil) - try data.write(to: target_path.appendingPathComponent(target)) + try diskQueue.sync { + try data.write(to: target_path.appendingPathComponent(target)) + } if getCacheSize() > cacheMaxSize { increseFreeSpace() } - DispatchQueue.main.async { - guard let orgItem = CloudFactory.shared.data.getData(storage: storage, fileId: id) else { + persistentContainer.performBackgroundTask { context in + let item1 = CloudFactory.shared.data.getData(storage: storage, fileId: id) + guard let orgItem = item1 else { try? FileManager.default.removeItem(at: target_path.appendingPathComponent(target)) return } - let viewContext = self.persistentContainer.viewContext - let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") fetchrequest.predicate = NSPredicate(format: "storage == %@ && id == %@ && chunkOffset == %lld && chunkSize == %lld", storage, id, offset, size) do { - if let items = try viewContext.fetch(fetchrequest) as? [FileCacheItem], items.count > 0 { + if let items = try context.fetch(fetchrequest) as? [FileCacheItem], items.count > 0 { try FileManager.default.removeItem(at: target_path.appendingPathComponent(target)) return } @@ -112,7 +256,7 @@ public class FileCache { print(error) } - let newitem = FileCacheItem(context: viewContext) + let newitem = FileCacheItem(context: context) newitem.filename = newId newitem.storage = storage newitem.id = id @@ -122,7 +266,7 @@ public class FileCache { newitem.mdate = orgItem.mdate newitem.orgSize = orgItem.size - try? viewContext.save() + try? context.save() } } catch { @@ -130,7 +274,42 @@ public class FileCache { } } + public func deleteAllCache() { + guard let base = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) else { + return + } + diskQueue.async { + do { + try FileManager.default.removeItem(at: base) + } + catch { + print(error) + } + } + persistentContainer.performBackgroundTask { context in + let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") + fetchrequest.predicate = NSPredicate(value: true) + do { + if let items = try context.fetch(fetchrequest) as? [FileCacheItem], items.count > 0 { + for item in items { + context.delete(item) + } + try context.save() + } + } + catch { + print(error) + } + } + } + public func increseFreeSpace() { + guard cacheMaxSize > 0 else { + if getCacheSize() > 0 { + deleteAllCache() + } + return + } guard let attributes = try? FileManager.default.attributesOfFileSystem(forPath: NSTemporaryDirectory()) else { return } @@ -149,27 +328,36 @@ public class FileCache { guard let base = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) else { return } - DispatchQueue.main.async { - let viewContext = self.persistentContainer.viewContext - + persistentContainer.performBackgroundTask { context in let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") fetchrequest.predicate = NSPredicate(value: true) fetchrequest.sortDescriptors = [NSSortDescriptor(key: "rdate", ascending: true)] do { - if let items = try viewContext.fetch(fetchrequest) as? [FileCacheItem], items.count > 0 { - for item in items { - if delSize > incSize { - break - } - guard let target = item.filename?.uuidString else { - continue + if let items = try context.fetch(fetchrequest) as? [FileCacheItem], items.count > 0 { + var delItems = [FileCacheItem]() + self.diskQueue.async { + for item in items { + if delSize > incSize { + break + } + guard let target = item.filename?.uuidString else { + continue + } + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) + do { + try FileManager.default.removeItem(at: target_path) + } + catch { + print(error) + } + delSize += Int(item.chunkSize) + delItems += [item] } - let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) - try FileManager.default.removeItem(at: target_path) - delSize += Int(item.chunkSize) - viewContext.delete(item) } - try viewContext.save() + for item in delItems { + context.delete(item) + } + try context.save() } } catch { @@ -184,18 +372,20 @@ public class FileCache { } let resourceKeys = Set([.fileAllocatedSizeKey]) - let directoryEnumerator = FileManager.default.enumerator(at: base, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)! - - var allocSize = 0 - for case let fileURL as URL in directoryEnumerator { - guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys), - let size = resourceValues.fileAllocatedSize - else { - continue - } - allocSize += size - } - return allocSize + return diskQueue.sync { + let directoryEnumerator = FileManager.default.enumerator(at: base, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)! + + var allocSize = 0 + for case let fileURL as URL in directoryEnumerator { + guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys), + let size = resourceValues.fileAllocatedSize + else { + continue + } + allocSize += size + } + return allocSize + } } // MARK: - Core Data stack @@ -239,7 +429,7 @@ public class FileCache { // MARK: - Core Data Saving support public func saveContext () { - let context = persistentContainer.viewContext + let context = self.persistentContainer.viewContext if context.hasChanges { do { try context.save() diff --git a/RemoteCloud/RemoteCloud/NetworkStorage.swift b/RemoteCloud/RemoteCloud/NetworkStorage.swift index 5f206f2..6f62125 100644 --- a/RemoteCloud/RemoteCloud/NetworkStorage.swift +++ b/RemoteCloud/RemoteCloud/NetworkStorage.swift @@ -313,7 +313,7 @@ public class SlotStream: RemoteStream { self.error = true break } - Thread.sleep(forTimeInterval: 1) + Thread.sleep(forTimeInterval: 0.1) if start.timeIntervalSinceNow < -120 { print("error on timeout") self.error = true diff --git a/RemoteCloud/RemoteCloud/RemoteItem.swift b/RemoteCloud/RemoteCloud/RemoteItem.swift index 7188526..dd2e32e 100644 --- a/RemoteCloud/RemoteCloud/RemoteItem.swift +++ b/RemoteCloud/RemoteCloud/RemoteItem.swift @@ -14,21 +14,34 @@ import CommonCrypto public class dataItems { public func listData(storage: String, parentID: String) -> [RemoteData] { - let viewContext = persistentContainer.viewContext - - let fetchrequest = NSFetchRequest(entityName: "RemoteData") - fetchrequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", parentID, storage) - fetchrequest.sortDescriptors = [NSSortDescriptor(key: "folder", ascending: false), - NSSortDescriptor(key: "name", ascending: true)] - do{ - return try viewContext.fetch(fetchrequest) as! [RemoteData] + if Thread.isMainThread { + let viewContext = persistentContainer.viewContext + + let fetchrequest = NSFetchRequest(entityName: "RemoteData") + fetchrequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", parentID, storage) + fetchrequest.sortDescriptors = [NSSortDescriptor(key: "folder", ascending: false), + NSSortDescriptor(key: "name", ascending: true)] + do{ + return try viewContext.fetch(fetchrequest) as! [RemoteData] + } + catch{ + return [] + } } - catch{ - return [] + else { + return DispatchQueue.main.sync { + listData(storage: storage, parentID: parentID) + } } } public func getMark(storage: String, targetID: String) -> Double? { + if !Thread.isMainThread { + return DispatchQueue.main.sync { + getMark(storage: storage, targetID: targetID) + } + } + var result = [UInt8](repeating: 0, count: Int(CC_SHA512_DIGEST_LENGTH)) guard let data = "storage=\(storage),target=\(targetID)".cString(using: .utf8) else { return nil @@ -36,36 +49,20 @@ public class dataItems { CC_SHA512(data, CC_LONG(data.count-1), &result) let target = result.map({ String(format: "%02hhx", $0) }).joined() - var position: Double? - if !Thread.isMainThread { - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - let viewContext = self.persistentContainer.viewContext - - let fetchrequest = NSFetchRequest(entityName: "Mark") - fetchrequest.predicate = NSPredicate(format: "id == %@ && storage == %@", target, storage) - - position = try? (viewContext.fetch(fetchrequest) as! [Mark]).first?.position - group.leave() - } - group.wait() - } - else { - let viewContext = self.persistentContainer.viewContext - - let fetchrequest = NSFetchRequest(entityName: "Mark") - fetchrequest.predicate = NSPredicate(format: "id == %@ && storage == %@", target, storage) - - position = try? (viewContext.fetch(fetchrequest) as! [Mark]).first?.position - } - return position + let viewContext = persistentContainer.viewContext + + let fetchrequest = NSFetchRequest(entityName: "Mark") + fetchrequest.predicate = NSPredicate(format: "id == %@ && storage == %@", target, storage) + + return try? (viewContext.fetch(fetchrequest) as! [Mark]).first?.position } public func getCloudMark(storage: String, parentID: String, onFinish: @escaping ()->Void) { var result = [UInt8](repeating: 0, count: Int(CC_SHA512_DIGEST_LENGTH)) guard let data = "storage=\(storage),target=\(parentID)".cString(using: .utf8) else { - onFinish() + DispatchQueue.global().async { + onFinish() + } return } CC_SHA512(data, CC_LONG(data.count-1), &result) @@ -78,17 +75,17 @@ public class dataItems { ckDatabase.perform(ckQuery, inZoneWith: nil, completionHandler: { (ckRecords, error) in guard error == nil else { print("\(String(describing: error?.localizedDescription))") - onFinish() + DispatchQueue.global().async { + onFinish() + } return } - DispatchQueue.main.async { - let viewContext = self.persistentContainer.viewContext - + self.persistentContainer.performBackgroundTask { context in let fetchRequest = NSFetchRequest(entityName: "Mark") fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", parentID, storage) - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } @@ -97,7 +94,7 @@ public class dataItems { let id = ckRecord["targetId"] as String? if let pos = pos { - let newitem = Mark(context: viewContext) + let newitem = Mark(context: context) newitem.id = id newitem.storage = storage newitem.parent = parentID @@ -105,8 +102,10 @@ public class dataItems { } } - try? viewContext.save() - onFinish() + try? context.save() + DispatchQueue.global().async { + onFinish() + } } }) } @@ -217,48 +216,50 @@ public class dataItems { } public func uploadCloudPlaylist(onFinish: @escaping ()->Void) { + if !Thread.isMainThread { + DispatchQueue.main.async { + self.uploadCloudPlaylist(onFinish: onFinish) + } + return + } + var serialStart = Int64(0) var serialEnd = Int64(0) - let group = DispatchGroup() var saveItems = [CKRecord]() - group.enter() - DispatchQueue.main.async { - let viewContext = self.persistentContainer.viewContext - let fetchRequest1 = NSFetchRequest(entityName: "PlayList") - fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@", "", "", "") - if let result = try? viewContext.fetch(fetchRequest1) as? [PlayList] { - if let forid = result.first { - serialStart = forid.index - serialEnd = forid.serial - } + + let viewContext = self.persistentContainer.viewContext + let fetchRequest1 = NSFetchRequest(entityName: "PlayList") + fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@", "", "", "") + if let result = try? viewContext.fetch(fetchRequest1) as? [PlayList] { + if let forid = result.first { + serialStart = forid.index + serialEnd = forid.serial } + } - let fetchrequest = NSFetchRequest(entityName: "PlayList") - fetchrequest.predicate = NSPredicate(format: "serial BETWEEN {%lld, %lld}", serialStart, serialEnd) - if let data = try? viewContext.fetch(fetchrequest) as? [PlayList] { - for item in data { - let ckRecord = CKRecord(recordType: "PlayList") - ckRecord["id"] = item.id - ckRecord["storage"] = item.storage - ckRecord["folder"] = item.folder - ckRecord["index"] = item.index - ckRecord["serial"] = item.serial - saveItems += [ckRecord] - } + let fetchrequest = NSFetchRequest(entityName: "PlayList") + fetchrequest.predicate = NSPredicate(format: "serial BETWEEN {%lld, %lld}", serialStart, serialEnd) + if let data = try? viewContext.fetch(fetchrequest) as? [PlayList] { + for item in data { + let ckRecord = CKRecord(recordType: "PlayList") + ckRecord["id"] = item.id + ckRecord["storage"] = item.storage + ckRecord["folder"] = item.folder + ckRecord["index"] = item.index + ckRecord["serial"] = item.serial + saveItems += [ckRecord] } + } - let ckRecord = CKRecord(recordType: "PlayList") - ckRecord["id"] = "" - ckRecord["storage"] = "" - ckRecord["folder"] = "" - ckRecord["index"] = serialStart - ckRecord["serial"] = serialEnd - saveItems += [ckRecord] + let ckRecord = CKRecord(recordType: "PlayList") + ckRecord["id"] = "" + ckRecord["storage"] = "" + ckRecord["folder"] = "" + ckRecord["index"] = serialStart + ckRecord["serial"] = serialEnd + saveItems += [ckRecord] - group.leave() - } - - group.notify(queue: .global()) { + DispatchQueue.global().async { let ckQuery = CKQuery(recordType: "PlayList", predicate: NSPredicate(value: true)) self.subGetCurrentPlaylist(q: ckQuery) { ckRecords in @@ -279,18 +280,17 @@ public class dataItems { onFinish() return } + let backgroudContext = persistentContainer.newBackgroundContext() operation.resultsLimit = 100 operation.recordFetchedBlock = { ckRecord in - DispatchQueue.main.async { - let viewContext = self.persistentContainer.viewContext - + backgroudContext.perform { let id = ckRecord["id"] as String? let storage = ckRecord["storage"] as String? let folder = ckRecord["folder"] as String? let index = ckRecord["index"] as Int64? let serial = ckRecord["serial"] as Int64? - let newitem = PlayList(context: viewContext) + let newitem = PlayList(context: backgroudContext) newitem.id = id newitem.storage = storage newitem.folder = folder @@ -301,9 +301,8 @@ public class dataItems { operation.queryCompletionBlock = { cursor, error in guard error == nil else { print("\(String(describing: error?.localizedDescription))") - DispatchQueue.main.async { - let viewContext = self.persistentContainer.viewContext - try? viewContext.save() + backgroudContext.perform { + try? backgroudContext.save() } onFinish() return @@ -313,9 +312,8 @@ public class dataItems { self.subGetCloudPlaylist(cursor: cursor, onFinish: onFinish) return } - DispatchQueue.main.async { - let viewContext = self.persistentContainer.viewContext - try? viewContext.save() + backgroudContext.perform { + try? backgroudContext.save() } onFinish() } @@ -373,27 +371,16 @@ public class dataItems { } public func touchPlaylist(items: [[String: Any]]) { - if !Thread.isMainThread { - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - self.touchPlaylist(items: items) - group.leave() - } - group.wait() - } - else { + persistentContainer.performBackgroundTask { context in let serial = Int64(Date().timeIntervalSince1970 * 1000) - let viewContext = self.persistentContainer.viewContext - let fetchRequest1 = NSFetchRequest(entityName: "PlayList") fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@", "", "", "") - if let result = try? viewContext.fetch(fetchRequest1) as? [PlayList] { + if let result = try? context.fetch(fetchRequest1) as? [PlayList] { if let forid = result.first { forid.serial = serial } else { - let forid = PlayList(context: viewContext) + let forid = PlayList(context: context) forid.folder = "" forid.id = "" forid.storage = "" @@ -406,165 +393,162 @@ public class dataItems { if let id = item["id"] as? String, let storage = item["storage"] as? String, let folder = item["folder"] as? String, let index = item["index"] as? Int64 { let fetchrequest = NSFetchRequest(entityName: "PlayList") fetchrequest.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@ && index == %lld", id, storage, folder, index) - if let data = try? viewContext.fetch(fetchrequest) as? [PlayList] { + if let data = try? context.fetch(fetchrequest) as? [PlayList] { if let target = data.first { target.serial = serial } } } } - try? viewContext.save() + try? context.save() } } public func getPlaylist() -> [[String: Any]] { - var result = [[String: Any]]() if !Thread.isMainThread { - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - result = self.getPlaylist() - group.leave() + return DispatchQueue.main.sync { + self.getPlaylist() } - group.wait() } - else { - let viewContext = self.persistentContainer.viewContext + let viewContext = self.persistentContainer.viewContext - var startSerial = Int64(0) - var endSerial = Int64(0) - let fetchRequest1 = NSFetchRequest(entityName: "PlayList") - fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@", "", "", "") - if let result = try? viewContext.fetch(fetchRequest1) as? [PlayList] { - if let forid = result.first { - endSerial = forid.serial - startSerial = forid.index - } + var result = [[String: Any]]() + var startSerial = Int64(0) + var endSerial = Int64(0) + let fetchRequest1 = NSFetchRequest(entityName: "PlayList") + fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@", "", "", "") + if let result = try? viewContext.fetch(fetchRequest1) as? [PlayList] { + if let forid = result.first { + endSerial = forid.serial + startSerial = forid.index } + } - let fetchRequest2 = NSFetchRequest(entityName: "PlayList") - fetchRequest2.predicate = NSPredicate(format: "NOT serial BETWEEN {%lld, %lld}", startSerial, endSerial) - if let result = try? viewContext.fetch(fetchRequest2) as? [PlayList] { - for item in result { - viewContext.delete(item) - } + let fetchRequest2 = NSFetchRequest(entityName: "PlayList") + fetchRequest2.predicate = NSPredicate(format: "NOT serial BETWEEN {%lld, %lld}", startSerial, endSerial) + if let result = try? viewContext.fetch(fetchRequest2) as? [PlayList] { + for item in result { + viewContext.delete(item) } - try? viewContext.save() + } + try? viewContext.save() - let fetchrequest = NSFetchRequest(entityName: "PlayList") - fetchrequest.predicate = NSPredicate(format: "id != %@ && storage != %@ && serial BETWEEN {%lld, %lld}", "", "", startSerial, endSerial) - if let data = try? viewContext.fetch(fetchrequest) as? [PlayList] { - result = data.map { item in - var ret = [String: Any]() - ret["id"] = item.id - ret["storage"] = item.storage - ret["folder"] = item.folder - ret["index"] = item.index - return ret - } + let fetchrequest = NSFetchRequest(entityName: "PlayList") + fetchrequest.predicate = NSPredicate(format: "id != %@ && storage != %@ && serial BETWEEN {%lld, %lld}", "", "", startSerial, endSerial) + if let data = try? viewContext.fetch(fetchrequest) as? [PlayList] { + result = data.map { item in + var ret = [String: Any]() + ret["id"] = item.id + ret["storage"] = item.storage + ret["folder"] = item.folder + ret["index"] = item.index + return ret } } return result } public func updatePlaylist(prevItem: [String: Any], newItem: [String: Any]) { - let viewContext = persistentContainer.viewContext - let serial = Int64(Date().timeIntervalSince1970 * 1000) + persistentContainer.performBackgroundTask { context in + let serial = Int64(Date().timeIntervalSince1970 * 1000) - let fetchRequest1 = NSFetchRequest(entityName: "PlayList") - fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@", "", "", "") - if let result = try? viewContext.fetch(fetchRequest1) as? [PlayList] { - if let forid = result.first { - forid.serial = serial + let fetchRequest1 = NSFetchRequest(entityName: "PlayList") + fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@", "", "", "") + if let result = try? context.fetch(fetchRequest1) as? [PlayList] { + if let forid = result.first { + forid.serial = serial + } + else { + let forid = PlayList(context: context) + forid.folder = "" + forid.id = "" + forid.storage = "" + forid.index = serial + forid.serial = serial + } } else { - let forid = PlayList(context: viewContext) + let forid = PlayList(context: context) forid.folder = "" forid.id = "" forid.storage = "" forid.index = serial forid.serial = serial } - } - else { - let forid = PlayList(context: viewContext) - forid.folder = "" - forid.id = "" - forid.storage = "" - forid.index = serial - forid.serial = serial - } - try? viewContext.save() + try? context.save() - var newData: PlayList - if let id = prevItem["id"] as? String, let storage = prevItem["storage"] as? String, let folder = prevItem["folder"] as? String { - let fetchRequest = NSFetchRequest(entityName: "PlayList") - if let index = prevItem["index"] as? Int64 { - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@ && index == %lld", id, storage, folder, index) - } - else { - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@", id, storage, folder) - } - if let result = try? viewContext.fetch(fetchRequest) as? [PlayList] { - newData = result.first ?? PlayList(context: viewContext) - for object in result.dropFirst() { - viewContext.delete(object) + var newData: PlayList + if let id = prevItem["id"] as? String, let storage = prevItem["storage"] as? String, let folder = prevItem["folder"] as? String { + let fetchRequest = NSFetchRequest(entityName: "PlayList") + if let index = prevItem["index"] as? Int64 { + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@ && index == %lld", id, storage, folder, index) + } + else { + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@ && folder == %@", id, storage, folder) + } + if let result = try? context.fetch(fetchRequest) as? [PlayList] { + newData = result.first ?? PlayList(context: context) + for object in result.dropFirst() { + context.delete(object) + } + } + else { + newData = PlayList(context: context) } } else { - newData = PlayList(context: viewContext) - } - } - else { - newData = PlayList(context: viewContext) - } - if let newid = newItem["id"] as? String, let newstorage = newItem["storage"] as? String, let newfolder = newItem["folder"] as? String { - newData.id = newid - newData.storage = newstorage - newData.folder = newfolder - if let index = newItem["index"] as? Int64 { - newData.index = index + newData = PlayList(context: context) + } + if let newid = newItem["id"] as? String, let newstorage = newItem["storage"] as? String, let newfolder = newItem["folder"] as? String { + newData.id = newid + newData.storage = newstorage + newData.folder = newfolder + if let index = newItem["index"] as? Int64 { + newData.index = index + } + else { + newData.index = Int64(Date().timeIntervalSince1970 * 1000) + } + newData.serial = serial } else { - newData.index = Int64(Date().timeIntervalSince1970 * 1000) + context.delete(newData) } - newData.serial = serial - } - else { - viewContext.delete(newData) + try? context.save() } - try? viewContext.save() } - public func setMark(storage: String, targetID: String, parentID: String, position: Double?) { + public func setMark(storage: String, targetID: String, parentID: String, position: Double?, onFinish: @escaping ()->Void) { var result = [UInt8](repeating: 0, count: Int(CC_SHA512_DIGEST_LENGTH)) guard let data = "storage=\(storage),target=\(targetID)".cString(using: .utf8) else { + onFinish() return } CC_SHA512(data, CC_LONG(data.count-1), &result) let target = result.map({ String(format: "%02hhx", $0) }).joined() - let viewContext = persistentContainer.viewContext - - let fetchRequest = NSFetchRequest(entityName: "Mark") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", target, storage) - if let result = try? viewContext.fetch(fetchRequest) { - for object in result { - viewContext.delete(object as! NSManagedObject) + persistentContainer.performBackgroundTask { context in + let fetchRequest = NSFetchRequest(entityName: "Mark") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", target, storage) + if let result = try? context.fetch(fetchRequest) { + for object in result { + context.delete(object as! NSManagedObject) + } } - } - - if let position = position { - let newitem = Mark(context: viewContext) - newitem.id = target - newitem.storage = storage - newitem.parent = parentID - newitem.position = position - try? viewContext.save() - } - else { - try? viewContext.save() + if let position = position { + let newitem = Mark(context: context) + newitem.id = target + newitem.storage = storage + newitem.parent = parentID + newitem.position = position + + try? context.save() + } + else { + try? context.save() + } + onFinish() } } @@ -652,6 +636,11 @@ public class dataItems { } public func getImage(storage: String, parentId: String, baseName: String) -> RemoteData? { + if !Thread.isMainThread { + return DispatchQueue.main.sync { + self.getImage(storage: storage, parentId: parentId, baseName: baseName) + } + } let viewContext = persistentContainer.viewContext let fetchrequest = NSFetchRequest(entityName: "RemoteData") @@ -680,7 +669,37 @@ public class dataItems { return nil } + public func getData(storage: String, fileId: String, onFinish: @escaping (RemoteData?)->Void) { + if !Thread.isMainThread { + DispatchQueue.main.async { + let viewContext = self.persistentContainer.viewContext + + let fetchrequest = NSFetchRequest(entityName: "RemoteData") + fetchrequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, storage) + let ret = ((try? viewContext.fetch(fetchrequest)) as? [RemoteData])?.first + DispatchQueue.global().async { + onFinish(ret) + } + } + } + else { + let viewContext = self.persistentContainer.viewContext + + let fetchrequest = NSFetchRequest(entityName: "RemoteData") + fetchrequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, storage) + let ret = ((try? viewContext.fetch(fetchrequest)) as? [RemoteData])?.first + DispatchQueue.global().async { + onFinish(ret) + } + } + } + public func getData(storage: String, fileId: String) -> RemoteData? { + if !Thread.isMainThread { + return DispatchQueue.main.sync { + self.getData(storage: storage, fileId: fileId) + } + } let viewContext = persistentContainer.viewContext let fetchrequest = NSFetchRequest(entityName: "RemoteData") @@ -689,6 +708,11 @@ public class dataItems { } public func getData(path: String) -> RemoteData? { + if !Thread.isMainThread { + return DispatchQueue.main.sync { + self.getData(path: path) + } + } let viewContext = persistentContainer.viewContext let fetchrequest = NSFetchRequest(entityName: "RemoteData") diff --git a/RemoteCloud/RemoteCloud/RemoteStorage.swift b/RemoteCloud/RemoteCloud/RemoteStorage.swift index 3fcecdf..ea701cb 100644 --- a/RemoteCloud/RemoteCloud/RemoteStorage.swift +++ b/RemoteCloud/RemoteCloud/RemoteStorage.swift @@ -16,6 +16,7 @@ public enum CloudStorages: CaseIterable { case GoogleDrive case OneDrive case pCloud + case WebDAV case CryptCarotDAV case CryptRclone case Cryptomator @@ -86,7 +87,8 @@ public class RemoteItem { self.subid = nil } else { - guard let origin = CloudFactory.shared.data.getData(storage: storage, fileId: id) else { + let item1 = CloudFactory.shared.data.getData(storage: storage, fileId: id) + guard let origin = item1 else { return nil } guard let name = origin.name else { @@ -201,6 +203,7 @@ public class CloudFactory { let classmap = Dictionary(uniqueKeysWithValues: CloudStorages.allCases.map() { (CloudFactory.getServiceName(service: $0), $0) }) do { if let d = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(sList) as? [String: String] { + print(d) let tList = d.map() { key, value -> (String, RemoteStorage?) in guard let jsondata = value.data(using: .utf8) else { return (key, nil) @@ -245,6 +248,8 @@ public class CloudFactory { return UIImage(named: "onedrive", in: Bundle(for: type(of: self)), compatibleWith: nil) case .pCloud: return UIImage(named: "pcloud", in: Bundle(for: type(of: self)), compatibleWith: nil) + case .WebDAV: + return UIImage(named: "webdav", in: Bundle(for: type(of: self)), compatibleWith: nil) case .CryptCarotDAV: return UIImage(named: "carot", in: Bundle(for: type(of: self)), compatibleWith: nil) case .CryptRclone: @@ -266,6 +271,8 @@ public class CloudFactory { return "OneDrive" case .pCloud: return "pCloud" + case .WebDAV: + return "WebDAV" case .CryptCarotDAV: return "CryptCarotDAV" case .CryptRclone: @@ -289,6 +296,8 @@ public class CloudFactory { return OneDriveStorage(name: tagname) case .pCloud: return pCloudStorage(name: tagname) + case .WebDAV: + return WebDAVStorage(name: tagname) case .CryptCarotDAV: return CryptCarotDAV(name: tagname) case .CryptRclone: @@ -325,38 +334,36 @@ public class CloudFactory { func loadChild(item: RemoteData, onFinish: @escaping ([RemoteData])->Void) { self[item.storage ?? ""]?.list(fileId: item.id ?? "") { - DispatchQueue.main.async { - var loadData = [RemoteData]() - let children = self.data.listData(storage: item.storage ?? "", parentID: item.id ?? "") - loadData += children.filter({ $0.folder }) - DispatchQueue.global().async { - onFinish(loadData) - } + var loadData = [RemoteData]() + let children = self.data.listData(storage: item.storage ?? "", parentID: item.id ?? "") + loadData += children.filter({ $0.folder }) + DispatchQueue.global().async { + onFinish(loadData) } } } func findChild(storage: String, id: String, onFinish: @escaping ([RemoteData])->Void) { - DispatchQueue.main.async { - let children = self.data.listData(storage: storage, parentID: id) - DispatchQueue.global().async { - var loadData = [RemoteData]() - if children.count > 0 { - for item in children.filter({ $0.folder }) { - let group = DispatchGroup() - group.enter() - self.findChild(storage: item.storage ?? "", id: item.id ?? "") { newload in - loadData += newload - group.leave() - } - group.wait() + let children = self.data.listData(storage: storage, parentID: id) + DispatchQueue.global().async { + var loadData = [RemoteData]() + if children.count > 0 { + for item in children.filter({ $0.folder }) { + let group = DispatchGroup() + group.enter() + self.findChild(storage: item.storage ?? "", id: item.id ?? "") { newload in + loadData += newload + group.leave() } - onFinish(loadData) + group.wait() } - else { - if let item = self.data.getData(storage: storage, fileId: id) { - loadData += [item] - } + onFinish(loadData) + } + else { + if let item = self.data.getData(storage: storage, fileId: id) { + loadData += [item] + } + DispatchQueue.global().async { onFinish(loadData) } } @@ -590,18 +597,16 @@ public class RemoteStorageBase: NSObject, RemoteStorage { } func deleteItems(name: String) { - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + CloudFactory.shared.data.persistentContainer.performBackgroundTask { context in let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "storage == %@", name) - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } - try? viewContext.save() + try? context.save() } } @@ -617,31 +622,29 @@ public class RemoteStorageBase: NSObject, RemoteStorage { onFinish?() } - // needs main thread - func deleteChild(parent: String) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", parent, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - for item in items { - viewContext.delete(item) + func deleteChild(parent: String, context: NSManagedObjectContext) { + context.perform { + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", parent, self.storageName ?? "") + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { + for item in items { + context.delete(item) + } } } } - // needs main thread - func deleteChildRecursive(parent: String) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", parent, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - for item in items { - if let p = item.id { - deleteChildRecursive(parent: p) + func deleteChildRecursive(parent: String, context: NSManagedObjectContext) { + context.perform { + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", parent, self.storageName ?? "") + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { + for item in items { + if let p = item.id { + self.deleteChildRecursive(parent: p, context: context) + } + context.delete(item) } - viewContext.delete(item) } } } @@ -649,46 +652,33 @@ public class RemoteStorageBase: NSObject, RemoteStorage { public func list(fileId: String, onFinish: (() -> Void)?) { if fileId == "" { - let group = DispatchGroup() - var path = "" - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - - self.deleteChild(parent: fileId) - try? viewContext.save() - } - group.notify(queue: .global()){ - self.ListChildren(onFinish: onFinish) + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.deleteChild(parent: fileId, context: backgroundContext) + backgroundContext.performAndWait { + try? backgroundContext.save() } + self.ListChildren(onFinish: onFinish) } else { - let group = DispatchGroup() var path = "" - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + + backgroundContext.performAndWait { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { if let items = result as? [RemoteData] { path = items.first?.path ?? "" } } if path != "" { - self.deleteChild(parent: fileId) - - try? viewContext.save() + self.deleteChild(parent: fileId, context: backgroundContext) } } - group.notify(queue: .global()){ + backgroundContext.performAndWait { if path != "" { + try? backgroundContext.save() self.ListChildren(fileId: fileId, path: path, onFinish: onFinish) } else { @@ -703,27 +693,22 @@ public class RemoteStorageBase: NSObject, RemoteStorage { ListChildren(onFinish: onFinish) } else { - let group = DispatchGroup() var ids: [String] = [] - DispatchQueue.main.async { - group.enter() - defer { group.leave() } - - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.performAndWait { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "path == %@", path) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let result = try? backgroundContext.fetch(fetchRequest), let items = result as? [RemoteData] { ids = items.filter { $0.id != nil }.map { $0.id! } } for id in ids { - self.deleteChild(parent: id) - - try? viewContext.save() + self.deleteChild(parent: id, context: backgroundContext) } } - group.notify(queue: .global()){ + backgroundContext.perform { + try? backgroundContext.save() + if ids.isEmpty { onFinish?() return @@ -740,12 +725,8 @@ public class RemoteStorageBase: NSObject, RemoteStorage { makeFolder(parentId: parentId, parentPath: "", newname: newname, onFinish: onFinish) } else{ - let group = DispatchGroup() var path = "" - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -756,14 +737,25 @@ public class RemoteStorageBase: NSObject, RemoteStorage { } } } - group.notify(queue: .global()){ - if path != "" { - self.makeFolder(parentId: parentId, parentPath: path, newname: newname, onFinish: onFinish) - } - else { - onFinish?(nil) + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + path = items.first?.path ?? "" + } + } } } + if path != "" { + self.makeFolder(parentId: parentId, parentPath: path, newname: newname, onFinish: onFinish) + } + else { + onFinish?(nil) + } } } diff --git a/RemoteCloud/RemoteCloud/Storages/Cryptomator.swift b/RemoteCloud/RemoteCloud/Storages/Cryptomator.swift index a9d0160..f88a9a1 100644 --- a/RemoteCloud/RemoteCloud/Storages/Cryptomator.swift +++ b/RemoteCloud/RemoteCloud/Storages/Cryptomator.swift @@ -595,8 +595,8 @@ public class Cryptomator: ChildStorage { func findParentStorage(baseId: String = "", onFinish: @escaping ([RemoteData])->Void){ let fixId = baseId == "" ? baseRootFileId: baseId CloudFactory.shared[baseRootStorage]?.list(fileId: fixId) { - DispatchQueue.main.async { - let result = CloudFactory.shared.data.listData(storage: self.baseRootStorage, parentID: fixId) + let result = CloudFactory.shared.data.listData(storage: self.baseRootStorage, parentID: fixId) + DispatchQueue.global().async { onFinish(result) } } @@ -726,16 +726,10 @@ public class Cryptomator: ChildStorage { } } - func storeItem(parentId: String, item: RemoteItem, name: String, isFolder: Bool, dirId: String, deflatedName: String, path: String, group: DispatchGroup) { + func storeItem(parentId: String, item: RemoteItem, name: String, isFolder: Bool, dirId: String, deflatedName: String, path: String, context: NSManagedObjectContext) { os_log("%{public}@", log: log, type: .debug, "storeItem(cryptomator:\(storageName ?? "")) \(name)") - group.enter() - DispatchQueue.main.async { - defer { - group.leave() - } - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + context.perform { let newid = "\(dirId)/\(deflatedName)" let newname = name let newcdate = item.cDate @@ -745,13 +739,13 @@ public class Cryptomator: ChildStorage { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = newid newitem.name = newname @@ -771,7 +765,7 @@ public class Cryptomator: ChildStorage { else { newitem.path = "\(path)/\(newname)" } - try? viewContext.save() + try? context.save() } } @@ -780,14 +774,14 @@ public class Cryptomator: ChildStorage { onFinish?() return } - let group = DispatchGroup() - group.enter() + group.enter() findParentStorage(path: [DATA_DIR_NAME, String(dirIdHash.prefix(2)), String(dirIdHash.suffix(30))]) { items in defer { group.leave() } + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() for item in items { if item.name.hasSuffix(self.LONG_NAME_FILE_EXT) { // long name @@ -807,10 +801,14 @@ public class Cryptomator: ChildStorage { return } if t == .directory { - self.storeItem(parentId: fileId, item: item, name: decryptedName, isFolder: true, dirId: dirId, deflatedName: item.name, path: path, group: group) + self.storeItem(parentId: fileId, item: item, name: decryptedName, isFolder: true, dirId: dirId, deflatedName: item.name, path: path, context: backgroundContext) } else if t == .regular { - self.storeItem(parentId: fileId,item: item, name: decryptedName, isFolder: false, dirId: dirId, deflatedName: item.name, path: path, group: group) + self.storeItem(parentId: fileId,item: item, name: decryptedName, isFolder: false, dirId: dirId, deflatedName: item.name, path: path, context: backgroundContext) + } + group.enter() + backgroundContext.perform { + group.leave() } } } @@ -823,15 +821,18 @@ public class Cryptomator: ChildStorage { continue } if t == .directory { - self.storeItem(parentId: fileId, item: item, name: decryptedName, isFolder: true, dirId: dirId, deflatedName: item.name, path: path, group: group) + self.storeItem(parentId: fileId, item: item, name: decryptedName, isFolder: true, dirId: dirId, deflatedName: item.name, path: path, context: backgroundContext) } else if t == .regular { - self.storeItem(parentId: fileId, item: item, name: decryptedName, isFolder: false, dirId: dirId, deflatedName: item.name, path: path, group: group) + self.storeItem(parentId: fileId, item: item, name: decryptedName, isFolder: false, dirId: dirId, deflatedName: item.name, path: path, context: backgroundContext) + } + group.enter() + backgroundContext.perform { + group.leave() } } } } - group.notify(queue: .global()) { onFinish?() } @@ -1135,12 +1136,12 @@ public class Cryptomator: ChildStorage { return } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + CloudFactory.shared.data.persistentContainer.performBackgroundTask { + context in var ret: String? let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, self.baseRootStorage) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { if let item = items.first { let newid = "\(parentDirId)/\(deflatedName ?? encDirname)" let newcdate = item.cdate @@ -1148,13 +1149,13 @@ public class Cryptomator: ChildStorage { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newid, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = newid newitem.name = newname @@ -1175,7 +1176,7 @@ public class Cryptomator: ChildStorage { newitem.path = "\(parentPath)/\(newname)" } ret = newid - try? viewContext.save() + try? context.save() } } @@ -1315,14 +1316,14 @@ public class Cryptomator: ChildStorage { ret = false return } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + CloudFactory.shared.data.persistentContainer.performBackgroundTask { context in let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName!) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { for item in items { - viewContext.delete(item) + context.delete(item) } + try? context.save() } } } @@ -1359,14 +1360,14 @@ public class Cryptomator: ChildStorage { ret = false return } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + CloudFactory.shared.data.persistentContainer.performBackgroundTask { context in let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName!) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { for item in items { - viewContext.delete(item) + context.delete(item) } + try? context.save() } } } @@ -1444,23 +1445,23 @@ public class Cryptomator: ChildStorage { } } } - group3.notify(queue: .main) { + group3.notify(queue: .global()) { DispatchQueue.global().async { self.removeDirCache(fileId: fileId) } - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - for item in items { - viewContext.delete(item) + CloudFactory.shared.data.persistentContainer.performBackgroundTask { context in + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { + for item in items { + context.delete(item) + } + try? context.save() + } + DispatchQueue.global().async { + onFinish?(ret) } - try? viewContext.save() - } - - DispatchQueue.global().async { - onFinish?(ret) } } } @@ -1514,15 +1515,14 @@ public class Cryptomator: ChildStorage { } self.removeDirCache(dirId: parentDirId) - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + CloudFactory.shared.data.persistentContainer.performBackgroundTask { context in let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { for item in items { - viewContext.delete(item) + context.delete(item) } - try? viewContext.save() + try? context.save() } DispatchQueue.global().async { @@ -1563,29 +1563,42 @@ public class Cryptomator: ChildStorage { return } - let group = DispatchGroup() - var going = true - var parentPath = "" + var parentPath1: String? var parentId = c.parent if parentId != "" { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") if let result = try? viewContext.fetch(fetchRequest) { if let items = result as? [RemoteData] { - parentPath = items.first?.path ?? "" - return + parentPath1 = items.first?.path ?? "" + } + } + } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath1 = items.first?.path ?? "" + } } } - going = false } } + guard let parentPath = parentPath1 else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + var going = true let group2 = DispatchGroup() guard let ename = self.encryptFilename(cleartextName: newname, dirId: parentDirId) else { onFinish?(nil) @@ -1599,14 +1612,9 @@ public class Cryptomator: ChildStorage { else { deflatedName = self.deflate(longFileName: encFilename) group2.enter() - group.notify(queue: .global()) { - guard going else { - return - } - self.uploadMetadataFile(shortName: deflatedName!, orgName: encFilename) { success in - going = going && success - group2.leave() - } + self.uploadMetadataFile(shortName: deflatedName!, orgName: encFilename) { success in + going = going && success + group2.leave() } } @@ -1672,12 +1680,11 @@ public class Cryptomator: ChildStorage { self.removeDirCache(dirId: parentDirId) - DispatchQueue.main.async { + CloudFactory.shared.data.persistentContainer.performBackgroundTask { context in var ret: String? = nil - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { if let item = items.first { let newid = "\(parentDirId)/\(deflatedName ?? encFilename)" let newname = newname @@ -1686,7 +1693,7 @@ public class Cryptomator: ChildStorage { let newfolder = item.folder let newsize = item.size - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = newid newitem.name = newname @@ -1710,10 +1717,10 @@ public class Cryptomator: ChildStorage { } if ret != nil { for item in items { - viewContext.delete(item) + context.delete(item) } } - try? viewContext.save() + try? context.save() } DispatchQueue.global().async { @@ -1754,16 +1761,14 @@ public class Cryptomator: ChildStorage { return } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + CloudFactory.shared.data.persistentContainer.performBackgroundTask { context in var newcdate: Date? = nil var newmdate: Date? = nil var newId: String? = nil let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.baseRootStorage) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { if let baseItem = items.first { newcdate = baseItem.cdate newmdate = baseItem.mdate @@ -1771,11 +1776,11 @@ public class Cryptomator: ChildStorage { } let fetchRequest1 = NSFetchRequest(entityName: "RemoteData") fetchRequest1.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName!) - if let result = try? viewContext.fetch(fetchRequest1), let items1 = result as? [RemoteData] { + if let result = try? context.fetch(fetchRequest1), let items1 = result as? [RemoteData] { if let pitem = items1.first { pitem.cdate = newcdate pitem.mdate = newmdate - try? viewContext.save() + try? context.save() newId = pitem.id } @@ -1807,157 +1812,162 @@ public class Cryptomator: ChildStorage { } // first, find name for moving item - let group = DispatchGroup() - var orgname: String? = nil + var orgname1: String? = nil var isFolder = false - group.enter() - DispatchQueue.main.async { - defer { - group.leave() - } + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { if let item = items.first { - orgname = item.name + orgname1 = item.name isFolder = item.folder } } } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let item = items.first { + orgname1 = item.name + isFolder = item.folder + } + } + } + } - group.notify(queue: .global()) { - guard let orgname = orgname else { + guard let orgname = orgname1 else { + onFinish?(nil) + return + } + + let array = fileId.components(separatedBy: "/") + let parentDirId = array[0] + let deflateId = array[1] + + guard let parentIdHash = self.resolveDirectory(dirId: parentDirId) else { + onFinish?(nil) + return + } + + self.resolveDirId(fileId: fixToParentId) { toParentDirId in + guard let toParentDirId = toParentDirId else { onFinish?(nil) return } - - let array = fileId.components(separatedBy: "/") - let parentDirId = array[0] - let deflateId = array[1] - - guard let parentIdHash = self.resolveDirectory(dirId: parentDirId) else { + guard let toParentIdHash = self.resolveDirectory(dirId: toParentDirId) else { onFinish?(nil) return } - self.resolveDirId(fileId: fixToParentId) { toParentDirId in - guard let toParentDirId = toParentDirId else { - onFinish?(nil) - return - } - guard let toParentIdHash = self.resolveDirectory(dirId: toParentDirId) else { + // find base item for toDir + self.findParentStorage(path: [self.DATA_DIR_NAME, String(toParentIdHash.prefix(2)), String(toParentIdHash.suffix(30))], expandDir: false) { toItems in + guard toItems.count > 0 else { onFinish?(nil) return } + let toItem = toItems[0] //baseItem for toDir - // find base item for toDir - self.findParentStorage(path: [self.DATA_DIR_NAME, String(toParentIdHash.prefix(2)), String(toParentIdHash.suffix(30))], expandDir: false) { toItems in - guard toItems.count > 0 else { + // find base item for moving item + self.findParentStorage(path: [self.DATA_DIR_NAME, String(parentIdHash.prefix(2)), String(parentIdHash.suffix(30)), deflateId], expandDir: false) { items in + guard items.count > 0 else { onFinish?(nil) return } - let toItem = toItems[0] //baseItem for toDir + let item = items[0] //baseItem for moving - // find base item for moving item - self.findParentStorage(path: [self.DATA_DIR_NAME, String(parentIdHash.prefix(2)), String(parentIdHash.suffix(30)), deflateId], expandDir: false) { items in - guard items.count > 0 else { + // move base item to toDir + item.move(toParentId: toItem.id) { newBaseId in + guard let newBaseId = newBaseId else { onFinish?(nil) return } - let item = items[0] //baseItem for moving - // move base item to toDir - item.move(toParentId: toItem.id) { newBaseId in - guard let newBaseId = newBaseId else { + // parent dirid is changed, encrypted name will be changed + guard let encFilename = self.encryptFilename(cleartextName: orgname, dirId: toParentDirId) else { + onFinish?(nil) + return + } + let uploadname = isFolder ? "0"+encFilename : encFilename + let deflatedName: String? + var done = false + let group2 = DispatchGroup() + if encFilename.count <= 129 { + deflatedName = nil + done = true + } + else { + // long name + deflatedName = self.deflate(longFileName: uploadname) + group2.enter() + self.uploadMetadataFile(shortName: deflatedName!, orgName: uploadname) { success in + done = success + group2.leave() + } + } + + group2.notify(queue: .global()) { + guard done else { onFinish?(nil) return } - // parent dirid is changed, encrypted name will be changed - guard let encFilename = self.encryptFilename(cleartextName: orgname, dirId: toParentDirId) else { + // rename to new encrypted name + guard let newBaseItem = CloudFactory.shared[self.baseRootStorage]?.get(fileId: newBaseId == "" ? self.baseRootFileId : newBaseId) else { onFinish?(nil) return } - let uploadname = isFolder ? "0"+encFilename : encFilename - let deflatedName: String? - var done = false - let group2 = DispatchGroup() - if encFilename.count <= 129 { - deflatedName = nil - done = true - } - else { - // long name - deflatedName = self.deflate(longFileName: uploadname) - group2.enter() - self.uploadMetadataFile(shortName: deflatedName!, orgName: uploadname) { success in - done = success - group2.leave() - } - } - - group2.notify(queue: .global()) { - guard done else { + newBaseItem.rename(newname: deflatedName ?? uploadname) { nbItem in + guard nbItem != nil else { onFinish?(nil) return } - // rename to new encrypted name - guard let newBaseItem = CloudFactory.shared[self.baseRootStorage]?.get(fileId: newBaseId == "" ? self.baseRootFileId : newBaseId) else { - onFinish?(nil) - return - } - newBaseItem.rename(newname: deflatedName ?? uploadname) { nbItem in - guard nbItem != nil else { - onFinish?(nil) + // move done successfully + self.removeDirCache(fileId: fromParentId) + self.removeDirCache(fileId: toParentId) + + // if old id is longname, remove old matadata + if deflateId.hasSuffix(self.LONG_NAME_FILE_EXT) { + guard let s = CloudFactory.shared[self.baseRootStorage] as? RemoteStorageBase else { return } - - // move done successfully - self.removeDirCache(fileId: fromParentId) - self.removeDirCache(fileId: toParentId) - - // if old id is longname, remove old matadata - if deflateId.hasSuffix(self.LONG_NAME_FILE_EXT) { - guard let s = CloudFactory.shared[self.baseRootStorage] as? RemoteStorageBase else { - return - } - self.findParentStorage(path: [self.METADATA_DIR_NAME, String(deflateId.prefix(2)), String(deflateId.dropFirst(2).prefix(2))]) { metaItems in - for metaItem in metaItems { - if metaItem.name == deflateId { - s.deleteItem(fileId: metaItem.id) { success2 in - } - return + self.findParentStorage(path: [self.METADATA_DIR_NAME, String(deflateId.prefix(2)), String(deflateId.dropFirst(2).prefix(2))]) { metaItems in + for metaItem in metaItems { + if metaItem.name == deflateId { + s.deleteItem(fileId: metaItem.id) { success2 in } + return } } } - - // register record - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - let fetchRequest = NSFetchRequest(entityName: "RemoteData") - fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { - if let item = items.first { - item.id = "\(toParentDirId)/\(deflatedName ?? uploadname)" - item.cdate = newBaseItem.cDate - item.mdate = newBaseItem.mDate - item.parent = toParentId - if toParentId == "" { - item.path = "\(self.storageName ?? ""):/\(item.name ?? "")" - } - else { - item.path = "\(toItem.path)/\(item.name ?? "")" - } - try? viewContext.save() - let newId = item.id - - DispatchQueue.global().async { - onFinish?(newId) - } + } + + // register record + CloudFactory.shared.data.persistentContainer.performBackgroundTask { context in + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { + if let item = items.first { + item.id = "\(toParentDirId)/\(deflatedName ?? uploadname)" + item.cdate = newBaseItem.cDate + item.mdate = newBaseItem.mDate + item.parent = toParentId + if toParentId == "" { + item.path = "\(self.storageName ?? ""):/\(item.name ?? "")" + } + else { + item.path = "\(toItem.path)/\(item.name ?? "")" + } + try? context.save() + let newId = item.id + + DispatchQueue.global().async { + onFinish?(newId) } } } @@ -2039,12 +2049,11 @@ public class Cryptomator: ChildStorage { } self.removeDirCache(fileId: parentId) - DispatchQueue.main.async { + CloudFactory.shared.data.persistentContainer.performBackgroundTask { context in var ret: String? = nil - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", newBaseId, self.baseRootStorage) - if let result = try? viewContext.fetch(fetchRequest), let items = result as? [RemoteData] { + if let result = try? context.fetch(fetchRequest), let items = result as? [RemoteData] { if let item = items.first { let newid = "\(dirId)/\(deflatedName ?? encFilename)" let newname = uploadname @@ -2053,7 +2062,7 @@ public class Cryptomator: ChildStorage { let newfolder = item.folder let newsize = self.ConvertDecryptSize(size: item.size) - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = newid newitem.name = newname @@ -2073,7 +2082,7 @@ public class Cryptomator: ChildStorage { else { newitem.path = "\(parentPath)/\(newname)" } - try? viewContext.save() + try? context.save() ret = newid } } diff --git a/RemoteCloud/RemoteCloud/Storages/DropBoxStorage.swift b/RemoteCloud/RemoteCloud/Storages/DropBoxStorage.swift index 9cb4d48..b479a53 100644 --- a/RemoteCloud/RemoteCloud/Storages/DropBoxStorage.swift +++ b/RemoteCloud/RemoteCloud/Storages/DropBoxStorage.swift @@ -37,6 +37,7 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD } var webAuthSession: ASWebAuthenticationSession? + let uploadSemaphore = DispatchSemaphore(value: 5) public convenience init(name: String) { self.init() @@ -138,7 +139,7 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD task.resume() } - func storeItem(item: [String: Any], parentFileId: String? = nil, parentPath: String? = nil, group: DispatchGroup?) { + func storeItem(item: [String: Any], parentFileId: String? = nil, parentPath: String? = nil, context: NSManagedObjectContext) { let formatter = ISO8601DateFormatter() guard let id = item["id"] as? String else { @@ -159,25 +160,22 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD let size = item["size"] as? Int64 ?? 0 let hashstr = item["content_hash"] as? String ?? "" - group?.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + context.perform { var prevParent: String? let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { if let item = object as? RemoteData { prevParent = item.parent } - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = id newitem.name = name @@ -192,7 +190,6 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD newitem.hashstr = hashstr newitem.parent = (parentFileId == nil) ? prevParent : parentFileId newitem.path = "\(self.storageName ?? ""):\(path_display)" - group?.leave() } } @@ -298,14 +295,13 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD override func ListChildren(fileId: String, path: String, onFinish: (() -> Void)?) { listFolder(path: fileId, cursor: "") { result in if let items = result { - let group = DispatchGroup() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() for item in items { - self.storeItem(item: item, parentFileId: fileId, group: group) + self.storeItem(item: item, parentFileId: fileId, context: backgroundContext) } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?() } @@ -318,6 +314,15 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD } override func readFile(fileId: String, start: Int64? = nil, length: Int64? = nil, callCount: Int = 0, onFinish: ((Data?) -> Void)?) { + + if let cache = CloudFactory.shared.cache.getCache(storage: storageName!, id: fileId, offset: start ?? 0, size: length ?? -1) { + if let data = try? Data(contentsOf: cache) { + os_log("%{public}@", log: log, type: .debug, "hit cache(dropbox:\(storageName ?? "") \(fileId) \(start ?? -1) \(length ?? -1) \((start ?? 0) + (length ?? 0))") + onFinish?(data) + return + } + } + if lastCall.timeIntervalSinceNow > -callWait || callSemaphore.wait(wallTimeout: .now()+5) == .timedOut { if cancelTime.timeIntervalSinceNow > 0 { cancelTime = Date(timeIntervalSinceNow: 0.5) @@ -437,15 +442,10 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD print(e) throw RetryError.Retry } - let group = DispatchGroup() - group.enter() - DispatchQueue.global().async { - self.storeItem(item: json, parentFileId: parentId, group: group) - group.leave() - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: json, parentFileId: parentId, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(true) } @@ -610,19 +610,19 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD guard let id = metadata["id"] as? String else { throw RetryError.Retry } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } - - self.deleteChildRecursive(parent: id) - + } + self.deleteChildRecursive(parent: id, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(true) } @@ -647,11 +647,11 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD } override func renameItem(fileId: String, newname: String, callCount: Int = 0, onFinish: ((String?) -> Void)?) { - DispatchQueue.main.async { + + var parentId: String? = nil + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - var parentId: String? = nil - let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") if let result = try? viewContext.fetch(fetchRequest) as? [RemoteData] { @@ -659,15 +659,28 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD parentId = item.parent } } - DispatchQueue.global().async { - if let parentId = parentId { - self.renameItem(fileId: fileId, parentId: parentId, newname: newname, onFinish: onFinish) - } - else { - onFinish?(nil) + } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) as? [RemoteData] { + if let item = result.first { + parentId = item.parent + } } } } + DispatchQueue.global().async { + if let parentId = parentId { + self.renameItem(fileId: fileId, parentId: parentId, newname: newname, onFinish: onFinish) + } + else { + onFinish?(nil) + } + } } func renameItem(fileId: String, parentId: String, newname: String, callCount: Int = 0, onFinish: ((String?) -> Void)?) { @@ -730,24 +743,19 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD guard let id = metadata["id"] as? String else { throw RetryError.Retry } - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } - self.storeItem(item: metadata, parentFileId: parentId, group: group) - group.leave() } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + self.storeItem(item: metadata, parentFileId: parentId, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(id) } @@ -776,11 +784,11 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD onFinish?(nil) return } - DispatchQueue.main.async { + var orgname: String? = nil + + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - var orgname: String? = nil - let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") if let result = try? viewContext.fetch(fetchRequest) as? [RemoteData] { @@ -788,15 +796,28 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD orgname = item.name } } - DispatchQueue.global().async { - if let orgname = orgname { - self.moveItem(fileId: fileId, orgname: orgname, toParentId: toParentId, onFinish: onFinish) - } - else { - onFinish?(nil) + } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) as? [RemoteData] { + if let item = result.first { + orgname = item.name + } } } } + DispatchQueue.global().async { + if let orgname = orgname { + self.moveItem(fileId: fileId, orgname: orgname, toParentId: toParentId, onFinish: onFinish) + } + else { + onFinish?(nil) + } + } } func moveItem(fileId: String, orgname: String, toParentId: String, callCount: Int = 0, onFinish: ((String?) -> Void)?) { @@ -859,24 +880,19 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD guard let id = metadata["id"] as? String else { throw RetryError.Retry } - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } - self.storeItem(item: metadata, parentFileId: toParentId, group: group) - group.leave() } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + self.storeItem(item: metadata, parentFileId: toParentId, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(id) } @@ -911,12 +927,8 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD override func uploadFile(parentId: String, sessionId: String, uploadname: String, target: URL, onFinish: ((String?)->Void)?) { os_log("%{public}@", log: log, type: .debug, "uploadFile(dropbox:\(storageName ?? "") \(uploadname)->\(parentId) \(target)") - let group = DispatchGroup() var parentPath = "" - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -931,7 +943,24 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD parentPath = String(parentPath.dropFirst(p.count)) } } - group.notify(queue: .global()){ + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } + } + let p = "\(self.storageName ?? ""):" + if parentPath.hasPrefix(p) { + parentPath = String(parentPath.dropFirst(p.count)) + } + } + } + DispatchQueue.global().async { do { let attr = try FileManager.default.attributesOfItem(atPath: target.path) let fileSize = attr[.size] as! UInt64 @@ -946,15 +975,21 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD } } catch { + onFinish?(nil) return } } } func uploadShortFile(parentId: String, sessionId: String, parentPath: String, uploadname: String, target: URL, onFinish: ((String?)->Void)?) { + uploadSemaphore.wait() checkToken() { success in + let onFinish: (String?)->Void = { id in + self.uploadSemaphore.signal() + onFinish?(id) + } guard success else { - onFinish?(nil) + onFinish(nil) return } let url = "https://content.dropboxapi.com/2/files/upload" @@ -968,7 +1003,7 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD request.setValue(jsondata, forHTTPHeaderField: "Dropbox-API-Arg") guard let postData = try? Data(contentsOf: target) else { - onFinish?(nil) + onFinish(nil) return } request.httpBody = postData @@ -977,44 +1012,40 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD do { if let error = error { print(error) - onFinish?(nil) + onFinish(nil) return } guard let data = data else { - onFinish?(nil) + onFinish(nil) return } let object = try JSONSerialization.jsonObject(with: data, options: []) guard let json = object as? [String: Any] else { - onFinish?(nil) + onFinish(nil) return } print(json) guard let id = json["id"] as? String else { - onFinish?(nil) + onFinish(nil) return } - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } - self.storeItem(item: json, parentFileId: parentId, group: group) - group.leave() } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + self.storeItem(item: json, parentFileId: parentId, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() print("done") - - onFinish?(id) + DispatchQueue.global().async { + onFinish(id) + } } } catch let e { @@ -1035,9 +1066,14 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD var buf:[UInt8] = [UInt8](repeating: 0, count: 32*1024*1024) let len = targetStream.read(&buf, maxLength: buf.count) + uploadSemaphore.wait() checkToken() { success in + let onFinish: (String?)->Void = { id in + self.uploadSemaphore.signal() + onFinish?(id) + } guard success else { - onFinish?(nil) + onFinish(nil) return } let url = "https://content.dropboxapi.com/2/files/upload_session/start" @@ -1050,47 +1086,49 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD let jsondata: [String: Any] = [ "close": false] guard let argData = try? JSONSerialization.data(withJSONObject: jsondata) else { - onFinish?(nil) + onFinish(nil) return } request.setValue(String(data: argData, encoding: .utf8) ?? "", forHTTPHeaderField: "Dropbox-API-Arg") let tmpurl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(Int.random(in: 0...0xffffffff))") try? Data(bytes: buf, count: len).write(to: tmpurl) - #if !targetEnvironment(macCatalyst) let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).\(self.storageName ?? "").\(Int.random(in: 0..<0xffffffff))") //config.isDiscretionary = true config.sessionSendsLaunchEvents = true - #else - let config = URLSessionConfiguration.default - #endif let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) self.sessions += [session] let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier - self.task_upload[taskid] = ["data": Data(), "target": targetStream, "parentId": parentId, "parentPath": parentPath, "uploadname": uploadname, "offset": len, "tmpfile": tmpurl, "orgtarget": target, "accessToken": self.accessToken, "session": sessionId] - self.onFinsh_upload[taskid] = onFinish - task.resume() + let taskId = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + self.taskQueue.async { + self.task_upload[taskId] = ["data": Data(), "target": targetStream, "parentId": parentId, "parentPath": parentPath, "uploadname": uploadname, "offset": len, "tmpfile": tmpurl, "orgtarget": target, "accessToken": self.accessToken, "session": sessionId] + self.onFinsh_upload[taskId] = onFinish + task.resume() + } } } - var task_upload = [Int: [String: Any]]() - var onFinsh_upload = [Int: ((String?)->Void)?]() + var task_upload = [String: [String: Any]]() + var onFinsh_upload = [String: ((String?)->Void)?]() var sessions = [URLSession]() - + let taskQueue = DispatchQueue(label: "taskDict") + public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { CloudFactory.shared.urlSessionDidFinishCallback?(session) } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - if var taskdata = self.task_upload[dataTask.taskIdentifier] { + let taskId = (session.configuration.identifier ?? "") + ".\(dataTask.taskIdentifier)" + if var taskdata = taskQueue.sync(execute: { self.task_upload[taskId] }) { guard var recvData = taskdata["data"] as? Data else { return } recvData.append(data) taskdata["data"] = recvData - self.task_upload[dataTask.taskIdentifier] = taskdata + taskQueue.async { + self.task_upload[taskId] = taskdata + } } } @@ -1099,10 +1137,15 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - guard let taskdata = self.task_upload.removeValue(forKey: task.taskIdentifier) else { + let taskId = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + guard let taskdata = taskQueue.sync(execute: { + self.task_upload.removeValue(forKey: taskId) + }) else { return } - guard let onFinish = self.onFinsh_upload.removeValue(forKey: task.taskIdentifier) else { + guard let onFinish = taskQueue.sync(execute: { + self.onFinsh_upload.removeValue(forKey: taskId) + }) else { print("onFinish not found") return } @@ -1143,24 +1186,19 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD print(object) throw RetryError.Failed } - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } - self.storeItem(item: object, parentFileId: parentId, group: group) - group.leave() } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + self.storeItem(item: object, parentFileId: parentId, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() print("done") DispatchQueue.global().async { onFinish?(id) @@ -1283,10 +1321,12 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD try? Data(bytes: buf, count: len).write(to: tmpurl) let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier - self.task_upload[taskid] = ["data": Data(), "session_id": session_id, "target": targetStream, "parentId": parentId, "parentPath": parentPath, "uploadname": uploadname, "offset": len+offset, "tmpfile": tmpurl, "orgtarget": orgtarget, "accessToken": token, "session": sessionId] - self.onFinsh_upload[taskid] = onFinish - task.resume() + let taskId = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + taskQueue.async { + self.task_upload[taskId] = ["data": Data(), "session_id": session_id, "target": targetStream, "parentId": parentId, "parentPath": parentPath, "uploadname": uploadname, "offset": len+offset, "tmpfile": tmpurl, "orgtarget": orgtarget, "accessToken": token, "session": sessionId] + self.onFinsh_upload[taskId] = onFinish + task.resume() + } } else { targetStream.close() @@ -1304,10 +1344,12 @@ public class DropBoxStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionD try? Data(bytes: buf, count: len).write(to: tmpurl) let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier - self.task_upload[taskid] = ["data": Data(), "parentId": parentId, "tmpfile": tmpurl, "accessToken": token] - self.onFinsh_upload[taskid] = onFinish - task.resume() + let taskId = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + taskQueue.async { + self.task_upload[taskId] = ["data": Data(), "parentId": parentId, "tmpfile": tmpurl, "accessToken": token] + self.onFinsh_upload[taskId] = onFinish + task.resume() + } } } catch { diff --git a/RemoteCloud/RemoteCloud/Storages/GoogleDriveStorage.swift b/RemoteCloud/RemoteCloud/Storages/GoogleDriveStorage.swift index 4214fb6..15170dc 100644 --- a/RemoteCloud/RemoteCloud/Storages/GoogleDriveStorage.swift +++ b/RemoteCloud/RemoteCloud/Storages/GoogleDriveStorage.swift @@ -19,7 +19,8 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess var webAuthSession: ASWebAuthenticationSession? var spaces = "drive" - + let uploadSemaphore = DispatchSemaphore(value: 5) + public convenience init(name: String) { self.init() service = CloudFactory.getServiceName(service: .GoogleDrive) @@ -201,11 +202,13 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess request.httpBody = postData let downloadTask = session.downloadTask(with: request) - let taskid = downloadTask.taskIdentifier - self.task_upload[taskid] = info - self.task_upload[taskid]?["refresh"] = rToken - self.onFinsh_upload[taskid] = onFinish - downloadTask.resume() + let taskid = (session.configuration.identifier ?? "") + ".\(downloadTask.taskIdentifier)" + taskQueue.async { + self.task_upload[taskid] = info + self.task_upload[taskid]?["refresh"] = rToken + self.onFinsh_upload[taskid] = onFinish + downloadTask.resume() + } } override func revokeToken(token: String, onFinish: ((Bool) -> Void)?) { @@ -415,7 +418,7 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess } } - func storeItem(item: [String: Any], parentFileId: String? = nil, parentPath: String? = nil, teamID: String? = nil, group: DispatchGroup?) { + func storeItem(item: [String: Any], parentFileId: String? = nil, parentPath: String? = nil, teamID: String? = nil, context: NSManagedObjectContext) { let formatter = ISO8601DateFormatter() formatter.formatOptions.insert(.withFractionalSeconds) @@ -448,16 +451,13 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess fixId = id } - group?.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + context.perform { var prevParent: String? var prevPath: String? let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fixId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { if let item = object as? RemoteData { prevPath = item.path @@ -465,12 +465,12 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess prevPath = component?.dropLast().joined(separator: "/") prevParent = item.parent } - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } if trashed == 0 { - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = fixId newitem.name = name @@ -493,20 +493,16 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess } } } - group?.leave() } } - func storeRootItems(group: DispatchGroup?) { - group?.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + func storeRootItems(context: NSManagedObjectContext) { + context.perform { let fetchRequest1 = NSFetchRequest(entityName: "RemoteData") fetchRequest1.predicate = NSPredicate(format: "parent == %@ && storage == %@", "", self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest1) { + if let result = try? context.fetch(fetchRequest1) { for object in result { - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } @@ -516,13 +512,13 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess for id in items { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { if result.count > 0 { continue } } - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = id newitem.name = names[id] @@ -535,11 +531,10 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess newitem.parent = "" newitem.path = "\(self.storageName ?? ""):/\(names[id]!)" } - group?.leave() } } - func storeTeamDriveItem(item: [String: Any], group: DispatchGroup?) { + func storeTeamDriveItem(item: [String: Any], context: NSManagedObjectContext) { let formatter = ISO8601DateFormatter() formatter.formatOptions.insert(.withFractionalSeconds) @@ -550,19 +545,16 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess return } - group?.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + context.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", "\(id) \(id)", self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = "\(id) \(id)" newitem.name = name @@ -574,7 +566,7 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess newitem.hashstr = "" newitem.parent = "teamdrives" newitem.path = "\(self.storageName ?? ""):/teamDrives/\(name)" - group?.leave() + } } @@ -583,14 +575,13 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess let fixFileId = (fileId == "") ? rootName : fileId listFiles(q: "'\(fixFileId)'+in+parents", pageToken: "") { result in if let items = result { - let group = DispatchGroup() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() for item in items { - self.storeItem(item: item, parentFileId: fileId, parentPath: path, group: group) + self.storeItem(item: item, parentFileId: fileId, parentPath: path, context: backgroundContext) } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?() } @@ -603,11 +594,10 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess return } if fileId == "" { - let group = DispatchGroup() - storeRootItems(group: group) - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + storeRootItems(context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?() } @@ -617,14 +607,12 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess if fileId == "teamdrives" { listTeamdrives(q: "", pageToken: "") { result in if let items = result { - let group = DispatchGroup() - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() for item in items { - self.storeTeamDriveItem(item: item, group: group) + self.storeTeamDriveItem(item: item, context: backgroundContext) } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?() } @@ -643,14 +631,12 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess let fixFileId = comp[1] listFiles(q: "'\(fixFileId)'+in+parents", pageToken: "", teamDrive: teamId) { result in if let items = result { - let group = DispatchGroup() - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() for item in items { - self.storeItem(item: item, parentFileId: fileId, parentPath: path, teamID: teamId, group: group) + self.storeItem(item: item, parentFileId: fileId, parentPath: path, teamID: teamId, context: backgroundContext) } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?() } @@ -665,14 +651,12 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess let fixFileId = (fileId == "mydrive") ? rootName : fileId listFiles(q: "'\(fixFileId)'+in+parents", pageToken: "") { result in if let items = result { - let group = DispatchGroup() - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() for item in items { - self.storeItem(item: item, parentFileId: fileId, parentPath: path, group: group) + self.storeItem(item: item, parentFileId: fileId, parentPath: path, context: backgroundContext) } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?() } @@ -823,11 +807,10 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess throw RetryError.Retry } - let group = DispatchGroup() - self.storeTeamDriveItem(item: json, group: group) - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeTeamDriveItem(item: json, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(id) } @@ -1011,8 +994,7 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess print(e) throw RetryError.Retry } - let group = DispatchGroup() - group.enter() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() DispatchQueue.global().async { let teamID: String? if fileId.contains(" ") { @@ -1022,14 +1004,12 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess else { teamID = nil } - self.storeItem(item: json, parentFileId: parentId, parentPath: parentPath, teamID: teamID, group: group) - group.leave() - } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() - DispatchQueue.global().async { - onFinish?(true) + self.storeItem(item: json, parentFileId: parentId, parentPath: parentPath, teamID: teamID, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?(true) + } } } } @@ -1075,10 +1055,12 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess request.setValue("gzip", forHTTPHeaderField: "Accept-Encoding") let downloadTask = session.downloadTask(with: request) - let taskid = downloadTask.taskIdentifier - self.task_upload[taskid] = ["parentId": parentId, "parentPath": parentPath] - self.onFinsh_upload[taskid] = onFinish - downloadTask.resume() + let taskid = (session.configuration.identifier ?? "") + ".\(downloadTask.taskIdentifier)" + taskQueue.async { + self.task_upload[taskid] = ["parentId": parentId, "parentPath": parentPath] + self.onFinsh_upload[taskid] = onFinish + downloadTask.resume() + } } func updateFile(fileId: String, metadata: [String: Any], callCount: Int = 0, onFinish: ((String?) -> Void)?) { @@ -1220,12 +1202,8 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess formParentFix = (fromParentId == "") ? rootName : fromParentId } - let group = DispatchGroup() if toParentId != "" { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -1236,80 +1214,91 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess } } } - } - group.notify(queue: .global()){ - os_log("%{public}@", log: self.log, type: .debug, "moveItem(google:\(self.storageName ?? "") \(formParentFix)->\(toParentFix)") - self.lastCall = Date() - self.checkToken() { success in - guard success else { - self.callSemaphore.signal() - onFinish?(nil) - return - } - - var request: URLRequest = URLRequest(url: URL(string: "https://www.googleapis.com/drive/v3/files/\(targetId)?addParents=\(toParentFix)&removeParents=\(formParentFix)&supportsTeamDrives=true")!) - request.httpMethod = "PATCH" - request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization") - request.setValue("gzip", forHTTPHeaderField: "Accept-Encoding") - request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type") - let json: [String: Any] = [:] - let postData = try? JSONSerialization.data(withJSONObject: json) - request.httpBody = postData - - let task = URLSession.shared.dataTask(with: request) { data, response, error in - self.callSemaphore.signal() - do { - guard let data = data else { - throw RetryError.Retry - } - let object = try JSONSerialization.jsonObject(with: data, options: []) - guard let json = object as? [String: Any] else { - throw RetryError.Retry + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", toParentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + toParentPath = items.first?.path ?? "" } - if let e = json["error"] { - print(e) - throw RetryError.Retry + } + } + } + } + os_log("%{public}@", log: self.log, type: .debug, "moveItem(google:\(self.storageName ?? "") \(formParentFix)->\(toParentFix)") + self.lastCall = Date() + self.checkToken() { success in + guard success else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + + var request: URLRequest = URLRequest(url: URL(string: "https://www.googleapis.com/drive/v3/files/\(targetId)?addParents=\(toParentFix)&removeParents=\(formParentFix)&supportsTeamDrives=true")!) + request.httpMethod = "PATCH" + request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization") + request.setValue("gzip", forHTTPHeaderField: "Accept-Encoding") + request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type") + let json: [String: Any] = [:] + let postData = try? JSONSerialization.data(withJSONObject: json) + request.httpBody = postData + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + self.callSemaphore.signal() + do { + guard let data = data else { + throw RetryError.Retry + } + let object = try JSONSerialization.jsonObject(with: data, options: []) + guard let json = object as? [String: Any] else { + throw RetryError.Retry + } + if let e = json["error"] { + print(e) + throw RetryError.Retry + } + guard let id = json["id"] as? String else { + throw RetryError.Retry + } + DispatchQueue.global().async { + let fixId: String + if fileId.contains(" ") { + // teamdrive + let comp = fileId.components(separatedBy: " ") + let driveId = comp[0] + fixId = "\(driveId) \(id)" } - guard let id = json["id"] as? String else { - throw RetryError.Retry + else { + fixId = id } - DispatchQueue.global().async { - let fixId: String - if fileId.contains(" ") { - // teamdrive - let comp = fileId.components(separatedBy: " ") - let driveId = comp[0] - fixId = "\(driveId) \(id)" + self.getFile(fileId: fixId, parentId: toParentId, parentPath: toParentPath) { s in + if s { + onFinish?(fixId) } else { - fixId = id - } - self.getFile(fileId: fixId, parentId: toParentId, parentPath: toParentPath) { s in - if s { - onFinish?(fixId) - } - else { - onFinish?(nil) - } + onFinish?(nil) } } } - catch RetryError.Retry { - if callCount < 100 { - DispatchQueue.global().asyncAfter(deadline: .now()+Double.random(in: 0..(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", "\(driveId) \(driveId)", self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } - try? viewContext.save() + try? context.save() DispatchQueue.global().async { onFinish?(true) @@ -1408,13 +1395,10 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess onFinish?(false) return } - - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - - self.deleteChildRecursive(parent: driveId) - - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.deleteChildRecursive(parent: driveId, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(true) } @@ -1428,12 +1412,10 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess onFinish?(false) return } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - - self.deleteChildRecursive(parent: id) - - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.deleteChildRecursive(parent: id, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(true) } @@ -1448,12 +1430,10 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess onFinish?(false) return } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - - self.deleteChildRecursive(parent: id) - - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.deleteChildRecursive(parent: id, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(true) } @@ -1508,12 +1488,10 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess guard let id = json["id"] as? String else { throw RetryError.Retry } - - let group = DispatchGroup() - self.storeTeamDriveItem(item: json, group: group) - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeTeamDriveItem(item: json, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(id) } @@ -1627,10 +1605,7 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess let group = DispatchGroup() if fixParentId != rootName { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -1641,8 +1616,26 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess } } } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } + } + } + } } + uploadSemaphore.wait() group.notify(queue: .global()) { + let onFinish: (String?)->Void = { id in + self.uploadSemaphore.signal() + onFinish?(id) + } self.checkToken() { success in do { guard success else { @@ -1688,26 +1681,24 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess request2.setValue("bytes 0-\(len-1)/\(fileSize)", forHTTPHeaderField: "Content-Range") - #if !targetEnvironment(macCatalyst) let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).\(self.storageName ?? "").\(Int.random(in: 0..<0xffffffff))") //config.isDiscretionary = true config.sessionSendsLaunchEvents = true - #else - let config = URLSessionConfiguration.default - #endif let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) self.sessions += [session] let task2 = session.uploadTask(with: request2, fromFile: tmpurl) - let taskid = task2.taskIdentifier - self.task_upload[taskid] = ["target": targetStream, "data": Data(),"upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "aToken": self.accessToken, "rToken": self.refreshToken, "offset": len, "size": Int(fileSize), "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] - self.onFinsh_upload[taskid] = onFinish - task2.resume() + let taskid = (session.configuration.identifier ?? "") + ".\(task2.taskIdentifier)" + self.taskQueue.async { + self.task_upload[taskid] = ["target": targetStream, "data": Data(),"upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "aToken": self.accessToken, "rToken": self.refreshToken, "offset": len, "size": Int(fileSize), "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] + self.onFinsh_upload[taskid] = onFinish + task2.resume() + } } catch { targetStream.close() try? FileManager.default.removeItem(at: target) - onFinish?(nil) + onFinish(nil) } } task.resume() @@ -1715,25 +1706,31 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess catch { targetStream.close() try? FileManager.default.removeItem(at: target) - onFinish?(nil) + onFinish(nil) } } } } - var task_upload = [Int: [String: Any]]() - var onFinsh_upload = [Int: ((String?)->Void)?]() + var task_upload = [String: [String: Any]]() + var onFinsh_upload = [String: ((String?)->Void)?]() var sessions = [URLSession]() + let taskQueue = DispatchQueue(label: "taskDict") public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { CloudFactory.shared.urlSessionDidFinishCallback?(session) } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - guard let taskdata = self.task_upload.removeValue(forKey: task.taskIdentifier) else { + let taskId = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + guard let taskdata = taskQueue.sync(execute: { + self.task_upload.removeValue(forKey: taskId) + }) else { return } - guard let onFinish = self.onFinsh_upload.removeValue(forKey: task.taskIdentifier) else { + guard let onFinish = taskQueue.sync(execute: { + self.onFinsh_upload.removeValue(forKey: taskId) + }) else { print("onFinish not found") return } @@ -1842,11 +1839,12 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess request.setValue("bytes \(offset)-\(offset+len-1)/\(fileSize)", forHTTPHeaderField: "Content-Range") let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier - self.task_upload[taskid] = ["target": targetStream, "data": Data(),"upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "aToken": aToken, "rToken": rToken, "offset": offset+len, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] - self.onFinsh_upload[taskid] = onFinish - task.resume() - + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + taskQueue.async { + self.task_upload[taskid] = ["target": targetStream, "data": Data(),"upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "aToken": aToken, "rToken": rToken, "offset": offset+len, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] + self.onFinsh_upload[taskid] = onFinish + task.resume() + } case 404: print("404 start from begining") @@ -1872,10 +1870,12 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess request.setValue("bytes \(offset)-\(offset+len-1)/\(fileSize)", forHTTPHeaderField: "Content-Range") let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier - self.task_upload[taskid] = ["target": targetStream, "data": Data(),"upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "aToken": aToken, "rToken": rToken, "offset": offset+len, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] - self.onFinsh_upload[taskid] = onFinish - task.resume() + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + taskQueue.async { + self.task_upload[taskid] = ["target": targetStream, "data": Data(),"upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "aToken": aToken, "rToken": rToken, "offset": offset+len, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] + self.onFinsh_upload[taskid] = onFinish + task.resume() + } default: print("\(httpResponse.statusCode) resume request") @@ -1901,10 +1901,12 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess try? Data().write(to: tmpurl) let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier - self.task_upload[taskid] = ["target": targetStream, "data": Data(),"upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "aToken": aToken, "rToken": rToken, "offset": 0, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] - self.onFinsh_upload[taskid] = onFinish - task.resume() + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + taskQueue.async { + self.task_upload[taskid] = ["target": targetStream, "data": Data(),"upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "aToken": aToken, "rToken": rToken, "offset": 0, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] + self.onFinsh_upload[taskid] = onFinish + task.resume() + } } } @@ -1916,13 +1918,16 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - if var taskdata = self.task_upload[dataTask.taskIdentifier] { + let taskId = (session.configuration.identifier ?? "") + ".\(dataTask.taskIdentifier)" + if var taskdata = taskQueue.sync(execute: { self.task_upload[taskId] }) { guard var recvData = taskdata["data"] as? Data else { return } recvData.append(data) taskdata["data"] = recvData - self.task_upload[dataTask.taskIdentifier] = taskdata + taskQueue.async { + self.task_upload[taskId] = taskdata + } } } @@ -1935,10 +1940,15 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { - guard let taskdata = self.task_upload.removeValue(forKey: downloadTask.taskIdentifier) else { + let taskId = (session.configuration.identifier ?? "") + ".\(downloadTask.taskIdentifier)" + guard let taskdata = taskQueue.sync(execute: { + self.task_upload.removeValue(forKey: taskId) + }) else { return } - guard let onFinish = self.onFinsh_upload.removeValue(forKey: downloadTask.taskIdentifier) else { + guard let onFinish = taskQueue.sync(execute: { + self.onFinsh_upload.removeValue(forKey: taskId) + }) else { print("onFinish not found") return } @@ -1948,9 +1958,10 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess return } if taskdata["location"] as? String == nil, let _ = taskdata["target"] as? URL, let _ = httpResponse.allHeaderFields["Location"] as? String { - - self.task_upload[downloadTask.taskIdentifier] = taskdata - self.onFinsh_upload[downloadTask.taskIdentifier] = onFinish + taskQueue.async { + self.task_upload[taskId] = taskdata + self.onFinsh_upload[taskId] = onFinish + } return } do { @@ -1996,19 +2007,15 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess onFinish?(nil) return } - let group = DispatchGroup() - group.enter() - DispatchQueue.global().async { - self.storeItem(item: json, parentFileId: parentId, parentPath: parentPath, group: group) - group.leave() - } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: json, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() print("done") - self.sessions.removeAll(where: { $0.configuration.identifier == session.configuration.identifier }) - session.finishTasksAndInvalidate() + self.taskQueue.async { + self.sessions.removeAll(where: { $0.configuration.identifier == session.configuration.identifier }) + } onFinish?(id) } @@ -2256,11 +2263,13 @@ public class GoogleDriveStorageCustom: GoogleDriveStorage { request.httpBody = postData let downloadTask = session.downloadTask(with: request) - let taskid = downloadTask.taskIdentifier - self.task_upload[taskid] = info - self.task_upload[taskid]?["refresh"] = rToken - self.onFinsh_upload[taskid] = onFinish - downloadTask.resume() + let taskid = (session.configuration.identifier ?? "") + ".\(downloadTask.taskIdentifier)" + taskQueue.async { + self.task_upload[taskid] = info + self.task_upload[taskid]?["refresh"] = rToken + self.onFinsh_upload[taskid] = onFinish + downloadTask.resume() + } } } diff --git a/RemoteCloud/RemoteCloud/Storages/LocalStorage.swift b/RemoteCloud/RemoteCloud/Storages/LocalStorage.swift index 59a4fca..0de6256 100644 --- a/RemoteCloud/RemoteCloud/Storages/LocalStorage.swift +++ b/RemoteCloud/RemoteCloud/Storages/LocalStorage.swift @@ -40,50 +40,41 @@ public class LocalStorage: RemoteStorageBase { return } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroudContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroudContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroudContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroudContext.delete(object as! NSManagedObject) } } } - let group = DispatchGroup() for fileURL in fileURLs { - storeItem(item: fileURL, parentFileId: fileId, parentPath: path, group: group) + storeItem(item: fileURL, parentFileId: fileId, parentPath: path, context: backgroudContext) } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroudContext.perform { + try? backgroudContext.save() DispatchQueue.global().async { onFinish?() } } } - func storeItem(item: URL, parentFileId: String? = nil, parentPath: String? = nil, group: DispatchGroup?) { + func storeItem(item: URL, parentFileId: String? = nil, parentPath: String? = nil, context: NSManagedObjectContext) { guard let attr = try? FileManager.default.attributesOfItem(atPath: item.path) else { return } let id = getIdFromURL(url: item) let name = item.lastPathComponent.precomposedStringWithCanonicalMapping - group?.enter() - DispatchQueue.main.async { - defer { - group?.leave() - } - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + context.perform { var prevParent: String? var prevPath: String? let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { if let item = object as? RemoteData { prevPath = item.path @@ -91,7 +82,7 @@ public class LocalStorage: RemoteStorageBase { prevPath = component?.dropLast().joined(separator: "/") prevParent = item.parent } - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } @@ -101,7 +92,7 @@ public class LocalStorage: RemoteStorageBase { guard t == .typeRegular || t == .typeDirectory else { return } - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = id newitem.name = name @@ -201,12 +192,11 @@ public class LocalStorage: RemoteStorageBase { do { try FileManager.default.createDirectory(at: targetURL, withIntermediateDirectories: false) - let group = DispatchGroup() - storeItem(item: targetURL, parentFileId: parentId, parentPath: parentPath, group: group) + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + storeItem(item: targetURL, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) let id = getIdFromURL(url: targetURL) - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(id) } @@ -229,13 +219,9 @@ public class LocalStorage: RemoteStorageBase { let name = fromURL.lastPathComponent var targetURL = documentsURL var parentPath = "" - let group1 = DispatchGroup() if toParentId != "" { targetURL = documentsURL.appendingPathComponent(toParentId, isDirectory: true) - group1.enter() - DispatchQueue.main.async { - defer { group1.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -246,43 +232,47 @@ public class LocalStorage: RemoteStorageBase { } } } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", toParentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } + } + } + } } - group1.enter() - DispatchQueue.main.async { - defer { group1.leave() } - - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest2 = NSFetchRequest(entityName: "RemoteData") fetchRequest2.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest2) { + if let result = try? backgroundContext.fetch(fetchRequest2) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } } targetURL = targetURL.appendingPathComponent(name) - - group1.notify(queue: .global()) { - do { - try FileManager.default.moveItem(at: fromURL, to: targetURL) - let group2 = DispatchGroup() - DispatchQueue.main.async { - self.storeItem(item: targetURL, parentFileId: toParentId, parentPath: parentPath, group: group2) - } - let id = self.getIdFromURL(url: targetURL) - group2.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() - DispatchQueue.global().async { - onFinish?(id) - } - } + do { + try FileManager.default.moveItem(at: fromURL, to: targetURL) + DispatchQueue.main.async { + self.storeItem(item: targetURL, parentFileId: toParentId, parentPath: parentPath, context: backgroundContext) } - catch { - onFinish?(nil) + let id = self.getIdFromURL(url: targetURL) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?(id) + } } } + catch { + onFinish?(nil) + } } override func deleteItem(fileId: String, callCount: Int = 0, onFinish: ((Bool) -> Void)?) { @@ -291,20 +281,20 @@ public class LocalStorage: RemoteStorageBase { do { try FileManager.default.removeItem(at: targetURL) - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.performAndWait { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } - self.deleteChildRecursive(parent: fileId) - - try? viewContext.save() + self.deleteChildRecursive(parent: fileId, context: backgroundContext) + } + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(true) } @@ -322,35 +312,28 @@ public class LocalStorage: RemoteStorageBase { do { try FileManager.default.moveItem(at: fromURL, to: newURL) - let group1 = DispatchGroup() - group1.enter() var parentPath: String? var parentId: String? - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest2 = NSFetchRequest(entityName: "RemoteData") fetchRequest2.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest2) as? [RemoteData] { + if let result = try? backgroundContext.fetch(fetchRequest2) as? [RemoteData] { for object in result { parentPath = object.path let component = parentPath?.components(separatedBy: "/") parentPath = component?.dropLast().joined(separator: "/") parentId = object.parent - viewContext.delete(object) + backgroundContext.delete(object) } } - group1.leave() } - group1.notify(queue: .global()) { - let group2 = DispatchGroup() - self.storeItem(item: newURL, parentFileId: parentId, parentPath: parentPath, group: group2) + self.storeItem(item: newURL, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() let id = self.getIdFromURL(url: newURL) - group2.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() - DispatchQueue.global().async { - onFinish?(id) - } + DispatchQueue.global().async { + onFinish?(id) } } } @@ -365,12 +348,11 @@ public class LocalStorage: RemoteStorageBase { do { try FileManager.default.setAttributes([FileAttributeKey.modificationDate: newdate], ofItemAtPath: targetURL.path) - let group2 = DispatchGroup() - self.storeItem(item: targetURL, group: group2) + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: targetURL, context: backgroundContext) let id = getIdFromURL(url: targetURL) - group2.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(id) } @@ -398,12 +380,8 @@ public class LocalStorage: RemoteStorageBase { newURL = newURL.appendingPathComponent(uploadname) var parentPath = "" - let group1 = DispatchGroup() if parentId != "" { - group1.enter() - DispatchQueue.main.async { - defer { group1.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -414,6 +392,19 @@ public class LocalStorage: RemoteStorageBase { } } } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } + } + } + } } do { @@ -426,16 +417,13 @@ public class LocalStorage: RemoteStorageBase { UploadManeger.shared.UploadProgress(identifier: sessionId, possition: Int(fileSize)) - let group2 = DispatchGroup() - group1.notify(queue: .global()) { - self.storeItem(item: newURL, parentFileId: parentId, parentPath: parentPath, group: group2) - let id = self.getIdFromURL(url: newURL) - group2.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() - DispatchQueue.global().async { - onFinish?(id) - } + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: newURL, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) + let id = self.getIdFromURL(url: newURL) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?(id) } } } diff --git a/RemoteCloud/RemoteCloud/Storages/OneDriveStorage.swift b/RemoteCloud/RemoteCloud/Storages/OneDriveStorage.swift index 5b6e883..e3ae6a1 100644 --- a/RemoteCloud/RemoteCloud/Storages/OneDriveStorage.swift +++ b/RemoteCloud/RemoteCloud/Storages/OneDriveStorage.swift @@ -17,7 +17,8 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession } var webAuthSession: ASWebAuthenticationSession? - + let uploadSemaphore = DispatchSemaphore(value: 5) + public convenience init(name: String) { self.init() service = CloudFactory.getServiceName(service: .OneDrive) @@ -279,7 +280,7 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession } } - func storeItem(item: [String: Any], parentFileId: String? = nil, parentPath: String? = nil, group: DispatchGroup?) { + func storeItem(item: [String: Any], parentFileId: String? = nil, parentPath: String? = nil, context: NSManagedObjectContext) { guard let id = item["id"] as? String else { return } @@ -301,16 +302,13 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession formatter.formatOptions.insert(.withFractionalSeconds) let formatter2 = ISO8601DateFormatter() - group?.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + context.perform { var prevParent: String? var prevPath: String? let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { if let item = object as? RemoteData { prevPath = item.path @@ -318,11 +316,11 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession prevPath = component?.dropLast().joined(separator: "/") prevParent = item.parent } - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = id newitem.name = name @@ -354,22 +352,19 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession newitem.path = "\(path)/\(name)" } } - group?.leave() } - } override func ListChildren(fileId: String, path: String, onFinish: (() -> Void)?) { listFiles(itemId: fileId, nextLink: "") { result in if let items = result { - let group = DispatchGroup() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() for item in items { - self.storeItem(item: item, parentFileId: fileId, parentPath: path, group: group) + self.storeItem(item: item, parentFileId: fileId, parentPath: path, context: backgroundContext) } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?() } @@ -506,6 +501,15 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession } override func readFile(fileId: String, start: Int64? = nil, length: Int64? = nil, callCount: Int = 0, onFinish: ((Data?) -> Void)?) { + + if let cache = CloudFactory.shared.cache.getCache(storage: storageName!, id: fileId, offset: start ?? 0, size: length ?? -1) { + if let data = try? Data(contentsOf: cache) { + os_log("%{public}@", log: log, type: .debug, "hit cache(OneDrive:\(storageName ?? "") \(fileId) \(start ?? -1) \(length ?? -1) \((start ?? 0) + (length ?? 0))") + onFinish?(data) + return + } + } + os_log("%{public}@", log: log, type: .debug, "readFile(onedrive:\(storageName ?? "") \(fileId) \(start ?? -1) \(length ?? -1) \((start ?? 0) + (length ?? 0))") getLink(fileId: fileId, start: start, length: length, onFinish: onFinish) @@ -574,24 +578,19 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession guard let id = json["id"] as? String else { throw RetryError.Retry } - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } - self.storeItem(item: json, parentFileId: parentId, parentPath: parentPath, group: group) - group.leave() } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + self.storeItem(item: json, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(id) } @@ -659,20 +658,19 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession throw RetryError.Failed } if httpResponse.statusCode == 204 { - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } - - self.deleteChildRecursive(parent: fileId) - - try? viewContext.save() + } + self.deleteChildRecursive(parent: fileId, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(true) } @@ -752,15 +750,10 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession print(e) throw RetryError.Retry } - let group = DispatchGroup() - group.enter() - DispatchQueue.global().async { - self.storeItem(item: json, parentFileId: parentId, parentPath: parentPath, group: group) - group.leave() - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: json, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(true) } @@ -960,11 +953,7 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession } else { var toParentPath = "" - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -975,7 +964,20 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession } } } - group.notify(queue: .global()) { + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", toParentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + toParentPath = items.first?.path ?? "" + } + } + } + } + DispatchQueue.global().sync { let json: [String: Any] = ["parentReference": ["id": toParentId]] self.updateItem(fileId: fileId, json: json, parentId: toParentId, parentPath: toParentPath, onFinish: onFinish) } @@ -1003,12 +1005,8 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession let len = targetStream.read(&buf, maxLength: buf.count) var parentPath = "\(storageName ?? ""):/" - let group = DispatchGroup() if parentId != "" { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -1019,110 +1017,128 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession } } } - } - group.notify(queue: .global()) { - self.checkToken() { success in - do { - guard success else { - throw RetryError.Failed - } - guard self.apiEndPoint != "" else { - throw RetryError.Failed - } - let attr = try FileManager.default.attributesOfItem(atPath: target.path) - let fileSize = attr[.size] as! UInt64 - - UploadManeger.shared.UploadFixSize(identifier: sessionId, size: Int(fileSize)) - - let path = (parentId == "") ? "root" : "items/\(parentId)" - var allowedCharacterSet = CharacterSet.alphanumerics - allowedCharacterSet.insert(charactersIn: "-._~") - let encoded_name = uploadname.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) - let url = "\(self.apiEndPoint)/\(path):/\(encoded_name!):/createUploadSession" - var request: URLRequest = URLRequest(url: URL(string: url)!) - request.httpMethod = "POST" - request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization") - request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type") - let json: [String: Any] = [ - "@microsoft.graph.conflictBehavior": "rename"] - let postData = try? JSONSerialization.data(withJSONObject: json) - request.httpBody = postData + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - let task = URLSession.shared.dataTask(with: request) {data, response, error in - do { - if let error = error { - print(error) - throw RetryError.Failed - } - guard let data = data else { - print(response!) - throw RetryError.Failed - } - print(String(data: data, encoding: .utf8) ?? "") - guard let object = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - throw RetryError.Failed - } - guard let uploadUrl = object["uploadUrl"] as? String else { - throw RetryError.Failed - } + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } + } + } + } + } + uploadSemaphore.wait() + self.checkToken() { success in + let onFinish: (String?)->Void = { id in + self.uploadSemaphore.signal() + onFinish?(id) + } + do { + guard success else { + throw RetryError.Failed + } + guard self.apiEndPoint != "" else { + throw RetryError.Failed + } + let attr = try FileManager.default.attributesOfItem(atPath: target.path) + let fileSize = attr[.size] as! UInt64 + + UploadManeger.shared.UploadFixSize(identifier: sessionId, size: Int(fileSize)) + + let path = (parentId == "") ? "root" : "items/\(parentId)" + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let encoded_name = uploadname.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) + let url = "\(self.apiEndPoint)/\(path):/\(encoded_name!):/createUploadSession" + var request: URLRequest = URLRequest(url: URL(string: url)!) + request.httpMethod = "POST" + request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization") + request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type") + let json: [String: Any] = [ + "@microsoft.graph.conflictBehavior": "rename"] + let postData = try? JSONSerialization.data(withJSONObject: json) + request.httpBody = postData + + let task = URLSession.shared.dataTask(with: request) {data, response, error in + do { + if let error = error { + print(error) + throw RetryError.Failed + } + guard let data = data else { + print(response!) + throw RetryError.Failed + } + print(String(data: data, encoding: .utf8) ?? "") + guard let object = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + throw RetryError.Failed + } + guard let uploadUrl = object["uploadUrl"] as? String else { + throw RetryError.Failed + } - var request: URLRequest = URLRequest(url: URL(string: uploadUrl)!) - request.httpMethod = "PUT" + var request: URLRequest = URLRequest(url: URL(string: uploadUrl)!) + request.httpMethod = "PUT" - let tmpurl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(Int.random(in: 0...0xffffffff))") - try? Data(bytes: buf, count: len).write(to: tmpurl) + let tmpurl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(Int.random(in: 0...0xffffffff))") + try? Data(bytes: buf, count: len).write(to: tmpurl) - request.setValue("bytes 0-\(len-1)/\(fileSize)", forHTTPHeaderField: "Content-Range") + request.setValue("bytes 0-\(len-1)/\(fileSize)", forHTTPHeaderField: "Content-Range") - #if !targetEnvironment(macCatalyst) - let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).\(self.storageName ?? "").\(Int.random(in: 0..<0xffffffff))") - //config.isDiscretionary = true - config.sessionSendsLaunchEvents = true - #else - let config = URLSessionConfiguration.default - #endif - let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) - self.sessions += [session] + let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).\(self.storageName ?? "").\(Int.random(in: 0..<0xffffffff))") + //config.isDiscretionary = true + config.sessionSendsLaunchEvents = true + let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) + self.sessions += [session] - let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier + let task = session.uploadTask(with: request, fromFile: tmpurl) + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + self.taskQueue.async { self.task_upload[taskid] = ["target": targetStream, "data": Data(), "upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "offset": len, "size": Int(fileSize), "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] self.onFinsh_upload[taskid] = onFinish task.resume() } - catch { - targetStream.close() - try? FileManager.default.removeItem(at: target) - onFinish?(nil) - } } - task.resume() - } - catch { - targetStream.close() - try? FileManager.default.removeItem(at: target) - onFinish?(nil) + catch { + targetStream.close() + try? FileManager.default.removeItem(at: target) + onFinish(nil) + } } + task.resume() + } + catch { + targetStream.close() + try? FileManager.default.removeItem(at: target) + onFinish(nil) } } } - var task_upload = [Int: [String: Any]]() - var onFinsh_upload = [Int: ((String?)->Void)?]() + var task_upload = [String: [String: Any]]() + var onFinsh_upload = [String: ((String?)->Void)?]() var sessions = [URLSession]() - + let taskQueue = DispatchQueue(label: "taskDict") + public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { CloudFactory.shared.urlSessionDidFinishCallback?(session) } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - if var taskdata = self.task_upload[dataTask.taskIdentifier] { + let taskid = (session.configuration.identifier ?? "") + ".\(dataTask.taskIdentifier)" + if var taskdata = taskQueue.sync(execute: { self.task_upload[taskid] }) { guard var recvData = taskdata["data"] as? Data else { return } recvData.append(data) taskdata["data"] = recvData - self.task_upload[dataTask.taskIdentifier] = taskdata + taskQueue.async { + self.task_upload[taskid] = taskdata + } } } @@ -1135,11 +1151,16 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - guard let taskdata = self.task_upload.removeValue(forKey: task.taskIdentifier) else { + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + guard let taskdata = taskQueue.sync(execute: { + self.task_upload.removeValue(forKey: taskid) + }) else { print(task.response ?? "") return } - guard let onFinish = self.onFinsh_upload.removeValue(forKey: task.taskIdentifier) else { + guard let onFinish = taskQueue.sync(execute: { + self.onFinsh_upload.removeValue(forKey: taskid) + }) else { print("onFinish not found") return } @@ -1195,10 +1216,12 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession request.httpMethod = "GET" let task = session.downloadTask(with: request) - let taskid = task.taskIdentifier - self.task_upload[taskid] = ["upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "size": fileSize, "orgtarget": target, "session": sessionId] - self.onFinsh_upload[taskid] = onFinish - task.resume() + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + taskQueue.async { + self.task_upload[taskid] = ["upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "size": fileSize, "orgtarget": target, "session": sessionId] + self.onFinsh_upload[taskid] = onFinish + task.resume() + } return } if let nextExpectedRanges = object["nextExpectedRanges"] as? [String] { @@ -1215,10 +1238,12 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession request.setValue("bytes \(offset)-\(offset+len-1)/\(fileSize)", forHTTPHeaderField: "Content-Range") let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier - self.task_upload[taskid] = ["target": targetStream, "data": Data(), "upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "offset": offset+len, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] - self.onFinsh_upload[taskid] = onFinish - task.resume() + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + taskQueue.async { + self.task_upload[taskid] = ["target": targetStream, "data": Data(), "upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "offset": offset+len, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] + self.onFinsh_upload[taskid] = onFinish + task.resume() + } } else { guard let id = object["id"] as? String else { @@ -1229,15 +1254,10 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession targetStream.close() try? FileManager.default.removeItem(at: target) } - let group = DispatchGroup() - group.enter() - DispatchQueue.global().async { - self.storeItem(item: object, parentFileId: parentId, parentPath: parentPath, group: group) - group.leave() - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: object, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() print("done") self.sessions.removeAll(where: { $0.configuration.identifier == session.configuration.identifier }) @@ -1255,10 +1275,15 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { - guard let taskdata = self.task_upload.removeValue(forKey: downloadTask.taskIdentifier) else { + let taskid = (session.configuration.identifier ?? "") + ".\(downloadTask.taskIdentifier)" + guard let taskdata = taskQueue.sync(execute: { + self.task_upload.removeValue(forKey: taskid) + }) else { return } - guard let onFinish = self.onFinsh_upload.removeValue(forKey: downloadTask.taskIdentifier) else { + guard let onFinish = taskQueue.sync(execute: { + self.onFinsh_upload.removeValue(forKey: taskid) + }) else { print("onFinish not found") return } @@ -1321,10 +1346,12 @@ public class OneDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSession request.setValue("bytes \(offset)-\(offset+len-1)/\(fileSize)", forHTTPHeaderField: "Content-Range") let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier - self.task_upload[taskid] = ["target": targetStream, "data": Data(), "upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "offset": offset+len, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] - self.onFinsh_upload[taskid] = onFinish - task.resume() + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + taskQueue.async { + self.task_upload[taskid] = ["target": targetStream, "data": Data(), "upload": uploadUrl, "parentId": parentId, "parentPath": parentPath, "offset": offset+len, "size": fileSize, "tmpfile": tmpurl, "orgtarget": target, "session": sessionId] + self.onFinsh_upload[taskid] = onFinish + task.resume() + } } catch { onFinish?(nil) diff --git a/RemoteCloud/RemoteCloud/Storages/WebDAVStorage.swift b/RemoteCloud/RemoteCloud/Storages/WebDAVStorage.swift new file mode 100644 index 0000000..71d2765 --- /dev/null +++ b/RemoteCloud/RemoteCloud/Storages/WebDAVStorage.swift @@ -0,0 +1,2263 @@ +// +// WebDAVStorage.swift +// RemoteCloud +// +// Created by rei8 on 2019/11/22. +// Copyright © 2019 lithium03. All rights reserved. +// + +import Foundation +import os.log +import CoreData + +class ViewControllerWebDAV: UIViewController, UITextFieldDelegate, URLSessionTaskDelegate, URLSessionDataDelegate { + var textURI: UITextField! + var textUser: UITextField! + var textPass: UITextField! + var stackView: UIStackView! + + var onCancel: (()->Void)! + var onFinish: ((String, String, String)->Void)! + var done: Bool = false + + let activityIndicatorView = UIActivityIndicatorView() + var uri = "" + var user = "" + var pass = "" + var testState = 0 + + override func viewDidLoad() { + super.viewDidLoad() + + self.title = "WebDAV" + if #available(iOS 13.0, *) { + view.backgroundColor = .systemBackground + } else { + view.backgroundColor = .white + } + + stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = 10 + view.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + + let stackView1 = UIStackView() + stackView1.axis = .horizontal + stackView1.alignment = .center + stackView1.spacing = 20 + stackView.addArrangedSubview(stackView1) + + let label1 = UILabel() + label1.text = "URL" + stackView1.addArrangedSubview(label1) + + textURI = UITextField() + textURI.borderStyle = .roundedRect + textURI.keyboardType = .URL + textURI.textContentType = .URL + textURI.enablesReturnKeyAutomatically = true + textURI.delegate = self + textURI.clearButtonMode = .whileEditing + textURI.returnKeyType = .done + textURI.placeholder = "https://localhost/webdav/" + stackView1.addArrangedSubview(textURI) + let widthConstraint = textURI.widthAnchor.constraint(equalToConstant: 300) + widthConstraint.priority = .defaultHigh + widthConstraint.isActive = true + + let stackView2 = UIStackView() + stackView2.axis = .horizontal + stackView2.alignment = .center + stackView2.spacing = 20 + stackView.addArrangedSubview(stackView2) + + let label2 = UILabel() + label2.text = "Username" + stackView2.addArrangedSubview(label2) + + textUser = UITextField() + textUser.borderStyle = .roundedRect + textUser.delegate = self + textUser.clearButtonMode = .whileEditing + textUser.textContentType = .username + textUser.placeholder = "(option)" + stackView2.addArrangedSubview(textUser) + let widthConstraint2 = textUser.widthAnchor.constraint(equalToConstant: 200) + widthConstraint2.priority = .defaultHigh + widthConstraint2.isActive = true + + let stackView3 = UIStackView() + stackView3.axis = .horizontal + stackView3.alignment = .center + stackView3.spacing = 20 + stackView.addArrangedSubview(stackView3) + + let label3 = UILabel() + label3.text = "Password" + stackView3.addArrangedSubview(label3) + + textPass = UITextField() + textPass.borderStyle = .roundedRect + textPass.delegate = self + textPass.clearButtonMode = .whileEditing + textPass.isSecureTextEntry = true + textPass.textContentType = .password + textPass.placeholder = "(option)" + stackView3.addArrangedSubview(textPass) + let widthConstraint3 = textPass.widthAnchor.constraint(equalToConstant: 200) + widthConstraint3.priority = .defaultHigh + widthConstraint3.isActive = true + + + let stackView4 = UIStackView() + stackView4.axis = .horizontal + stackView4.alignment = .center + stackView4.spacing = 20 + stackView.addArrangedSubview(stackView4) + + let button1 = UIButton(type: .system) + button1.setTitle("Done", for: .normal) + button1.addTarget(self, action: #selector(buttonEvent), for: .touchUpInside) + stackView4.addArrangedSubview(button1) + + let button2 = UIButton(type: .system) + button2.setTitle("Cancel", for: .normal) + button2.addTarget(self, action: #selector(buttonEvent), for: .touchUpInside) + stackView4.addArrangedSubview(button2) + + activityIndicatorView.center = view.center + if #available(iOS 13.0, *) { + activityIndicatorView.style = .large + } else { + // Fallback on earlier versions + activityIndicatorView.style = .whiteLarge + } + activityIndicatorView.hidesWhenStopped = true + view.addSubview(activityIndicatorView) + } + + @objc func buttonEvent(_ sender: UIButton) { + if sender.currentTitle == "Done" { + textURI.resignFirstResponder() + if textURI.text == "" { + return + } + if activityIndicatorView.isAnimating { + return + } + activityIndicatorView.startAnimating() + uri = textURI.text ?? "" + user = textUser.text ?? "" + pass = textPass.text ?? "" + checkServer() + } + else { + navigationController?.popViewController(animated: true) + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil && !done { + onCancel() + } + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + + if textField == textURI { + if activityIndicatorView.isAnimating { + return true + } + activityIndicatorView.startAnimating() + uri = textURI.text ?? "" + user = textUser.text ?? "" + pass = textPass.text ?? "" + checkServer() + } + return true + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + if textURI.isFirstResponder { + textURI.resignFirstResponder() + } + if textUser.isFirstResponder { + textUser.resignFirstResponder() + } + if textPass.isFirstResponder { + textPass.resignFirstResponder() + } + } + + func checkServer() { + guard let url = URL(string: uri) else { + activityIndicatorView.stopAnimating() + return + } + testState = 0 + var request: URLRequest = URLRequest(url: url) + request.httpMethod = "OPTIONS" + let task = dataSession.dataTask(with: request) + + task.resume() + } + + lazy var dataSession: URLSession = { + let configuration = URLSessionConfiguration.default + + return URLSession(configuration: configuration, + delegate: self, + delegateQueue: nil) + }() + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + + guard let response = response as? HTTPURLResponse else { + DispatchQueue.main.async { + self.activityIndicatorView.stopAnimating() + } + return + } + guard response.statusCode == 200 else { + print(response) + DispatchQueue.main.async { + self.activityIndicatorView.stopAnimating() + } + return + } + if testState == 0 { + guard let allow = response.allHeaderFields["Allow"] as? String ?? response.allHeaderFields["allow"] as? String else { + print(response) + DispatchQueue.main.async { + self.activityIndicatorView.stopAnimating() + } + return + } + guard allow.lowercased().contains("propfind") else { + print(allow) + DispatchQueue.main.async { + self.activityIndicatorView.stopAnimating() + } + return + } + guard let dav = response.allHeaderFields["Dav"] as? String ?? response.allHeaderFields["dav"] as? String ?? + response.allHeaderFields["DAV"] as? String else { + print(response) + DispatchQueue.main.async { + self.activityIndicatorView.stopAnimating() + } + return + } + guard dav.contains("1") else { + print(dav) + DispatchQueue.main.async { + self.activityIndicatorView.stopAnimating() + } + return + } + + guard let url = URL(string: uri) else { + DispatchQueue.main.async { + self.activityIndicatorView.stopAnimating() + } + return + } + var request: URLRequest = URLRequest(url: url) + request.httpMethod = "HEAD" + let task = dataSession.dataTask(with: request) + testState = 1 + task.resume() + } + else if testState == 1 { + done = true + onFinish(uri, user, pass) + } + completionHandler(.allow) + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let error = error { + print(error) + DispatchQueue.main.async { + self.activityIndicatorView.stopAnimating() + } + return + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + let authMethod = challenge.protectionSpace.authenticationMethod + guard authMethod == NSURLAuthenticationMethodHTTPBasic || authMethod == NSURLAuthenticationMethodHTTPDigest else { + completionHandler(.performDefaultHandling, nil) + return + } + + guard challenge.previousFailureCount < 3 else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + let credential = URLCredential(user: user, password: pass, persistence: .forSession) + completionHandler(.useCredential, credential) + } +} + +public class WebDAVStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDataDelegate { + + public override func getStorageType() -> CloudStorages { + return .WebDAV + } + + public override func cancel() { + if let acceptRange = acceptRange, !acceptRange { + cancelTime = Date(timeIntervalSinceNow: 10) + } + else { + super.cancel() + } + } + + var cache_accessUsername = "" + var accessUsername: String { + if let name = storageName { + if let user = getKeyChain(key: "\(name)_accessUsername") { + cache_accessUsername = user + } + return cache_accessUsername + } + else { + return "" + } + } + + var cache_accessPassword = "" + var accessPassword: String { + if let name = storageName { + if let pass = getKeyChain(key: "\(name)_accessPassword") { + cache_accessPassword = pass + } + return cache_accessPassword + } + else { + return "" + } + } + + var cache_aaccessURI = "" + var accessURI: String { + if let name = storageName { + if let uri = getKeyChain(key: "\(name)_accessURI") { + cache_aaccessURI = uri + } + return cache_aaccessURI + } + else { + return "" + } + } + + var acceptRange: Bool? + let checkSemaphore = DispatchSemaphore(value: 1) + let uploadSemaphore = DispatchSemaphore(value: 5) + + lazy var dataSession: URLSession = { + let configuration = URLSessionConfiguration.default + + return URLSession(configuration: configuration, + delegate: self, + delegateQueue: nil) + }() + + var dataTasks: [Int: (Data?, Error?)->Void] = [:] + var recvData: [Int: Data] = [:] + var headerHandler: [Int: (URLResponse)->URLSession.ResponseDisposition] = [:] + + var wholeReading: [URL] = [] + + public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + let authMethod = challenge.protectionSpace.authenticationMethod + guard authMethod == NSURLAuthenticationMethodHTTPBasic || authMethod == NSURLAuthenticationMethodHTTPDigest else { + completionHandler(.performDefaultHandling, nil) + return + } + + guard challenge.previousFailureCount < 3 else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + let credential = URLCredential(user: accessUsername, password: accessPassword, persistence: .forSession) + completionHandler(.useCredential, credential) + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + if let taskdata = taskQueue.sync(execute: { + self.task_upload.removeValue(forKey: taskid) + }) { + // upload is done + guard let onFinish = taskQueue.sync(execute: { + self.onFinsh_upload.removeValue(forKey: taskid) + }) else { + print("onFinish not found") + return + } + if let target = taskdata["target"] as? URL { + try? FileManager.default.removeItem(at: target) + } + do { + guard let url = taskdata["url"] as? String else { + print(task.response ?? "") + throw RetryError.Failed + } + if let error = error { + print(error) + throw RetryError.Failed + } + guard let httpResponse = task.response as? HTTPURLResponse else { + print(task.response ?? "") + throw RetryError.Failed + } + guard httpResponse.statusCode == 201 else { + print(httpResponse) + throw RetryError.Failed + } + //print(httpResponse) + onFinish?(url) + } + catch { + onFinish?(nil) + } + } + else { + //print(task.taskIdentifier, "didCompleteWithError") + if let error = error { + print(error) + let process = dataTasks[task.taskIdentifier] + process?(nil, error) + } + else if let data = recvData[task.taskIdentifier] { + let process = dataTasks[task.taskIdentifier] + process?(data, nil) + } + dataTasks.removeValue(forKey: task.taskIdentifier) + recvData.removeValue(forKey: task.taskIdentifier) + } + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + //print(dataTask.taskIdentifier, "didReceive response") + if let handler = headerHandler[dataTask.taskIdentifier] { + completionHandler(handler(response)) + } + else { + completionHandler(.allow) + } + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + //print(dataTask.taskIdentifier, "didReceive data") + let taskid = (session.configuration.identifier ?? "") + ".\(dataTask.taskIdentifier)" + if var taskdata = taskQueue.sync(execute: { self.task_upload[taskid] }) { + guard var recvData = taskdata["data"] as? Data else { + return + } + recvData.append(data) + taskdata["data"] = recvData + taskQueue.async { + self.task_upload[taskid] = taskdata + } + } + else { + if var prev = recvData[dataTask.taskIdentifier] { + prev.append(data) + recvData[dataTask.taskIdentifier] = prev + } + else { + recvData[dataTask.taskIdentifier] = data + } + } + } + + public convenience init(name: String) { + self.init() + service = CloudFactory.getServiceName(service: .WebDAV) + storageName = name + } + + public override func auth(onFinish: ((Bool) -> Void)?) -> Void { + DispatchQueue.main.async { + self.authorize(onFinish: onFinish) + } + } + + override func authorize(onFinish: ((Bool) -> Void)?) { + os_log("%{public}@", log: log, type: .debug, "authorize(google:\(storageName ?? ""))") + + DispatchQueue.main.async { + if let controller = UIApplication.topViewController() { + let inputView = ViewControllerWebDAV() + inputView.onCancel = { + onFinish?(false) + } + inputView.onFinish = { uri, user, pass in + let _ = self.setKeyChain(key: "\(self.storageName ?? "")_accessURI", value: uri) + let _ = self.setKeyChain(key: "\(self.storageName ?? "")_accessUsername", value: user) + let _ = self.setKeyChain(key: "\(self.storageName ?? "")_accessPassword", value: pass) + + DispatchQueue.global().async { + onFinish?(true) + } + } + controller.navigationController?.pushViewController(inputView, animated: true) + } + else { + onFinish?(false) + } + + } + } + + public override func logout() { + if let name = storageName { + let _ = delKeyChain(key: "\(name)_accessURI") + let _ = delKeyChain(key: "\(name)_accessUsername") + let _ = delKeyChain(key: "\(name)_accessPassword") + } + super.logout() + } + + func storeItem(item: [String: Any], parentFileId: String? = nil, parentPath: String? = nil, context: NSManagedObjectContext) { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" + let formatter2 = ISO8601DateFormatter() + + guard let id = item["href"] as? String else { + return + } + if id.removingPercentEncoding == parentFileId?.removingPercentEncoding { + return + } + if let idURL = URL(string: id), let aurl = URL(string: accessURI), idURL.path == aurl.path { + return + } + guard let propstat = item["propstat"] as? [String: Any] else { + return + } + guard let prop = propstat["prop"] as? [String: String] else { + return + } + let name: String + if let dispname = prop["displayname"] { + name = dispname + } + else { + guard let idURL = URL(string: id) else { + return + } + guard let orgname = idURL.lastPathComponent.removingPercentEncoding else { + return + } + name = orgname + } + let ctime = prop["creationdate"] ?? prop["Win32CreationTime"] + let mtime = prop["lastmodified"] ?? prop["getlastmodified"] ?? prop["Win32LastModifiedTime"] + let size = Int64(prop["getcontentlength"] ?? "0") + let folder = prop["resourcetype"] == "collection" + + context.perform { + var prevParent: String? + var prevPath: String? + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") + if let result = try? context.fetch(fetchRequest) { + for object in result { + if let item = object as? RemoteData { + prevPath = item.path + let component = prevPath?.components(separatedBy: "/") + prevPath = component?.dropLast().joined(separator: "/") + prevParent = item.parent + } + context.delete(object as! NSManagedObject) + } + } + + let newitem = RemoteData(context: context) + newitem.storage = self.storageName + newitem.id = id + newitem.name = name + let comp = name.components(separatedBy: ".") + if comp.count >= 1 { + newitem.ext = comp.last!.lowercased() + } + newitem.cdate = formatter.date(from: ctime ?? "") ?? formatter2.date(from: ctime ?? "") + newitem.mdate = formatter.date(from: mtime ?? "") ?? formatter2.date(from: mtime ?? "") + newitem.folder = folder + newitem.size = size ?? 0 + newitem.hashstr = "" + newitem.parent = (parentFileId == nil) ? prevParent : parentFileId + if parentFileId == "" { + newitem.path = "\(self.storageName ?? ""):/\(name)" + } + else { + if let path = (parentPath == nil) ? prevPath : parentPath { + newitem.path = "\(path)/\(name)" + } + } + } + } + + class DAVcollectionParser: NSObject, XMLParserDelegate { + var onFinish: (([[String:Any]]?)->Void)? + + var response: [[String: Any]] = [] + var curElement: [String] = [] + var curProp: [String: Any] = [:] + var prop: [String: String] = [:] + + func parserDidStartDocument(_ parser: XMLParser) { + print("parser Start") + } + + func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { + switch elementName { + case let str where str.hasSuffix(":multistatus"): + print("start") + case let str where str.hasSuffix(":response"): + response.append([:]) + case let str where str.hasSuffix(":propstat"): + curProp = [:] + case let str where str.hasSuffix(":prop"): + prop = [:] + case let str where str.hasSuffix(":resourcetype"): + prop["resourcetype"] = "" + case let str where str.hasSuffix(":collection"): + prop["resourcetype"] = "collection" + default: + break + } + curElement.append(elementName) + } + + func parser(_ parser: XMLParser, foundCharacters string: String) { + //print(string) + switch curElement.last { + case let str where str?.hasSuffix(":href") ?? false: + response[response.count-1]["href"] = (response[response.count-1]["href"] as? String ?? "") + string + case let str where str?.hasSuffix(":status") ?? false: + curProp["status"] = string + case let str where str?.hasSuffix(":getlastmodified") ?? false: + prop["getlastmodified"] = string + case let str where str?.hasSuffix(":lastmodified") ?? false: + prop["lastmodified"] = string + case let str where str?.hasSuffix(":displayname") ?? false: + prop["displayname"] = (prop["displayname"] ?? "") + string + case let str where str?.hasSuffix(":getcontentlength") ?? false: + prop["getcontentlength"] = string + case let str where str?.hasSuffix(":creationdate") ?? false: + prop["creationdate"] = string + case let str where str?.hasSuffix(":Win32CreationTime") ?? false: + prop["Win32CreationTime"] = string + case let str where str?.hasSuffix(":Win32LastModifiedTime") ?? false: + prop["Win32LastModifiedTime"] = string + default: + break + } + } + + func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { + switch elementName { + case let str where str.hasSuffix(":multistatus"): + print("end") + case let str where str.hasSuffix(":propstat"): + response[response.count-1]["propstat"] = curProp + case let str where str.hasSuffix(":prop"): + curProp["prop"] = prop + default: + break + } + curElement = curElement.dropLast() + } + + func parserDidEndDocument(_ parser: XMLParser) { + print("parser End") + onFinish?(response) + } + + func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { + print(parseError.localizedDescription) + onFinish?(nil) + } + } + + func listFolder(path: String, callCount: Int = 0, onFinish: (([[String:Any]]?)->Void)?) { + if lastCall.timeIntervalSinceNow > -self.callWait || callSemaphore.wait(wallTimeout: .now()+5) == .timedOut { + if cancelTime.timeIntervalSinceNow > 0 { + cancelTime = Date(timeIntervalSinceNow: 0.5) + onFinish?(nil) + return + } + DispatchQueue.global().asyncAfter(deadline: .now()+self.callWait) { + self.listFolder(path: path, callCount: callCount+1, onFinish: onFinish) + } + return + } + os_log("%{public}@", log: log, type: .debug, "listFolder(WebDAV:\(storageName ?? ""))") + lastCall = Date() + var request: URLRequest + guard var url = URL(string: accessURI) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if path != "" { + guard let pathURL = URL(string: path) else { + return + } + if pathURL.host != nil { + url = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: url) else { + return + } + url = u + } + } + //print(url) + request = URLRequest(url: url) + + request.httpMethod = "PROPFIND" + request.setValue("1", forHTTPHeaderField: "Depth") + request.setValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type") + + let reqStr = [ + "", + "", + "", + "", + ].joined(separator: "\r\n")+"\r\n" + request.httpBody = reqStr.data(using: .utf8) + + let task = dataSession.dataTask(with: request) + dataTasks[task.taskIdentifier] = { data, error in + self.callSemaphore.signal() + do { + guard let data = data else { + throw RetryError.Retry + } + let parser: XMLParser? = XMLParser(data: data) + let dav = DAVcollectionParser() + dav.onFinish = onFinish + parser?.delegate = dav + parser?.parse() + //print(String(data: data, encoding: .utf8)!) + } + catch RetryError.Retry { + if callCount < 10 { + DispatchQueue.global().asyncAfter(deadline: .now()+self.callWait) { + self.listFolder(path: path, callCount: callCount+1, onFinish: onFinish) + } + return + } + onFinish?(nil) + return + } catch let e { + print(e) + onFinish?(nil) + return + } + } + task.resume() + } + + override func ListChildren(fileId: String, path: String, onFinish: (() -> Void)?) { + listFolder(path: fileId) { result in + if let items = result { + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + for item in items { + self.storeItem(item: item, parentFileId: fileId, parentPath: path, context: backgroundContext) + } + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?() + } + } + } + else { + DispatchQueue.global().async { + onFinish?() + } + } + } + } + + func checkAcceptRange(fileId: String, onFinish: @escaping ()->Void) { + + if acceptRange != nil { + onFinish() + return + } + + if checkSemaphore.wait(wallTimeout: .now()+5) == .timedOut { + DispatchQueue.global().asyncAfter(deadline: .now()+callWait+Double.random(in: 0..<0.5)) { + self.checkAcceptRange(fileId: fileId, onFinish: onFinish) + } + return + } + + os_log("%{public}@", log: log, type: .debug, "checkAcceptRange(WebDAV:\(storageName ?? "") \(fileId)") + + var request: URLRequest + guard var url = URL(string: accessURI) else { + self.checkSemaphore.signal() + onFinish() + return + } + if fileId != "" { + guard let pathURL = URL(string: fileId) else { + self.checkSemaphore.signal() + onFinish() + return + } + if pathURL.host != nil { + url = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: url) else { + self.checkSemaphore.signal() + onFinish() + return + } + url = u + } + } + //print(url) + request = URLRequest(url: url) + request.httpMethod = "HEAD" + let task = dataSession.dataTask(with: request) + headerHandler[task.taskIdentifier] = { response in + defer { + DispatchQueue.global().async { + onFinish() + } + } + + guard let response = response as? HTTPURLResponse else { + return .allow + } + guard response.statusCode == 200 else { + print(response) + return .allow + } + + guard let accept = response.allHeaderFields["Accept-Ranges"] as? String ?? response.allHeaderFields["accept-ranges"] as? String else { + print(response) + return .allow + } + if accept.lowercased().contains("bytes") { + self.acceptRange = true + } + else { + self.acceptRange = false + } + return .allow + } + dataTasks[task.taskIdentifier] = { data, error in + self.checkSemaphore.signal() + } + task.resume() + } + + func readRangeRead(fileId: String, start: Int64? = nil, length: Int64? = nil, callCount: Int = 0, onFinish: ((Data?) -> Void)?) { + + if let cache = CloudFactory.shared.cache.getCache(storage: storageName!, id: fileId, offset: start ?? 0, size: length ?? -1) { + if let data = try? Data(contentsOf: cache) { + os_log("%{public}@", log: log, type: .debug, "hit cache(WebDAV:\(storageName ?? "") \(fileId) \(start ?? -1) \(length ?? -1) \((start ?? 0) + (length ?? 0))") + onFinish?(data) + return + } + } + + if lastCall.timeIntervalSinceNow > -callWait || callSemaphore.wait(wallTimeout: .now()+5) == .timedOut { + if cancelTime.timeIntervalSinceNow > 0 { + cancelTime = Date(timeIntervalSinceNow: 0.5) + onFinish?(nil) + return + } + DispatchQueue.global().asyncAfter(deadline: .now()+callWait+Double.random(in: 0..<0.5)) { + self.readRangeRead(fileId: fileId, start: start, length: length, callCount: callCount+1, onFinish: onFinish) + } + return + } + lastCall = Date() + os_log("%{public}@", log: log, type: .debug, "readFile(WebDAV:\(storageName ?? "") \(fileId) \(start ?? -1) \(length ?? -1) \((start ?? 0) + (length ?? 0))") + + var request: URLRequest + guard var url = URL(string: accessURI) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if fileId != "" { + guard let pathURL = URL(string: fileId) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if pathURL.host != nil { + url = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: url) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + url = u + } + } + //print(url) + request = URLRequest(url: url) + if start != nil || length != nil { + let s = start ?? 0 + if length == nil { + request.setValue("bytes=\(s)-", forHTTPHeaderField: "Range") + } + else { + request.setValue("bytes=\(s)-\(s+length!-1)", forHTTPHeaderField: "Range") + } + } + + let task = dataSession.dataTask(with: request) + dataTasks[task.taskIdentifier] = { data, error in + self.callSemaphore.signal() + var waittime = self.callWait + if let error = error { + print(error) + if (error as NSError).code == -1009 { + waittime += 30 + } + } + if let l = length { + if data?.count ?? 0 != l { + if callCount > 50 { + onFinish?(data) + return + } + DispatchQueue.global().asyncAfter(deadline: .now()+self.callWait+Double.random(in: 0.. Void)?) { + + if let data = CloudFactory.shared.cache.getPartialFile(storage: storageName!, id: fileId, offset: start ?? 0, size: length ?? -1) { + os_log("%{public}@", log: log, type: .debug, "hit cache(WebDAV:\(storageName ?? "") \(fileId) \(start ?? -1) \(length ?? -1) \((start ?? 0) + (length ?? 0))") + onFinish?(data) + return + } + + guard var url = URL(string: accessURI) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if fileId != "" { + guard let pathURL = URL(string: fileId) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if pathURL.host != nil { + url = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: url) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + url = u + } + } + //print(url) + + if wholeReading.contains(url) { + DispatchQueue.global().asyncAfter(deadline: .now()+Double.random(in: 1..<5)) { + if self.cancelTime.timeIntervalSinceNow > 0 { + self.cancelTime = Date(timeIntervalSinceNow: 0.5) + onFinish?(nil) + return + } + self.readWholeRead(fileId: fileId, start: start, length: length, callCount: callCount+1, onFinish: onFinish) + } + return + } + + if lastCall.timeIntervalSinceNow > -callWait || callSemaphore.wait(wallTimeout: .now()+5) == .timedOut { + if cancelTime.timeIntervalSinceNow > 0 { + cancelTime = Date(timeIntervalSinceNow: 0.5) + onFinish?(nil) + return + } + DispatchQueue.global().asyncAfter(deadline: .now()+callWait+Double.random(in: 0..<0.5)) { + self.readWholeRead(fileId: fileId, start: start, length: length, callCount: callCount+1, onFinish: onFinish) + } + return + } + lastCall = Date() + wholeReading += [url] + + var request: URLRequest + request = URLRequest(url: url) + + os_log("%{public}@", log: log, type: .debug, "readFile(WebDAV:\(storageName ?? "") \(fileId) whole read") + + let task = dataSession.dataTask(with: request) + let timer1 = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { t in + if self.cancelTime.timeIntervalSinceNow > 0 { + print("cancel") + self.cancelTime = Date(timeIntervalSinceNow: 1) + onFinish?(nil) + task.cancel() + return + } + } + dataTasks[task.taskIdentifier] = { data, error in + self.callSemaphore.signal() + timer1.invalidate() + var waittime = self.callWait + if let error = error { + print(error) + if (error as NSError).code == -1009 { + waittime += 30 + } + } + if let d = data { + CloudFactory.shared.cache.saveFile(storage: self.storageName!, id: fileId, data: d) + let s = Int(start ?? 0) + if let len = length, s+Int(len) < d.count { + onFinish?(d.subdata(in: s..<(s+Int(len)))) + } + else { + onFinish?(d.subdata(in: s.. Void)?) { + + if let acceptRange = acceptRange { + if acceptRange { + readRangeRead(fileId: fileId, start: start, length: length, callCount: callCount, onFinish: onFinish) + } + else { + readWholeRead(fileId: fileId, start: start, length: length, callCount: callCount, onFinish: onFinish) + } + } + else { + checkAcceptRange(fileId: fileId) { + self.readFile(fileId: fileId, start: start, length: length, callCount: callCount, onFinish: onFinish) + } + } + } + + public override func getRaw(fileId: String) -> RemoteItem? { + return NetworkRemoteItem(storage: storageName ?? "", id: fileId) + } + + public override func getRaw(path: String) -> RemoteItem? { + return NetworkRemoteItem(path: path) + } + + public override func makeFolder(parentId: String, parentPath: String, newname: String, callCount: Int = 0, onFinish: ((String?) -> Void)?) { + if lastCall.timeIntervalSinceNow > -callWait || callSemaphore.wait(wallTimeout: .now()+5) == .timedOut { + if cancelTime.timeIntervalSinceNow > 0 { + cancelTime = Date(timeIntervalSinceNow: 0.5) + onFinish?(nil) + return + } + DispatchQueue.global().asyncAfter(deadline: .now()+callWait) { + self.makeFolder(parentId: parentId, parentPath: parentPath, newname: newname, callCount: callCount+1, onFinish: onFinish) + } + return + } + os_log("%{public}@", log: log, type: .debug, "makeFolder(WebDAV:\(storageName ?? "") \(parentId) \(newname)") + lastCall = Date() + + guard var url = URL(string: accessURI) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if parentId != "" { + guard let pathURL = URL(string: parentId) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if pathURL.host != nil { + url = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: url) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + url = u + } + } + url.appendPathComponent(newname, isDirectory: true) + //print(url) + + var request = URLRequest(url: url) + request.httpMethod = "MKCOL" + + var request2 = URLRequest(url: url) + request2.httpMethod = "PROPFIND" + request2.setValue("0", forHTTPHeaderField: "Depth") + request2.setValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type") + + let reqStr = [ + "", + "", + "", + "", + ].joined(separator: "\r\n")+"\r\n" + request2.httpBody = reqStr.data(using: .utf8) + + let task = dataSession.dataTask(with: request) + let task2 = dataSession.dataTask(with: request2) + headerHandler[task.taskIdentifier] = { response in + guard let response = response as? HTTPURLResponse else { + return .cancel + } + guard response.statusCode == 201 else { + print(response) + return .cancel + } + task2.resume() + return .allow + } + dataTasks[task.taskIdentifier] = { data, error in + self.callSemaphore.signal() + do { + if let error = error { + print(error) + throw RetryError.Retry + } + } + catch RetryError.Retry { + if callCount < 10 { + DispatchQueue.global().asyncAfter(deadline: .now()+self.callWait) { + self.makeFolder(parentId: parentId, parentPath: parentPath, newname: newname, callCount: callCount+1, onFinish: onFinish) + } + return + } + onFinish?(nil) + return + } catch let e { + print(e) + onFinish?(nil) + return + } + } + dataTasks[task2.taskIdentifier] = { data, error in + do { + guard let data = data else { + throw RetryError.Retry + } + let parser: XMLParser? = XMLParser(data: data) + let dav = DAVcollectionParser() + dav.onFinish = { result in + guard let result = result else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + if let item = result.first, let id = item["href"] as? String { + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: item, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?(id) + } + } + } + else { + DispatchQueue.global().async { + onFinish?(nil) + } + } + } + parser?.delegate = dav + parser?.parse() + //print(String(data: data, encoding: .utf8)!) + } + catch RetryError.Retry { + onFinish?(nil) + return + } catch let e { + print(e) + onFinish?(nil) + return + } + } + task.resume() + } + + override func deleteItem(fileId: String, callCount: Int = 0, onFinish: ((Bool) -> Void)?) { + if lastCall.timeIntervalSinceNow > -callWait || callSemaphore.wait(wallTimeout: .now()+5) == .timedOut { + if cancelTime.timeIntervalSinceNow > 0 { + cancelTime = Date(timeIntervalSinceNow: 0.5) + onFinish?(false) + return + } + DispatchQueue.global().asyncAfter(deadline: .now()+callWait) { + self.deleteItem(fileId: fileId, callCount: callCount+1, onFinish: onFinish) + } + return + } + os_log("%{public}@", log: log, type: .debug, "deleteItem(WebDAV:\(storageName ?? "") \(fileId)") + lastCall = Date() + + guard var url = URL(string: accessURI) else { + self.callSemaphore.signal() + onFinish?(false) + return + } + if fileId != "" { + guard let pathURL = URL(string: fileId) else { + self.callSemaphore.signal() + onFinish?(false) + return + } + if pathURL.host != nil { + url = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: url) else { + self.callSemaphore.signal() + onFinish?(false) + return + } + url = u + } + } + //print(url) + + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + + let task = dataSession.dataTask(with: request) + headerHandler[task.taskIdentifier] = { response in + self.callSemaphore.signal() + guard let response = response as? HTTPURLResponse else { + return .cancel + } + guard response.statusCode == 204 || response.statusCode == 404 else { + print(response) + return .cancel + } + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") + if let result = try? backgroundContext.fetch(fetchRequest) { + for object in result { + backgroundContext.delete(object as! NSManagedObject) + } + } + } + self.deleteChildRecursive(parent: fileId, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?(true) + } + } + return .allow + } + dataTasks[task.taskIdentifier] = { data, error in + do { + if let error = error { + print(error) + throw RetryError.Retry + } + } + catch RetryError.Retry { + if callCount < 10 { + DispatchQueue.global().asyncAfter(deadline: .now()+self.callWait) { + self.deleteItem(fileId: fileId, callCount: callCount+1, onFinish: onFinish) + } + return + } + onFinish?(false) + } + catch let e { + print(e) + onFinish?(false) + } + } + task.resume() + } + + + override func renameItem(fileId: String, newname: String, callCount: Int = 0, onFinish: ((String?) -> Void)?) { + if lastCall.timeIntervalSinceNow > -callWait || callSemaphore.wait(wallTimeout: .now()+5) == .timedOut { + if cancelTime.timeIntervalSinceNow > 0 { + cancelTime = Date(timeIntervalSinceNow: 0.5) + onFinish?(nil) + return + } + DispatchQueue.global().asyncAfter(deadline: .now()+callWait) { + self.renameItem(fileId: fileId, newname: newname, callCount: callCount+1, onFinish: onFinish) + } + return + } + os_log("%{public}@", log: log, type: .debug, "renameItem(dropbox:\(storageName ?? "") \(fileId) \(newname)") + lastCall = Date() + + guard var url = URL(string: accessURI) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if fileId != "" { + guard let pathURL = URL(string: fileId) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if pathURL.host != nil { + url = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: url) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + url = u + } + } + var destURL = url + destURL.deleteLastPathComponent() + destURL.appendPathComponent(newname) + //print(url) + //print(destURL) + + var request = URLRequest(url: url) + request.httpMethod = "MOVE" + request.setValue(destURL.absoluteString, forHTTPHeaderField: "Destination") + + var request2 = URLRequest(url: destURL) + request2.httpMethod = "PROPFIND" + request2.setValue("0", forHTTPHeaderField: "Depth") + request2.setValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type") + + let reqStr = [ + "", + "", + "", + "", + ].joined(separator: "\r\n")+"\r\n" + request2.httpBody = reqStr.data(using: .utf8) + + let task = dataSession.dataTask(with: request) + let task2 = dataSession.dataTask(with: request2) + headerHandler[task.taskIdentifier] = { response in + self.callSemaphore.signal() + guard let response = response as? HTTPURLResponse else { + return .cancel + } + guard response.statusCode == 201 else { + print(response) + return .cancel + } + task2.resume() + return .allow + } + dataTasks[task.taskIdentifier] = { data, error in + do { + if let error = error { + print(error) + throw RetryError.Retry + } + } + catch RetryError.Retry { + if callCount < 10 { + DispatchQueue.global().asyncAfter(deadline: .now()+self.callWait) { + self.renameItem(fileId: fileId, newname: newname, callCount: callCount+1, onFinish: onFinish) + } + return + } + onFinish?(nil) + } + catch let e { + print(e) + onFinish?(nil) + } + } + dataTasks[task2.taskIdentifier] = { data, error in + do { + guard let data = data else { + throw RetryError.Retry + } + let parser: XMLParser? = XMLParser(data: data) + let dav = DAVcollectionParser() + dav.onFinish = { result in + guard let result = result else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + if let item = result.first, let id = item["href"] as? String { + var prevParent: String? + var prevPath: String? + + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + let group = DispatchGroup() + group.enter() + backgroundContext.perform { + defer { + group.leave() + } + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") + if let result = try? backgroundContext.fetch(fetchRequest) { + for object in result { + if let item = object as? RemoteData { + prevPath = item.path + let component = prevPath?.components(separatedBy: "/") + prevPath = component?.dropLast().joined(separator: "/") + prevParent = item.parent + } + backgroundContext.delete(object as! NSManagedObject) + } + } + } + self.deleteChildRecursive(parent: fileId, context: backgroundContext) + group.notify(queue: .global()) { + self.storeItem(item: item, parentFileId: prevParent, parentPath: prevPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?(id) + } + } + } + } + else { + DispatchQueue.global().async { + onFinish?(nil) + } + } + } + parser?.delegate = dav + parser?.parse() + //print(String(data: data, encoding: .utf8)!) + } + catch RetryError.Retry { + onFinish?(nil) + return + } catch let e { + print(e) + onFinish?(nil) + return + } + } + task.resume() + } + + override func moveItem(fileId: String, fromParentId: String, toParentId: String, callCount: Int = 0, onFinish: ((String?) -> Void)?) { + + if lastCall.timeIntervalSinceNow > -callWait || callSemaphore.wait(wallTimeout: .now()+Double.random(in: 0.. 0 { + cancelTime = Date(timeIntervalSinceNow: 0.5) + onFinish?(nil) + return + } + DispatchQueue.global().asyncAfter(deadline: .now()+Double.random(in: 0..(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", toParentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + toParentPath = items.first?.path ?? "" + } + } + } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", toParentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + toParentPath = items.first?.path ?? "" + } + } + } + } + } + + os_log("%{public}@", log: self.log, type: .debug, "moveItem(WebDAV:\(self.storageName ?? "") \(fromParentId)->\(toParentId)") + self.lastCall = Date() + + guard var url = URL(string: accessURI) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + var destURL = url + if fileId != "" { + guard let pathURL = URL(string: fileId) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if pathURL.host != nil { + url = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: url) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + url = u + } + } + if toParentId != "" { + guard let pathURL = URL(string: toParentId) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + if pathURL.host != nil { + destURL = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: destURL) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + destURL = u + } + } + let name = url.lastPathComponent + destURL.appendPathComponent(name) + //print(url) + //print(destURL) + + var request = URLRequest(url: url) + request.httpMethod = "MOVE" + request.setValue(destURL.absoluteString, forHTTPHeaderField: "Destination") + + var request2 = URLRequest(url: destURL) + request2.httpMethod = "PROPFIND" + request2.setValue("0", forHTTPHeaderField: "Depth") + request2.setValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type") + + let reqStr = [ + "", + "", + "", + "", + ].joined(separator: "\r\n")+"\r\n" + request2.httpBody = reqStr.data(using: .utf8) + + let task = dataSession.dataTask(with: request) + let task2 = dataSession.dataTask(with: request2) + headerHandler[task.taskIdentifier] = { response in + self.callSemaphore.signal() + guard let response = response as? HTTPURLResponse else { + return .cancel + } + guard response.statusCode == 201 else { + print(response) + return .cancel + } + task2.resume() + return .allow + } + dataTasks[task.taskIdentifier] = { data, error in + do { + if let error = error { + print(error) + throw RetryError.Retry + } + } + catch RetryError.Retry { + if callCount < 10 { + DispatchQueue.global().asyncAfter(deadline: .now()+self.callWait) { + self.moveItem(fileId: fileId, fromParentId: fromParentId, toParentId: toParentId, callCount: callCount+1, onFinish: onFinish) + } + return + } + onFinish?(nil) + } + catch let e { + print(e) + onFinish?(nil) + } + } + dataTasks[task2.taskIdentifier] = { data, error in + do { + guard let data = data else { + throw RetryError.Retry + } + let parser: XMLParser? = XMLParser(data: data) + let dav = DAVcollectionParser() + dav.onFinish = { result in + guard let result = result else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + if let item = result.first, let id = item["href"] as? String { + var prevParent: String? + var prevPath: String? + + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + let group = DispatchGroup() + group.enter() + backgroundContext.perform { + defer { + group.leave() + } + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") + if let result = try? backgroundContext.fetch(fetchRequest) { + for object in result { + if let item = object as? RemoteData { + prevPath = item.path + let component = prevPath?.components(separatedBy: "/") + prevPath = component?.dropLast().joined(separator: "/") + prevParent = item.parent + } + backgroundContext.delete(object as! NSManagedObject) + } + } + } + self.deleteChildRecursive(parent: fileId, context: backgroundContext) + group.notify(queue: .global()) { + self.storeItem(item: item, parentFileId: toParentId, parentPath: toParentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?(id) + } + } + } + } + else { + DispatchQueue.global().async { + onFinish?(nil) + } + } + } + parser?.delegate = dav + parser?.parse() + //print(String(data: data, encoding: .utf8)!) + } + catch RetryError.Retry { + onFinish?(nil) + return + } catch let e { + print(e) + onFinish?(nil) + return + } + } + task.resume() + } + + override func changeTime(fileId: String, newdate: Date, callCount: Int = 0, onFinish: ((String?) -> Void)?) { + if lastCall.timeIntervalSinceNow > -callWait || callSemaphore.wait(wallTimeout: .now()+Double.random(in: 0.. 0 { + cancelTime = Date(timeIntervalSinceNow: 0.5) + onFinish?(nil) + return + } + DispatchQueue.global().asyncAfter(deadline: .now()+Double.random(in: 0..", + "", + "", + "", + ].joined(separator: "\r\n")+"\r\n" + request.httpBody = reqStr.data(using: .utf8) + + let task = dataSession.dataTask(with: request) + let task3 = dataSession.dataTask(with: request) + dataTasks[task.taskIdentifier] = { data, error in + do { + guard let data = data else { + throw RetryError.Retry + } + let lastmodified: (String)->String = { date in + [ + "", + "", + "", + "", + "\(date)", + "", + "", + "", + ].joined(separator: "\r\n")+"\r\n" + } + let win32lastmodified: (String)->String = { date in + [ + "", + "", + "", + "", + "\(date)", + "", + "", + "", + ].joined(separator: "\r\n")+"\r\n" + } + let parser: XMLParser? = XMLParser(data: data) + let dav = DAVcollectionParser() + dav.onFinish = { result in + guard let result = result else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" + formatter.timeZone = TimeZone(identifier: "GMT") + let formatter2 = ISO8601DateFormatter() + var reqStr2: String? + if let item = result.first, let propstat = item["propstat"] as? [String: Any], let prop = propstat["prop"] as? [String: String] { + if let mtime = prop["getlastmodified"] { + if formatter.date(from: mtime) != nil { + reqStr2 = lastmodified(formatter.string(from: newdate)) + } + else if formatter2.date(from: mtime) != nil { + reqStr2 = lastmodified(formatter2.string(from: newdate)) + } + else { + reqStr2 = lastmodified(formatter.string(from: newdate)) + } + } + else if let mtime = prop["Win32LastModifiedTime"] { + if formatter.date(from: mtime) != nil { + reqStr2 = win32lastmodified(formatter.string(from: newdate)) + } + else if formatter2.date(from: mtime) != nil { + reqStr2 = win32lastmodified(formatter2.string(from: newdate)) + } + else { + reqStr2 = win32lastmodified(formatter.string(from: newdate)) + } + } + else { + reqStr2 = lastmodified(formatter.string(from: newdate)) + } + } + guard let reqStr3 = reqStr2 else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + + var request2 = URLRequest(url: url) + request2.httpMethod = "PROPPATCH" + request2.setValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type") + + request2.httpBody = reqStr3.data(using: .utf8) + let task2 = self.dataSession.dataTask(with: request2) + self.dataTasks[task2.taskIdentifier] = { data, error in + if let error = error { + print(error) + onFinish?(nil) + return + } + guard let data = data else { + onFinish?(nil) + return + } + //print(String(data: data, encoding: .utf8)!) + let parser: XMLParser? = XMLParser(data: data) + let dav = DAVcollectionParser() + dav.onFinish = { result in + guard let result = result else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + guard let item = result.first else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + guard let propstat = item["propstat"] as? [String: Any] else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + guard let status = propstat["status"] as? String else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + if status.contains("200") { + task3.resume() + } + else { + task3.cancel() + } + } + parser?.delegate = dav + parser?.parse() + } + + task2.resume() + } + parser?.delegate = dav + parser?.parse() + //print(String(data: data, encoding: .utf8)!) + } + catch RetryError.Retry { + onFinish?(nil) + return + } catch let e { + print(e) + onFinish?(nil) + return + } + } + dataTasks[task3.taskIdentifier] = { data, error in + do { + guard let data = data else { + throw RetryError.Retry + } + let parser: XMLParser? = XMLParser(data: data) + let dav = DAVcollectionParser() + dav.onFinish = { result in + guard let result = result else { + DispatchQueue.global().async { + onFinish?(nil) + } + return + } + if let item = result.first, let id = item["href"] as? String { + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: item, parentFileId: nil, parentPath: nil, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?(id) + } + } + } + else { + DispatchQueue.global().async { + onFinish?(nil) + } + } + } + parser?.delegate = dav + parser?.parse() + print(String(data: data, encoding: .utf8)!) + } + catch RetryError.Retry { + onFinish?(nil) + return + } catch let e { + print(e) + onFinish?(nil) + return + } + } + task.resume() + } + + override func uploadFile(parentId: String, sessionId: String, uploadname: String, target: URL, onFinish: ((String?)->Void)?) { + + do { + let attr = try FileManager.default.attributesOfItem(atPath: target.path) + let fileSize = attr[.size] as! UInt64 + + UploadManeger.shared.UploadFixSize(identifier: sessionId, size: Int(fileSize)) + } + catch { + print(error) + try? FileManager.default.removeItem(at: target) + onFinish?(nil) + return + } + + guard var url = URL(string: accessURI) else { + try? FileManager.default.removeItem(at: target) + onFinish?(nil) + return + } + if parentId != "" { + guard let pathURL = URL(string: parentId) else { + try? FileManager.default.removeItem(at: target) + onFinish?(nil) + return + } + if pathURL.host != nil { + url = pathURL + } + else { + var allowedCharacterSet = CharacterSet.alphanumerics + allowedCharacterSet.insert(charactersIn: "-._~") + let p = pathURL.pathComponents.map({ $0 == "/" ? "/" : $0.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet)! }) + let p2: String + if p.first == "/" { + p2 = String(p.joined(separator: "/").dropFirst()) + } + else { + p2 = p.joined(separator: "/") + } + guard let u = URL(string: p2, relativeTo: url) else { + self.callSemaphore.signal() + onFinish?(nil) + return + } + url = u + } + } + url.appendPathComponent(uploadname) + + os_log("%{public}@", log: log, type: .debug, "uploadFile(google:\(storageName ?? "") \(uploadname)->\(parentId) \(target)") + + var parentPath = "\(storageName ?? ""):/" + if parentId != "" { + if Thread.isMainThread { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } + } + } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } + } + } + } + } + uploadSemaphore.wait() + let onFinish2: (String?)->Void = { id in + self.uploadSemaphore.signal() + onFinish?(id) + } + + var request: URLRequest = URLRequest(url: url) + request.httpMethod = "PUT" + + let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).\(self.storageName ?? "").\(Int.random(in: 0..<0xffffffff))") + //config.isDiscretionary = true + config.sessionSendsLaunchEvents = true + let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) + self.sessions += [session] + + let task = session.uploadTask(with: request, fromFile: target) + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + self.taskQueue.async { + self.task_upload[taskid] = ["data": Data(), "target": target, "url": url.absoluteString, "session": sessionId] + self.onFinsh_upload[taskid] = { urlstr in + guard let urlstr = urlstr, let url = URL(string: urlstr) else { + print("failed") + onFinish2(nil) + return + } + var request = URLRequest(url: url) + request.httpMethod = "PROPFIND" + request.setValue("0", forHTTPHeaderField: "Depth") + request.setValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type") + + let reqStr = [ + "", + "", + "", + "", + ].joined(separator: "\r\n")+"\r\n" + request.httpBody = reqStr.data(using: .utf8) + + let task2 = self.dataSession.dataTask(with: request) + self.dataTasks[task2.taskIdentifier] = { data, error in + do { + guard let data = data else { + throw RetryError.Retry + } + let parser: XMLParser? = XMLParser(data: data) + let dav = DAVcollectionParser() + dav.onFinish = { result in + guard let result = result else { + DispatchQueue.global().async { + onFinish2(nil) + } + return + } + if let item = result.first, let id = item["href"] as? String { + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: item, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish2(id) + } + } + } + else { + DispatchQueue.global().async { + onFinish2(nil) + } + } + } + parser?.delegate = dav + parser?.parse() + //print(String(data: data, encoding: .utf8)!) + } + catch RetryError.Retry { + onFinish2(nil) + return + } catch let e { + print(e) + onFinish2(nil) + return + } + } + task2.resume() + } + task.resume() + } + } + + var task_upload = [String: [String: Any]]() + var onFinsh_upload = [String: ((String?)->Void)?]() + var sessions = [URLSession]() + let taskQueue = DispatchQueue(label: "taskDict") + + public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + CloudFactory.shared.urlSessionDidFinishCallback?(session) + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + print("\(bytesSent) / \(totalBytesSent) / \(totalBytesExpectedToSend)") + + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + if let taskdata = taskQueue.sync(execute: { self.task_upload[taskid] }) { + guard let sessionId = taskdata["session"] as? String else { + return + } + UploadManeger.shared.UploadProgress(identifier: sessionId, possition: Int(totalBytesSent)) + } + } + + public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + print("d \(bytesWritten) / \(totalBytesWritten) / \(totalBytesExpectedToWrite)") + } + +} diff --git a/RemoteCloud/RemoteCloud/Storages/pCloudStorage.swift b/RemoteCloud/RemoteCloud/Storages/pCloudStorage.swift index 42687f2..0cdc11a 100644 --- a/RemoteCloud/RemoteCloud/Storages/pCloudStorage.swift +++ b/RemoteCloud/RemoteCloud/Storages/pCloudStorage.swift @@ -17,7 +17,8 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa } var webAuthSession: ASWebAuthenticationSession? - + let uploadSemaphore = DispatchSemaphore(value: 5) + public convenience init(name: String) { self.init() service = CloudFactory.getServiceName(service: .pCloud) @@ -276,7 +277,7 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa } } - func storeItem(item: [String: Any], parentFileId: String? = nil, parentPath: String? = nil, group: DispatchGroup?) { + func storeItem(item: [String: Any], parentFileId: String? = nil, parentPath: String? = nil, context: NSManagedObjectContext) { guard let id = item["id"] as? String else { return } @@ -295,16 +296,13 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa let size = item["size"] as? Int64 ?? 0 let hashint = item["hash"] as? Int - group?.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + context.perform { var prevParent: String? var prevPath: String? let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? context.fetch(fetchRequest) { for object in result { if let item = object as? RemoteData { prevPath = item.path @@ -312,11 +310,11 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa prevPath = component?.dropLast().joined(separator: "/") prevParent = item.parent } - viewContext.delete(object as! NSManagedObject) + context.delete(object as! NSManagedObject) } } - let newitem = RemoteData(context: viewContext) + let newitem = RemoteData(context: context) newitem.storage = self.storageName newitem.id = id newitem.name = name @@ -338,9 +336,7 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa newitem.path = "\(path)/\(name)" } } - group?.leave() } - } override func ListChildren(fileId: String, path: String, onFinish: (() -> Void)?) { @@ -357,14 +353,12 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa } listFolder(folderId: folderId) { result in if let items = result { - let group = DispatchGroup() - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() for item in items { - self.storeItem(item: item, parentFileId: fileId, parentPath: path, group: group) + self.storeItem(item: item, parentFileId: fileId, parentPath: path, context: backgroundContext) } - group.notify(queue: .main){ - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?() } @@ -498,6 +492,15 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa } override func readFile(fileId: String, start: Int64? = nil, length: Int64? = nil, callCount: Int = 0, onFinish: ((Data?) -> Void)?) { + + if let cache = CloudFactory.shared.cache.getCache(storage: storageName!, id: fileId, offset: start ?? 0, size: length ?? -1) { + if let data = try? Data(contentsOf: cache) { + os_log("%{public}@", log: log, type: .debug, "hit cache(pCloud:\(storageName ?? "") \(fileId) \(start ?? -1) \(length ?? -1) \((start ?? 0) + (length ?? 0))") + onFinish?(data) + return + } + } + os_log("%{public}@", log: log, type: .debug, "readFile(pCloud:\(storageName ?? "") \(fileId) \(start ?? -1) \(length ?? -1) \((start ?? 0) + (length ?? 0))") let id: Int @@ -594,17 +597,14 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa onFinish?(nil) return } - let group = DispatchGroup() - group.enter() DispatchQueue.global().async { - self.storeItem(item: metadata, parentFileId: parentId, parentPath: parentPath, group: group) - group.leave() - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() - DispatchQueue.global().async { - onFinish?(newid) + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: metadata, parentFileId: parentId, parentPath: parentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?(newid) + } } } } @@ -744,20 +744,19 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa onFinish?(false) return } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundCountext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundCountext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundCountext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundCountext.delete(object as! NSManagedObject) } } - - self.deleteChildRecursive(parent: fileId) - - try? viewContext.save() + } + self.deleteChildRecursive(parent: fileId, context: backgroundCountext) + backgroundCountext.perform { + try? backgroundCountext.save() DispatchQueue.global().async { onFinish?(true) } @@ -771,20 +770,19 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa onFinish?(false) return } - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundCountext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundCountext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", fileId, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundCountext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundCountext.delete(object as! NSManagedObject) } } - - self.deleteChildRecursive(parent: fileId) - - try? viewContext.save() + } + self.deleteChildRecursive(parent: fileId, context: backgroundCountext) + backgroundCountext.perform { + try? backgroundCountext.save() DispatchQueue.global().async { onFinish?(true) } @@ -958,15 +956,10 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa onFinish?(nil) return } - let group = DispatchGroup() - group.enter() - DispatchQueue.global().async { - self.storeItem(item: metadata, group: group) - group.leave() - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: metadata, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(newid) } @@ -980,15 +973,10 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa onFinish?(nil) return } - let group = DispatchGroup() - group.enter() - DispatchQueue.global().async { - self.storeItem(item: metadata, group: group) - group.leave() - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: metadata, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(newid) } @@ -1020,13 +1008,9 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa onFinish?(nil) return } - let group = DispatchGroup() var toParentPath = "" if toParentId != "" { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -1037,15 +1021,24 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa } } } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", toParentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + toParentPath = items.first?.path ?? "" + } + } + } + } } - group.enter() - DispatchQueue.global().async { - self.storeItem(item: metadata, parentFileId: toParentId, parentPath: toParentPath, group: group) - group.leave() - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: metadata, parentFileId: toParentId, parentPath: toParentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(newid) } @@ -1059,13 +1052,9 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa onFinish?(nil) return } - let group = DispatchGroup() var toParentPath = "" if toParentId != "" { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -1076,15 +1065,24 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa } } } + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", toParentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + toParentPath = items.first?.path ?? "" + } + } + } + } } - group.enter() - DispatchQueue.global().async { - self.storeItem(item: metadata, parentFileId: toParentId, parentPath: toParentPath, group: group) - group.leave() - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: metadata, parentFileId: toParentId, parentPath: toParentPath, context: backgroundContext) + backgroundContext.perform { + try? backgroundContext.save() DispatchQueue.global().async { onFinish?(newid) } @@ -1124,13 +1122,9 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa } targetStream.open() - let group = DispatchGroup() var parentPath = "\(storageName ?? ""):/" if parentId != "" { - group.enter() - DispatchQueue.main.async { - defer { group.leave() } - + if Thread.isMainThread { let viewContext = CloudFactory.shared.data.persistentContainer.viewContext let fetchRequest = NSFetchRequest(entityName: "RemoteData") @@ -1141,105 +1135,133 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa } } } - } - group.notify(queue: .global()){ - self.checkToken() { success in - guard success else { - targetStream.close() - try? FileManager.default.removeItem(at: target) - onFinish?(nil) - return - } - - var request: URLRequest = URLRequest(url: URL(string: "https://api.pcloud.com/uploadfile")!) - request.httpMethod = "POST" - request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization") - let boundary = "Boundary-\(UUID().uuidString)" - request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - - let boundaryText = "--\(boundary)\r\n" - var body = Data() - var footer = Data() - - body.append(boundaryText.data(using: .utf8)!) - body.append("Content-Disposition: form-data; name=\"folderid\"\r\n\r\n".data(using: .utf8)!) - body.append(String(folderId).data(using: .utf8)!) - body.append("\r\n".data(using: .utf8)!) - body.append(boundaryText.data(using: .utf8)!) - body.append("Content-Disposition: form-data; name=\"timeformat\"\r\n\r\n".data(using: .utf8)!) - body.append("timestamp".data(using: .utf8)!) - body.append("\r\n".data(using: .utf8)!) - - body.append(boundaryText.data(using: .utf8)!) - body.append("Content-Disposition: form-data; filename=\"\(uploadname)\"\r\n".data(using: .utf8)!) - body.append("Content-Length: \(fileSize)\r\n\r\n".data(using: .utf8)!) - //file body is here - footer.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) - - let tmpurl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(Int.random(in: 0...0xffffffff))") - guard let outStream = OutputStream(url: tmpurl, append: false) else { - onFinish?(nil) - return - } - outStream.open() - - guard body.withUnsafeBytes({ pointer in - outStream.write(pointer.bindMemory(to: UInt8.self).baseAddress!, maxLength: pointer.count) - }) == body.count else { - outStream.close() - try? FileManager.default.removeItem(at: tmpurl) - targetStream.close() - try? FileManager.default.removeItem(at: target) - onFinish?(nil) - return - } - - var offset = 0 - while offset < fileSize { - var buflen = Int(fileSize) - offset - if buflen > 1024*1024 { - buflen = 1024*1024 - } - var buf:[UInt8] = [UInt8](repeating: 0, count: buflen) - let len = targetStream.read(&buf, maxLength: buf.count) - if len <= 0 || outStream.write(buf, maxLength: len) != len { - print(targetStream.streamError!) - outStream.close() - try? FileManager.default.removeItem(at: tmpurl) - targetStream.close() - try? FileManager.default.removeItem(at: target) - onFinish?(nil) - return + else { + DispatchQueue.main.sync { + let viewContext = CloudFactory.shared.data.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "RemoteData") + fetchRequest.predicate = NSPredicate(format: "id == %@ && storage == %@", parentId, self.storageName ?? "") + if let result = try? viewContext.fetch(fetchRequest) { + if let items = result as? [RemoteData] { + parentPath = items.first?.path ?? "" + } } - offset += len } + } + } + uploadSemaphore.wait() + self.checkToken() { success in + let onFinish: (String?)->Void = { id in + self.uploadSemaphore.signal() + onFinish?(id) + } + guard success else { + targetStream.close() + try? FileManager.default.removeItem(at: target) + onFinish(nil) + return + } + + var request: URLRequest = URLRequest(url: URL(string: "https://api.pcloud.com/uploadfile")!) + request.httpMethod = "POST" + request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization") + let boundary = "Boundary-\(UUID().uuidString)" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + let boundaryText = "--\(boundary)\r\n" + var body = Data() + var footer = Data() + + body.append(boundaryText.data(using: .utf8)!) + body.append("Content-Disposition: form-data; name=\"folderid\"\r\n\r\n".data(using: .utf8)!) + body.append(String(folderId).data(using: .utf8)!) + body.append("\r\n".data(using: .utf8)!) + body.append(boundaryText.data(using: .utf8)!) + body.append("Content-Disposition: form-data; name=\"timeformat\"\r\n\r\n".data(using: .utf8)!) + body.append("timestamp".data(using: .utf8)!) + body.append("\r\n".data(using: .utf8)!) - guard footer.withUnsafeBytes({ pointer in - outStream.write(pointer.bindMemory(to: UInt8.self).baseAddress!, maxLength: pointer.count) - }) == footer.count else { + body.append(boundaryText.data(using: .utf8)!) + body.append("Content-Disposition: form-data; filename=\"\(uploadname)\"\r\n".data(using: .utf8)!) + body.append("Content-Length: \(fileSize)\r\n\r\n".data(using: .utf8)!) + //file body is here + footer.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) + + let tmpurl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(Int.random(in: 0...0xffffffff))") + guard let outStream = OutputStream(url: tmpurl, append: false) else { + onFinish(nil) + return + } + outStream.open() + + guard body.withUnsafeBytes({ pointer in + outStream.write(pointer.bindMemory(to: UInt8.self).baseAddress!, maxLength: pointer.count) + }) == body.count else { + outStream.close() + try? FileManager.default.removeItem(at: tmpurl) + targetStream.close() + try? FileManager.default.removeItem(at: target) + onFinish(nil) + return + } + + var offset = 0 + while offset < fileSize { + var buflen = Int(fileSize) - offset + if buflen > 1024*1024 { + buflen = 1024*1024 + } + var buf:[UInt8] = [UInt8](repeating: 0, count: buflen) + let len = targetStream.read(&buf, maxLength: buf.count) + if len <= 0 || outStream.write(buf, maxLength: len) != len { + print(targetStream.streamError!) outStream.close() try? FileManager.default.removeItem(at: tmpurl) targetStream.close() try? FileManager.default.removeItem(at: target) - onFinish?(nil) + onFinish(nil) return } + offset += len + } - targetStream.close() + guard footer.withUnsafeBytes({ pointer in + outStream.write(pointer.bindMemory(to: UInt8.self).baseAddress!, maxLength: pointer.count) + }) == footer.count else { outStream.close() + try? FileManager.default.removeItem(at: tmpurl) + targetStream.close() + try? FileManager.default.removeItem(at: target) + onFinish(nil) + return + } + + targetStream.close() + outStream.close() - #if !targetEnvironment(macCatalyst) - let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).\(self.storageName ?? "").\(Int.random(in: 0..<0xffffffff))") - //config.isDiscretionary = true - config.sessionSendsLaunchEvents = true - #else - let config = URLSessionConfiguration.default - #endif - let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) - self.sessions += [session] + do { + let attr = try FileManager.default.attributesOfItem(atPath: tmpurl.path) + let fileSize = attr[.size] as! UInt64 - let task = session.uploadTask(with: request, fromFile: tmpurl) - let taskid = task.taskIdentifier + UploadManeger.shared.UploadFixSize(identifier: sessionId, size: Int(fileSize)) + } + catch { + print(error) + try? FileManager.default.removeItem(at: tmpurl) + try? FileManager.default.removeItem(at: target) + onFinish(nil) + return + } + + let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).\(self.storageName ?? "").\(Int.random(in: 0..<0xffffffff))") + //config.isDiscretionary = true + config.sessionSendsLaunchEvents = true + let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) + self.sessions += [session] + + let task = session.uploadTask(with: request, fromFile: tmpurl) + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + self.taskQueue.async { self.task_upload[taskid] = ["data": Data(), "tmpfile": tmpurl, "parentId": parentId, "parentPath": parentPath] self.onFinsh_upload[taskid] = onFinish task.resume() @@ -1247,22 +1269,26 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa } } - var task_upload = [Int: [String: Any]]() - var onFinsh_upload = [Int: ((String?)->Void)?]() + var task_upload = [String: [String: Any]]() + var onFinsh_upload = [String: ((String?)->Void)?]() var sessions = [URLSession]() - + let taskQueue = DispatchQueue(label: "taskDict") + public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { CloudFactory.shared.urlSessionDidFinishCallback?(session) } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - if var taskdata = self.task_upload[dataTask.taskIdentifier] { + let taskid = (session.configuration.identifier ?? "") + ".\(dataTask.taskIdentifier)" + if var taskdata = taskQueue.sync(execute: { self.task_upload[taskid] }) { guard var recvData = taskdata["data"] as? Data else { return } recvData.append(data) taskdata["data"] = recvData - self.task_upload[dataTask.taskIdentifier] = taskdata + taskQueue.async { + self.task_upload[taskid] = taskdata + } } } @@ -1276,11 +1302,16 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - guard let taskdata = self.task_upload.removeValue(forKey: task.taskIdentifier) else { + let taskid = (session.configuration.identifier ?? "") + ".\(task.taskIdentifier)" + guard let taskdata = taskQueue.sync(execute: { + self.task_upload.removeValue(forKey: taskid) + }) else { print(task.response ?? "") return } - guard let onFinish = self.onFinsh_upload.removeValue(forKey: task.taskIdentifier) else { + guard let onFinish = taskQueue.sync(execute: { + self.onFinsh_upload.removeValue(forKey: taskid) + }) else { print("onFinish not found") return } @@ -1320,20 +1351,14 @@ public class pCloudStorage: NetworkStorage, URLSessionTaskDelegate, URLSessionDa print(object) throw RetryError.Failed } - let group = DispatchGroup() - group.enter() - DispatchQueue.global().async { - self.storeItem(item: metadata, parentFileId: parentId, parentPath: parentPath, group: group) - group.leave() - } - group.notify(queue: .main) { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - try? viewContext.save() + let backgroundCountext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + self.storeItem(item: metadata, parentFileId: parentId, parentPath: parentPath, context: backgroundCountext) + backgroundCountext.perform { + try? backgroundCountext.save() DispatchQueue.global().async { onFinish?(newid) } } - } catch { onFinish?(nil) diff --git a/RemoteCloud/RemoteCloud/SubItem/SubItem.swift b/RemoteCloud/RemoteCloud/SubItem/SubItem.swift index 128ea89..2511aa0 100644 --- a/RemoteCloud/RemoteCloud/SubItem/SubItem.swift +++ b/RemoteCloud/RemoteCloud/SubItem/SubItem.swift @@ -22,14 +22,13 @@ extension RemoteStorageBase: RemoteSubItem { return } if item.name.lowercased().hasSuffix(".cue") { - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + let backgroundContext = CloudFactory.shared.data.persistentContainer.newBackgroundContext() + backgroundContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@", item.id, self.storageName ?? "") - if let result = try? viewContext.fetch(fetchRequest) { + if let result = try? backgroundContext.fetch(fetchRequest) { for object in result { - viewContext.delete(object as! NSManagedObject) + backgroundContext.delete(object as! NSManagedObject) } } } @@ -49,94 +48,102 @@ extension RemoteStorageBase: RemoteSubItem { } var wavFile: RemoteWaveFile? var wavId = "" - let group = DispatchGroup() - group.enter() - DispatchQueue.main.async { - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - + backgroundContext.perform { let fetchRequest = NSFetchRequest(entityName: "RemoteData") fetchRequest.predicate = NSPredicate(format: "parent == %@ && storage == %@ && name == %@", item.parent, self.storageName ?? "", wav) - guard let result = try? viewContext.fetch(fetchRequest) as? [RemoteData], let wavdata = result.first, let wavitem = self.get(fileId: wavdata.id ?? "") else { - onFinish?() + guard let result = try? backgroundContext.fetch(fetchRequest) as? [RemoteData], let wavdata = result.first else { + DispatchQueue.global().async { + onFinish?() + } return } - wavId = wavdata.id ?? "" - DispatchQueue.global().async { - defer { - group.leave() + guard let wavitem = self.get(fileId: wavdata.id ?? "") else { + DispatchQueue.global().async { + onFinish?() + } + return } + wavId = wavdata.id ?? "" + let wavstream = wavitem.open() - guard let wav = RemoteWaveFile(stream: wavstream, size: wavitem.size) else { - onFinish?() + var wav: RemoteWaveFile? + for _ in 0..<5 { + wav = RemoteWaveFile(stream: wavstream, size: wavitem.size) + if wav != nil { + break + } + } + guard wav != nil else { wavstream.isLive = false wavitem.cancel() return } wavFile = wav wavstream.isLive = false - } - } - group.notify(queue: .main) { - guard let wavFile = wavFile else { - onFinish?() - return - } - let bytesPerSec = wavFile.wavFormat.BitsPerSample/8 * wavFile.wavFormat.SampleRate * wavFile.wavFormat.NumChannels - let bytesPerFrame = bytesPerSec / 75 - let endTime = wavFile.wavSize / bytesPerFrame - - let viewContext = CloudFactory.shared.data.persistentContainer.viewContext - - var diskTitle: String? - var diskPerformer: String? - for (index, track) in cue.tracks.enumerated() { - if index == 0 { - diskTitle = track["title"] as? String - diskPerformer = track["performer"] as? String - continue + + guard let wavFile = wavFile else { + DispatchQueue.global().async { + onFinish?() + } + return } + let bytesPerSec = wavFile.wavFormat.BitsPerSample/8 * wavFile.wavFormat.SampleRate * wavFile.wavFormat.NumChannels + let bytesPerFrame = bytesPerSec / 75 + let endTime = wavFile.wavSize / bytesPerFrame - let id = "\(item.id)\t\(index)" - guard let title = track["title"] as? String ?? diskTitle else { - continue - } - guard let performer = track["performer"] as? String ?? diskPerformer else { - continue + var diskTitle: String? + var diskPerformer: String? + for (index, track) in cue.tracks.enumerated() { + if index == 0 { + diskTitle = track["title"] as? String + diskPerformer = track["performer"] as? String + continue + } + + let id = "\(item.id)\t\(index)" + guard let title = track["title"] as? String ?? diskTitle else { + continue + } + guard let performer = track["performer"] as? String ?? diskPerformer else { + continue + } + let name = String(format: "%02d : %@ - %@", index, performer, title) + guard let start = track["start"] as? Int64 else { + continue + } + let end = track["end"] as? Int64 ?? Int64(endTime) + let size = 44 + (end - start) * Int64(bytesPerFrame) + let timelen = Double(end - start) / 75.0 + var sec = Int(timelen) + let msec = Int((timelen - Double(sec))*1000) + let min = Int(sec / 60) + sec -= min * 60 + let infostr = String(format: "%02d:%02d.%03d", min, sec, msec) + + let newitem = RemoteData(context: backgroundContext) + newitem.storage = self.storageName + newitem.id = id + newitem.name = name + newitem.ext = "wav" + newitem.cdate = item.cDate + newitem.mdate = item.mDate + newitem.folder = false + newitem.size = size + newitem.hashstr = "" + newitem.parent = item.id + newitem.path = item.path + newitem.substart = start + newitem.subend = end + newitem.subid = wavId + newitem.subinfo = infostr } - let name = String(format: "%02d : %@ - %@", index, performer, title) - guard let start = track["start"] as? Int64 else { - continue + try? backgroundContext.save() + DispatchQueue.global().async { + onFinish?() } - let end = track["end"] as? Int64 ?? Int64(endTime) - let size = 44 + (end - start) * Int64(bytesPerFrame) - let timelen = Double(end - start) / 75.0 - var sec = Int(timelen) - let msec = Int((timelen - Double(sec))*1000) - let min = Int(sec / 60) - sec -= min * 60 - let infostr = String(format: "%02d:%02d.%03d", min, sec, msec) - - let newitem = RemoteData(context: viewContext) - newitem.storage = self.storageName - newitem.id = id - newitem.name = name - newitem.ext = "wav" - newitem.cdate = item.cDate - newitem.mdate = item.mDate - newitem.folder = false - newitem.size = size - newitem.hashstr = "" - newitem.parent = item.id - newitem.path = item.path - newitem.substart = start - newitem.subend = end - newitem.subid = wavId - newitem.subinfo = infostr } - try? viewContext.save() - onFinish?() } } } diff --git a/RemoteCloud/RemoteCloud/UploadManager.swift b/RemoteCloud/RemoteCloud/UploadManager.swift index e8a7993..b032162 100644 --- a/RemoteCloud/RemoteCloud/UploadManager.swift +++ b/RemoteCloud/RemoteCloud/UploadManager.swift @@ -103,6 +103,7 @@ public class UploadManeger { if let i = self.uploadIdentifers.firstIndex(of: identifier) { self.uploadIdentifers.remove(at: i) } + self.uploadSessions[identifier]?.position = self.uploadSessions[identifier]?.size ?? 0 self.finishedSessions[identifier] = self.uploadSessions[identifier] self.finishedSessions[identifier]?.finishDate = Date() self.uploadSessions[identifier] = nil @@ -161,10 +162,10 @@ class UploadItemCell: UITableViewCell { } if let p = info?.position, let s = info?.size, s != 0 { let ratio = Float(p) / Float(s) - progressView.setProgress(ratio, animated: true) + progressView.setProgress(ratio, animated: false) } else { - progressView.setProgress(0, animated: true) + progressView.setProgress(0, animated: false) } } } diff --git a/ccViewer/CryptCloudViewer.xcodeproj/project.pbxproj b/ccViewer/CryptCloudViewer.xcodeproj/project.pbxproj index fcecb74..468688e 100644 --- a/ccViewer/CryptCloudViewer.xcodeproj/project.pbxproj +++ b/ccViewer/CryptCloudViewer.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ 3CA2C9632235969900481DB5 /* TableViewControllerItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA2C9622235969900481DB5 /* TableViewControllerItems.swift */; }; 3CBBC1812252F5C70014AB10 /* ViewControllerTutorialPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBBC1802252F5C70014AB10 /* ViewControllerTutorialPage.swift */; }; 3CC48515225719160003C44A /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC48514225719160003C44A /* CloudKit.framework */; }; - 3CC4851D2258945B0003C44A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 3CC4851C2258945B0003C44A /* Settings.bundle */; }; 3CC48521225AC1690003C44A /* NavigationControllerHide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC48520225AC1690003C44A /* NavigationControllerHide.swift */; }; 3CEF9AC9223EE865008F90B5 /* ViewControllerPDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEF9AC8223EE865008F90B5 /* ViewControllerPDF.swift */; }; 3CF15E7922370B9300328A90 /* ViewControllerText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF15E7822370B9300328A90 /* ViewControllerText.swift */; }; @@ -63,6 +62,9 @@ 3F3D274423743B00009A4C52 /* TimePickerKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3D274323743B00009A4C52 /* TimePickerKeyboard.swift */; }; 3F48A078237AFDFF008B6690 /* GoogleCast.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F48A076237AFD99008B6690 /* GoogleCast.framework */; platformFilter = ios; }; 3F48A079237AFDFF008B6690 /* GoogleCast.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3F48A076237AFD99008B6690 /* GoogleCast.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3F84578A238E5039004E78EC /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F845789238E5039004E78EC /* SceneDelegate.swift */; }; + 3F84578C238E622C004E78EC /* About.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3F84578B238E622C004E78EC /* About.storyboard */; }; + 3F84578E238E6896004E78EC /* ViewControllerAbout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F84578D238E6896004E78EC /* ViewControllerAbout.swift */; }; 3F92E7032336B1F700D1FF9B /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3F92E7022336B1F700D1FF9B /* Media.xcassets */; }; 3FA954D62322EC22005B086A /* ViewControllerConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA954D52322EC22005B086A /* ViewControllerConvert.swift */; }; 3FC2F2392381355800EB747C /* ccViewer.help in Resources */ = {isa = PBXBuildFile; fileRef = 3FC2F2382381355800EB747C /* ccViewer.help */; }; @@ -127,7 +129,6 @@ 3CA2C9622235969900481DB5 /* TableViewControllerItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewControllerItems.swift; sourceTree = ""; }; 3CBBC1802252F5C70014AB10 /* ViewControllerTutorialPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerTutorialPage.swift; sourceTree = ""; }; 3CC48514225719160003C44A /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; - 3CC4851C2258945B0003C44A /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 3CC48520225AC1690003C44A /* NavigationControllerHide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControllerHide.swift; sourceTree = ""; }; 3CEF9AC8223EE865008F90B5 /* ViewControllerPDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerPDF.swift; sourceTree = ""; }; 3CF15E7822370B9300328A90 /* ViewControllerText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerText.swift; sourceTree = ""; }; @@ -181,6 +182,9 @@ 3F19BC21232455C80040ED19 /* RemoteCloud.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RemoteCloud.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3F3D274323743B00009A4C52 /* TimePickerKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerKeyboard.swift; sourceTree = ""; }; 3F48A076237AFD99008B6690 /* GoogleCast.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GoogleCast.framework; path = "../GoogleCastSDK-ios-4.4.6_dynamic/GoogleCast.framework"; sourceTree = ""; }; + 3F845789238E5039004E78EC /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 3F84578B238E622C004E78EC /* About.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = About.storyboard; sourceTree = ""; }; + 3F84578D238E6896004E78EC /* ViewControllerAbout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerAbout.swift; sourceTree = ""; }; 3F9151D5236C191600E5EF65 /* GoogleCast.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GoogleCast.framework; path = "../GoogleCastSDK-ios-4.4.5_dynamic/GoogleCast.framework"; sourceTree = ""; }; 3F92E7022336B1F700D1FF9B /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = ""; }; 3FA954D52322EC22005B086A /* ViewControllerConvert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerConvert.swift; sourceTree = ""; }; @@ -271,7 +275,6 @@ 3F19BC21232455C80040ED19 /* RemoteCloud.framework */, 3F19BC1E232455C80040ED19 /* ffplayer.framework */, 3F19BC1B232455C80040ED19 /* ffconverter.framework */, - 3CC4851C2258945B0003C44A /* Settings.bundle */, 3CFCF697222F347A00843054 /* ccViewer */, 3CFCF6AF222F347B00843054 /* ccViewerTests */, 3CFCF6BA222F347B00843054 /* ccViewerUITests */, @@ -296,6 +299,7 @@ 3CA2C96122355E9B00481DB5 /* ccViewer.entitlements */, 3FC2F2382381355800EB747C /* ccViewer.help */, 3C0693342253CCD40040DC0B /* Info.plist */, + 3F84578B238E622C004E78EC /* About.storyboard */, 3CFCF698222F347A00843054 /* AppDelegate.swift */, 3CA2C9582234DE2D00481DB5 /* CollectionViewControllerCloud.swift */, 3C40091122401D020028A45A /* CustomPlayerViewController.swift */, @@ -303,6 +307,7 @@ 3C0693472253DBD40040DC0B /* ProductManager.swift */, 3C0693482253DBD50040DC0B /* PurchaceManager.swift */, 3C0693492253DBD50040DC0B /* PurchaceViewController.swift */, + 3F845789238E5039004E78EC /* SceneDelegate.swift */, 3CA2C9622235969900481DB5 /* TableViewControllerItems.swift */, 3C0D4418224B106C001FCAAC /* TableViewControllerItemsEdit.swift */, 3C79B383225FE3DE0036E22F /* TableViewControllerPlaylist.swift */, @@ -310,6 +315,7 @@ 3CFCF6C9222F351900843054 /* TableViewControllerRoot.swift */, 3CF89CEF22333C14002FF0B5 /* TableViewControllerSetting.swift */, 3F3D274323743B00009A4C52 /* TimePickerKeyboard.swift */, + 3F84578D238E6896004E78EC /* ViewControllerAbout.swift */, 3FA954D52322EC22005B086A /* ViewControllerConvert.swift */, 3C0654A6223978BF003BD932 /* ViewControllerFirst.swift */, 3C06549E22381567003BD932 /* ViewControllerImage.swift */, @@ -396,7 +402,6 @@ 3CFCF693222F347A00843054 /* Resources */, 3CA2C9552234980C00481DB5 /* Embed Frameworks */, 3F9151E5236C463100E5EF65 /* ShellScript */, - 3F1102F923704BAE00994810 /* ShellScript */, ); buildRules = ( ); @@ -526,11 +531,11 @@ files = ( 3CFCF6A6222F347B00843054 /* LaunchScreen.storyboard in Resources */, 3C4009192240653C0028A45A /* Localizable.strings in Resources */, + 3F84578C238E622C004E78EC /* About.storyboard in Resources */, 3F92E7032336B1F700D1FF9B /* Media.xcassets in Resources */, 3CFCF6A3222F347B00843054 /* Assets.xcassets in Resources */, 3FC2F2392381355800EB747C /* ccViewer.help in Resources */, 3CFCF69E222F347A00843054 /* Main.storyboard in Resources */, - 3CC4851D2258945B0003C44A /* Settings.bundle in Resources */, 3C0693432253D1EC0040DC0B /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -552,23 +557,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3F1102F923704BAE00994810 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\nbuildPlist=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n\necho $buildPlist\nAPP_VERSION=$(/usr/libexec/PlistBuddy -c \"Print CFBundleShortVersionString\" $buildPlist)\n/usr/libexec/PlistBuddy -c \"Set :PreferenceSpecifiers:0:DefaultValue ${APP_VERSION}\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Settings.bundle/Root.plist\"\n"; - }; 3F9151E5236C463100E5EF65 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -599,6 +587,7 @@ 3CA2C9592234DE2D00481DB5 /* CollectionViewControllerCloud.swift in Sources */, 3C06934B2253DBD50040DC0B /* PurchaceManager.swift in Sources */, 3C40091222401D020028A45A /* CustomPlayerViewController.swift in Sources */, + 3F84578A238E5039004E78EC /* SceneDelegate.swift in Sources */, 3CF15E7922370B9300328A90 /* ViewControllerText.swift in Sources */, 3F3D274423743B00009A4C52 /* TimePickerKeyboard.swift in Sources */, 3C06934C2253DBD50040DC0B /* PurchaceViewController.swift in Sources */, @@ -606,6 +595,7 @@ 3CEF9AC9223EE865008F90B5 /* ViewControllerPDF.swift in Sources */, 3FA954D62322EC22005B086A /* ViewControllerConvert.swift in Sources */, 3C79B38622608C4D0036E22F /* TableViewControllerPlayListFolder.swift in Sources */, + 3F84578E238E6896004E78EC /* ViewControllerAbout.swift in Sources */, 3C0654A7223978BF003BD932 /* ViewControllerFirst.swift in Sources */, 3CFCF6CD223059AF00843054 /* ViewControllerProtect.swift in Sources */, 3C06549F22381567003BD932 /* ViewControllerImage.swift in Sources */, @@ -813,7 +803,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ccViewer/ccViewer.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 56; + CURRENT_PROJECT_VERSION = 60; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = 7A9X38B4YU; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; @@ -848,7 +838,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ccViewer/ccViewer.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 56; + CURRENT_PROJECT_VERSION = 60; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = 7A9X38B4YU; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; diff --git a/ccViewer/Settings.bundle/Licence.plist b/ccViewer/Settings.bundle/Licence.plist deleted file mode 100644 index 8c3605d..0000000 --- a/ccViewer/Settings.bundle/Licence.plist +++ /dev/null @@ -1,40 +0,0 @@ - - - - - StringsTable - Licence - PreferenceSpecifiers - - - Type - PSGroupSpecifier - Title - FFmpeg version 4.2.1 - FooterText - -GNU LESSER GENERAL PUBLIC LICENSE -Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -Everyone is permitted to copy and distribute verbatim copies -of this license document, but changing it is not allowed. - - - - Type - PSGroupSpecifier - Title - Google Cast SDK (iOS Sender v4.4.5) - FooterText - -Google Cast SDK Additional Developer Terms of Service -https://developers.google.com/cast/docs/terms - -Chrome Sender, Media Player Library and Receiver use the Closure library, which is licensed under the Apache 2.0 License, and the lit-html library, which is licensed under the BSD 3-Clause License. - - - - - diff --git a/ccViewer/Settings.bundle/Root.plist b/ccViewer/Settings.bundle/Root.plist deleted file mode 100644 index 84e9d89..0000000 --- a/ccViewer/Settings.bundle/Root.plist +++ /dev/null @@ -1,29 +0,0 @@ - - - - - StringsTable - Root - PreferenceSpecifiers - - - Type - PSTitleValueSpecifier - DefaultValue - 1.0.0 - Title - Version - Key - sbVersion - - - Title - Licences - Type - PSChildPaneSpecifier - File - Licence - - - - diff --git a/ccViewer/ccViewer/About.storyboard b/ccViewer/ccViewer/About.storyboard new file mode 100644 index 0000000..369034b --- /dev/null +++ b/ccViewer/ccViewer/About.storyboard @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FFmpeg version 4.2.1

GNU LESSER GENERAL PUBLIC LICENSE +Version 2.1, February 1999 +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. +

Google Cast SDK (iOS Sender v4.4.6)

Google Cast SDK Additional Developer Terms of Service +https://developers.google.com/cast/docs/terms +Chrome Sender, Media Player Library and Receiver use the Closure library, which is licensed under the Apache 2.0 License, and the lit-html library, which is licensed under the BSD 3-Clause License. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ccViewer/ccViewer/AppDelegate.swift b/ccViewer/ccViewer/AppDelegate.swift index 3b332d1..e01df95 100644 --- a/ccViewer/ccViewer/AppDelegate.swift +++ b/ccViewer/ccViewer/AppDelegate.swift @@ -14,16 +14,17 @@ import AVFoundation import RemoteCloud import ffplayer +#if !targetEnvironment(macCatalyst) import GoogleCast +#endif @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate, PurchaseManagerDelegate { +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + let kReceiverAppID = "5171613F" //let kReceiverAppID = "AA0DBCA4" - let kDebugLoggingEnabled = false - - var window: UIWindow? - var completionHandlers = [String: ()->Void]() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -37,75 +38,58 @@ class AppDelegate: UIResponder, UIApplicationDelegate, PurchaseManagerDelegate { try FileManager.default.removeItem(at: fileURL) } } catch { print(error) } - - UIApplication.shared.beginReceivingRemoteControlEvents() - do { - try AVAudioSession.sharedInstance().setCategory(.playback) - try AVAudioSession.sharedInstance().setActive(true) - } catch { - print(error) - } - // google chromecast + + // MARK: google chromecast #if !targetEnvironment(macCatalyst) let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID) let options = GCKCastOptions(discoveryCriteria: criteria) options.physicalVolumeButtonsWillControlDeviceVolume = true GCKCastContext.setSharedInstanceWith(options) // Enable logger. - GCKLogger.sharedInstance().delegate = self + //GCKLogger.sharedInstance().delegate = self //let filter = GCKLoggerFilter() //filter.minimumLevel = .debug //GCKLogger.sharedInstance().filter = filter GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true #endif - - window?.clipsToBounds = true - - //--------------------------------------- - // アプリ内課金設定 - //--------------------------------------- - // デリゲート設定 - PurchaseManager.sharedManager().delegate = self - // オブザーバー登録 - SKPaymentQueue.default().add(PurchaseManager.sharedManager()) + // MARK: RemoteCloud CloudFactory.shared.urlSessionDidFinishCallback = urlSessionDidFinishEvents - - return true - } - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. - FFPlayerViewController.inFocus = false - } + // MARK: Payment + PurchaseManager.sharedManager().delegate = self + SKPaymentQueue.default().add(PurchaseManager.sharedManager()) - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + return true } - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. - } + // MARK: UISceneSession Lifecycle - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - FFPlayerViewController.inFocus = true + @available(iOS 13.0, *) + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + if options.userActivities.filter({$0.activityType == "info.lithium03.ccViewer.about"}).first != nil { + return UISceneConfiguration(name: "About Configuration", sessionRole: connectingSceneSession.role) + } + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - // Saves changes in the application's managed object context before the application terminates. - CloudFactory.shared.data.saveContext() + @available(iOS 13.0, *) + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } + // MARK: background URLSession + func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { completionHandlers[identifier] = completionHandler print("handleEventsForBackgroundURLSession \(identifier)") } - + func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { let sessionIdentifier = session.configuration.identifier print("urlSessionDidFinishEvents \(sessionIdentifier ?? "")") @@ -118,47 +102,62 @@ class AppDelegate: UIResponder, UIApplicationDelegate, PurchaseManagerDelegate { handler() } } - - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - - // Determine who sent the URL. - let sendingAppID = options[.sourceApplication] - print("source application = \(sendingAppID ?? "Unknown")") + + // MARK: - MENU + #if targetEnvironment(macCatalyst) + override func buildMenu(with builder: UIMenuBuilder) { + super.buildMenu(with: builder) - // Process the URL. - guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true), - let targetPath = components.path else { - print("Invalid URL or album path missing") - return false + let aboutCommand = UICommand(title: NSLocalizedString("About CryptCloudViewer", comment: ""), action: #selector(aboutAction)) + let aboutMenu = UIMenu(title: "About", image: nil, identifier: .about, options: .displayInline, children: [aboutCommand]) + builder.replace(menu: .about, with: aboutMenu) + } + + @objc func aboutAction(_ sender: AnyObject) { + let userActivity = NSUserActivity(activityType: "info.lithium03.ccViewer.about") + let windowScene = UIApplication.shared.connectedScenes + .filter { $0.activationState == .foregroundActive } + .first + if let windowScene = windowScene as? UIWindowScene { + let session = windowScene.session + UIApplication.shared.requestSceneSessionActivation(session, userActivity: userActivity, options: nil) } + } + #endif + + // MARK: iOS12 back compatibility - print(targetPath) - return true + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + FFPlayerViewController.inFocus = false } - - // 課金終了(前回アプリ起動時課金処理が中断されていた場合呼ばれる) + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + FFPlayerViewController.inFocus = true + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + // Saves changes in the application's managed object context before the application terminates. + CloudFactory.shared.data.saveContext() + CloudFactory.shared.cache.saveContext() + } + +} + +extension AppDelegate: PurchaseManagerDelegate { + func purchaseManager(_ purchaseManager: PurchaseManager!, didFinishUntreatedPurchaseWithTransaction transaction: SKPaymentTransaction!, decisionHandler: ((_ complete: Bool) -> Void)!) { print("#### didFinishUntreatedPurchaseWithTransaction ####") // TODO: コンテンツ解放処理 //コンテンツ解放が終了したら、この処理を実行(true: 課金処理全部完了, false 課金処理中断) decisionHandler(true) } + } -// MARK: - GCKLoggerDelegate -extension AppDelegate: GCKLoggerDelegate { - func logMessage(_ message: String, - at _: GCKLoggerLevel, - fromFunction function: String, - location: String) { - if kDebugLoggingEnabled { - // Send SDK's log messages directly to the console. - print("\(location): \(function) - \(message)") - } - } -} - - extension UIApplication { class func topViewController(controller: UIViewController? = nil) -> UIViewController? { var controller2 = controller diff --git a/ccViewer/ccViewer/CustomPlayerViewController.swift b/ccViewer/ccViewer/CustomPlayerViewController.swift index 114ee52..95a6f2d 100644 --- a/ccViewer/ccViewer/CustomPlayerViewController.swift +++ b/ccViewer/ccViewer/CustomPlayerViewController.swift @@ -40,7 +40,9 @@ extension AVPlayerViewController { class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { static var pipVideo = false + let itemQueue = DispatchQueue(label: "playItemQueue") var playItems = [[String: Any]]() + var playIndex = [Int]() var loop = false var shuffle = false var playtitle = "" @@ -50,7 +52,8 @@ class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { var customDelegate = [URL: [CustomAVARLDelegate]]() var infoItems = [URL: [String: Any]]() var image: MPMediaItemArtwork? - var playCount = -1 + var queue = DispatchQueue(label: "load_items") + var playCounts = [URL: Int]() func getURL(storage: String, fileId: String) -> URL { var allowedCharacterSet = CharacterSet.alphanumerics @@ -62,7 +65,7 @@ class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { return url! } - func getPlayItem(url: URL) -> AVPlayerItem { + func getAsset(url: URL) -> AVURLAsset { let asset = AVURLAsset(url: url) let newDelegate = CustomAVARLDelegate() if var prev = self.customDelegate[url] { @@ -72,42 +75,112 @@ class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { else { self.customDelegate[url] = [newDelegate] } - asset.resourceLoader.setDelegate(newDelegate, queue: newDelegate.queue) - let playerItem = AVPlayerItem(asset: asset) - return playerItem + asset.resourceLoader.setDelegate(newDelegate, queue: queue) + return asset } - lazy var player: AVQueuePlayer = { - if self.loop && self.playItems.count <= 2 { - self.playItems += self.playItems + func PrepareAVPlayerItem(itemIndex: Int, needplay: Bool = false) { + let item = playItems[itemIndex] + let storage = item["storage"] as! String + let id = item["id"] as! String + let url = getURL(storage: storage, fileId: id) + self.infoItems[url] = item + let asset = getAsset(url: url) + let playableKey = "playable" + // Load the "playable" property + asset.loadValuesAsynchronously(forKeys: [playableKey]) { + var error: NSError? = nil + let status = asset.statusOfValue(forKey: playableKey, error: &error) + switch status { + case .loaded: + // Sucessfully loaded. Continue processing. + let playitem = AVPlayerItem(asset: asset) + self.itemQueue.async { + let pos: CMTime + if let lc = item["loadCount"] as? Int, lc > 0 { + self.playItems[itemIndex]["loadCount"] = lc + 1 + let skip = UserDefaults.standard.integer(forKey: "playStartSkipSec") + let duration = UserDefaults.standard.integer(forKey: "playStopAfterSec") + if duration > 0 { + playitem.forwardPlaybackEndTime = CMTimeMakeWithSeconds(Double(skip+duration), preferredTimescale: Int32(NSEC_PER_SEC)) + } + if skip > 0 { + pos = CMTimeMakeWithSeconds(Double(skip), preferredTimescale: Int32(NSEC_PER_SEC)) + playitem.seek(to: pos) { finished in + } + } + } + else { + self.playItems[itemIndex]["loadCount"] = 1 + if let stop = item["stop"] as? Double { + playitem.forwardPlaybackEndTime = CMTimeMakeWithSeconds(stop, preferredTimescale: Int32(NSEC_PER_SEC)) + } + if let start = item["start"] as? Double { + pos = CMTimeMakeWithSeconds(start, preferredTimescale: Int32(NSEC_PER_SEC)) + playitem.seek(to: pos) { finished in + } + } + } + } + let center = NotificationCenter.default + center.addObserver(self, selector: #selector(self.newErrorLogEntry), name: .AVPlayerItemNewErrorLogEntry, object: playitem) + center.addObserver(self, selector: #selector(self.failedToPlayToEndTime), name: .AVPlayerItemFailedToPlayToEndTime, object: playitem) + center.addObserver(self, selector: #selector(self.didPlayToEndTime), name: .AVPlayerItemDidPlayToEndTime, object: playitem) + + DispatchQueue.main.async { + self.player.insert(playitem, after: nil) + if needplay { + self.player.play() + } + } + + if self.playIndex.count > 0 { + self.enqueuePlayItem() + } + + default: + // Handle all other cases + if let lc = item["loadCount"] as? Int, lc > 0 { + self.playItems[itemIndex]["loadCount"] = lc + 1 + } + else { + self.playItems[itemIndex]["loadCount"] = 1 + } + } } + } + + lazy var player: AVQueuePlayer = { + self.playIndex = Array(0..AVPlayerItem in + for item in self.playItems { let storage = item["storage"] as! String let id = item["id"] as! String let url = getURL(storage: storage, fileId: id) - let playitem = getPlayItem(url: url) - self.infoItems[url] = item - let pos: CMTime - if let stop = item["stop"] as? Double { - playitem.forwardPlaybackEndTime = CMTimeMakeWithSeconds(stop, preferredTimescale: Int32(NSEC_PER_SEC)) - } - if let start = item["start"] as? Double { - pos = CMTimeMakeWithSeconds(start, preferredTimescale: Int32(NSEC_PER_SEC)) - playitem.seek(to: pos) { finished in - } - } - return playitem - }) - if !self.loop { - playCount = items.count + self.playCounts[url] = 0 } - var player = AVQueuePlayer(items: items) + var player = AVQueuePlayer() return player }() + func enqueuePlayItem(needplay: Bool = false) { + itemQueue.async { + if let next = self.playIndex.first { + self.PrepareAVPlayerItem(itemIndex: next, needplay: needplay) + self.playIndex = Array(self.playIndex.dropFirst()) + } + else if self.loop || UserDefaults.standard.bool(forKey: "keepOpenWhenDone") { + self.playIndex = Array(0.. 0 { - customDelegate[url] = prev + else { + if let url = cururl { + self.playCounts[url] = self.playCounts[url]! + 1 } - else { - customDelegate.removeValue(forKey: url) + self.player.advanceToNextItem() + if let url = url, var prev = self.customDelegate[url] { + prev = Array(prev.dropFirst()) + if prev.count > 0 { + self.customDelegate[url] = prev + } + else { + self.customDelegate.removeValue(forKey: url) + } } - } - if playCount <= 1 { - playCount = playItems.count - } - else { - player.play() - if playCount > 0 { - playCount -= 1 + let c = self.playCounts.first?.value ?? 0 + if !self.loop && self.playCounts.values.allSatisfy({ $0 == c }) { + // stop + } + else { + self.player.play() } } } @@ -272,12 +347,15 @@ class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { basename = components.joined(separator: ".") } + if let imageitem = CloudFactory.shared.data.getImage(storage: storage, parentId: parentId, baseName: basename) { - if let imagestream = CloudFactory.shared[storage]?.get(fileId: imageitem.id ?? "")?.open() { - imagestream.read(position: 0, length: Int(imageitem.size)) { data in - if let data = data, let image = UIImage(data: data) { - self.image = MPMediaItemArtwork(boundsSize: image.size) { size in - return image + DispatchQueue.global().async { + if let imagestream = CloudFactory.shared[storage]?.get(fileId: imageitem.id ?? "")?.open() { + imagestream.read(position: 0, length: Int(imageitem.size)) { data in + if let data = data, let image = UIImage(data: data) { + self.image = MPMediaItemArtwork(boundsSize: image.size) { size in + return image + } } } } @@ -289,6 +367,7 @@ class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { } func play(parent: UIViewController) { + UIApplication.shared.beginReceivingRemoteControlEvents() do { try AVAudioSession.sharedInstance().setCategory(.playback) try AVAudioSession.sharedInstance().setActive(true) @@ -302,37 +381,34 @@ class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { player.addObserver(self, forKeyPath: #keyPath(AVPlayer.status), options: [.new, .initial], context: nil) player.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.status), options:[.new, .initial], context: nil) player.addObserver(self, forKeyPath: #keyPath(AVPlayer.timeControlStatus), options:[.new, .old], context: nil) - player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 1) , queue: DispatchQueue.main) { [weak self] time in + player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 1) , queue: DispatchQueue.global()) { [weak self] time in guard self?.player.timeControlStatus == .playing, time.seconds > 1.0 else { return } - if var info = MPNowPlayingInfoCenter.default().nowPlayingInfo { - info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time.seconds - info[MPMediaItemPropertyPlaybackDuration] = self?.player.currentItem?.asset.duration.seconds - info[MPMediaItemPropertyArtwork] = - self?.image - info[MPMediaItemPropertyTitle] = self?.playtitle - MPNowPlayingInfoCenter.default().nowPlayingInfo = info - } - else { - var info = [String: Any]() - info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time.seconds - info[MPMediaItemPropertyPlaybackDuration] = self?.player.currentItem?.asset.duration.seconds - info[MPMediaItemPropertyArtwork] = - self?.image - info[MPMediaItemPropertyTitle] = self?.playtitle - MPNowPlayingInfoCenter.default().nowPlayingInfo = info + DispatchQueue.main.async { + if var info = MPNowPlayingInfoCenter.default().nowPlayingInfo { + info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time.seconds + info[MPMediaItemPropertyPlaybackDuration] = self?.player.currentItem?.asset.duration.seconds + info[MPMediaItemPropertyArtwork] = + self?.image + info[MPMediaItemPropertyTitle] = self?.playtitle + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + } + else { + var info = [String: Any]() + info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time.seconds + info[MPMediaItemPropertyPlaybackDuration] = self?.player.currentItem?.asset.duration.seconds + info[MPMediaItemPropertyArtwork] = + self?.image + info[MPMediaItemPropertyTitle] = self?.playtitle + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + } } } let center = NotificationCenter.default - for pitem in self.player.items() { - center.addObserver(self, selector: #selector(newErrorLogEntry), name: .AVPlayerItemNewErrorLogEntry, object: pitem) - center.addObserver(self, selector: #selector(failedToPlayToEndTime), name: .AVPlayerItemFailedToPlayToEndTime, object: pitem) - center.addObserver(self, selector: #selector(didPlayToEndTime), name: .AVPlayerItemDidPlayToEndTime, object: pitem) - } - center.addObserver(forName: .avPlayerViewDisappear, object: playerViewController, queue: nil) { notification in + print("PlayerViewDisappear") if !CustomPlayerView.pipVideo { self.finishDisplay() } @@ -340,9 +416,13 @@ class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { setupRemoteTransportControls() - parent.present(playerViewController, animated: true) { - self.player.play() + enqueuePlayItem(needplay: true) + if loop || UserDefaults.standard.bool(forKey: "keepOpenWhenDone") { + if self.playItems.count < 2 { + enqueuePlayItem() + } } + parent.present(self.playerViewController, animated: true, completion: nil) } @objc func appMovedToForeground() { @@ -402,80 +482,51 @@ class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { } } - func loopSetup() { - if self.shuffle { - self.playItems.shuffle() - } - let items = self.playItems.map({ (item: [String: Any])->AVPlayerItem in - let storage = item["storage"] as! String - let id = item["id"] as! String - let url = getURL(storage: storage, fileId: id) - let playitem = getPlayItem(url: url) - let pos: CMTime - if let stop = item["stop"] as? Double { - playitem.forwardPlaybackEndTime = CMTimeMakeWithSeconds(stop, preferredTimescale: Int32(NSEC_PER_SEC)) - } - if let start = item["start"] as? Double { - pos = CMTimeMakeWithSeconds(start, preferredTimescale: Int32(NSEC_PER_SEC)) - playitem.seek(to: pos) { finished in - } - } - return playitem - }) - - let center = NotificationCenter.default - for item in items { - center.addObserver(self, selector: #selector(newErrorLogEntry), name: .AVPlayerItemNewErrorLogEntry, object: item) - center.addObserver(self, selector: #selector(failedToPlayToEndTime), name: .AVPlayerItemFailedToPlayToEndTime, object: item) - center.addObserver(self, selector: #selector(didPlayToEndTime), name: .AVPlayerItemDidPlayToEndTime, object: item) - player.insert(item, after: nil) - } - } - func nextTrack() { - print("nextTrack ", playCount) - print(player.items()) - if playCount > 0 { - playCount -= 1 - } - let url = prevURL - if player.items().count <= 2 { - if loop || UserDefaults.standard.bool(forKey: "keepOpenWhenDone") { - loopSetup() - } - } - if player.items().count <= 1 { - DispatchQueue.main.async { - MPNowPlayingInfoCenter.default().nowPlayingInfo = nil - } - iscancel = true - playerViewController.dismiss(animated: false) { - self.finishRemoteTransportControls() - for items in self.customDelegate { - for delegate in items.value { - delegate.item?.cancel() - delegate.stream?.isLive = false + let cururl = (self.player.currentItem?.asset as? AVURLAsset)?.url + DispatchQueue.global().async { + print("nextTrack ", self.playCounts) + print(self.player.items()) + let url = self.prevURL + if self.player.items().count <= 2 { + self.enqueuePlayItem() + } + if self.player.items().count <= 1 && (!self.loop && !UserDefaults.standard.bool(forKey: "keepOpenWhenDone")) { + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + self.iscancel = true + self.playerViewController.dismiss(animated: false) { + self.finishRemoteTransportControls() + for items in self.customDelegate { + for delegate in items.value { + delegate.item?.cancel() + delegate.stream?.isLive = false + } + } + self.customDelegate.removeAll() + self.onFinish?(0) } } - self.customDelegate.removeAll() - self.onFinish?(0) } - } - else { - if let url = url, var prev = customDelegate[url] { - prev = Array(prev.dropFirst()) - if prev.count > 0 { - customDelegate[url] = prev + else { + if let url = cururl { + self.playCounts[url] = self.playCounts[url]! + 1 } - else { - customDelegate.removeValue(forKey: url) + if let url = url, var prev = self.customDelegate[url] { + prev = Array(prev.dropFirst()) + if prev.count > 0 { + self.customDelegate[url] = prev + } + else { + self.customDelegate.removeValue(forKey: url) + } + } + let c = self.playCounts.first?.value ?? 0 + if !self.loop && self.playCounts.values.allSatisfy({ $0 == c }) { + self.player.pause() } } } - if playCount == 0 { - player.pause() - playCount = playItems.count - } } @objc func didPlayToEndTime(_ notification: Notification) { @@ -531,7 +582,14 @@ class CustomPlayerView: NSObject, AVPlayerViewControllerDelegate { } } self.customDelegate.removeAll() - group.notify(queue: .main) { + group.notify(queue: .global()) { + DispatchQueue.main.asyncAfter(deadline: .now()+2) { + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + print(error) + } + } self.onFinish?(ret) } } diff --git a/ccViewer/ccViewer/Info.plist b/ccViewer/ccViewer/Info.plist index 116f362..9e29f59 100644 --- a/ccViewer/ccViewer/Info.plist +++ b/ccViewer/ccViewer/Info.plist @@ -52,6 +52,33 @@ Use for start time lock NSPhotoLibraryUsageDescription For select image to upload + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + UISceneConfigurationName + About Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + About + + + + UIBackgroundModes audio diff --git a/ccViewer/ccViewer/NavigationControllerHide.swift b/ccViewer/ccViewer/NavigationControllerHide.swift index 620e478..7fed34b 100644 --- a/ccViewer/ccViewer/NavigationControllerHide.swift +++ b/ccViewer/ccViewer/NavigationControllerHide.swift @@ -16,12 +16,12 @@ class NavigationControllerHide: UINavigationController { // Do any additional setup after loading the view. } - override var childForStatusBarStyle: UIViewController? { - return visibleViewController + override open var childForStatusBarStyle: UIViewController? { + return topViewController ?? super.childForStatusBarStyle } - override var childForStatusBarHidden: UIViewController? { - return visibleViewController + override open var childForStatusBarHidden: UIViewController? { + return topViewController ?? super.childForStatusBarHidden } /* diff --git a/ccViewer/ccViewer/SceneDelegate.swift b/ccViewer/ccViewer/SceneDelegate.swift new file mode 100644 index 0000000..ff92489 --- /dev/null +++ b/ccViewer/ccViewer/SceneDelegate.swift @@ -0,0 +1,67 @@ +// +// SceneDelegate.swift +// CryptCloudViewer +// +// Created by rei8 on 2019/11/27. +// Copyright © 2019 lithium03. All rights reserved. +// + +import UIKit + +import ffplayer +import RemoteCloud + +@available(iOS 13.0, *) +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let winScene = (scene as? UIWindowScene) else { return } + + if connectionOptions.userActivities.filter({$0.activityType == "info.lithium03.ccViewer.about"}).first != nil { + if let sizes = winScene.sizeRestrictions { + let size = CGSize(width: 400, height: 300) + sizes.minimumSize = size + sizes.maximumSize = size + } + } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + CloudFactory.shared.data.saveContext() + CloudFactory.shared.cache.saveContext() + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + FFPlayerViewController.inFocus = true + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + FFPlayerViewController.inFocus = false + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + +} + diff --git a/ccViewer/ccViewer/TableViewControllerItems.swift b/ccViewer/ccViewer/TableViewControllerItems.swift index 7797e9b..7fb1285 100644 --- a/ccViewer/ccViewer/TableViewControllerItems.swift +++ b/ccViewer/ccViewer/TableViewControllerItems.swift @@ -51,15 +51,17 @@ extension TableViewControllerItems: SortItemsDelegate { result_base = folders result_base += files - // filter - let text = navigationItem.searchController?.searchBar.text ?? "" - if text.isEmpty { - result = result_base - } else { - result = result_base.compactMap { ($0.name?.lowercased().contains(text.lowercased()) ?? false) ? $0 : nil } + DispatchQueue.main.async { + // filter + let text = self.navigationItem.searchController?.searchBar.text ?? "" + if text.isEmpty { + self.result = self.result_base + } else { + self.result = self.result_base.compactMap { ($0.name?.lowercased().contains(text.lowercased()) ?? false) ? $0 : nil } + } + + self.tableView.reloadData() } - - tableView.reloadData() } } @@ -237,15 +239,6 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, } - override var prefersStatusBarHidden: Bool { - return false - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - setNeedsStatusBarAppearanceUpdate() - } - func restoreToolbarButton() { #if !targetEnvironment(macCatalyst) let castContext = GCKCastContext.sharedInstance() @@ -297,15 +290,19 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, self.result_base = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) self.DoSort() - if UserDefaults.standard.bool(forKey: "cloudPlaypos") { - CloudFactory.shared.data.getCloudMark(storage: self.storageName, parentID: self.rootFileId) { - DispatchQueue.main.async { - self.tableView.reloadData() + DispatchQueue.global().async { + if UserDefaults.standard.bool(forKey: "cloudPlaypos") { + CloudFactory.shared.data.getCloudMark(storage: self.storageName, parentID: self.rootFileId) { + DispatchQueue.main.async { + self.tableView.reloadData() + } } } + self.editting = false + DispatchQueue.main.async { + self.tableView.reloadData() + } } - editting = false - self.tableView.reloadData() } override func didMove(toParent parent: UIViewController?) { @@ -507,9 +504,9 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, self.tableView.reloadData() if subItem { (CloudFactory.shared[storageName] as? RemoteSubItem)?.listsubitem(fileId: self.rootFileId) { - DispatchQueue.main.async { - self.result_base = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) - self.DoSort() + self.result_base = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) + self.DoSort() + DispatchQueue.global().async { if UserDefaults.standard.bool(forKey: "cloudPlaypos") { CloudFactory.shared.data.getCloudMark(storage: self.storageName, parentID: self.rootFileId) { DispatchQueue.main.async { @@ -618,9 +615,13 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, style: .default, handler:{ action in let pos = CloudFactory.shared.data.getMark(storage: item.storage, targetID: item.id) + let group = DispatchGroup() if pos != nil { - CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: nil) - let group = DispatchGroup() + group.enter() + CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: nil) { + group.leave() + } + self.activityIndicator.startAnimating() if UserDefaults.standard.bool(forKey: "cloudPlaypos") { CloudFactory.shared.data.setCloudMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: nil, group: group) @@ -631,8 +632,10 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, } } else { - CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: 0) - let group = DispatchGroup() + group.enter() + CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: 0) { + group.leave() + } self.activityIndicator.startAnimating() if UserDefaults.standard.bool(forKey: "cloudPlaypos") { CloudFactory.shared.data.setCloudMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: 0, group: group) @@ -927,23 +930,27 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, next?.rootPath = path next?.rootFileId = result[indexPath.row].id ?? "" next?.storageName = storageName - let newroot = CloudFactory.shared.data.listData(storage: storageName, parentID: next?.rootFileId ?? "") + let newroot = CloudFactory.shared.data.listData(storage: self.storageName, parentID: next?.rootFileId ?? "") if newroot.count == 0 { - activityIndicator.startAnimating() - - CloudFactory.shared[storageName]?.list(fileId: result[indexPath.row].id ?? "") { - DispatchQueue.main.async { - self.activityIndicator.stopAnimating() - self.semaphore.signal() - self.navigationController?.pushViewController(next!, animated: true) + self.activityIndicator.startAnimating() + DispatchQueue.global().async { + CloudFactory.shared[self.storageName]?.list(fileId: self.result[indexPath.row].id ?? "") { + DispatchQueue.main.async { + self.activityIndicator.stopAnimating() + self.semaphore.signal() + self.navigationController?.pushViewController(next!, animated: true) + } } } } else { - semaphore.signal() + self.semaphore.signal() self.navigationController?.pushViewController(next!, animated: true) } } + else { + semaphore.signal() + } } else if hasSubItem(name: result[indexPath.row].name) { if let path = result[indexPath.row].path { @@ -970,6 +977,9 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, self.navigationController?.pushViewController(next!, animated: true) } } + else { + semaphore.signal() + } } else { if let item = CloudFactory.shared[storageName]?.get(fileId: result[indexPath.row].id ?? "") { @@ -982,6 +992,9 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, autoDetectRun(item: item) } } + else { + semaphore.signal() + } } } @@ -1098,9 +1111,12 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, if UserDefaults.standard.bool(forKey: "cloudPlaypos") { CloudFactory.shared.data.setCloudMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: 0, group: group) } - group.notify(queue: .main) { - CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: 0) - self.tableView.reloadData() + group.notify(queue: .global()) { + CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: 0) { + DispatchQueue.main.async { + self.tableView.reloadData() + } + } } } guard let target = url else { @@ -1174,9 +1190,12 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, if UserDefaults.standard.bool(forKey: "cloudPlaypos") { CloudFactory.shared.data.setCloudMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: 0, group: group) } - group.notify(queue: .main) { - CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: 0) - self.tableView.reloadData() + group.notify(queue: .global()) { + CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: 0) { + DispatchQueue.main.async { + self.tableView.reloadData() + } + } } } guard let target = url else { @@ -1207,10 +1226,13 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, if UserDefaults.standard.bool(forKey: "cloudPlaypos") { CloudFactory.shared.data.setCloudMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: pos, group: group) } - group.notify(queue: .main) { - CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: pos) - self.activityIndicator.stopAnimating() - self.tableView.reloadData() + group.notify(queue: .global()) { + CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: pos) { + DispatchQueue.main.async { + self.activityIndicator.stopAnimating() + self.tableView.reloadData() + } + } } } } @@ -1303,11 +1325,12 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, DispatchQueue.main.async { next?.imagedata = fixedImage next?.modalPresentationStyle = .fullScreen - self.downloadProgress.dismiss(animated: false, completion: nil) - self.activityIndicator.startAnimating() - self.semaphore.signal() - self.present(next!, animated: true) { - self.activityIndicator.stopAnimating() + self.downloadProgress.dismiss(animated: false) { + self.activityIndicator.startAnimating() + self.semaphore.signal() + self.present(next!, animated: true) { + self.activityIndicator.stopAnimating() + } } } } @@ -1402,10 +1425,13 @@ class TableViewControllerItems: UITableViewController, UISearchResultsUpdating, if UserDefaults.standard.bool(forKey: "cloudPlaypos") { CloudFactory.shared.data.setCloudMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: pos, group: group) } - group.notify(queue: .main) { - CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: pos) - self.activityIndicator.stopAnimating() - self.tableView.reloadData() + group.notify(queue: .global()) { + CloudFactory.shared.data.setMark(storage: item.storage, targetID: item.id, parentID: self.rootFileId, position: pos) { + DispatchQueue.main.async { + self.activityIndicator.stopAnimating() + self.tableView.reloadData() + } + } } } } diff --git a/ccViewer/ccViewer/TableViewControllerItemsEdit.swift b/ccViewer/ccViewer/TableViewControllerItemsEdit.swift index 939ee8f..735a035 100644 --- a/ccViewer/ccViewer/TableViewControllerItemsEdit.swift +++ b/ccViewer/ccViewer/TableViewControllerItemsEdit.swift @@ -35,15 +35,17 @@ extension TableViewControllerItemsEdit: SortItemsDelegate { result_base = folders result_base += files - // filter - let text = navigationItem.searchController?.searchBar.text ?? "" - if text.isEmpty { - result = result_base - } else { - result = result_base.compactMap { ($0.name?.lowercased().contains(text.lowercased()) ?? false) ? $0 : nil } + DispatchQueue.main.async { + // filter + let text = self.navigationItem.searchController?.searchBar.text ?? "" + if text.isEmpty { + self.result = self.result_base + } else { + self.result = self.result_base.compactMap { ($0.name?.lowercased().contains(text.lowercased()) ?? false) ? $0 : nil } + } + + self.tableView.reloadData() } - - tableView.reloadData() } } @@ -123,6 +125,8 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati var selectionLabel: UILabel! var selectionButton: UIButton! + + var headerView: UIView! override func viewDidLoad() { super.viewDidLoad() @@ -175,7 +179,7 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati definesPresentationContext = true let headerCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "header")! - let headerView: UIView = headerCell.contentView + headerView = headerCell.contentView let label = headerView.viewWithTag(1) as? UILabel label?.text = rootPath tableView.tableHeaderView = headerCell @@ -237,23 +241,24 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati } } - func checkDupName(testName: String, onFinish: ((Bool)->Void)?) { + func checkDupName(testNames: [String], onFinish: (([Bool])->Void)?) { CloudFactory.shared[storageName]?.list(fileId: rootFileId) { - DispatchQueue.main.async { - self.result_base = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) - self.DoSort() - + self.result_base = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) + self.DoSort() + + var ret: [Bool] = [] + for testName in testNames { + var pass = true for item in self.result { if item.name == testName { - DispatchQueue.global().async { - onFinish?(false) - } - return + pass = false + break } } - DispatchQueue.global().async { - onFinish?(true) - } + ret += [pass] + } + DispatchQueue.global().async { + onFinish?(ret) } } } @@ -262,9 +267,9 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati self.result = [] self.tableView.reloadData() CloudFactory.shared[storageName]?.list(fileId: rootFileId) { - DispatchQueue.main.async { - self.result_base = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) - self.DoSort() + self.result_base = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) + self.DoSort() + DispatchQueue.global().async { if UserDefaults.standard.bool(forKey: "cloudPlaypos") { CloudFactory.shared.data.getCloudMark(storage: self.storageName, parentID: self.rootFileId) { DispatchQueue.main.async { @@ -321,6 +326,7 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati handler:{ action in let picker = UIDocumentPickerViewController(documentTypes: ["public.data"], in: .open) + picker.allowsMultipleSelection = true picker.delegate = self self.present(picker, animated: true, completion: nil) }) @@ -334,11 +340,11 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { return } - let picker = UIImagePickerController() - picker.mediaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary) ?? [] - picker.allowsEditing = false - picker.delegate = self DispatchQueue.main.async { + let picker = UIImagePickerController() + picker.mediaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary) ?? [] + picker.allowsEditing = false + picker.delegate = self self.present(picker, animated: true, completion: nil) } } @@ -405,8 +411,8 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati if newname == "" { return } - self.checkDupName(testName: newname) { success in - guard success else { + self.checkDupName(testNames: [newname]) { success in + guard success[0] else { return } let tmpurl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) @@ -431,12 +437,17 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati } } service.upload(parentId: self.rootFileId, sessionId: sessionId, uploadname: newname, target: tmpurl) { id in - UploadManeger.shared.UploadDone(identifier: sessionId) + if id == nil { + UploadManeger.shared.UploadFailed(identifier: sessionId, errorStr: "failed to upload") + } + else { + UploadManeger.shared.UploadDone(identifier: sessionId) + } guard id != nil, self.gone else { return } - DispatchQueue.main.asyncAfter(deadline: .now()) { + DispatchQueue.main.asyncAfter(deadline: .now()) { self.result = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) self.result_base = self.result self.tableView.reloadData() @@ -471,7 +482,65 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati guard let service = CloudFactory.shared[storageName] else { return } - if let url = urls.first { + if urls.count > 1 { + var passUrl: [URL] = [] + checkDupName(testNames: urls.map({ $0.lastPathComponent })) { pass in + for (url, ok) in zip(urls, pass) { + if ok { + passUrl += [url] + } + } + + for url in passUrl { + let newname = url.lastPathComponent + let sessionId = UUID().uuidString + let tmpurl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + UploadManeger.shared.UploadStart(identifier: sessionId, filename: newname) + + DispatchQueue.global().async { + do { + guard CFURLStartAccessingSecurityScopedResource(url as CFURL) else { + UploadManeger.shared.UploadFailed(identifier: sessionId, errorStr: "CFURLStartAccessingSecurityScopedResource") + + return + } + defer { + CFURLStopAccessingSecurityScopedResource(url as CFURL) + } + do { + if FileManager.default.fileExists(atPath: tmpurl.path) { + try FileManager.default.removeItem(at: tmpurl) + } + try FileManager.default.copyItem(at: url, to: tmpurl) + } + catch let error { + print(error) + UploadManeger.shared.UploadFailed(identifier: sessionId, errorStr: error.localizedDescription) + return + } + } + service.upload(parentId: self.rootFileId, sessionId: sessionId, uploadname: newname, target: tmpurl) { id in + if id == nil { + UploadManeger.shared.UploadFailed(identifier: sessionId, errorStr: "failed to upload") + } + else { + UploadManeger.shared.UploadDone(identifier: sessionId) + } + + guard id != nil, self.gone else { + return + } + DispatchQueue.main.asyncAfter(deadline: .now()) { + self.result = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) + self.result_base = self.result + self.tableView.reloadData() + } + } + } + } + } + } + else if let url = urls.first { let alert = UIAlertController(title: rootPath, message: upitemStr, preferredStyle: .alert) @@ -483,8 +552,8 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati if newname == "" { return } - self.checkDupName(testName: newname) { success in - guard success else { + self.checkDupName(testNames: [newname]) { success in + guard success[0] else { return } let sessionId = UUID().uuidString @@ -514,12 +583,17 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati } } service.upload(parentId: self.rootFileId, sessionId: sessionId, uploadname: newname, target: tmpurl) { id in - UploadManeger.shared.UploadDone(identifier: sessionId) + if id == nil { + UploadManeger.shared.UploadFailed(identifier: sessionId, errorStr: "failed to upload") + } + else { + UploadManeger.shared.UploadDone(identifier: sessionId) + } guard id != nil, self.gone else { return } - DispatchQueue.main.asyncAfter(deadline: .now()) { + DispatchQueue.main.asyncAfter(deadline: .now()) { self.result = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) self.result_base = self.result self.tableView.reloadData() @@ -562,8 +636,8 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati return } - self.checkDupName(testName: newname) { success in - guard success else { + self.checkDupName(testNames: [newname]) { success in + guard success[0] else { return } DispatchQueue.main.async { @@ -613,8 +687,8 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati return } self.selection = [] - self.checkDupName(testName: newname) { success in - guard success else { + self.checkDupName(testNames: [newname]) { success in + guard success[0] else { return } DispatchQueue.main.async { @@ -658,56 +732,52 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati present(alert, animated: true, completion: nil) } - + @objc func timeButtonDidTap(_ sender: UIButton) { guard let id = selection.first, let item = CloudFactory.shared[storageName]?.get(fileId: id) else { return } - let datePicker = UIDatePicker() - datePicker.datePickerMode = .dateAndTime - datePicker.setDate(item.mDate ?? Date(), animated: false) - - let alert = UIAlertController(title: "\n\n\n\n\n\n\n\n", - message: nil, - preferredStyle: .alert) - - alert.view.addSubview(datePicker) - datePicker.centerXAnchor.constraint(equalTo: alert.view.centerXAnchor).isActive = true - - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) - let defaultAction = UIAlertAction(title: "OK", - style: .default, - handler:{ action in - self.selection = [] - DispatchQueue.main.async { - self.activityIndicator.startAnimating() - } - item.changetime(newdate: datePicker.date) { id in - var result = "failed" - if id != nil { - self.result = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) - self.result_base = self.result - result = "done" - } - DispatchQueue.main.async { - self.activityIndicator.stopAnimating() - self.tableView.reloadData() - - let alert2 = UIAlertController(title: "Result", - message: "Time change \(result)", - preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default) - alert2.addAction(okAction) - - self.present(alert2, animated: true, completion: nil) - } - } - }) - alert.addAction(cancelAction) - alert.addAction(defaultAction) - - present(alert, animated: true, completion: nil) + let contentVC = DatePickerPopupView() + contentVC.modalPresentationStyle = .popover + contentVC.preferredContentSize = CGSize(width: 350, height: 200) + contentVC.popoverPresentationController?.sourceRect = sender.bounds + contentVC.popoverPresentationController?.sourceView = sender + contentVC.popoverPresentationController?.permittedArrowDirections = .any + contentVC.popoverPresentationController?.delegate = self + contentVC.targetDate = item.mDate ?? Date() + contentVC.didFinish = { newDate in + guard let newDate = newDate else { + return + } + if newDate == item.mDate { + return + } + DispatchQueue.main.async { + self.activityIndicator.startAnimating() + } + item.changetime(newdate: newDate) { id in + var result = "failed" + if id != nil { + self.result = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) + self.result_base = self.result + result = "done" + } + DispatchQueue.main.async { + self.activityIndicator.stopAnimating() + self.tableView.reloadData() + + let alert2 = UIAlertController(title: "Result", + message: "Time change \(result)", + preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default) + alert2.addAction(okAction) + + self.present(alert2, animated: true, completion: nil) + } + } + } + present(contentVC, animated: true, completion: nil) } @objc func moveButtonDidTap(_ sender: UIButton) { @@ -745,21 +815,19 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati } } } - group.notify(queue: .global()) { + group.notify(queue: .main) { self.result = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) self.result_base = self.result - DispatchQueue.main.async { - self.activityIndicator.stopAnimating() - self.tableView.reloadData() - - let alert2 = UIAlertController(title: "Result", - message: "Move \(scount)/\(items.count) items", - preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default) - alert2.addAction(okAction) - - self.present(alert2, animated: true, completion: nil) - } + self.activityIndicator.stopAnimating() + self.tableView.reloadData() + + let alert2 = UIAlertController(title: "Result", + message: "Move \(scount)/\(items.count) items", + preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default) + alert2.addAction(okAction) + + self.present(alert2, animated: true, completion: nil) } } self.navigationController?.pushViewController(root, animated: true) @@ -794,21 +862,19 @@ class TableViewControllerItemsEdit: UITableViewController, UISearchResultsUpdati group.leave() } } - group.notify(queue: .global()) { + group.notify(queue: .main) { self.result = CloudFactory.shared.data.listData(storage: self.storageName, parentID: self.rootFileId) self.result_base = self.result - DispatchQueue.main.async { - self.activityIndicator.stopAnimating() - self.tableView.reloadData() - - let alert2 = UIAlertController(title: "Result", - message: "Delete \(scount)/\(items.count) items", - preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default) - alert2.addAction(okAction) + self.activityIndicator.stopAnimating() + self.tableView.reloadData() + + let alert2 = UIAlertController(title: "Result", + message: "Delete \(scount)/\(items.count) items", + preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default) + alert2.addAction(okAction) - self.present(alert2, animated: true, completion: nil) - } + self.present(alert2, animated: true, completion: nil) } }) alert.addAction(cancelAction) @@ -1089,3 +1155,29 @@ class ViewControllerPathSelect: UIViewController, UITableViewDelegate, UITableVi onDone(rootFileId) } } + +class DatePickerPopupView: UIViewController { + var targetDate = Date() + var didFinish: ((Date?)->Void)? + + var datePicker: UIDatePicker! + + override func viewDidLoad() { + super.viewDidLoad() + + if #available(iOS 13.0, *) { + view.backgroundColor = .systemBackground + } else { + view.backgroundColor = .white + } + + datePicker = UIDatePicker() + datePicker.datePickerMode = .dateAndTime + datePicker.setDate(targetDate, animated: false) + view.addSubview(datePicker) + } + + override func viewWillDisappear(_ animated: Bool) { + didFinish?(datePicker.date) + } +} diff --git a/ccViewer/ccViewer/TableViewControllerPlaylist.swift b/ccViewer/ccViewer/TableViewControllerPlaylist.swift index 3e14f44..048f1be 100644 --- a/ccViewer/ccViewer/TableViewControllerPlaylist.swift +++ b/ccViewer/ccViewer/TableViewControllerPlaylist.swift @@ -99,15 +99,6 @@ class TableViewControllerPlaylist: UITableViewController, UISearchResultsUpdatin } } - override var prefersStatusBarHidden: Bool { - return false - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - setNeedsStatusBarAppearanceUpdate() - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) diff --git a/ccViewer/ccViewer/TableViewControllerSetting.swift b/ccViewer/ccViewer/TableViewControllerSetting.swift index 94c5411..895af1d 100644 --- a/ccViewer/ccViewer/TableViewControllerSetting.swift +++ b/ccViewer/ccViewer/TableViewControllerSetting.swift @@ -105,7 +105,8 @@ class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { NSLocalizedString("Cache limit", comment: "")], [NSLocalizedString("View online help", comment: ""), NSLocalizedString("View privacy policy", comment: ""), - NSLocalizedString("Version", comment: "")] + NSLocalizedString("Version", comment: ""), + NSLocalizedString("About", comment: "")] ] // MARK: - Table view data source @@ -304,7 +305,7 @@ class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { } case 10: switch indexPath.row { - case 0...1: + case 0...1,3: cell.accessoryType = .detailButton case 2: cell.detailTextLabel?.text = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String @@ -417,6 +418,10 @@ class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { case 1: let url = URL(string: NSLocalizedString("Privacy policy URL", comment: ""))! UIApplication.shared.open(url) + case 3: + let storyboardAbout = UIStoryboard(name: "About", bundle: nil) + let next = storyboardAbout.instantiateViewController(withIdentifier: "AboutView") + self.present(next, animated: false) default: break } diff --git a/ccViewer/ccViewer/ViewControllerAbout.swift b/ccViewer/ccViewer/ViewControllerAbout.swift new file mode 100644 index 0000000..13940e9 --- /dev/null +++ b/ccViewer/ccViewer/ViewControllerAbout.swift @@ -0,0 +1,51 @@ +// +// ViewControllerAbout.swift +// CryptCloudViewer +// +// Created by rei8 on 2019/11/27. +// Copyright © 2019 lithium03. All rights reserved. +// + +import UIKit + +class ViewControllerAbout: UIViewController { + + @IBOutlet weak var textVersion: UILabel! + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + let version = "\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "")" + let build = "\(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "")" + let appname = "\(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "")" + textVersion.text = "\(appname)\n\nVersion \(version)(\(build))" + } + + @IBAction func tapOK(_ sender: UIButton) { + dismiss(animated: true) { + #if targetEnvironment(macCatalyst) + guard let session = self.view.window?.windowScene?.session else { + return + } + let options = UIWindowSceneDestructionRequestOptions() + options.windowDismissalAnimation = .standard + UIApplication.shared.requestSceneSessionDestruction(session, options: options) + #endif + } + } + + @IBAction func backToTop(segue: UIStoryboardSegue) { + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/ccViewer/ccViewer/ViewControllerText.swift b/ccViewer/ccViewer/ViewControllerText.swift index a1bdc3c..36067b7 100644 --- a/ccViewer/ccViewer/ViewControllerText.swift +++ b/ccViewer/ccViewer/ViewControllerText.swift @@ -61,6 +61,8 @@ class ViewControllerText: UIViewController, UIPickerViewDelegate, UIPickerViewDa activityIndicator.translatesAutoresizingMaskIntoConstraints = false activityIndicator.widthAnchor.constraint(equalToConstant: 100).isActive = true activityIndicator.heightAnchor.constraint(equalToConstant: 100).isActive = true + activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true } override func viewWillDisappear(_ animated: Bool) { diff --git a/ccViewer/ccViewer/ja.lproj/Localizable.strings b/ccViewer/ccViewer/ja.lproj/Localizable.strings index e7ec545..f9caf3b 100644 --- a/ccViewer/ccViewer/ja.lproj/Localizable.strings +++ b/ccViewer/ccViewer/ja.lproj/Localizable.strings @@ -5,6 +5,8 @@ Created by rei6 on 2019/03/19. Copyright © 2019 lithium03. All rights reserved. */ +"About CryptCloudViewer" = "CryptCloudViewerについて"; + "Require Authentication" = "認証が必要です"; "Authentication failed" = "認証失敗しました"; "Require password" = "パスワードが必要です"; diff --git a/ffconverter/ffconverter/encoder.swift b/ffconverter/ffconverter/encoder.swift index d9ba1fc..2c3f146 100644 --- a/ffconverter/ffconverter/encoder.swift +++ b/ffconverter/ffconverter/encoder.swift @@ -35,6 +35,7 @@ class Encoder { let sound_fq = 48000.0 let ch: Int + let bufferQueue: DispatchQueue var lpcmToAACConverter: AVAudioConverter? = nil var sound_buffer = Data() var sound_RDBs: [Data] = [] @@ -42,6 +43,7 @@ class Encoder { init(channel: Int) { ch = channel + bufferQueue = DispatchQueue(label: "bufferQueue \(channel)") } func process_sound(writer: TS_writer?, final: Bool = false) { @@ -67,30 +69,35 @@ class Encoder { } let outBuffer = AVAudioCompressedBuffer(format: outputFormat, packetCapacity: 1, maximumPacketSize: 1024 * 2) - let inputBlock : AVAudioConverterInputBlock = { (inNumPackets, outStatus) -> AVAudioBuffer? in + let inputBlock : AVAudioConverterInputBlock = { [weak self] (inNumPackets, outStatus) -> AVAudioBuffer? in outStatus.pointee = .noDataNow - let sample_size = MemoryLayout.size * 2 - if self.sound_buffer.count <= (final ? 0 : sample_size * 1024) { + guard let self = self else { return nil } - var samples = self.sound_buffer.count / sample_size - if samples > 1024 { - samples = 1024 - } - guard let inputBuffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: AVAudioFrameCount(samples)) else { - return nil - } - guard let target = inputBuffer.floatChannelData else { - return nil - } - let _ = self.sound_buffer.withUnsafeBytes { wav_data in - memcpy(target.pointee, wav_data.baseAddress, samples*sample_size) + return self.bufferQueue.sync { + let sample_size = MemoryLayout.size * 2 + if self.sound_buffer.count <= (final ? 0 : sample_size * 1024) { + return nil + } + var samples = self.sound_buffer.count / sample_size + if samples > 1024 { + samples = 1024 + } + guard let inputBuffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: AVAudioFrameCount(samples)) else { + return nil + } + guard let target = inputBuffer.floatChannelData else { + return nil + } + let _ = self.sound_buffer.withUnsafeBytes { wav_data in + memcpy(target.pointee, wav_data.baseAddress, samples*sample_size) + } + self.sound_buffer = self.sound_buffer.subdata(in: (samples*sample_size).., pts: Double) { - audios[channel].sound_buffer.append(pcm_data) - let sample_size = Double(MemoryLayout.size) - audios[channel].sound_pts = pts - Double(audios[channel].sound_buffer.count)/(sample_size*2)/audios[channel].sound_fq - if audios[channel].sound_buffer.count < Int(0.1*sample_size*2*audios[channel].sound_fq) { - return + let group = DispatchGroup() + group.enter() + audios[channel].bufferQueue.async { + defer { + group.leave() + } + self.audios[channel].sound_buffer.append(pcm_data) + let sample_size = Double(MemoryLayout.size) + self.audios[channel].sound_pts = pts - Double(self.audios[channel].sound_buffer.count)/(sample_size*2)/self.audios[channel].sound_fq + if self.audios[channel].sound_buffer.count < Int(0.1*sample_size*2*self.audios[channel].sound_fq) { + return + } } + group.wait() audios[channel].process_sound(writer: writer[channel+1]) } diff --git a/ffplayer/ffplayer/player.swift b/ffplayer/ffplayer/player.swift index b2e4472..8b759a8 100644 --- a/ffplayer/ffplayer/player.swift +++ b/ffplayer/ffplayer/player.swift @@ -16,6 +16,14 @@ import RemoteCloud public class Player { public class func play(parent: UIViewController, item: RemoteItem, start: Double?, onFinish: @escaping (Double?)->Void) { DispatchQueue.main.async { + UIApplication.shared.beginReceivingRemoteControlEvents() + do { + try AVAudioSession.sharedInstance().setCategory(.playback) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + print(error) + } + let bridge = StreamBridge(item: item, name: item.name, count: 1) let skip = UserDefaults.standard.integer(forKey: "playStartSkipSec") let stop = UserDefaults.standard.integer(forKey: "playStopAfterSec") @@ -76,6 +84,11 @@ public class Player { } } bridge.run(parent: parent) { ret, pos in + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + print(error) + } if ret >= 0 { onFinish(pos) } @@ -87,6 +100,14 @@ public class Player { } public class func play(parent: UIViewController, items: [RemoteItem], shuffle: Bool, loop: Bool, onFinish: @escaping (Bool)->Void) { + UIApplication.shared.beginReceivingRemoteControlEvents() + do { + try AVAudioSession.sharedInstance().setCategory(.playback) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + print(error) + } + var playItems = items; if shuffle { playItems.shuffle() @@ -120,10 +141,20 @@ public class Player { cont = true } else { + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + print(error) + } onFinish(true) } } else { + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + print(error) + } onFinish(false) } } @@ -448,15 +479,17 @@ class StreamBridge { basename = components.joined(separator: ".") } - if let imageitem = CloudFactory.shared.data.getImage(storage: remote.storage, parentId: parentId, baseName: basename) { - if let imagestream = CloudFactory.shared[remote.storage]?.get(fileId: imageitem.id ?? "")?.open() { - imagestream.read(position: 0, length: Int(imageitem.size)) { data in - if let data = data, let image = UIImage(data: data) { - self.image = MPMediaItemArtwork(boundsSize: image.size) { size in - return image - } - DispatchQueue.main.async { - self.player.artworkView.image = image + if let imageitem = CloudFactory.shared.data.getImage(storage: self.remote.storage, parentId: parentId, baseName: basename) { + DispatchQueue.global().async { + if let imagestream = CloudFactory.shared[self.remote.storage]?.get(fileId: imageitem.id ?? "")?.open() { + imagestream.read(position: 0, length: Int(imageitem.size)) { data in + if let data = data, let image = UIImage(data: data) { + self.image = MPMediaItemArtwork(boundsSize: image.size) { size in + return image + } + DispatchQueue.main.async { + self.player.artworkView.image = image + } } } } @@ -581,12 +614,14 @@ class StreamBridge { self.finishRemoteTransportControls() MPNowPlayingInfoCenter.default().nowPlayingInfo = nil } - if self.player.possition >= self.player.totalTime - 1 { + if self.player.possition >= self.player.totalTime - 3 { onFinish(ret, 0) } else { onFinish(ret, self.player.possition) } + self.stream.isLive = false + self.remote.cancel() } } }