11# Copyright (c) 2024 FileCloud. All Rights Reserved.
22import datetime
3+ import threading
34import logging
45import pathlib
56import re
@@ -49,6 +50,35 @@ def str_to_bool(value):
4950
5051log = logging .getLogger (__name__ )
5152
53+ class Progress :
54+ """
55+ Way to track progress of uploads/downloads.
56+
57+ Either use this object in another thread or
58+ override update() to get progress updates.
59+ """
60+
61+ def __init__ (self ) -> None :
62+ self ._completed_bytes = 0
63+ self ._total_bytes = 0
64+ self ._lock = threading .Lock ()
65+
66+ """
67+ Progress callback of uploads/downloads
68+ """
69+ def update (self , completed_bytes : int , total_bytes : int , chunk_complete : bool ) -> None :
70+ with self ._lock :
71+ self ._completed_bytes = completed_bytes
72+ self ._total_bytes = total_bytes
73+
74+ def completed_bytes (self ) -> int :
75+ with self ._lock :
76+ return self ._completed_bytes
77+
78+ def total_bytes (self ) -> int :
79+ with self ._lock :
80+ return self ._total_bytes
81+
5282
5383class FCServer :
5484 """
@@ -496,7 +526,8 @@ def waitforfileremoval(self, path: str, maxwaits: float = 30):
496526 raise TimeoutError (f"File { path } not removed after { maxwaits } seconds" )
497527
498528 def downloadfile_no_retry (
499- self , path : str , dstPath : Union [pathlib .Path , str ], redirect : bool = True
529+ self , path : str , dstPath : Union [pathlib .Path , str ], redirect : bool = True ,
530+ progress : Optional [Progress ] = None
500531 ) -> None :
501532 """
502533 Download file at 'path' to local 'dstPath'
@@ -511,23 +542,29 @@ def downloadfile_no_retry(
511542 stream = True ,
512543 ) as resp :
513544 resp .raise_for_status ()
545+ content_length = int (resp .headers .get ("Content-Length" , "-1" ))
546+ completed_bytes = 0
514547 with open (dstPath , "wb" ) as dstF :
515548 for chunk in resp .iter_content (128 * 1024 ):
549+ completed_bytes += len (chunk )
516550 dstF .write (chunk )
551+ if progress is not None :
552+ progress .update (completed_bytes , content_length , False )
517553
518554 def downloadfile (
519- self , path : str , dstPath : Union [pathlib .Path , str ], redirect : bool = True
555+ self , path : str , dstPath : Union [pathlib .Path , str ], redirect : bool = True ,
556+ progress : Optional [Progress ] = None
520557 ) -> None :
521558 """
522559 Download file at 'path' to local 'dstPath'. Retries.
523560 """
524561 if self .retries is None :
525- return self .downloadfile_no_retry (path , dstPath , redirect )
562+ return self .downloadfile_no_retry (path , dstPath , redirect , progress )
526563
527564 retries = self .retries
528565 while True :
529566 try :
530- self .downloadfile_no_retry (path , dstPath , redirect )
567+ self .downloadfile_no_retry (path , dstPath , redirect , progress )
531568 return
532569 except :
533570 retries = retries .increment ()
@@ -568,29 +605,32 @@ def upload_bytes(
568605 data : bytes ,
569606 serverpath : str ,
570607 datemodified : datetime .datetime = datetime .datetime .now (),
608+ progress : Optional [Progress ] = None
571609 ) -> None :
572610 """
573611 Upload bytes 'data' to server at 'serverpath'.
574612 """
575- self .upload (BufferedReader (BytesIO (data )), serverpath , datemodified ) # type: ignore
613+ self .upload (BufferedReader (BytesIO (data )), serverpath , datemodified , progress = progress ) # type: ignore
576614
577615 def upload_str (
578616 self ,
579617 data : str ,
580618 serverpath : str ,
581619 datemodified : datetime .datetime = datetime .datetime .now (),
620+ progress : Optional [Progress ] = None
582621 ) -> None :
583622 """
584623 Upload str 'data' UTF-8 encoded to server at 'serverpath'.
585624 """
586- self .upload_bytes (data .encode ("utf-8" ), serverpath , datemodified )
625+ self .upload_bytes (data .encode ("utf-8" ), serverpath , datemodified , progress = progress )
587626
588627 def upload_file (
589628 self ,
590629 localpath : pathlib .Path ,
591630 serverpath : str ,
592631 datemodified : datetime .datetime = datetime .datetime .now (),
593632 adminproxyuserid : Optional [str ] = None ,
633+ progress : Optional [Progress ] = None
594634 ) -> None :
595635 """
596636 Upload file at 'localpath' to server at 'serverpath'.
@@ -601,6 +641,7 @@ def upload_file(
601641 serverpath ,
602642 datemodified ,
603643 adminproxyuserid = adminproxyuserid ,
644+ progress = progress
604645 )
605646
606647 def _serverdatetime (self , dt : datetime .datetime ):
@@ -619,6 +660,7 @@ def upload(
619660 serverpath : str ,
620661 datemodified : datetime .datetime ,
621662 adminproxyuserid : Optional [str ] = None ,
663+ progress : Optional [Progress ] = None ,
622664 ) -> None :
623665 """
624666 Upload seekable stream at uploadf to server at 'serverpath'
@@ -681,6 +723,8 @@ def read(self, size=-1):
681723 size = min (size , max_read )
682724 data = super ().read (size )
683725 self .pos += len (data )
726+ if progress is not None :
727+ progress .update (self .pos , data_size , False )
684728 return data
685729
686730 def __len__ (self ) -> int :
@@ -811,6 +855,9 @@ def close(self):
811855
812856 pos += curr_slice_size
813857
858+ if progress is not None :
859+ progress .update (pos , data_size , True )
860+
814861 def share (self , path : str , adminproxyuserid : str = "" ) -> FCShare :
815862 """
816863 Share 'path'
0 commit comments