1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use std::num::NonZeroU64;
use atuin_client::api_client::Client;
use atuin_client::record::sqlite_store::SqliteStore;
use atuin_client::record::sync::{ClientSource, Operation, SyncSession};
use atuin_client::settings::Settings;
use atuin_common::encryption::paseto_v4;
use atuin_domain::record::{HostId, RecordTag};
use clap::Args;
use eyre::{Context as _, Result};
use uuid::Uuid;
#[derive(Args, Debug)]
pub struct Push {
/// The tag to push (eg, 'history'). Defaults to all tags
#[arg(long, short)]
pub tag: Option<RecordTag>,
/// The host to push, in the form of a UUID host ID. Defaults to the current host.
#[arg(long)]
pub host: Option<Uuid>,
/// Force push records
///
/// This will override both host and tag, to be all hosts and all tags. First clear the remote store, then upload all of the
/// local store
#[arg(long, default_value = "false")]
pub force: bool,
/// Page Size
///
/// How many records to upload at once. Defaults to 100
#[arg(long, default_value = "100")]
pub page: NonZeroU64,
}
impl Push {
pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
let host_id = Settings::host_id().await?;
if self.force {
println!("Forcing remote store overwrite!");
println!("Clearing remote store");
let caps = atuin_client::api_client::caps_client(settings)?;
let client = Client::new(
settings.sync_address.clone(),
&settings.sync_auth_token().await?,
settings.network_connect_timeout,
settings.network_timeout * 10, // we may be deleting a lot of data... so up the
// timeout
&settings.extra_headers,
caps,
)
.expect("failed to create client");
client.delete_store().await?;
}
// We can actually just use the existing diff/etc to push
// 1. Diff
// 2. Get operations
// 3. Filter operations by
// a) are they an upload op?
// b) are they for the host/tag we are pushing here?
let key = paseto_v4::Key::try_load_from_path(&settings.key_path)
.context("could not load encryption key")?;
let session = SyncSession::builder()
.store(store)
.client_source(ClientSource::FromSettings {
settings,
caps: None,
})
.build()
.connect()
.await?
.with_page_size(self.page);
let keyed = session.keyed(&key);
let (diff, remote_index) = session.diff().await?;
// Skip on --force: that path intentionally replaces remote with local.
if !self.force
&& let Some(err) = keyed.key_valid_against(&remote_index).await
{
return Err(crate::print_error::format_sync_error(err));
}
let operations = SyncSession::operations(diff)?;
let operations = operations
.into_iter()
.filter(|op| match op {
// No noops or downloads thx
Operation::Noop { .. } | Operation::Download { .. } => false,
// push, so yes plz to uploads!
Operation::Upload { series, .. } => {
if self.force {
return true;
}
if let Some(h) = self.host {
if HostId(h) != series.host_id {
return false;
}
} else if series.host_id != host_id {
return false;
}
if let Some(t) = self.tag.clone()
&& t != series.tag
{
return false;
}
true
}
})
.collect();
let (uploaded, _) = keyed.sync_remote(operations).await?;
println!("Uploaded {uploaded} records");
Ok(())
}
}