mattermost-community-enterp.../vendor/github.com/mholt/archives/archives.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

378 lines
12 KiB
Go

package archives
import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"time"
)
// FileInfo is a virtualized, generalized file abstraction for interacting with archives.
type FileInfo struct {
fs.FileInfo
// The file header as used/provided by the archive format.
// Typically, you do not need to set this field when creating
// an archive.
Header any
// The path of the file as it appears in the archive.
// This is equivalent to Header.Name (for most Header
// types). We require it to be specified here because
// it is such a common field and we want to preserve
// format-agnosticism (no type assertions) for basic
// operations.
//
// When extracting, this name or path may not have
// been sanitized; it should not be trusted at face
// value. Consider using path.Clean() before using.
//
// If this is blank when inserting a file into an
// archive, the filename's base may be assumed
// by default to be the name in the archive.
NameInArchive string
// For symbolic and hard links, the target of the link.
// Not supported by all archive formats.
LinkTarget string
// A callback function that opens the file to read its
// contents. The file must be closed when reading is
// complete.
Open func() (fs.File, error)
}
func (f FileInfo) Stat() (fs.FileInfo, error) { return f.FileInfo, nil }
// FilesFromDisk is an opinionated function that returns a list of FileInfos
// by walking the directories in the filenames map. The keys are the names on
// disk, and the values become their associated names in the archive.
//
// Map keys that specify directories on disk will be walked and added to the
// archive recursively, rooted at the named directory. They should use the
// platform's path separator (backslash on Windows; slash on everything else).
// For convenience, map keys that end in a separator ('/', or '\' on Windows)
// will enumerate contents only, without adding the folder itself to the archive.
//
// Map values should typically use slash ('/') as the separator regardless of
// the platform, as most archive formats standardize on that rune as the
// directory separator for filenames within an archive. For convenience, map
// values that are empty string are interpreted as the base name of the file
// (sans path) in the root of the archive; and map values that end in a slash
// will use the base name of the file in that folder of the archive.
//
// File gathering will adhere to the settings specified in options.
//
// This function is used primarily when preparing a list of files to add to
// an archive.
func FilesFromDisk(ctx context.Context, options *FromDiskOptions, filenames map[string]string) ([]FileInfo, error) {
var files []FileInfo
for rootOnDisk, rootInArchive := range filenames {
if err := ctx.Err(); err != nil {
return nil, err
}
walkErr := filepath.WalkDir(rootOnDisk, func(filename string, d fs.DirEntry, err error) error {
if err := ctx.Err(); err != nil {
return err
}
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
nameInArchive := nameOnDiskToNameInArchive(filename, rootOnDisk, rootInArchive)
// this is the root folder and we are adding its contents to target rootInArchive
if info.IsDir() && nameInArchive == "" {
return nil
}
// handle symbolic links
var linkTarget string
if isSymlink(info) {
if options != nil && options.FollowSymlinks {
originalFilename := filename
filename, info, err = followSymlink(filename)
if err != nil {
return err
}
if info.IsDir() {
symlinkDirFiles, err := FilesFromDisk(ctx, options, map[string]string{filename: nameInArchive})
if err != nil {
return fmt.Errorf("getting files from symlink directory %s dereferenced to %s: %w", originalFilename, linkTarget, err)
}
files = append(files, symlinkDirFiles...)
return nil
}
} else {
// preserve symlinks
linkTarget, err = os.Readlink(filename)
if err != nil {
return fmt.Errorf("%s: readlink: %w", filename, err)
}
}
}
// handle file attributes
if options != nil && options.ClearAttributes {
info = noAttrFileInfo{info}
}
file := FileInfo{
FileInfo: info,
NameInArchive: nameInArchive,
LinkTarget: linkTarget,
Open: func() (fs.File, error) {
return os.Open(filename)
},
}
files = append(files, file)
return nil
})
if walkErr != nil {
return nil, walkErr
}
}
return files, nil
}
// nameOnDiskToNameInArchive converts a filename from disk to a name in an archive,
// respecting rules defined by FilesFromDisk. nameOnDisk is the full filename on disk
// which is expected to be prefixed by rootOnDisk (according to fs.WalkDirFunc godoc)
// and which will be placed into a folder rootInArchive in the archive.
func nameOnDiskToNameInArchive(nameOnDisk, rootOnDisk, rootInArchive string) string {
// These manipulations of rootInArchive could be done just once instead of on
// every walked file since they don't rely on nameOnDisk which is the only
// variable that changes during the walk, but combining all the logic into this
// one function is easier to reason about and test. I suspect the performance
// penalty is insignificant.
if strings.HasSuffix(rootOnDisk, string(filepath.Separator)) {
// "map keys that end in a separator will enumerate contents only,
// without adding the folder itself to the archive."
rootInArchive = trimTopDir(rootInArchive)
} else if rootInArchive == "" {
// "map values that are empty string are interpreted as the base name
// of the file (sans path) in the root of the archive"
rootInArchive = filepath.Base(rootOnDisk)
}
if rootInArchive == "." {
// an in-archive root of "." is an escape hatch for the above rule
// where an empty in-archive root means to use the base name of the
// file; if the user does not want this, they can specify a "." to
// still put it in the root of the archive
rootInArchive = ""
}
if strings.HasSuffix(rootInArchive, "/") {
// "map values that end in a slash will use the base name of the file in
// that folder of the archive."
rootInArchive += filepath.Base(rootOnDisk)
}
truncPath := strings.TrimPrefix(nameOnDisk, rootOnDisk)
return path.Join(rootInArchive, filepath.ToSlash(truncPath))
}
// trimTopDir strips the top or first directory from the path.
// It expects a forward-slashed path.
//
// Examples: "a/b/c" => "b/c", "/a/b/c" => "b/c"
func trimTopDir(dir string) string {
return strings.TrimPrefix(dir, topDir(dir)+"/")
}
// topDir returns the top or first directory in the path.
// It expects a forward-slashed path.
//
// Examples: "a/b/c" => "a", "/a/b/c" => "/a"
func topDir(dir string) string {
var start int
if len(dir) > 0 && dir[0] == '/' {
start = 1
}
if pos := strings.Index(dir[start:], "/"); pos >= 0 {
return dir[:pos+start]
}
return dir
}
// noAttrFileInfo is used to zero out some file attributes (issue #280).
type noAttrFileInfo struct{ fs.FileInfo }
// Mode preserves only the type and permission bits.
func (no noAttrFileInfo) Mode() fs.FileMode {
return no.FileInfo.Mode() & (fs.ModeType | fs.ModePerm)
}
func (noAttrFileInfo) ModTime() time.Time { return time.Time{} }
func (noAttrFileInfo) Sys() any { return nil }
// FromDiskOptions specifies various options for gathering files from disk.
type FromDiskOptions struct {
// If true, symbolic links will be dereferenced, meaning that
// the link will not be added as a link, but what the link
// points to will be added as a file.
FollowSymlinks bool
// If true, some file attributes will not be preserved.
// Name, size, type, and permissions will still be preserved.
ClearAttributes bool
}
// FileHandler is a callback function that is used to handle files as they are read
// from an archive; it is kind of like fs.WalkDirFunc. Handler functions that open
// their files must not overlap or run concurrently, as files may be read from the
// same sequential stream; always close the file before returning.
//
// If the special error value fs.SkipDir is returned, the directory of the file
// (or the file itself if it is a directory) will not be walked. Note that because
// archive contents are not necessarily ordered, skipping directories requires
// memory, and skipping lots of directories may run up your memory bill.
//
// Any other returned error will terminate a walk and be returned to the caller.
type FileHandler func(ctx context.Context, info FileInfo) error
// openAndCopyFile opens file for reading, copies its
// contents to w, then closes file.
func openAndCopyFile(file FileInfo, w io.Writer) error {
fileReader, err := file.Open()
if err != nil {
return err
}
defer fileReader.Close()
// When file is in use and size is being written to, creating the compressed
// file will fail with "archive/tar: write too long." Using CopyN gracefully
// handles this.
_, err = io.CopyN(w, fileReader, file.Size())
if err != nil && err != io.EOF {
return err
}
return nil
}
// fileIsIncluded returns true if filename is included according to
// filenameList; meaning it is in the list, its parent folder/path
// is in the list, or the list is nil.
func fileIsIncluded(filenameList []string, filename string) bool {
// include all files if there is no specific list
if filenameList == nil {
return true
}
for _, fn := range filenameList {
// exact matches are of course included
if filename == fn {
return true
}
// also consider the file included if its parent folder/path is in the list
if strings.HasPrefix(filename, strings.TrimSuffix(fn, "/")+"/") {
return true
}
}
return false
}
func isSymlink(info fs.FileInfo) bool {
return info.Mode()&os.ModeSymlink != 0
}
// streamSizeBySeeking determines the size of the stream by
// seeking to the end, then back again, so the resulting
// seek position upon returning is the same as when called
// (assuming no errors).
func streamSizeBySeeking(s io.Seeker) (int64, error) {
currentPosition, err := s.Seek(0, io.SeekCurrent)
if err != nil {
return 0, fmt.Errorf("getting current offset: %w", err)
}
maxPosition, err := s.Seek(0, io.SeekEnd)
if err != nil {
return 0, fmt.Errorf("fast-forwarding to end: %w", err)
}
_, err = s.Seek(currentPosition, io.SeekStart)
if err != nil {
return 0, fmt.Errorf("returning to prior offset %d: %w", currentPosition, err)
}
return maxPosition, nil
}
// skipList keeps a list of non-intersecting paths
// as long as its add method is used. Identical
// elements are rejected, more specific paths are
// replaced with broader ones, and more specific
// paths won't be added when a broader one already
// exists in the list. Trailing slashes are ignored.
type skipList []string
func (s *skipList) add(dir string) {
trimmedDir := strings.TrimSuffix(dir, "/")
var dontAdd bool
for i := 0; i < len(*s); i++ {
trimmedElem := strings.TrimSuffix((*s)[i], "/")
if trimmedDir == trimmedElem {
return
}
// don't add dir if a broader path already exists in the list
if strings.HasPrefix(trimmedDir, trimmedElem+"/") {
dontAdd = true
continue
}
// if dir is broader than a path in the list, remove more specific path in list
if strings.HasPrefix(trimmedElem, trimmedDir+"/") {
*s = append((*s)[:i], (*s)[i+1:]...)
i--
}
}
if !dontAdd {
*s = append(*s, dir)
}
}
// followSymlink follows a symlink until it finds a non-symlink,
// returning the target path, file info, and any error that occurs.
// It also checks for symlink loops and maximum depth.
func followSymlink(filename string) (string, os.FileInfo, error) {
visited := make(map[string]bool)
visited[filename] = true
// Limit in Linux kernel: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/namei.c?id=v3.5#n624
const maxDepth = 40
for {
linkPath, err := os.Readlink(filename)
if err != nil {
return "", nil, fmt.Errorf("%s: readlink: %w", filename, err)
}
if !filepath.IsAbs(linkPath) {
linkPath = filepath.Join(filepath.Dir(filename), linkPath)
}
info, err := os.Lstat(linkPath)
if err != nil {
return "", nil, fmt.Errorf("%s: statting dereferenced symlink: %w", filename, err)
}
// Not a symlink, we've found the target, return it
if info.Mode()&os.ModeSymlink == 0 {
return linkPath, info, nil
}
if visited[linkPath] {
return "", nil, fmt.Errorf("%s: symlink loop", filename)
}
if len(visited) >= maxDepth {
return "", nil, fmt.Errorf("%s: maximum symlink depth (%d) exceeded", filename, maxDepth)
}
visited[linkPath] = true
filename = linkPath
}
}