22import re
33import shutil
44import tempfile
5+ import subprocess
56
6- import rclone
77from statemachine import State , StateMachine
88
99default_output_dir = "out"
1010
11- # TODO(mitchellthompkins): This won't work on windows, check the rclone
12- # documentation for the windows default location
13- default_rclone_conf_dir = os .path .join (
14- os .environ ["HOME" ], ".config" , "rclone" , "rclone.conf"
15- )
11+ try :
12+ if shutil .which ("rclone" ):
13+ # Get the rclone config file path dynamically
14+ cmd = ["rclone" , "config" , "file" ]
15+ out = subprocess .check_output (cmd ).decode ().strip ()
16+ # The output format is usually:
17+ # "Configuration file is stored at:\n/path/to/rclone.conf"
18+ # We need to parse the last line
19+ default_rclone_conf_dir = out .splitlines ()[- 1 ]
20+ else :
21+ raise FileNotFoundError ("rclone executable not found" )
22+ except (subprocess .CalledProcessError , FileNotFoundError ):
23+ # Fallback to the previous default if rclone command fails or is not found
24+ default_rclone_conf_dir = os .path .join (
25+ os .environ ["HOME" ], ".config" , "rclone" , "rclone.conf"
26+ )
1627
1728
1829class ConfigFileError (Exception ):
@@ -77,99 +88,106 @@ def before_is_valid(self, line: str) -> None:
7788 self .cfg_file .write (line )
7889
7990
80- def get_rclone_instance (
91+ def get_rclone_config_path (
8192 config : str , files : str , remote_folder_name : str
82- ) -> rclone . RClone :
93+ ) -> str :
8394 """
8495 Opens a config file and strips out all of the non-crypt type entries,
8596 modifies the remote to be local directory.
8697
87- Returns an rclone instance .
98+ Returns the path to the temporary rclone config file .
8899 """
89- rclone_instance = None
100+ config_path = None
90101
91102 try :
92103 with open (config , "r" ) as f :
93104 config_file = f .readlines ()
94105
95- with tempfile .NamedTemporaryFile (
96- mode = "wt" , delete = True
97- ) as tmp_config_file :
98- with open (tmp_config_file .name , "w" ) as config :
99- config_state = ConfigWriterControl (config )
100-
101- for line in config_file :
102- state_id = config_state .current_state .id
103-
104- if state_id == "searching_for_start" :
105- start_of_entry = re .search ("\\ [.*?\\ ]" , line )
106-
107- if start_of_entry is not None :
108- config_state .validate (line )
106+ # Create a temporary file that persists until manually deleted or
107+ # cleaned up by caller.
108+ # We use delete=False so we can return the path and use it later.
109+ # It will be created in the system temp dir or temp_dir_name if
110+ # passed? Actually, let's create it inside remote_folder_name
111+ # (which is a temp dir).
112+
113+ config_path = os .path .join (remote_folder_name , "rclone.conf" )
114+
115+ with open (config_path , "w" ) as config_out :
116+ config_state = ConfigWriterControl (config_out )
117+
118+ for line in config_file :
119+ state_id = config_state .current_state .id
120+
121+ if state_id == "searching_for_start" :
122+ start_of_entry = re .search ("\\ [.*?\\ ]" , line )
123+
124+ if start_of_entry is not None :
125+ config_state .validate (line )
126+ else :
127+ config_state .search ()
128+
129+ elif state_id == "type_check" :
130+ regex_str = "type\\ s*=\\ s*([\\ S\\ s]+)"
131+ entry_type = re .search (regex_str , line )
132+ if entry_type is not None :
133+ entry_type = entry_type .group (1 ).strip ()
134+ if entry_type == "crypt" :
135+ valid_str = f"type = { entry_type } \n "
136+ config_state .is_valid (valid_str )
109137 else :
110- config_state .search ()
111-
112- elif state_id == "type_check" :
113- regex_str = "type\\ s*=\\ s*([\\ S\\ s]+)"
114- entry_type = re .search (regex_str , line )
115- if entry_type is not None :
116- entry_type = entry_type .group (1 ).strip ()
117- if entry_type == "crypt" :
118- valid_str = f"type = { entry_type } \n "
119- config_state .is_valid (valid_str )
120- else :
121- config_state .is_invalid ()
122-
123- elif state_id == "writing" :
124- regex_str = "remote\\ s*=\\ s*([\\ S\\ s]+)"
125- remote = re .search (regex_str , line )
126- if remote is not None :
127- config_state .write (
128- f"remote =\
129- { remote_folder_name } /\n "
130- )
131-
132- elif line == "\n " :
133- config_state .write (line )
134- config_state .write_complete ()
138+ config_state .is_invalid ()
135139
136- else :
137- config_state .write (line )
140+ elif state_id == "writing" :
141+ regex_str = "remote\\ s*=\\ s*([\\ S\\ s]+)"
142+ remote = re .search (regex_str , line )
143+ if remote is not None :
144+ config_state .write (
145+ f"remote =\
146+ { remote_folder_name } /\n "
147+ )
138148
139- config_state .complete ()
149+ elif line == "\n " :
150+ config_state .write (line )
151+ config_state .write_complete ()
140152
141- # Open the modified temporary file and create our instance
142- with open (tmp_config_file .name , "r" ) as t :
143- o = t .read ()
144- rclone_instance = rclone .with_config (o )
153+ else :
154+ config_state .write (line )
145155
146- # I think that given a file, any file, rclone.with_config() will always
147- # return _something_ as it doesn't validate the config file
148- if rclone_instance is None :
149- raise ConfigFileError ("The rclone instance was not created." )
156+ config_state .complete ()
150157
151158 except FileNotFoundError as err :
152159 print_error (err )
160+ return None
153161
154- return rclone_instance
162+ return config_path
155163
156164
157- def rclone_copy (rclone_instance : rclone . RClone , output_dir : str ) -> None :
165+ def rclone_copy (config_path : str , output_dir : str ) -> None :
158166 """
159167 Calls the rclone copy function via a shell instance and places the
160168 decrypted files into the output_dir
161169 """
162170 # convert list of remotes in str format into a list
163- remotes = rclone_instance .listremotes ()["out" ].decode ().splitlines ()
171+ list_cmd = ["rclone" , "--config" , config_path , "listremotes" ]
172+ try :
173+ out = subprocess .check_output (list_cmd ).decode ()
174+ remotes = out .splitlines ()
175+ except subprocess .CalledProcessError as e :
176+ print_error (f"Failed to list remotes: { e } " )
177+ return
164178
165179 for r in remotes :
166180 print (f"Copying and decrypting: { r } " )
167- rclone_instance .copy (f"{ r } " , f"{ output_dir } " )
168- # TODO(@mitchellthompkins): rclone.copy still returns 0 for an
169- # unsuccessful decryption. As long as the call itself doesn't fail, it
170- # will return 0. Need to come up with someway to detect success
171- # if success['code'] == 0:
172- # break
181+ copy_cmd = [
182+ "rclone" ,
183+ "--config" ,
184+ config_path ,
185+ "copy" ,
186+ f"{ r } " ,
187+ f"{ output_dir } " ,
188+ ]
189+ # TODO(@mitchellthompkins): check return code for success
190+ subprocess .run (copy_cmd , check = True )
173191
174192
175193def decrypt (
@@ -195,10 +213,10 @@ def decrypt(
195213
196214 try :
197215 with tempfile .TemporaryDirectory (dir = os .getcwd ()) as temp_dir_name :
198- rclone_instance = get_rclone_instance (config , files , temp_dir_name )
216+ config_path = get_rclone_config_path (config , files , temp_dir_name )
199217
200- if rclone_instance is None :
201- raise ConfigFileError ("rclone_instance cannot be None" )
218+ if config_path is None :
219+ raise ConfigFileError ("config_path cannot be None" )
202220
203221 if output_dir is default_output_dir :
204222 # If no output_dir is provided, put the de-crypted file into a
@@ -231,7 +249,7 @@ def decrypt(
231249 # Do the copy, we wrap this in a try in case the user
232250 # interrupts the process, otherwise the file won't be
233251 # moved back
234- rclone_copy (rclone_instance , output_dir )
252+ rclone_copy (config_path , output_dir )
235253 print (f"Decryption complete. Files saved to: { output_dir } " )
236254 except KeyboardInterrupt :
237255 print ("\n \t terminated rclone copy!" )
0 commit comments