mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	* fixes a fixture status to upload confirmed * add another fixture as noise to break tests as soon they are exposed to api * v4 delete test added check that artifact is no longer visible in internal api with status pending delete * removal of http 404 on empty list: actions/upload-artifact@v4 now backoff on http 404 of ListArtifacts endpoint * fixes artifacts with pending delete etc. are able to be found and downloaded if the storage is not freed
		
			
				
	
	
		
			214 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			214 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2023 The Gitea Authors. All rights reserved.
 | ||
| // SPDX-License-Identifier: MIT
 | ||
| 
 | ||
| // This artifact server is inspired by https://github.com/nektos/act/blob/master/pkg/artifacts/server.go.
 | ||
| // It updates url setting and uses ObjectStore to handle artifacts persistence.
 | ||
| 
 | ||
| package actions
 | ||
| 
 | ||
| import (
 | ||
| 	"context"
 | ||
| 	"errors"
 | ||
| 	"time"
 | ||
| 
 | ||
| 	"code.gitea.io/gitea/models/db"
 | ||
| 	"code.gitea.io/gitea/modules/timeutil"
 | ||
| 	"code.gitea.io/gitea/modules/util"
 | ||
| 
 | ||
| 	"xorm.io/builder"
 | ||
| )
 | ||
| 
 | ||
| // ArtifactStatus is the status of an artifact, uploading, expired or need-delete
 | ||
| type ArtifactStatus int64
 | ||
| 
 | ||
| const (
 | ||
| 	ArtifactStatusUploadPending   ArtifactStatus = iota + 1 // 1, ArtifactStatusUploadPending is the status of an artifact upload that is pending
 | ||
| 	ArtifactStatusUploadConfirmed                           // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed
 | ||
| 	ArtifactStatusUploadError                               // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored
 | ||
| 	ArtifactStatusExpired                                   // 4, ArtifactStatusExpired is the status of an artifact that is expired
 | ||
| 	ArtifactStatusPendingDeletion                           // 5, ArtifactStatusPendingDeletion is the status of an artifact that is pending deletion
 | ||
| 	ArtifactStatusDeleted                                   // 6, ArtifactStatusDeleted is the status of an artifact that is deleted
 | ||
| )
 | ||
| 
 | ||
| func (status ArtifactStatus) ToString() string {
 | ||
| 	switch status {
 | ||
| 	case ArtifactStatusUploadPending:
 | ||
| 		return "upload is not yet completed"
 | ||
| 	case ArtifactStatusUploadConfirmed:
 | ||
| 		return "upload is completed"
 | ||
| 	case ArtifactStatusUploadError:
 | ||
| 		return "upload failed"
 | ||
| 	case ArtifactStatusExpired:
 | ||
| 		return "expired"
 | ||
| 	case ArtifactStatusPendingDeletion:
 | ||
| 		return "pending deletion"
 | ||
| 	case ArtifactStatusDeleted:
 | ||
| 		return "deleted"
 | ||
| 	default:
 | ||
| 		return "unknown"
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func init() {
 | ||
| 	db.RegisterModel(new(ActionArtifact))
 | ||
| }
 | ||
| 
 | ||
| // ActionArtifact is a file that is stored in the artifact storage.
 | ||
| type ActionArtifact struct {
 | ||
| 	ID                 int64 `xorm:"pk autoincr"`
 | ||
| 	RunID              int64 `xorm:"index unique(runid_name_path)"` // The run id of the artifact
 | ||
| 	RunnerID           int64
 | ||
| 	RepoID             int64 `xorm:"index"`
 | ||
| 	OwnerID            int64
 | ||
| 	CommitSHA          string
 | ||
| 	StoragePath        string             // The path to the artifact in the storage
 | ||
| 	FileSize           int64              // The size of the artifact in bytes
 | ||
| 	FileCompressedSize int64              // The size of the artifact in bytes after gzip compression
 | ||
| 	ContentEncoding    string             // The content encoding of the artifact
 | ||
| 	ArtifactPath       string             `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it
 | ||
| 	ArtifactName       string             `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it
 | ||
| 	Status             ArtifactStatus     `xorm:"index"`                         // The status of the artifact, uploading, expired or need-delete
 | ||
| 	CreatedUnix        timeutil.TimeStamp `xorm:"created"`
 | ||
| 	UpdatedUnix        timeutil.TimeStamp `xorm:"updated index"`
 | ||
| 	ExpiredUnix        timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
 | ||
| }
 | ||
| 
 | ||
| func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) {
 | ||
| 	if err := t.LoadJob(ctx); err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 	artifact, err := getArtifactByNameAndPath(ctx, t.Job.RunID, artifactName, artifactPath)
 | ||
| 	if errors.Is(err, util.ErrNotExist) {
 | ||
| 		artifact := &ActionArtifact{
 | ||
| 			ArtifactName: artifactName,
 | ||
| 			ArtifactPath: artifactPath,
 | ||
| 			RunID:        t.Job.RunID,
 | ||
| 			RunnerID:     t.RunnerID,
 | ||
| 			RepoID:       t.RepoID,
 | ||
| 			OwnerID:      t.OwnerID,
 | ||
| 			CommitSHA:    t.CommitSHA,
 | ||
| 			Status:       ArtifactStatusUploadPending,
 | ||
| 			ExpiredUnix:  timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
 | ||
| 		}
 | ||
| 		if _, err := db.GetEngine(ctx).Insert(artifact); err != nil {
 | ||
| 			return nil, err
 | ||
| 		}
 | ||
| 		return artifact, nil
 | ||
| 	} else if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	if _, err := db.GetEngine(ctx).ID(artifact.ID).Cols("expired_unix").Update(&ActionArtifact{
 | ||
| 		ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
 | ||
| 	}); err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	return artifact, nil
 | ||
| }
 | ||
| 
 | ||
| func getArtifactByNameAndPath(ctx context.Context, runID int64, name, fpath string) (*ActionArtifact, error) {
 | ||
| 	var art ActionArtifact
 | ||
| 	has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ?", runID, name, fpath).Get(&art)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	} else if !has {
 | ||
| 		return nil, util.ErrNotExist
 | ||
| 	}
 | ||
| 	return &art, nil
 | ||
| }
 | ||
