@@ -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+ }
0 commit comments