Skip to content

Commit 9fb0601

Browse files
author
shrey
committed
add: call rclone with popen; refactor: google drive connection to use popen
1 parent 8bfa77d commit 9fb0601

5 files changed

Lines changed: 98 additions & 35 deletions

File tree

datashuttle/configs/aws_regions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
# AWS regions
55
# -----------------------------------------------------------------------------
66

7-
# These function are used for type checking and providing intellisense to the developer
8-
97

108
def get_aws_regions() -> Dict[str, str]:
119
"""Return a dict of available AWS S3 bucket regions."""

datashuttle/datashuttle_class.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
)
1919

2020
if TYPE_CHECKING:
21+
import subprocess
22+
2123
from datashuttle.utils.custom_types import (
2224
DisplayMode,
2325
OverwriteExistingFiles,
@@ -853,26 +855,27 @@ def setup_google_drive_connection(self) -> None:
853855
local_vars=locals(),
854856
)
855857

856-
if self.cfg["gdrive_client_id"]:
857-
gdrive_client_secret = gdrive.get_client_secret()
858-
else:
859-
gdrive_client_secret = None
860-
861858
browser_available = gdrive.ask_user_for_browser(log=True)
862859

863-
if not browser_available:
860+
service_account_filepath = None
861+
gdrive_client_secret = None
862+
863+
if browser_available and self.cfg["gdrive_client_id"]:
864+
gdrive_client_secret = gdrive.get_client_secret()
865+
866+
elif not browser_available:
864867
service_account_filepath = (
865868
gdrive.prompt_and_get_service_account_filepath(
866869
log=True,
867870
)
868871
)
869-
else:
870-
service_account_filepath = None
871872

