mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-07 07:40:29 +00:00
179 lines
4.7 KiB
Go
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
|
|
}
|