1- import re
1+ import json
22import subprocess
33from typing import Any , Callable , Dict , List , Optional , Tuple , Union
44from 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