mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 14:14:55 +00:00
refactor(mobile): use ios file-sync as lib
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,3 +47,4 @@ startup.png
|
||||
~*~
|
||||
|
||||
ios/App/App/capacitor.config.json
|
||||
android/app/src/main/assets/capacitor.config.json
|
||||
|
||||
@@ -18,6 +18,7 @@ dependencies {
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capacitor-splash-screen')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':logseq-capacitor-file-sync')
|
||||
implementation project(':capacitor-voice-recorder')
|
||||
implementation project(':send-intent')
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"appId": "com.logseq.app",
|
||||
"appName": "Logseq",
|
||||
"bundledWebRuntime": false,
|
||||
"webDir": "public",
|
||||
"plugins": {
|
||||
"SplashScreen": {
|
||||
"launchShowDuration": 500,
|
||||
"launchAutoHide": false,
|
||||
"androidScaleType": "CENTER_CROP",
|
||||
"splashImmersive": false,
|
||||
"backgroundColor": "#002b36"
|
||||
},
|
||||
"Keyboard": {
|
||||
"resize": "none"
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"scheme": "Logseq"
|
||||
},
|
||||
"server": {
|
||||
"url": "http://192.168.199.241:3001",
|
||||
"cleartext": true
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,10 @@
|
||||
"pkg": "@capacitor/status-bar",
|
||||
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@logseq/capacitor-file-sync",
|
||||
"classpath": "com.logseq.app.FileSyncPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "capacitor-voice-recorder",
|
||||
"classpath": "com.tchvu3.capacitorvoicerecorder.VoiceRecorder"
|
||||
|
||||
@@ -29,6 +29,9 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa
|
||||
include ':capacitor-status-bar'
|
||||
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||
|
||||
include ':logseq-capacitor-file-sync'
|
||||
project(':logseq-capacitor-file-sync').projectDir = new File('../node_modules/@logseq/capacitor-file-sync/android')
|
||||
|
||||
include ':capacitor-voice-recorder'
|
||||
project(':capacitor-voice-recorder').projectDir = new File('../node_modules/capacitor-voice-recorder/android')
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ const config: CapacitorConfig = {
|
||||
},
|
||||
ios: {
|
||||
scheme: 'Logseq'
|
||||
},
|
||||
cordova: {
|
||||
staticPlugins: [
|
||||
'@logseq/capacitor-file-sync', // AgeEncryption requires static link
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,14 +25,8 @@
|
||||
D32752BE275496C60039291C /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D32752BD275496C60039291C /* CloudKit.framework */; };
|
||||
D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A09275C92880003FBDC /* FileContainer.swift */; };
|
||||
D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
|
||||
FE443F1C27FF5420007ECE65 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1B27FF5420007ECE65 /* Extensions.swift */; };
|
||||
FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1D27FF54AA007ECE65 /* Payload.swift */; };
|
||||
FE443F2027FF54C9007ECE65 /* SyncClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1F27FF54C9007ECE65 /* SyncClient.swift */; };
|
||||
FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
|
||||
FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
|
||||
FE8C946B27FD762700C8017B /* FileSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946927FD762700C8017B /* FileSync.swift */; };
|
||||
FE8C946C27FD762700C8017B /* FileSync.m in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946A27FD762700C8017B /* FileSync.m */; };
|
||||
FEE688A428448F8C0019510E /* AgeEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE688A328448F8C0019510E /* AgeEncryption.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -87,14 +81,8 @@
|
||||
D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = "<group>"; };
|
||||
D3D62A0B275C928F0003FBDC /* FileContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileContainer.m; sourceTree = "<group>"; };
|
||||
DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FE443F1B27FF5420007ECE65 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
FE443F1D27FF54AA007ECE65 /* Payload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payload.swift; sourceTree = "<group>"; };
|
||||
FE443F1F27FF54C9007ECE65 /* SyncClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncClient.swift; sourceTree = "<group>"; };
|
||||
FE647FF327BDFEDE00F3206B /* FsWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FsWatcher.swift; sourceTree = "<group>"; };
|
||||
FE647FF527BDFEF500F3206B /* FsWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FsWatcher.m; sourceTree = "<group>"; };
|
||||
FE8C946927FD762700C8017B /* FileSync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSync.swift; sourceTree = "<group>"; };
|
||||
FE8C946A27FD762700C8017B /* FileSync.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileSync.m; sourceTree = "<group>"; };
|
||||
FEE688A328448F8C0019510E /* AgeEncryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgeEncryption.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -154,7 +142,6 @@
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
7435D10B2704659F00AB88E0 /* FolderPicker.swift */,
|
||||
FE647FF327BDFEDE00F3206B /* FsWatcher.swift */,
|
||||
FE443F1A27FF53A2007ECE65 /* FileSync */,
|
||||
FE647FF527BDFEF500F3206B /* FsWatcher.m */,
|
||||
7435D10E2704660B00AB88E0 /* FolderPicker.m */,
|
||||
D3D62A09275C92880003FBDC /* FileContainer.swift */,
|
||||
@@ -193,19 +180,6 @@
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FE443F1A27FF53A2007ECE65 /* FileSync */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FE8C946927FD762700C8017B /* FileSync.swift */,
|
||||
FE443F1F27FF54C9007ECE65 /* SyncClient.swift */,
|
||||
FE443F1D27FF54AA007ECE65 /* Payload.swift */,
|
||||
FE443F1B27FF5420007ECE65 /* Extensions.swift */,
|
||||
FE8C946A27FD762700C8017B /* FileSync.m */,
|
||||
FEE688A328448F8C0019510E /* AgeEncryption.swift */,
|
||||
);
|
||||
path = FileSync;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -356,20 +330,14 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */,
|
||||
5FF8632C283B5BFD0047731B /* Utils.m in Sources */,
|
||||
FEE688A428448F8C0019510E /* AgeEncryption.swift in Sources */,
|
||||
FE8C946B27FD762700C8017B /* FileSync.swift in Sources */,
|
||||
FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */,
|
||||
5FF8632A283B5ADB0047731B /* Utils.swift in Sources */,
|
||||
D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */,
|
||||
D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */,
|
||||
FE443F1C27FF5420007ECE65 /* Extensions.swift in Sources */,
|
||||
FE8C946C27FD762700C8017B /* FileSync.m in Sources */,
|
||||
7435D10F2704660B00AB88E0 /* FolderPicker.m in Sources */,
|
||||
7435D10C2704659F00AB88E0 /* FolderPicker.swift in Sources */,
|
||||
FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */,
|
||||
FE443F2027FF54C9007ECE65 /* SyncClient.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
//
|
||||
// AgeEncryption.swift
|
||||
// Logseq
|
||||
//
|
||||
// Created by Mono Wang on 5/30/R4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AgeEncryption
|
||||
|
||||
public enum AgeEncryption {
|
||||
public static func keygen() -> (String, String) {
|
||||
let cSecretKey = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
|
||||
let cPublicKey = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
|
||||
|
||||
rust_age_encryption_keygen(cSecretKey, cPublicKey);
|
||||
let secretKey = String(cString: cSecretKey.pointee!)
|
||||
let publicKey = String(cString: cPublicKey.pointee!)
|
||||
|
||||
rust_age_encryption_free_str(cSecretKey.pointee!)
|
||||
rust_age_encryption_free_str(cPublicKey.pointee!)
|
||||
cSecretKey.deallocate()
|
||||
cPublicKey.deallocate()
|
||||
|
||||
return (secretKey, publicKey)
|
||||
}
|
||||
|
||||
public static func toRawX25519Key(_ secretKey: String) -> Data? {
|
||||
let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
|
||||
|
||||
let ret = rust_age_encryption_to_raw_x25519_key(secretKey.cString(using: .utf8), cOutput)
|
||||
if ret >= 0 {
|
||||
let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: 32)
|
||||
let rawKey = Data.init(buffer: cOutputBuf)
|
||||
rust_age_encryption_free_vec(cOutput.pointee, ret)
|
||||
cOutput.deallocate()
|
||||
return rawKey
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public static func encryptWithPassphrase(_ plaintext: Data, _ passphrase: String, armor: Bool) -> Data? {
|
||||
plaintext.withUnsafeBytes { (cPlaintext) in
|
||||
let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
|
||||
|
||||
let ret = rust_age_encrypt_with_user_passphrase(passphrase.cString(using: .utf8), cPlaintext.bindMemory(to: CChar.self).baseAddress, Int32(plaintext.count), armor ? 1 : 0, cOutput)
|
||||
if ret > 0 {
|
||||
let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
|
||||
let ciphertext = Data.init(buffer: cOutputBuf)
|
||||
rust_age_encryption_free_vec(cOutput.pointee, ret)
|
||||
cOutput.deallocate()
|
||||
return ciphertext
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func decryptWithPassphrase(_ ciphertext: Data, _ passphrase: String) -> Data? {
|
||||
ciphertext.withUnsafeBytes { (cCiphertext) in
|
||||
let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
|
||||
|
||||
let ret = rust_age_decrypt_with_user_passphrase(passphrase.cString(using: .utf8), cCiphertext.bindMemory(to: CChar.self).baseAddress, Int32(ciphertext.count), cOutput)
|
||||
if ret > 0 {
|
||||
let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
|
||||
let plaintext = Data.init(buffer: cOutputBuf)
|
||||
rust_age_encryption_free_vec(cOutput.pointee, ret)
|
||||
cOutput.deallocate()
|
||||
return plaintext
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func encryptWithX25519(_ plaintext: Data, _ publicKey: String, armor: Bool) -> Data? {
|
||||
plaintext.withUnsafeBytes { (cPlaintext) in
|
||||
let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
|
||||
|
||||
let ret = rust_age_encrypt_with_x25519(publicKey.cString(using: .utf8), cPlaintext.bindMemory(to: CChar.self).baseAddress, Int32(plaintext.count), armor ? 1 : 0, cOutput)
|
||||
if ret > 0 {
|
||||
let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
|
||||
let ciphertext = Data.init(buffer: cOutputBuf)
|
||||
rust_age_encryption_free_vec(cOutput.pointee, ret)
|
||||
cOutput.deallocate()
|
||||
return ciphertext
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func decryptWithX25519(_ ciphertext: Data, _ secretKey: String) -> Data? {
|
||||
ciphertext.withUnsafeBytes { (cCiphertext) in
|
||||
let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
|
||||
|
||||
let ret = rust_age_decrypt_with_x25519(secretKey.cString(using: .utf8), cCiphertext.bindMemory(to: CChar.self).baseAddress, Int32(ciphertext.count), cOutput)
|
||||
if ret >= 0 {
|
||||
let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
|
||||
let plaintext = Data.init(buffer: cOutputBuf)
|
||||
rust_age_encryption_free_vec(cOutput.pointee, ret)
|
||||
cOutput.deallocate()
|
||||
return plaintext
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
//
|
||||
// Extensions.swift
|
||||
// Logseq
|
||||
//
|
||||
// Created by Mono Wang on 4/8/R4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
// via https://github.com/krzyzanowskim/CryptoSwift
|
||||
extension Array where Element == UInt8 {
|
||||
public init(hex: String) {
|
||||
self = Array.init()
|
||||
self.reserveCapacity(hex.unicodeScalars.lazy.underestimatedCount)
|
||||
var buffer: UInt8?
|
||||
var skip = hex.hasPrefix("0x") ? 2 : 0
|
||||
for char in hex.unicodeScalars.lazy {
|
||||
guard skip == 0 else {
|
||||
skip -= 1
|
||||
continue
|
||||
}
|
||||
guard char.value >= 48 && char.value <= 102 else {
|
||||
removeAll()
|
||||
return
|
||||
}
|
||||
let v: UInt8
|
||||
let c: UInt8 = UInt8(char.value)
|
||||
switch c {
|
||||
case let c where c <= 57:
|
||||
v = c - 48
|
||||
case let c where c >= 65 && c <= 70:
|
||||
v = c - 55
|
||||
case let c where c >= 97:
|
||||
v = c - 87
|
||||
default:
|
||||
removeAll()
|
||||
return
|
||||
}
|
||||
if let b = buffer {
|
||||
append(b << 4 | v)
|
||||
buffer = nil
|
||||
} else {
|
||||
buffer = v
|
||||
}
|
||||
}
|
||||
if let b = buffer {
|
||||
append(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public init?(hexEncoded: String) {
|
||||
self.init(Array<UInt8>(hex: hexEncoded))
|
||||
}
|
||||
|
||||
var hexDescription: String {
|
||||
return map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
|
||||
var MD5: String {
|
||||
let computed = Insecure.MD5.hash(data: self)
|
||||
return computed.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var MD5: String {
|
||||
let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!)
|
||||
return computed.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
|
||||
static func random(length: Int) -> String {
|
||||
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
return String((0..<length).map{ _ in letters.randomElement()! })
|
||||
}
|
||||
|
||||
func fnameEncrypt(rawKey: Data) -> String? {
|
||||
guard !self.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
guard let raw = self.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let key = SymmetricKey(data: rawKey)
|
||||
let nonce = try! ChaChaPoly.Nonce(data: Data(repeating: 0, count: 12))
|
||||
guard let sealed = try? ChaChaPoly.seal(raw, using: key, nonce: nonce) else { return nil }
|
||||
|
||||
// strip nonce here, since it's all zero
|
||||
return "e." + (sealed.ciphertext + sealed.tag).hexDescription
|
||||
|
||||
}
|
||||
|
||||
func fnameDecrypt(rawKey: Data) -> String? {
|
||||
// well-formated, non-empty encrypted string
|
||||
guard self.hasPrefix("e.") && self.count > 36 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let encryptedHex = self.suffix(from: self.index(self.startIndex, offsetBy: 2))
|
||||
guard let encryptedRaw = Data(hexEncoded: String(encryptedHex)) else {
|
||||
// invalid hex
|
||||
return nil
|
||||
}
|
||||
|
||||
let key = SymmetricKey(data: rawKey)
|
||||
let nonce = Data(repeating: 0, count: 12)
|
||||
|
||||
guard let sealed = try? ChaChaPoly.SealedBox(combined: nonce + encryptedRaw) else {
|
||||
return nil
|
||||
}
|
||||
guard let outputRaw = try? ChaChaPoly.open(sealed, using: key) else {
|
||||
return nil
|
||||
}
|
||||
return String(data: outputRaw, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
extension URL {
|
||||
func relativePath(from base: URL) -> String? {
|
||||
// Ensure that both URLs represent files:
|
||||
guard self.isFileURL && base.isFileURL else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove/replace "." and "..", make paths absolute:
|
||||
let destComponents = self.standardized.pathComponents
|
||||
let baseComponents = base.standardized.pathComponents
|
||||
|
||||
// Find number of common path components:
|
||||
var i = 0
|
||||
while i < destComponents.count && i < baseComponents.count
|
||||
&& destComponents[i] == baseComponents[i] {
|
||||
i += 1
|
||||
}
|
||||
|
||||
// Build relative path:
|
||||
var relComponents = Array(repeating: "..", count: baseComponents.count - i)
|
||||
relComponents.append(contentsOf: destComponents[i...])
|
||||
return relComponents.joined(separator: "/")
|
||||
}
|
||||
|
||||
func ensureParentDir() {
|
||||
let dirURL = self.deletingLastPathComponent()
|
||||
try? FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
func writeData(data: Data) throws {
|
||||
self.ensureParentDir()
|
||||
if FileManager.default.fileExists(atPath: self.path) {
|
||||
try FileManager.default.removeItem(at: self)
|
||||
}
|
||||
try data.write(to: self, options: .atomic)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Crypto helper
|
||||
|
||||
extension SymmetricKey {
|
||||
public init(passwordString keyString: String) throws {
|
||||
guard let keyData = keyString.data(using: .utf8) else {
|
||||
print("ERROR: Could not create raw Data from String")
|
||||
throw CryptoKitError.incorrectParameterSize
|
||||
}
|
||||
// SymmetricKeySize.bits256
|
||||
let keyDigest = SHA256.hash(data: keyData)
|
||||
|
||||
self.init(data: keyDigest)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// FileSync.m
|
||||
// Logseq
|
||||
//
|
||||
// Created by Mono Wang on 2/24/R4.
|
||||
//
|
||||
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
CAP_PLUGIN(FileSync, "FileSync",
|
||||
CAP_PLUGIN_METHOD(setEnv, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(keygen, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getLocalFilesMeta, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getLocalAllFilesMeta, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(renameLocalFile, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(deleteLocalFiles, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(updateLocalFiles, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(deleteRemoteFiles, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(updateRemoteFiles, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(encryptFnames, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(decryptFnames, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(decryptWithPassphrase, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(encryptWithPassphrase, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(updateLocalVersionFiles, CAPPluginReturnPromise);
|
||||
)
|
||||
@@ -1,635 +0,0 @@
|
||||
//
|
||||
// FileSync.swift
|
||||
// Logseq
|
||||
//
|
||||
// Created by Mono Wang on 2/24/R4.
|
||||
//
|
||||
|
||||
import Capacitor
|
||||
import Foundation
|
||||
import AWSMobileClient
|
||||
import CryptoKit
|
||||
|
||||
|
||||
// MARK: Global variable
|
||||
|
||||
// Defualts to dev
|
||||
var URL_BASE = URL(string: "https://api-dev.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
|
||||
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
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 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.encryptWithX25519(plaindata, publicKey, armor: true) {
|
||||
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: Metadata type
|
||||
|
||||
public struct SyncMetadata: CustomStringConvertible, Equatable {
|
||||
var md5: String
|
||||
var size: Int
|
||||
var ctime: Int64
|
||||
var mtime: Int64
|
||||
|
||||
public init?(of fileURL: URL) {
|
||||
do {
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey, .contentModificationDateKey,
|
||||
.creationDateKey])
|
||||
guard fileAttributes.isRegularFile! else {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
defer {
|
||||
file.closeFile()
|
||||
}
|
||||
var ctx = Insecure.MD5.init()
|
||||
while autoreleasepool(invoking: {
|
||||
let data = file.readData(ofLength: bufferSize)
|
||||
if data.count > 0 {
|
||||
ctx.update(data: data)
|
||||
return true // continue
|
||||
} else {
|
||||
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))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: FileSync Plugin
|
||||
|
||||
@objc(FileSync)
|
||||
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)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
FNAME_ENCRYPTION_KEY = nil
|
||||
return
|
||||
}
|
||||
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
|
||||
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/")!
|
||||
BUCKET = "logseq-file-sync-bucket-prod"
|
||||
REGION = "us-east-1"
|
||||
case "development", "develop", "dev":
|
||||
URL_BASE = URL(string: "https://api-dev.logseq.com/file-sync/")!
|
||||
BUCKET = "logseq-file-sync-bucket"
|
||||
REGION = "us-east-2"
|
||||
default:
|
||||
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")
|
||||
return
|
||||
}
|
||||
guard var fnames = call.getArray("filePaths") as? [String] else {
|
||||
call.reject("required parameters: filePaths")
|
||||
return
|
||||
}
|
||||
|
||||
let nFiles = fnames.count
|
||||
fnames = fnames.compactMap { $0.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) }
|
||||
if fnames.count != nFiles {
|
||||
call.reject("cannot encrypt \(nFiles - fnames.count) file names")
|
||||
}
|
||||
call.resolve(["value": fnames])
|
||||
}
|
||||
|
||||
@objc func decryptFnames(_ call: CAPPluginCall) {
|
||||
guard fnameEncryptionEnabled() else {
|
||||
call.reject("fname encryption key not set")
|
||||
return
|
||||
}
|
||||
guard var fnames = call.getArray("filePaths") as? [String] else {
|
||||
call.reject("required parameters: filePaths")
|
||||
return
|
||||
}
|
||||
let nFiles = fnames.count
|
||||
fnames = fnames.compactMap { $0.fnameDecrypt(rawKey: FNAME_ENCRYPTION_KEY!)?.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) }
|
||||
if fnames.count != nFiles {
|
||||
call.reject("cannot decrypt \(nFiles - fnames.count) file names")
|
||||
}
|
||||
call.resolve(["value": fnames])
|
||||
}
|
||||
|
||||
@objc func encryptWithPassphrase(_ call: CAPPluginCall) {
|
||||
guard let passphrase = call.getString("passphrase"),
|
||||
let content = call.getString("content") else {
|
||||
call.reject("required parameters: passphrase, content")
|
||||
return
|
||||
}
|
||||
guard let plaintext = content.data(using: .utf8) else {
|
||||
call.reject("cannot decode ciphertext with utf8")
|
||||
return
|
||||
}
|
||||
self.bridge?.saveCall(call)
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
if let encrypted = AgeEncryption.encryptWithPassphrase(plaintext, passphrase, armor: true) {
|
||||
call.resolve(["data": String(data: encrypted, encoding: .utf8) as Any])
|
||||
} else {
|
||||
call.reject("cannot encrypt with passphrase")
|
||||
}
|
||||
self.bridge?.releaseCall(call)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func decryptWithPassphrase(_ call: CAPPluginCall) {
|
||||
guard let passphrase = call.getString("passphrase"),
|
||||
let content = call.getString("content") else {
|
||||
call.reject("required parameters: passphrase, content")
|
||||
return
|
||||
}
|
||||
guard let ciphertext = content.data(using: .utf8) else {
|
||||
call.reject("cannot decode ciphertext with utf8")
|
||||
return
|
||||
}
|
||||
self.bridge?.saveCall(call)
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
if let decrypted = AgeEncryption.decryptWithPassphrase(ciphertext, passphrase) {
|
||||
call.resolve(["data": String(data: decrypted, encoding: .utf8) as Any])
|
||||
} else {
|
||||
call.reject("cannot decrypt with passphrase")
|
||||
}
|
||||
self.bridge?.releaseCall(call)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getLocalFilesMeta(_ call: CAPPluginCall) {
|
||||
// filePaths are url encoded
|
||||
guard let basePath = call.getString("basePath"),
|
||||
let filePaths = call.getArray("filePaths") as? [String] else {
|
||||
call.reject("required paremeters: basePath, filePaths")
|
||||
return
|
||||
}
|
||||
guard let baseURL = URL(string: basePath) else {
|
||||
call.reject("invalid basePath")
|
||||
return
|
||||
}
|
||||
|
||||
var fileMetadataDict: [String: [String: Any]] = [:]
|
||||
for percentFilePath in filePaths {
|
||||
let filePath = percentFilePath.removingPercentEncoding!
|
||||
let url = baseURL.appendingPathComponent(filePath)
|
||||
if let meta = SyncMetadata(of: url) {
|
||||
var metaObj: [String: Any] = ["md5": meta.md5,
|
||||
"size": meta.size,
|
||||
"mtime": meta.mtime]
|
||||
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) {
|
||||
let filePath = fileURL.relativePath(from: baseURL)!
|
||||
var metaObj: [String: Any] = ["md5": meta.md5,
|
||||
"size": meta.size,
|
||||
"mtime": meta.mtime]
|
||||
if fnameEncryptionEnabled() {
|
||||
metaObj["encryptedFname"] = filePath.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!)
|
||||
}
|
||||
fileMetadataDict[filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!] = metaObj
|
||||
}
|
||||
} else if fileURL.isICloudPlaceholder() {
|
||||
try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
call.resolve(["result": fileMetadataDict])
|
||||
}
|
||||
|
||||
|
||||
@objc func renameLocalFile(_ call: CAPPluginCall) {
|
||||
guard let basePath = call.getString("basePath"),
|
||||
let baseURL = URL(string: basePath) else {
|
||||
call.reject("invalid basePath")
|
||||
return
|
||||
}
|
||||
guard let from = call.getString("from") else {
|
||||
call.reject("invalid from file")
|
||||
return
|
||||
}
|
||||
guard let to = call.getString("to") else {
|
||||
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 {
|
||||
call.reject("can not rename file: \(error.localizedDescription)")
|
||||
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)}),
|
||||
let filePaths = call.getArray("filePaths") as? [String],
|
||||
let graphUUID = call.getString("graphUUID") ,
|
||||
let token = call.getString("token") else {
|
||||
call.reject("required paremeters: basePath, filePaths, graphUUID, token")
|
||||
return
|
||||
}
|
||||
|
||||
// [encrypted-fname: original-fname]
|
||||
var encryptedFilePathDict: [String: String] = [:]
|
||||
if fnameEncryptionEnabled() {
|
||||
for filePath in filePaths {
|
||||
if let encryptedPath = filePath.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) {
|
||||
encryptedFilePathDict[encryptedPath] = filePath
|
||||
} else {
|
||||
call.reject("cannot decrypt all file names")
|
||||
}
|
||||
}
|
||||
} 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
|
||||
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)
|
||||
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": ["file": 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 {
|
||||
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 localFileURL.writeData(data: decryptedRawData)
|
||||
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.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],
|
||||
let graphUUID = call.getString("graphUUID") ,
|
||||
let token = call.getString("token") else {
|
||||
call.reject("required paremeters: basePath, filePaths, graphUUID, token")
|
||||
return
|
||||
}
|
||||
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)")
|
||||
call.reject(error.localizedDescription)
|
||||
} 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!)
|
||||
// empty progress handler
|
||||
let progressHandler = {(fraction: Double) in
|
||||
}
|
||||
|
||||
client.download(url: remoteFileURL, progressHandler: progressHandler) {result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
print("debug download \(error) in \(filePath)")
|
||||
case .success(let tempURL):
|
||||
do {
|
||||
let rawData = try Data(contentsOf: tempURL!)
|
||||
guard let decryptedRawData = maybeDecrypt(rawData) else {
|
||||
throw NSError(domain: FileSyncErrorDomain,
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "can not decrypt remote file"])
|
||||
}
|
||||
try localFileURL.writeData(data: decryptedRawData)
|
||||
downloaded.append(filePath)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.notify(queue: .main) {
|
||||
call.resolve(["ok": true, "data": downloaded])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filePaths: Encrypted file paths
|
||||
@objc func deleteRemoteFiles(_ call: CAPPluginCall) {
|
||||
guard var filePaths = call.getArray("filePaths") as? [String],
|
||||
let graphUUID = call.getString("graphUUID"),
|
||||
let token = call.getString("token"),
|
||||
let txid = call.getInt("txid") else {
|
||||
call.reject("required paremeters: filePaths, graphUUID, token, txid")
|
||||
return
|
||||
}
|
||||
guard !filePaths.isEmpty else {
|
||||
call.reject("empty filePaths")
|
||||
return
|
||||
}
|
||||
|
||||
let nFiles = filePaths.count
|
||||
if fnameEncryptionEnabled() {
|
||||
filePaths = filePaths.compactMap { $0.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) }
|
||||
}
|
||||
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 {
|
||||
call.reject("delete \(error!)")
|
||||
return
|
||||
}
|
||||
guard let txid = txid else {
|
||||
call.reject("missing txid")
|
||||
return
|
||||
}
|
||||
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)}),
|
||||
let filePaths = call.getArray("filePaths") as? [String],
|
||||
let graphUUID = call.getString("graphUUID"),
|
||||
let token = call.getString("token"),
|
||||
let txid = call.getInt("txid") else {
|
||||
call.reject("required paremeters: basePath, filePaths, graphUUID, token, txid")
|
||||
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 {
|
||||
self.debugNotification(["event": "upload:error", "data": ["message": "error while refreshing credential: \(error!)"]])
|
||||
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
|
||||
let progressHandler = {(filePath: String, fraction: Double) in
|
||||
self.debugNotification(["event": "upload:progress",
|
||||
"data": ["file": 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!)")
|
||||
return
|
||||
}
|
||||
// 3. update_files
|
||||
guard !uploadedFileKeyDict.isEmpty else {
|
||||
self.debugNotification(["event": "upload:error", "data": ["message": "no file to update"]])
|
||||
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 {
|
||||
call.reject("cannot encrypt file name")
|
||||
return
|
||||
}
|
||||
uploadedFileKeyMd5Dict[encryptedFilePath] = [fileKey, fileMd5Dict[filePath]!]
|
||||
}
|
||||
} else {
|
||||
for (filePath, fileKey) in uploadedFileKeyDict {
|
||||
uploadedFileKeyMd5Dict[filePath] = [fileKey, fileMd5Dict[filePath]!]
|
||||
}
|
||||
}
|
||||
client.updateFiles(uploadedFileKeyMd5Dict) { (txid, error) in
|
||||
guard error == nil else {
|
||||
self.debugNotification(["event": "upload:error", "data": ["message": "error while updating files: \(error!)"]])
|
||||
call.reject("error updateFiles: \(error!)")
|
||||
return
|
||||
}
|
||||
guard let txid = txid else {
|
||||
call.reject("error: missing txid")
|
||||
return
|
||||
}
|
||||
self.debugNotification(["event": "upload:done", "data": ["files": filePaths, "txid": txid]])
|
||||
call.resolve(["ok": true, "files": uploadedFileKeyDict, "txid": txid])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// Payload.swift
|
||||
// Logseq
|
||||
//
|
||||
// Created by Mono Wang on 4/8/R4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetFilesResponse: Decodable {
|
||||
let PresignedFileUrls: [String: String]
|
||||
}
|
||||
|
||||
struct DeleteFilesResponse: Decodable {
|
||||
let TXId: Int
|
||||
let DeleteSuccFiles: [String]
|
||||
let DeleteFailedFiles: [String: String]
|
||||
}
|
||||
|
||||
public struct S3Credential: Decodable {
|
||||
let AccessKeyId: String
|
||||
let Expiration: String
|
||||
let SecretKey: String
|
||||
let SessionToken: String
|
||||
}
|
||||
|
||||
struct GetTempCredentialResponse: Decodable {
|
||||
let Credentials: S3Credential
|
||||
let S3Prefix: String
|
||||
}
|
||||
|
||||
struct UpdateFilesResponse: Decodable {
|
||||
let TXId: Int
|
||||
let UpdateSuccFiles: [String]
|
||||
let UpdateFailedFiles: [String: String]
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
//
|
||||
// SyncClient.swift
|
||||
// Logseq
|
||||
//
|
||||
// Created by Mono Wang on 4/8/R4.
|
||||
//
|
||||
|
||||
import os
|
||||
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
|
||||
] 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: FileSyncErrorDomain, 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: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func getVersionFiles(at filePaths: [String], completionHandler: @escaping ([String: URL], Error?) -> Void) {
|
||||
let url = URL_BASE.appendingPathComponent("get_version_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
|
||||
] 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: FileSyncErrorDomain, 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": "version-download:prepare"])
|
||||
completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
|
||||
} else {
|
||||
// Handle unexpected error
|
||||
completionHandler([:], NSError(domain: FileSyncErrorDomain, 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,
|
||||
"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: FileSyncErrorDomain,
|
||||
code: 409,
|
||||
userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
|
||||
return
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
if response.statusCode != 200 {
|
||||
completionHandler(nil, NSError(domain: FileSyncErrorDomain,
|
||||
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: FileSyncErrorDomain, 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: FileSyncErrorDomain,
|
||||
code: 409,
|
||||
userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
|
||||
return
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
if response.statusCode != 200 {
|
||||
completionHandler(nil, NSError(domain: FileSyncErrorDomain,
|
||||
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: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "update fail for some files: \(resp?.UpdateFailedFiles.debugDescription)"]))
|
||||
}
|
||||
} else {
|
||||
// Handle unexpected error
|
||||
completionHandler(nil, NSError(domain: FileSyncErrorDomain, 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: FileSyncErrorDomain, code: 401, userInfo: [NSLocalizedDescriptionKey: "unauthorized"]))
|
||||
return
|
||||
}
|
||||
if response.statusCode != 200 {
|
||||
completionHandler(nil, NSError(domain: FileSyncErrorDomain,
|
||||
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: FileSyncErrorDomain, code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
// [filePath, Key]
|
||||
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
|
||||
} else if REGION == "us-east-1" {
|
||||
region = .USEast1
|
||||
} // TODO: string to REGION conversion
|
||||
|
||||
let configuration = AWSServiceConfiguration(region: region, credentialsProvider: credentialsProvider)
|
||||
configuration?.timeoutIntervalForRequest = 5.0
|
||||
configuration?.timeoutIntervalForResource = 5.0
|
||||
|
||||
let group = DispatchGroup()
|
||||
var keyFileDict: [String: String] = [:]
|
||||
var fileKeyDict: [String: String] = [:]
|
||||
var fileMd5Dict: [String: String] = [:]
|
||||
|
||||
|
||||
for (filePath, fileLocalURL) in files {
|
||||
guard let rawData = try? Data(contentsOf: fileLocalURL) 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
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||
|
||||
# pod specs for AgeEncryption
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
|
||||
platform :ios, '13.0'
|
||||
use_frameworks!
|
||||
|
||||
@@ -20,6 +23,7 @@ def capacitor_pods
|
||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||
pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'LogseqCapacitorFileSync', :path => '../../node_modules/@logseq/capacitor-file-sync'
|
||||
pod 'CapacitorVoiceRecorder', :path => '../../node_modules/capacitor-voice-recorder'
|
||||
pod 'SendIntent', :path => '../../node_modules/send-intent'
|
||||
end
|
||||
@@ -27,9 +31,6 @@ end
|
||||
target 'Logseq' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
pod 'AWSMobileClient'
|
||||
pod 'AWSS3'
|
||||
pod 'AgeEncryption', :podspec => './LogseqSpecs/AgeEncryption.podspec'
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"@capacitor/status-bar": "^4.0.0",
|
||||
"@excalidraw/excalidraw": "0.10.0",
|
||||
"@kanru/rage-wasm": "0.2.1",
|
||||
"@logseq/capacitor-file-sync": "0.0.5",
|
||||
"@logseq/react-tweet-embed": "1.3.1-1",
|
||||
"@sentry/react": "^6.18.2",
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns frontend.mobile.util
|
||||
(:require ["@capacitor/core" :refer [Capacitor registerPlugin]]
|
||||
["@capacitor/splash-screen" :refer [SplashScreen]]
|
||||
["@logseq/capacitor-file-sync" :refer [FileSync]]
|
||||
[clojure.string :as string]
|
||||
[promesa.core :as p]))
|
||||
|
||||
@@ -24,14 +25,11 @@
|
||||
(defonce folder-picker (registerPlugin "FolderPicker"))
|
||||
(when (native-ios?)
|
||||
(defonce ios-utils (registerPlugin "Utils"))
|
||||
(defonce ios-file-container (registerPlugin "FileContainer"))
|
||||
(defonce file-sync (registerPlugin "FileSync")))
|
||||
(defonce ios-file-container (registerPlugin "FileContainer")))
|
||||
|
||||
(when (native-android?)
|
||||
(defonce file-sync (registerPlugin "FileSync")))
|
||||
|
||||
;; NOTE: both iOS and android share the same FsWatcher API
|
||||
;; NOTE: both iOS and android share the same API
|
||||
(when (native-platform?)
|
||||
(defonce file-sync FileSync)
|
||||
(defonce fs-watcher (registerPlugin "FsWatcher")))
|
||||
|
||||
(defn hide-splash []
|
||||
|
||||
@@ -480,6 +480,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@kanru/rage-wasm/-/rage-wasm-0.2.1.tgz#dd8fdd3133992c42bf68c0086d8cad40a13bc329"
|
||||
integrity sha512-sYi4F2mL6Mpcz7zbS4myasw11xLBEbgZkDMRVg9jNxTKt6Ct/LT7/vCHDmEzAFcPcPqixD5De6Ql3bJijAX0/w==
|
||||
|
||||
"@logseq/capacitor-file-sync@0.0.5":
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@logseq/capacitor-file-sync/-/capacitor-file-sync-0.0.5.tgz#e391d3ec9eb65d200fa5af18738913d19a223f39"
|
||||
integrity sha512-3cdpwt5lsEE7occQwJKaalaKGXxgucSDzFNeRkRQMylRehlZskAQtCjgDFR7Wt3tBQZdLZmjpgj7ioYQesWbTA==
|
||||
|
||||
"@logseq/react-tweet-embed@1.3.1-1":
|
||||
version "1.3.1-1"
|
||||
resolved "https://registry.yarnpkg.com/@logseq/react-tweet-embed/-/react-tweet-embed-1.3.1-1.tgz#119d22be8234de006fc35c3fa2a36f85634c5be6"
|
||||
|
||||
Reference in New Issue
Block a user