2 ## Licensed to the .NET Foundation under one or more agreements.
3 ## The .NET Foundation licenses this file to you under the MIT license.
4 ## See the LICENSE file in the project root for more information.
6 ## This file provides utility functions to the adjacent python scripts
8 from hashlib import sha256
9 from io import StringIO
15 class WrappedStringIO(StringIO):
16 """A wrapper around StringIO to allow writing str objects"""
18 if sys.version_info < (3, 0, 0):
19 if isinstance(s, str):
21 super(WrappedStringIO, self).write(s)
23 class UpdateFileWriter:
24 """A file-like context object which will only write to a file if the result would be different
27 filename (str): The name of the file to update
28 stream (WrappedStringIO): The file-like stream provided upon context enter
31 filename (str): Sets the filename attribute
35 def __init__(self, filename):
36 self.filename = filename
40 self.stream = WrappedStringIO()
43 def __exit__(self, exc_type, exc_value, traceback):
45 new_content = self.stream.getvalue()
50 with open(self.filename, 'r') as fstream:
51 cur_hash.update(fstream.read().encode('utf-8'))
57 new_hash.update(new_content.encode('utf-8'))
58 update = new_hash.digest() != cur_hash.digest()
63 with open(self.filename, 'w') as fstream:
64 fstream.write(new_content)
68 def open_for_update(filename):
69 return UpdateFileWriter(filename)
71 def split_entries(entries, directory):
72 """Given a list of entries in a directory, listing return a set of file and a set of dirs"""
73 files = set([entry for entry in entries if os.path.isfile(os.path.join(directory, entry))])
74 dirs = set([entry for entry in entries if os.path.isdir(os.path.join(directory, entry))])
78 def update_directory(srcpath, dstpath, recursive=True, destructive=True, shallow=False):
79 """Updates dest directory with files from src directory
82 destpath (str): The destination path to sync with the source
83 srcpath (str): The source path to sync to the destination
84 recursive(boolean): If True, descend into and update subdirectories (default: True)
85 destructive(boolean): If True, delete files in the destination which do not exist in the source (default: True)
86 shallow(boolean): If True, only use os.stat to diff files. Do not examine contents (default: False)
88 srcfiles, srcdirs = split_entries(os.listdir(srcpath), srcpath)
89 dstfiles, dstdirs = split_entries(os.listdir(dstpath), dstpath)
92 # Update files in both src and destination which are different in destination
93 commonfiles = srcfiles.intersection(dstfiles)
94 _, mismatches, errors = filecmp.cmpfiles(srcpath, dstpath, commonfiles, shallow=shallow)
97 raise RuntimeError("Comparison failed for the following files(s): {}".format(errors))
99 for mismatch in mismatches:
100 shutil.copyfile(os.path.join(srcpath, mismatch), os.path.join(dstpath, mismatch))
102 # Copy over files from source which do not exist in the destination
103 for missingfile in srcfiles.difference(dstfiles):
104 shutil.copyfile(os.path.join(srcpath, missingfile), os.path.join(dstpath, missingfile))
106 #If destructive, delete files in destination which do not exist in sourc
108 for deadfile in dstfiles.difference(srcfiles):
110 os.remove(os.path.join(dstpath, deadfile))
112 for deaddir in dstdirs.difference(srcdirs):
114 shutil.rmtree(os.path.join(dstpath, deaddir))
116 #If recursive, do this again for each source directory
118 for dirname in srcdirs:
119 dstdir, srcdir = os.path.join(dstpath, dirname), os.path.join(srcpath, dirname)
120 if not os.path.exists(dstdir):
122 update_directory(srcdir, dstdir, recursive, destructive, shallow)