// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "io/fs" "log" "os" "path" "path/filepath" "sort" "strings" "time" ) // An Archive describes an archive to write: a collection of files. // Directories are implied by the files and not explicitly listed. type Archive struct { Files []File } // A File describes a single file to write to an archive. type File struct { Name string // name in archive Time time.Time // modification time Mode fs.FileMode Size int64 Src string // source file in OS file system } // Info returns a FileInfo about the file, for use with tar.FileInfoHeader // and zip.FileInfoHeader. func (f *File) Info() fs.FileInfo { return fileInfo{f} } // A fileInfo is an implementation of fs.FileInfo describing a File. type fileInfo struct { f *File } func (i fileInfo) Name() string { return path.Base(i.f.Name) } func (i fileInfo) ModTime() time.Time { return i.f.Time } func (i fileInfo) Mode() fs.FileMode { return i.f.Mode } func (i fileInfo) IsDir() bool { return i.f.Mode&fs.ModeDir != 0 } func (i fileInfo) Size() int64 { return i.f.Size } func (i fileInfo) Sys() any { return nil } func (i fileInfo) String() string { return fs.FormatFileInfo(i) } // NewArchive returns a new Archive containing all the files in the directory dir. // The archive can be amended afterward using methods like Add and Filter. func NewArchive(dir string) (*Archive, error) { a := new(Archive) err := fs.WalkDir(os.DirFS(dir), ".", func(name string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } info, err := d.Info() if err != nil { return err } a.Add(name, filepath.Join(dir, name), info) return nil }) if err != nil { return nil, err } a.Sort() return a, nil } // Add adds a file with the given name and info to the archive. // The content of the file comes from the operating system file src. // After a sequence of one or more calls to Add, // the caller should invoke Sort to re-sort the archive's files. func (a *Archive) Add(name, src string, info fs.FileInfo) { a.Files = append(a.Files, File{ Name: name, Time: info.ModTime(), Mode: info.Mode(), Size: info.Size(), Src: src, }) } func nameLess(x, y string) bool { for i := 0; i < len(x) && i < len(y); i++ { if x[i] != y[i] { // foo/bar/baz before foo/bar.go, because foo/bar is before foo/bar.go if x[i] == '/' { return true } if y[i] == '/' { return false } return x[i] < y[i] } } return len(x) < len(y) } // Sort sorts the files in the archive. // It is only necessary to call Sort after calling Add or RenameGoMod. // NewArchive returns a sorted archive, and the other methods // preserve the sorting of the archive. func (a *Archive) Sort() { sort.Slice(a.Files, func(i, j int) bool { return nameLess(a.Files[i].Name, a.Files[j].Name) }) } // Clone returns a copy of the Archive. // Method calls like Add and Filter invoked on the copy do not affect the original, // nor do calls on the original affect the copy. func (a *Archive) Clone() *Archive { b := &Archive{ Files: make([]File, len(a.Files)), } copy(b.Files, a.Files) return b } // AddPrefix adds a prefix to all file names in the archive. func (a *Archive) AddPrefix(prefix string) { for i := range a.Files { a.Files[i].Name = path.Join(prefix, a.Files[i].Name) } } // Filter removes files from the archive for which keep(name) returns false. func (a *Archive) Filter(keep func(name string) bool) { files := a.Files[:0] for _, f := range a.Files { if keep(f.Name) { files = append(files, f) } } a.Files = files } // SetMode changes the mode of every file in the archive // to be mode(name, m), where m is the file's current mode. func (a *Archive) SetMode(mode func(name string, m fs.FileMode) fs.FileMode) { for i := range a.Files { a.Files[i].Mode = mode(a.Files[i].Name, a.Files[i].Mode) } } // Remove removes files matching any of the patterns from the archive. // The patterns use the syntax of path.Match, with an extension of allowing // a leading **/ or trailing /**, which match any number of path elements // (including no path elements) before or after the main match. func (a *Archive) Remove(patterns ...string) { a.Filter(func(name string) bool { for _, pattern := range patterns { match, err := amatch(pattern, name) if err != nil { log.Fatalf("archive remove: %v", err) } if match { return false } } return true }) } // SetTime sets the modification time of all files in the archive to t. func (a *Archive) SetTime(t time.Time) { for i := range a.Files { a.Files[i].Time = t } } // RenameGoMod renames the go.mod files in the archive to _go.mod, // for use with the module form, which cannot contain other go.mod files. func (a *Archive) RenameGoMod() { for i, f := range a.Files { if strings.HasSuffix(f.Name, "/go.mod") { a.Files[i].Name = strings.TrimSuffix(f.Name, "go.mod") + "_go.mod" } } } func amatch(pattern, name string) (bool, error) { // firstN returns the prefix of name corresponding to the first n path elements. // If n <= 0, firstN returns the entire name. firstN := func(name string, n int) string { for i := 0; i < len(name); i++ { if name[i] == '/' { if n--; n == 0 { return name[:i] } } } return name } // lastN returns the suffix of name corresponding to the last n path elements. // If n <= 0, lastN returns the entire name. lastN := func(name string, n int) string { for i := len(name) - 1; i >= 0; i-- { if name[i] == '/' { if n--; n == 0 { return name[i+1:] } } } return name } if p, ok := strings.CutPrefix(pattern, "**/"); ok { return path.Match(p, lastN(name, 1+strings.Count(p, "/"))) } if p, ok := strings.CutSuffix(pattern, "/**"); ok { return path.Match(p, firstN(name, 1+strings.Count(p, "/"))) } return path.Match(pattern, name) }