+9
bun.lock
+9
bun.lock
···
22
22
"@tauri-apps/plugin-deep-link": "~2",
23
23
"@tauri-apps/plugin-fs": "~2",
24
24
"@tauri-apps/plugin-opener": "^2",
25
+
"@tauri-apps/plugin-process": "~2",
26
+
"@tauri-apps/plugin-shell": "~2",
25
27
"@tauri-apps/plugin-store": "~2",
26
28
"@tauri-apps/plugin-updater": "~2",
29
+
"@tauri-apps/plugin-websocket": "~2",
27
30
"antd": "^5.26.4",
28
31
"class-variance-authority": "^0.7.1",
29
32
"clsx": "^2.1.1",
···
639
642
640
643
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.4.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ=="],
641
644
645
+
"@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ=="],
646
+
647
+
"@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA=="],
648
+
642
649
"@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-mre8er0nXPhyEWQzWCpUd+UnEoBQYcoA5JYlwpwOV9wcxKqlXTGfminpKsE37ic8NUb2BIZqf0QQ9/U3ib2+/A=="],
643
650
644
651
"@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="],
652
+
653
+
"@tauri-apps/plugin-websocket": ["@tauri-apps/plugin-websocket@2.4.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-0TpWqPBb5G3I1qPE/rPnPUmMyAQTWsw2B75Ab6SQ6X4EcypPGyqTyvt3OXymG6PzJeZ+D9wE614Fnf0wXnj1mg=="],
645
654
646
655
"@theguild/remark-mermaid": ["@theguild/remark-mermaid@0.2.0", "", { "dependencies": { "mermaid": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-o8n57TJy0OI4PCrNw8z6S+vpHtrwoQZzTA5Y3fL0U1NDRIoMg/78duWgEBFsCZcWM1G6zjE91yg1aKCsDwgE2Q=="],
647
656
+3
package.json
+3
package.json
···
28
28
"@tauri-apps/plugin-deep-link": "~2",
29
29
"@tauri-apps/plugin-fs": "~2",
30
30
"@tauri-apps/plugin-opener": "^2",
31
+
"@tauri-apps/plugin-process": "~2",
32
+
"@tauri-apps/plugin-shell": "~2",
31
33
"@tauri-apps/plugin-store": "~2",
32
34
"@tauri-apps/plugin-updater": "~2",
35
+
"@tauri-apps/plugin-websocket": "~2",
33
36
"antd": "^5.26.4",
34
37
"class-variance-authority": "^0.7.1",
35
38
"clsx": "^2.1.1",
+173
-2
src-tauri/Cargo.lock
+173
-2
src-tauri/Cargo.lock
···
237
237
name = "atproto-backup"
238
238
version = "0.1.0"
239
239
dependencies = [
240
+
"chrono",
240
241
"serde",
241
242
"serde_json",
242
243
"tauri",
···
245
246
"tauri-plugin-deep-link",
246
247
"tauri-plugin-fs",
247
248
"tauri-plugin-opener",
249
+
"tauri-plugin-process",
250
+
"tauri-plugin-shell",
248
251
"tauri-plugin-single-instance",
249
252
"tauri-plugin-store",
250
253
"tauri-plugin-updater",
254
+
"tauri-plugin-websocket",
255
+
"tokio",
251
256
]
252
257
253
258
[[package]]
···
520
525
dependencies = [
521
526
"android-tzdata",
522
527
"iana-time-zone",
528
+
"js-sys",
523
529
"num-traits",
524
530
"serde",
531
+
"wasm-bindgen",
525
532
"windows-link",
526
533
]
527
534
···
742
749
]
743
750
744
751
[[package]]
752
+
name = "data-encoding"
753
+
version = "2.9.0"
754
+
source = "registry+https://github.com/rust-lang/crates.io-index"
755
+
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
756
+
757
+
[[package]]
745
758
name = "deranged"
746
759
version = "0.4.0"
747
760
source = "registry+https://github.com/rust-lang/crates.io-index"
···
940
953
version = "1.2.2"
941
954
source = "registry+https://github.com/rust-lang/crates.io-index"
942
955
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
956
+
957
+
[[package]]
958
+
name = "encoding_rs"
959
+
version = "0.8.35"
960
+
source = "registry+https://github.com/rust-lang/crates.io-index"
961
+
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
962
+
dependencies = [
963
+
"cfg-if",
964
+
]
943
965
944
966
[[package]]
945
967
name = "endi"
···
1636
1658
"tokio",
1637
1659
"tokio-rustls",
1638
1660
"tower-service",
1639
-
"webpki-roots",
1661
+
"webpki-roots 1.0.1",
1640
1662
]
1641
1663
1642
1664
[[package]]
···
2562
2584
]
2563
2585
2564
2586
[[package]]
2587
+
name = "os_pipe"
2588
+
version = "1.2.2"
2589
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2590
+
checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224"
2591
+
dependencies = [
2592
+
"libc",
2593
+
"windows-sys 0.59.0",
2594
+
]
2595
+
2596
+
[[package]]
2565
2597
name = "osakit"
2566
2598
version = "0.3.1"
2567
2599
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3255
3287
"wasm-bindgen-futures",
3256
3288
"wasm-streams",
3257
3289
"web-sys",
3258
-
"webpki-roots",
3290
+
"webpki-roots 1.0.1",
3259
3291
]
3260
3292
3261
3293
[[package]]
···
3616
3648
]
3617
3649
3618
3650
[[package]]
3651
+
name = "sha1"
3652
+
version = "0.10.6"
3653
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3654
+
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
3655
+
dependencies = [
3656
+
"cfg-if",
3657
+
"cpufeatures",
3658
+
"digest",
3659
+
]
3660
+
3661
+
[[package]]
3619
3662
name = "sha2"
3620
3663
version = "0.10.9"
3621
3664
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3627
3670
]
3628
3671
3629
3672
[[package]]
3673
+
name = "shared_child"
3674
+
version = "1.1.1"
3675
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3676
+
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
3677
+
dependencies = [
3678
+
"libc",
3679
+
"sigchld",
3680
+
"windows-sys 0.60.2",
3681
+
]
3682
+
3683
+
[[package]]
3630
3684
name = "shlex"
3631
3685
version = "1.3.0"
3632
3686
source = "registry+https://github.com/rust-lang/crates.io-index"
3633
3687
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
3688
+
3689
+
[[package]]
3690
+
name = "sigchld"
3691
+
version = "0.2.4"
3692
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3693
+
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
3694
+
dependencies = [
3695
+
"libc",
3696
+
"os_pipe",
3697
+
"signal-hook",
3698
+
]
3699
+
3700
+
[[package]]
3701
+
name = "signal-hook"
3702
+
version = "0.3.18"
3703
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3704
+
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
3705
+
dependencies = [
3706
+
"libc",
3707
+
"signal-hook-registry",
3708
+
]
3634
3709
3635
3710
[[package]]
3636
3711
name = "signal-hook-registry"
···
4120
4195
]
4121
4196
4122
4197
[[package]]
4198
+
name = "tauri-plugin-process"
4199
+
version = "2.3.0"
4200
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4201
+
checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab"
4202
+
dependencies = [
4203
+
"tauri",
4204
+
"tauri-plugin",
4205
+
]
4206
+
4207
+
[[package]]
4208
+
name = "tauri-plugin-shell"
4209
+
version = "2.3.0"
4210
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4211
+
checksum = "2b9ffadec5c3523f11e8273465cacb3d86ea7652a28e6e2a2e9b5c182f791d25"
4212
+
dependencies = [
4213
+
"encoding_rs",
4214
+
"log",
4215
+
"open",
4216
+
"os_pipe",
4217
+
"regex",
4218
+
"schemars 0.8.22",
4219
+
"serde",
4220
+
"serde_json",
4221
+
"shared_child",
4222
+
"tauri",
4223
+
"tauri-plugin",
4224
+
"thiserror 2.0.12",
4225
+
"tokio",
4226
+
]
4227
+
4228
+
[[package]]
4123
4229
name = "tauri-plugin-single-instance"
4124
4230
version = "2.3.0"
4125
4231
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4181
4287
"url",
4182
4288
"windows-sys 0.60.2",
4183
4289
"zip",
4290
+
]
4291
+
4292
+
[[package]]
4293
+
name = "tauri-plugin-websocket"
4294
+
version = "2.4.0"
4295
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4296
+
checksum = "402eaba4a045e93f579637c364343d818de002c4b1abd9244f0fd6fcb9ba43dd"
4297
+
dependencies = [
4298
+
"futures-util",
4299
+
"http",
4300
+
"log",
4301
+
"rand 0.8.5",
4302
+
"serde",
4303
+
"serde_json",
4304
+
"tauri",
4305
+
"tauri-plugin",
4306
+
"thiserror 2.0.12",
4307
+
"tokio",
4308
+
"tokio-tungstenite",
4184
4309
]
4185
4310
4186
4311
[[package]]
···
4421
4546
"io-uring",
4422
4547
"libc",
4423
4548
"mio",
4549
+
"parking_lot",
4424
4550
"pin-project-lite",
4551
+
"signal-hook-registry",
4425
4552
"slab",
4426
4553
"socket2",
4427
4554
"tokio-macros",
···
4450
4577
]
4451
4578
4452
4579
[[package]]
4580
+
name = "tokio-tungstenite"
4581
+
version = "0.27.0"
4582
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4583
+
checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
4584
+
dependencies = [
4585
+
"futures-util",
4586
+
"log",
4587
+
"rustls",
4588
+
"rustls-pki-types",
4589
+
"tokio",
4590
+
"tokio-rustls",
4591
+
"tungstenite",
4592
+
"webpki-roots 0.26.11",
4593
+
]
4594
+
4595
+
[[package]]
4453
4596
name = "tokio-util"
4454
4597
version = "0.7.15"
4455
4598
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4667
4810
version = "0.2.5"
4668
4811
source = "registry+https://github.com/rust-lang/crates.io-index"
4669
4812
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
4813
+
4814
+
[[package]]
4815
+
name = "tungstenite"
4816
+
version = "0.27.0"
4817
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4818
+
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
4819
+
dependencies = [
4820
+
"bytes",
4821
+
"data-encoding",
4822
+
"http",
4823
+
"httparse",
4824
+
"log",
4825
+
"rand 0.9.1",
4826
+
"rustls",
4827
+
"rustls-pki-types",
4828
+
"sha1",
4829
+
"thiserror 2.0.12",
4830
+
"utf-8",
4831
+
]
4670
4832
4671
4833
[[package]]
4672
4834
name = "typeid"
···
5016
5178
"pkg-config",
5017
5179
"soup3-sys",
5018
5180
"system-deps",
5181
+
]
5182
+
5183
+
[[package]]
5184
+
name = "webpki-roots"
5185
+
version = "0.26.11"
5186
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5187
+
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
5188
+
dependencies = [
5189
+
"webpki-roots 1.0.1",
5019
5190
]
5020
5191
5021
5192
[[package]]
+7
-1
src-tauri/Cargo.toml
+7
-1
src-tauri/Cargo.toml
···
16
16
17
17
[build-dependencies]
18
18
tauri-build = { version = "2", features = [] }
19
+
tauri = { version = "2.0.0", features = [ "tray-icon" ] }
19
20
20
21
[dependencies]
21
-
tauri = { version = "2", features = [] }
22
+
tauri = { version = "2", features = ["tray-icon"] }
22
23
tauri-plugin-opener = "2"
23
24
serde = { version = "1", features = ["derive"] }
24
25
serde_json = "1"
25
26
tauri-plugin-deep-link = "2"
26
27
tauri-plugin-store = "2"
27
28
tauri-plugin-fs = "2"
29
+
tauri-plugin-process = "2"
30
+
tauri-plugin-shell = "2"
31
+
tokio = { version = "1", features = ["full"] }
32
+
chrono = { version = "0.4", features = ["serde"] }
33
+
tauri-plugin-websocket = "2"
28
34
29
35
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
30
36
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
+2
-1
src-tauri/capabilities/default.json
+2
-1
src-tauri/capabilities/default.json
+1
src-tauri/capabilities/desktop.json
+1
src-tauri/capabilities/desktop.json
+126
src-tauri/src/background.rs
+126
src-tauri/src/background.rs
···
1
+
use serde::{Deserialize, Serialize};
2
+
use serde_json::json;
3
+
use std::sync::Arc;
4
+
use std::time::{Duration, Instant};
5
+
use tauri::{AppHandle, Emitter, Manager, State};
6
+
use tauri_plugin_store::StoreExt;
7
+
use tokio::sync::Mutex;
8
+
use tokio::time::sleep;
9
+
10
+
#[derive(Debug, Clone, Serialize, Deserialize)]
11
+
pub struct BackupSettings {
12
+
pub frequency: String, // "daily" or "weekly"
13
+
pub last_backup_date: Option<String>,
14
+
}
15
+
16
+
pub struct BackgroundScheduler {
17
+
app: AppHandle,
18
+
is_running: Arc<Mutex<bool>>,
19
+
}
20
+
21
+
impl BackgroundScheduler {
22
+
pub fn new(app: AppHandle) -> Self {
23
+
Self {
24
+
app,
25
+
is_running: Arc::new(Mutex::new(false)),
26
+
}
27
+
}
28
+
29
+
pub async fn start(&self) {
30
+
let mut is_running = self.is_running.lock().await;
31
+
if *is_running {
32
+
return;
33
+
}
34
+
*is_running = true;
35
+
drop(is_running);
36
+
37
+
let is_running = self.is_running.clone();
38
+
let app = self.app.clone(); // <-- assuming it's Arc<App>
39
+
40
+
tokio::spawn(async move {
41
+
let mut last_check = Instant::now();
42
+
43
+
while *is_running.lock().await {
44
+
if last_check.elapsed() >= Duration::from_secs(30 * 60) {
45
+
last_check = Instant::now();
46
+
47
+
if let Err(e) = Self::check_and_perform_backup(app.clone()).await {
48
+
eprintln!("Background backup check failed: {}", e);
49
+
}
50
+
}
51
+
52
+
sleep(Duration::from_secs(5 * 60)).await;
53
+
}
54
+
});
55
+
}
56
+
57
+
pub async fn stop(&self) {
58
+
let mut is_running = self.is_running.lock().await;
59
+
*is_running = false;
60
+
}
61
+
62
+
async fn check_and_perform_backup(app: AppHandle) -> Result<(), Box<dyn std::error::Error>> {
63
+
// Get settings from store
64
+
let store = app.store("settings.json")?;
65
+
let raw_settings: Option<serde_json::Value> = store.get("settings");
66
+
67
+
let value = raw_settings.unwrap_or(json!({
68
+
"frequency": "daily",
69
+
"last_backup_date": null
70
+
}));
71
+
72
+
let settings: BackupSettings = serde_json::from_value(value)?;
73
+
74
+
// Check if backup is needed
75
+
if Self::should_perform_backup(&settings).await? {
76
+
println!("Background: Backup due, starting backup...");
77
+
78
+
// Emit event to frontend to perform backup
79
+
app.emit("perform-backup", ())?;
80
+
81
+
// Update last backup date
82
+
let mut updated_settings = settings;
83
+
updated_settings.last_backup_date = Some(chrono::Utc::now().to_rfc3339());
84
+
store.set("settings", json!(updated_settings));
85
+
store.save()?;
86
+
87
+
println!("Background: Backup completed");
88
+
}
89
+
90
+
Ok(())
91
+
}
92
+
93
+
async fn should_perform_backup(
94
+
settings: &BackupSettings,
95
+
) -> Result<bool, Box<dyn std::error::Error>> {
96
+
if settings.last_backup_date.is_none() {
97
+
return Ok(true);
98
+
}
99
+
100
+
let last_backup =
101
+
chrono::DateTime::parse_from_rfc3339(&settings.last_backup_date.as_ref().unwrap())?;
102
+
let now = chrono::Utc::now();
103
+
let time_diff = now.signed_duration_since(last_backup);
104
+
105
+
let required_interval = match settings.frequency.as_str() {
106
+
"daily" => chrono::Duration::days(1),
107
+
"weekly" => chrono::Duration::weeks(1),
108
+
_ => chrono::Duration::days(1),
109
+
};
110
+
111
+
Ok(time_diff >= required_interval)
112
+
}
113
+
}
114
+
115
+
#[tauri::command]
116
+
pub async fn start_background_scheduler(app: AppHandle) {
117
+
let scheduler = BackgroundScheduler::new(app);
118
+
scheduler.start().await;
119
+
}
120
+
121
+
#[tauri::command]
122
+
pub async fn stop_background_scheduler() -> Result<(), String> {
123
+
// This would need to be implemented with a global scheduler reference
124
+
// For now, we'll handle this differently
125
+
Ok(())
126
+
}
+37
-1
src-tauri/src/lib.rs
+37
-1
src-tauri/src/lib.rs
···
1
+
use tauri::{Emitter, Manager};
1
2
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
2
3
use tauri_plugin_deep_link::DeepLinkExt;
3
4
5
+
mod background;
6
+
mod tray;
7
+
use background::{start_background_scheduler, stop_background_scheduler};
8
+
use tray::{create_system_tray};
9
+
4
10
#[tauri::command]
5
11
fn greet(name: &str) -> String {
6
12
format!("Hello, {}! You've been greeted from Rust!", name)
···
9
15
#[cfg_attr(mobile, tauri::mobile_entry_point)]
10
16
pub fn run() {
11
17
let mut builder = tauri::Builder::default()
18
+
.plugin(tauri_plugin_websocket::init())
19
+
.plugin(tauri_plugin_shell::init())
20
+
.plugin(tauri_plugin_process::init())
12
21
.plugin(tauri_plugin_updater::Builder::new().build())
13
22
.plugin(tauri_plugin_autostart::init(
14
23
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
···
18
27
.plugin(tauri_plugin_store::Builder::new().build())
19
28
.plugin(tauri_plugin_deep_link::init())
20
29
.plugin(tauri_plugin_opener::init())
21
-
.invoke_handler(tauri::generate_handler![greet])
30
+
.invoke_handler(tauri::generate_handler![
31
+
greet,
32
+
start_background_scheduler,
33
+
stop_background_scheduler
34
+
])
35
+
.on_menu_event(|app, event| match event.id.as_ref() {
36
+
"quit" => {
37
+
std::process::exit(0);
38
+
}
39
+
"show" => {
40
+
let window = app.get_webview_window("main").unwrap();
41
+
window.show().unwrap();
42
+
window.set_focus().unwrap();
43
+
}
44
+
"hide" => {
45
+
let window = app.get_webview_window("main").unwrap();
46
+
window.hide().unwrap();
47
+
}
48
+
"backup_now" => {
49
+
// Emit event to trigger backup
50
+
app.emit("perform-backup", ()).unwrap();
51
+
}
52
+
_ => {
53
+
println!("menu item {:?} not handled", event.id);
54
+
}
55
+
})
22
56
.setup(|app| {
23
57
#[cfg(any(windows, target_os = "linux"))]
24
58
{
25
59
app.deep_link().register_all()?;
26
60
}
61
+
let tray = create_system_tray(app);
62
+
27
63
Ok(())
28
64
});
29
65
+24
src-tauri/src/tray.rs
+24
src-tauri/src/tray.rs
···
1
+
use tauri::{
2
+
menu::{Menu, MenuItem},
3
+
tray::{TrayIcon, TrayIconBuilder},
4
+
};
5
+
6
+
pub fn create_system_tray(app: &tauri::App) -> Result<TrayIcon, tauri::Error> {
7
+
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
8
+
let show_i = MenuItem::with_id(app, "show", "Show", true, None::<&str>)?;
9
+
let hide_i = MenuItem::with_id(app, "hide", "Hide", true, None::<&str>)?;
10
+
let backup_now_i = MenuItem::with_id(app, "backup_now", "Backup Now", true, None::<&str>)?;
11
+
12
+
let menu = Menu::with_items(app, &[
13
+
&quit_i,
14
+
&show_i,
15
+
&hide_i,
16
+
&backup_now_i,
17
+
])?;
18
+
19
+
TrayIconBuilder::new()
20
+
.menu(&menu)
21
+
.menu_on_left_click(true)
22
+
.icon(app.default_window_icon().unwrap().clone())
23
+
.build(app)
24
+
}
+12
-1
src-tauri/tauri.conf.json
+12
-1
src-tauri/tauri.conf.json
···
21
21
],
22
22
"security": {
23
23
"csp": null
24
+
},
25
+
"trayIcon": {
26
+
"iconPath": "icons/icon.png",
27
+
"iconAsTemplate": true
24
28
}
25
29
},
26
30
"bundle": {
···
32
36
"icons/128x128@2x.png",
33
37
"icons/icon.icns",
34
38
"icons/icon.ico"
35
-
]
39
+
],
40
+
"createUpdaterArtifacts": true
36
41
},
37
42
"plugins": {
38
43
"deep-link": {
···
41
46
"atprotobackups"
42
47
]
43
48
}
49
+
},
50
+
"updater": {
51
+
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU2Njg2QTE3N0I2NTI0QUEKUldTcUpHVjdGMnBvVnFNWWE4TkdDQlp2eFo2RUZVVmp4b1IxbGdvS3JHOWd1dkRvQm1MdWsvZ1gK",
52
+
"endpoints": [
53
+
"https://github.com/Turtlepaw/atproto-backup/releases/latest/download/latest.json"
54
+
]
44
55
}
45
56
}
46
57
}
+72
-4
src/App.tsx
+72
-4
src/App.tsx
···
8
8
import { initializeLocalStorage } from "./localstorage_ployfill";
9
9
import { Home } from "./routes/Home";
10
10
import { ThemeProvider } from "./theme-provider";
11
-
import { Toaster } from "sonner";
11
+
import { toast, Toaster } from "sonner";
12
12
import { ScrollArea } from "./components/ui/scroll-area";
13
-
import { BackupManager } from "./lib/backup";
13
+
import { BackupAgent } from "./lib/backup";
14
14
import { settingsManager } from "./lib/settings";
15
+
import { check } from "@tauri-apps/plugin-updater";
16
+
import { relaunch } from "@tauri-apps/plugin-process";
17
+
import {
18
+
BackgroundBackupService,
19
+
handleBackgroundBackup,
20
+
} from "./lib/backgroundBackup";
15
21
16
22
function AppContent() {
17
23
const { isLoading, isAuthenticated, profile, client, login, logout, agent } =
···
34
40
initStorage();
35
41
}, []);
36
42
37
-
// Auto-backup functionality
43
+
// Background backup service initialization
44
+
useEffect(() => {
45
+
if (!isAuthenticated || !agent) return;
46
+
47
+
const backgroundService = BackgroundBackupService.getInstance();
48
+
backgroundService.initialize();
49
+
50
+
// Listen for background backup requests
51
+
const handleBackgroundBackupRequest = () => {
52
+
handleBackgroundBackup(agent);
53
+
};
54
+
55
+
window.addEventListener(
56
+
"background-backup-requested",
57
+
handleBackgroundBackupRequest
58
+
);
59
+
60
+
return () => {
61
+
window.removeEventListener(
62
+
"background-backup-requested",
63
+
handleBackgroundBackupRequest
64
+
);
65
+
backgroundService.stop();
66
+
};
67
+
}, [isAuthenticated, agent]);
68
+
69
+
// Auto-backup functionality (for when app is open)
38
70
useEffect(() => {
39
71
if (!isAuthenticated || !agent) return;
40
72
···
76
108
const performBackup = async () => {
77
109
try {
78
110
console.log("Automatic backup due, starting backup...");
79
-
const manager = new BackupManager(agent);
111
+
const manager = new BackupAgent(agent);
80
112
await manager.startBackup();
81
113
82
114
// Update the last backup date
···
100
132
}
101
133
};
102
134
}, [isAuthenticated, agent]);
135
+
136
+
useEffect(() => {
137
+
(async () => {
138
+
const update = await check();
139
+
if (update) {
140
+
console.log(
141
+
`found update ${update.version} from ${update.date} with notes ${update.body}`
142
+
);
143
+
toast("Downloading new update...");
144
+
let downloaded = 0;
145
+
let contentLength = 0;
146
+
// alternatively we could also call update.download() and update.install() separately
147
+
await update.downloadAndInstall((event) => {
148
+
switch (event.event) {
149
+
case "Started":
150
+
//@ts-expect-error
151
+
contentLength = event.data.contentLength;
152
+
console.log(
153
+
`started downloading ${event.data.contentLength} bytes`
154
+
);
155
+
break;
156
+
case "Progress":
157
+
downloaded += event.data.chunkLength;
158
+
console.log(`downloaded ${downloaded} from ${contentLength}`);
159
+
break;
160
+
case "Finished":
161
+
console.log("download finished");
162
+
break;
163
+
}
164
+
});
165
+
166
+
toast("Update ready, restarting...");
167
+
await relaunch();
168
+
}
169
+
})();
170
+
}, []);
103
171
104
172
return (
105
173
<main className="bg-background dark min-h-screen flex flex-col">
src/assets/ATTRIBUTIONS.md
public/ATTRIBUTIONS.md
src/assets/ATTRIBUTIONS.md
public/ATTRIBUTIONS.md
src/assets/cloudy_sunset.png
public/cloudy_sunset.png
src/assets/cloudy_sunset.png
public/cloudy_sunset.png
src/assets/milky_way.jpg
public/milky_way.jpg
src/assets/milky_way.jpg
public/milky_way.jpg
src/assets/night_sky.jpg
public/night_sky.jpg
src/assets/night_sky.jpg
public/night_sky.jpg
-1
src/assets/react.svg
-1
src/assets/react.svg
···
1
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
+88
src/components/BackgroundTest.tsx
+88
src/components/BackgroundTest.tsx
···
1
+
import { useState } from "react";
2
+
import { Button } from "@/components/ui/button";
3
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
+
import { invoke } from "@tauri-apps/api/core";
5
+
import { listen } from "@tauri-apps/api/event";
6
+
import { toast } from "sonner";
7
+
8
+
export function BackgroundTest() {
9
+
const [isListening, setIsListening] = useState(false);
10
+
11
+
const startListening = async () => {
12
+
try {
13
+
await listen("perform-backup", () => {
14
+
toast("🎯 Background backup event received!");
15
+
console.log("Background backup event received");
16
+
});
17
+
setIsListening(true);
18
+
toast("🎯 Listening for background backup events");
19
+
} catch (error) {
20
+
console.error("Failed to start listening:", error);
21
+
toast("Failed to start listening");
22
+
}
23
+
};
24
+
25
+
const testBackgroundScheduler = async () => {
26
+
try {
27
+
await invoke("start_background_scheduler");
28
+
toast("🎯 Background scheduler started");
29
+
} catch (error) {
30
+
console.error("Failed to start background scheduler:", error);
31
+
toast("Failed to start background scheduler");
32
+
}
33
+
};
34
+
35
+
const testEmitEvent = async () => {
36
+
try {
37
+
// This will trigger the background backup
38
+
await invoke("emit", { event: "perform-backup", payload: null });
39
+
toast("🎯 Test backup event emitted");
40
+
} catch (error) {
41
+
console.error("Failed to emit event:", error);
42
+
toast("Failed to emit event");
43
+
}
44
+
};
45
+
46
+
return (
47
+
<Card className="bg-card border-white/10 mb-4">
48
+
<CardHeader>
49
+
<CardTitle className="text-white">🎯 Background Backup Test</CardTitle>
50
+
</CardHeader>
51
+
<CardContent className="space-y-4">
52
+
<div className="flex gap-2">
53
+
<Button
54
+
onClick={startListening}
55
+
disabled={isListening}
56
+
variant="outline"
57
+
className="cursor-pointer"
58
+
>
59
+
{isListening ? "Listening..." : "Start Listening"}
60
+
</Button>
61
+
62
+
<Button
63
+
onClick={testBackgroundScheduler}
64
+
variant="outline"
65
+
className="cursor-pointer"
66
+
>
67
+
Start Scheduler
68
+
</Button>
69
+
70
+
<Button
71
+
onClick={testEmitEvent}
72
+
variant="outline"
73
+
className="cursor-pointer"
74
+
>
75
+
Test Event
76
+
</Button>
77
+
</div>
78
+
79
+
<div className="text-xs text-white/60">
80
+
<p>• Click "Start Listening" to listen for backup events</p>
81
+
<p>• Click "Start Scheduler" to start background scheduler</p>
82
+
<p>• Click "Test Event" to manually trigger a backup event</p>
83
+
<p>• Check browser console for detailed logs</p>
84
+
</div>
85
+
</CardContent>
86
+
</Card>
87
+
);
88
+
}
+115
src/components/TestAutoBackup.tsx
+115
src/components/TestAutoBackup.tsx
···
1
+
import { useState } from "react";
2
+
import { Button } from "@/components/ui/button";
3
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
+
import { TestAutoBackupScheduler } from "@/lib/testAutoBackup";
5
+
import { useAuth } from "@/Auth";
6
+
import { settingsManager } from "@/lib/settings";
7
+
import { toast } from "sonner";
8
+
9
+
export function TestAutoBackup() {
10
+
const [isRunning, setIsRunning] = useState(false);
11
+
const [scheduler, setScheduler] = useState<TestAutoBackupScheduler | null>(
12
+
null
13
+
);
14
+
const [lastBackupDate, setLastBackupDate] = useState<string | undefined>();
15
+
const [frequency, setFrequency] = useState<"daily" | "weekly">("daily");
16
+
const { agent } = useAuth();
17
+
18
+
const startTest = () => {
19
+
if (!agent) {
20
+
toast("No agent available");
21
+
return;
22
+
}
23
+
24
+
const testScheduler = new TestAutoBackupScheduler(agent);
25
+
testScheduler.start();
26
+
setScheduler(testScheduler);
27
+
setIsRunning(true);
28
+
toast("🧪 Test auto-backup started! Check console for logs.");
29
+
};
30
+
31
+
const stopTest = () => {
32
+
if (scheduler) {
33
+
scheduler.stop();
34
+
setScheduler(null);
35
+
setIsRunning(false);
36
+
toast("🧪 Test auto-backup stopped");
37
+
}
38
+
};
39
+
40
+
const checkStatus = async () => {
41
+
const lastBackup = await settingsManager.getLastBackupDate();
42
+
const freq = await settingsManager.getBackupFrequency();
43
+
setLastBackupDate(lastBackup);
44
+
setFrequency(freq);
45
+
};
46
+
47
+
const clearLastBackup = async () => {
48
+
await settingsManager.updateSettings({ lastBackupDate: undefined });
49
+
await checkStatus();
50
+
toast("🧪 Cleared last backup date");
51
+
};
52
+
53
+
return (
54
+
<Card className="bg-card border-white/10 mb-4">
55
+
<CardHeader>
56
+
<CardTitle className="text-white">🧪 Test Auto-Backup</CardTitle>
57
+
</CardHeader>
58
+
<CardContent className="space-y-4">
59
+
<div className="space-y-2">
60
+
<p className="text-white/80 text-sm">
61
+
Current frequency: <span className="font-bold">{frequency}</span>
62
+
</p>
63
+
<p className="text-white/80 text-sm">
64
+
Last backup:{" "}
65
+
{lastBackupDate
66
+
? new Date(lastBackupDate).toLocaleString()
67
+
: "None"}
68
+
</p>
69
+
</div>
70
+
71
+
<div className="flex gap-2">
72
+
<Button
73
+
onClick={startTest}
74
+
disabled={isRunning}
75
+
variant="outline"
76
+
className="cursor-pointer"
77
+
>
78
+
Start Test (30s intervals)
79
+
</Button>
80
+
81
+
<Button
82
+
onClick={stopTest}
83
+
disabled={!isRunning}
84
+
variant="outline"
85
+
className="cursor-pointer"
86
+
>
87
+
Stop Test
88
+
</Button>
89
+
90
+
<Button
91
+
onClick={checkStatus}
92
+
variant="outline"
93
+
className="cursor-pointer"
94
+
>
95
+
Check Status
96
+
</Button>
97
+
98
+
<Button
99
+
onClick={clearLastBackup}
100
+
variant="outline"
101
+
className="cursor-pointer"
102
+
>
103
+
Clear Last Backup
104
+
</Button>
105
+
</div>
106
+
107
+
<div className="text-xs text-white/60">
108
+
<p>• Test uses 1 minute for "daily" and 2 minutes for "weekly"</p>
109
+
<p>• Check browser console for detailed logs</p>
110
+
<p>• Change frequency in Settings to test different intervals</p>
111
+
</div>
112
+
</CardContent>
113
+
</Card>
114
+
);
115
+
}
+2
-2
src/lib/autoBackup.ts
+2
-2
src/lib/autoBackup.ts
···
1
1
import { Agent } from "@atproto/api";
2
-
import { BackupManager } from "./backup";
2
+
import { BackupAgent } from "./backup";
3
3
import { settingsManager } from "./settings";
4
4
5
5
export class AutoBackupScheduler {
···
78
78
79
79
private async performBackup(): Promise<void> {
80
80
try {
81
-
const manager = new BackupManager(this.agent);
81
+
const manager = new BackupAgent(this.agent);
82
82
await manager.startBackup();
83
83
84
84
// Update the last backup date
+86
src/lib/backgroundBackup.ts
+86
src/lib/backgroundBackup.ts
···
1
+
import { invoke } from "@tauri-apps/api/core";
2
+
import { listen } from "@tauri-apps/api/event";
3
+
import { BackupAgent } from "./backup";
4
+
import { settingsManager } from "./settings";
5
+
import { toast } from "sonner";
6
+
7
+
export class BackgroundBackupService {
8
+
private static instance: BackgroundBackupService;
9
+
private isInitialized = false;
10
+
11
+
private constructor() {}
12
+
13
+
static getInstance(): BackgroundBackupService {
14
+
if (!BackgroundBackupService.instance) {
15
+
BackgroundBackupService.instance = new BackgroundBackupService();
16
+
}
17
+
return BackgroundBackupService.instance;
18
+
}
19
+
20
+
async initialize(): Promise<void> {
21
+
if (this.isInitialized) return;
22
+
23
+
// Start the background scheduler
24
+
try {
25
+
await invoke("start_background_scheduler");
26
+
console.log("Background backup scheduler started");
27
+
} catch (error) {
28
+
console.error("Failed to start background scheduler:", error);
29
+
}
30
+
31
+
// Listen for backup events from the background scheduler
32
+
await listen("perform-backup", async () => {
33
+
console.log("Background backup event received");
34
+
await this.performBackgroundBackup();
35
+
});
36
+
37
+
this.isInitialized = true;
38
+
}
39
+
40
+
private async performBackgroundBackup(): Promise<void> {
41
+
try {
42
+
// Get the agent from the auth context
43
+
// This is a bit tricky since we need to access the agent from the React context
44
+
// For now, we'll emit an event that the main app can listen to
45
+
window.dispatchEvent(new CustomEvent("background-backup-requested"));
46
+
47
+
console.log("Background backup request dispatched");
48
+
} catch (error) {
49
+
console.error("Background backup failed:", error);
50
+
}
51
+
}
52
+
53
+
async stop(): Promise<void> {
54
+
try {
55
+
await invoke("stop_background_scheduler");
56
+
console.log("Background backup scheduler stopped");
57
+
} catch (error) {
58
+
console.error("Failed to stop background scheduler:", error);
59
+
}
60
+
}
61
+
}
62
+
63
+
// Export a function to handle background backup requests
64
+
export async function handleBackgroundBackup(agent: any): Promise<void> {
65
+
try {
66
+
console.log("Performing background backup...");
67
+
68
+
const manager = new BackupAgent(agent);
69
+
await manager.startBackup();
70
+
71
+
// Update the last backup date
72
+
await settingsManager.setLastBackupDate(new Date().toISOString());
73
+
74
+
console.log("Background backup completed successfully");
75
+
76
+
// Show a notification (if the app is minimized, this might not be visible)
77
+
toast("Automatic backup completed", {
78
+
description: "Your data has been backed up automatically",
79
+
});
80
+
} catch (error) {
81
+
console.error("Background backup failed:", error);
82
+
toast("Automatic backup failed", {
83
+
description: "Check the console for details",
84
+
});
85
+
}
86
+
}
+283
-11
src/lib/backup.ts
+283
-11
src/lib/backup.ts
···
15
15
timestamp: string;
16
16
backupType: string;
17
17
filePath: string;
18
+
blobsPath?: string;
19
+
blobCount?: number;
18
20
stats: CarStats;
19
21
}
20
22
21
-
export class BackupManager {
23
+
export interface BlobReference {
24
+
cid: string;
25
+
mimeType?: string;
26
+
size?: number;
27
+
}
28
+
29
+
export type BackupStage =
30
+
| "fetching"
31
+
| "writing"
32
+
| "blobs"
33
+
| "cleanup"
34
+
| "complete";
35
+
export interface ProgressInfo {
36
+
stage: BackupStage;
37
+
message: string;
38
+
progress?: number; // 0-100 percentage
39
+
current?: number;
40
+
total?: number;
41
+
}
42
+
43
+
export type ProgressCallback = (progress: ProgressInfo) => void;
44
+
45
+
export class BackupAgent {
22
46
private agent: Agent;
23
47
private maxBackups = 3;
48
+
private downloadBlobs = true;
49
+
private progressCallback?: ProgressCallback;
50
+
private overwriteBackups = false;
24
51
25
-
constructor(agent: Agent) {
52
+
constructor(
53
+
agent: Agent,
54
+
options?: {
55
+
downloadBlobs?: boolean;
56
+
onProgress?: ProgressCallback;
57
+
overwriteBackups?: boolean;
58
+
}
59
+
) {
26
60
this.agent = agent;
61
+
this.downloadBlobs = options?.downloadBlobs ?? true;
62
+
this.progressCallback = options?.onProgress;
63
+
this.overwriteBackups = options?.overwriteBackups ?? false;
64
+
}
65
+
66
+
private reportProgress(progress: ProgressInfo) {
67
+
if (this.progressCallback) {
68
+
this.progressCallback(progress);
69
+
}
27
70
}
28
71
29
72
async startBackup(): Promise<Metadata> {
30
73
const did = this.agent.did;
31
74
if (did == null) throw Error("Unauthenticated");
32
75
33
-
// Get the repo data
34
-
const data = await this.agent.com.atproto.sync.getRepo({ did: did });
76
+
try {
77
+
// Stage 1: Fetching repo data
78
+
this.reportProgress({
79
+
stage: "fetching",
80
+
message: "Fetching repository data...",
81
+
progress: 10,
82
+
});
83
+
84
+
const data = await this.agent.com.atproto.sync.getRepo({ did: did });
85
+
86
+
// Stage 2: Writing backup file
87
+
this.reportProgress({
88
+
stage: "writing",
89
+
message: "Writing backup to file...",
90
+
progress: 30,
91
+
});
92
+
93
+
const metadata = await this.writeBackupToFile(data.data, did);
94
+
95
+
// Stage 3: Download blobs if enabled
96
+
if (this.downloadBlobs) {
97
+
this.reportProgress({
98
+
stage: "blobs",
99
+
message: "Preparing to download blobs...",
100
+
progress: 40,
101
+
});
102
+
103
+
await this.downloadBlobsForBackup(metadata);
104
+
} else {
105
+
this.reportProgress({
106
+
stage: "blobs",
107
+
message: "Skipping blob download (disabled)",
108
+
progress: 80,
109
+
});
110
+
}
35
111
36
-
// Write to backup location
37
-
const metadata = await this.writeBackupToFile(data.data, did);
112
+
// Clean up old backups or overwrite existing one
113
+
if (this.overwriteBackups) {
114
+
this.reportProgress({
115
+
stage: "cleanup",
116
+
message: "Cleaning up previous backup...",
117
+
progress: 90,
118
+
});
119
+
await this.cleanupAllBackups();
120
+
} else {
121
+
this.reportProgress({
122
+
stage: "cleanup",
123
+
message: "Cleaning up old backups...",
124
+
progress: 90,
125
+
});
126
+
await this.cleanupOldBackups();
127
+
}
38
128
39
-
// Clean up old backups after creating new one
40
-
await this.cleanupOldBackups();
129
+
// Stage 5: Complete
130
+
this.reportProgress({
131
+
stage: "complete",
132
+
message: "Backup completed successfully!",
133
+
progress: 100,
134
+
});
41
135
42
-
return metadata;
136
+
return metadata;
137
+
} catch (error: any) {
138
+
this.reportProgress({
139
+
stage: "complete",
140
+
message: `Backup failed: ${error.message}`,
141
+
progress: 0,
142
+
});
143
+
throw error;
144
+
}
43
145
}
44
146
45
147
private async writeBackupToFile(
···
50
152
// Create backup directory structure
51
153
await createBackupDir();
52
154
const backupDir = await getBackupDir();
53
-
const timestamp = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
54
-
const backupPath = await join(backupDir, `${timestamp}_backup`);
155
+
156
+
let backupPath: string;
157
+
if (this.overwriteBackups) {
158
+
// Use a consistent name for overwriting
159
+
backupPath = await join(backupDir, "current_backup");
160
+
161
+
// Remove existing backup if it exists
162
+
try {
163
+
await remove(backupPath, { recursive: true });
164
+
} catch (e) {
165
+
// Directory might not exist, which is fine
166
+
}
167
+
} else {
168
+
// Use timestamp-based naming, overwrite if exists for today
169
+
const timestamp = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
170
+
backupPath = await join(backupDir, `${timestamp}_backup`);
171
+
172
+
// Remove existing backup for today if it exists
173
+
try {
174
+
await remove(backupPath, { recursive: true });
175
+
console.log(`Overwriting existing backup for ${timestamp}`);
176
+
} catch (e) {
177
+
// Directory might not exist, which is fine
178
+
}
179
+
}
180
+
55
181
await mkdir(backupPath);
56
182
57
183
// Write the repo data as binary file
···
81
207
}
82
208
}
83
209
210
+
private async downloadBlobsForBackup(metadata: Metadata): Promise<void> {
211
+
try {
212
+
const backupDir = await resolve(metadata.filePath, "..");
213
+
const blobDir = await join(backupDir, "blobs");
214
+
await mkdir(blobDir);
215
+
216
+
// Extract blob references from the CAR file
217
+
this.reportProgress({
218
+
stage: "blobs",
219
+
message: "Extracting blob references...",
220
+
progress: 45,
221
+
});
222
+
223
+
const blobRefs = await this.extractBlobReferences();
224
+
225
+
if (blobRefs.length === 0) {
226
+
this.reportProgress({
227
+
stage: "blobs",
228
+
message: "No blobs found in backup",
229
+
progress: 80,
230
+
});
231
+
console.log("No blobs found in backup");
232
+
return;
233
+
}
234
+
235
+
console.log(`Downloading ${blobRefs.length} blobs...`);
236
+
let downloadedCount = 0;
237
+
238
+
for (let i = 0; i < blobRefs.length; i++) {
239
+
const blobRef = blobRefs[i];
240
+
const progress = 50 + Math.round((i / blobRefs.length) * 30); // 50-80% range
241
+
242
+
this.reportProgress({
243
+
stage: "blobs",
244
+
message: `Downloading blob ${i + 1} of ${blobRefs.length}...`,
245
+
progress,
246
+
current: i + 1,
247
+
total: blobRefs.length,
248
+
});
249
+
250
+
try {
251
+
const blobData = await this.agent.com.atproto.sync.getBlob({
252
+
did: metadata.did,
253
+
cid: blobRef,
254
+
});
255
+
256
+
const blobPath = await join(blobDir, `${blobRef}.blob`);
257
+
await writeFile(blobPath, blobData.data);
258
+
downloadedCount++;
259
+
260
+
// Optional: Save blob metadata
261
+
const blobMetadata = {
262
+
cid: blobRef,
263
+
size: blobData.data.length,
264
+
downloadedAt: new Date().toISOString(),
265
+
};
266
+
267
+
const blobMetadataPath = await join(blobDir, `${blobRef}.json`);
268
+
await writeFile(
269
+
blobMetadataPath,
270
+
new TextEncoder().encode(JSON.stringify(blobMetadata, null, 2))
271
+
);
272
+
} catch (error) {
273
+
console.error(`Failed to download blob ${blobRef}:`, error);
274
+
}
275
+
}
276
+
277
+
// Update main metadata with blob information
278
+
const updatedMetadata = {
279
+
...metadata,
280
+
blobsPath: blobDir,
281
+
blobCount: downloadedCount,
282
+
};
283
+
284
+
const metadataPath = await join(backupDir, "metadata.json");
285
+
await writeFile(
286
+
metadataPath,
287
+
new TextEncoder().encode(JSON.stringify(updatedMetadata, null, 2))
288
+
);
289
+
290
+
this.reportProgress({
291
+
stage: "blobs",
292
+
message: `Downloaded ${downloadedCount}/${blobRefs.length} blobs`,
293
+
progress: 80,
294
+
});
295
+
296
+
console.log(`Downloaded ${downloadedCount}/${blobRefs.length} blobs`);
297
+
} catch (error) {
298
+
console.error("Failed to download blobs:", error);
299
+
// Don't throw - blob download failure shouldn't fail the entire backup
300
+
}
301
+
}
302
+
303
+
private async extractBlobReferences(): Promise<string[]> {
304
+
let allBlobs = [];
305
+
let cursor: string | undefined;
306
+
while (true) {
307
+
const blobs = await this.agent.com.atproto.sync.listBlobs({
308
+
did: this.agent.assertDid,
309
+
limit: 500,
310
+
cursor,
311
+
});
312
+
allBlobs.push(...blobs.data.cids);
313
+
if (blobs.data.cursor) cursor = blobs.data.cursor;
314
+
else break;
315
+
}
316
+
317
+
return allBlobs;
318
+
}
319
+
320
+
private async cleanupAllBackups(): Promise<void> {
321
+
try {
322
+
const backups = await this.getBackups();
323
+
324
+
for (const backup of backups) {
325
+
await this.deleteBackup(backup);
326
+
}
327
+
328
+
if (backups.length > 0) {
329
+
console.log(`Deleted ${backups.length} existing backup(s)`);
330
+
}
331
+
} catch (error) {
332
+
console.error("Failed to cleanup all backups:", error);
333
+
// Don't throw here - we don't want backup creation to fail because of cleanup issues
334
+
}
335
+
}
336
+
84
337
private async cleanupOldBackups(): Promise<void> {
85
338
try {
86
339
const backups = await this.getBackups();
···
172
425
}
173
426
174
427
return data;
428
+
}
429
+
430
+
// Method to restore a specific blob
431
+
async restoreBlob(
432
+
backupMetadata: Metadata,
433
+
blobCid: string
434
+
): Promise<Uint8Array | null> {
435
+
if (!backupMetadata.blobsPath) {
436
+
throw new Error("No blobs available for this backup");
437
+
}
438
+
439
+
try {
440
+
const blobPath = await join(backupMetadata.blobsPath, `${blobCid}.blob`);
441
+
const blobData = await readTextFile(blobPath);
442
+
return new TextEncoder().encode(blobData);
443
+
} catch (error) {
444
+
console.error(`Failed to restore blob ${blobCid}:`, error);
445
+
return null;
446
+
}
175
447
}
176
448
}
+104
src/lib/testAutoBackup.ts
+104
src/lib/testAutoBackup.ts
···
1
+
import { Agent } from "@atproto/api";
2
+
import { BackupAgent } from "./backup";
3
+
import { settingsManager } from "./settings";
4
+
5
+
export class TestAutoBackupScheduler {
6
+
private agent: Agent;
7
+
private intervalId: NodeJS.Timeout | null = null;
8
+
private isRunning = false;
9
+
10
+
constructor(agent: Agent) {
11
+
this.agent = agent;
12
+
}
13
+
14
+
start(): void {
15
+
if (this.isRunning) return;
16
+
17
+
this.isRunning = true;
18
+
// Check every 30 seconds for testing (instead of 1 hour)
19
+
this.intervalId = setInterval(() => {
20
+
this.checkAndPerformBackup();
21
+
}, 30 * 1000); // 30 seconds
22
+
23
+
// Also check immediately when starting
24
+
this.checkAndPerformBackup();
25
+
}
26
+
27
+
stop(): void {
28
+
if (this.intervalId) {
29
+
clearInterval(this.intervalId);
30
+
this.intervalId = null;
31
+
}
32
+
this.isRunning = false;
33
+
}
34
+
35
+
private async checkAndPerformBackup(): Promise<void> {
36
+
try {
37
+
const shouldBackup = await this.shouldPerformBackup();
38
+
39
+
if (shouldBackup) {
40
+
console.log("🧪 TEST: Automatic backup due, starting backup...");
41
+
await this.performBackup();
42
+
} else {
43
+
console.log("🧪 TEST: No backup needed yet");
44
+
}
45
+
} catch (error) {
46
+
console.error("Error in automatic backup check:", error);
47
+
}
48
+
}
49
+
50
+
private async shouldPerformBackup(): Promise<boolean> {
51
+
try {
52
+
const lastBackupDate = await settingsManager.getLastBackupDate();
53
+
const frequency = await settingsManager.getBackupFrequency();
54
+
55
+
if (!lastBackupDate) {
56
+
console.log("🧪 TEST: No previous backup, should do one");
57
+
return true;
58
+
}
59
+
60
+
const lastBackup = new Date(lastBackupDate);
61
+
const now = new Date();
62
+
const timeDiff = now.getTime() - lastBackup.getTime();
63
+
64
+
// For testing: use shorter intervals
65
+
if (frequency === "daily") {
66
+
// Test with 1 minute instead of 24 hours
67
+
const oneMinute = 60 * 1000;
68
+
const shouldBackup = timeDiff >= oneMinute;
69
+
console.log(
70
+
`🧪 TEST: Daily backup check - ${timeDiff}ms since last backup, need ${oneMinute}ms`
71
+
);
72
+
return shouldBackup;
73
+
} else if (frequency === "weekly") {
74
+
// Test with 2 minutes instead of 7 days
75
+
const twoMinutes = 2 * 60 * 1000;
76
+
const shouldBackup = timeDiff >= twoMinutes;
77
+
console.log(
78
+
`🧪 TEST: Weekly backup check - ${timeDiff}ms since last backup, need ${twoMinutes}ms`
79
+
);
80
+
return shouldBackup;
81
+
}
82
+
83
+
return false;
84
+
} catch (error) {
85
+
console.error("Error checking if backup is due:", error);
86
+
return false;
87
+
}
88
+
}
89
+
90
+
private async performBackup(): Promise<void> {
91
+
try {
92
+
console.log("🧪 TEST: Starting automatic backup...");
93
+
const manager = new BackupAgent(this.agent);
94
+
await manager.startBackup();
95
+
96
+
// Update the last backup date
97
+
await settingsManager.setLastBackupDate(new Date().toISOString());
98
+
99
+
console.log("🧪 TEST: Automatic backup completed successfully");
100
+
} catch (error) {
101
+
console.error("🧪 TEST: Automatic backup failed:", error);
102
+
}
103
+
}
104
+
}
+35
src/lib/testBackground.ts
+35
src/lib/testBackground.ts
···
1
+
import { invoke } from "@tauri-apps/api/core";
2
+
import { listen } from "@tauri-apps/api/event";
3
+
import { toast } from "sonner";
4
+
5
+
export class BackgroundTestService {
6
+
static async testBackgroundBackup() {
7
+
try {
8
+
// Test the background scheduler
9
+
await invoke("start_background_scheduler");
10
+
toast("Background scheduler started");
11
+
12
+
// Listen for backup events
13
+
await listen("perform-backup", () => {
14
+
toast("Background backup event received!");
15
+
console.log("Background backup event received");
16
+
});
17
+
18
+
console.log("Background test service initialized");
19
+
} catch (error) {
20
+
console.error("Background test failed:", error);
21
+
toast("Background test failed");
22
+
}
23
+
}
24
+
25
+
static async triggerTestBackup() {
26
+
try {
27
+
// Emit a test backup event
28
+
await invoke("emit", { event: "perform-backup", payload: null });
29
+
toast("Test backup event triggered");
30
+
} catch (error) {
31
+
console.error("Failed to trigger test backup:", error);
32
+
toast("Failed to trigger test backup");
33
+
}
34
+
}
35
+
}
+47
-23
src/routes/Home.tsx
+47
-23
src/routes/Home.tsx
···
1
+
import { useAuth } from "@/Auth";
2
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
3
+
import { Badge } from "@/components/ui/badge";
1
4
import { Button } from "@/components/ui/button";
2
-
import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
3
-
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
4
5
import {
5
6
DropdownMenu,
6
7
DropdownMenuContent,
7
8
DropdownMenuItem,
8
9
DropdownMenuTrigger,
9
10
} from "@/components/ui/dropdown-menu";
10
-
import { openPath } from "@tauri-apps/plugin-opener";
11
+
import { Progress } from "@/components/ui/progress";
12
+
import { BackupAgent, BackupStage, Metadata } from "@/lib/backup";
11
13
import { createBackupDir, getBackupDir } from "@/lib/paths";
12
-
import { useEffect, useState, useRef } from "react";
14
+
import { settingsManager } from "@/lib/settings";
15
+
import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
16
+
import { openPath } from "@tauri-apps/plugin-opener";
13
17
import {
14
-
History,
15
-
LoaderCircleIcon,
16
18
ChevronDown,
17
-
FolderOpen,
18
-
Database,
19
19
FileText,
20
+
FolderOpen,
20
21
HardDrive,
21
-
Package,
22
22
Heart,
23
-
Users,
24
-
User,
23
+
History,
24
+
Images,
25
+
LoaderCircleIcon,
26
+
Package,
25
27
Settings as SettingsIcon,
28
+
User,
29
+
Users,
26
30
} from "lucide-react";
27
-
import { BackupManager, Metadata } from "@/lib/backup";
28
-
import { useAuth } from "@/Auth";
31
+
import { useEffect, useRef, useState } from "react";
29
32
import { toast } from "sonner";
30
-
import { settingsManager } from "@/lib/settings";
31
-
import { Badge } from "@/components/ui/badge";
32
-
import { Progress } from "@/components/ui/progress";
33
33
import Settings from "./Settings";
34
34
35
35
export function Home({
···
84
84
variant="ghost"
85
85
size="sm"
86
86
onClick={() => setShowSettings(true)}
87
-
className="text-white/80 hover:text-white"
87
+
className="text-white/80 hover:text-white cursor-pointer"
88
88
>
89
89
<SettingsIcon className="w-4 h-4" />
90
90
</Button>
91
91
</div>
92
92
93
+
{/* For testing auto backup */}
94
+
{/* <TestAutoBackup /> */}
95
+
{/* <BackgroundTest /> */}
96
+
93
97
<div className="bg-card rounded-lg p-4 mb-4">
94
98
<p className="mb-2 text-white">Backups</p>
95
99
<div className="flex gap-2">
···
117
121
</Button>
118
122
119
123
<StartBackup onBackupComplete={handleBackupComplete} />
124
+
125
+
{/* <Button
126
+
variant="outline"
127
+
className="cursor-pointer"
128
+
onClick={async () => {
129
+
await BackgroundTestService.testBackgroundBackup();
130
+
}}
131
+
>
132
+
Test Background
133
+
</Button> */}
120
134
</div>
121
135
</div>
122
136
···
127
141
128
142
function StartBackup({ onBackupComplete }: { onBackupComplete: () => void }) {
129
143
const [isLoading, setIsLoading] = useState(false);
144
+
const [stage, setStage] = useState<BackupStage | null>(null);
145
+
const [_, setProgress] = useState<number | undefined>();
130
146
const { agent } = useAuth();
131
147
132
148
return (
···
140
156
toast("Agent not initialized, try to reload the app.");
141
157
return;
142
158
}
143
-
const manager = new BackupManager(agent!!);
159
+
const manager = new BackupAgent(agent!!, {
160
+
onProgress: (progress) => {
161
+
setStage(progress.stage);
162
+
setProgress(progress.progress);
163
+
},
164
+
});
144
165
await manager.startBackup();
145
166
await settingsManager.setLastBackupDate(new Date().toISOString());
146
167
toast("Backup complete!");
···
155
176
disabled={isLoading}
156
177
>
157
178
{isLoading ? (
158
-
<LoaderCircleIcon className="animate-spin text-white/80" />
179
+
<>
180
+
<LoaderCircleIcon className="animate-spin text-white/80" />
181
+
<span className="capitalize">{stage}</span>
182
+
</>
159
183
) : (
160
184
<span>Backup now</span>
161
185
)}
···
194
218
}
195
219
setIsLoading(true);
196
220
try {
197
-
const manager = new BackupManager(agent);
221
+
const manager = new BackupAgent(agent);
198
222
const backupsList = await manager.getBackups();
199
223
// Sort backups by timestamp (newest first)
200
224
const sortedBackups = backupsList.sort(
···
336
360
</div>
337
361
338
362
<div className="flex items-center gap-2 p-3 rounded-lg bg-white/5">
339
-
<Database className="w-5 h-5 text-purple-400" />
363
+
<Images className="w-5 h-5 text-purple-400" />
340
364
<div>
341
-
<p className="text-white/60 text-xs">Blocks</p>
365
+
<p className="text-white/60 text-xs">Attachments</p>
342
366
<p className="text-white font-semibold">
343
-
{backup.stats?.totalBlocks?.toLocaleString() || "-"}
367
+
{backup.blobCount?.toLocaleString() || "-"}
344
368
</p>
345
369
</div>
346
370
</div>
+1
-1
src/routes/Login.tsx
+1
-1
src/routes/Login.tsx
···
100
100
<div
101
101
className="min-h-screen flex items-center justify-center bg-background px-4 relative"
102
102
style={{
103
-
backgroundImage: "url(/src/assets/milky_way.jpg)",
103
+
backgroundImage: "url(/milky_way.jpg)",
104
104
backgroundSize: "300%",
105
105
backgroundPosition: "center",
106
106
backgroundRepeat: "no-repeat",