download & provision

This commit is contained in:
FTCHD
2025-01-27 18:21:35 +02:00
parent 4a0d036f4d
commit bf813e5804
6 changed files with 442 additions and 36 deletions
+121
View File
@@ -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<boolean>} 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<boolean>} 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<void>}
*/
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')
}
}
+66 -21
View File
@@ -1,9 +1,12 @@
import { appLocalDataDir } from '@tauri-apps/api/path'
import { getCurrentWindow } from '@tauri-apps/api/window' 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 { debug, error, info, trace, warn } from '@tauri-apps/plugin-log'
import { exit } from '@tauri-apps/plugin-process' import { exit } from '@tauri-apps/plugin-process'
import { Command } from '@tauri-apps/plugin-shell' 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 { useStore } from './lib/store'
import { initLoadingTray, initTray, rebuildTrayMenu } from './lib/tray' import { initLoadingTray, initTray, rebuildTrayMenu } from './lib/tray'
@@ -31,23 +34,69 @@ console.log('main')
console.error('main') console.error('main')
async function startRclone() { 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 { try {
const remotes = await listRemotes() const remotes = await listRemotes()
console.log('rclone already running', remotes) console.log('rclone rcd already running')
useStore.setState({ rcloneLoaded: true }) useStore.setState({ rcloneLoaded: true })
useStore.setState({ remotes: remotes }) useStore.setState({ remotes: remotes })
return return
} catch (e) { } catch {}
console.error('Failed to start rclone', e)
}
const command = Command.sidecar('binaries/rclone', [ //! this works
'rcd', // const command = Command.sidecar('binaries/rclone', [
'--rc-no-auth', // 'rcd',
'--rc-serve', // '--rc-no-auth',
// '-rc-addr', // '--rc-serve',
// ':5572', // // '-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) => { command.addListener('close', async (event) => {
console.log('close', event) console.log('close', event)
@@ -65,11 +114,11 @@ async function startRclone() {
console.log('error', event) 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, 200))
await new Promise((resolve) => setTimeout(resolve, 100))
useStore.setState({ rcloneLoaded: true }) useStore.setState({ rcloneLoaded: true })
@@ -94,7 +143,3 @@ getCurrentWindow().listen('rebuild-tray', async (e) => {
initLoadingTray() initLoadingTray()
.then(() => startRclone()) .then(() => startRclone())
.then(() => initTray()) .then(() => initTray())
// await initLoadingTray()
// await startRclone()
// await initTray()
+157
View File
@@ -17,6 +17,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 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]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.8" version = "0.7.8"
@@ -113,6 +124,7 @@ dependencies = [
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tauri-plugin-store", "tauri-plugin-store",
"zip",
] ]
[[package]] [[package]]
@@ -347,6 +359,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -522,6 +540,27 @@ dependencies = [
"serde", "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]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.18.5" version = "0.18.5"
@@ -595,6 +634,8 @@ version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
dependencies = [ dependencies = [
"jobserver",
"libc",
"shlex", "shlex",
] ]
@@ -656,6 +697,16 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "cocoa" name = "cocoa"
version = "0.26.0" version = "0.26.0"
@@ -705,6 +756,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@@ -953,6 +1010,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
"subtle",
] ]
[[package]] [[package]]
@@ -1771,6 +1829,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "home" name = "home"
version = "0.5.11" version = "0.5.11"
@@ -2112,6 +2179,15 @@ dependencies = [
"cfb", "cfb",
] ]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.10.1" version = "2.10.1"
@@ -2194,6 +2270,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.76" version = "0.3.76"
@@ -2942,12 +3027,35 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "pathdiff" name = "pathdiff"
version = "0.2.3" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@@ -6391,6 +6499,55 @@ dependencies = [
"syn 2.0.95", "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]] [[package]]
name = "zvariant" name = "zvariant"
version = "4.0.0" version = "4.0.0"
+1
View File
@@ -35,6 +35,7 @@ tauri-plugin-process = "2"
tauri-plugin-notification = { version = "2.0.0", features = [ "windows7-compat" ] } tauri-plugin-notification = { version = "2.0.0", features = [ "windows7-compat" ] }
tauri-plugin-os = "2" tauri-plugin-os = "2"
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } 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] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.2.0" tauri-plugin-single-instance = "2.2.0"
+37 -15
View File
@@ -167,6 +167,11 @@
{ {
"identifier": "shell:allow-execute", "identifier": "shell:allow-execute",
"allow": [ "allow": [
{
"name": "./rclone",
"cmd": "./rclone",
"args": true
},
{ {
"name": "rclone", "name": "rclone",
"cmd": "rclone", "cmd": "rclone",
@@ -181,21 +186,6 @@
"name": "mount", "name": "mount",
"cmd": "mount", "cmd": "mount",
"args": true "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", "name": "binaries/rclone",
"args": true, "args": true,
"sidecar": 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", "opener:default",
"positioner:default", "positioner:default",
{ {
@@ -245,6 +261,12 @@
}, },
{ {
"url": "https://www.google.com" "url": "https://www.google.com"
},
{
"url": "https://downloads.rclone.org/**"
},
{
"url": "https://github.com/**"
} }
] ]
}, },
+60
View File
@@ -6,6 +6,65 @@
// cwd: String, // 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)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let mut app = tauri::Builder::default() let mut app = tauri::Builder::default()
@@ -30,6 +89,7 @@ pub fn run() {
.plugin(tauri_plugin_log::Builder::new().build()) .plugin(tauri_plugin_log::Builder::new().build())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![unzip_file, get_arch])
.setup(|_app| Ok(())) .setup(|_app| Ok(()))
// .setup(|app| { // .setup(|app| {
// if cfg!(debug_assertions) { // if cfg!(debug_assertions) {