瀏覽代碼

refactor(ios): use URLSession to download and upload

Andelf 3 年之前
父節點
當前提交
ff2063b2b4
共有 2 個文件被更改,包括 257 次插入144 次删除
  1. 113 80
      ios/App/App/FileSync/FileSync.swift
  2. 144 64
      ios/App/App/FileSync/SyncClient.swift

+ 113 - 80
ios/App/App/FileSync/FileSync.swift

@@ -22,10 +22,9 @@ var ENCRYPTION_SECRET_KEY: String? = nil
 var ENCRYPTION_PUBLIC_KEY: String? = nil
 var FNAME_ENCRYPTION_KEY: Data? = nil
 
+let FileSyncErrorDomain = "com.logseq.app.FileSyncErrorDomain"
 
 // MARK: Helpers
-
-
 @inline(__always) func fnameEncryptionEnabled() -> Bool {
     guard let _ = FNAME_ENCRYPTION_KEY else {
         return false
@@ -73,7 +72,7 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
     var size: Int
     var ctime: Int64
     var mtime: Int64
-
+    
     public init?(of fileURL: URL) {
         do {
             let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey, .contentModificationDateKey,
@@ -84,7 +83,7 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
             size = fileAttributes.fileSize ?? 0
             mtime = Int64((fileAttributes.contentModificationDate?.timeIntervalSince1970 ?? 0.0) * 1000)
             ctime = Int64((fileAttributes.creationDate?.timeIntervalSince1970 ?? 0.0) * 1000)
-
+            
             // incremental MD5 checksum
             let bufferSize = 512 * 1024
             let file = try FileHandle(forReadingFrom: fileURL)
@@ -101,14 +100,14 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
                     return false // eof
                 }
             }) {}
-
+            
             let computed = ctx.finalize()
             md5 = computed.map { String(format: "%02hhx", $0) }.joined()
         } catch {
             return nil
         }
     }
-
+    
     public var description: String {
         return "SyncMetadata(md5=\(md5), size=\(size), mtime=\(mtime))"
     }
@@ -120,7 +119,7 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
 public class FileSync: CAPPlugin, SyncDebugDelegate {
     override public func load() {
         print("debug FileSync iOS plugin loaded!")
-
+        
         AWSMobileClient.default().initialize { (userState, error) in
             guard error == nil else {
                 print("error initializing AWSMobileClient. Error: \(error!.localizedDescription)")
@@ -128,18 +127,18 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             }
         }
     }
-
+    
     // NOTE: for debug, or an activity indicator
     public func debugNotification(_ message: [String: Any]) {
         self.notifyListeners("debug", data: message)
     }
-
+    
     @objc func keygen(_ call: CAPPluginCall) {
         let (secretKey, publicKey) = AgeEncryption.keygen()
         call.resolve(["secretKey": secretKey,
                       "publicKey": publicKey])
     }
-
+    
     @objc func setKey(_ call: CAPPluginCall) {
         let secretKey = call.getString("secretKey")
         let publicKey = call.getString("publicKey")
@@ -156,16 +155,16 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
         ENCRYPTION_SECRET_KEY = secretKey
         ENCRYPTION_PUBLIC_KEY = publicKey
         FNAME_ENCRYPTION_KEY = AgeEncryption.toRawX25519Key(secretKey)
-
+        
     }
-
+    
     @objc func setEnv(_ call: CAPPluginCall) {
         guard let env = call.getString("env") else {
             call.reject("required parameter: env")
             return
         }
         self.setKey(call)
-
+        
         switch env {
         case "production", "product", "prod":
             URL_BASE = URL(string: "https://api.logseq.com/file-sync/")!
@@ -179,11 +178,11 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.reject("invalid env: \(env)")
             return
         }
-
+        
         self.debugNotification(["event": "setenv:\(env)"])
         call.resolve(["ok": true])
     }
-
+    
     @objc func encryptFnames(_ call: CAPPluginCall) {
         guard fnameEncryptionEnabled() else {
             call.reject("fname encryption key not set")
@@ -193,7 +192,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.reject("required parameters: filePaths")
             return
         }
-
+        
         let nFiles = fnames.count
         fnames = fnames.compactMap { $0.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) }
         if fnames.count != nFiles {
@@ -201,7 +200,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
         }
         call.resolve(["value": fnames])
     }
