mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-07 07:40:29 +00:00
143 lines
4.1 KiB
Go
143 lines
4.1 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package downloadartifact
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/act/container"
|
|
"gitea.com/gitea/runner/internal/pkg/action"
|
|
|
|
"github.com/bmatcuk/doublestar/v4"
|
|
)
|
|
|
|
func fetchOnce(ctx context.Context, c *action.Context, src *source, signed, target string, skipDecompress bool) (string, error) {
|
|
resp, err := src.download(ctx, signed, 30*time.Second)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
file, err := os.CreateTemp("", "artifact-*")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.Remove(file.Name())
|
|
defer file.Close()
|
|
hash := sha256.New()
|
|
size, err := io.Copy(io.MultiWriter(file, hash), resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
digest := "sha256:" + hex.EncodeToString(hash.Sum(nil))
|
|
if isZip(resp, signed) && !skipDecompress {
|
|
return digest, extractZip(ctx, c, file, size, target)
|
|
}
|
|
fileName := rawFileName(resp.Header.Get("Content-Disposition"))
|
|
if fileName == ".." || strings.ContainsAny(fileName, `/\`) || c.Windows() && !filepath.IsLocal(fileName) {
|
|
return "", action.Failf("Artifact filename is not valid: %s", fileName)
|
|
}
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
return digest, err
|
|
}
|
|
return digest, c.Container.CopyTarStream(ctx, target, func(tw *container.TarWriter) error {
|
|
if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: fileName, Mode: 0o644, Size: size, ModTime: time.Now()}); err != nil {
|
|
return err
|
|
}
|
|
_, err := io.Copy(tw, file)
|
|
return err
|
|
})
|
|
}
|
|
|
|
var zipTypes = []string{"application/zip", "application/x-zip-compressed", "application/zip-compressed"}
|
|
|
|
func isZip(resp *http.Response, signed string) bool {
|
|
mediaType, _, _ := strings.Cut(resp.Header.Get("Content-Type"), ";")
|
|
mediaType = strings.ToLower(strings.TrimSpace(mediaType))
|
|
signedURL, _ := url.Parse(signed)
|
|
return slices.Contains(zipTypes, mediaType) || signedURL != nil && strings.HasSuffix(strings.ToLower(signedURL.Path), ".zip")
|
|
}
|
|
|
|
func rawFileName(disposition string) string {
|
|
_, params, err := mime.ParseMediaType(disposition)
|
|
name := params["filename"] // filename* arrives decoded in here
|
|
if err != nil || name == "" {
|
|
return "artifact"
|
|
}
|
|
if unescaped, err := url.PathUnescape(name); err == nil && !strings.Contains(strings.ToLower(disposition), "filename*") {
|
|
name = unescaped
|
|
}
|
|
if name = path.Base(name); name == "/" || name == "." {
|
|
return "artifact"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func extractZip(ctx context.Context, c *action.Context, file io.ReaderAt, size int64, target string) error {
|
|
reader, err := zip.NewReader(file, size)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
return c.Container.CopyTarStream(ctx, target, func(tw *container.TarWriter) error {
|
|
for _, entry := range reader.File {
|
|
name := path.Clean(strings.ReplaceAll(entry.Name, `\`, "/"))
|
|
if name == "." {
|
|
continue
|
|
}
|
|
if path.IsAbs(name) || name == ".." || strings.HasPrefix(name, "../") || c.Windows() && !filepath.IsLocal(name) {
|
|
return action.Failf("Artifact entry path is not valid: %s", entry.Name)
|
|
}
|
|
header := &tar.Header{Typeflag: tar.TypeReg, Name: name, Mode: 0o644, Size: int64(entry.UncompressedSize64), ModTime: now}
|
|
if entry.FileInfo().IsDir() {
|
|
header.Typeflag, header.Name, header.Mode, header.Size = tar.TypeDir, name+"/", 0o755, 0
|
|
}
|
|
if err := tw.WriteHeader(header); err != nil {
|
|
return err
|
|
}
|
|
body, err := entry.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(tw, body)
|
|
if err := errors.Join(err, body.Close()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func matchName(pattern, name string) bool {
|
|
if strings.HasPrefix(pattern, "#") {
|
|
return false
|
|
}
|
|
negate := false
|
|
for strings.HasPrefix(pattern, "!") {
|
|
negate, pattern = !negate, pattern[1:]
|
|
}
|
|
matched, err := doublestar.Match(pattern, name)
|
|
if err != nil {
|
|
matched = pattern == name
|
|
}
|
|
if strings.HasPrefix(name, ".") && !strings.HasPrefix(pattern, ".") {
|
|
matched = false
|
|
}
|
|
return matched != negate
|
|
}
|