SyncClient.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. //
  2. // SyncClient.swift
  3. // Logseq
  4. //
  5. // Created by Mono Wang on 4/8/R4.
  6. //
  7. import Foundation
  8. import AWSMobileClient
  9. import AWSS3
  10. public protocol SyncDebugDelegate {
  11. func debugNotification(_ message: [String: Any])
  12. }
  13. public class SyncClient {
  14. private var token: String
  15. private var graphUUID: String?
  16. private var txid: Int = 0
  17. private var s3prefix: String?
  18. public var delegate: SyncDebugDelegate? = nil
  19. public init(token: String) {
  20. self.token = token
  21. }
  22. public init(token: String, graphUUID: String) {
  23. self.token = token
  24. self.graphUUID = graphUUID
  25. }
  26. public init(token: String, graphUUID: String, txid: Int) {
  27. self.token = token
  28. self.graphUUID = graphUUID
  29. self.txid = txid
  30. }
  31. // get_files
  32. // => file_path, file_url
  33. public func getFiles(at filePaths: [String], completionHandler: @escaping ([String: URL], Error?) -> Void) {
  34. let url = URL_BASE.appendingPathComponent("get_files")
  35. var request = URLRequest(url: url)
  36. request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
  37. request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
  38. request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
  39. let payload = [
  40. "GraphUUID": self.graphUUID ?? "",
  41. "Files": filePaths.map { filePath in filePath.encodeAsFname()}
  42. ] as [String : Any]
  43. let bodyData = try? JSONSerialization.data(
  44. withJSONObject: payload,
  45. options: []
  46. )
  47. request.httpMethod = "POST"
  48. request.httpBody = bodyData
  49. let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
  50. guard error == nil else {
  51. completionHandler([:], error)
  52. return
  53. }
  54. if (response as? HTTPURLResponse)?.statusCode != 200 {
  55. let body = String(data: data!, encoding: .utf8) ?? "";
  56. completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
  57. return
  58. }
  59. if let data = data {
  60. let resp = try? JSONDecoder().decode([String:[String:String]].self, from: data)
  61. let files = resp?["PresignedFileUrls"] ?? [:]
  62. self.delegate?.debugNotification(["event": "download:prepare"])
  63. completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
  64. } else {
  65. // Handle unexpected error
  66. completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
  67. }
  68. }
  69. task.resume()
  70. }
  71. public func deleteFiles(_ filePaths: [String], completionHandler: @escaping (Int?, Error?) -> Void) {
  72. let url = URL_BASE.appendingPathComponent("delete_files")
  73. var request = URLRequest(url: url)
  74. request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
  75. request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
  76. request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
  77. let payload = [
  78. "GraphUUID": self.graphUUID ?? "",
  79. "Files": filePaths.map { filePath in filePath.encodeAsFname()},
  80. "TXId": self.txid,
  81. ] as [String : Any]
  82. let bodyData = try? JSONSerialization.data(
  83. withJSONObject: payload,
  84. options: []
  85. )
  86. request.httpMethod = "POST"
  87. request.httpBody = bodyData
  88. let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
  89. guard error == nil else {
  90. completionHandler(nil, error)
  91. return
  92. }
  93. if let response = response as? HTTPURLResponse {
  94. let body = String(data: data!, encoding: .utf8) ?? ""
  95. if response.statusCode == 409 {
  96. if body.contains("txid_to_validate") {
  97. completionHandler(nil, NSError(domain: "",
  98. code: 409,
  99. userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
  100. return
  101. }
  102. // fallthrough
  103. }
  104. if response.statusCode != 200 {
  105. completionHandler(nil, NSError(domain: "",
  106. code: response.statusCode,
  107. userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
  108. return
  109. }
  110. }
  111. if let data = data {
  112. do {
  113. let resp = try JSONDecoder().decode(DeleteFilesResponse.self, from: data)
  114. // TODO: handle api resp?
  115. self.delegate?.debugNotification(["event": "delete"])
  116. completionHandler(resp.TXId, nil)
  117. } catch {
  118. completionHandler(nil, error)
  119. }
  120. } else {
  121. // Handle unexpected error
  122. completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
  123. }
  124. }
  125. task.resume()
  126. }
  127. // (txid, error)
  128. // filePath => [S3Key, md5]
  129. public func updateFiles(_ fileKeyDict: [String: [String]], completionHandler: @escaping (Int?, Error?) -> Void) {
  130. let url = URL_BASE.appendingPathComponent("update_files")
  131. var request = URLRequest(url: url)
  132. request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
  133. request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
  134. request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
  135. let payload = [
  136. "GraphUUID": self.graphUUID ?? "",
  137. "Files": Dictionary(uniqueKeysWithValues: fileKeyDict.map { ($0, $1) }) as [String: [String]] as Any,
  138. "TXId": self.txid,
  139. ] as [String : Any]
  140. let bodyData = try? JSONSerialization.data(
  141. withJSONObject: payload,
  142. options: []
  143. )
  144. request.httpMethod = "POST"
  145. request.httpBody = bodyData
  146. let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
  147. guard error == nil else {
  148. completionHandler(nil, error)
  149. return
  150. }
  151. if let response = response as? HTTPURLResponse {
  152. let body = String(data: data!, encoding: .utf8) ?? ""
  153. if response.statusCode == 409 {
  154. if body.contains("txid_to_validate") {
  155. completionHandler(nil, NSError(domain: "",
  156. code: 409,
  157. userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
  158. return
  159. }
  160. // fallthrough
  161. }
  162. if response.statusCode != 200 {
  163. completionHandler(nil, NSError(domain: "",
  164. code: response.statusCode,
  165. userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
  166. return
  167. }
  168. }
  169. if let data = data {
  170. let resp = try? JSONDecoder().decode(UpdateFilesResponse.self, from: data)
  171. if resp?.UpdateFailedFiles.isEmpty ?? true {
  172. completionHandler(resp?.TXId, nil)
  173. } else {
  174. completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "update fail for some files: \(resp?.UpdateFailedFiles.debugDescription)"]))
  175. }
  176. } else {
  177. // Handle unexpected error
  178. completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
  179. }
  180. }
  181. task.resume()
  182. }
  183. public func getTempCredential(completionHandler: @escaping (S3Credential?, Error?) -> Void) {
  184. let url = URL_BASE.appendingPathComponent("get_temp_credential")
  185. var request = URLRequest(url: url)
  186. request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
  187. request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
  188. request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
  189. request.httpMethod = "POST"
  190. request.httpBody = Data()
  191. let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
  192. guard error == nil else {
  193. completionHandler(nil, error)
  194. return
  195. }
  196. if let response = response as? HTTPURLResponse {
  197. let body = String(data: data!, encoding: .utf8) ?? ""
  198. if response.statusCode == 401 {
  199. completionHandler(nil, NSError(domain: "", code: 401, userInfo: [NSLocalizedDescriptionKey: "unauthorized"]))
  200. return
  201. }
  202. if response.statusCode != 200 {
  203. completionHandler(nil, NSError(domain: "",
  204. code: response.statusCode,
  205. userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
  206. return
  207. }
  208. }
  209. if let data = data {
  210. let resp = try? JSONDecoder().decode(GetTempCredentialResponse.self, from: data)
  211. // NOTE: remove BUCKET prefix here.
  212. self.s3prefix = resp?.S3Prefix.replacingOccurrences(of: "\(BUCKET)/", with: "")
  213. self.delegate?.debugNotification(["event": "upload:prepare"])
  214. completionHandler(resp?.Credentials, nil)
  215. } else {
  216. // Handle unexpected error
  217. completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
  218. }
  219. }
  220. task.resume()
  221. }
  222. // [filePath, Key]
  223. public func uploadTempFiles(_ files: [String: URL], credentials: S3Credential, completionHandler: @escaping ([String: String], [String: String], Error?) -> Void) {
  224. let credentialsProvider = AWSBasicSessionCredentialsProvider(
  225. accessKey: credentials.AccessKeyId, secretKey: credentials.SecretKey, sessionToken: credentials.SessionToken)
  226. let configuration = AWSServiceConfiguration(region: .USEast2, credentialsProvider: credentialsProvider)
  227. configuration?.timeoutIntervalForRequest = 5.0
  228. configuration?.timeoutIntervalForResource = 5.0
  229. let tuConf = AWSS3TransferUtilityConfiguration()
  230. tuConf.bucket = BUCKET
  231. //x tuConf.isAccelerateModeEnabled = true
  232. let transferKey = String.random(length: 10)
  233. AWSS3TransferUtility.register(
  234. with: configuration!,
  235. transferUtilityConfiguration: tuConf,
  236. forKey: transferKey
  237. ) { (error) in
  238. if let error = error {
  239. print("error while register tu \(error)")
  240. }
  241. }
  242. let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: transferKey)
  243. let uploadExpression = AWSS3TransferUtilityUploadExpression()
  244. let group = DispatchGroup()
  245. var keyFileDict: [String: String] = [:]
  246. var fileKeyDict: [String: String] = [:]
  247. var fileMd5Dict: [String: String] = [:]
  248. let uploadCompletionHandler = { (task: AWSS3TransferUtilityUploadTask, error: Error?) -> Void in
  249. // ignore any errors in first level of handler
  250. if let error = error {
  251. self.delegate?.debugNotification(["event": "upload:error", "data": ["key": task.key, "error": error.localizedDescription]])
  252. }
  253. if let HTTPResponse = task.response {
  254. if HTTPResponse.statusCode != 200 || task.status != .completed {
  255. print("debug uploading error \(HTTPResponse)")
  256. }
  257. }
  258. // only save successful keys
  259. let filePath = keyFileDict[task.key]!
  260. fileKeyDict[filePath] = task.key
  261. keyFileDict.removeValue(forKey: task.key)
  262. self.delegate?.debugNotification(["event": "upload:file", "data": ["file": filePath, "key": task.key]])
  263. group.leave() // notify finish upload
  264. }
  265. for (filePath, fileLocalURL) in files {
  266. print("debug, upload temp \(fileLocalURL) \(filePath)")
  267. guard let rawData = try? Data(contentsOf: fileLocalURL) else { continue }
  268. guard let encryptedRawDat = maybeEncrypt(rawData) else { continue }
  269. group.enter()
  270. let randFileName = String.random(length: 15).appending(".").appending(fileLocalURL.pathExtension)
  271. let key = "\(self.s3prefix!)/ios\(randFileName)"
  272. keyFileDict[key] = filePath
  273. fileMd5Dict[filePath] = rawData.MD5
  274. transferUtility?.uploadData(encryptedRawDat, key: key, contentType: "application/octet-stream", expression: uploadExpression, completionHandler: uploadCompletionHandler)
  275. .continueWith(block: { (task) in
  276. if let error = task.error {
  277. completionHandler([:], [:], error)
  278. }
  279. return nil
  280. })
  281. }
  282. group.notify(queue: .main) {
  283. AWSS3TransferUtility.remove(forKey: transferKey)
  284. completionHandler(fileKeyDict, fileMd5Dict, nil)
  285. }
  286. }
  287. }