rclone_decrypt 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Mitchell Thompkins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.1
2
+ Name: rclone_decrypt
3
+ Version: 0.1.2
4
+ Summary: Wrapper around rclone to decrypt files encrypted with rclone
5
+ Home-page: https://github.com/MitchellThompkins/rclone-decrypt
6
+ License: MIT
7
+ Author: Mitchell Thompkins
8
+ Author-email: mitchell.thompkins@gmail.com
9
+ Requires-Python: >=3.8.1
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Dist: click (>=8.1.3,<9.0.0)
18
+ Requires-Dist: python-rclone (>=0.0.2,<0.0.3)
19
+ Requires-Dist: python-statemachine (>=2.0.0,<3.0.0)
20
+ Requires-Dist: tkinterdnd2 (>=0.3.0,<0.4.0)
21
+ Project-URL: Repository, https://github.com/MitchellThompkins/rclone-decrypt
22
+ Description-Content-Type: text/markdown
23
+
24
+ # rclone-decrypt
25
+ ## Status
26
+ ![test](https://github.com/mitchellthompkins/rclone-decrypt/actions/workflows/test.yml/badge.svg)
27
+ ![build](https://github.com/mitchellthompkins/rclone-decrypt/actions/workflows/build_app.yml/badge.svg)
28
+
29
+ ## Description
30
+ `rclone-decrypt` is a utility which will decrypt files that were encrypted with
31
+ [rclone](https://rclone.org/). The anticipated use-case is that a user has
32
+ independently downloaded an **encrypted** file or directory directly from a
33
+ remote cloud storage (Backblaze B2/Amazon Drive/Dropbox/etc...) and now wants to
34
+ decrypt it.
35
+
36
+ Given an rclone.conf file, this tool is simply a wrapper around `rclone` which
37
+ sets up a "local remote" to host the downloaded encrypted files and then calls
38
+ `rclone copy` in order to decrypt the files into a desired output folder.
39
+
40
+ Ostensibly I did this because my family backs-up our local NAS to a remote host
41
+ but the rest of my family prefers to download files one-off from the cloud host
42
+ and are not comfortable using the rclone CLI. This offers a CLI in addition to
43
+ an easy-to-use GUI to make life simple.
44
+
45
+ ### Notes
46
+ * **Use at your own risk! Be sure you have copies of anything you're trying to
47
+ decrypt, just in case something goes wrong!**
48
+ * When decrypting files with encrypted filenames or folder names, the directory
49
+ or filename must _only_ consist of the encrypted version. For example, if an
50
+ encrypted file was downloaded as `path_to_encypted_file_4567asd8fasdf67asdf`
51
+ where `4567asd8fasdf67asdf` is the encrypted part, the filename must be
52
+ renamed to exclude the `path_to_encypted_file_` portion. Otherwise rclone will
53
+ complain about invalid encryption names.
54
+ * Windows is _not_ currently supported, although it probably would not take very
55
+ much work to get it there. I do not have ready access to a windows environment
56
+ on which to test.
57
+ * I'd love to make the GUI look more modern, but most solutions involve a style
58
+ which seems incompatible with
59
+ [tkinterdnd2](https://github.com/Eliav2/tkinterdnd2) which provides the drag
60
+ and drop feature.
61
+
62
+ ## Installation
63
+ ```
64
+ pip3 install rclone-decrypt
65
+ ```
66
+
67
+ ## Requirements
68
+ ### General
69
+ * `rclone` must be installed and in `$PATH`
70
+
71
+ ### Python environment
72
+ * `Python >= 3.7 <3.12`
73
+ * `Python-tk` must be installed if using the GUI
74
+
75
+ ### Executable
76
+ **UNDER DEVELOPMENT** An OSX `.app` is generated but is currently untested.
77
+
78
+ ## Usage
79
+ ### CLI usage
80
+ ```
81
+ > rclone-decrypt --config /path/to/rclone.conf --files /path/to/file/or/dir/
82
+ ```
83
+
84
+ Example usages:
85
+ ```
86
+ > rclone-decrypt --config rclone.conf --files /home/my_encrypted_dir
87
+ > rclone-decrypt --config rclone.conf --files /0f12hh28evsof1kgflv67ldcn/9g6h49o4ht35u7o5e4iv5a1h28
88
+ > rclone-decrypt --config rclone.conf --files /home/my_encrypted_file.bin
89
+ ```
90
+ ### GUI usage
91
+ If the python package is installed directly then the GUI can be invoked from the
92
+ command line, as shown below. Otherwise the packaged binary can be downloaded
93
+ and executed directly.
94
+ * Files can be dropped directly into the big white box.
95
+ * As files are dropped, if no output directory has been provided though the file
96
+ dialog, an output directory called 'out' will be created at the same directory
97
+ level as the last dropped file to be decrypted.
98
+ * A default location for `rclone.conf` is provided, others can be browsed for.
99
+ ```
100
+ rclone-decrypt --gui
101
+ ```
102
+
103
+ ![rclone_example](docs/imgs/rclone_gui.png)
104
+
105
+ ## Development
106
+ ```
107
+ source .venv/bin/activate
108
+ poetry install
109
+ poetry run pytest
110
+ poetry run flake8
111
+ deactivate
112
+ ```
113
+
@@ -0,0 +1,89 @@
1
+ # rclone-decrypt
2
+ ## Status
3
+ ![test](https://github.com/mitchellthompkins/rclone-decrypt/actions/workflows/test.yml/badge.svg)
4
+ ![build](https://github.com/mitchellthompkins/rclone-decrypt/actions/workflows/build_app.yml/badge.svg)
5
+
6
+ ## Description
7
+ `rclone-decrypt` is a utility which will decrypt files that were encrypted with
8
+ [rclone](https://rclone.org/). The anticipated use-case is that a user has
9
+ independently downloaded an **encrypted** file or directory directly from a
10
+ remote cloud storage (Backblaze B2/Amazon Drive/Dropbox/etc...) and now wants to
11
+ decrypt it.
12
+
13
+ Given an rclone.conf file, this tool is simply a wrapper around `rclone` which
14
+ sets up a "local remote" to host the downloaded encrypted files and then calls
15
+ `rclone copy` in order to decrypt the files into a desired output folder.
16
+
17
+ Ostensibly I did this because my family backs-up our local NAS to a remote host
18
+ but the rest of my family prefers to download files one-off from the cloud host
19
+ and are not comfortable using the rclone CLI. This offers a CLI in addition to
20
+ an easy-to-use GUI to make life simple.
21
+
22
+ ### Notes
23
+ * **Use at your own risk! Be sure you have copies of anything you're trying to
24
+ decrypt, just in case something goes wrong!**
25
+ * When decrypting files with encrypted filenames or folder names, the directory
26
+ or filename must _only_ consist of the encrypted version. For example, if an
27
+ encrypted file was downloaded as `path_to_encypted_file_4567asd8fasdf67asdf`
28
+ where `4567asd8fasdf67asdf` is the encrypted part, the filename must be
29
+ renamed to exclude the `path_to_encypted_file_` portion. Otherwise rclone will
30
+ complain about invalid encryption names.
31
+ * Windows is _not_ currently supported, although it probably would not take very
32
+ much work to get it there. I do not have ready access to a windows environment
33
+ on which to test.
34
+ * I'd love to make the GUI look more modern, but most solutions involve a style
35
+ which seems incompatible with
36
+ [tkinterdnd2](https://github.com/Eliav2/tkinterdnd2) which provides the drag
37
+ and drop feature.
38
+
39
+ ## Installation
40
+ ```
41
+ pip3 install rclone-decrypt
42
+ ```
43
+
44
+ ## Requirements
45
+ ### General
46
+ * `rclone` must be installed and in `$PATH`
47
+
48
+ ### Python environment
49
+ * `Python >= 3.7 <3.12`
50
+ * `Python-tk` must be installed if using the GUI
51
+
52
+ ### Executable
53
+ **UNDER DEVELOPMENT** An OSX `.app` is generated but is currently untested.
54
+
55
+ ## Usage
56
+ ### CLI usage
57
+ ```
58
+ > rclone-decrypt --config /path/to/rclone.conf --files /path/to/file/or/dir/
59
+ ```
60
+
61
+ Example usages:
62
+ ```
63
+ > rclone-decrypt --config rclone.conf --files /home/my_encrypted_dir
64
+ > rclone-decrypt --config rclone.conf --files /0f12hh28evsof1kgflv67ldcn/9g6h49o4ht35u7o5e4iv5a1h28
65
+ > rclone-decrypt --config rclone.conf --files /home/my_encrypted_file.bin
66
+ ```
67
+ ### GUI usage
68
+ If the python package is installed directly then the GUI can be invoked from the
69
+ command line, as shown below. Otherwise the packaged binary can be downloaded
70
+ and executed directly.
71
+ * Files can be dropped directly into the big white box.
72
+ * As files are dropped, if no output directory has been provided though the file
73
+ dialog, an output directory called 'out' will be created at the same directory
74
+ level as the last dropped file to be decrypted.
75
+ * A default location for `rclone.conf` is provided, others can be browsed for.
76
+ ```
77
+ rclone-decrypt --gui
78
+ ```
79
+
80
+ ![rclone_example](docs/imgs/rclone_gui.png)
81
+
82
+ ## Development
83
+ ```
84
+ source .venv/bin/activate
85
+ poetry install
86
+ poetry run pytest
87
+ poetry run flake8
88
+ deactivate
89
+ ```
@@ -0,0 +1,29 @@
1
+ [tool.poetry]
2
+ name = "rclone_decrypt"
3
+ version = "0.1.2"
4
+ description = "Wrapper around rclone to decrypt files encrypted with rclone"
5
+ authors = ["Mitchell Thompkins <mitchell.thompkins@gmail.com>"]
6
+ license = "MIT"
7
+
8
+ repository = "https://github.com/MitchellThompkins/rclone-decrypt"
9
+
10
+ # README file(s) are used as the package description
11
+ readme = "README.md"
12
+
13
+ [tool.poetry.dependencies]
14
+ python = ">=3.8.1"
15
+ click = "^8.1.3"
16
+ python-rclone = "^0.0.2"
17
+ python-statemachine = "^2.0.0"
18
+ tkinterdnd2 = "^0.3.0"
19
+
20
+ [tool.poetry.dev-dependencies]
21
+ pytest = "^5.2"
22
+ flake8 = "^6.0.0"
23
+
24
+ [build-system]
25
+ requires = ["poetry-core>=1.0.0"]
26
+ build-backend = "poetry.core.masonry.api"
27
+
28
+ [tool.poetry.scripts]
29
+ rclone-decrypt = "rclone_decrypt.cli:cli"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -0,0 +1,49 @@
1
+ import click
2
+ import rclone_decrypt.decrypt as decrypt
3
+ import rclone_decrypt.gui as GUI
4
+
5
+
6
+ @click.command()
7
+ @click.option(
8
+ "--config",
9
+ help=f"config file. default config file is:\
10
+ {decrypt.default_rclone_conf_dir}",
11
+ default=decrypt.default_rclone_conf_dir,
12
+ required=True,
13
+ )
14
+ @click.option(
15
+ "--files",
16
+ help="dir or file to decrypt",
17
+ default=None)
18
+ @click.option(
19
+ "--output_dir",
20
+ help=f"output dir in which to put files. default folder is:\
21
+ {decrypt.default_output_dir}",
22
+ default=decrypt.default_output_dir,
23
+ )
24
+ @click.option(
25
+ "--gui",
26
+ help="start the GUI",
27
+ is_flag=True,
28
+ default=False)
29
+ @click.option(
30
+ "--gui_debug",
31
+ help="print debug messages",
32
+ is_flag=True,
33
+ default=False)
34
+ def cli(config, files, output_dir, gui, gui_debug):
35
+ if gui is True:
36
+ GUI.start_gui(gui_debug)
37
+ else:
38
+ try:
39
+ if files is None:
40
+ raise ValueError("files cannot be None")
41
+ else:
42
+ decrypt.decrypt(files, config, output_dir)
43
+
44
+ except ValueError as err:
45
+ decrypt.print_error(err)
46
+
47
+
48
+ if __name__ == "__main__":
49
+ cli()
@@ -0,0 +1,223 @@
1
+ import os
2
+ import rclone
3
+ import re
4
+ import tempfile
5
+
6
+ from statemachine import StateMachine, State
7
+
8
+ default_output_dir = "out"
9
+
10
+ # TODO(mitchellthompkins): This won't work on windows, check the rclone
11
+ # documentation for the windows default location
12
+ default_rclone_conf_dir = os.path.join(
13
+ os.environ["HOME"], ".config", "rclone", "rclone.conf"
14
+ )
15
+
16
+
17
+ class ConfigFileError(Exception):
18
+ def __init__(self, *args, **kwargs):
19
+ default_message = """There is a problem with the rclone
20
+ configuration file"""
21
+
22
+ if not args:
23
+ args = (default_message,)
24
+
25
+ # Call super constructor
26
+ super().__init__(*args, **kwargs)
27
+
28
+
29
+ def print_error(msg: str) -> None:
30
+ """
31
+ Print generic error.
32
+ """
33
+ print(f"ERROR: {msg}")
34
+
35
+
36
+ class ConfigWriterControl(StateMachine):
37
+ searching_for_start = State(initial=True)
38
+ type_check = State()
39
+ writing = State()
40
+ completed = State(final=True)
41
+
42
+ search = searching_for_start.to(searching_for_start)
43
+ validate = searching_for_start.to(type_check)
44
+ is_valid = type_check.to(writing)
45
+ is_invalid = type_check.to(searching_for_start)
46
+ write = type_check.to(writing) | writing.to(writing)
47
+ write_complete = writing.to(searching_for_start)
48
+ complete = searching_for_start.to(completed) | writing.to(completed)
49
+
50
+ def __init__(self, cfg_file: str) -> None:
51
+ self.cfg_file = cfg_file
52
+ self.cached_entry_start = None
53
+
54
+ super(ConfigWriterControl, self).__init__()
55
+
56
+ def before_validate(self, line: str) -> None:
57
+ self.cached_entry_start = line
58
+
59
+ def before_write(self, line: str) -> None:
60
+ self.cfg_file.write(line)
61
+
62
+ def before_is_valid(self, line: str) -> None:
63
+ self.cfg_file.write(self.cached_entry_start)
64
+ self.cfg_file.write(line)
65
+
66
+
67
+ def get_rclone_instance(config: str, files: str,
68
+ remote_folder_name: str) -> rclone.RClone:
69
+ """
70
+ Opens a config file and strips out all of the non-crypt type entries,
71
+ modifies the remote to be local directory.
72
+
73
+ Returns an rclone instance.
74
+ """
75
+ rclone_instance = None
76
+
77
+ try:
78
+ with open(config, "r") as f:
79
+ config_file = f.readlines()
80
+
81
+ with tempfile.NamedTemporaryFile(mode="wt", delete=True)\
82
+ as tmp_config_file:
83
+
84
+ with open(tmp_config_file.name, "w") as config:
85
+ config_state = ConfigWriterControl(config)
86
+
87
+ for line in config_file:
88
+ if config_state.current_state.id ==\
89
+ "searching_for_start":
90
+ start_of_entry = re.search("\\[.*?\\]", line)
91
+
92
+ if start_of_entry is not None:
93
+ config_state.validate(line)
94
+ else:
95
+ config_state.search()
96
+
97
+ elif config_state.current_state.id == "type_check":
98
+ entry_type = re.search(
99
+ "type\\s*=\\s*([\\S\\s]+)", line)
100
+ if entry_type is not None:
101
+ entry_type = entry_type.group(1).strip()
102
+ if entry_type == "crypt":
103
+ config_state.is_valid(
104
+ f"type = {entry_type}\n")
105
+ else:
106
+ config_state.is_invalid()
107
+
108
+ elif config_state.current_state.id == "writing":
109
+ remote = re.search(
110
+ "remote\\s*=\\s*([\\S\\s]+)", line)
111
+ if remote is not None:
112
+ config_state.write(
113
+ f"remote =\
114
+ {remote_folder_name}/\n"
115
+ )
116
+
117
+ elif line == "\n":
118
+ config_state.write(line)
119
+ config_state.write_complete()
120
+
121
+ else:
122
+ config_state.write(line)
123
+
124
+ config_state.complete()
125
+
126
+ # Open the modified temporary file and create our instance
127
+ with open(tmp_config_file.name, "r") as t:
128
+ o = t.read()
129
+ rclone_instance = rclone.with_config(o)
130
+
131
+ # I think that given a file, any file, rclone.with_config() will always
132
+ # return _something_ as it doesn't validate the config file
133
+ if rclone_instance is None:
134
+ raise ConfigFileError("The rclone instance was not created.")
135
+
136
+ except FileNotFoundError as err:
137
+ print_error(err)
138
+
139
+ return rclone_instance
140
+
141
+
142
+ def rclone_copy(rclone_instance: rclone.RClone, output_dir: str) -> None:
143
+ """
144
+ Calls the rclone copy function via a shell instance and places the
145
+ decrypted files into the output_dir
146
+ """
147
+ # convert list of remotes in str format into a list
148
+ remotes = rclone_instance.listremotes()["out"].decode().splitlines()
149
+
150
+ for r in remotes:
151
+ rclone_instance.copy(f"{r}", f"{output_dir}")
152
+ # TODO(@mitchellthompkins): rclone.copy still returns 0 for an
153
+ # unsuccessful decryption. As long as the call itself doesn't fail, it
154
+ # will return 0. Need to come up with someway to detect success
155
+ # if success['code'] == 0:
156
+ # break
157
+
158
+
159
+ def decrypt(
160
+ files: str,
161
+ config: str = default_rclone_conf_dir,
162
+ output_dir: str = default_output_dir
163
+ ) -> None:
164
+ """
165
+ Sets up the files or directories to be decrypted by moving them to the
166
+ correct relative path. The appropriate temporary config file is generated
167
+ and the appropriate rclone_copy function is then called to perform the
168
+ decryption.
169
+
170
+ Explicitly, this creates a temporary directory at the same root as where
171
+ this is called from, moves the files (or file) to be decrypted to that
172
+ directory, modifies a temporary config file in order to point rclone to a
173
+ folder in _this_ directory, calls `rclone --config config file copy
174
+ remote:local_tmp_dir out` and then moves the files back to their original
175
+ location.
176
+ """
177
+ try:
178
+ with tempfile.TemporaryDirectory(dir=os.getcwd()) as temp_dir_name:
179
+ rclone_instance = get_rclone_instance(config, files, temp_dir_name)
180
+
181
+ if rclone_instance is None:
182
+ raise ConfigFileError("rclone_instance cannot be None")
183
+
184
+ if output_dir is default_output_dir:
185
+ # If no output_dir is provided, put the de-crypted file into a
186
+ # folder called 'out' that lives at the same base dir as that
187
+ # of the input file
188
+ base_file_dir = os.path.basename(
189
+ os.path.dirname(files))
190
+
191
+ file_input_dir = os.path.dirname(
192
+ os.path.abspath(base_file_dir))
193
+
194
+ output_dir = os.path.join(file_input_dir, output_dir)
195
+
196
+ # if the output folder doesn't exist, make it
197
+ if not os.path.isdir(output_dir):
198
+ os.mkdir(output_dir)
199
+
200
+ # When folder names are encrypted, I don't think that the config
201
+ # file can look wherever it wants in a sub directory, so the folder
202
+ # we're looking for must live in the same root directory as where
203
+ # rclone is called from
204
+ actual_path = os.path.abspath(files)
205
+ dir_or_file_name = os.path.basename(actual_path)
206
+ temp_file_path = os.path.join(temp_dir_name, dir_or_file_name)
207
+
208
+ # Move the folder
209
+ os.rename(actual_path, temp_file_path)
210
+
211
+ try:
212
+ # Do the copy, we wrap this in a try in case the user
213
+ # interrupts the process, otherwise the file won't be
214
+ # moved back
215
+ rclone_copy(rclone_instance, output_dir)
216
+ except KeyboardInterrupt:
217
+ print("\n\tterminated rclone copy!")
218
+
219
+ # Move it back
220
+ os.rename(temp_file_path, actual_path)
221
+
222
+ except ConfigFileError as err:
223
+ print_error(err)
@@ -0,0 +1,163 @@
1
+ import logging
2
+ import os
3
+ import tkinter
4
+ import tkinter.filedialog
5
+ import rclone_decrypt.decrypt as decrypt
6
+
7
+ from tkinterdnd2 import DND_FILES, TkinterDnD
8
+
9
+
10
+ class DecryptWindow:
11
+ def __init__(self, title: str, geometry: str, debug: bool):
12
+ self.title = title
13
+ self.geometry = geometry
14
+ self.window = TkinterDnD.Tk()
15
+ self.selected_entry = None
16
+ self.defined_output_dir = False
17
+
18
+ self.debug = debug
19
+ self.files = []
20
+ self.config_file = decrypt.default_rclone_conf_dir
21
+ self.output_dir = decrypt.default_output_dir
22
+
23
+ self.browse_config_button = tkinter.Button(
24
+ self.window, text="Browse", command=self.get_config
25
+ )
26
+
27
+ self.browse_output_button = tkinter.Button(
28
+ self.window, text="Browse", command=self.get_output
29
+ )
30
+
31
+ self.remove_button = tkinter.Button(
32
+ self.window, text="Remove Selected", command=self.remove_entry
33
+ )
34
+
35
+ self.decrypt_button = tkinter.Button(
36
+ self.window, text="Decrypt", command=self.decrypt
37
+ )
38
+
39
+ self.lb = tkinter.Listbox(self.window, width=66, height=10)
40
+
41
+ self.config_label = tkinter.Label(
42
+ self.window, text="Select a config file:")
43
+
44
+ self.output_label = tkinter.Label(
45
+ self.window, text="Select an output directory:"
46
+ )
47
+
48
+ self.instruction_label = tkinter.Label(
49
+ self.window,
50
+ text="\nDrag files and folders to decrypt into the box below:"
51
+ )
52
+
53
+ self.config_entry = tkinter.Text(self.window, height=1, width=70)
54
+ self.config_entry.insert(tkinter.END, self.config_file)
55
+ self.config_entry.config(state=tkinter.DISABLED)
56
+
57
+ self.output_entry = tkinter.Text(self.window, height=1, width=70)
58
+ self.output_entry.insert(tkinter.END, self.output_dir)
59
+ self.output_entry.config(state=tkinter.DISABLED)
60
+
61
+ def decrypt(self):
62
+ for f in self.files:
63
+ # Files with spaces get {} prepended and appended
64
+ f = f.strip("{}")
65
+ decrypt.decrypt(f, self.config_file, self.output_dir)
66
+
67
+ def select(self, evt):
68
+ if len(self.files) != 0:
69
+ self.selected_entry = self.lb.get(self.lb.curselection())
70
+
71
+ def get_config(self):
72
+ file = tkinter.filedialog.askopenfile(
73
+ mode="r", filetypes=[("rclone config", "*.conf")]
74
+ )
75
+ if file:
76
+ self.config_file = os.path.abspath(file.name)
77
+
78
+ self.config_entry.config(state=tkinter.NORMAL)
79
+ self.config_entry.delete("1.0", tkinter.END)
80
+ self.config_entry.insert(tkinter.END, self.config_file)
81
+ self.config_entry.config(state=tkinter.DISABLED)
82
+
83
+ def get_output(self):
84
+ dir = tkinter.filedialog.askdirectory()
85
+ if dir:
86
+ self.output_dir = os.path.abspath(dir)
87
+ self.defined_output_dir = True
88
+
89
+ self.output_entry.config(state=tkinter.NORMAL)
90
+ self.output_entry.delete("1.0", tkinter.END)
91
+ self.output_entry.insert(tkinter.END, self.output_dir)
92
+ self.output_entry.config(state=tkinter.DISABLED)
93
+
94
+ def add_to_list(self, path):
95
+ if path not in self.files:
96
+ self.files.append(path)
97
+ self.lb.insert(tkinter.END, path)
98
+ else:
99
+ if self.debug:
100
+ logging.warning(f"{path} already in list.")
101
+
102
+ if self.defined_output_dir is False:
103
+ self.output_dir = decrypt.default_output_dir
104
+
105
+ dirname = os.path.dirname(path.strip("{}"))
106
+ self.output_dir = os.path.join(dirname, self.output_dir)
107
+
108
+ self.output_entry.config(state=tkinter.NORMAL)
109
+ self.output_entry.delete("1.0", tkinter.END)
110
+ self.output_entry.insert(tkinter.END, self.output_dir)
111
+ self.output_entry.config(state=tkinter.DISABLED)
112
+
113
+ def remove_entry(self):
114
+ if self.selected_entry is not None:
115
+ self.files.remove(self.selected_entry)
116
+
117
+ entry = self.lb.get(0, tkinter.END).index(self.selected_entry)
118
+ self.lb.delete(entry)
119
+ self.selected_entry = None
120
+
121
+ def render(self):
122
+ self.window.title(self.title)
123
+ self.window.geometry(self.geometry)
124
+
125
+ self.lb.drop_target_register(DND_FILES)
126
+ self.lb.dnd_bind("<<Drop>>", lambda e: self.add_to_list(e.data))
127
+ self.lb.bind("<<ListboxSelect>>", self.select)
128
+
129
+ # row0
130
+ self.config_label.grid(sticky="E", row=0, column=0, pady=2)
131
+ self.config_entry.grid(row=0, column=1, pady=2)
132
+ self.browse_config_button.grid(
133
+ sticky="W", row=0, column=2, padx=10, pady=2)
134
+
135
+ # row1
136
+ self.output_label.grid(sticky="E", row=1, column=0, pady=2)
137
+ self.output_entry.grid(row=1, column=1, pady=2)
138
+ self.browse_output_button.grid(
139
+ sticky="W", row=1, column=2, padx=10, pady=2)
140
+
141
+ # row2
142
+ self.instruction_label.grid(row=2, column=1, padx=10, pady=2)
143
+
144
+ # row3
145
+ self.lb.grid(row=3, column=1, padx=2, pady=2)
146
+ self.remove_button.grid(sticky="W", row=3, column=2, padx=10, pady=20)
147
+
148
+ # row4
149
+ self.decrypt_button.grid(row=4, column=1, pady=20)
150
+
151
+ self.window.mainloop()
152
+
153
+
154
+ def start_gui(debug: bool = False):
155
+ title = "rclone-decrypt"
156
+ geometry = "1770x600+100+200"
157
+
158
+ w = DecryptWindow(title, geometry, debug)
159
+ w.render()
160
+
161
+
162
+ if __name__ == "__main__":
163
+ start_gui()