Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
bb212ae
Add separate config files for all connection methods.
JoeZiminski Sep 12, 2025
f02d6f1
Add password machinery for windows.
JoeZiminski Sep 24, 2025
bcc5da4
Prototype input for set up ssh connection.
JoeZiminski Sep 29, 2025
0c4017e
Refactor rclone call functions to handle password.
JoeZiminski Sep 29, 2025
9581d5a
Adding to google drive.
JoeZiminski Sep 30, 2025
ba27591
Added password to google dirve.
JoeZiminski Sep 30, 2025
18158b9
Working on aws.
JoeZiminski Oct 1, 2025
0bbfeca
Done adding to Linux.
JoeZiminski Oct 1, 2025
5329c58
Add to macOS.
JoeZiminski Oct 1, 2025
4372550
Set different names for different projects.
JoeZiminski Oct 1, 2025
366d67b
Refactoring.
JoeZiminski Oct 2, 2025
ebf073a
Refactoring more.
JoeZiminski Oct 2, 2025
53dcc5c
Tidy up rclone_password.py
JoeZiminski Oct 3, 2025
22a8db0
Editing setup_ssh.
JoeZiminski Oct 4, 2025
b32a979
Adding password to SSH GUI.
JoeZiminski Oct 4, 2025
d8364d3
Updating setup ssh.
JoeZiminski Oct 6, 2025
b9469d6
Playing with setup gdrive.
JoeZiminski Oct 6, 2025
e2d2bf7
Setup gdrive 2.
JoeZiminski Oct 6, 2025
6ad0d6b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 6, 2025
c2b6e19
Rough version working on all screens.
JoeZiminski Oct 6, 2025
6a12c54
Rough version working on all screens2.
JoeZiminski Oct 6, 2025
8f42d67
Start tidying up TUI.
JoeZiminski Oct 6, 2025
b7348ba
Refactoring Rclone Configs.
JoeZiminski Oct 7, 2025
b13a984
Finished refactor of configs.
JoeZiminski Oct 7, 2025
190f324
Add doc to setup_gdrive.
JoeZiminski Oct 7, 2025
c9a8fb2
Handling TODOs.
JoeZiminski Oct 7, 2025
71fed57
Refactor gdrive page.
JoeZiminski Oct 7, 2025
c743859
Fix variable.
JoeZiminski Oct 8, 2025
37e78f7
Extend the gdrive setup message.
JoeZiminski Oct 8, 2025
73995eb
Rework `_try_set_rclone_password`
JoeZiminski Oct 8, 2025
18ffdd5
Add TODO fixes.
JoeZiminski Oct 8, 2025
611a440
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 8, 2025
5cf30aa
Fix message asking to use security, add try / except to async
JoeZiminski Oct 9, 2025
e53259b
Add backward compatability message.
JoeZiminski Oct 9, 2025
89a6d3c
minor edit.
JoeZiminski Oct 9, 2025
3b7915c
Add docstrings to datashuttle class.
JoeZiminski Oct 13, 2025
46ed30e
Start renaming from password to encrpytion.
JoeZiminski Oct 13, 2025
4d7d7ba
Rename from setting rclone password to setting rclone encrpytion.
JoeZiminski Oct 13, 2025
a2246c2
Begin adding docstrings and type hints.
JoeZiminski Oct 13, 2025
c7d3fb1
Add docstrings and type hints.
JoeZiminski Oct 13, 2025
54cd2ee
Fix linting.
JoeZiminski Oct 30, 2025
eaa06b6
Add first rclone encryption test.
JoeZiminski Oct 30, 2025
3b4ac9d
Update yaml for testing.
JoeZiminski Oct 30, 2025
f9d3dbf
install pass on linux.
JoeZiminski Oct 30, 2025
f54d863
Initialise pass on linux.
JoeZiminski Oct 30, 2025
c1d1c65
Try a different pass set up command.
JoeZiminski Oct 30, 2025
6d1dcbd
Fix ssh and gdrive tests.
JoeZiminski Oct 31, 2025
9a45284
Fixing aws tests.
JoeZiminski Oct 31, 2025
7f1f077
Refactor ssh connection set up tests.
JoeZiminski Oct 31, 2025
bbadcec
Finalise aws and other tests, revert changes made to setup_aws during…
JoeZiminski Nov 1, 2025
f4b6d4c
Add note on docs.
JoeZiminski Nov 3, 2025
c4af1ac
Fix rebase error introduced.
JoeZiminski Nov 3, 2025
e0c91f1
Test AWS only because it is hanging in a weird way.
JoeZiminski Nov 3, 2025
1502e1c
Fix purge.
JoeZiminski Nov 4, 2025
342b5f5
Try in verbose mode :(
JoeZiminski Nov 4, 2025
4834b7c
Remove suggest next test and see if this works.
JoeZiminski Nov 4, 2025
2cf1ef4
Finally I think have the test fixes.
JoeZiminski Nov 4, 2025
2b42a46
Remove tests again for checking.
JoeZiminski Nov 5, 2025
210c57c
Try removing another test.
JoeZiminski Nov 5, 2025
74310ef
Try again.
JoeZiminski Nov 5, 2025
e90077a
Revert CI changes.
JoeZiminski Nov 5, 2025
2e0f793
Update documentation.
JoeZiminski Nov 5, 2025
f40bc33
Quick fix for running on local filesystem mode.
JoeZiminski Nov 5, 2025
8f3e444
Extend Mock Configs class.
JoeZiminski Nov 6, 2025
b316947
Fixing tests.
JoeZiminski Nov 18, 2025
60b30be
Fix validate_from_path.
JoeZiminski Nov 18, 2025
fd91242
Fix perform_rclone_check.
JoeZiminski Nov 18, 2025
3b0d6ee
Fix linting.
JoeZiminski Nov 18, 2025
d51d94e
Some tidy up and extend use cases for new encrpytion checker.
JoeZiminski Nov 19, 2025
4ed8a8a
Apply some fixes from self review 1.
JoeZiminski Nov 19, 2025
2c644a2
Rename to rclone_file_is_encrypted.
JoeZiminski Nov 19, 2025
31c80b0
Remove unecessary type hint ignore.
JoeZiminski Nov 20, 2025
ccb718b
Rename preliminary_setup_gdrive_config_for_without_browser.
JoeZiminski Nov 20, 2025
ee69564
Small tidy ups.
JoeZiminski Nov 20, 2025
1700651
Revert change to create_folders conditional.
JoeZiminski Nov 20, 2025
7cea970
Small refactor.
JoeZiminski Nov 20, 2025
2e3a710
Remove --ask-password.
JoeZiminski Nov 20, 2025
ccbad2a
Update datashuttle/utils/rclone_encryption.py
JoeZiminski Nov 20, 2025
94614fc
Update datashuttle/utils/rclone.py
JoeZiminski Nov 20, 2025
cec3c50
Improve a test docstring.
JoeZiminski Nov 20, 2025
1991857
Remove debugging clause.
JoeZiminski Nov 20, 2025
0f50c1b
Update datashuttle/configs/rclone_configs.py
JoeZiminski Nov 20, 2025
611efb2
Fix typo in test docstring: 'ecrypted' → 'encrypted' (#637)
Copilot Nov 20, 2025
ed1a89d
Update datashuttle/configs/rclone_configs.py
JoeZiminski Nov 20, 2025
f5f44da
Update datashuttle/utils/rclone_encryption.py
JoeZiminski Nov 20, 2025
b1a5032
Fix broken try / except.
JoeZiminski Nov 20, 2025
ebcd89d
Merge branch 'add_password_to_rclone_config_for_aws_gdrive' of github…
JoeZiminski Nov 20, 2025
06b4a93
Replace direct raise with utils function.
JoeZiminski Nov 20, 2025
6fdb357
Fixes to rclone_encryption.py
JoeZiminski Nov 20, 2025
e873b76
Small fixes rclone encryption.
JoeZiminski Nov 20, 2025
3d2cd66
Update datashuttle/datashuttle_class.py
JoeZiminski Nov 20, 2025
531eb02
Update datashuttle/configs/canonical_folders.py
JoeZiminski Nov 20, 2025
8f14c9e
Update datashuttle/datashuttle_class.py
JoeZiminski Nov 20, 2025
07bef3d
Update datashuttle/configs/rclone_configs.py
JoeZiminski Nov 20, 2025
e49b526
Fix broken datashuttle version check.
JoeZiminski Nov 20, 2025
4d5450e
Update datashuttle/utils/rclone_encryption.py
JoeZiminski Nov 20, 2025
34981f5
Update docs/source/pages/get_started/set-up-a-project.md
JoeZiminski Nov 20, 2025
33ce2d7
Update datashuttle/tui/screens/setup_aws.py
JoeZiminski Nov 20, 2025
bf84a44
Update datashuttle/tui/screens/setup_aws.py
JoeZiminski Nov 20, 2025
651c7dd
Merge branch 'add_password_to_rclone_config_for_aws_gdrive' of github…
JoeZiminski Nov 20, 2025
07a3c0e
Small fixes to sucess message.
JoeZiminski Nov 20, 2025
f42d7b2
Tidy up docs.
JoeZiminski Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/code_test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ jobs:
python -m pip install --upgrade pip
pip install .[dev]

- name: Install pass on Linux
# this is required for Rclone config encryption
if: runner.os == 'Linux'
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y pass gnupg git

# Create a dedicated GPG home for this job
export GNUPGHOME="$(mktemp -d)"
echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV" # <-- make it available to later steps

# Generate a non-interactive key (no passphrase), no expiry
gpg --batch --yes --pinentry-mode loopback --passphrase '' \
--quick-gen-key "CI Key <[email protected]>" default default 0

# Initialize pass with the key fingerprint (more robust than UID)
FPR="$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/ {print $10; exit}')"
pass init "$FPR"

# run SSH tests only on Linux because Windows and macOS
# are already run within a virtual container and so cannot
# run Linux containers because nested containerisation is disabled.
Expand Down Expand Up @@ -97,7 +117,6 @@ jobs:
run: |
pytest --ignore=tests/tests_transfers/ssh --ignore=tests/tests_transfers/gdrive --ignore=tests/tests_transfers/aws


build_sdist_wheels:
name: Build source distribution
needs: [test]
Expand Down
30 changes: 30 additions & 0 deletions datashuttle/configs/canonical_folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
if TYPE_CHECKING:
from datashuttle.utils.custom_types import TopLevelFolder

import platform

from datashuttle.configs import canonical_configs
from datashuttle.utils.folder_class import Folder

Expand Down Expand Up @@ -80,6 +82,11 @@ def get_datashuttle_path() -> Path:
return Path.home() / ".datashuttle"


def get_internal_datashuttle_from_path() -> Path:
"""Get a placeholder path for `validate_project_from_path()`."""
return get_datashuttle_path() / "_datashuttle_from_path"


def get_project_datashuttle_path(project_name: str) -> Tuple[Path, Path]:
"""Return the datashuttle config path for the project.

Expand All @@ -91,3 +98,26 @@ def get_project_datashuttle_path(project_name: str) -> Tuple[Path, Path]:
temp_logs_path = base_path / "temp_logs"

return base_path, temp_logs_path


def get_rclone_config_base_path() -> Path:
"""Return the path to the Rclone config file.

This is used for RClone config files for transfer targets (ssh, aws, gdrive).
This should match where RClone itself stores the config by default,
as described here: https://rclone.org/docs/#config-string

Because RClone's resolution process for where it stores its config files
is a little complex, in some rare cases the path returned below may not match
where RClone actually stores its configs. In such cases, local filesystem configs,
which are stored in the default `rclone.conf` file for backwards compatibility
reasons, and transfer configs, which are stored in their own file at the path
returned from this function, are stored in separate places. This is generally
not a significant issue.
"""
if platform.system() == "Windows":
appdata_path = Path().home() / "AppData" / "Roaming"
if appdata_path.is_dir():
return appdata_path / "rclone"

return Path().home() / ".config" / "rclone"
51 changes: 7 additions & 44 deletions datashuttle/configs/config_class.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Dict, Optional, Union, cast
from typing import TYPE_CHECKING, Dict, Union, cast

if TYPE_CHECKING:
from collections.abc import ItemsView, KeysView, ValuesView

from datashuttle.utils.custom_types import (
OverwriteExistingFiles,
TopLevelFolder,
)

Expand All @@ -20,6 +19,7 @@
canonical_configs,
canonical_folders,
load_configs,
rclone_configs,
)
from datashuttle.utils import folders, utils

Expand All @@ -37,6 +37,9 @@ def __init__(
) -> None:
"""Initialize the Configs class with project name, file path, and config dictionary.

This class also holds `RCloneConfigs` that manage the Rclone config files
used for transfer.

Parameters
----------
project_name
Expand Down Expand Up @@ -64,6 +67,8 @@ def __init__(
self.hostkeys_path: Path
self.project_metadata_path: Path

self.rclone = rclone_configs.RCloneConfigs(self, self.file_path.parent)

def setup_after_load(self) -> None:
"""Set up the config after loading it."""
load_configs.convert_str_and_pathlib_paths(self, "str_to_path")
Expand Down Expand Up @@ -249,48 +254,6 @@ def get_base_folder(

return base_folder

def get_rclone_config_name(
self, connection_method: Optional[str] = None
) -> str:
"""Generate the rclone configuration name for the central project.

These configs are created by datashuttle but managed and stored by rclone.
"""
if connection_method is None:
connection_method = self["connection_method"]

assert connection_method != "local_only", (
"This state assumes a central connection."
)

return f"central_{self.project_name}_{connection_method}"

def make_rclone_transfer_options(
self, overwrite_existing_files: OverwriteExistingFiles, dry_run: bool
) -> Dict:
"""Create a dictionary of rclone transfer options.

Originally these arguments were collected from configs, but now
they are passed via function arguments. The `show_transfer_progress`
and `dry_run` options are fixed here.
"""
allowed_overwrite = ["never", "always", "if_source_newer"]

if overwrite_existing_files not in allowed_overwrite:
utils.log_and_raise_error(
f"`overwrite_existing_files` not "
f"recognised, must be one of: "
f"{allowed_overwrite}",
ValueError,
)

return {
"overwrite_existing_files": overwrite_existing_files,
"show_transfer_progress": True,
"transfer_verbosity": "vv",
"dry_run": dry_run,
}

def init_paths(self) -> None:
"""Initialize paths used by datashuttle."""
self.project_metadata_path = self["local_path"] / ".datashuttle"
Expand Down
133 changes: 133 additions & 0 deletions datashuttle/configs/rclone_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Optional

if TYPE_CHECKING:
from pathlib import Path

from datashuttle.configs.configs_class import Configs

import yaml

from datashuttle.configs import canonical_folders
from datashuttle.utils import rclone_encryption


class RCloneConfigs:
"""Class to manage the RClone configuration file.

This is a file that RClone creates to hold all information about local and
central transfer targets. For example, the SSH RClone config holds the private key,
the GDrive rclone config holds the access token, etc.

In datashuttle, local filesystem configs uses the Rclone default configuration file,
that RClone manages, for backwards compatibility reasons. However, SSH, AWS and GDrive
configs are stored in separate config files (set using RClone's --config argument).
Then being separate means these files can be separately encrypted.

This class tracks the state on whether a RClone config is encrypted, as well
as provides the default names for the rclone conf (e.g. central_<project_name>_<connection_method>).

Parameters
----------
datashuttle_configs
Parent Configs class.

config_base_class
Path to the datashuttle configs folder where all configs for the project are stored.

"""

def __init__(self, datashuttle_configs: Configs, config_base_path: Path):
"""Construct the class."""
self.datashuttle_configs = datashuttle_configs
self.rclone_encryption_state_file_path = (
config_base_path / "rclone_ps_state.yaml"
)

def load_rclone_config_is_encrypted(self) -> dict:
"""Track whether the Rclone config file is encrypted.

This could be read directly from the RClone config file, but requires
a subprocess call which can be slow on Windows. As this function is
called a lot, we track this explicitly when a rclone config is
encrypted / unencrypted and store to disk between sessions.
"""
assert rclone_encryption.connection_method_requires_encryption(
self.datashuttle_configs["connection_method"]
)

if self.rclone_encryption_state_file_path.is_file():
with open(self.rclone_encryption_state_file_path, "r") as file:
rclone_config_is_encrypted = yaml.full_load(file)
else:
rclone_config_is_encrypted = {
"ssh": False,
"gdrive": False,
"aws": False,
}

with open(self.rclone_encryption_state_file_path, "w") as file:
yaml.dump(rclone_config_is_encrypted, file)

return rclone_config_is_encrypted

def set_rclone_config_encryption_state(self, value: bool) -> None:
"""Store the current state of the rclone config encryption for the `connection_method`.

Note that this is stored to disk each call (rather than tracked in memory)
to ensure it is updated properly if changed through the Python API
while the TUI is also running.
"""
assert rclone_encryption.connection_method_requires_encryption(
self.datashuttle_configs["connection_method"]
)

rclone_config_is_encrypted = self.load_rclone_config_is_encrypted()

rclone_config_is_encrypted[
self.datashuttle_configs["connection_method"]
] = value

with open(self.rclone_encryption_state_file_path, "w") as file:
yaml.dump(rclone_config_is_encrypted, file)

def rclone_file_is_encrypted(
self,
) -> bool:
"""Return whether the config file associated with the current `connection_method` is encrypted."""
assert rclone_encryption.connection_method_requires_encryption(
self.datashuttle_configs["connection_method"]
)

rclone_config_is_encrypted = self.load_rclone_config_is_encrypted()

return rclone_config_is_encrypted[
self.datashuttle_configs["connection_method"]
]

def get_rclone_config_name(
self, connection_method: Optional[str] = None
) -> str:
"""Generate the rclone configuration name for the central project."""
if connection_method is None:
connection_method = self.datashuttle_configs["connection_method"]

return f"central_{self.datashuttle_configs.project_name}_{connection_method}"

def get_rclone_central_connection_config_filepath(self) -> Path:
"""Return the full filepath to the rclone `.conf` config file."""
return (
canonical_folders.get_rclone_config_base_path()
/ f"{self.get_rclone_config_name()}.conf"
)

def delete_existing_rclone_config_file(self) -> None:
"""Delete the Rclone config file if it exists."""
rclone_config_filepath = (
self.get_rclone_central_connection_config_filepath()
)

if rclone_config_filepath.exists():
rclone_config_filepath.unlink()
self.set_rclone_config_encryption_state(False)
Loading
Loading