|
@@ -10,13 +10,18 @@ import Foundation
|
|
|
import AWSMobileClient
|
|
|
import CryptoKit
|
|
|
|
|
|
-// MARK: Global variables
|
|
|
+
|
|
|
+// MARK: Global variable
|
|
|
|
|
|
// Defualts to dev
|
|
|
var URL_BASE = URL(string: "https://api.logseq.com/file-sync/")!
|
|
|
var BUCKET: String = "logseq-file-sync-bucket"
|
|
|
var REGION: String = "us-east-2"
|
|
|
|
|
|
+var ENCRYPTION_SECRET_KEY: String? = nil
|
|
|
+var ENCRYPTION_PUBLIC_KEY: String? = nil
|
|
|
+
|
|
|
+// MARK: Metadata type
|
|
|
|
|
|
public struct SyncMetadata: CustomStringConvertible, Equatable {
|
|
|
var md5: String
|
|
@@ -29,9 +34,9 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
|
|
|
return nil
|
|
|
}
|
|
|
size = fileAttributes.fileSize ?? 0
|
|
|
-
|
|
|
- // incremental MD5sum
|
|
|
- let bufferSize = 1024 * 1024
|
|
|
+
|
|
|
+ // incremental MD5 checksum
|
|
|
+ let bufferSize = 512 * 1024
|
|
|
let file = try FileHandle(forReadingFrom: fileURL)
|
|
|
defer {
|
|
|
file.closeFile()
|
|
@@ -46,7 +51,7 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
|
|
|
return false // eof
|
|
|
}
|
|
|
}) {}
|
|
|
-
|
|
|
+
|
|
|
let computed = ctx.finalize()
|
|
|
md5 = computed.map { String(format: "%02hhx", $0) }.joined()
|
|
|
} catch {
|
|
@@ -59,6 +64,38 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// MARK: encryption helper
|
|
|
+
|
|
|
+func maybeEncrypt(_ plaindata: Data) -> Data! {
|
|
|
+ // avoid encryption twice
|
|
|
+ if plaindata.starts(with: "-----BEGIN AGE ENCRYPTED FILE-----".data(using: .utf8)!) ||
|
|
|
+ plaindata.starts(with: "age-encryption.org/v1\n".data(using: .utf8)!) {
|
|
|
+ return plaindata
|
|
|
+ }
|
|
|
+ if let publicKey = ENCRYPTION_PUBLIC_KEY {
|
|
|
+ // use armor = false, for smaller size
|
|
|
+ if let cipherdata = AgeEncryption.encryptWithPassphrase(plaindata, publicKey, armor: false) {
|
|
|
+ return cipherdata
|
|
|
+ }
|
|
|
+ return nil // encryption fail
|
|
|
+ }
|
|
|
+ return plaindata
|
|
|
+}
|
|
|
+
|
|
|
+func maybeDecrypt(_ cipherdata: Data) -> Data! {
|
|
|
+ if let secretKey = ENCRYPTION_SECRET_KEY {
|
|
|
+ if cipherdata.starts(with: "-----BEGIN AGE ENCRYPTED FILE-----".data(using: .utf8)!) ||
|
|
|
+ cipherdata.starts(with: "age-encryption.org/v1\n".data(using: .utf8)!) {
|
|
|
+ if let plaindata = AgeEncryption.decryptWithX25519(cipherdata, secretKey) {
|
|
|
+ return plaindata
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ // not an encrypted file
|
|
|
+ return cipherdata
|
|
|
+ }
|
|
|
+ return cipherdata
|
|
|
+}
|
|
|
|
|
|
// MARK: FileSync Plugin
|
|
|
|
|
@@ -66,6 +103,7 @@ public struct SyncMetadata: CustomStringConvertible, Equatable {
|
|
|
public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
override public func load() {
|
|
|
print("debug File sync iOS plugin loaded!")
|
|
|
+
|
|
|
AWSMobileClient.default().initialize { (userState, error) in
|
|
|
guard error == nil else {
|
|
|
print("error initializing AWSMobileClient. Error: \(error!.localizedDescription)")
|
|
@@ -73,17 +111,40 @@ 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")
|
|
|
+ if secretKey == nil && publicKey == nil {
|
|
|
+ ENCRYPTION_SECRET_KEY = nil
|
|
|
+ ENCRYPTION_PUBLIC_KEY = nil
|
|
|
+ }
|
|
|
+ guard let secretKey = secretKey, let publicKey = publicKey else {
|
|
|
+ call.reject("both secretKey and publicKey should be provided")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ENCRYPTION_SECRET_KEY = secretKey
|
|
|
+ ENCRYPTION_PUBLIC_KEY = publicKey
|
|
|
+ }
|
|
|
+
|
|
|
@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-prod.logseq.com/file-sync/")!
|
|
@@ -97,10 +158,11 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
call.reject("invalid env: \(env)")
|
|
|
return
|
|
|
}
|
|
|
+
|
|
|
self.debugNotification(["event": "setenv:\(env)"])
|
|
|
call.resolve(["ok": true])
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
@objc func getLocalFilesMeta(_ call: CAPPluginCall) {
|
|
|
guard let basePath = call.getString("basePath"),
|
|
|
let filePaths = call.getArray("filePaths") as? [String] else {
|
|
@@ -111,7 +173,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
call.reject("invalid basePath")
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
var fileMetadataDict: [String: [String: Any]] = [:]
|
|
|
for filePath in filePaths {
|
|
|
let url = baseURL.appendingPathComponent(filePath)
|
|
@@ -120,20 +182,20 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
"size": meta.size]
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
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) {
|
|
@@ -147,8 +209,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 {
|
|
@@ -163,10 +225,10 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
call.reject("invalid to file")
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
let fromUrl = baseURL.appendingPathComponent(from)
|
|
|
let toUrl = baseURL.appendingPathComponent(to)
|
|
|
-
|
|
|
+
|
|
|
do {
|
|
|
try FileManager.default.moveItem(at: fromUrl, to: toUrl)
|
|
|
} catch {
|
|
@@ -174,23 +236,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)
|
|
|
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)}),
|
|
@@ -200,10 +262,10 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
call.reject("required paremeters: basePath, filePaths, graphUUID, token")
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
let client = SyncClient(token: token, graphUUID: graphUUID)
|
|
|
client.delegate = self // receives notification
|
|
|
-
|
|
|
+
|
|
|
client.getFiles(at: filePaths) { (fileURLs, error) in
|
|
|
if let error = error {
|
|
|
print("debug getFiles error \(error)")
|
|
@@ -212,9 +274,9 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
} else {
|
|
|
// handle multiple completionHandlers
|
|
|
let group = DispatchGroup()
|
|
|
-
|
|
|
+
|
|
|
var downloaded: [String] = []
|
|
|
-
|
|
|
+
|
|
|
for (filePath, remoteFileURL) in fileURLs {
|
|
|
group.enter()
|
|
|
|
|
@@ -235,11 +297,11 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
self.debugNotification(["event": "download:done"])
|
|
|
call.resolve(["ok": true, "data": downloaded])
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
@objc func deleteRemoteFiles(_ call: CAPPluginCall) {
|
|
|
guard let filePaths = call.getArray("filePaths") as? [String],
|
|
|
let graphUUID = call.getString("graphUUID"),
|
|
@@ -252,7 +314,7 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
call.reject("empty filePaths")
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
|
|
|
client.deleteFiles(filePaths) { txid, error in
|
|
|
guard error == nil else {
|
|
@@ -266,7 +328,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)}),
|
|
@@ -280,12 +342,10 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
|
|
|
guard !filePaths.isEmpty else {
|
|
|
return call.reject("empty filePaths")
|
|
|
}
|
|
|
-
|
|
|
- print("debug begin updateRemoteFiles \(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 {
|
|
@@ -293,14 +353,14 @@ 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)
|
|
|
files[filePath.encodeAsFname()] = fileURL
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 2. upload_temp_file
|
|
|
client.uploadTempFiles(files, credentials: credentials!) { (uploadedFileKeyDict, error) in
|
|
|
guard error == nil else {
|