Move asset verification into the artifact package

This commit is contained in:
2026-11-01 17:55:53 +00:00
parent aa03a78476
commit 61a07bc950
11 changed files with 321 additions and 216 deletions

View File

@@ -3,8 +3,6 @@ package gitea
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
@@ -12,10 +10,11 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"go.maximumdirect.net/releasekit/internal/artifact"
)
type DeliveryState string
@@ -50,20 +49,6 @@ type UploadResult struct {
Uncertain []AssetIdentity
}
// VerifyLocalAssets checks that every expected file still matches its declared
// size and digest before delivery begins.
func VerifyLocalAssets(assets []ExpectedAsset) error {
if err := validateExpectedAssets(assets); err != nil {
return err
}
for _, asset := range assets {
if err := verifyLocalAsset(asset); err != nil {
return err
}
}
return nil
}
// UploadMissing uploads assets classified missing by InspectAssets. It stops
// before the first write if any declared existing asset conflicts or cannot be
// verified, and never replaces an existing attachment.
@@ -93,8 +78,8 @@ func (c Client) uploadMissing(ctx context.Context, destination Destination, rele
return UploadResult{State: DeliveryPartial}, failure(FailureInvalid, "upload release assets", 0, errors.New("Gitea owner and repository must be path segments"))
}
expected := orderedExpected(inspection.Assets)
if err := validateExpectedAssets(expected); err != nil {
return UploadResult{State: DeliveryPartial}, err
if err := artifact.ValidateExpectedAssets(expected); err != nil {
return UploadResult{State: DeliveryPartial}, failure(FailureInvalid, "preflight release assets", 0, err)
}
result := UploadResult{State: DeliveryPartial}
preflightConflict := false
@@ -132,20 +117,26 @@ func (c Client) uploadMissing(ctx context.Context, destination Destination, rele
return result, nil
}
missing := make(map[string]ExpectedAsset, len(result.Pending))
missing := make(map[string]artifact.ExpectedAsset, len(result.Pending))
for _, observation := range inspection.Assets {
if observation.State == AssetMissing {
missing[observation.Expected.Filename] = observation.Expected
}
}
if verifyLocal {
missingAssets := make([]artifact.ExpectedAsset, 0, len(result.Pending))
for _, item := range expected {
if _, needsUpload := missing[item.Filename]; !needsUpload {
continue
}
if err := verifyLocalAsset(item); err != nil {
return result, err
missingAssets = append(missingAssets, item)
}
if err := artifact.VerifyLocalAssets(missingAssets); err != nil {
kind := FailureInvalid
if errors.Is(err, artifact.ErrAssetConflict) {
kind = FailureConflict
}
return result, failure(kind, "verify local assets", 0, err)
}
}
@@ -164,7 +155,7 @@ func (c Client) uploadMissing(ctx context.Context, destination Destination, rele
if !shouldReconcileWrite(status, uploadErr) {
return result, uploadErr
}
inspection, inspectErr := c.InspectAssets(ctx, destination, releaseID, []ExpectedAsset{item})
inspection, inspectErr := c.InspectAssets(ctx, destination, releaseID, []artifact.ExpectedAsset{item})
if inspectErr != nil || len(inspection.Assets) != 1 {
result.State = DeliveryUnknown
result.Pending = removeIdentity(result.Pending, item.Filename)
@@ -207,7 +198,7 @@ func (c Client) uploadMissing(ctx context.Context, destination Destination, rele
return result, nil
}
func (c Client) uploadAsset(ctx context.Context, client *http.Client, token, apiRoot string, releaseID int64, asset ExpectedAsset) (int, error) {
func (c Client) uploadAsset(ctx context.Context, client *http.Client, token, apiRoot string, releaseID int64, asset artifact.ExpectedAsset) (int, error) {
uploadURL := apiRoot + "/releases/" + strconv.FormatInt(releaseID, 10) + "/assets"
parsed, err := url.Parse(uploadURL)
if err != nil {
@@ -279,49 +270,16 @@ func (c Client) uploadAsset(ctx context.Context, client *http.Client, token, api
return response.StatusCode, nil
}
func verifyLocalAsset(asset ExpectedAsset) error {
if len(asset.Content) > 0 {
if int64(len(asset.Content)) != asset.Size || digest(asset.Content) != asset.SHA256 {
return failure(FailureConflict, "verify local asset", 0, fmt.Errorf("local content does not match declared asset %q", asset.Filename))
}
return nil
}
if asset.Path == "" || filepath.Base(asset.Path) != asset.Filename {
return failure(FailureInvalid, "verify local asset", 0, fmt.Errorf("local path for %q does not match its filename", asset.Filename))
}
file, err := os.Open(asset.Path)
if err != nil {
return failure(FailureInvalid, "verify local asset", 0, fmt.Errorf("open %q: %w", asset.Filename, err))
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return failure(FailureInvalid, "verify local asset", 0, fmt.Errorf("stat %q: %w", asset.Filename, err))
}
if !info.Mode().IsRegular() || info.Size() != asset.Size {
return failure(FailureConflict, "verify local asset", 0, fmt.Errorf("local file does not match declared size for %q", asset.Filename))
}
hasher := sha256.New()
size, err := io.Copy(hasher, file)
if err != nil {
return failure(FailureInvalid, "verify local asset", 0, fmt.Errorf("read %q: %w", asset.Filename, err))
}
if size != asset.Size || hex.EncodeToString(hasher.Sum(nil)) != asset.SHA256 {
return failure(FailureConflict, "verify local asset", 0, fmt.Errorf("local content does not match declared asset %q", asset.Filename))
}
return nil
}
func orderedExpected(observations []AssetInspection) []ExpectedAsset {
result := make([]ExpectedAsset, 0, len(observations))
func orderedExpected(observations []AssetInspection) []artifact.ExpectedAsset {
result := make([]artifact.ExpectedAsset, 0, len(observations))
for _, observation := range observations {
result = append(result, observation.Expected)
}
sort.Slice(result, func(i, j int) bool {
if result[i].Filename == ManifestFilename {
if result[i].Filename == artifact.ManifestFilename {
return false
}
if result[j].Filename == ManifestFilename {
if result[j].Filename == artifact.ManifestFilename {
return true
}
return result[i].Filename < result[j].Filename
@@ -329,7 +287,7 @@ func orderedExpected(observations []AssetInspection) []ExpectedAsset {
return result
}
func identity(asset ExpectedAsset) AssetIdentity {
func identity(asset artifact.ExpectedAsset) AssetIdentity {
return AssetIdentity{Filename: asset.Filename, SHA256: asset.SHA256}
}