11import json
22import os
33import re
4- import subprocess
54import time
65import docker
76import requests
@@ -69,7 +68,7 @@ def _container_state(container: Dict[str, Any]) -> str:
6968
7069
7170class BackupManager :
72- """Volume Backup Manager using Restic — hybrid local binary + Docker runner"""
71+ """Volume Backup Manager using Restic container runner"""
7372
7473 RESTIC_LOCAL_ARGS = ["--insecure-no-password" , "--json" ]
7574
@@ -81,10 +80,6 @@ def __init__(self):
8180 self .repository_path = config_manager .get_value ("volume_backup" , "repopath" )
8281 self .restic_image = config_manager .get_value ("volume_backup" , "image" ) or "restic/restic"
8382
84- # Verify local restic binary for read-only operations
85- if not os .path .isfile ("/usr/local/bin/restic" ) or not os .access ("/usr/local/bin/restic" , os .X_OK ):
86- raise CustomException (500 , "Restic binary not found at /usr/local/bin/restic" , "Restic Unavailable" )
87-
8883 # Ensure cache dir exists
8984 os .makedirs (RESTIC_CACHE_PATH , exist_ok = True )
9085
@@ -96,27 +91,7 @@ def __init__(self):
9691 raise CustomException ()
9792
9893 # ------------------------------------------------------------------
99- # Local Restic execution — for list / delete / repo ops (fast, no Docker)
100- # ------------------------------------------------------------------
101- def _run_restic_local (self , command : List [str ]) -> str :
102- full_cmd = ["/usr/local/bin/restic" , "-r" , self .repository_path ] + command + self .RESTIC_LOCAL_ARGS
103- try :
104- result = subprocess .run (full_cmd , capture_output = True , text = True , timeout = 3600 )
105- if result .returncode != 0 :
106- err = result .stderr .strip () or f"exit code { result .returncode } "
107- raise CustomException (500 , err , "Restic Error" )
108- return result .stdout
109- except CustomException :
110- raise
111- except subprocess .TimeoutExpired :
112- raise CustomException (500 , "Restic operation timed out after 3600s" , "Restic Timeout" )
113- except FileNotFoundError :
114- raise CustomException (500 , "Restic binary not found" , "Restic Unavailable" )
115- except Exception as e :
116- raise CustomException (500 , f"Restic command failed: { e } " , "Restic Error" )
117-
118- # ------------------------------------------------------------------
119- # Docker Restic execution — for backup / restore (needs volume mounts)
94+ # Docker Restic execution — all Restic operations use the same runtime
12095 # ------------------------------------------------------------------
12196 def _resolve_host_path (self , container_path : str ) -> str :
12297 """Resolve a container-internal path to its host-level equivalent
@@ -245,12 +220,15 @@ def _build_restic_volume_mounts(self, volumes_info: List[Dict[str, Any]]) -> tup
245220
246221 return extra_volumes , container_paths
247222
223+ def _run_restic_repo_command (self , command : List [str ]) -> str :
224+ return self ._run_restic_container (command , {})
225+
248226 # ------------------------------------------------------------------
249- # Repository management (local — only touches repo path)
227+ # Repository management
250228 # ------------------------------------------------------------------
251229 def _check_repository (self ) -> bool :
252230 try :
253- cfg = json .loads (self ._run_restic_local (["cat" , "config" ]))
231+ cfg = json .loads (self ._run_restic_repo_command (["cat" , "config" ]))
254232 return bool (cfg .get ("id" ) and cfg .get ("version" ))
255233 except CustomException :
256234 return False
@@ -261,7 +239,7 @@ def _init_repository(self):
261239 if self ._check_repository ():
262240 return
263241 try :
264- output = self ._run_restic_local (["init" ])
242+ output = self ._run_restic_repo_command (["init" ])
265243 result = json .loads (output )
266244 if result .get ("message_type" ) != "initialized" :
267245 logger .error (f"Unexpected init response: { result } " )
@@ -270,7 +248,7 @@ def _init_repository(self):
270248 raise
271249
272250 # ------------------------------------------------------------------
273- # Read-only operations — local binary (fast, no Docker)
251+ # Repository operations — container Restic
274252 # ------------------------------------------------------------------
275253 def list_snapshots (self , app_id : Optional [str ] = None ) -> List [Dict [str , Any ]]:
276254 try :
@@ -281,7 +259,7 @@ def list_snapshots(self, app_id: Optional[str] = None) -> List[Dict[str, Any]]:
281259 if app_id :
282260 command .extend (["--tag" , app_id ])
283261
284- output = self ._run_restic_local (command )
262+ output = self ._run_restic_repo_command (command )
285263 return json .loads (output ) if output .strip () else []
286264 except (json .JSONDecodeError , KeyError ):
287265 raise CustomException (500 , "Failed to parse snapshot list" , "Parse Error" )
@@ -295,7 +273,7 @@ def delete_snapshot(self, snapshot_id: str) -> None:
295273 if not self ._check_repository ():
296274 raise CustomException (400 , "Repository not initialized" , "Repository Error" )
297275
298- output = self ._run_restic_local (["forget" , snapshot_id ])
276+ output = self ._run_restic_repo_command (["forget" , snapshot_id ])
299277 if output .strip ():
300278 raise CustomException (400 , f"Delete failed: { output } " , f"Snapshot: { snapshot_id } " )
301279 except CustomException :
0 commit comments