Skip to content

Commit 8547ceb

Browse files
Merge pull request #15 from MitchellThompkins/update-python-rclone-dep
Remove python-rclone dependency
2 parents 252fc7a + ac759aa commit 8547ceb

6 files changed

Lines changed: 110 additions & 111 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: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "rclone_decrypt"
3-
version = "0.1.6"
3+
version = "0.1.7"
44
description = "Wrapper around rclone to decrypt files encrypted with rclone"
55
authors = ["Mitchell Thompkins <mitchell.thompkins@gmail.com>"]
66
license = "MIT"
@@ -13,7 +13,6 @@ readme = "README.md"
1313
[tool.poetry.dependencies]
1414
python = ">=3.10"
1515
click = "^8.1.3"
16-
python-rclone = "^0.0.2"
1716
python-statemachine = "^2.0.0"
1817
tkinterdnd2 = "^0.3.0"
1918

src/rclone_decrypt/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.1.6"
1+
__version__ = "0.1.7"

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: 91 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,28 @@
22
import re
33
import shutil
44
import tempfile
5+
import subprocess
56

6-
import rclone
77
from statemachine import State, StateMachine
88

99
default_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

1829
class 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

175193
def 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\tterminated rclone copy!")

tests/test_rclone_decrypt.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def test_no_config_file():
183183
Test behavior when provided no config file
184184
"""
185185
files = os.path.join("tests", "something_fake")
186-
instance = decrypt.get_rclone_instance("", files, "a_dir_name")
186+
instance = decrypt.get_rclone_config_path("", files, "a_dir_name")
187187

188188
assert instance is None
189189

@@ -193,8 +193,10 @@ def test_config_file():
193193
Test behavior when provided valid config file
194194
"""
195195
files = os.path.join("tests", "something_fake")
196-
instance = decrypt.get_rclone_instance(
197-
decrypt_rclone_config_file, files, "a_dir_name"
198-
)
196+
with tempfile.TemporaryDirectory() as temp_dir:
197+
instance = decrypt.get_rclone_config_path(
198+
decrypt_rclone_config_file, files, temp_dir
199+
)
199200

200-
assert instance is not None
201+
assert instance is not None
202+
assert os.path.exists(instance)

0 commit comments

Comments
 (0)