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) {
|
pub fn pull_sync_with_toast(&mut self) {
|
||||||
let result = match self.config.settings.backend {
|
let result = match self.config.settings.backend {
|
||||||
SyncBackend::Gist => crate::sync::gist::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::gist::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::gist::PullStrategy::Merge),
|
SyncBackend::S3 => crate::sync::s3::pull_with_strategy(&mut self.config, crate::sync::PullStrategy::Merge),
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(count) => self.toast(format!("pulled {count} items"), true),
|
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::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 crate::{connection, import, ui};
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
@@ -75,8 +75,8 @@ pub fn run() -> Result<()> {
|
|||||||
fn run_sync(command: SyncCommand) -> Result<()> {
|
fn run_sync(command: SyncCommand) -> Result<()> {
|
||||||
let mut cfg = SshellConfig::load()?;
|
let mut cfg = SshellConfig::load()?;
|
||||||
let strat = |s: PullStrategy| match s {
|
let strat = |s: PullStrategy| match s {
|
||||||
PullStrategy::Merge => gist::PullStrategy::Merge,
|
PullStrategy::Merge => sync::PullStrategy::Merge,
|
||||||
PullStrategy::Overwrite => gist::PullStrategy::Overwrite,
|
PullStrategy::Overwrite => sync::PullStrategy::Overwrite,
|
||||||
};
|
};
|
||||||
match command {
|
match command {
|
||||||
SyncCommand::Push => match cfg.settings.backend {
|
SyncCommand::Push => match cfg.settings.backend {
|
||||||
|
|||||||
+201
@@ -1,3 +1,204 @@
|
|||||||
|
mod crypto;
|
||||||
pub mod gist;
|
pub mod gist;
|
||||||
pub mod s3;
|
pub mod s3;
|
||||||
pub mod webdav;
|
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::{CredentialEntry, SshellConfig};
|
||||||
use crate::config::{ConnectionSource, CredentialEntry, CredentialStore, SshellConfig};
|
use super::{GIST_TOKEN_REF, PullStrategy, build_sync_payload, merge_remote};
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
mod crypto;
|
|
||||||
|
|
||||||
const FILE_NAME: &str = "sshell-config.toml";
|
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> {
|
pub fn push(cfg: &mut SshellConfig) -> Result<String> {
|
||||||
let token = gist_token(cfg)?;
|
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"),
|
_ => 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 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 anyhow::{Context, Result, bail};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
use crate::config::SshellConfig;
|
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 anyhow::{Context, Result, bail};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use reqwest::header::{ACCEPT, CONTENT_TYPE};
|
use reqwest::header::{ACCEPT, CONTENT_TYPE};
|
||||||
|
|||||||
Reference in New Issue
Block a user