def dest_realpath(self, unpack_path, dst): """Return the canonicalized version of path dst within (canonical) image path unpack_path. We can't use os.path.realpath() because if dst is an absolute symlink, we need to use the *image's* root directory, not the host. Thus, we have to resolve symlinks manually.""" unpack_path = ch.Path(unpack_path) dst_canon = ch.Path(unpack_path) dst = ch.Path(dst) dst_parts = list(reversed( dst.parts)) # easier to operate on end of list iter_ct = 0 while (len(dst_parts) > 0): iter_ct += 1 if (iter_ct > 100): # arbitrary ch.FATAL("can't COPY: too many path components") ch.TRACE("current destination: %d %s" % (iter_ct, dst_canon)) #ch.TRACE("parts remaining: %s" % dst_parts) part = dst_parts.pop() if (part == "/" or part == "//"): # 3 or more slashes yields "/" ch.TRACE("skipping root") continue cand = dst_canon // part ch.TRACE("checking: %s" % cand) if (not cand.is_symlink()): ch.TRACE("not symlink") dst_canon = cand else: target = ch.Path(os.readlink(cand)) ch.TRACE("symlink to: %s" % target) assert (len(target.parts) > 0) # POSIX says no empty symlinks if (target.is_absolute()): ch.TRACE("absolute") dst_canon = ch.Path(unpack_path) else: ch.TRACE("relative") dst_parts.extend(reversed(target.parts)) return dst_canon
def chdir(self, path): if (path.startswith("/")): self.workdir = ch.Path(path) else: self.workdir //= path
def workdir(self): return ch.Path(images[image_i].metadata["cwd"])
def execute_(self): if (cli.context == "-"): ch.FATAL("can't COPY: no context because \"-\" given") if (len(self.srcs) < 1): ch.FATAL("can't COPY: must specify at least one source") # Complain about unsupported stuff. if (self.options.pop("chown", False)): self.unsupported_forever_warn("--chown") # Any remaining options are invalid. self.options_assert_empty() # Find the context directory. if (self.from_ is None): context = cli.context else: if (self.from_ == image_i or self.from_ == image_alias): ch.FATAL("COPY --from: stage %s is the current stage" % self.from_) if (not self.from_ in images): # FIXME: Would be nice to also report if a named stage is below. if (isinstance(self.from_, int) and self.from_ < image_ct): if (self.from_ < 0): ch.FATAL( "COPY --from: invalid negative stage index %d" % self.from_) else: ch.FATAL("COPY --from: stage %d does not exist yet" % self.from_) else: ch.FATAL("COPY --from: stage %s does not exist" % self.from_) context = images[self.from_].unpack_path context_canon = os.path.realpath(context) ch.VERBOSE("context: %s" % context) # Expand source wildcards. srcs = list() for src in self.srcs: matches = glob.glob("%s/%s" % (context, src)) # glob can't take Path if (len(matches) == 0): ch.FATAL("can't copy: not found: %s" % src) for i in matches: srcs.append(i) ch.VERBOSE("source: %s" % i) # Validate sources are within context directory. (Can't convert to # canonical paths yet because we need the source path as given.) for src in srcs: src_canon = os.path.realpath(src) if (not os.path.commonpath([src_canon, context_canon ]).startswith(context_canon)): ch.FATAL("can't COPY from outside context: %s" % src) # Locate the destination. unpack_canon = os.path.realpath(images[image_i].unpack_path) if (self.dst.startswith("/")): dst = ch.Path(self.dst) else: dst = env.workdir // self.dst ch.VERBOSE("destination, as given: %s" % dst) dst_canon = self.dest_realpath(unpack_canon, dst) # strips trailing slash ch.VERBOSE("destination, canonical: %s" % dst_canon) if (not os.path.commonpath([dst_canon, unpack_canon ]).startswith(unpack_canon)): ch.FATAL("can't COPY: destination not in image: %s" % dst_canon) # Create the destination directory if needed. if (self.dst.endswith("/") or len(srcs) > 1 or os.path.isdir(srcs[0])): if (not os.path.exists(dst_canon)): ch.mkdirs(dst_canon) elif (not os.path.isdir(dst_canon)): # not symlink b/c realpath() ch.FATAL("can't COPY: not a directory: %s" % dst_canon) # Copy each source. for src in srcs: if (os.path.isfile(src)): self.copy_src_file(src, dst_canon) elif (os.path.isdir(src)): self.copy_src_dir(src, dst_canon) else: ch.FATAL("can't COPY: unknown file type: %s" % src)
def copy_src_dir(self, src, dst): """Copy the contents of directory src, named by COPY, either explicitly or with wildcards, to dst. src might be a symlink, but dst is a canonical path. Both must be at the top level of the COPY instruction; i.e., this function must not be called recursively. dst must exist already and be a directory. Unlike subdirectories, the metadata of dst will not be altered to match src.""" def onerror(x): ch.FATAL("can't scan directory: %s: %s" % (x.filename, x.strerror)) # Use Path objects in this method because the path arithmetic was # getting too hard with strings. src = ch.Path(os.path.realpath(src)) dst = ch.Path(dst) assert (os.path.isdir(src) and not os.path.islink(src)) assert (os.path.isdir(dst) and not os.path.islink(dst)) ch.DEBUG("copying named directory: %s -> %s" % (src, dst)) for (dirpath, dirnames, filenames) in os.walk(src, onerror=onerror): dirpath = ch.Path(dirpath) subdir = dirpath.relative_to(src) dst_dir = dst // subdir # dirnames can contain symlinks, which we handle as files, so we'll # rebuild it; the walk will not descend into those "directories". dirnames2 = dirnames.copy() # shallow copy dirnames[:] = list() # clear in place for d in dirnames2: d = ch.Path(d) src_path = dirpath // d dst_path = dst_dir // d ch.TRACE("dir: %s -> %s" % (src_path, dst_path)) if (os.path.islink(src_path)): filenames.append(d) # symlink, handle as file ch.TRACE("symlink to dir, will handle as file") continue else: dirnames.append(d) # directory, descend into later # If destination exists, but isn't a directory, remove it. if (os.path.exists(dst_path)): if (os.path.isdir(dst_path) and not os.path.islink(dst_path)): ch.TRACE("dst_path exists and is a directory") else: ch.TRACE("dst_path exists, not a directory, removing") ch.unlink(dst_path) # If destination directory doesn't exist, create it. if (not os.path.exists(dst_path)): ch.TRACE("mkdir dst_path") ch.ossafe(os.mkdir, "can't mkdir: %s" % dst_path, dst_path) # Copy metadata, now that we know the destination exists and is a # directory. ch.ossafe(shutil.copystat, "can't copy metadata: %s -> %s" % (src_path, dst_path), src_path, dst_path, follow_symlinks=False) for f in filenames: f = ch.Path(f) src_path = dirpath // f dst_path = dst_dir // f ch.TRACE("file or symlink via copy2: %s -> %s" % (src_path, dst_path)) if (not (os.path.isfile(src_path) or os.path.islink(src_path))): ch.FATAL("can't COPY: unknown file type: %s" % src_path) if (os.path.exists(dst_path)): ch.TRACE("destination exists, removing") if (os.path.isdir(dst_path) and not os.path.islink(dst_path)): ch.rmtree(dst_path) else: ch.unlink(dst_path) ch.copy2(src_path, dst_path, follow_symlinks=False)