Skip to content

Commit d5ea3b8

Browse files
committed
feat: Add a new feature to retry failed rsync
1 parent aa4a96e commit d5ea3b8

9 files changed

Lines changed: 98 additions & 7 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ regex = "1.12"
4343
rusqlite = { version = "0.39", features = ["bundled"] }
4444
dirs = "6.0.0"
4545
base64 = "0.22.1"
46+
urlencoding = "2.1.3"

src/api.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ pub async fn get_torrents(
125125
settings::Status::Copying => "Copying".to_string(),
126126
settings::Status::Completed => "Completed".to_string(),
127127
settings::Status::Failed => "Failed".to_string(),
128+
settings::Status::CopyError => "CopyError".to_string(),
128129
settings::Status::Downloading(_) => {
129130
format!("Downloading: {:.0}%", progress * 100.0)
130131
}
@@ -489,3 +490,87 @@ pub async fn delete_torrent(
489490
log::info!("Successfully deleted {}", identifier);
490491
HttpResponse::Ok().json("Deleted")
491492
}
493+
494+
/// API endpoint to retry a failed rsync transfer.
495+
///
496+
/// # Arguments
497+
///
498+
/// * `request` - Reference to the `HttpRequest` object.
499+
/// * `state` - Reference to the `SharedState` object.
500+
/// * `config` - Reference to the `Config` object.
501+
/// * `query` - Query parameters: `name` (required).
502+
///
503+
/// #### Sample Request
504+
/// ```shell
505+
/// curl -X POST "http://localhost:3000/retry?name=Foo+Bar+1080p"
506+
/// ```
507+
///
508+
/// #### Status
509+
/// * `200`: Retry queued.
510+
/// * `400`: Torrent is not in `CopyError` state.
511+
/// * `404`: Torrent not found in state.
512+
///
513+
/// # Returns
514+
///
515+
/// Returns a JSON string indicating the result.
516+
#[utoipa::path(
517+
post,
518+
path = "/retry",
519+
params(
520+
("name" = String, Query, description = "Torrent name")
521+
),
522+
responses(
523+
(status = 200, description = "Retry queued", body = String),
524+
(status = 400, description = "Not in CopyError state", body = String),
525+
(status = 404, description = "Not found", body = String),
526+
)
527+
)]
528+
pub async fn retry_torrent(
529+
request: HttpRequest,
530+
state: web::Data<settings::SharedState>,
531+
config: web::Data<settings::Config>,
532+
query: web::Query<HashMap<String, String>>,
533+
) -> impl Responder {
534+
if !authenticator(request, &config) {
535+
return HttpResponse::Unauthorized().json("Unauthorized");
536+
}
537+
538+
let name = match query.get("name") {
539+
Some(i) => i,
540+
None => return HttpResponse::BadRequest().body("Missing name"),
541+
};
542+
543+
// Find the hash for the given name in state
544+
// TODO: Allow to override certain/all of put item
545+
let (hash, put_item) = {
546+
let db = state.read().await;
547+
let found = db.iter().find(|(_, entry)| entry.name == *name);
548+
match found {
549+
None => return HttpResponse::NotFound().body("Torrent not found in state"),
550+
Some((hash, entry)) => {
551+
match entry.status {
552+
settings::Status::CopyError => (hash.clone(), entry.put_item.clone()),
553+
_ => return HttpResponse::BadRequest().body("Torrent is not in CopyError state"),
554+
}
555+
}
556+
}
557+
};
558+
559+
// Transition back to Copying and re-spawn rsync
560+
{
561+
let mut db = state.write().await;
562+
if let Some(entry) = db.get_mut(&hash) {
563+
entry.status = settings::Status::Copying;
564+
}
565+
}
566+
567+
let state_clone = state.as_ref().clone();
568+
let hash_clone = hash.clone();
569+
let name_clone = name.clone();
570+
tokio::spawn(async move {
571+
crate::rsync::run(state_clone, hash_clone, name_clone, put_item).await;
572+
});
573+
574+
log::info!("Retry queued for: {}", name);
575+
HttpResponse::Ok().json("Retry queued")
576+
}

src/background.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ pub fn spawn_worker(
204204
};
205205

206206
match entry.status {
207-
settings::Status::Copying => continue,
207+
settings::Status::Copying | settings::Status::CopyError => continue,
208208

209209
settings::Status::Failed => {
210210
let config_cloned = config.clone();

src/database.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ fn encode_status(status: &Status) -> (&'static str, f64) {
230230
Status::Copying => ("Copying", 0.0),
231231
Status::Completed => ("Completed", 1.0),
232232
Status::Failed => ("Failed", 0.0),
233+
Status::CopyError => ("CopyError", 0.0),
233234
}
234235
}
235236

@@ -248,6 +249,7 @@ fn decode_status(status: &str, progress: f64) -> Status {
248249
"Copying" => Status::Copying,
249250
"Completed" => Status::Completed,
250251
"Failed" => Status::Failed,
252+
"CopyError" => Status::CopyError,
251253
_ => Status::Downloading(progress),
252254
}
253255
}

src/display.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ impl Ansi {
8080
#[macro_export]
8181
macro_rules! warning {
8282
($($arg:tt)*) => {{
83-
use crate::display::Ansi;
83+
use $crate::display::Ansi;
8484

8585
eprintln!();
8686
let head = "=".repeat(81);
@@ -149,7 +149,7 @@ macro_rules! warning {
149149
#[macro_export]
150150
macro_rules! error {
151151
($($arg:tt)*) => {{
152-
use crate::display::Ansi;
152+
use $crate::display::Ansi;
153153

154154
eprintln!();
155155
let head = "=".repeat(81);

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ pub async fn start() -> std::io::Result<()> {
9595
.route("/torrent", web::get().to(api::get_torrents))
9696
.route("/torrent", web::put().to(api::put_torrent))
9797
.route("/torrent", web::delete().to(api::delete_torrent))
98+
.route("/retry", web::post().to(api::retry_torrent))
9899
.route("/swagger", web::get().to(swagger::redirector))
99100
.route("/ui", web::get().to(swagger::redirector))
100101
.route("/authenticator", web::post().to(ui::authenticator))

src/rsync.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub async fn run(
4545
log::error!("Failed to start rsync for {}: {}", name, e);
4646
let mut db = state.write().await;
4747
if let Some(entry) = db.get_mut(&hash) {
48-
entry.status = settings::Status::Failed;
48+
entry.status = settings::Status::CopyError;
4949
}
5050
return;
5151
}
@@ -63,7 +63,7 @@ pub async fn run(
6363
log::error!("Failed waiting for rsync process for {}: {}", name, e);
6464
let mut db = state.write().await;
6565
if let Some(entry) = db.get_mut(&hash) {
66-
entry.status = settings::Status::Failed;
66+
entry.status = settings::Status::CopyError;
6767
}
6868
return;
6969
}
@@ -90,7 +90,7 @@ pub async fn run(
9090
e.status = if status.success() {
9191
settings::Status::Completed
9292
} else {
93-
settings::Status::Failed
93+
settings::Status::CopyError
9494
};
9595
}
9696
}

src/settings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ pub enum Status {
244244
Copying,
245245
Completed,
246246
Failed,
247+
CopyError,
247248
}
248249

249250
/// ### RsyncTrack

src/swagger.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use utoipa_swagger_ui::SwaggerUi;
1111
api::version,
1212
api::get_torrents,
1313
api::put_torrent,
14-
api::delete_torrent
14+
api::delete_torrent,
15+
api::retry_torrent
1516
),
1617
components(schemas(settings::PutItem)),
1718
security(

0 commit comments

Comments
 (0)