diff --git a/lib/rclone/init.ts b/lib/rclone/init.ts new file mode 100644 index 0000000..4f8cd73 --- /dev/null +++ b/lib/rclone/init.ts @@ -0,0 +1,121 @@ +import { invoke } from '@tauri-apps/api/core' +import { BaseDirectory, appLocalDataDir } from '@tauri-apps/api/path' +import { tempDir } from '@tauri-apps/api/path' +import { copyFile, mkdir, remove } from '@tauri-apps/plugin-fs' +import { writeFile } from '@tauri-apps/plugin-fs' +import { fetch } from '@tauri-apps/plugin-http' +import { platform } from '@tauri-apps/plugin-os' +import { Command } from '@tauri-apps/plugin-shell' + +/** + * Checks if rclone is installed and accessible from the system PATH + * @returns {Promise} True if rclone is installed and working + */ +export async function checkRcloneInstalled() { + const output = await Command.create('rclone').execute() + // console.log('[checkRcloneInstalled] output', output) + return ( + output.stdout.includes('Available commands') || output.stderr.includes('Available commands') + ) +} + +/** + * Checks if rclone is bundled with the application in the app's local data directory + * @returns {Promise} True if bundled rclone is present and working + */ +export async function checkRcloneBundled() { + const output = await Command.create('./rclone', [], { + cwd: `${await appLocalDataDir()}`, + }).execute() + // console.log('[checkRcloneBundled] output', output) + return ( + output.stdout.includes('Available commands') || output.stderr.includes('Available commands') + ) +} + +/** + * Downloads and provisions the latest version of rclone for the current platform + * @throws {Error} If architecture detection fails or installation is unsuccessful + * @returns {Promise} + */ +export async function provisionRclone() { + const currentVersionString = await fetch('https://downloads.rclone.org/version.txt').then( + (res) => res.text() + ) + console.log('currentVersionString', currentVersionString) + + const currentVersion = currentVersionString.split('v')?.[1]?.trim() + + if (!currentVersion) { + console.error('Failed to get latest version') + return + } + console.log('currentVersion', currentVersion) + + const currentPlatform = platform() + console.log('currentPlatform', currentPlatform) + + const currentOs = + currentPlatform === 'macos' ? 'osx' : currentPlatform === 'windows' ? 'win' : 'linux' + console.log('currentOs', currentOs) + + let tempDirPath = await tempDir() + if (tempDirPath.endsWith('/')) { + tempDirPath = tempDirPath.slice(0, -1) + } + console.log('tempDirPath', tempDirPath) + + const arch = (await invoke('get_arch')) as 'arm64' | 'amd64' | '386' | 'unknown' + console.log('arch', arch) + + if (arch === 'unknown') { + throw new Error('Failed to get architecture, please try again later.') + } + + const downloadUrl = `https://downloads.rclone.org/v${currentVersion}/rclone-v${currentVersion}-${currentOs}-${arch}.zip` + console.log('downloadUrl', downloadUrl) + + const downloadedFile = await fetch(downloadUrl).then((res) => res.arrayBuffer()) + + await remove('rclone', { + recursive: true, + baseDir: BaseDirectory.Temp, + }) + + await mkdir('rclone', { + baseDir: BaseDirectory.Temp, + }) + + const zipPath = `${tempDirPath}/rclone/rclone-v${currentVersion}-${currentOs}-${arch}.zip` + console.log('zipPath', zipPath) + + await writeFile(zipPath, new Uint8Array(downloadedFile)) + + await invoke('unzip_file', { + zipPath, + outputFolder: `${tempDirPath}/rclone/rclone-ui`, + }) + + console.log('Successfully unzipped file') + + const unarchivedPath = `${tempDirPath}/rclone/rclone-ui/rclone-v${currentVersion}-${currentOs}-${arch}` + console.log('unarchivedPath', unarchivedPath) + + // const unarchivedFolder = await readDir(unarchivedPath) + // console.log('unarchivedFolder', unarchivedFolder) + + const rcloneBinaryPath = unarchivedPath + '/' + 'rclone' + console.log('rcloneBinaryPath', rcloneBinaryPath) + + if (!rcloneBinaryPath) { + throw new Error('Could not find rclone binary in zip') + } + + await copyFile(rcloneBinaryPath, `${await appLocalDataDir()}/rclone`) + + const hasInstalled = await checkRcloneInstalled() + + if (!hasInstalled) { + throw new Error('Failed to install rclone') + } +} diff --git a/main.ts b/main.ts index 7544e5e..5037c69 100644 --- a/main.ts +++ b/main.ts @@ -1,9 +1,12 @@ +import { appLocalDataDir } from '@tauri-apps/api/path' import { getCurrentWindow } from '@tauri-apps/api/window' -import { message } from '@tauri-apps/plugin-dialog' +import { confirm, message } from '@tauri-apps/plugin-dialog' +import {} from '@tauri-apps/plugin-fs' import { debug, error, info, trace, warn } from '@tauri-apps/plugin-log' import { exit } from '@tauri-apps/plugin-process' import { Command } from '@tauri-apps/plugin-shell' -import { listRemotes } from './lib/rclone' +import { listRemotes } from './lib/rclone/api' +import { checkRcloneBundled, checkRcloneInstalled, provisionRclone } from './lib/rclone/init' import { useStore } from './lib/store' import { initLoadingTray, initTray, rebuildTrayMenu } from './lib/tray' @@ -31,23 +34,69 @@ console.log('main') console.error('main') async function startRclone() { + let hasLocalRclone = false + try { + hasLocalRclone = await checkRcloneInstalled() + console.log('hasLocalRclone', hasLocalRclone) + } catch (error) { + console.error('Failed to check if rclone is installed', error) + } + console.log('hasLocalRclone', hasLocalRclone) + + let hasBundledRclone = false + try { + hasBundledRclone = await checkRcloneBundled() + console.log('hasBundledRclone', hasBundledRclone) + } catch (error) { + console.error('Failed to check if rclone is bundled', error) + } + console.log('hasBundledRclone', hasBundledRclone) + + if (!hasLocalRclone && !hasBundledRclone) { + try { + await provisionRclone() + } catch (error) { + await confirm(error?.message || 'Failed to provision rclone, please try again later.', { + title: 'Error', + kind: 'error', + }) + return await exit(0) + } + } + try { const remotes = await listRemotes() - console.log('rclone already running', remotes) + console.log('rclone rcd already running') useStore.setState({ rcloneLoaded: true }) useStore.setState({ remotes: remotes }) return - } catch (e) { - console.error('Failed to start rclone', e) - } + } catch {} - const command = Command.sidecar('binaries/rclone', [ - 'rcd', - '--rc-no-auth', - '--rc-serve', - // '-rc-addr', - // ':5572', - ]) + //! this works + // const command = Command.sidecar('binaries/rclone', [ + // 'rcd', + // '--rc-no-auth', + // '--rc-serve', + // // '-rc-addr', + // // ':5572', + // ]) + + //! sidecar does not work with global or appdata binaries + const command = Command.create( + hasLocalRclone ? 'rclone' : './rclone', + [ + 'rcd', + '--rc-no-auth', + '--rc-serve', + // '-rc-addr', + // ':5572', + ], + hasLocalRclone + ? undefined + : { + cwd: `${await appLocalDataDir()}`, + } + ) command.addListener('close', async (event) => { console.log('close', event) @@ -65,11 +114,11 @@ async function startRclone() { console.log('error', event) }) - // console.log('command', command) + //! so we have to do this: + //! not await the call + const childProcess = command.execute() - const childProcess = await command.spawn() - - await new Promise((resolve) => setTimeout(resolve, 100)) + await new Promise((resolve) => setTimeout(resolve, 200)) useStore.setState({ rcloneLoaded: true }) @@ -94,7 +143,3 @@ getCurrentWindow().listen('rebuild-tray', async (e) => { initLoadingTray() .then(() => startRclone()) .then(() => initTray()) - -// await initLoadingTray() -// await startRclone() -// await initTray() diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c63b259..540a6d4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.7.8" @@ -113,6 +124,7 @@ dependencies = [ "tauri-plugin-shell", "tauri-plugin-single-instance", "tauri-plugin-store", + "zip", ] [[package]] @@ -347,6 +359,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -522,6 +540,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -595,6 +634,8 @@ version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -656,6 +697,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "cocoa" version = "0.26.0" @@ -705,6 +756,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.4.0" @@ -953,6 +1010,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1771,6 +1829,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.11" @@ -2112,6 +2179,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -2194,6 +2270,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.76" @@ -2942,12 +3027,35 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -6391,6 +6499,55 @@ dependencies = [ "syn 2.0.95", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "4.0.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d3c68a9..ea68429 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,6 +35,7 @@ tauri-plugin-process = "2" tauri-plugin-notification = { version = "2.0.0", features = [ "windows7-compat" ] } tauri-plugin-os = "2" fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } +zip = "0.6.6" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-single-instance = "2.2.0" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 9663700..2f103f2 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -167,6 +167,11 @@ { "identifier": "shell:allow-execute", "allow": [ + { + "name": "./rclone", + "cmd": "./rclone", + "args": true + }, { "name": "rclone", "cmd": "rclone", @@ -181,21 +186,6 @@ "name": "mount", "cmd": "mount", "args": true - }, - { - "name": "qjs", - "cmd": "qjs", - "args": true - }, - { - "name": "fuse", - "cmd": "go-nfsv4", - "args": true - }, - { - "name": "mount_nfs", - "cmd": "mount_nfs", - "args": true } ] }, @@ -206,6 +196,16 @@ "name": "binaries/rclone", "args": true, "sidecar": true + }, + { + "name": "./rclone", + "args": true, + "sidecar": true + }, + { + "name": "rclone", + "args": true, + "sidecar": true } ] }, @@ -235,6 +235,22 @@ } ] }, + { + "identifier": "fs:allow-mkdir", + "allow": [ + { + "path": "/**" + } + ] + }, + { + "identifier": "fs:allow-remove", + "allow": [ + { + "path": "/**" + } + ] + }, "opener:default", "positioner:default", { @@ -245,6 +261,12 @@ }, { "url": "https://www.google.com" + }, + { + "url": "https://downloads.rclone.org/**" + }, + { + "url": "https://github.com/**" } ] }, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 040b96e..d3c784e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,6 +6,65 @@ // cwd: String, // } + + +use std::fs::{self, File}; +use std::path::Path; +use zip::ZipArchive; + +#[tauri::command] +fn unzip_file(zip_path: &str, output_folder: &str) -> Result<(), String> { + // Open the zip file + let file = File::open(zip_path).map_err(|e| e.to_string())?; + + // Create output directory if it doesn't exist + fs::create_dir_all(output_folder).map_err(|e| e.to_string())?; + + // Create ZIP archive reader + let mut archive = ZipArchive::new(file).map_err(|e| e.to_string())?; + + // Extract everything + for i in 0..archive.len() { + let mut file = archive.by_index(i).map_err(|e| e.to_string())?; + let outpath = Path::new(output_folder).join(file.name()); + + if file.name().ends_with('/') { + fs::create_dir_all(&outpath).map_err(|e| e.to_string())?; + } else { + if let Some(p) = outpath.parent() { + fs::create_dir_all(p).map_err(|e| e.to_string())?; + } + let mut outfile = File::create(&outpath).map_err(|e| e.to_string())?; + std::io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?; + } + + // Get and set permissions (Unix only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)) + .map_err(|e| e.to_string())?; + } + } + } + + Ok(()) +} + +#[tauri::command] +fn get_arch() -> String { + let arch = std::env::consts::ARCH; + + match arch { + "aarch64" => "arm64".to_string(), + "x86_64" => "amd64".to_string(), + "i386" => "386".to_string(), + _ => "unknown".to_string(), + } +} + + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let mut app = tauri::Builder::default() @@ -30,6 +89,7 @@ pub fn run() { .plugin(tauri_plugin_log::Builder::new().build()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_opener::init()) + .invoke_handler(tauri::generate_handler![unzip_file, get_arch]) .setup(|_app| Ok(())) // .setup(|app| { // if cfg!(debug_assertions) {