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 |