| # Copyright 2015 The Bazel Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Archive manipulation library for the Docker rules.""" |
| |
| import os |
| import tarfile |
| |
| # Use a deterministic mtime that doesn't confuse other programs. |
| # See: https://github.com/bazelbuild/bazel/issues/1299 |
| PORTABLE_MTIME = 946684800 # 2000-01-01 00:00:00.000 UTC |
| |
| DEFAULT_MTIME = 0 # The posix beginning of time |
| |
| |
| class TarFileWriter(object): |
| """A wrapper to write tar files.""" |
| |
| class Error(Exception): |
| pass |
| |
| def __init__(self, |
| name, |
| root_directory='./', |
| preserve_tar_mtimes=False): |
| """TarFileWriter wraps tarfile.open(). |
| |
| Args: |
| name: the tar file name. |
| root_directory: virtual root to prepend to elements in the archive. |
| preserve_tar_mtimes: if true, keep file mtimes from input tar file. |
| """ |
| mode = 'w:' |
| self.name = name |
| self.root_directory = root_directory.rstrip('/') |
| |
| self.preserve_mtime = preserve_tar_mtimes |
| |
| self.fileobj = None |
| self.tar = tarfile.open(name=name, mode=mode, fileobj=self.fileobj) |
| self.members = set([]) |
| self.directories = set([]) |
| |
| def __enter__(self): |
| return self |
| |
| def __exit__(self, t, v, traceback): |
| self.close() |
| |
| def add_dir(self, |
| name, |
| path, |
| uid=0, |
| gid=0, |
| uname='', |
| gname='', |
| mtime=None, |
| mode=None, |
| depth=100): |
| """Recursively add a directory. |
| |
| Args: |
| name: the destination path of the directory to add. |
| path: the path of the directory to add. |
| uid: owner user identifier. |
| gid: owner group identifier. |
| uname: owner user names. |
| gname: owner group names. |
| mtime: modification time to put in the archive. |
| mode: unix permission mode of the file, default 0644 (0755). |
| depth: maximum depth to recurse in to avoid infinite loops |
| with cyclic mounts. |
| |
| Raises: |
| TarFileWriter.Error: when the recursion depth has exceeded the |
| `depth` argument. |
| """ |
| if not (name == self.root_directory or name.startswith('/') |
| or name.startswith(self.root_directory + '/')): |
| name = os.path.join(self.root_directory, name) |
| if mtime is None: |
| mtime = DEFAULT_MTIME |
| if os.path.isdir(path): |
| # Remove trailing '/' (index -1 => last character) |
| if name[-1] == '/': |
| name = name[:-1] |
| # Add the x bit to directories to prevent non-traversable directories. |
| # The x bit is set only to if the read bit is set. |
| dirmode = (mode | ((0o444 & mode) >> 2)) if mode else mode |
| self.add_file( |
| name + '/', |
| tarfile.DIRTYPE, |
| uid=uid, |
| gid=gid, |
| uname=uname, |
| gname=gname, |
| mtime=mtime, |
| mode=dirmode) |
| if depth <= 0: |
| raise self.Error('Recursion depth exceeded, probably in ' |
| 'an infinite directory loop.') |
| # Iterate over the sorted list of file so we get a deterministic result. |
| filelist = os.listdir(path) |
| filelist.sort() |
| for f in filelist: |
| new_name = os.path.join(name, f) |
| new_path = os.path.join(path, f) |
| self.add_dir(new_name, new_path, uid, gid, uname, gname, mtime, mode, |
| depth - 1) |
| else: |
| self.add_file(name, |
| tarfile.REGTYPE, |
| file_content=path, |
| uid=uid, |
| gid=gid, |
| uname=uname, |
| gname=gname, |
| mtime=mtime, |
| mode=mode) |
| |
| def _addfile(self, info, fileobj=None): |
| """Add a file in the tar file if there is no conflict.""" |
| if not info.name.endswith('/') and info.type == tarfile.DIRTYPE: |
| # Enforce the ending / for directories so we correctly deduplicate. |
| info.name += '/' |
| if info.name not in self.members: |
| self.tar.addfile(info, fileobj) |
| self.members.add(info.name) |
| elif info.type != tarfile.DIRTYPE: |
| print(('Duplicate file in archive: %s, ' |
| 'picking first occurrence' % info.name)) |
| |
| def add_file(self, |
| name, |
| kind=tarfile.REGTYPE, |
| link=None, |
| file_content=None, |
| uid=0, |
| gid=0, |
| uname='', |
| gname='', |
| mtime=None, |
| mode=None): |
| """Add a file to the current tar. |
| |
| Args: |
| name: the name of the file to add. |
| kind: the type of the file to add, see tarfile.*TYPE. |
| link: if the file is a link, the destination of the link. |
| file_content: file to read the content from. Provide either this |
| one or `content` to specifies a content for the file. |
| uid: owner user identifier. |
| gid: owner group identifier. |
| uname: owner user names. |
| gname: owner group names. |
| mtime: modification time to put in the archive. |
| mode: unix permission mode of the file, default 0644 (0755). |
| """ |
| if file_content and os.path.isdir(file_content): |
| # Recurse into directory |
| self.add_dir(name, file_content, uid, gid, uname, gname, mtime, mode) |
| return |
| if not (name == self.root_directory or name.startswith('/') or |
| name.startswith(self.root_directory + '/')): |
| name = os.path.join(self.root_directory, name) |
| if kind == tarfile.DIRTYPE: |
| name = name.rstrip('/') |
| if name in self.directories: |
| return |
| if mtime is None: |
| mtime = DEFAULT_MTIME |
| |
| components = name.rsplit('/', 1) |
| if len(components) > 1: |
| d = components[0] |
| self.add_file(d, |
| tarfile.DIRTYPE, |
| uid=uid, |
| gid=gid, |
| uname=uname, |
| gname=gname, |
| mtime=mtime, |
| mode=0o755) |
| tarinfo = tarfile.TarInfo(name) |
| tarinfo.mtime = mtime |
| tarinfo.uid = uid |
| tarinfo.gid = gid |
| tarinfo.uname = uname |
| tarinfo.gname = gname |
| tarinfo.type = kind |
| if mode is None: |
| tarinfo.mode = 0o644 if kind == tarfile.REGTYPE else 0o755 |
| else: |
| tarinfo.mode = mode |
| if link: |
| tarinfo.linkname = link |
| if file_content: |
| with open(file_content, 'rb') as f: |
| tarinfo.size = os.fstat(f.fileno()).st_size |
| self._addfile(tarinfo, f) |
| else: |
| if kind == tarfile.DIRTYPE: |
| self.directories.add(name) |
| self._addfile(tarinfo) |
| |
| def close(self): |
| """Close the output tar file. |
| |
| This class should not be used anymore after calling that method. |
| |
| Raises: |
| TarFileWriter.Error: if an error happens when compressing the output file. |
| """ |
| self.tar.close() |
| # Close the gzip file object if necessary. |
| if self.fileobj: |
| self.fileobj.close() |