Skip to content

Commit 1717044

Browse files
committed
perf: Re-architecture save_path to avoid mismatches during rsync and replace with a fixed value guaranteed to exist
1 parent 5208d2e commit 1717044

7 files changed

Lines changed: 140 additions & 12 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ dotenv = "0.15"
4141
url = "2.5"
4242
regex = "1.12"
4343
rusqlite = { version = "0.39", features = ["bundled"] }
44+
dirs = "6.0.0"

src/api.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{constant, settings};
1+
use crate::{constant, settings, savepath};
22
use crate::{database, qb};
33

44
use actix_web::{web, HttpRequest, HttpResponse, Responder};
@@ -310,13 +310,12 @@ pub async fn put_torrent(
310310
.collect();
311311

312312
let mut response: Vec<HashMap<String, String>> = Vec::new();
313-
for item in resolve_payload(&body.into_inner()) {
313+
for mut item in resolve_payload(&body.into_inner()) {
314314
let tag = Uuid::new_v4().to_string();
315315
let url = item.url.to_string();
316316
let name = item.name.as_ref().unwrap().to_string();
317317
let hash = item.hash.as_ref().unwrap().to_uppercase().to_string();
318318
let trackers = item.trackers.as_ref().unwrap().to_vec();
319-
let save_path = item.save_path.to_string();
320319

321320
if hashes.contains(&hash) {
322321
response.push(HashMap::from([(name, 409.to_string())]));
@@ -331,9 +330,11 @@ pub async fn put_torrent(
331330
);
332331

333332
let mut params = vec![("urls", &url), ("tags", &tag)];
334-
if !save_path.is_empty() {
335-
params.push(("savepath", &save_path));
333+
if item.save_path.is_empty() {
334+
item.save_path = savepath::get_default_save_path(&client, &config, &name).await;
336335
}
336+
log::info!("Destination for '{}': {}", name, item.save_path);
337+
params.push(("savepath", &item.save_path));
337338

338339
let resp = client
339340
.post(format!("{}/api/v2/torrents/add", config.qbit_url))

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod settings;
1818
mod squire;
1919
mod swagger;
2020
mod telegram;
21+
mod savepath;
2122

2223
/// Contains entrypoint and initializer settings to trigger the asynchronous `HTTPServer`
2324
///

src/rsync.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ pub async fn run(
5050
state: settings::SharedState,
5151
hash: String,
5252
name: String,
53-
source: String,
5453
put_item: settings::PutItem,
5554
) {
5655
log::info!("Starting rsync for {}", name);
@@ -59,14 +58,15 @@ pub async fn run(
5958
"{}@{}:{}",
6059
put_item.remote_username, put_item.remote_host, put_item.remote_path
6160
);
61+
log::info!("{} -> {}", &put_item.save_path, &remote);
6262

6363
let child_result = Command::new("rsync")
6464
.args([
6565
"-az",
6666
"--progress",
6767
"--partial",
6868
"--inplace",
69-
&source,
69+
&put_item.save_path,
7070
&remote,
7171
])
7272
.stdout(std::process::Stdio::piped())

src/savepath.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use std::path::Path;
2+
use reqwest::Client;
3+
use serde_json::Value;
4+
5+
use crate::{settings, squire};
6+
7+
/// Constructs a default "Downloads" directory in the HOME folder.
8+
///
9+
/// # Arguments
10+
///
11+
/// * `child_dir` - Child directory that has to be appended to the default "Downloads" folder.
12+
///
13+
/// # Returns
14+
///
15+
/// Returns the constructed "Downloads" directory.
16+
fn default_download_path(child_dir: &str) -> String {
17+
match dirs::home_dir() {
18+
Some(home) => {
19+
let path = home.join("Downloads").join(child_dir);
20+
match path.to_str() {
21+
Some(path_str) => path_str.to_string(),
22+
None => {
23+
log::warn!(
24+
"Downloads path contains invalid UTF-8, falling back to /tmp"
25+
);
26+
format!("/tmp/{}", child_dir)
27+
}
28+
}
29+
}
30+
None => {
31+
log::info!(
32+
"Could not determine HOME directory, falling back to /tmp"
33+
);
34+
format!("/tmp/{}", child_dir)
35+
}
36+
}
37+
}
38+
39+
/// Fetches the default save path configured in qBittorrent.
40+
///
41+
/// # Arguments
42+
///
43+
/// * `client` - Authenticated HTTP client for qBittorrent API requests.
44+
/// * `config` - Application configuration containing the qBittorrent URL.
45+
///
46+
/// # Returns
47+
///
48+
/// Returns the default save path as a `String`, or a fallback path if the
49+
/// request fails or the field is absent.
50+
pub async fn get_default_save_path(
51+
client: &Client,
52+
config: &settings::Config,
53+
child_dir: &String,
54+
) -> String {
55+
// 1. Check environment variable override
56+
let default_save_env = squire::get_env_var("save_path", None);
57+
if !default_save_env.is_empty() {
58+
match std::fs::create_dir_all(&default_save_env) {
59+
Ok(_) => {
60+
log::debug!(
61+
"Verified save_path environment directory exists: {}",
62+
default_save_env
63+
);
64+
}
65+
Err(err) => {
66+
log::warn!(
67+
"Failed to create save_path (from env) directory '{}': {}",
68+
default_save_env,
69+
err
70+
);
71+
}
72+
}
73+
let joined = Path::new(&default_save_env).join(child_dir);
74+
return joined.to_string_lossy().into_owned();
75+
}
76+
77+
// 2. Fetch qBittorrent preferences
78+
let response = match client
79+
.get(format!("{}/api/v2/app/preferences", config.qbit_url))
80+
.send()
81+
.await
82+
{
83+
Ok(resp) => resp,
84+
Err(err) => {
85+
log::error!(
86+
"Failed to fetch qBittorrent preferences: {}",
87+
err
88+
);
89+
return default_download_path(child_dir);
90+
}
91+
};
92+
93+
// 3. Parse JSON response
94+
let resp_json: Value = match response.json().await {
95+
Ok(json) => json,
96+
Err(err) => {
97+
log::error!(
98+
"Failed to parse qBittorrent preferences JSON: {}",
99+
err
100+
);
101+
return default_download_path(child_dir);
102+
}
103+
};
104+
105+
// 4. Extract save_path
106+
match resp_json["save_path"].as_str() {
107+
Some(path) if !path.is_empty() => {
108+
log::info!("Using qBittorrent save path: {}", path);
109+
Path::new(path)
110+
.join(child_dir)
111+
.to_string_lossy()
112+
.into_owned()
113+
}
114+
Some(_) => {
115+
log::info!(
116+
"qBittorrent save_path is empty, using fallback"
117+
);
118+
default_download_path(child_dir)
119+
}
120+
None => {
121+
log::info!(
122+
"qBittorrent preferences missing save_path, using fallback"
123+
);
124+
default_download_path(child_dir)
125+
}
126+
}
127+
}

src/settings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ fn default_path() -> String {
262262

263263
/// Gets the default save path from the `save_path` environment variable.
264264
fn default_save_path() -> String {
265-
squire::get_env_var("save_path", None)
265+
String::new()
266266
}
267267

268268
/// Determines whether files should be deleted after copying.

src/squire.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,6 @@ pub fn spawn_worker(
221221
for t in arr {
222222
let hash = t["hash"].as_str().unwrap_or("").to_string();
223223
let progress = t["progress"].as_f64().unwrap_or(0.0);
224-
let content_path = t["content_path"].as_str().unwrap_or("").to_string();
225224

226225
let Some(entry) = db.get_mut(&hash) else {
227226
continue;
@@ -268,8 +267,8 @@ pub fn spawn_worker(
268267
.await;
269268
if let Err(e) = qb::handle_response(resp, "DELETE torrent").await {
270269
log::error!("Failed to delete torrent: {}", e.status());
271-
if std::path::Path::new(&content_path).exists()
272-
&& let Err(err) = std::fs::remove_dir_all(content_path)
270+
if std::path::Path::new(&entry.put_item.save_path).exists()
271+
&& let Err(err) = std::fs::remove_dir_all(&entry.put_item.save_path)
273272
{
274273
log::error!("Failed to delete files: {}", err);
275274
notifier(
@@ -301,7 +300,6 @@ pub fn spawn_worker(
301300
state_clone,
302301
hash_clone,
303302
name_clone,
304-
content_path,
305303
put_item_clone,
306304
)
307305
.await;

0 commit comments

Comments
 (0)