package archive import ( "archive/tar" "errors" "fmt" "io/fs" "path" "path/filepath" "strings" "time" "code.icb4dc0.de/buildr/buildr/internal/ioutils" ) var defaultCreationTime = time.Unix(0, 0) type fileToTar struct { sourcePath string targetFileName string } type filesToTar []fileToTar func (files *filesToTar) IsEmpty() bool { return len(*files) == 0 } func (files *filesToTar) Add(toAdd fileToTar) { ref := *files ref = append(ref, toAdd) *files = ref } func (files *filesToTar) Remove(toRemove string) bool { ref := *files for i := range ref { if ref[i].targetFileName == toRemove { ref = append(ref[:i], ref[i+1:]...) *files = ref return true } } return false } func newNode() *archiveNode { return &archiveNode{ children: make(map[string]*archiveNode), files: new(filesToTar), } } type archiveNode struct { children map[string]*archiveNode files *filesToTar } func (n *archiveNode) writeToTar(writer *tar.Writer, fileSystem fs.StatFS, parent string) error { for segment, child := range n.children { if child.isEmpty() { continue } segmentPath := filepath.Join(parent, segment) header := &tar.Header{ Name: segmentPath, Typeflag: tar.TypeDir, // Use a fixed Mode, so that this isn't sensitive to the directory and umask // under which it was created. Additionally, windows can only set 0222, // 0444, or 0666, none of which are executable. Mode: 0o555, ModTime: defaultCreationTime, } if err := writer.WriteHeader(header); err != nil { return fmt.Errorf("failed to write tar header: %w", err) } if err := child.writeToTar(writer, fileSystem, segmentPath); err != nil { return err } } for _, fileSpec := range *n.files { info, err := fileSystem.Stat(fileSpec.sourcePath) if err != nil { return err } f, err := fileSystem.Open(fileSpec.sourcePath) if err != nil { return err } header, err := tar.FileInfoHeader(info, path.Join(parent, fileSpec.targetFileName)) if err != nil { return err } header.Name = path.Join(parent, fileSpec.targetFileName) if err = writer.WriteHeader(header); err != nil { return errors.Join(err, f.Close()) } if _, err = ioutils.CopyWithPooledBuffer(writer, f); err != nil { return errors.Join(err, f.Close()) } if err := f.Close(); err != nil { return err } } return nil } func (n *archiveNode) remove(pathToRemove string) bool { if pathToRemove == "" { return false } split := strings.Split(pathToRemove, "/") last := split[len(split)-1] current := n for _, segment := range split[:len(split)-1] { if child, ok := current.children[segment]; ok { current = child continue } else { return false } } if _, ok := current.children[last]; ok { delete(current.children, last) return true } else { return current.files.Remove(last) } } func (n *archiveNode) addFile(sourcePath, targetPath string) { dirPath, fileName := filepath.Split(targetPath) current := n for _, segment := range strings.Split(dirPath, "/") { if segment == "" { continue } if child, ok := current.children[segment]; ok { current = child } else { newNode := &archiveNode{ children: make(map[string]*archiveNode), files: new(filesToTar), } current.children[segment] = newNode current = newNode } } current.files.Add(fileToTar{ sourcePath: sourcePath, targetFileName: fileName, }) } func (n *archiveNode) addDir(dirPath string) { current := n for _, segment := range strings.Split(dirPath, "/") { if segment == "" { continue } if child, ok := current.children[segment]; ok { current = child } else { newNode := newNode() current.children[segment] = newNode current = newNode } } } func (n *archiveNode) isEmpty() bool { return len(n.children) == 0 && n.files.IsEmpty() }