| OLD | NEW |
| 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Creates a tree of hardlinks, symlinks or copy the inputs files.""" | 5 """File related utility functions. |
| 6 |
| 7 Creates a tree of hardlinks, symlinks or copy the inputs files. Calculate files |
| 8 hash. |
| 9 """ |
| 6 | 10 |
| 7 import ctypes | 11 import ctypes |
| 12 import hashlib |
| 8 import logging | 13 import logging |
| 9 import os | 14 import os |
| 10 import shutil | 15 import shutil |
| 16 import stat |
| 11 import sys | 17 import sys |
| 18 import time |
| 12 | 19 |
| 13 | 20 |
| 14 # Types of action accepted by recreate_tree(). | 21 # Types of action accepted by recreate_tree(). |
| 15 HARDLINK, SYMLINK, COPY = range(4)[1:] | 22 HARDLINK, SYMLINK, COPY = range(4)[1:] |
| 16 | 23 |
| 17 | 24 |
| 18 class MappingError(OSError): | 25 class MappingError(OSError): |
| 19 """Failed to recreate the tree.""" | 26 """Failed to recreate the tree.""" |
| 20 pass | 27 pass |
| 21 | 28 |
| 22 | 29 |
| 23 def os_link(source, link_name): | 30 def os_link(source, link_name): |
| 24 """Add support for os.link() on Windows.""" | 31 """Add support for os.link() on Windows.""" |
| 25 if sys.platform == 'win32': | 32 if sys.platform == 'win32': |
| 26 if not ctypes.windll.kernel32.CreateHardLinkW( | 33 if not ctypes.windll.kernel32.CreateHardLinkW( |
| 27 unicode(link_name), unicode(source), 0): | 34 unicode(link_name), unicode(source), 0): |
| 28 raise OSError() | 35 raise OSError() |
| 29 else: | 36 else: |
| 30 os.link(source, link_name) | 37 os.link(source, link_name) |
| 31 | 38 |
| 32 | 39 |
| 33 def preprocess_inputs(indir, infiles, blacklist): | 40 def expand_directories(indir, infiles, blacklist): |
| 34 """Reads the infiles and expands the directories and applies the blacklist. | 41 """Expands the directories, applies the blacklist and verifies files exist.""" |
| 35 | 42 logging.debug('expand_directories(%s, %s, %s)' % (indir, infiles, blacklist)) |
| 36 Returns the normalized indir and infiles. Converts infiles with a trailing | |
| 37 slash as the list of its files. | |
| 38 """ | |
| 39 logging.debug('preprocess_inputs(%s, %s, %s)' % (indir, infiles, blacklist)) | |
| 40 # Both need to be a local path. | |
| 41 indir = os.path.normpath(indir) | |
| 42 if not os.path.isdir(indir): | |
| 43 raise MappingError('%s is not a directory' % indir) | |
| 44 | |
| 45 # Do not call abspath until it was verified the directory exists. | |
| 46 indir = os.path.abspath(indir) | |
| 47 outfiles = [] | 43 outfiles = [] |
| 48 for relfile in infiles: | 44 for relfile in infiles: |
| 49 if os.path.isabs(relfile): | 45 if os.path.isabs(relfile): |
| 50 raise MappingError('Can\'t map absolute path %s' % relfile) | 46 raise MappingError('Can\'t map absolute path %s' % relfile) |
| 51 infile = os.path.normpath(os.path.join(indir, relfile)) | 47 infile = os.path.normpath(os.path.join(indir, relfile)) |
| 52 if not infile.startswith(indir): | 48 if not infile.startswith(indir): |
| 53 raise MappingError('Can\'t map file %s outside %s' % (infile, indir)) | 49 raise MappingError('Can\'t map file %s outside %s' % (infile, indir)) |
| 54 | 50 |
| 55 if relfile.endswith('/'): | 51 if relfile.endswith('/'): |
| 56 if not os.path.isdir(infile): | 52 if not os.path.isdir(infile): |
| 57 raise MappingError( | 53 raise MappingError( |
| 58 'Input directory %s must have a trailing slash' % infile) | 54 'Input directory %s must have a trailing slash' % infile) |
| 59 for dirpath, dirnames, filenames in os.walk(infile): | 55 for dirpath, dirnames, filenames in os.walk(infile): |
| 60 # Convert the absolute path to subdir + relative subdirectory. | 56 # Convert the absolute path to subdir + relative subdirectory. |
| 61 relpath = dirpath[len(indir)+1:] | 57 relpath = dirpath[len(indir)+1:] |
| 62 outfiles.extend(os.path.join(relpath, f) for f in filenames) | 58 outfiles.extend(os.path.join(relpath, f) for f in filenames) |
| 63 for index, dirname in enumerate(dirnames): | 59 for index, dirname in enumerate(dirnames): |
| 64 # Do not process blacklisted directories. | 60 # Do not process blacklisted directories. |
| 65 if blacklist(os.path.join(relpath, dirname)): | 61 if blacklist(os.path.join(relpath, dirname)): |
| 66 del dirnames[index] | 62 del dirnames[index] |
| 67 else: | 63 else: |
| 68 if not os.path.isfile(infile): | 64 if not os.path.isfile(infile): |
| 69 raise MappingError('Input file %s doesn\'t exist' % infile) | 65 raise MappingError('Input file %s doesn\'t exist' % infile) |
| 70 outfiles.append(relfile) | 66 outfiles.append(relfile) |
| 71 return outfiles, indir | 67 return outfiles |
| 72 | 68 |
| 73 | 69 |
| 74 def process_file(outfile, infile, action): | 70 def process_inputs(indir, infiles, need_hash, read_only): |
| 71 """Returns a dictionary of input files, populated with the files' mode and |
| 72 hash. |
| 73 |
| 74 The file mode is manipulated if read_only is True. In practice, we only save |
| 75 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). |
| 76 """ |
| 77 outdict = {} |
| 78 for infile in infiles: |
| 79 filepath = os.path.join(indir, infile) |
| 80 filemode = stat.S_IMODE(os.stat(filepath).st_mode) |
| 81 # Remove write access for non-owner. |
| 82 filemode &= ~(stat.S_IWGRP | stat.S_IWOTH) |
| 83 if read_only: |
| 84 filemode &= ~stat.S_IWUSR |
| 85 if filemode & stat.S_IXUSR: |
| 86 filemode |= (stat.S_IXGRP | stat.S_IXOTH) |
| 87 else: |
| 88 filemode &= ~(stat.S_IXGRP | stat.S_IXOTH) |
| 89 outdict[infile] = { |
| 90 'mode': filemode, |
| 91 } |
| 92 if need_hash: |
| 93 h = hashlib.sha1() |
| 94 with open(filepath, 'rb') as f: |
| 95 h.update(f.read()) |
| 96 outdict[infile]['sha-1'] = h.hexdigest() |
| 97 return outdict |
| 98 |
| 99 |
| 100 def link_file(outfile, infile, action): |
| 75 """Links a file. The type of link depends on |action|.""" | 101 """Links a file. The type of link depends on |action|.""" |
| 76 logging.debug('Mapping %s to %s' % (infile, outfile)) | 102 logging.debug('Mapping %s to %s' % (infile, outfile)) |
| 77 if os.path.isfile(outfile): | 103 if os.path.isfile(outfile): |
| 78 raise MappingError('%s already exist' % outfile) | 104 raise MappingError('%s already exist' % outfile) |
| 79 | 105 |
| 80 if action == COPY: | 106 if action == COPY: |
| 81 shutil.copy(infile, outfile) | 107 shutil.copy(infile, outfile) |
| 82 elif action == SYMLINK and sys.platform != 'win32': | 108 elif action == SYMLINK and sys.platform != 'win32': |
| 83 os.symlink(infile, outfile) | 109 os.symlink(infile, outfile) |
| 84 elif action == HARDLINK: | 110 elif action == HARDLINK: |
| 85 try: | 111 try: |
| 86 os_link(infile, outfile) | 112 os_link(infile, outfile) |
| 87 except OSError: | 113 except OSError: |
| 88 # Probably a different file system. | 114 # Probably a different file system. |
| 89 logging.warn( | 115 logging.warn( |
| 90 'Failed to hardlink, failing back to copy %s to %s' % ( | 116 'Failed to hardlink, failing back to copy %s to %s' % ( |
| 91 infile, outfile)) | 117 infile, outfile)) |
| 92 shutil.copy(infile, outfile) | 118 shutil.copy(infile, outfile) |
| 93 else: | 119 else: |
| 94 raise ValueError('Unknown mapping action %s' % action) | 120 raise ValueError('Unknown mapping action %s' % action) |
| 95 | 121 |
| 96 | 122 |
| 97 def recreate_tree(outdir, indir, infiles, action): | 123 def recreate_tree(outdir, indir, infiles, action): |
| 98 """Creates a new tree with only the input files in it. | 124 """Creates a new tree with only the input files in it. |
| 99 | 125 |
| 100 Arguments: | 126 Arguments: |
| 101 outdir: Temporary directory to create the files in. | 127 outdir: Output directory to create the files in. |
| 102 indir: Root directory the infiles are based in. | 128 indir: Root directory the infiles are based in. |
| 103 infiles: List of files to map from |indir| to |outdir|. Must have been | 129 infiles: List of files to map from |indir| to |outdir|. |
| 104 sanitized with preprocess_inputs(). | |
| 105 action: See assert below. | 130 action: See assert below. |
| 106 """ | 131 """ |
| 107 logging.debug( | 132 logging.debug( |
| 108 'recreate_tree(%s, %s, %s, %s)' % (outdir, indir, infiles, action)) | 133 'recreate_tree(%s, %s, %s, %s)' % (outdir, indir, infiles, action)) |
| 109 logging.info('Mapping from %s to %s' % (indir, outdir)) | 134 logging.info('Mapping from %s to %s' % (indir, outdir)) |
| 110 | 135 |
| 111 assert action in (HARDLINK, SYMLINK, COPY) | 136 assert action in (HARDLINK, SYMLINK, COPY) |
| 112 outdir = os.path.normpath(outdir) | 137 outdir = os.path.normpath(outdir) |
| 113 if not os.path.isdir(outdir): | 138 if not os.path.isdir(outdir): |
| 114 logging.info ('Creating %s' % outdir) | 139 logging.info ('Creating %s' % outdir) |
| 115 os.makedirs(outdir) | 140 os.makedirs(outdir) |
| 116 # Do not call abspath until the directory exists. | 141 # Do not call abspath until the directory exists. |
| 117 outdir = os.path.abspath(outdir) | 142 outdir = os.path.abspath(outdir) |
| 118 | 143 |
| 119 for relfile in infiles: | 144 for relfile in infiles: |
| 120 infile = os.path.join(indir, relfile) | 145 infile = os.path.join(indir, relfile) |
| 121 outfile = os.path.join(outdir, relfile) | 146 outfile = os.path.join(outdir, relfile) |
| 122 outsubdir = os.path.dirname(outfile) | 147 outsubdir = os.path.dirname(outfile) |
| 123 if not os.path.isdir(outsubdir): | 148 if not os.path.isdir(outsubdir): |
| 124 os.makedirs(outsubdir) | 149 os.makedirs(outsubdir) |
| 125 process_file(outfile, infile, action) | 150 link_file(outfile, infile, action) |
| 126 | 151 |
| 127 | 152 |
| 128 def _set_write_bit(path, read_only): | 153 def _set_write_bit(path, read_only): |
| 129 """Sets or resets the executable bit on a file or directory.""" | 154 """Sets or resets the executable bit on a file or directory.""" |
| 130 mode = os.stat(path).st_mode | 155 mode = os.stat(path).st_mode |
| 131 if read_only: | 156 if read_only: |
| 132 mode = mode & 0500 | 157 mode = mode & 0500 |
| 133 else: | 158 else: |
| 134 mode = mode | 0200 | 159 mode = mode | 0200 |
| 135 if hasattr(os, 'lchmod'): | 160 if hasattr(os, 'lchmod'): |
| 136 os.lchmod(path, mode) # pylint: disable=E1101 | 161 os.lchmod(path, mode) # pylint: disable=E1101 |
| 137 else: | 162 else: |
| 138 # TODO(maruel): Implement proper DACL modification on Windows. | 163 # TODO(maruel): Implement proper DACL modification on Windows. |
| 139 os.chmod(path, mode) | 164 os.chmod(path, mode) |
| 140 | 165 |
| 141 | 166 |
| 142 def make_writable(root, read_only): | 167 def make_writable(root, read_only): |
| 143 """Toggle the writable bit on a directory tree.""" | 168 """Toggle the writable bit on a directory tree.""" |
| 144 root = os.path.abspath(root) | 169 root = os.path.abspath(root) |
| 145 for dirpath, dirnames, filenames in os.walk(root, topdown=True): | 170 for dirpath, dirnames, filenames in os.walk(root, topdown=True): |
| 146 for filename in filenames: | 171 for filename in filenames: |
| 147 _set_write_bit(os.path.join(dirpath, filename), read_only) | 172 _set_write_bit(os.path.join(dirpath, filename), read_only) |
| 148 | 173 |
| 149 for dirname in dirnames: | 174 for dirname in dirnames: |
| 150 _set_write_bit(os.path.join(dirpath, dirname), read_only) | 175 _set_write_bit(os.path.join(dirpath, dirname), read_only) |
| 176 |
| 177 |
| 178 def rmtree(root): |
| 179 """Wrapper around shutil.rmtree() to retry automatically on Windows.""" |
| 180 if sys.platform == 'win32': |
| 181 for i in range(3): |
| 182 try: |
| 183 shutil.rmtree(root) |
| 184 break |
| 185 except WindowsError: # pylint: disable=E0602 |
| 186 delay = (i+1)*2 |
| 187 print >> sys.stderr, ( |
| 188 'The test has subprocess outliving it. Sleep %d seconds.' % delay) |
| 189 time.sleep(delay) |
| 190 else: |
| 191 shutil.rmtree(root) |
| OLD | NEW |