#!/usr/bin/python3 '''Recreate a valid block stream, and give progress updates''' # pylint: disable=wrong-import-position import os import re import sys import time import select import signal sys.path.append('/usr/local/lib') import modunits # nopep8 signal.signal(signal.SIGPIPE, signal.SIG_DFL) def usage(retval): '''Give a usage message''' if retval: write = sys.stderr.write else: write = sys.stdout.write write('usage: {} [-v] [-e size_in_bytes] [-b blocksize_in_bytes] [-t timeout_in_seconds] [-p]\n'.format(sys.argv[0])) write(' -v means verbose\n') write(' -e provides a size estimate, for when we cannot stat the file length (EG in a pipe)\n') write(' -b provides a block size in bytes\n') write(' -t provides a timeout duration in seconds\n') write(' -p says to pad the final block with nulls\n') write('\n') write(' size_in_bytes defaults to {}\n'.format(State.estimate_in_bytes)) write(' blocksize defaults to {}\n'.format(State.blocksize)) write(' timeout defaults to {}\n'.format(State.timeout)) sys.exit(retval) def percent(numerator, denominator): '''Compute a percentage as a string for eye-pleasing''' quotient = (numerator * 100.0) / denominator str_quotient = str(quotient) match_obj = re.match(r'^([0-9]+\.[0-9]).*$', str_quotient) return match_obj.group(1) def compute_eta_and_bps(size_now, final_size, initial_time, time_now): ''' Compute the Estimated Time of Arrival for the transfer: IOW, when the transfer is (was) expected to complete. ''' delta_time_now = time_now - initial_time if size_now <= 0.0: abs_secs = 'NaN' else: delta_seconds_at_completion = (final_size * delta_time_now) / size_now absolute_seconds_at_completion = initial_time + delta_seconds_at_completion abs_secs = absolute_seconds_at_completion if delta_time_now <= 0: bps = 'NaN' else: bytes_per_second = size_now / (time_now - initial_time) bps = modunits.modunits( 'computer-bits-per-second-si', bytes_per_second * 8, fractional_part_length=1, units='abbreviated', ) return time.ctime(abs_secs), bps class State(object): '''Hold transfer state, including command line option results''' verbose = False estimate_in_bytes = -1 blocksize = 2 ** 18 timeout = 180 pad = False stdin = sys.stdin.fileno() stdout = sys.stdout.fileno() def __init__(self, argv): while argv[1:]: if argv[1] == '-v': State.verbose = True elif argv[1] == '-p': State.pad = True elif argv[1] == '-e': State.estimate_in_bytes = int(argv[2]) del argv[2] elif argv[1] == '-b': State.blocksize = int(argv[2]) del argv[2] elif argv[1] == '-t': State.timeout = float(argv[2]) del argv[2] elif argv[1] in ['-h', '--help']: usage(0) else: sys.stderr.write('Unrecognized option: {}\n'.format(argv[1])) usage(1) del argv[1] self.file_size = -1 self.major_buffer = b'' self.minor_buffer = b'' def set_file_size(self): '''Decide the size of the file - in bytes''' try: self.file_size = os.fstat(State.stdin).st_size except OSError: self.file_size = self.estimate_in_bytes else: if self.file_size == 0: self.file_size = self.estimate_in_bytes assert self.file_size >= 0, "Was unable to find length of file via fstat or -e" @classmethod def input_on_stdin(cls): '''Return True if there is input on stdin, False if there is a timeout''' select_result = select.select([cls.stdin], [], [], cls.timeout) assert not select_result[1] assert not select_result[2] if not select_result[0]: # This is a timeout, return False return False # This is a stdin-ready condition, return True return True def run(self): '''Copy blocks from State.stdin to State.stdout''' blockno = 0 size_so_far = 0 initial_time = time.time() while True: if self.input_on_stdin(): self.minor_buffer = os.read(State.stdin, State.blocksize) if not self.minor_buffer: # EOF, write final block and exit program self.finish() sys.exit(0) size_so_far += len(self.minor_buffer) eta_time, bps = compute_eta_and_bps(size_so_far, self.file_size, initial_time, time.time()) sys.stderr.write('blockno {}: {}% of {}, ETA: {}, Rate: {}\n'.format( blockno, percent(size_so_far, self.file_size), modunits.modunits('computer-size-si', self.file_size), eta_time, bps, )) self.major_buffer += self.minor_buffer if len(self.major_buffer) >= State.blocksize: portion_to_write = self.major_buffer[:State.blocksize] self.major_buffer = self.major_buffer[State.blocksize:] os.write(State.stdout, portion_to_write) blockno += 1 else: # We hit a timeout, finish and exit shell-false sys.stderr.write('{}: timeout!\n'.format(sys.argv[0])) self.finish() sys.exit(1) def finish(self): '''Write the last block, possibly padded to a full block length''' if self.pad: if self.major_buffer: if self.verbose: sys.stderr.write('Padding final block\n') final_long_block = self.major_buffer + b'\0' * State.blocksize final_block = final_long_block[:State.blocksize] os.write(State.stdout, final_block) else: # major_buffer is empty, nothing to do if self.verbose: sys.stderr.write('Final block was whole\n') else: if self.verbose: sys.stderr.write('Final block not being padded by request of user\n') os.write(State.stdout, self.major_buffer) def main(): '''Main function''' state = State(sys.argv) state.set_file_size() state.run() main()