PK!Xfile_replicator/__init__.py__version__ = "0.1.4" PK! file_replicator/cli.pyimport os.path import click from .lib import make_file_replicator, replicate_all_files, replicate_files_on_change import file_replicator @click.command() @click.argument("src_dir") @click.argument("dest_parent_dir") @click.argument("connection_command", nargs=-1) @click.option( "--clean-out-first", is_flag=True, default=False, help="Optionally start by cleaning out the destination directory.", ) @click.option( "--with-initial-replication / --no-initial-replication", default=True, help="Perform (or not) an initial replication of all files.", ) @click.option( "--replicate-on-change / --no-replicate-on-change", default=True, help="Perform (or not) a wait-for-change-and-replicate cycle.", ) @click.version_option(version=file_replicator.__version__) def main( src_dir, dest_parent_dir, connection_command, clean_out_first, with_initial_replication, replicate_on_change, ): """Replicate files to another computer e.g. for remote development. SRC_DIR is the source directory on this machine. DEST_PARENT_DIR is the (absolute) destination parent directory on the remote machine accessed using the CONNECTION_COMMAND. The CONNECTION_COMMAND must result in a running instance of bash ready to receive commands on stdin. Example CONNECTION_COMMANDS include: ssh some.host.com bash docker exec -i my_container bash docker-compose exec -T my_container bash So a full use of the tool might look like: file-replicator my_code_dir /home/code -- docker exec -i a_container bash (the use of "--" prevents any further processing of command line arguments by file-replicator, leaving them all for docker) Initially, all files and required directories are recursively copied. Then it waits for changes before copying each modified or new file. This can be modified with the switches. Note that empty directories are not replicated until they contain a file. Lastly, the only time the tool deletes files or directories is if called with the optional --clean-out-first switch. """ if not connection_command: raise click.UsageError( "Please provide a connection command to access the destination server." ) if not os.path.isabs(dest_parent_dir): raise click.UsageError( "The destination parent directory must be an absolute path." ) if not os.path.exists(src_dir) or not os.path.isdir(src_dir): raise click.UsageError("The source destination must exist and be a directory.") if clean_out_first: click.secho( "Clearing out all destination files first!", fg="green", bold="true" ) with make_file_replicator( src_dir, dest_parent_dir, connection_command, clean_out_first=clean_out_first ) as copy_file: if with_initial_replication: replicate_all_files(src_dir, copy_file) if replicate_on_change: while replicate_files_on_change(src_dir, copy_file): click.secho( "Restarting watchers after detecting a new directory. Consider restarting!", fg="red", bold="true", ) PK!$r>  file_replicator/lib.pyimport contextlib import os.path import shutil import subprocess import time import inotify.adapters __all__ = ["make_file_replicator", "replicate_all_files", "replicate_files_on_change"] # Small receiver code (written in bash for minimum dependencies) which repeatadly reads # tar files from stdin and extracts them. # Note that this requires the full tar command, not the busybox "lightweight" version. RECEIVER_CODE = """ set -e if {clean_out_first}; then rm -rf {dest_dir}/* fi mkdir -p {dest_dir} cd {dest_dir} while true; do tar --no-same-owner --extract --verbose done """ @contextlib.contextmanager def make_file_replicator( src_dir, dest_parent_dir, bash_connection_command, clean_out_first=False ): """Yield a copy_file() function for replicating files over a "bash connection". The must be in the given . The final path in the becomes the destination directory in the . The must be a list. """ src_dir = os.path.abspath(src_dir) dest_parent_dir = os.path.abspath(dest_parent_dir) dest_dir = os.path.join(dest_parent_dir, os.path.basename(src_dir)) p = subprocess.Popen(bash_connection_command, stdin=subprocess.PIPE) # Get the remote end up and running waiting for tar files. receiver_code = RECEIVER_CODE.format( dest_dir=dest_dir, clean_out_first=str(clean_out_first).lower() ) p.stdin.write(receiver_code.encode()) p.stdin.flush() def copy_file(src_filename): src_filename = os.path.abspath(src_filename) assert src_filename.startswith(src_dir), src_filename assert os.path.isfile(src_filename) rel_src_filename = os.path.join(src_filename[(1 + len(src_dir)) :]) subprocess.run( ["tar", "--create", rel_src_filename, "--to-stdout"], cwd=src_dir, check=True, stdout=p.stdin, ) p.stdin.flush() try: yield copy_file finally: p.stdin.close() p.wait() def replicate_all_files(src_dir, copy_file): """Walk src_dir to copy all files using copy_file().""" for root, dirnames, filenames in os.walk(src_dir): for filename in filenames: copy_file(os.path.join(root, filename)) def replicate_files_on_change(src_dir, copy_file, timeout=None): """Wait for changes to files in src_dir and copy with copy_file(). If provided, the timeout indicates when to return after that many seconds of no change. This is an imperfect solution because there are seemingly unavoidable race conditions when watching for file changes or additions and new directories are involved. Returns True to indicate that new directories have been added and the function should be called again. Otherwise returns False. """ please_call_me_again = False i = inotify.adapters.InotifyTree(src_dir) for event in i.event_gen(yield_nones=False, timeout_s=timeout): (_, type_names, path, filename) = event # For debugging... # print( # "PATH=[{}] FILENAME=[{}] EVENT_TYPES={}".format(path, filename, type_names) # ) if "IN_CLOSE_WRITE" in type_names: copy_file(os.path.join(path, filename)) if "IN_CREATE" in type_names and "IN_ISDIR" in type_names: # Race condition danger because a new directory was created (see warning on https://pypi.org/project/inotify). # Wait a short while for things to settle, then replicate the new directory, then start the watchers again. time.sleep(0.5) new_directory = os.path.join(path, filename) replicate_all_files(new_directory, copy_file) please_call_me_again = True break return please_call_me_again PK!HmI3<0file_replicator-0.1.4.dist-info/entry_points.txtN+I/N.,()JI-J-LN,/|L<..PK!kBړ66'file_replicator-0.1.4.dist-info/LICENSEMIT License Copyright (c) 2019 Timothy Corbett-Clark Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HnHTU%file_replicator-0.1.4.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HӘr (file_replicator-0.1.4.dist-info/METADATAYQs6~ssvG$Mrs۝z۽{t$$$@+dٱ>2&]~,鿕u:;Le&bKZՔ:d9u[UvqEp!kWʊTMXaUe T{URO~1J=yIvx&9j؉Xıs}z\J{d<$%}ë*RmU. ǷAuV0D\\^ \Ztdl!7V>{WlD;?9r'~>;!;ߕH^t$s!;ސrՍGcS{$+ū/~f]'wg$}`*]I\l}Y;Չ֩4Hfl)s:RO0š RADk*QDZ%T=~ Cp]/0f;vR˶uMI1CZÉjT]:$O Ps#;$"&ȽT2_bGs]jy1nկD8[#BB3qa/h}еhR܇* u[͗>ٚ#M )N˲L _VvVvbU|BqmP[ue[wڋw(! !@U0"фRىRN%4Ÿm BjC5tvr3u+DP(;#dWgsB5eI"{δ6Wg]5 BN~J M$:0{lt|5r&r ûSl^j꽏DGDдB_!Sb0lGi˖,P, pkP#VE"LP1GeEr 3FX% 5b`5xG\(+Yyk'IrŴQsXpY4VGE (X*kEegU05L^gjdIzh/_5A28jmƒk~g6k#^z#ǫ\B_UDlK\J/j<)$DhGMD,+ i Q)Yء y7Ll1s"}ͅb.qð#ޱ+ebAs}Z94,7ܓ7b| z@s&̆(&DͻF*E4o3@Tdwq}u<=9'7ˣz__G'gY52=߿3͔V߮u -.I~OΝ)a}hVE];Q9 lq,Gn,Q'pmt@30C*YPs{iV=/BMR$-lIDP/*U7 1O˦dš2ZߡJ/W5e eV_)2(+lfU˯3LUx"Y.rCbh;%"6$λ]'GT~ abP#g(H Cvs`ԪuuT%РJ(*Sqn& ~% ~y@= p{Izo+$Ryc*2_Ry5L2AbĞY"Q C88MsN1kܡP\6z' q[e~(RkۿFc׆5a35P#[w)ll\#Uod) eBIe:D\(-uHD@E#76f˜.5'1PĜbk8N.=~^[Sz/s -PhEe9Ӂ~L{O09Y)CO=dw%ɷ4hGm-UM0Iaц}Ы=hB;DS^yfc_5ƭbe} é^I!50Oy4BL&NZͻrí94WSt@)&|@N0 Dv-=^BOg9Gmt2L9wdo8pTH:>&p4Ĥ1QSCokfB]V*ݳ:yسȿ"'ځF2+ppC x/$фSʳ+>ҦIBE/ud#t8}dft/r٥ه쀖۷,ԸƠ!*;m:Xu qr@Dd)oϟ̴hҵw4sqx@05f˥ÉɄxnayadF qyt}}zیoVVS+ji}÷h%S*i[IP8 5`VyVV'i8l)o/hr BnŻZ`kb #[jgTߊ/kr1=<C|YoDQݣ[ ҉{ kU%M\bJuFrcGF88gxoÕ?.{+7s@K6l@3%ipY~5/qP4kcURɭq, AW{d#?g../A]@~꯻U72=!*g$L78^Ot͖4Mex{Jm."%>Dg{~ȮNN>fU9&t?s&PK!H6%&file_replicator-0.1.4.dist-info/RECORDr@}Wih# csm4O?+R/NIM@pes &՟^k⮭kC)X 9EN__bImՇZ1ٌxEN%v-;+8Ew:͏|vCE6㺗#s$ M _nlq7Saf#栾"r\7< F-5;ۇvWKE4ߚ C_'u<þ- (-U'oLkb>9g>':x'6B'-_46[ Set&S~3 K\r@{j;(GvȕTe^; .%^dXa:ڨ6tx>>@XPK!Xfile_replicator/__init__.pyPK! Ofile_replicator/cli.pyPK!$r>  Z file_replicator/lib.pyPK!HmI3<0file_replicator-0.1.4.dist-info/entry_points.txtPK!kBړ66'file_replicator-0.1.4.dist-info/LICENSEPK!HnHTU%!file_replicator-0.1.4.dist-info/WHEELPK!HӘr (,"file_replicator-0.1.4.dist-info/METADATAPK!H6%&q-file_replicator-0.1.4.dist-info/RECORDPKB/