123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816 |
- package wclayer
- import (
- "bufio"
- "encoding/binary"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "path/filepath"
- "strings"
- "syscall"
- "github.com/Microsoft/go-winio"
- "github.com/Microsoft/hcsshim/internal/longpath"
- "github.com/Microsoft/hcsshim/internal/safefile"
- )
- var errorIterationCanceled = errors.New("")
- var mutatedUtilityVMFiles = map[string]bool{
- `EFI\Microsoft\Boot\BCD`: true,
- `EFI\Microsoft\Boot\BCD.LOG`: true,
- `EFI\Microsoft\Boot\BCD.LOG1`: true,
- `EFI\Microsoft\Boot\BCD.LOG2`: true,
- }
- const (
- filesPath = `Files`
- hivesPath = `Hives`
- utilityVMPath = `UtilityVM`
- utilityVMFilesPath = `UtilityVM\Files`
- )
- func openFileOrDir(path string, mode uint32, createDisposition uint32) (file *os.File, err error) {
- return winio.OpenForBackup(path, mode, syscall.FILE_SHARE_READ, createDisposition)
- }
- func hasPathPrefix(p, prefix string) bool {
- return strings.HasPrefix(p, prefix) && len(p) > len(prefix) && p[len(prefix)] == '\\'
- }
- type fileEntry struct {
- path string
- fi os.FileInfo
- err error
- }
- type legacyLayerReader struct {
- root string
- result chan *fileEntry
- proceed chan bool
- currentFile *os.File
- backupReader *winio.BackupFileReader
- }
- // newLegacyLayerReader returns a new LayerReader that can read the Windows
- // container layer transport format from disk.
- func newLegacyLayerReader(root string) *legacyLayerReader {
- r := &legacyLayerReader{
- root: root,
- result: make(chan *fileEntry),
- proceed: make(chan bool),
- }
- go r.walk()
- return r
- }
- func readTombstones(path string) (map[string]([]string), error) {
- tf, err := os.Open(filepath.Join(path, "tombstones.txt"))
- if err != nil {
- return nil, err
- }
- defer tf.Close()
- s := bufio.NewScanner(tf)
- if !s.Scan() || s.Text() != "\xef\xbb\xbfVersion 1.0" {
- return nil, errors.New("Invalid tombstones file")
- }
- ts := make(map[string]([]string))
- for s.Scan() {
- t := filepath.Join(filesPath, s.Text()[1:]) // skip leading `\`
- dir := filepath.Dir(t)
- ts[dir] = append(ts[dir], t)
- }
- if err = s.Err(); err != nil {
- return nil, err
- }
- return ts, nil
- }
- func (r *legacyLayerReader) walkUntilCancelled() error {
- root, err := longpath.LongAbs(r.root)
- if err != nil {
- return err
- }
- r.root = root
- ts, err := readTombstones(r.root)
- if err != nil {
- return err
- }
- err = filepath.Walk(r.root, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- // Indirect fix for https://github.com/moby/moby/issues/32838#issuecomment-343610048.
- // Handle failure from what may be a golang bug in the conversion of
- // UTF16 to UTF8 in files which are left in the recycle bin. Os.Lstat
- // which is called by filepath.Walk will fail when a filename contains
- // unicode characters. Skip the recycle bin regardless which is goodness.
- if strings.EqualFold(path, filepath.Join(r.root, `Files\$Recycle.Bin`)) && info.IsDir() {
- return filepath.SkipDir
- }
- if path == r.root || path == filepath.Join(r.root, "tombstones.txt") || strings.HasSuffix(path, ".$wcidirs$") {
- return nil
- }
- r.result <- &fileEntry{path, info, nil}
- if !<-r.proceed {
- return errorIterationCanceled
- }
- // List all the tombstones.
- if info.IsDir() {
- relPath, err := filepath.Rel(r.root, path)
- if err != nil {
- return err
- }
- if dts, ok := ts[relPath]; ok {
- for _, t := range dts {
- r.result <- &fileEntry{filepath.Join(r.root, t), nil, nil}
- if !<-r.proceed {
- return errorIterationCanceled
- }
- }
- }
- }
- return nil
- })
- if err == errorIterationCanceled {
- return nil
- }
- if err == nil {
- return io.EOF
- }
- return err
- }
- func (r *legacyLayerReader) walk() {
- defer close(r.result)
- if !<-r.proceed {
- return
- }
- err := r.walkUntilCancelled()
- if err != nil {
- for {
- r.result <- &fileEntry{err: err}
- if !<-r.proceed {
- return
- }
- }
- }
- }
- func (r *legacyLayerReader) reset() {
- if r.backupReader != nil {
- r.backupReader.Close()
- r.backupReader = nil
- }
- if r.currentFile != nil {
- r.currentFile.Close()
- r.currentFile = nil
- }
- }
- func findBackupStreamSize(r io.Reader) (int64, error) {
- br := winio.NewBackupStreamReader(r)
- for {
- hdr, err := br.Next()
- if err != nil {
- if err == io.EOF {
- err = nil
- }
- return 0, err
- }
- if hdr.Id == winio.BackupData {
- return hdr.Size, nil
- }
- }
- }
- func (r *legacyLayerReader) Next() (path string, size int64, fileInfo *winio.FileBasicInfo, err error) {
- r.reset()
- r.proceed <- true
- fe := <-r.result
- if fe == nil {
- err = errors.New("LegacyLayerReader closed")
- return
- }
- if fe.err != nil {
- err = fe.err
- return
- }
- path, err = filepath.Rel(r.root, fe.path)
- if err != nil {
- return
- }
- if fe.fi == nil {
- // This is a tombstone. Return a nil fileInfo.
- return
- }
- if fe.fi.IsDir() && hasPathPrefix(path, filesPath) {
- fe.path += ".$wcidirs$"
- }
- f, err := openFileOrDir(fe.path, syscall.GENERIC_READ, syscall.OPEN_EXISTING)
- if err != nil {
- return
- }
- defer func() {
- if f != nil {
- f.Close()
- }
- }()
- fileInfo, err = winio.GetFileBasicInfo(f)
- if err != nil {
- return
- }
- if !hasPathPrefix(path, filesPath) {
- size = fe.fi.Size()
- r.backupReader = winio.NewBackupFileReader(f, false)
- if path == hivesPath || path == filesPath {
- // The Hives directory has a non-deterministic file time because of the
- // nature of the import process. Use the times from System_Delta.
- var g *os.File
- g, err = os.Open(filepath.Join(r.root, hivesPath, `System_Delta`))
- if err != nil {
- return
- }
- attr := fileInfo.FileAttributes
- fileInfo, err = winio.GetFileBasicInfo(g)
- g.Close()
- if err != nil {
- return
- }
- fileInfo.FileAttributes = attr
- }
- // The creation time and access time get reset for files outside of the Files path.
- fileInfo.CreationTime = fileInfo.LastWriteTime
- fileInfo.LastAccessTime = fileInfo.LastWriteTime
- } else {
- // The file attributes are written before the backup stream.
- var attr uint32
- err = binary.Read(f, binary.LittleEndian, &attr)
- if err != nil {
- return
- }
- fileInfo.FileAttributes = attr
- beginning := int64(4)
- // Find the accurate file size.
- if !fe.fi.IsDir() {
- size, err = findBackupStreamSize(f)
- if err != nil {
- err = &os.PathError{Op: "findBackupStreamSize", Path: fe.path, Err: err}
- return
- }
- }
- // Return back to the beginning of the backup stream.
- _, err = f.Seek(beginning, 0)
- if err != nil {
- return
- }
- }
- r.currentFile = f
- f = nil
- return
- }
- func (r *legacyLayerReader) Read(b []byte) (int, error) {
- if r.backupReader == nil {
- if r.currentFile == nil {
- return 0, io.EOF
- }
- return r.currentFile.Read(b)
- }
- return r.backupReader.Read(b)
- }
- func (r *legacyLayerReader) Seek(offset int64, whence int) (int64, error) {
- if r.backupReader == nil {
- if r.currentFile == nil {
- return 0, errors.New("no current file")
- }
- return r.currentFile.Seek(offset, whence)
- }
- return 0, errors.New("seek not supported on this stream")
- }
- func (r *legacyLayerReader) Close() error {
- r.proceed <- false
- <-r.result
- r.reset()
- return nil
- }
- type pendingLink struct {
- Path, Target string
- TargetRoot *os.File
- }
- type pendingDir struct {
- Path string
- Root *os.File
- }
- type legacyLayerWriter struct {
- root *os.File
- destRoot *os.File
- parentRoots []*os.File
- currentFile *os.File
- bufWriter *bufio.Writer
- currentFileName string
- currentFileRoot *os.File
- backupWriter *winio.BackupFileWriter
- Tombstones []string
- HasUtilityVM bool
- uvmDi []dirInfo
- addedFiles map[string]bool
- PendingLinks []pendingLink
- pendingDirs []pendingDir
- currentIsDir bool
- }
- // newLegacyLayerWriter returns a LayerWriter that can write the contaler layer
- // transport format to disk.
- func newLegacyLayerWriter(root string, parentRoots []string, destRoot string) (w *legacyLayerWriter, err error) {
- w = &legacyLayerWriter{
- addedFiles: make(map[string]bool),
- }
- defer func() {
- if err != nil {
- w.CloseRoots()
- w = nil
- }
- }()
- w.root, err = safefile.OpenRoot(root)
- if err != nil {
- return
- }
- w.destRoot, err = safefile.OpenRoot(destRoot)
- if err != nil {
- return
- }
- for _, r := range parentRoots {
- f, err := safefile.OpenRoot(r)
- if err != nil {
- return w, err
- }
- w.parentRoots = append(w.parentRoots, f)
- }
- w.bufWriter = bufio.NewWriterSize(ioutil.Discard, 65536)
- return
- }
- func (w *legacyLayerWriter) CloseRoots() {
- if w.root != nil {
- w.root.Close()
- w.root = nil
- }
- if w.destRoot != nil {
- w.destRoot.Close()
- w.destRoot = nil
- }
- for i := range w.parentRoots {
- w.parentRoots[i].Close()
- }
- w.parentRoots = nil
- }
- func (w *legacyLayerWriter) initUtilityVM() error {
- if !w.HasUtilityVM {
- err := safefile.MkdirRelative(utilityVMPath, w.destRoot)
- if err != nil {
- return err
- }
- // Server 2016 does not support multiple layers for the utility VM, so
- // clone the utility VM from the parent layer into this layer. Use hard
- // links to avoid unnecessary copying, since most of the files are
- // immutable.
- err = cloneTree(w.parentRoots[0], w.destRoot, utilityVMFilesPath, mutatedUtilityVMFiles)
- if err != nil {
- return fmt.Errorf("cloning the parent utility VM image failed: %s", err)
- }
- w.HasUtilityVM = true
- }
- return nil
- }
- func (w *legacyLayerWriter) reset() error {
- err := w.bufWriter.Flush()
- if err != nil {
- return err
- }
- w.bufWriter.Reset(ioutil.Discard)
- if w.currentIsDir {
- r := w.currentFile
- br := winio.NewBackupStreamReader(r)
- // Seek to the beginning of the backup stream, skipping the fileattrs
- if _, err := r.Seek(4, io.SeekStart); err != nil {
- return err
- }
- for {
- bhdr, err := br.Next()
- if err == io.EOF {
- // end of backupstream data
- break
- }
- if err != nil {
- return err
- }
- switch bhdr.Id {
- case winio.BackupReparseData:
- // The current file is a `.$wcidirs$` metadata file that
- // describes a directory reparse point. Delete the placeholder
- // directory to prevent future files being added into the
- // destination of the reparse point during the ImportLayer call
- if err := safefile.RemoveRelative(w.currentFileName, w.currentFileRoot); err != nil {
- return err
- }
- w.pendingDirs = append(w.pendingDirs, pendingDir{Path: w.currentFileName, Root: w.currentFileRoot})
- default:
- // ignore all other stream types, as we only care about directory reparse points
- }
- }
- w.currentIsDir = false
- }
- if w.backupWriter != nil {
- w.backupWriter.Close()
- w.backupWriter = nil
- }
- if w.currentFile != nil {
- w.currentFile.Close()
- w.currentFile = nil
- w.currentFileName = ""
- w.currentFileRoot = nil
- }
- return nil
- }
- // copyFileWithMetadata copies a file using the backup/restore APIs in order to preserve metadata
- func copyFileWithMetadata(srcRoot, destRoot *os.File, subPath string, isDir bool) (fileInfo *winio.FileBasicInfo, err error) {
- src, err := safefile.OpenRelative(
- subPath,
- srcRoot,
- syscall.GENERIC_READ|winio.ACCESS_SYSTEM_SECURITY,
- syscall.FILE_SHARE_READ,
- safefile.FILE_OPEN,
- safefile.FILE_OPEN_REPARSE_POINT)
- if err != nil {
- return nil, err
- }
- defer src.Close()
- srcr := winio.NewBackupFileReader(src, true)
- defer srcr.Close()
- fileInfo, err = winio.GetFileBasicInfo(src)
- if err != nil {
- return nil, err
- }
- extraFlags := uint32(0)
- if isDir {
- extraFlags |= safefile.FILE_DIRECTORY_FILE
- }
- dest, err := safefile.OpenRelative(
- subPath,
- destRoot,
- syscall.GENERIC_READ|syscall.GENERIC_WRITE|winio.WRITE_DAC|winio.WRITE_OWNER|winio.ACCESS_SYSTEM_SECURITY,
- syscall.FILE_SHARE_READ,
- safefile.FILE_CREATE,
- extraFlags)
- if err != nil {
- return nil, err
- }
- defer dest.Close()
- err = winio.SetFileBasicInfo(dest, fileInfo)
- if err != nil {
- return nil, err
- }
- destw := winio.NewBackupFileWriter(dest, true)
- defer func() {
- cerr := destw.Close()
- if err == nil {
- err = cerr
- }
- }()
- _, err = io.Copy(destw, srcr)
- if err != nil {
- return nil, err
- }
- return fileInfo, nil
- }
- // cloneTree clones a directory tree using hard links. It skips hard links for
- // the file names in the provided map and just copies those files.
- func cloneTree(srcRoot *os.File, destRoot *os.File, subPath string, mutatedFiles map[string]bool) error {
- var di []dirInfo
- err := safefile.EnsureNotReparsePointRelative(subPath, srcRoot)
- if err != nil {
- return err
- }
- err = filepath.Walk(filepath.Join(srcRoot.Name(), subPath), func(srcFilePath string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- relPath, err := filepath.Rel(srcRoot.Name(), srcFilePath)
- if err != nil {
- return err
- }
- fileAttributes := info.Sys().(*syscall.Win32FileAttributeData).FileAttributes
- // Directories, reparse points, and files that will be mutated during
- // utility VM import must be copied. All other files can be hard linked.
- isReparsePoint := fileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0
- // In go1.9, FileInfo.IsDir() returns false if the directory is also a symlink.
- // See: https://github.com/golang/go/commit/1989921aef60c83e6f9127a8448fb5ede10e9acc
- // Fixes the problem by checking syscall.FILE_ATTRIBUTE_DIRECTORY directly
- isDir := fileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0
- if isDir || isReparsePoint || mutatedFiles[relPath] {
- fi, err := copyFileWithMetadata(srcRoot, destRoot, relPath, isDir)
- if err != nil {
- return err
- }
- if isDir && !isReparsePoint {
- di = append(di, dirInfo{path: relPath, fileInfo: *fi})
- }
- } else {
- err = safefile.LinkRelative(relPath, srcRoot, relPath, destRoot)
- if err != nil {
- return err
- }
- }
- return nil
- })
- if err != nil {
- return err
- }
- return reapplyDirectoryTimes(destRoot, di)
- }
- func (w *legacyLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo) error {
- if err := w.reset(); err != nil {
- return err
- }
- if name == utilityVMPath {
- return w.initUtilityVM()
- }
- name = filepath.Clean(name)
- if hasPathPrefix(name, utilityVMPath) {
- if !w.HasUtilityVM {
- return errors.New("missing UtilityVM directory")
- }
- if !hasPathPrefix(name, utilityVMFilesPath) && name != utilityVMFilesPath {
- return errors.New("invalid UtilityVM layer")
- }
- createDisposition := uint32(safefile.FILE_OPEN)
- if (fileInfo.FileAttributes & syscall.FILE_ATTRIBUTE_DIRECTORY) != 0 {
- st, err := safefile.LstatRelative(name, w.destRoot)
- if err != nil && !os.IsNotExist(err) {
- return err
- }
- if st != nil {
- // Delete the existing file/directory if it is not the same type as this directory.
- existingAttr := st.Sys().(*syscall.Win32FileAttributeData).FileAttributes
- if (uint32(fileInfo.FileAttributes)^existingAttr)&(syscall.FILE_ATTRIBUTE_DIRECTORY|syscall.FILE_ATTRIBUTE_REPARSE_POINT) != 0 {
- if err = safefile.RemoveAllRelative(name, w.destRoot); err != nil {
- return err
- }
- st = nil
- }
- }
- if st == nil {
- if err = safefile.MkdirRelative(name, w.destRoot); err != nil {
- return err
- }
- }
- if fileInfo.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
- w.uvmDi = append(w.uvmDi, dirInfo{path: name, fileInfo: *fileInfo})
- }
- } else {
- // Overwrite any existing hard link.
- err := safefile.RemoveRelative(name, w.destRoot)
- if err != nil && !os.IsNotExist(err) {
- return err
- }
- createDisposition = safefile.FILE_CREATE
- }
- f, err := safefile.OpenRelative(
- name,
- w.destRoot,
- syscall.GENERIC_READ|syscall.GENERIC_WRITE|winio.WRITE_DAC|winio.WRITE_OWNER|winio.ACCESS_SYSTEM_SECURITY,
- syscall.FILE_SHARE_READ,
- createDisposition,
- safefile.FILE_OPEN_REPARSE_POINT,
- )
- if err != nil {
- return err
- }
- defer func() {
- if f != nil {
- f.Close()
- safefile.RemoveRelative(name, w.destRoot)
- }
- }()
- err = winio.SetFileBasicInfo(f, fileInfo)
- if err != nil {
- return err
- }
- w.backupWriter = winio.NewBackupFileWriter(f, true)
- w.bufWriter.Reset(w.backupWriter)
- w.currentFile = f
- w.currentFileName = name
- w.currentFileRoot = w.destRoot
- w.addedFiles[name] = true
- f = nil
- return nil
- }
- fname := name
- if (fileInfo.FileAttributes & syscall.FILE_ATTRIBUTE_DIRECTORY) != 0 {
- err := safefile.MkdirRelative(name, w.root)
- if err != nil {
- return err
- }
- fname += ".$wcidirs$"
- w.currentIsDir = true
- }
- f, err := safefile.OpenRelative(fname, w.root, syscall.GENERIC_READ|syscall.GENERIC_WRITE, syscall.FILE_SHARE_READ, safefile.FILE_CREATE, 0)
- if err != nil {
- return err
- }
- defer func() {
- if f != nil {
- f.Close()
- safefile.RemoveRelative(fname, w.root)
- }
- }()
- strippedFi := *fileInfo
- strippedFi.FileAttributes = 0
- err = winio.SetFileBasicInfo(f, &strippedFi)
- if err != nil {
- return err
- }
- if hasPathPrefix(name, hivesPath) {
- w.backupWriter = winio.NewBackupFileWriter(f, false)
- w.bufWriter.Reset(w.backupWriter)
- } else {
- w.bufWriter.Reset(f)
- // The file attributes are written before the stream.
- err = binary.Write(w.bufWriter, binary.LittleEndian, uint32(fileInfo.FileAttributes))
- if err != nil {
- w.bufWriter.Reset(ioutil.Discard)
- return err
- }
- }
- w.currentFile = f
- w.currentFileName = name
- w.currentFileRoot = w.root
- w.addedFiles[name] = true
- f = nil
- return nil
- }
- func (w *legacyLayerWriter) AddLink(name string, target string) error {
- if err := w.reset(); err != nil {
- return err
- }
- target = filepath.Clean(target)
- var roots []*os.File
- if hasPathPrefix(target, filesPath) {
- // Look for cross-layer hard link targets in the parent layers, since
- // nothing is in the destination path yet.
- roots = w.parentRoots
- } else if hasPathPrefix(target, utilityVMFilesPath) {
- // Since the utility VM is fully cloned into the destination path
- // already, look for cross-layer hard link targets directly in the
- // destination path.
- roots = []*os.File{w.destRoot}
- }
- if roots == nil || (!hasPathPrefix(name, filesPath) && !hasPathPrefix(name, utilityVMFilesPath)) {
- return errors.New("invalid hard link in layer")
- }
- // Find to try the target of the link in a previously added file. If that
- // fails, search in parent layers.
- var selectedRoot *os.File
- if _, ok := w.addedFiles[target]; ok {
- selectedRoot = w.destRoot
- } else {
- for _, r := range roots {
- if _, err := safefile.LstatRelative(target, r); err != nil {
- if !os.IsNotExist(err) {
- return err
- }
- } else {
- selectedRoot = r
- break
- }
- }
- if selectedRoot == nil {
- return fmt.Errorf("failed to find link target for '%s' -> '%s'", name, target)
- }
- }
- // The link can't be written until after the ImportLayer call.
- w.PendingLinks = append(w.PendingLinks, pendingLink{
- Path: name,
- Target: target,
- TargetRoot: selectedRoot,
- })
- w.addedFiles[name] = true
- return nil
- }
- func (w *legacyLayerWriter) Remove(name string) error {
- name = filepath.Clean(name)
- if hasPathPrefix(name, filesPath) {
- w.Tombstones = append(w.Tombstones, name)
- } else if hasPathPrefix(name, utilityVMFilesPath) {
- err := w.initUtilityVM()
- if err != nil {
- return err
- }
- // Make sure the path exists; os.RemoveAll will not fail if the file is
- // already gone, and this needs to be a fatal error for diagnostics
- // purposes.
- if _, err := safefile.LstatRelative(name, w.destRoot); err != nil {
- return err
- }
- err = safefile.RemoveAllRelative(name, w.destRoot)
- if err != nil {
- return err
- }
- } else {
- return fmt.Errorf("invalid tombstone %s", name)
- }
- return nil
- }
- func (w *legacyLayerWriter) Write(b []byte) (int, error) {
- if w.backupWriter == nil && w.currentFile == nil {
- return 0, errors.New("closed")
- }
- return w.bufWriter.Write(b)
- }
- func (w *legacyLayerWriter) Close() error {
- if err := w.reset(); err != nil {
- return err
- }
- if err := safefile.RemoveRelative("tombstones.txt", w.root); err != nil && !os.IsNotExist(err) {
- return err
- }
- for _, pd := range w.pendingDirs {
- err := safefile.MkdirRelative(pd.Path, pd.Root)
- if err != nil {
- return err
- }
- }
- if w.HasUtilityVM {
- err := reapplyDirectoryTimes(w.destRoot, w.uvmDi)
- if err != nil {
- return err
- }
- }
- return nil
- }
|