refactor: extract shared sync logic from gist into sync module
This commit is contained in:
+3
-3
@@ -77,9 +77,9 @@ impl App {
|
||||
|
||||
pub fn pull_sync_with_toast(&mut self) {
|
||||
let result = match self.config.settings.backend {
|
||||
SyncBackend::Gist => crate::sync::gist::pull_with_strategy(&mut self.config, crate::sync::gist::PullStrategy::Merge),
|
||||
SyncBackend::Webdav => crate::sync::webdav::pull_with_strategy(&mut self.config, crate::sync::gist::PullStrategy::Merge),
|
||||
SyncBackend::S3 => crate::sync::s3::pull_with_strategy(&mut self.config, crate::sync::gist::PullStrategy::Merge),
|
||||
SyncBackend::Gist => crate::sync::gist::pull_with_strategy(&mut self.config, crate::sync::PullStrategy::Merge),
|
||||
SyncBackend::Webdav => crate::sync::webdav::pull_with_strategy(&mut self.config, crate::sync::PullStrategy::Merge),
|
||||
SyncBackend::S3 => crate::sync::s3::pull_with_strategy(&mut self.config, crate::sync::PullStrategy::Merge),
|
||||
};
|
||||
match result {
|
||||
Ok(count) => self.toast(format!("pulled {count} items"), true),
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
use crate::config::{ConnectionType, CredentialEntry, SshellConfig, SyncBackend, config_path, find_binary};
|
||||
use crate::sync::{gist, s3, webdav};
|
||||
use crate::sync::{self, gist, s3, webdav};
|
||||
use crate::{connection, import, ui};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
@@ -75,8 +75,8 @@ pub fn run() -> Result<()> {
|
||||
fn run_sync(command: SyncCommand) -> Result<()> {
|
||||
let mut cfg = SshellConfig::load()?;
|
||||
let strat = |s: PullStrategy| match s {
|
||||
PullStrategy::Merge => gist::PullStrategy::Merge,
|
||||
PullStrategy::Overwrite => gist::PullStrategy::Overwrite,
|
||||
PullStrategy::Merge => sync::PullStrategy::Merge,
|
||||
PullStrategy::Overwrite => sync::PullStrategy::Overwrite,
|
||||
};
|
||||
match command {
|
||||
SyncCommand::Push => match cfg.settings.backend {
|
||||
|
||||
+201
@@ -1,3 +1,204 @@
|
||||
mod crypto;
|
||||
pub mod gist;
|
||||
pub mod s3;
|
||||
pub mod webdav;
|
||||
|
||||
use crate::config::ConnectionType;
|
||||
use crate::config::{ConnectionSource, CredentialStore, SshellConfig};
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
pub(crate) const GIST_TOKEN_REF: &str = "__gist_token";
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PullStrategy {
|
||||
Merge,
|
||||
Overwrite,
|
||||
}
|
||||
|
||||
pub(crate) fn localize_shell_profile(
|
||||
cfg: &SshellConfig,
|
||||
name: &str,
|
||||
profile: &mut crate::config::ConnectionProfile,
|
||||
) -> bool {
|
||||
let ConnectionType::Shell {
|
||||
shell_name,
|
||||
command,
|
||||
local_args,
|
||||
auth_ref,
|
||||
..
|
||||
} = &mut profile.kind
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
|
||||
local_args.clear();
|
||||
*auth_ref = None;
|
||||
|
||||
let Some(local_command) = cfg.local_shell_command(shell_name) else {
|
||||
return false;
|
||||
};
|
||||
*command = local_command;
|
||||
|
||||
if let Some(local_profile) = cfg.connections.get(name) {
|
||||
profile.local_tags = local_profile.local_tags.clone();
|
||||
profile.added_order = local_profile.added_order;
|
||||
profile.usage_count = local_profile.usage_count;
|
||||
if let ConnectionType::Shell {
|
||||
local_args: existing_local_args,
|
||||
..
|
||||
} = &local_profile.kind
|
||||
{
|
||||
*local_args = existing_local_args.clone();
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn build_sync_payload(
|
||||
cfg: &SshellConfig,
|
||||
sync_password: Option<&str>,
|
||||
) -> Result<toml::Value> {
|
||||
let mut payload = cfg.clone();
|
||||
payload.settings.sync_password = None;
|
||||
payload.settings.webdav_password = None;
|
||||
payload.settings.s3_secret_key = None;
|
||||
|
||||
let synced_refs: Vec<String> = payload
|
||||
.connections
|
||||
.iter()
|
||||
.filter(|(_, profile)| profile.sync())
|
||||
.filter_map(|(_, profile)| profile.auth_ref().map(String::from))
|
||||
.collect();
|
||||
|
||||
payload.credentials.entries.retain(|name, _| {
|
||||
name != GIST_TOKEN_REF && synced_refs.iter().any(|r| r == name)
|
||||
});
|
||||
|
||||
let encrypted = if !payload.credentials.entries.is_empty() {
|
||||
if let Some(pw) = sync_password {
|
||||
Some(crypto::encrypt_credentials(&payload.credentials, pw)?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut table = toml::map::Map::new();
|
||||
table.insert(
|
||||
"version".to_string(),
|
||||
toml::Value::Integer(payload.version as i64),
|
||||
);
|
||||
table.insert("settings".to_string(), to_toml_value(&payload.settings)?);
|
||||
|
||||
let mut conns = toml::map::Map::new();
|
||||
for (name, profile) in &mut payload.connections {
|
||||
profile.local_tags.clear();
|
||||
if !payload.settings.sync_usage_count {
|
||||
profile.usage_count = 0;
|
||||
}
|
||||
match &mut profile.kind {
|
||||
ConnectionType::Shell {
|
||||
auth_ref,
|
||||
command,
|
||||
shell_name: _,
|
||||
local_args,
|
||||
sync,
|
||||
..
|
||||
} => {
|
||||
if !*sync {
|
||||
continue;
|
||||
}
|
||||
local_args.clear();
|
||||
*command = String::new();
|
||||
*auth_ref = None;
|
||||
}
|
||||
ConnectionType::Ssh { sync, .. } => {
|
||||
if !*sync {
|
||||
continue;
|
||||
}
|
||||
profile.source = ConnectionSource::Manual;
|
||||
}
|
||||
}
|
||||
conns.insert(name.clone(), to_toml_value(&*profile)?);
|
||||
}
|
||||
table.insert("connections".to_string(), toml::Value::Table(conns));
|
||||
|
||||
if let Some(enc) = encrypted {
|
||||
table.insert(
|
||||
"credentials_encrypted".to_string(),
|
||||
toml::Value::String(enc),
|
||||
);
|
||||
} else if !payload.credentials.entries.is_empty() {
|
||||
table.insert(
|
||||
"credentials".to_string(),
|
||||
to_toml_value(&payload.credentials)?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(toml::Value::Table(table))
|
||||
}
|
||||
|
||||
pub(crate) fn merge_remote(
|
||||
cfg: &mut SshellConfig,
|
||||
remote: toml::Value,
|
||||
strategy: PullStrategy,
|
||||
) -> Result<usize> {
|
||||
let mut count = 0;
|
||||
|
||||
if let Some(conns) = remote.get("connections").and_then(|v| v.as_table()) {
|
||||
for (name, profile_val) in conns {
|
||||
let should_insert = match strategy {
|
||||
PullStrategy::Merge => !cfg.connections.contains_key(name),
|
||||
PullStrategy::Overwrite => true,
|
||||
};
|
||||
if should_insert
|
||||
&& let Ok(mut profile) = profile_val
|
||||
.clone()
|
||||
.try_into::<crate::config::ConnectionProfile>()
|
||||
&& localize_shell_profile(cfg, name, &mut profile)
|
||||
{
|
||||
cfg.connections.insert(name.clone(), profile);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let remote_credentials =
|
||||
if let Some(enc) = remote.get("credentials_encrypted").and_then(|v| v.as_str()) {
|
||||
let sync_password = cfg
|
||||
.settings
|
||||
.sync_password
|
||||
.as_deref()
|
||||
.context("sync_password not set; needed to decrypt remote credentials")?;
|
||||
Some(crypto::decrypt_credentials(enc, sync_password)?)
|
||||
} else if let Some(creds_val) = remote.get("credentials") {
|
||||
creds_val.clone().try_into::<CredentialStore>().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(remote_creds) = remote_credentials {
|
||||
for (name, credential) in remote_creds.entries {
|
||||
if name == GIST_TOKEN_REF {
|
||||
continue;
|
||||
}
|
||||
let should_insert = match strategy {
|
||||
PullStrategy::Merge => !cfg.credentials.entries.contains_key(&name),
|
||||
PullStrategy::Overwrite => true,
|
||||
};
|
||||
if should_insert {
|
||||
cfg.credentials.entries.insert(name, credential);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.save()?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub(crate) fn to_toml_value<T: serde::Serialize>(val: &T) -> Result<toml::Value> {
|
||||
toml::Value::try_from(val).map_err(|e| anyhow::anyhow!("toml conversion failed: {e}"))
|
||||
}
|
||||
|
||||
+2
-196
@@ -1,19 +1,10 @@
|
||||
use crate::config::ConnectionType;
|
||||
use crate::config::{ConnectionSource, CredentialEntry, CredentialStore, SshellConfig};
|
||||
use crate::config::{CredentialEntry, SshellConfig};
|
||||
use super::{GIST_TOKEN_REF, PullStrategy, build_sync_payload, merge_remote};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use reqwest::blocking::Client;
|
||||
use serde_json::json;
|
||||
|
||||
mod crypto;
|
||||
|
||||
const FILE_NAME: &str = "sshell-config.toml";
|
||||
pub(crate) const GIST_TOKEN_REF: &str = "__gist_token";
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PullStrategy {
|
||||
Merge,
|
||||
Overwrite,
|
||||
}
|
||||
|
||||
pub fn push(cfg: &mut SshellConfig) -> Result<String> {
|
||||
let token = gist_token(cfg)?;
|
||||
@@ -96,188 +87,3 @@ fn gist_token(cfg: &SshellConfig) -> Result<String> {
|
||||
_ => bail!("set GITHUB_TOKEN or create password credential __gist_token"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn localize_shell_profile(
|
||||
cfg: &SshellConfig,
|
||||
name: &str,
|
||||
profile: &mut crate::config::ConnectionProfile,
|
||||
) -> bool {
|
||||
let ConnectionType::Shell {
|
||||
shell_name,
|
||||
command,
|
||||
local_args,
|
||||
auth_ref,
|
||||
..
|
||||
} = &mut profile.kind
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
|
||||
local_args.clear();
|
||||
*auth_ref = None;
|
||||
|
||||
let Some(local_command) = cfg.local_shell_command(shell_name) else {
|
||||
return false;
|
||||
};
|
||||
*command = local_command;
|
||||
|
||||
if let Some(local_profile) = cfg.connections.get(name) {
|
||||
profile.local_tags = local_profile.local_tags.clone();
|
||||
profile.added_order = local_profile.added_order;
|
||||
profile.usage_count = local_profile.usage_count;
|
||||
if let ConnectionType::Shell {
|
||||
local_args: existing_local_args,
|
||||
..
|
||||
} = &local_profile.kind
|
||||
{
|
||||
*local_args = existing_local_args.clone();
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn build_sync_payload(cfg: &SshellConfig, sync_password: Option<&str>) -> Result<toml::Value> {
|
||||
let mut payload = cfg.clone();
|
||||
payload.settings.sync_password = None;
|
||||
payload.settings.webdav_password = None;
|
||||
payload.settings.s3_secret_key = None;
|
||||
|
||||
// Collect auth_refs from synced connections
|
||||
let synced_refs: Vec<String> = payload
|
||||
.connections
|
||||
.iter()
|
||||
.filter(|(_, profile)| profile.sync())
|
||||
.filter_map(|(_, profile)| profile.auth_ref().map(String::from))
|
||||
.collect();
|
||||
|
||||
payload.credentials.entries.retain(|name, _| {
|
||||
name != GIST_TOKEN_REF && synced_refs.iter().any(|r| r == name)
|
||||
});
|
||||
|
||||
let encrypted = if !payload.credentials.entries.is_empty() {
|
||||
if let Some(pw) = sync_password {
|
||||
Some(crypto::encrypt_credentials(&payload.credentials, pw)?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut table = toml::map::Map::new();
|
||||
table.insert(
|
||||
"version".to_string(),
|
||||
toml::Value::Integer(payload.version as i64),
|
||||
);
|
||||
table.insert("settings".to_string(), to_toml_value(&payload.settings)?);
|
||||
|
||||
let mut conns = toml::map::Map::new();
|
||||
for (name, profile) in &mut payload.connections {
|
||||
profile.local_tags.clear();
|
||||
if !payload.settings.sync_usage_count {
|
||||
profile.usage_count = 0;
|
||||
}
|
||||
match &mut profile.kind {
|
||||
ConnectionType::Shell {
|
||||
auth_ref,
|
||||
command,
|
||||
shell_name: _,
|
||||
local_args,
|
||||
sync,
|
||||
..
|
||||
} => {
|
||||
if !*sync {
|
||||
continue;
|
||||
}
|
||||
local_args.clear();
|
||||
*command = String::new();
|
||||
*auth_ref = None;
|
||||
}
|
||||
ConnectionType::Ssh { sync, .. } => {
|
||||
if !*sync {
|
||||
continue;
|
||||
}
|
||||
profile.source = ConnectionSource::Manual;
|
||||
}
|
||||
}
|
||||
conns.insert(name.clone(), to_toml_value(&*profile)?);
|
||||
}
|
||||
table.insert("connections".to_string(), toml::Value::Table(conns));
|
||||
|
||||
if let Some(enc) = encrypted {
|
||||
table.insert(
|
||||
"credentials_encrypted".to_string(),
|
||||
toml::Value::String(enc),
|
||||
);
|
||||
} else if !payload.credentials.entries.is_empty() {
|
||||
table.insert("credentials".to_string(), to_toml_value(&payload.credentials)?);
|
||||
}
|
||||
|
||||
Ok(toml::Value::Table(table))
|
||||
}
|
||||
|
||||
pub(crate) fn merge_remote(
|
||||
cfg: &mut SshellConfig,
|
||||
remote: toml::Value,
|
||||
strategy: PullStrategy,
|
||||
) -> Result<usize> {
|
||||
let mut count = 0;
|
||||
|
||||
// Merge connections
|
||||
if let Some(conns) = remote.get("connections").and_then(|v| v.as_table()) {
|
||||
for (name, profile_val) in conns {
|
||||
let should_insert = match strategy {
|
||||
PullStrategy::Merge => !cfg.connections.contains_key(name),
|
||||
PullStrategy::Overwrite => true,
|
||||
};
|
||||
if should_insert
|
||||
&& let Ok(mut profile) = profile_val
|
||||
.clone()
|
||||
.try_into::<crate::config::ConnectionProfile>()
|
||||
&& localize_shell_profile(cfg, name, &mut profile)
|
||||
{
|
||||
cfg.connections.insert(name.clone(), profile);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge credentials: try encrypted first, then plaintext fallback
|
||||
let remote_credentials =
|
||||
if let Some(enc) = remote.get("credentials_encrypted").and_then(|v| v.as_str()) {
|
||||
let sync_password = cfg
|
||||
.settings
|
||||
.sync_password
|
||||
.as_deref()
|
||||
.context("sync_password not set; needed to decrypt remote credentials")?;
|
||||
Some(crypto::decrypt_credentials(enc, sync_password)?)
|
||||
} else if let Some(creds_val) = remote.get("credentials") {
|
||||
creds_val.clone().try_into::<CredentialStore>().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(remote_creds) = remote_credentials {
|
||||
for (name, credential) in remote_creds.entries {
|
||||
if name == GIST_TOKEN_REF {
|
||||
continue;
|
||||
}
|
||||
let should_insert = match strategy {
|
||||
PullStrategy::Merge => !cfg.credentials.entries.contains_key(&name),
|
||||
PullStrategy::Overwrite => true,
|
||||
};
|
||||
if should_insert {
|
||||
cfg.credentials.entries.insert(name, credential);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.save()?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub(crate) fn to_toml_value<T: serde::Serialize>(val: &T) -> Result<toml::Value> {
|
||||
toml::Value::try_from(val).map_err(|e| anyhow::anyhow!("toml conversion failed: {e}"))
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use crate::config::SshellConfig;
|
||||
use super::gist::{PullStrategy, build_sync_payload, merge_remote};
|
||||
use super::{PullStrategy, build_sync_payload, merge_remote};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use hmac::{Hmac, Mac};
|
||||
use reqwest::blocking::Client;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use crate::config::SshellConfig;
|
||||
use super::gist::{PullStrategy, build_sync_payload, merge_remote};
|
||||
use super::{PullStrategy, build_sync_payload, merge_remote};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{ACCEPT, CONTENT_TYPE};
|
||||
|
||||
Reference in New Issue
Block a user