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.
- rclone_decrypt-0.1.2/LICENSE.md +21 -0
- rclone_decrypt-0.1.2/PKG-INFO +113 -0
- rclone_decrypt-0.1.2/README.md +89 -0
- rclone_decrypt-0.1.2/pyproject.toml +29 -0
- rclone_decrypt-0.1.2/src/rclone_decrypt/__init__.py +1 -0
- rclone_decrypt-0.1.2/src/rclone_decrypt/cli.py +49 -0
- rclone_decrypt-0.1.2/src/rclone_decrypt/decrypt.py +223 -0
- rclone_decrypt-0.1.2/src/rclone_decrypt/gui.py +163 -0
|
@@ -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
|
+

|
|
27
|
+

|
|
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
|
+

|
|
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
|
+

|
|
4
|
+

|
|
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
|
+

|
|
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()
|