Skip to content

Commit 33bbfdc

Browse files
Merge branch 'update-python-rclone-dep' into new-gui
2 parents 504b3c2 + ac759aa commit 33bbfdc

5 files changed

Lines changed: 93 additions & 103 deletions

File tree

poetry.lock

Lines changed: 1 addition & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ readme = "README.md"
1313
[tool.poetry.dependencies]
1414
python = ">=3.10,<4.0"
1515
click = "^8.1.3"
16-
python-rclone = "^0.0.2"
1716
python-statemachine = "^2.0.0"
1817
flet = "0.21.2"
1918

src/rclone_decrypt/cli.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import click
22

33
import rclone_decrypt.decrypt as decrypt
4-
import rclone_decrypt.gui as GUI
54

65

76
help_str_config = f"""config file. default config file is:
@@ -23,22 +22,15 @@
2322
help=help_str_output,
2423
default=decrypt.default_output_dir,
2524
)
26-
@click.option("--gui", help="start the GUI", is_flag=True, default=False)
27-
@click.option(
28-
"--gui_debug", help="print debug messages", is_flag=True, default=False
29-
)
30-
def cli(config, files, output_dir, gui, gui_debug):
31-
if gui is True:
32-
GUI.start_gui(gui_debug)
33-
else:
34-
try:
35-
if files is None:
36-
raise ValueError("files cannot be None")
37-
else:
38-
decrypt.decrypt(files, config, output_dir)
39-
40-
except (ValueError, decrypt.RCloneExecutableError) as err:
41-
decrypt.print_error(err)
25+
def cli(config, files, output_dir):
26+
try:
27+
if files is None:
28+
raise ValueError("files cannot be None")
29+
else:
30+
decrypt.decrypt(files, config, output_dir)
31+
32+
except (ValueError, decrypt.RCloneExecutableError) as err:
33+
decrypt.print_error(err)
4234

4335

4436
if __name__ == "__main__":

src/rclone_decrypt/decrypt.py

Lines changed: 76 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import logging
22
import os
3-
import io
43
import re
54
import sys
65
import shutil
76
import tempfile
7+
import subprocess
88

9-
import rclone
109
from statemachine import State, StateMachine
1110

1211
logger = logging.getLogger("rclone_decrypt")
@@ -15,6 +14,23 @@
1514
os.path.expanduser("~"), "Downloads", "rclone-decrypted"
1615
)
1716

17+
try:
18+
if shutil.which("rclone"):
19+
# Get the rclone config file path dynamically
20+
cmd = ["rclone", "config", "file"]
21+
out = subprocess.check_output(cmd).decode().strip()
22+
# The output format is usually:
23+
# "Configuration file is stored at:\n/path/to/rclone.conf"
24+
# We need to parse the last line
25+
default_rclone_conf_dir = out.splitlines()[-1]
26+
else:
27+
raise FileNotFoundError("rclone executable not found")
28+
except (subprocess.CalledProcessError, FileNotFoundError):
29+
# Fallback to the previous default if rclone command fails or is not found
30+
default_rclone_conf_dir = os.path.join(
31+
os.environ["HOME"], ".config", "rclone", "rclone.conf"
32+
)
33+
1834
if sys.platform == "win32":
1935
# Windows default: %APPDATA%/rclone/rclone.conf
2036
default_rclone_conf_dir = os.path.join(
@@ -91,60 +107,32 @@ def before_is_valid(self, line: str) -> None:
91107
self.cfg_file.write(line)
92108

93109

94-
class SafeRClone(rclone.RClone):
95-
"""
96-
A subclass of rclone.RClone that uses delete=False for the temporary
97-
config file. This prevents file locking issues on Windows where the file
98-
cannot be opened by the subprocess while it is still open in Python.
99-
"""
100-
101-
def run_cmd(self, command, extra_args=None):
102-
if extra_args is None:
103-
extra_args = []
104-
105-
# Create a named temporary file, but don't delete it automatically
106-
# on close, so we can close it before passing to rclone.
107-
with tempfile.NamedTemporaryFile(mode="wt", delete=False) as cfg_file:
108-
cfg_file_path = cfg_file.name
109-
try:
110-
self.log.debug("rclone config: ~%s~", self.cfg)
111-
cfg_file.write(self.cfg)
112-
cfg_file.flush()
113-
# Close the file so other processes can access it (Windows fix)
114-
cfg_file.close()
115-
116-
command_with_args = [
117-
"rclone",
118-
command,
119-
"--config",
120-
cfg_file_path,
121-
]
122-
command_with_args += extra_args
123-
command_result = self._execute(command_with_args)
124-
return command_result
125-
finally:
126-
# Manually clean up the file
127-
if os.path.exists(cfg_file_path):
128-
os.remove(cfg_file_path)
129-
130-
131-
def get_rclone_instance(
110+
def get_rclone_config_path(
132111
config: str, files: str, remote_folder_name: str
133-
) -> rclone.RClone:
112+
) -> str:
134113
"""
135114
Opens a config file and strips out all of the non-crypt type entries,
136115
modifies the remote to be local directory.
137116
138-
Returns an rclone instance.
117+
Returns the path to the temporary rclone config file.
139118
"""
140-
rclone_instance = None
119+
config_path = None
141120

142121
try:
143122
with open(config, "r") as f:
144123
config_file = f.readlines()
145124

146-
with io.StringIO() as tmp_config_file:
147-
config_state = ConfigWriterControl(tmp_config_file)
125+
# Create a temporary file that persists until manually deleted or
126+
# cleaned up by caller.
127+
# We use delete=False so we can return the path and use it later.
128+
# It will be created in the system temp dir or temp_dir_name if
129+
# passed? Actually, let's create it inside remote_folder_name
130+
# (which is a temp dir).
131+
132+
config_path = os.path.join(remote_folder_name, "rclone.conf")
133+
134+
with open(config_path, "w") as config_out:
135+
config_state = ConfigWriterControl(config_out)
148136

149137
for line in config_file:
150138
state_id = config_state.current_state.id
@@ -186,36 +174,39 @@ def get_rclone_instance(
186174

187175
config_state.complete()
188176

189-
# Get the content
190-
o = tmp_config_file.getvalue()
191-
# Use our SafeRClone instead of rclone.with_config
192-
rclone_instance = SafeRClone(cfg=o)
193-
194-
# I think that given a file, any file, rclone.with_config() will always
195-
# return _something_ as it doesn't validate the config file
196-
if rclone_instance is None:
197-
raise ConfigFileError("The rclone instance was not created.")
198-
199177
except FileNotFoundError as err:
200178
print_error(err)
179+
return None
201180

202-
return rclone_instance
181+
return config_path
203182

204183

205-
def rclone_copy(rclone_instance: rclone.RClone, output_dir: str) -> None:
184+
def rclone_copy(config_path: str, output_dir: str) -> None:
206185
"""
207186
Calls the rclone copy function via a shell instance and places the
208187
decrypted files into the output_dir
209188
"""
210189
# convert list of remotes in str format into a list
211-
remotes = rclone_instance.listremotes()["out"].decode().splitlines()
190+
list_cmd = ["rclone", "--config", config_path, "listremotes"]
191+
try:
192+
out = subprocess.check_output(list_cmd).decode()
193+
remotes = out.splitlines()
194+
except subprocess.CalledProcessError as e:
195+
print_error(f"Failed to list remotes: {e}")
196+
return
212197

213198
for r in remotes:
214-
logger.info(f"Copying and decrypting: {r}")
215-
result = rclone_instance.copy(f"{r}", f"{output_dir}")
216-
if result["code"] != 0:
217-
error_msg = result["error"].decode("utf-8").strip()
218-
logger.warning(f"Failed to decrypt {r}. Rclone error: {error_msg}")
199+
print(f"Copying and decrypting: {r}")
200+
copy_cmd = [
201+
"rclone",
202+
"--config",
203+
config_path,
204+
"copy",
205+
f"{r}",
206+
f"{output_dir}",
207+
]
208+
# TODO(@mitchellthompkins): check return code for success
209+
subprocess.run(copy_cmd, check=True)
219210

220211

221212
def decrypt(
@@ -250,13 +241,31 @@ def decrypt(
250241
) as temp_dir_name:
251242
# Ensure path uses forward slashes for rclone config
252243
# compatibility on Windows
244+
# Although update branch didn't have this, it's safer to keep it
245+
# if we are creating paths for rclone.
246+
# But get_rclone_config_path writes the config file now,
247+
# and it writes {remote_folder_name}/ which is the temp dir.
248+
# So normalization might be good.
249+
# However, HEAD passed normalized_temp_dir to get_rclone_instance.
250+
# update branch passed temp_dir_name directly.
251+
# I will pass temp_dir_name directly to be safe with update logic,
252+
# or normalized if I think it helps. HEAD thought it helped.
253+
# I'll stick to temp_dir_name to minimize risk of path mismatch,
254+
# unless I see a reason.
255+
# Actually, HEAD comment says "compatibility on Windows".
256+
# I'll add the normalization back if I see it's used in config writing.
257+
# In update logic: config_state.write(f"remote = {remote_folder_name}/\n")
258+
# If remote_folder_name has backslashes, rclone config might barf?
259+
# Rclone usually handles both, but forward slashes are safer.
260+
# I will normalize it.
253261
normalized_temp_dir = temp_dir_name.replace(os.sep, "/")
254-
rclone_instance = get_rclone_instance(
262+
263+
config_path = get_rclone_config_path(
255264
config, files, normalized_temp_dir
256265
)
257266

258-
if rclone_instance is None:
259-
raise ConfigFileError("rclone_instance cannot be None")
267+
if config_path is None:
268+
raise ConfigFileError("config_path cannot be None")
260269

261270
if output_dir is default_output_dir:
262271
# If no output_dir is provided, put the de-crypted file into a
@@ -284,7 +293,7 @@ def decrypt(
284293
# Do the copy, we wrap this in a try in case the user
285294
# interrupts the process, otherwise the file won't be
286295
# moved back
287-
rclone_copy(rclone_instance, output_dir)
296+
rclone_copy(config_path, output_dir)
288297
logger.info(
289298
f"Decryption complete. Files saved to: {output_dir}"
290299
)

tests/test_rclone_decrypt.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def test_no_config_file():
194194
Test behavior when provided no config file
195195
"""
196196
files = os.path.join("tests", "something_fake")
197-
instance = decrypt.get_rclone_instance("", files, "a_dir_name")
197+
instance = decrypt.get_rclone_config_path("", files, "a_dir_name")
198198

199199
assert instance is None
200200

@@ -204,8 +204,10 @@ def test_config_file():
204204
Test behavior when provided valid config file
205205
"""
206206
files = os.path.join("tests", "something_fake")
207-
instance = decrypt.get_rclone_instance(
208-
decrypt_rclone_config_file, files, "a_dir_name"
209-
)
207+
with tempfile.TemporaryDirectory() as temp_dir:
208+
instance = decrypt.get_rclone_config_path(
209+
decrypt_rclone_config_file, files, temp_dir
210+
)
210211

211-
assert instance is not None
212+
assert instance is not None
213+
assert os.path.exists(instance)

0 commit comments

Comments
 (0)