use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use searpc::{SearpcClient, UnixSocketTransport};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::{debug, error, info, trace, warn};
mod config;
mod http_client;
mod rpc_client;
use config::{check_daemon_running, init_config, DeviceIdManager, UserConfig};
use http_client::SeafileHttpClient;
use rpc_client::SeafileRpc as _;
#[derive(Parser)]
#[command(name = "seaf-cli")]
#[command(about = "Command line interface for Seafile client", long_about = None)]
struct Cli {
#[arg(short = 'c', long = "confdir", global = true)]
confdir: Option<PathBuf>,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Init {
#[arg(short = 'd', long)]
dir: PathBuf,
},
Start,
Stop,
List {
#[arg(long)]
json: bool,
},
ListRemote {
#[arg(long)]
json: bool,
#[arg(short = 's', long)]
server: Option<String>,
#[arg(short = 'u', long)]
username: Option<String>,
#[arg(short = 'p', long)]
password: Option<String>,
#[arg(short = 'T', long)]
token: Option<String>,
#[arg(short = 'a', long)]
tfa: Option<String>,
#[arg(short = 'C')]
user_config: Option<PathBuf>,
},
Status,
Download {
#[arg(short = 'l', long)]
library: String,
#[arg(short = 's', long)]
server: Option<String>,
#[arg(short = 'd', long)]
dir: Option<PathBuf>,
#[arg(short = 'u', long)]
username: Option<String>,
#[arg(short = 'p', long)]
password: Option<String>,
#[arg(short = 'T', long)]
token: Option<String>,
#[arg(short = 'a', long)]
tfa: Option<String>,
#[arg(short = 'e', long)]
libpasswd: Option<String>,
#[arg(short = 'C')]
user_config: Option<PathBuf>,
},
DownloadByName {
#[arg(short = 'L', long)]
libraryname: String,
#[arg(short = 's', long)]
server: Option<String>,
#[arg(short = 'd', long)]
dir: Option<PathBuf>,
#[arg(short = 'u', long)]
username: Option<String>,
#[arg(short = 'p', long)]
password: Option<String>,
#[arg(short = 'T', long)]
token: Option<String>,
#[arg(short = 'a', long)]
tfa: Option<String>,
#[arg(short = 'e', long)]
libpasswd: Option<String>,
#[arg(short = 'C')]
user_config: Option<PathBuf>,
},
Sync {
#[arg(short = 'l', long)]
library: String,
#[arg(short = 's', long)]
server: Option<String>,
#[arg(short = 'd', long)]
folder: PathBuf,
#[arg(short = 'u', long)]
username: Option<String>,
#[arg(short = 'p', long)]
password: Option<String>,
#[arg(short = 'T', long)]
token: Option<String>,
#[arg(short = 'a', long)]
tfa: Option<String>,
#[arg(short = 'e', long)]
libpasswd: Option<String>,
#[arg(short = 'C')]
user_config: Option<PathBuf>,
},
Desync {
#[arg(short = 'd', long)]
folder: PathBuf,
},
Create {
#[arg(short = 'n', long)]
name: String,
#[arg(short = 't', long)]
desc: String,
#[arg(short = 'e', long)]
libpasswd: Option<String>,
#[arg(short = 's', long)]
server: Option<String>,
#[arg(short = 'u', long)]
username: Option<String>,
#[arg(short = 'p', long)]
password: Option<String>,
#[arg(short = 'T', long)]
token: Option<String>,
#[arg(short = 'a', long)]
tfa: Option<String>,
#[arg(short = 'C')]
user_config: Option<PathBuf>,
},
Config {
#[arg(short = 'k', long)]
key: String,
#[arg(short = 'v', long)]
value: Option<String>,
},
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_target(false)
.with_file(true)
.with_line_number(true)
.compact()
.init();
let cli = Cli::parse();
debug!("Parsed command line arguments");
if let Commands::Init { dir } = &cli.command {
let conf_dir = if let Some(dir) = cli.confdir.clone() {
dir
} else {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE")) .context("HOME or USERPROFILE environment variable not set")?;
PathBuf::from(home).join(".ccnet")
};
init_config(&conf_dir, dir)?;
return Ok(());
}
let conf_dir = match cli.confdir {
Some(dir) => dir,
None => {
let home = std::env::var("HOME")?;
PathBuf::from(home).join(".ccnet")
}
};
if matches!(cli.command, Commands::Start) {
return handle_start(&conf_dir);
}
let seafile_ini = conf_dir.join("seafile.ini");
let seafile_datadir = fs::read_to_string(&seafile_ini)
.context("Failed to read seafile.ini")?
.trim()
.to_string();
let datadir_path = PathBuf::from(&seafile_datadir);
match cli.command {
Commands::Init { .. } => unreachable!(),
Commands::Start => unreachable!(),
Commands::List { json } => {
debug!("Executing list command");
let socket_path = datadir_path.join("seafile.sock");
trace!(socket = %socket_path.display(), "Connecting to RPC server");
let transport = UnixSocketTransport::connect(&socket_path, "seafile-rpcserver")?;
let mut client = SearpcClient::new(transport);
debug!("Fetching repository list");
let repos = client.get_repo_list(-1, -1)?;
info!(count = repos.len(), "Retrieved {} repositories", repos.len());
if json {
println!("{}", serde_json::to_string_pretty(&repos)?);
} else {
println!("Name\tID\tPath");
for repo in repos {
println!("{}\t{}\t{}", repo.name, repo.id, repo.worktree);
}
}
}
Commands::ListRemote {
json,
server,
username,
password,
token,
tfa,
user_config,
} => {
debug!("Executing list-remote command");
let user_cfg = UserConfig::load(user_config.as_deref())?;
let server_url = server.or(user_cfg.server).context("Server URL required")?;
let username = username.or(user_cfg.user).context("Username required")?;
debug!(server = %server_url, user = %username, "Resolved server and user");
let token = if let Some(t) = token.or(user_cfg.token) {
debug!("Using provided token");
t
} else {
debug!("No token provided, authenticating");
let password = if let Some(p) = password {
p
} else {
rpassword::prompt_password(format!("Enter password for user {}: ", username))?
};
let device_mgr = DeviceIdManager::new(&conf_dir, &datadir_path);
let device_id = device_mgr.get_device_id()?;
let http_client = SeafileHttpClient::new(&server_url);
http_client.get_token(&username, &password, &device_id, tfa.as_deref())?
};
let http_client = SeafileHttpClient::new(&server_url);
debug!("Fetching remote repository list");
let repos = http_client.list_repos(&token)?;
info!(count = repos.len(), "Retrieved {} remote repositories", repos.len());
if json {
println!("{}", serde_json::to_string_pretty(&repos)?);
} else {
println!("Name\tID");
for repo in repos {
println!("{}\t{}", repo.name, repo.id);
}
}
}
Commands::Status => {
debug!("Executing status command");
let socket_path = datadir_path.join("seafile.sock");
trace!(socket = %socket_path.display(), "Connecting to RPC server");
let transport = UnixSocketTransport::connect(&socket_path, "seafile-rpcserver")?;
let mut client = SearpcClient::new(transport);
debug!("Fetching clone tasks");
let tasks = client.get_clone_tasks()?;
trace!(count = tasks.len(), "Found {} clone tasks", tasks.len());
println!("# {:<50}\t{:<20}\t{:<20}", "Name", "Status", "Progress");
for task in tasks {
trace!(repo = %task.repo_name, state = %task.state, "Processing clone task");
match task.state.as_str() {
"fetch" => {
if let Ok(tx_task) = client.find_transfer_task(&task.repo_id) {
let progress = if tx_task.block_total > 0 {
(tx_task.block_done as f64 / tx_task.block_total as f64) * 100.0
} else {
0.0
};
let rate = tx_task.rate as f64 / 1024.0;
debug!(repo = %task.repo_name, progress = %format!("{:.1}%", progress), rate = %format!("{:.1}KB/s", rate), "Download in progress");
println!(
"{:<50}\t{:<20}\t{:.1}%, {:.1}KB/s",
task.repo_name, "downloading", progress, rate
);
}
}
"error" => {
let err = client.sync_error_id_to_str(task.error)?;
error!(repo = %task.repo_name, error = %err, "Clone task error");
println!("{:<50}\t{:<20}\t{:<20}", task.repo_name, "error", err);
}
"done" => {
trace!(repo = %task.repo_name, "Clone task completed");
}
_ => {
println!("{:<50}\t{:<20}", task.repo_name, task.state);
}
}
}
debug!("Fetching repository sync status");
let repos = client.get_repo_list(-1, -1)?;
trace!(count = repos.len(), "Found {} repositories", repos.len());
for repo in repos {
let auto_sync = client.is_auto_sync_enabled()?;
if !auto_sync || !repo.auto_sync {
trace!(repo = %repo.name, "Auto sync disabled");
println!("{:<50}\t{:<20}", repo.name, "auto sync disabled");
continue;
}
match client.get_repo_sync_task(&repo.id) {
Ok(Some(task)) => match task.state.as_str() {
"uploading" | "downloading" => {
if let Ok(tx_task) = client.find_transfer_task(&repo.id) {
let progress = if tx_task.block_total > 0 {
(tx_task.block_done as f64 / tx_task.block_total as f64) * 100.0
} else {
0.0
};
let rate = tx_task.rate as f64 / 1024.0;
debug!(repo = %repo.name, state = %task.state, progress = %format!("{:.1}%", progress), "Transfer in progress");
println!(
"{:<50}\t{:<20}\t{:.1}%, {:.1}KB/s",
repo.name, task.state, progress, rate
);
}
}
"error" => {
let err = client.sync_error_id_to_str(task.error)?;
error!(repo = %repo.name, error = %err, "Sync error");
println!("{:<50}\t{:<20}\t{:<20}", repo.name, "error", err);
}
_ => {
trace!(repo = %repo.name, state = %task.state, "Sync state");
println!("{:<50}\t{:<20}", repo.name, task.state);
}
},
Ok(None) | Err(_) => {
trace!(repo = %repo.name, "Waiting for sync");
println!("{:<50}\t{:<20}", repo.name, "waiting for sync");
}
}
}
}
Commands::Download {
library,
server,
dir,
username,
password,
token,
tfa,
libpasswd,
user_config,
} => {
debug!(library = %library, "Executing download command");
let socket_path = datadir_path.join("seafile.sock");
trace!(socket = %socket_path.display(), "Connecting to RPC server");
let transport = UnixSocketTransport::connect(&socket_path, "seafile-rpcserver")?;
let mut client = SearpcClient::new(transport);
handle_download(
&mut client,
&conf_dir,
&datadir_path,
&library,
server.as_deref(),
dir.as_deref(),
username.as_deref(),
password.as_deref(),
token.as_deref(),
tfa.as_deref(),
libpasswd.as_deref(),
user_config.as_deref(),
)?;
}
Commands::DownloadByName {
libraryname,
server,
dir,
username,
password,
token,
tfa,
libpasswd,
user_config,
} => {
debug!(library_name = %libraryname, "Executing download-by-name command");
let user_cfg = UserConfig::load(user_config.as_deref())?;
let server_url = server.or(user_cfg.server).context("Server URL required")?;
let username = username.or(user_cfg.user).context("Username required")?;
debug!(server = %server_url, user = %username, "Resolved server and user");
let token = get_or_create_token(
&server_url,
&username,
password.as_deref(),
token.as_deref(),
tfa.as_deref(),
user_cfg.token.as_deref(),
&conf_dir,
&datadir_path,
)?;
let http_client = SeafileHttpClient::new(&server_url);
let repos = http_client.list_repos(&token)?;
debug!("Searching for library by name: {}", libraryname);
let library_id = repos
.iter()
.find(|r| r.name == libraryname)
.map(|r| r.id.clone())
.context("Library not found")?;
info!(library_id = %library_id, library_name = %libraryname, "Found library");
let socket_path = datadir_path.join("seafile.sock");
trace!(socket = %socket_path.display(), "Connecting to RPC server");
let transport = UnixSocketTransport::connect(&socket_path, "seafile-rpcserver")?;
let mut client = SearpcClient::new(transport);
handle_download(
&mut client,
&conf_dir,
&datadir_path,
&library_id,
Some(&server_url),
dir.as_deref(),
Some(&username),
password.as_deref(),
Some(&token),
tfa.as_deref(),
libpasswd.as_deref(),
user_config.as_deref(),
)?;
}
Commands::Sync {
library,
server,
folder,
username,
password,
token,
tfa,
libpasswd,
user_config,
} => {
debug!(library = %library, folder = %folder.display(), "Executing sync command");
if !folder.exists() {
error!(folder = %folder.display(), "Local directory does not exist");
anyhow::bail!("Local directory does not exist");
}
let socket_path = datadir_path.join("seafile.sock");
trace!(socket = %socket_path.display(), "Connecting to RPC server");
let transport = UnixSocketTransport::connect(&socket_path, "seafile-rpcserver")?;
let mut client = SearpcClient::new(transport);
handle_sync(
&mut client,
&conf_dir,
&datadir_path,
&library,
server.as_deref(),
&folder,
username.as_deref(),
password.as_deref(),
token.as_deref(),
tfa.as_deref(),
libpasswd.as_deref(),
user_config.as_deref(),
)?;
}
Commands::Desync { folder } => {
debug!(folder = %folder.display(), "Executing desync command");
let socket_path = datadir_path.join("seafile.sock");
trace!(socket = %socket_path.display(), "Connecting to RPC server");
let transport = UnixSocketTransport::connect(&socket_path, "seafile-rpcserver")?;
let mut client = SearpcClient::new(transport);
let repo_path = folder.canonicalize()?;
debug!(canonical_path = %repo_path.display(), "Resolved folder path");
let repos = client.get_repo_list(-1, -1)?;
let repo = repos
.iter()
.find(|r| PathBuf::from(&r.worktree) == repo_path)
.context("Not a library")?;
info!(repo_id = %repo.id, repo_name = %repo.name, "Desynchronizing library");
println!("Desynchronize {}", repo.name);
client.remove_repo(&repo.id)?;
debug!("Library desynchronized successfully");
}
Commands::Create {
name,
desc,
libpasswd,
server,
username,
password,
token,
tfa,
user_config,
} => {
debug!(name = %name, encrypted = libpasswd.is_some(), "Executing create command");
let user_cfg = UserConfig::load(user_config.as_deref())?;
let server_url = server.or(user_cfg.server).context("Server URL required")?;
let username = username.or(user_cfg.user).context("Username required")?;
debug!(server = %server_url, user = %username, "Resolved server and user");
let token = get_or_create_token(
&server_url,
&username,
password.as_deref(),
token.as_deref(),
tfa.as_deref(),
user_cfg.token.as_deref(),
&conf_dir,
&datadir_path,
)?;
let http_client = SeafileHttpClient::new(&server_url);
let repo_id = http_client.create_repo(&token, &name, &desc, libpasswd.as_deref())?;
info!(repo_id = %repo_id, name = %name, "Repository created");
println!("{}", repo_id);
}
Commands::Config { key, value } => {
debug!(key = %key, has_value = value.is_some(), "Executing config command");
let socket_path = datadir_path.join("seafile.sock");
trace!(socket = %socket_path.display(), "Connecting to RPC server");
let transport = UnixSocketTransport::connect(&socket_path, "seafile-rpcserver")?;
let mut client = SearpcClient::new(transport);
if let Some(val) = value {
debug!(key = %key, value = %val, "Setting config value");
client.set_config(&key, &val)?;
info!(key = %key, value = %val, "Config value set");
println!("Set {} = {}", key, val);
} else {
debug!(key = %key, "Getting config value");
let val = client.get_config(&key)?;
trace!(key = %key, value = %val, "Retrieved config value");
println!("{} = {}", key, val);
}
}
Commands::Stop => {
debug!("Executing stop command");
let socket_path = datadir_path.join("seafile.sock");
trace!(socket = %socket_path.display(), "Connecting to RPC server");
let transport = UnixSocketTransport::connect(&socket_path, "seafile-rpcserver")?;
let mut client = SearpcClient::new(transport);
info!("Sending shutdown request to daemon");
match client.shutdown() {
Ok(_) => {
info!("Seafile daemon stopped");
println!("Seafile daemon stopped");
}
Err(e) => {
debug!(error = %e, "Shutdown returned error (expected during shutdown)");
println!("Seafile daemon stopping...");
}
}
}
}
Ok(())
}
fn handle_start(conf_dir: &Path) -> Result<()> {
debug!("Starting daemon with conf_dir: {}", conf_dir.display());
let seafile_ini = conf_dir.join("seafile.ini");
let seafile_datadir = fs::read_to_string(&seafile_ini)
.context("Failed to read seafile.ini")?
.trim()
.to_string();
let datadir_path = PathBuf::from(&seafile_datadir);
let seafile_worktree = datadir_path
.parent()
.ok_or_else(|| anyhow!("Invalid data dir path: {}", datadir_path.display()))?
.join("seafile");
debug!(
"Data dir: {}, Worktree: {}",
datadir_path.display(),
seafile_worktree.display()
);
check_daemon_running(&datadir_path)?;
debug!("No existing daemon detected");
info!("Starting seafile daemon");
let status = Command::new("seaf-daemon")
.arg("--daemon")
.arg("-c")
.arg(conf_dir)
.arg("-d")
.arg(&datadir_path)
.arg("-w")
.arg(&seafile_worktree)
.status()
.context("Failed to start seaf-daemon")?;
if !status.success() {
anyhow::bail!("Failed to start seafile daemon");
}
debug!("seaf-daemon process started");
let socket_path = datadir_path.join("seafile.sock");
debug!("Waiting for socket: {}", socket_path.display());
for i in 0..4 {
std::thread::sleep(std::time::Duration::from_secs(1));
debug!("Connection attempt {} of 4", i + 1);
if let Ok(transport) = UnixSocketTransport::connect(&socket_path, "seafile-rpcserver") {
debug!("Connected to RPC server");
let mut client = SearpcClient::new(transport);
if client
.set_config_int("delete_confirm_threshold", 1000000)
.is_ok()
{
debug!("Set delete_confirm_threshold successfully");
break;
}
}
if i == 3 {
warn!("Could not set delete_confirm_threshold");
}
}
info!("Seafile daemon started successfully");
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn get_or_create_token(
server_url: &str,
username: &str,
password: Option<&str>,
token: Option<&str>,
tfa: Option<&str>,
config_token: Option<&str>,
conf_dir: &Path,
datadir_path: &Path,
) -> Result<String> {
if let Some(t) = token.or(config_token) {
debug!("Using existing token");
return Ok(t.to_string());
}
debug!("Obtaining new token from server");
let password = if let Some(p) = password {
p.to_string()
} else {
rpassword::prompt_password(format!("Enter password for user {}: ", username))?
};
let device_mgr = DeviceIdManager::new(conf_dir, datadir_path);
let device_id = device_mgr.get_device_id()?;
debug!(
"Authenticating as {} with device {}",
username,
&device_id[..8]
);
let http_client = SeafileHttpClient::new(server_url);
let token = http_client.get_token(username, &password, &device_id, tfa)?;
debug!("Authentication successful");
Ok(token)
}
#[allow(clippy::too_many_arguments)]
fn handle_download<T: searpc::Transport>(
client: &mut SearpcClient<T>,
conf_dir: &Path,
datadir_path: &Path,
repo_id: &str,
server: Option<&str>,
dir: Option<&Path>,
username: Option<&str>,
password: Option<&str>,
token: Option<&str>,
tfa: Option<&str>,
libpasswd: Option<&str>,
user_config: Option<&Path>,
) -> Result<()> {
let user_cfg = UserConfig::load(user_config)?;
let server_url = server
.or(user_cfg.server.as_deref())
.context("Server URL required")?;
let username = username
.or(user_cfg.user.as_deref())
.context("Username required")?;
let token = get_or_create_token(
server_url,
username,
password,
token,
tfa,
user_cfg.token.as_deref(),
conf_dir,
datadir_path,
)?;
let http_client = SeafileHttpClient::new(server_url);
let download_info = http_client.get_repo_download_info(&token, repo_id)?;
debug!("Received download_info from API:");
debug!(" repo_name: {}", download_info.repo_name);
debug!(" repo_version: {}", download_info.repo_version);
debug!(" encrypted: '{}'", download_info.encrypted);
debug!(" enc_version: {}", download_info.enc_version);
debug!(" magic: '{}'", download_info.magic);
debug!(" salt: '{}'", download_info.salt);
debug!(" random_key: '{}'", download_info.random_key);
let download_dir = if let Some(d) = dir {
d.to_path_buf()
} else {
datadir_path
.parent()
.ok_or_else(|| anyhow!("Invalid data dir path: {}", datadir_path.display()))?
.join("seafile")
};
let is_encrypted = !download_info.encrypted.is_empty() && download_info.encrypted != "0";
info!("Starting to download library {}", repo_id);
info!("Download directory: {}", download_dir.display());
debug!(
"Repository version: {}, name: {}",
download_info.repo_version, download_info.repo_name
);
debug!(
"Encrypted: {}, enc_version from API: {}",
is_encrypted, download_info.enc_version
);
let repo_passwd = if is_encrypted {
if let Some(pwd) = libpasswd {
Some(pwd.to_string())
} else {
Some(rpassword::prompt_password(
"Enter password for the library: ",
)?)
}
} else {
None
};
let mut more_info = serde_json::json!({
"server_url": http_client.get_base_url(),
"is_readonly": if download_info.permission.as_deref() == Some("r") { 1 } else { 0 },
});
if !download_info.salt.is_empty() {
more_info["repo_salt"] = serde_json::json!(&download_info.salt);
}
debug!("RPC download call parameters:");
debug!(" repo_id: {}", repo_id);
debug!(" repo_version: {}", download_info.repo_version);
debug!(" repo_name: {}", download_info.repo_name);
debug!(" worktree: {}", download_dir.display());
debug!(" token: {}...", &download_info.token[..8]);
debug!(
" passwd: {}",
if repo_passwd.is_some() {
"Some(<provided>)"
} else {
"None"
}
);
if download_info.magic.is_empty() {
debug!(" magic: None");
} else {
debug!(" magic: Some({})", download_info.magic);
}
debug!(" email: {}", download_info.email);
if download_info.random_key.is_empty() {
debug!(" random_key: None");
} else {
debug!(" random_key: Some({})", download_info.random_key);
}
debug!(" enc_version: {}", download_info.enc_version);
debug!(" more_info: {}", more_info.to_string());
let download_dir_str = download_dir
.to_str()
.ok_or_else(|| anyhow!("Path contains invalid UTF-8: {}", download_dir.display()))?;
client.download(
repo_id,
download_info.repo_version,
&download_info.repo_name,
download_dir_str,
&download_info.token,
repo_passwd.as_deref(), if download_info.magic.is_empty() {
None
} else {
Some(&download_info.magic)
},
&download_info.email,
if download_info.random_key.is_empty() {
None
} else {
Some(&download_info.random_key)
},
download_info.enc_version,
&more_info.to_string(),
)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn handle_sync<T: searpc::Transport>(
client: &mut SearpcClient<T>,
conf_dir: &Path,
datadir_path: &Path,
repo_id: &str,
server: Option<&str>,
folder: &Path,
username: Option<&str>,
password: Option<&str>,
token: Option<&str>,
tfa: Option<&str>,
libpasswd: Option<&str>,
user_config: Option<&Path>,
) -> Result<()> {
let user_cfg = UserConfig::load(user_config)?;
let server_url = server
.or(user_cfg.server.as_deref())
.context("Server URL required")?;
let username = username
.or(user_cfg.user.as_deref())
.context("Username required")?;
let token = get_or_create_token(
server_url,
username,
password,
token,
tfa,
user_cfg.token.as_deref(),
conf_dir,
datadir_path,
)?;
let http_client = SeafileHttpClient::new(server_url);
debug!("Getting download info for repo: {}", repo_id);
let download_info = http_client.get_repo_download_info(&token, repo_id)?;
debug!("download_info: {}", serde_json::to_string_pretty(&download_info)?);
let is_encrypted = !download_info.encrypted.is_empty() && download_info.encrypted != "0";
info!("Syncing library {} to folder {}", repo_id, folder.display());
debug!(
"Repository: {}, encrypted: {}, enc_version: {}",
download_info.repo_name, is_encrypted, download_info.enc_version
);
let repo_passwd = if is_encrypted {
if let Some(pwd) = libpasswd {
Some(pwd.to_string())
} else {
Some(rpassword::prompt_password(
"Enter password for the library: ",
)?)
}
} else {
None
};
let mut more_info = serde_json::json!({
"server_url": http_client.get_base_url(),
"is_readonly": if download_info.permission.as_deref() == Some("r") { 1 } else { 0 },
});
if !download_info.salt.is_empty() {
more_info["repo_salt"] = serde_json::json!(&download_info.salt);
}
let folder_str = folder
.to_str()
.ok_or_else(|| anyhow!("Path contains invalid UTF-8: {}", folder.display()))?;
client.clone(
repo_id,
download_info.repo_version,
&download_info.repo_name,
folder_str,
&download_info.token,
repo_passwd.as_deref(), if download_info.magic.is_empty() {
None
} else {
Some(&download_info.magic)
},
&download_info.email,
if download_info.random_key.is_empty() {
None
} else {
Some(&download_info.random_key)
},
download_info.enc_version,
&more_info.to_string(),
)?;
Ok(())
}