872-
self._setup_rclone_gdrive_config(
873-
gdrive_client_secret, service_account_filepath, log=True
873+
process = self._setup_rclone_gdrive_config(
874+
gdrive_client_secret, service_account_filepath
874875
)
875876

877+
rclone.await_call_rclone_with_popen_raise_on_fail(process, log=True)
878+
876879
rclone.check_successful_connection_and_raise_error_on_fail(self.cfg)
877880

878881
utils.log_and_message("Google Drive Connection Successful.")
@@ -1525,14 +1528,12 @@ def _setup_rclone_gdrive_config(
15251528
self,
15261529
gdrive_client_secret: str | None,
15271530
service_account_filepath: str | None,
1528-
log: bool,
1529-
) -> None:
1530-
rclone.setup_rclone_config_for_gdrive(
1531+
) -> subprocess.Popen:
1532+
return rclone.setup_rclone_config_for_gdrive(
15311533
self.cfg,
15321534
self.cfg.get_rclone_config_name("gdrive"),
15331535
gdrive_client_secret,
15341536
service_account_filepath,
1535-
log=log,
15361537
)
15371538

15381539
def _setup_rclone_aws_config(

datashuttle/tui/interface.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
from typing import TYPE_CHECKING, Any, Dict, List, Optional
55

66
if TYPE_CHECKING:
7+
import subprocess
8+
79
import paramiko
810

911
from datashuttle.configs.config_class import Configs
1012
from datashuttle.utils.custom_types import InterfaceOutput, TopLevelFolder
1113

1214
from datashuttle import DataShuttle
1315
from datashuttle.configs import load_configs
14-
from datashuttle.utils import aws, rclone, ssh
16+
from datashuttle.utils import aws, rclone, ssh, utils
1517

1618

1719
class Interface:
@@ -36,6 +38,9 @@ def __init__(self) -> None:
3638
self.name_templates: Dict = {}
3739
self.tui_settings: Dict = {}
3840

41+
self.google_drive_rclone_setup_process: subprocess.Popen | None = None
42+
self.gdrive_setup_process_killed: bool = False
43+
3944
def select_existing_project(self, project_name: str) -> InterfaceOutput:
4045
"""Load an existing project into `self.project`.
4146
@@ -510,16 +515,46 @@ def setup_google_drive_connection(
510515
) -> InterfaceOutput:
511516
"""Try to set up and validate connection to Google Drive."""
512517
try:
513-
self.project._setup_rclone_gdrive_config(
514-
gdrive_client_secret, service_account_filepath, log=False
518+
process = self.project._setup_rclone_gdrive_config(
519+
gdrive_client_secret, service_account_filepath
515520
)
516-
rclone.check_successful_connection_and_raise_error_on_fail(
517-
self.project.cfg
521+
self.google_drive_rclone_setup_process = process
522+
self.gdrive_setup_process_killed = False
523+
524+
self.await_successful_gdrive_connection_setup_raise_on_fail(
525+
process
518526
)
527+
519528
return True, None
520529
except BaseException as e:
521530
return False, str(e)
522531

532+
def terminate_google_drive_setup(self) -> None:
533+
"""Terminate rclone setup for google drive."""
534+
assert self.google_drive_rclone_setup_process is not None
535+
536+
process = self.google_drive_rclone_setup_process
537+
538+
if process.poll() is not None:
539+
self.gdrive_setup_process_killed = True
540+
process.kill()
541+
542+
def await_successful_gdrive_connection_setup_raise_on_fail(
543+
self, process: subprocess.Popen
544+
):
545+
"""Wait for rclone setup for google drive to finish and verify successful connection."""
546+
stdout, stderr = process.communicate()
547+
548+
if not self.gdrive_setup_process_killed:
549+
if process.returncode != 0:
550+
utils.log_and_raise_error(
551+
stderr.decode("utf-8"), ConnectionError
552+
)
553+
554+
rclone.check_successful_connection_and_raise_error_on_fail(
555+
self.project.cfg
556+
)
557+
523558
# Setup AWS
524559
# ----------------------------------------------------------------------------------
525560

datashuttle/tui/screens/setup_gdrive.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
8989
):
9090
if self.setup_worker and self.setup_worker.is_running:
9191
self.setup_worker.cancel() # fix
92+
self.interface.terminate_google_drive_setup()
9293
self.dismiss()
9394

9495
elif event.button.id == "setup_gdrive_ok_button":
@@ -197,6 +198,9 @@ def setup_gdrive_connection_using_service_account(
197198
self, service_account_filepath: Optional[str] = None
198199
) -> None:
199200
"""Set up the Google Drive connection using service account and show success message."""
201+
message = "Setting up connection."
202+
self.update_message_box_message(message)
203+
200204
asyncio.create_task(
201205
self.setup_gdrive_connection_and_update_ui(
202206
service_account_filepath=service_account_filepath

datashuttle/utils/rclone.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import os
1212
import platform
13+
import shlex
1314
import subprocess
1415
import tempfile
1516
from subprocess import CompletedProcess
@@ -94,6 +95,35 @@ def call_rclone_through_script(command: str) -> CompletedProcess:
9495
return output
9596

9697

98+
def call_rclone_with_popen(command: str) -> subprocess.Popen:
99+
"""Call rclone using `subprocess.Popen` for control over process termination.
100+
101+
It is not possible to kill a process while running it using `subprocess.run`.
102+
"""
103+
command = "rclone " + command
104+
process = subprocess.Popen(
105+
shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE
106+
)
107+
return process
108+
109+
110+
def await_call_rclone_with_popen_raise_on_fail(
111+
process: subprocess.Popen, log: bool = True
112+
):
113+
"""Await rclone the subprocess.Popen call.
114+
115+
Calling `process.communicate()` waits for the process to complete and returns
116+
the stdout and stderr.
117+
"""
118+
stdout, stderr = process.communicate()
119+
120+
if process.returncode != 0:
121+
utils.log_and_raise_error(stderr.decode("utf-8"), ConnectionError)
122+
123+
if log:
124+
log_rclone_config_output()
125+
126+
97127
# -----------------------------------------------------------------------------
98128
# Setup
99129
# -----------------------------------------------------------------------------
@@ -182,10 +212,15 @@ def setup_rclone_config_for_gdrive(
182212
rclone_config_name: str,
183213
gdrive_client_secret: str | None,
184214
service_account_filepath: Optional[str] = None,
185-
log: bool = True,
186-
):
215+
) -> subprocess.Popen:
187216
"""Set up rclone config for connections to Google Drive.
188217
218+
This function uses `call_rclone_with_popen` instead of `call_rclone`. This
219+
is done to have more control over the setup process in case the user wishes to
220+
cancel the setup. Since the rclone setup for google drive uses a local web server
221+
for authentication to google drive, the running process must be killed before the
222+
setup can be started again.
223+
189224
Parameters
190225
----------
191226
cfg
@@ -203,9 +238,6 @@ def setup_rclone_config_for_gdrive(
203238
service_account_filepath : path to service account file path for connection
204239
without browser
205240
206-
log
207-
Whether to log, if `True` logger must already be initialised.
208-
209241
"""
210242
client_id_key_value = (
211243
f"client_id {cfg['gdrive_client_id']} "
@@ -224,7 +256,7 @@ def setup_rclone_config_for_gdrive(
224256
else f"service_account_file {service_account_filepath}"
225257
)
226258

227-
output = call_rclone(
259+
process = call_rclone_with_popen(
228260
f"config create "
229261
f"{rclone_config_name} "
230262
f"drive "
@@ -233,16 +265,9 @@ def setup_rclone_config_for_gdrive(
233265
f"scope drive "
234266
f"root_folder_id {cfg['gdrive_root_folder_id']} "
235267
f"{service_account_filepath_arg}",
236-
pipe_std=True,
237268
)
238269

239-
if output.returncode != 0:
240-
utils.log_and_raise_error(
241-
output.stderr.decode("utf-8"), ConnectionError
242-
)
243-
244-
if log:
245-
log_rclone_config_output()
270+
return process
246271

247272

248273
def setup_rclone_config_for_aws(

0 commit comments

Comments
 (0)