-
+    
     @objc func decryptFnames(_ call: CAPPluginCall) {
         guard fnameEncryptionEnabled() else {
             call.reject("fname encryption key not set")
@@ -259,7 +258,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             }
         }
     }
-
+    
     @objc func getLocalFilesMeta(_ call: CAPPluginCall) {
         // filePaths are url encoded
         guard let basePath = call.getString("basePath"),
@@ -271,7 +270,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.reject("invalid basePath")
             return
         }
-
+        
         var fileMetadataDict: [String: [String: Any]] = [:]
         for percentFilePath in filePaths {
             let filePath = percentFilePath.removingPercentEncoding!
@@ -283,24 +282,24 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                 if fnameEncryptionEnabled() {
                     metaObj["encryptedFname"] = filePath.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!)
                 }
-
+                
                 fileMetadataDict[percentFilePath] = metaObj
             }
         }
-
+        
         call.resolve(["result": fileMetadataDict])
     }
-
+    
     @objc func getLocalAllFilesMeta(_ call: CAPPluginCall) {
         guard let basePath = call.getString("basePath"),
               let baseURL = URL(string: basePath) else {
                   call.reject("invalid basePath")
                   return
               }
-
+        
         var fileMetadataDict: [String: [String: Any]] = [:]
         if let enumerator = FileManager.default.enumerator(at: baseURL, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsPackageDescendants, .skipsHiddenFiles]) {
-
+            
             for case let fileURL as URL in enumerator {
                 if !fileURL.isSkipped() {
                     if let meta = SyncMetadata(of: fileURL) {
@@ -320,8 +319,8 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
         }
         call.resolve(["result": fileMetadataDict])
     }
-
-
+    
+    
     @objc func renameLocalFile(_ call: CAPPluginCall) {
         guard let basePath = call.getString("basePath"),
               let baseURL = URL(string: basePath) else {
@@ -336,10 +335,10 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.reject("invalid to file")
             return
         }
-
+        
         let fromUrl = baseURL.appendingPathComponent(from.removingPercentEncoding!)
         let toUrl = baseURL.appendingPathComponent(to.removingPercentEncoding!)
-
+        
         do {
             try FileManager.default.moveItem(at: fromUrl, to: toUrl)
         } catch {
@@ -347,23 +346,23 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             return
         }
         call.resolve(["ok": true])
-
+        
     }
-
+    
     @objc func deleteLocalFiles(_ call: CAPPluginCall) {
         guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
               let filePaths = call.getArray("filePaths") as? [String] else {
                   call.reject("required paremeters: basePath, filePaths")
                   return
               }
-
+        
         for filePath in filePaths {
             let fileUrl = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
             try? FileManager.default.removeItem(at: fileUrl) // ignore any delete errors
         }
         call.resolve(["ok": true])
     }
-
+    
     /// remote -> local
     @objc func updateLocalFiles(_ call: CAPPluginCall) {
         guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
@@ -373,7 +372,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                   call.reject("required paremeters: basePath, filePaths, graphUUID, token")
                   return
               }
-
+        
         // [encrypted-fname: original-fname]
         var encryptedFilePathDict: [String: String] = [:]
         if fnameEncryptionEnabled() {
@@ -387,49 +386,77 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
         } else {
             encryptedFilePathDict = Dictionary(uniqueKeysWithValues: filePaths.map { ($0, $0) })
         }
-
+        
         let encryptedFilePaths = Array(encryptedFilePathDict.keys)
-
+        
         let client = SyncClient(token: token, graphUUID: graphUUID)
         client.delegate = self // receives notification
-
+        
         client.getFiles(at: encryptedFilePaths) {  (fileURLs, error) in
-            if let error = error {
-                print("debug getFiles error \(error)")
+            guard error == nil else {
+                print("debug getFiles error \(String(describing: error))")
                 self.debugNotification(["event": "download:error", "data": ["message": "error while getting files \(filePaths)"]])
-                call.reject(error.localizedDescription)
-            } else {
-                // handle multiple completionHandlers
-                let group = DispatchGroup()
-
-                var downloaded: [String] = []
-
-                for (encryptedFilePath, remoteFileURL) in fileURLs {
-                    group.enter()
-
-                    let filePath = encryptedFilePathDict[encryptedFilePath]!
-                    // NOTE: fileURLs from getFiles API is percent-encoded
-                    let localFileURL = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
-                    remoteFileURL.download(toFile: localFileURL) {error in
-                        if let error = error {
-                            self.debugNotification(["event": "download:error", "data": ["message": "error while downloading \(filePath): \(error)"]])
-                            print("debug download \(error) in \(filePath)")
-                        } else {
+                call.reject(error!.localizedDescription)
+                return
+            }
+            // handle multiple completionHandlers
+            let group = DispatchGroup()
+            
+            var downloaded: [String] = []
+            
+            for (encryptedFilePath, remoteFileURL) in fileURLs {
+                group.enter()
+                
+                let filePath = encryptedFilePathDict[encryptedFilePath]!
+                // NOTE: fileURLs from getFiles API is percent-encoded
+                let localFileURL = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
+                
+                let progressHandler = {(fraction: Double) in
+                    self.debugNotification(["event": "download:progress",
+                                            "data": ["filePath": filePath,
+                                                     "fraction": fraction]])
+                }
+                
+                client.download(url: remoteFileURL, progressHandler: progressHandler) {result in
+                    switch result {
+                    case .failure(let error):
+                        self.debugNotification(["event": "download:error", "data": ["message": "error while downloading \(filePath): \(error)"]])
+                        print("debug download \(error) in \(filePath)")
+                    case .success(let tempURL):
+                        do {
+                            // Remove any existing document at file
+                            if FileManager.default.fileExists(atPath: localFileURL.path) {
+                                try FileManager.default.removeItem(at: localFileURL)
+                            } else {
+                                let parentURL = localFileURL.deletingLastPathComponent()
+                                try FileManager.default.createDirectory(at: parentURL, withIntermediateDirectories: true, attributes: nil)
+                            }
+                            let rawData = try Data(contentsOf: tempURL!)
+                            guard let decryptedRawData = maybeDecrypt(rawData) else {
+                                throw NSError(domain: FileSyncErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey: "can not decrypt downloaded file"])
+                            }
+                            try decryptedRawData.write(to: localFileURL, options: .atomic)
+                            
                             self.debugNotification(["event": "download:file", "data": ["file": filePath]])
+                            
                             downloaded.append(filePath)
+                        } catch {
+                            // Handle potential file system errors
+                            self.debugNotification(["event": "download:error", "data": ["message": "error while downloading \(filePath): \(error)"]])
+                            print("debug download \(error) in \(filePath)")
                         }
-                        group.leave()
                     }
+                    
+                    group.leave()
                 }
-                group.notify(queue: .main) {
-                    self.debugNotification(["event": "download:done"])
-                    call.resolve(["ok": true, "data": downloaded])
-                }
-
+            }
+            group.notify(queue: .main) {
+                self.debugNotification(["event": "download:done"])
+                call.resolve(["ok": true, "data": downloaded])
             }
         }
     }
-
+    
     @objc func updateLocalVersionFiles(_ call: CAPPluginCall) {
         guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
               let filePaths = call.getArray("filePaths") as? [String],
@@ -440,7 +467,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
               }
         let client = SyncClient(token: token, graphUUID: graphUUID)
         client.delegate = self // receives notification
-
+        
         client.getVersionFiles(at: filePaths) {  (fileURLDict, error) in
             if let error = error {
                 print("debug getVersionFiles error \(error)")
@@ -449,12 +476,12 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             } else {
                 // handle multiple completionHandlers
                 let group = DispatchGroup()
-
+                
                 var downloaded: [String] = []
-
+                
                 for (filePath, remoteFileURL) in fileURLDict {
                     group.enter()
-
+                    
                     // NOTE: fileURLs from getFiles API is percent-encoded
                     let localFileURL = baseURL.appendingPathComponent("logseq/version-files/").appendingPathComponent(filePath.removingPercentEncoding!)
                     remoteFileURL.download(toFile: localFileURL) {error in
@@ -472,11 +499,11 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                     self.debugNotification(["event": "version-download:done"])
                     call.resolve(["ok": true, "data": downloaded])
                 }
-
+                
             }
         }
     }
-
+    
     // filePaths: Encrypted file paths
     @objc func deleteRemoteFiles(_ call: CAPPluginCall) {
         guard var filePaths = call.getArray("filePaths") as? [String],
@@ -490,7 +517,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.reject("empty filePaths")
             return
         }
-
+        
         let nFiles = filePaths.count
         if fnameEncryptionEnabled() {
             filePaths = filePaths.compactMap { $0.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) }
@@ -498,7 +525,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
         if filePaths.count != nFiles {
             call.reject("cannot encrypt all file names")
         }
-
+        
         let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
         client.deleteFiles(filePaths) { txid, error in
             guard error == nil else {
@@ -512,7 +539,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             call.resolve(["ok": true, "txid": txid])
         }
     }
-
+    
     /// local -> remote
     @objc func updateRemoteFiles(_ call: CAPPluginCall) {
         guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
@@ -524,14 +551,14 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                   return
               }
         let fnameEncryption = call.getBool("fnameEncryption") ?? false // default to false
-
+        
         guard !filePaths.isEmpty else {
             return call.reject("empty filePaths")
         }
-
+        
         let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
         client.delegate = self
-
+        
         // 1. refresh_temp_credential
         client.getTempCredential() { (credentials, error) in
             guard error == nil else {
@@ -539,16 +566,21 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                 call.reject("error(getTempCredential): \(error!)")
                 return
             }
-
+            
             var files: [String: URL] = [:]
             for filePath in filePaths {
                 // NOTE: filePath from js may contain spaces
                 let fileURL = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
                 files[filePath] = fileURL
             }
-
+            
             // 2. upload_temp_file
-            client.uploadTempFiles(files, credentials: credentials!) { (uploadedFileKeyDict, fileMd5Dict, error) in
+            let progressHandler = {(filePath: String, fraction: Double) in
+                self.debugNotification(["event": "upload:progress",
+                                        "data": ["filePath": filePath,
+                                                 "fraction": fraction]])
+            }
+            client.uploadTempFiles(files, credentials: credentials!, progressHandler: progressHandler) { (uploadedFileKeyDict, fileMd5Dict, error) in
                 guard error == nil else {
                     self.debugNotification(["event": "upload:error", "data": ["message": "error while uploading temp files: \(error!)"]])
                     call.reject("error(uploadTempFiles): \(error!)")
@@ -560,10 +592,10 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                     call.reject("no file to update")
                     return
                 }
-
+                
                 // encrypted-file-name: (file-key, md5)
                 var uploadedFileKeyMd5Dict: [String: [String]] = [:]
-
+                
                 if fnameEncryptionEnabled() && fnameEncryption {
                     for (filePath, fileKey) in uploadedFileKeyDict {
                         guard let encryptedFilePath = filePath.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) else {
@@ -593,4 +625,5 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             }
         }
     }
+    
 }

+ 144 - 64
ios/App/App/FileSync/SyncClient.swift

@@ -5,6 +5,7 @@
 //  Created by Mono Wang on 4/8/R4.
 //
 
+import os
 import Foundation
 import AWSMobileClient
 import AWSS3
@@ -13,7 +14,6 @@ public protocol SyncDebugDelegate {
     func debugNotification(_ message: [String: Any])
 }
 
-
 public class SyncClient {
     private var token: String
     private var graphUUID: String?
@@ -36,7 +36,7 @@ public class SyncClient {
         self.graphUUID = graphUUID
         self.txid = txid
     }
-
+    
     // get_files
     // => file_path, file_url
     public func getFiles(at filePaths: [String], completionHandler: @escaping ([String: URL], Error?) -> Void) {
@@ -66,7 +66,7 @@ public class SyncClient {
             
             if (response as? HTTPURLResponse)?.statusCode != 200 {
                 let body = String(data: data!, encoding: .utf8) ?? "";
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
+                completionHandler([:], NSError(domain: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
                 return
             }
             
@@ -77,7 +77,7 @@ public class SyncClient {
                 completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
             } else {
                 // Handle unexpected error
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
+                completionHandler([:], NSError(domain: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
             }
         }
         task.resume()
@@ -110,7 +110,7 @@ public class SyncClient {
             
             if (response as? HTTPURLResponse)?.statusCode != 200 {
                 let body = String(data: data!, encoding: .utf8) ?? "";
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
+                completionHandler([:], NSError(domain: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
                 return
             }
             
@@ -121,7 +121,7 @@ public class SyncClient {
                 completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
             } else {
                 // Handle unexpected error
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
+                completionHandler([:], NSError(domain: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
             }
         }
         task.resume()
@@ -159,7 +159,7 @@ public class SyncClient {
                 
                 if response.statusCode == 409 {
                     if body.contains("txid_to_validate") {
-                        completionHandler(nil, NSError(domain: "",
+                        completionHandler(nil, NSError(domain: FileSyncErrorDomain,
                                                        code: 409,
                                                        userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
                         return
@@ -167,7 +167,7 @@ public class SyncClient {
                     // fallthrough
                 }
                 if response.statusCode != 200 {
-                    completionHandler(nil, NSError(domain: "",
+                    completionHandler(nil, NSError(domain: FileSyncErrorDomain,
                                                    code: response.statusCode,
                                                    userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
                     return
@@ -185,7 +185,7 @@ public class SyncClient {
                 }
             } else {
                 // Handle unexpected error
-                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
+                completionHandler(nil, NSError(domain: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
             }
         }
         task.resume()
@@ -224,7 +224,7 @@ public class SyncClient {
                 
                 if response.statusCode == 409 {
                     if body.contains("txid_to_validate") {
-                        completionHandler(nil, NSError(domain: "",
+                        completionHandler(nil, NSError(domain: FileSyncErrorDomain,
                                                        code: 409,
                                                        userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
                         return
@@ -232,7 +232,7 @@ public class SyncClient {
                     // fallthrough
                 }
                 if response.statusCode != 200 {
-                    completionHandler(nil, NSError(domain: "",
+                    completionHandler(nil, NSError(domain: FileSyncErrorDomain,
                                                    code: response.statusCode,
                                                    userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
                     return
@@ -244,11 +244,11 @@ public class SyncClient {
                 if resp?.UpdateFailedFiles.isEmpty ?? true {
                     completionHandler(resp?.TXId, nil)
                 } else {
-                    completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "update fail for some files: \(resp?.UpdateFailedFiles.debugDescription)"]))
+                    completionHandler(nil, NSError(domain: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "update fail for some files: \(resp?.UpdateFailedFiles.debugDescription)"]))
                 }
             } else {
                 // Handle unexpected error
-                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
+                completionHandler(nil, NSError(domain: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
             }
         }
         task.resume()
@@ -272,11 +272,11 @@ public class SyncClient {
             if let response = response as? HTTPURLResponse {
                 let body = String(data: data!, encoding: .utf8) ?? ""
                 if response.statusCode == 401 {
-                    completionHandler(nil, NSError(domain: "", code: 401, userInfo: [NSLocalizedDescriptionKey: "unauthorized"]))
+                    completionHandler(nil, NSError(domain: FileSyncErrorDomain, code: 401, userInfo: [NSLocalizedDescriptionKey: "unauthorized"]))
                     return
                 }
                 if response.statusCode != 200 {
-                    completionHandler(nil, NSError(domain: "",
+                    completionHandler(nil, NSError(domain: FileSyncErrorDomain,
                                                    code: response.statusCode,
                                                    userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
                     return
@@ -290,16 +290,22 @@ public class SyncClient {
                 completionHandler(resp?.Credentials, nil)
             } else {
                 // Handle unexpected error
-                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
+                completionHandler(nil, NSError(domain: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
             }
         }
         task.resume()
     }
     
     // [filePath, Key]
-    public func uploadTempFiles(_ files: [String: URL], credentials: S3Credential, completionHandler: @escaping ([String: String], [String: String], Error?) -> Void) {
+    public func uploadTempFiles(_ files: [String: URL],
+                                credentials: S3Credential,
+                                // key, fraction
+                                progressHandler: @escaping ((String, Double) -> Void),
+                                completionHandler: @escaping ([String: String], [String: String], Error?) -> Void)
+    {
         let credentialsProvider = AWSBasicSessionCredentialsProvider(
             accessKey: credentials.AccessKeyId, secretKey: credentials.SecretKey, sessionToken: credentials.SessionToken)
+        
         var region = AWSRegionType.USEast2
         if REGION == "us-east-2" {
             region = .USEast2
@@ -311,71 +317,145 @@ public class SyncClient {
         configuration?.timeoutIntervalForRequest = 5.0
         configuration?.timeoutIntervalForResource = 5.0
         
-        let tuConf = AWSS3TransferUtilityConfiguration()
-        tuConf.bucket = BUCKET
-        //x tuConf.isAccelerateModeEnabled = true
-        
-        let transferKey = String.random(length: 10)
-        AWSS3TransferUtility.register(
-            with: configuration!,
-            transferUtilityConfiguration: tuConf,
-            forKey: transferKey
-        ) { (error) in
-            if let error = error {
-                print("error while register tu \(error)")
-            }
-        }
-        
-        let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: transferKey)
-        let uploadExpression = AWSS3TransferUtilityUploadExpression()
-        
         let group = DispatchGroup()
         var keyFileDict: [String: String] = [:]
         var fileKeyDict: [String: String] = [:]
         var fileMd5Dict: [String: String] = [:]
         
-        let uploadCompletionHandler = { (task: AWSS3TransferUtilityUploadTask, error: Error?) -> Void in
-            // ignore any errors in first level of handler
-            if let error = error {
-                self.delegate?.debugNotification(["event": "upload:error", "data": ["key": task.key, "error": error.localizedDescription]])
-            }
-            if let HTTPResponse = task.response {
-                if HTTPResponse.statusCode != 200 || task.status != .completed {
-                    print("debug uploading error \(HTTPResponse)")
-                }
-            }
-            
-            // only save successful keys
-            let filePath = keyFileDict[task.key]!
-            fileKeyDict[filePath] = task.key
-            keyFileDict.removeValue(forKey: task.key)
-            self.delegate?.debugNotification(["event": "upload:file", "data": ["file": filePath, "key": task.key]])
-            group.leave() // notify finish upload
-        }
         
         for (filePath, fileLocalURL) in files {
-            print("debug, upload temp \(fileLocalURL) \(filePath)")
             guard let rawData = try? Data(contentsOf: fileLocalURL) else { continue }
-            guard let encryptedRawDat = maybeEncrypt(rawData) else { continue }
+            guard let encryptedRawData = maybeEncrypt(rawData) else { continue }
             group.enter()
             
             let randFileName = String.random(length: 15).appending(".").appending(fileLocalURL.pathExtension)
             let key = "\(self.s3prefix!)/ios\(randFileName)"
-
+            
             keyFileDict[key] = filePath
             fileMd5Dict[filePath] = rawData.MD5
-            transferUtility?.uploadData(encryptedRawDat, key: key, contentType: "application/octet-stream", expression: uploadExpression, completionHandler: uploadCompletionHandler)
-                .continueWith(block: { (task) in
-                    if let error = task.error {
-                        completionHandler([:], [:], error)
-                    }
-                    return nil
-                })
+            
+            guard let presignURL = getPresignedPutURL(configration: configuration!, key: key) else {
+                completionHandler([:], [:], NSError(domain: FileSyncErrorDomain,
+                                                    code: 0,
+                                                    userInfo: [NSLocalizedDescriptionKey: "cannot get presigned url"]))
+                return
+            }
+            
+            let progressHandler = {(fraction: Double) in
+                progressHandler(filePath, fraction)
+            }
+            putContent(url: presignURL, content: encryptedRawData, progressHandler: progressHandler) { error in
+                guard error == nil else {
+                    print("debug put error \(error!)")
+                    completionHandler([:], [:], error!)
+                    return
+                }
+                // only save successful keys
+                fileKeyDict[filePath] = key
+                keyFileDict.removeValue(forKey: key)
+                group.leave()
+            }
         }
         
         group.notify(queue: .main) {
-            AWSS3TransferUtility.remove(forKey: transferKey)
             completionHandler(fileKeyDict, fileMd5Dict, nil)
         }
     }
+    
+    public func putContent(url: URL, content: Data,
+                           progressHandler: @escaping ((Double) -> Void),
+                           completion: @escaping (Error?) -> Void) {
+        var observation: NSKeyValueObservation! = nil
+        
+        var request = URLRequest(url: url)
+        request.httpMethod = "PUT"
+        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
+        request.httpBody = content
+        
+        let task = URLSession.shared.dataTask(with: request) { data, response, error in
+            observation?.invalidate()
+            
+            guard error == nil else {
+                completion(error!)
+                return
+            }
+            if let response = response as? HTTPURLResponse {
+                guard (200 ..< 299) ~= response.statusCode else {
+                    NSLog("debug error put content \(String(data: data!, encoding: .utf8))")
+                    completion(NSError(domain: FileSyncErrorDomain,
+                                       code: response.statusCode,
+                                       userInfo: [NSLocalizedDescriptionKey: "http put request failed"]))
+                    return
+                }
+            }
+            completion(nil)
+        }
+        
+        observation = task.progress.observe(\.fractionCompleted) { progress, _ in
+            progressHandler(progress.fractionCompleted)
+        }
+        
+        task.resume()
+        
+    }
+    
+    public func download(url: URL,
+                         progressHandler: @escaping ((Double) -> Void), // FIXME: cannot get total bytes
+                         completion: @escaping (Result<URL?, Error>) -> Void) {
+        var observation: NSKeyValueObservation! = nil
+        
+        let task = URLSession.shared.downloadTask(with: url) {(tempURL, response, error) in
+            observation?.invalidate()
+            
+            guard let tempURL = tempURL else {
+                completion(.failure(error!))
+                return
+            }
+            guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
+                completion(.failure(NSError(domain: FileSyncErrorDomain,
+                                            code: 0,
+                                            userInfo: [NSLocalizedDescriptionKey: "http get request failed"])))
+                return
+            }
+            completion(.success(tempURL))
+        }
+        
+        observation = task.progress.observe(\.fractionCompleted) { progress, _ in
+            progressHandler(progress.fractionCompleted)
+        }
+        
+        task.resume()
+    }
+    
+    public func download(url: URL, progressHandler: @escaping ((Double) -> Void)) async -> Result<URL?, Error> {
+        return await withCheckedContinuation { continuation in
+            download(url: url, progressHandler: progressHandler) { result in
+                continuation.resume(returning: result)
+            }
+        }
+    }
+    
+    private func getPresignedPutURL(configration: AWSServiceConfiguration, key: String) -> URL? {
+        let req = AWSS3GetPreSignedURLRequest()
+        
+        req.key = key
+        req.bucket = BUCKET
+        req.httpMethod = .PUT
+        req.expires = Date(timeIntervalSinceNow: 600) // 10min
+        
+        var presignedURLString: String? = nil
+        AWSS3PreSignedURLBuilder(configuration: configration).getPreSignedURL(req).continueWith { task -> Any? in
+            if let error = task.error as NSError? {
+                NSLog("error generating presigend url \(error)")
+                return nil
+            }
+            presignedURLString = task.result?.absoluteString
+            return nil
+        }
+        if let presignedURLString = presignedURLString {
+            return URL(string: presignedURLString)
+        } else {
+            return nil
+        }
+    }
 }