Move asset verification into the artifact package
This commit is contained in:
@@ -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}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user