1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6 "fmt"
7 "strings"
8 "time"
9 "unicode"
10 "unicode/utf8"
11)
12
13// Limits and defaults of the CI artifacts (artifacts v4, CI-4 D3).
14const (
15 // CIArtifactMaxNameLen is the longest artifact name, in characters.
16 CIArtifactMaxNameLen = 256
17 // CIArtifactMaxMediaTypeLen is the longest media type of an artifact.
18 CIArtifactMaxMediaTypeLen = 255
19 // CIArtifactMaxBlockIDLen is the longest block id (Azure's, decoded).
20 CIArtifactMaxBlockIDLen = 64
21 // CIArtifactMaxBlocks bounds the blocks of a commit and the blocks staged
22 // for one artifact.
23 CIArtifactMaxBlocks = 50_000
24 // CIArtifactMaxPendingPerJob bounds the unfinished uploads of a job, and
25 // CIArtifactMaxPerRun the artifacts of a run, unfinished ones included
26 // (the toolkit uploads one artifact at a time).
27 CIArtifactMaxPendingPerJob = 32
28 CIArtifactMaxPerRun = 1000
29 // CIArtifactChunk is the largest piece of data of one message, both ways.
30 CIArtifactChunk = 1 << 20
31 // CIArtifactMaxBlockSize bounds the in-memory staging of a toolkit block.
32 CIArtifactMaxBlockSize = 8 << 20
33 // CIArtifactMinRetention is the shortest retention of an artifact.
34 CIArtifactMinRetention = 24 * time.Hour
35 // CIArtifactPendingMax is how long an upload may stay unfinished before the
36 // sweep deletes it, whatever its job does.
37 CIArtifactPendingMax = 24 * time.Hour
38 // DefaultCIArtifactQuota is the space the artifacts of one repository may
39 // use, uploads in progress included.
40 DefaultCIArtifactQuota = 10 << 30
41 // DefaultCIArtifactMaxSize is the largest artifact.
42 DefaultCIArtifactMaxSize = 5 << 30
43 // CIArtifactZip is the media type of an archive.
44 CIArtifactZip = "application/zip"
45)
46
47// CIArtifactStore identifies the storage of an artifact's contents.
48type CIArtifactStore string
49
50const (
51 // CIArtifactStoreLegacy is the pre-blob-store layout under ci/artifacts.
52 CIArtifactStoreLegacy CIArtifactStore = ""
53 // CIArtifactStoreBlob stores the contents through ports.BlobStore.
54 CIArtifactStoreBlob CIArtifactStore = "blob"
55)
56
57// CIArtifactState is where an artifact is in its upload.
58type CIArtifactState string
59
60const (
61 // CIArtifactPending is created, its blocks being uploaded.
62 CIArtifactPending CIArtifactState = "pending"
63 // CIArtifactCommitted has its content; not visible yet.
64 CIArtifactCommitted CIArtifactState = "committed"
65 // CIArtifactFinal is finalized: visible to the run and its owners.
66 CIArtifactFinal CIArtifactState = "final"
67)
68
69// CIArtifact is a file that a job of a run uploaded with
70// actions/upload-artifact. It belongs to the run: every job of the run, in any
71// attempt, sees the finalized ones.
72type CIArtifact struct {
73 // ID is global, monotonic and never reused.
74 ID int64
75 Repo RepoName
76 RunID int64
77 // Attempt is the attempt of the run of the job that uploads it.
78 Attempt int32
79 JobID string
80 // Assignment is the hash of the job token of the assignment that uploads
81 // it: a pending artifact belongs to that assignment only.
82 Assignment TokenHash
83 Name string
84 MediaType string
85 Size int64
86 // Digest is "sha256:<hex>" of the content, known from the commit.
87 Digest string
88 State CIArtifactState
89 Store CIArtifactStore
90 CreatedAt time.Time
91 ExpiresAt time.Time
92 // Charged is what the artifact counts against the quota of its
93 // repository: the blocks staged while pending, then the content.
94 Charged int64
95 // ListDigest identifies the block list of the commit.
96 ListDigest []byte
97 // Version is the store version of the record; 0 for a new one.
98 Version uint64
99}
100
101// BlobKey names the contents by immutable run and artifact ids only.
102func (a CIArtifact) BlobKey() string {
103 return fmt.Sprintf("ci/%020d/%020d", a.RunID, a.ID)
104}
105
106// BlockKey names a staged block; file is generated by the use case, not a user.
107func (a CIArtifact) BlockKey(file string) string { return a.BlobKey() + ".blocks/" + file }
108
109// Final reports whether the artifact is finalized.
110func (a CIArtifact) Final() bool { return a.State == CIArtifactFinal }
111
112// Visible reports whether the artifact is finalized and not expired at now.
113func (a CIArtifact) Visible(now time.Time) bool { return a.Final() && now.Before(a.ExpiresAt) }
114
115// UploadedBy reports whether the job, at its run attempt, uploads or uploaded
116// the artifact. It says nothing of the assignment.
117func (a CIArtifact) UploadedBy(j CIJob) bool {
118 return a.RunID == j.RunID && a.Attempt == j.RunAttempt && a.JobID == j.ID
119}
120
121// PendingOf reports whether the artifact is an unfinished upload of the
122// current assignment of the job.
123func (a CIArtifact) PendingOf(j CIJob) bool {
124 return !a.Final() && a.UploadedBy(j) && a.Assignment == j.TokenHash && j.HasToken()
125}
126
127// ParseArtifactName checks an artifact name: 1 to 256 characters of valid
128// UTF-8, without control characters, '"', ':', '<', '>', '|', '*', '?', '\'
129// or '/' (the characters @actions/artifact refuses). A name is never used as a
130// path: these rules are the toolkit's.
131func ParseArtifactName(name string) error {
132 if name == "" || !utf8.ValidString(name) || utf8.RuneCountInString(name) > CIArtifactMaxNameLen {
133 return fmt.Errorf("%w: an artifact name has 1 to %d characters of valid UTF-8", ErrInvalid, CIArtifactMaxNameLen)
134 }
135 for _, r := range name {
136 if unicode.IsControl(r) || strings.ContainsRune(`":<>|*?\/`, r) {
137 return fmt.Errorf("%w: an artifact name has no control character and none of \" : < > | * ? \\ /", ErrInvalid)
138 }
139 }
140 return nil
141}
142
143// ParseArtifactMediaType checks the media type of an artifact uploaded as is
144// and returns it, CIArtifactZip for an empty one: at most 255 printable ASCII
145// characters.
146func ParseArtifactMediaType(mt string) (string, error) {
147 if mt == "" {
148 return CIArtifactZip, nil
149 }
150 if len(mt) > CIArtifactMaxMediaTypeLen {
151 return "", fmt.Errorf("%w: a media type has at most %d characters", ErrInvalid, CIArtifactMaxMediaTypeLen)
152 }
153 for i := 0; i < len(mt); i++ {
154 if mt[i] < 0x20 || mt[i] > 0x7e {
155 return "", fmt.Errorf("%w: a media type has printable ASCII characters only", ErrInvalid)
156 }
157 }
158 return mt, nil
159}
160
161// ParseArtifactDigest checks a digest given by an uploader: "sha256:" and 64
162// hexadecimal characters, returned in lower case. Empty is no digest.
163func ParseArtifactDigest(d string) (string, error) {
164 if d == "" {
165 return "", nil
166 }
167 hex, ok := strings.CutPrefix(strings.ToLower(d), "sha256:")
168 if !ok || len(hex) != 64 || strings.Trim(hex, "0123456789abcdef") != "" {
169 return "", fmt.Errorf("%w: a digest is \"sha256:\" and 64 hexadecimal characters", ErrInvalid)
170 }
171 return "sha256:" + hex, nil
172}
173
174// CheckArtifactBlockID checks a block id: 1 to 64 bytes.
175func CheckArtifactBlockID(id []byte) error {
176 if len(id) == 0 || len(id) > CIArtifactMaxBlockIDLen {
177 return fmt.Errorf("%w: a block id has 1 to %d bytes", ErrInvalid, CIArtifactMaxBlockIDLen)
178 }
179 return nil
180}
181
182// ArtifactExpiry is the expiry an artifact gets: the one asked for (zero:
183// none) kept between one day from now and the longest retention, now plus the
184// retention without one. A retention below one day counts as one day.
185func ArtifactExpiry(asked, now time.Time, retention time.Duration) time.Time {
186 retention = max(retention, CIArtifactMinRetention)
187 switch {
188 case asked.IsZero():
189 return now.Add(retention)
190 case asked.Before(now.Add(CIArtifactMinRetention)):
191 return now.Add(CIArtifactMinRetention)
192 case asked.After(now.Add(retention)):
193 return now.Add(retention)
194 }
195 return asked
196}