Skip to content

Commit 185b14e

Browse files
Use rclone --stats command for all transfer operations (#48)
* Use rclone --stats command for all transfer operations * Update version
1 parent 1525ab9 commit 185b14e

3 files changed

Lines changed: 64 additions & 87 deletions

File tree

rclone_python/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "0.1.15"
1+
VERSION = "0.1.16"

rclone_python/rclone.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,13 +650,14 @@ def _rclone_transfer_operation(
650650
# add global rclone flags
651651
if ignore_existing:
652652
command += " --ignore-existing"
653-
command += " --progress"
654653

655654
# in path
656655
command += f' "{in_path}"'
657656
# out path
658657
command += f' "{out_path}"'
659658

659+
command += " --stats 0.1s --stats-unit bytes --use-json-log -v"
660+
660661
# optional named arguments/flags
661662
command += utils.args2string(args)
662663

rclone_python/utils.py

Lines changed: 61 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import re
1+
import json
22
import subprocess
33
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
44
from rich.progress import Progress, TaskID, Task
@@ -56,31 +56,6 @@ def shorten_filepath(in_path: str, max_length: int) -> str:
5656
return in_path
5757

5858

59-
def convert2bits(value: float, unit: str) -> float:
60-
"""Returns the corresponding bit value to a value with a certain binary prefix (based on powers of 2) like KiB or MiB.
61-
62-
Args:
63-
value (float): Bit value using a certain binary prefix like KiB or MiB.
64-
unit (str): The binary prefix.
65-
66-
Returns:
67-
float: The corresponding bit value.
68-
"""
69-
exp = {
70-
"B": 0,
71-
"KiB": 1,
72-
"MiB": 2,
73-
"GiB": 3,
74-
"TiB": 4,
75-
"PiB": 5,
76-
"EiB": 6,
77-
"ZiB": 7,
78-
"YiB": 8,
79-
}
80-
81-
return value * 1024 ** exp[unit]
82-
83-
8459
# ---------------------------------------------------------------------------- #
8560
# Progressbar related functions #
8661
# ---------------------------------------------------------------------------- #
@@ -108,10 +83,12 @@ def rclone_progress(
10883
process = subprocess.Popen(
10984
command, stdout=subprocess.PIPE, stderr=stderr, shell=True
11085
)
111-
for line in iter(process.stdout.readline, b""):
112-
var = line.decode()
11386

114-
valid, update_dict = extract_rclone_progress(buffer)
87+
# rclone prints stats to stderr. each line is one update
88+
for line in iter(process.stderr.readline, b""):
89+
line = line.decode()
90+
91+
valid, update_dict = extract_rclone_progress(line)
11592

11693
if valid:
11794
if show_progress:
@@ -124,12 +101,6 @@ def rclone_progress(
124101
if debug:
125102
pbar.log(buffer)
126103

127-
# reset the buffer
128-
buffer = ""
129-
else:
130-
# buffer until we
131-
buffer += var
132-
133104
if show_progress:
134105
complete_task(total_progress_id, pbar)
135106
for _, task_id in subprocesses.items():
@@ -140,48 +111,48 @@ def rclone_progress(
140111
return process
141112

142113

143-
def extract_rclone_progress(buffer: str) -> Tuple[bool, Union[Dict[str, Any], None]]:
144-
# matcher that checks if the progress update block is completely buffered yet (defines start and stop)
145-
# it gets the sent bits, total bits, progress, transfer-speed and eta
146-
reg_transferred = re.findall(
147-
r"Transferred:\s+(\d+.\d+ \w+) \/ (\d+.\d+ \w+), (\d{1,3})%, (\d+.\d+ \w+\/\w+), ETA (\S+)",
148-
buffer,
149-
)
114+
def extract_rclone_progress(line: str) -> Tuple[bool, Union[Dict[str, Any], None]]:
115+
"""Extracts and returns the progress updates from the rclone transfer operation.
116+
The returned Dictionary includes the original rclone stats output inside of "rclone_output".
117+
All file sizes and speeds are give in bytes.
118+
119+
Args:
120+
line (str): One output line of the rclone transfer operation with the --use-json-log flag enabled.
121+
122+
Returns:
123+
Tuple[bool, Union[Dict[str, Any], None]]: The retrieved update Dictionary.
124+
"""
150125

151-
if reg_transferred: # transferred block is completely buffered
126+
try:
127+
stats: Dict = json.loads(line).get("stats", None)
128+
except ValueError:
129+
stats = None
130+
131+
if stats is not None:
152132
# get the progress of the individual files
153-
# matcher gets the currently transferring files and their individual progress
154-
# returns list of tuples: (name, progress, file_size, unit)
155-
prog_transferring = []
156-
prog_regex = re.findall(
157-
r"\* +(.+):[ ]+(\d{1,3})% \/(\d+.\d+)([a-zA-Z]+),", buffer
158-
)
159-
for item in prog_regex:
160-
prog_transferring.append(
161-
(
162-
item[0],
163-
int(item[1]),
164-
float(item[2]),
165-
# the suffix B of the unit is missing for subprocesses
166-
item[3] + "B",
167-
)
133+
tasks = []
134+
for t in stats.get("transferring", []):
135+
tasks.append(
136+
{
137+
"name": t["name"],
138+
"total": t["size"],
139+
"sent": t["bytes"],
140+
"progress": t["percentage"],
141+
}
168142
)
169143

170-
out = {"prog_transferring": prog_transferring}
171-
sent_bits, total_bits, progress, transfer_speed_str, eta = reg_transferred[0]
172-
out["progress"] = float(progress.strip())
173-
out["total_bits"] = float(re.findall(r"\d+.\d+", total_bits)[0])
174-
out["sent_bits"] = float(re.findall(r"\d+.\d+", sent_bits)[0])
175-
out["unit_sent"] = re.findall(r"[a-zA-Z]+", sent_bits)[0]
176-
out["unit_total"] = re.findall(r"[a-zA-Z]+", total_bits)[0]
177-
out["transfer_speed"] = float(re.findall(r"\d+.\d+", transfer_speed_str)[0])
178-
out["transfer_speed_unit"] = re.findall(
179-
r"[a-zA-Z]+/[a-zA-Z]+", transfer_speed_str
180-
)[0]
181-
out["eta"] = eta
144+
out = {
145+
"tasks": tasks,
146+
"total": stats["totalBytes"],
147+
"sent": stats["bytes"],
148+
"progress": (
149+
stats["bytes"] / stats["totalBytes"] if stats["totalBytes"] != 0 else 0
150+
),
151+
"transfer_speed": stats["speed"],
152+
"rclone_output": stats,
153+
}
182154

183155
return True, out
184-
185156
else:
186157
return False, None
187158

@@ -251,35 +222,40 @@ def update_tasks(
251222

252223
pbar.update(
253224
total_progress,
254-
completed=convert2bits(update_dict["sent_bits"], update_dict["unit_sent"]),
255-
total=convert2bits(update_dict["total_bits"], update_dict["unit_total"]),
225+
completed=update_dict["sent"],
226+
total=update_dict["total"],
256227
)
257228

258-
sp_names = set()
259-
for sp_file_name, sp_progress, sp_size, sp_unit in update_dict["prog_transferring"]:
229+
task_names = set()
230+
for task in update_dict["tasks"]:
260231
task_id = None
261-
sp_names.add(sp_file_name)
262232

263-
if sp_file_name not in subprocesses:
233+
task_name = task["name"]
234+
task_size = task["total"]
235+
task_progress = task["progress"]
236+
237+
task_names.add(task_name)
238+
239+
if task_name not in subprocesses:
264240
task_id = pbar.add_task(" ", visible=False)
265-
subprocesses[sp_file_name] = task_id
241+
subprocesses[task_name] = task_id
266242
else:
267-
task_id = subprocesses[sp_file_name]
243+
task_id = subprocesses[task_name]
268244

269245
pbar.update(
270246
task_id,
271247
# set the description every time to reset the '├'
272-
description=f" ├─{sp_file_name}",
273-
completed=convert2bits(sp_size, sp_unit) * sp_progress / 100.0,
274-
total=convert2bits(sp_size, sp_unit),
248+
description=f" ├─{task_name}",
249+
completed=task_size * task_progress / 100.0,
250+
total=task_size,
275251
# hide subprocesses if we only upload a single file
276252
visible=len(subprocesses) > 1,
277253
)
278254

279255
# make all processes invisible that are no longer provided by rclone (bc. their upload completed)
280-
missing = list(sorted(subprocesses.keys() - sp_names))
281-
for missing_sp_id in missing:
282-
pbar.update(subprocesses[missing_sp_id], visible=False)
256+
missing = list(sorted(subprocesses.keys() - task_names))
257+
for missing_task_id in missing:
258+
pbar.update(subprocesses[missing_task_id], visible=False)
283259

284260
# change symbol for the last visible process
285261
for task in reversed(pbar.tasks):

0 commit comments

Comments
 (0)