| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- //
- // SyncClient.swift
- // Logseq
- //
- // Created by Mono Wang on 4/8/R4.
- //
- import Foundation
- import AWSMobileClient
- import AWSS3
- public protocol SyncDebugDelegate {
- func debugNotification(_ message: [String: Any])
- }
- public class SyncClient {
- private var token: String
- private var graphUUID: String?
- private var txid: Int = 0
- private var s3prefix: String?
-
- public var delegate: SyncDebugDelegate? = nil
-
- public init(token: String) {
- self.token = token
- }
-
- public init(token: String, graphUUID: String) {
- self.token = token
- self.graphUUID = graphUUID
- }
-
- public init(token: String, graphUUID: String, txid: Int) {
- self.token = token
- self.graphUUID = graphUUID
- self.txid = txid
- }
- // get_files
- // => file_path, file_url
- public func getFiles(at filePaths: [String], completionHandler: @escaping ([String: URL], Error?) -> Void) {
- let url = URL_BASE.appendingPathComponent("get_files")
-
- var request = URLRequest(url: url)
- request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
- request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
- request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-
- let payload = [
- "GraphUUID": self.graphUUID ?? "",
- "Files": filePaths.map { filePath in filePath.encodeAsFname()}
- ] as [String : Any]
- let bodyData = try? JSONSerialization.data(
- withJSONObject: payload,
- options: []
- )
- request.httpMethod = "POST"
- request.httpBody = bodyData
-
- let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
- guard error == nil else {
- completionHandler([:], error)
- return
- }
-
- if (response as? HTTPURLResponse)?.statusCode != 200 {
- let body = String(data: data!, encoding: .utf8) ?? "";
- completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
- return
- }
-
- if let data = data {
- let resp = try? JSONDecoder().decode([String:[String:String]].self, from: data)
- let files = resp?["PresignedFileUrls"] ?? [:]
- self.delegate?.debugNotification(["event": "download:prepare"])
- completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
- } else {
- // Handle unexpected error
- completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
- }
- }
- task.resume()
- }
-
-
- public func deleteFiles(_ filePaths: [String], completionHandler: @escaping (Int?, Error?) -> Void) {
- let url = URL_BASE.appendingPathComponent("delete_files")
-
- var request = URLRequest(url: url)
- request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
- request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
- request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-
- let payload = [
- "GraphUUID": self.graphUUID ?? "",
- "Files": filePaths.map { filePath in filePath.encodeAsFname()},
- "TXId": self.txid,
- ] as [String : Any]
- let bodyData = try? JSONSerialization.data(
- withJSONObject: payload,
- options: []
- )
- request.httpMethod = "POST"
- request.httpBody = bodyData
-
- let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
- guard error == nil else {
- completionHandler(nil, error)
- return
- }
-
- if let response = response as? HTTPURLResponse {
- let body = String(data: data!, encoding: .utf8) ?? ""
-
- if response.statusCode == 409 {
- if body.contains("txid_to_validate") {
- completionHandler(nil, NSError(domain: "",
- code: 409,
- userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
- return
- }
- // fallthrough
- }
- if response.statusCode != 200 {
- completionHandler(nil, NSError(domain: "",
- code: response.statusCode,
- userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
- return
- }
- }
-
- if let data = data {
- do {
- let resp = try JSONDecoder().decode(DeleteFilesResponse.self, from: data)
- // TODO: handle api resp?
- self.delegate?.debugNotification(["event": "delete"])
- completionHandler(resp.TXId, nil)
- } catch {
- completionHandler(nil, error)
- }
- } else {
- // Handle unexpected error
- completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
- }
- }
- task.resume()
- }
-
- // (txid, error)
- // filePath => [S3Key, md5]
- public func updateFiles(_ fileKeyDict: [String: [String]], completionHandler: @escaping (Int?, Error?) -> Void) {
- let url = URL_BASE.appendingPathComponent("update_files")
-
- var request = URLRequest(url: url)
- request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
- request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
- request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-
- let payload = [
- "GraphUUID": self.graphUUID ?? "",
- "Files": Dictionary(uniqueKeysWithValues: fileKeyDict.map { ($0, $1) }) as [String: [String]] as Any,
- "TXId": self.txid,
- ] as [String : Any]
- let bodyData = try? JSONSerialization.data(
- withJSONObject: payload,
- options: []
- )
- request.httpMethod = "POST"
- request.httpBody = bodyData
-
- let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
- guard error == nil else {
- completionHandler(nil, error)
- return
- }
-
- if let response = response as? HTTPURLResponse {
- let body = String(data: data!, encoding: .utf8) ?? ""
-
- if response.statusCode == 409 {
- if body.contains("txid_to_validate") {
- completionHandler(nil, NSError(domain: "",
- code: 409,
- userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
- return
- }
- // fallthrough
- }
- if response.statusCode != 200 {
- completionHandler(nil, NSError(domain: "",
- code: response.statusCode,
- userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
- return
- }
- }
-
- if let data = data {
- let resp = try? JSONDecoder().decode(UpdateFilesResponse.self, from: data)
- 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)"]))
- }
- } else {
- // Handle unexpected error
- completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
- }
- }
- task.resume()
- }
-
- public func getTempCredential(completionHandler: @escaping (S3Credential?, Error?) -> Void) {
- let url = URL_BASE.appendingPathComponent("get_temp_credential")
-
- var request = URLRequest(url: url)
- request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
- request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
- request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
- request.httpMethod = "POST"
- request.httpBody = Data()
-
- let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
- guard error == nil else {
- completionHandler(nil, error)
- return
- }
- 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"]))
- return
- }
- if response.statusCode != 200 {
- completionHandler(nil, NSError(domain: "",
- code: response.statusCode,
- userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
- return
- }
- }
- if let data = data {
- let resp = try? JSONDecoder().decode(GetTempCredentialResponse.self, from: data)
- // NOTE: remove BUCKET prefix here.
- self.s3prefix = resp?.S3Prefix.replacingOccurrences(of: "\(BUCKET)/", with: "")
- self.delegate?.debugNotification(["event": "upload:prepare"])
- completionHandler(resp?.Credentials, nil)
- } else {
- // Handle unexpected error
- completionHandler(nil, NSError(domain: "", 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) {
- let credentialsProvider = AWSBasicSessionCredentialsProvider(
- accessKey: credentials.AccessKeyId, secretKey: credentials.SecretKey, sessionToken: credentials.SessionToken)
- let configuration = AWSServiceConfiguration(region: .USEast2, credentialsProvider: credentialsProvider)
- 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 }
- 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
- })
- }
-
- group.notify(queue: .main) {
- AWSS3TransferUtility.remove(forKey: transferKey)
- completionHandler(fileKeyDict, fileMd5Dict, nil)
- }
- }
- }
|