| 
 | ||
| // UpdateArtifactByID updates an artifact by id
 | ||
| func UpdateArtifactByID(ctx context.Context, id int64, art *ActionArtifact) error {
 | ||
| 	art.ID = id
 | ||
| 	_, err := db.GetEngine(ctx).ID(id).AllCols().Update(art)
 | ||
| 	return err
 | ||
| }
 | ||
| 
 | ||
| type FindArtifactsOptions struct {
 | ||
| 	db.ListOptions
 | ||
| 	RepoID               int64
 | ||
| 	RunID                int64
 | ||
| 	ArtifactName         string
 | ||
| 	Status               int
 | ||
| 	FinalizedArtifactsV4 bool
 | ||
| }
 | ||
| 
 | ||
| func (opts FindArtifactsOptions) ToOrders() string {
 | ||
| 	return "id"
 | ||
| }
 | ||
| 
 | ||
| var _ db.FindOptionsOrder = (*FindArtifactsOptions)(nil)
 | ||
| 
 | ||
| func (opts FindArtifactsOptions) ToConds() builder.Cond {
 | ||
| 	cond := builder.NewCond()
 | ||
| 	if opts.RepoID > 0 {
 | ||
| 		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
 | ||
| 	}
 | ||
| 	if opts.RunID > 0 {
 | ||
| 		cond = cond.And(builder.Eq{"run_id": opts.RunID})
 | ||
| 	}
 | ||
| 	if opts.ArtifactName != "" {
 | ||
| 		cond = cond.And(builder.Eq{"artifact_name": opts.ArtifactName})
 | ||
| 	}
 | ||
| 	if opts.Status > 0 {
 | ||
| 		cond = cond.And(builder.Eq{"status": opts.Status})
 | ||
| 	}
 | ||
| 	if opts.FinalizedArtifactsV4 {
 | ||
| 		cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired}))
 | ||
| 		cond = cond.And(builder.Eq{"content_encoding": "application/zip"})
 | ||
| 	}
 | ||
| 
 | ||
| 	return cond
 | ||
| }
 | ||
| 
 | ||
| // ActionArtifactMeta is the meta-data of an artifact
 | ||
| type ActionArtifactMeta struct {
 | ||
| 	ArtifactName string
 | ||
| 	FileSize     int64
 | ||
| 	Status       ArtifactStatus
 | ||
| }
 | ||
| 
 | ||
| // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run
 | ||
| func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) {
 | ||
| 	arts := make([]*ActionArtifactMeta, 0, 10)
 | ||
| 	return arts, db.GetEngine(ctx).Table("action_artifact").
 | ||
| 		Where("run_id=? AND (status=? OR status=?)", runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired).
 | ||
| 		GroupBy("artifact_name").
 | ||
| 		Select("artifact_name, sum(file_size) as file_size, max(status) as status").
 | ||
| 		Find(&arts)
 | ||
| }
 | ||
| 
 | ||
| // ListNeedExpiredArtifacts returns all need expired artifacts but not deleted
 | ||
| func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) {
 | ||
| 	arts := make([]*ActionArtifact, 0, 10)
 | ||
| 	return arts, db.GetEngine(ctx).
 | ||
| 		Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts)
 | ||
| }
 | ||
| 
 | ||
| // ListPendingDeleteArtifacts returns all artifacts in pending-delete status.
 | ||
| // limit is the max number of artifacts to return.
 | ||
| func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifact, error) {
 | ||
| 	arts := make([]*ActionArtifact, 0, limit)
 | ||
| 	return arts, db.GetEngine(ctx).
 | ||
| 		Where("status = ?", ArtifactStatusPendingDeletion).Limit(limit).Find(&arts)
 | ||
| }
 | ||
| 
 | ||
| // SetArtifactExpired sets an artifact to expired
 | ||
| func SetArtifactExpired(ctx context.Context, artifactID int64) error {
 | ||
| 	_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusExpired})
 | ||
| 	return err
 | ||
| }
 | ||
| 
 | ||
| // SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
 | ||
| func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
 | ||
| 	_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
 | ||
| 	return err
 | ||
| }
 | ||
| 
 | ||
| // SetArtifactDeleted sets an artifact to deleted
 | ||
| func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
 | ||
| 	_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted})
 | ||
| 	return err
 | ||
| }
 |