Files
runner/internal/pkg/action/uploadartifact/walk.go
T
2026-11-06 03:17:49 +00:00

179 lines
4.7 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package uploadartifact
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"slices"
"strings"
"time"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/internal/pkg/action"
"gitea.com/gitea/runner/internal/pkg/action/glob"
)
type found struct {
path, source string
mode fs.FileMode
mtime time.Time
body io.Reader
}
type treeEntry struct {
name, target string
dir bool
}
type tree struct {
real string
entries map[string][]treeEntry
}
const listScript = `test -e "$1" || exit 1
[ -d "$1" ] || { printf 'f\0'; exit 0; }
cd "$1" || exit 2
shift
pwd -P; printf '\0'
find . "$@" -type d ! -name . -print0; printf '\0'
find . "$@" -type l -exec sh -c 'for l; do printf "%s\0" "$l"; readlink "$l"; printf "\0"; done' sh {} +; printf '\0'
find . "$@" ! -type d ! -type l -print0`
func list(ctx context.Context, c *action.Context, source string, includeHidden bool) (*tree, error) {
if c.Host() {
info, err := os.Stat(source)
if err != nil {
return nil, err
}
resolved, err := filepath.EvalSymlinks(source)
if err != nil || !info.IsDir() {
return &tree{real: resolved}, err
}
entries, err := os.ReadDir(resolved)
result := &tree{real: resolved, entries: map[string][]treeEntry{".": nil}}
for _, entry := range entries {
result.entries["."] = append(result.entries["."], treeEntry{name: entry.Name(), dir: entry.IsDir()})
}
return result, err
}
command := []string{"sh", "-c", listScript, "sh", source}
if !includeHidden {
command = append(command, "!", "-name", ".", "-name", ".*", "-prune", "-o")
}
output, err := c.Container.ExecOutput(ctx, command)
if err != nil {
return nil, err
}
defer output.Close()
data, err := io.ReadAll(output)
if err != nil {
return nil, err
}
if err := output.Close(); err != nil {
return nil, err
}
if string(data) == "f\x00" {
return &tree{real: source}, nil
}
fields := strings.Split(string(data), "\x00")
if len(fields) < 4 {
return nil, fmt.Errorf("invalid directory listing for %s", source)
}
result := &tree{real: strings.TrimSuffix(fields[0], "\n"), entries: map[string][]treeEntry{".": nil}}
section := 0
for index := 1; index < len(fields)-1; index++ {
name := fields[index]
if name == "" {
section++
continue
}
entry := treeEntry{name: path.Base(name), dir: section == 0}
switch section {
case 0:
result.entries[path.Clean(name)] = nil
case 1:
index++
if index >= len(fields)-1 {
return nil, fmt.Errorf("invalid symlink listing for %s", source)
}
entry.target = strings.TrimSuffix(fields[index], "\n")
}
result.entries[path.Dir(name)] = append(result.entries[path.Dir(name)], entry)
}
for _, entries := range result.entries {
slices.SortFunc(entries, func(left, right treeEntry) int { return strings.Compare(left.name, right.name) })
}
return result, nil
}
func selectFiles(ctx context.Context, c *action.Context, globber *glob.Globber, includeHidden bool) ([]found, error) {
var files []found
join := path.Join
if c.Windows() {
join = filepath.Join
}
var walk func(string, string, []string, *tree, string) error
walk = func(virtual, source string, chain []string, listed *tree, relative string) error {
match := globber.Match(virtual)
if match == glob.MatchNone && !globber.PartialMatch(virtual) || !includeHidden && strings.HasPrefix(c.Base(virtual), ".") {
return nil
}
if listed == nil {
var err error
listed, err = list(ctx, c, source, includeHidden)
var exit container.ExitCodeError
if errors.Is(err, fs.ErrNotExist) || errors.As(err, &exit) && exit == 1 {
return nil
} else if err != nil {
return err
}
relative = "."
}
if listed.entries == nil {
if match&glob.MatchFile != 0 {
files = append(files, found{path: virtual, source: listed.real})
}
return nil
}
resolved := join(listed.real, relative)
if slices.Contains(chain, resolved) {
return nil
}
chain = append(slices.Clip(chain), resolved)
for _, entry := range listed.entries[relative] {
child := join(virtual, entry.name)
source := join(resolved, entry.name)
var subtree *tree
switch {
case entry.target != "":
source = entry.target
if !path.IsAbs(source) {
source = path.Join(resolved, source)
}
case entry.dir && !c.Host():
subtree = listed
case !entry.dir && !c.Host():
subtree = &tree{real: source}
}
if err := walk(child, source, chain, subtree, path.Join(relative, entry.name)); err != nil {
return err
}
}
return nil
}
for _, root := range globber.SearchPaths() {
if err := walk(root, root, nil, nil, "."); err != nil {
return nil, err
}
}
return files, nil
}