1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package ports
4
5import (
6 "context"
7 "io"
8 "strings"
9 "time"
10)
11
12// BlobStore keeps the contents of release assets and CI artifacts, never
13// their metadata. Keys are built by the use cases from ids only ("a/b/c",
14// ASCII, no empty segment, no ".", no ".." segment, no leading "/", ≤ 512
15// bytes); an adapter rejects any other key with domain.ErrInvalid.
16// *blobdisk.Store and *blobs3.Store implement it.
17type BlobStore interface {
18 Put(ctx context.Context, key string, r io.Reader, size int64) (BlobInfo, error) // size ≥ 0 required; replaces; a failure leaves no object
19 Open(ctx context.Context, key string, offset, length int64) (io.ReadCloser, BlobInfo, error) // length < 0: to the end; offset ≥ size → domain.ErrOutOfRange
20 Stat(ctx context.Context, key string) (BlobInfo, error)
21 Delete(ctx context.Context, key string) error // absent: nil
22 List(ctx context.Context, prefix string, fn func(BlobInfo) error) error // lexicographic; fn error stops and is returned
23 NewMultipart(ctx context.Context, key string) (MultipartUpload, error)
24 ListMultipart(ctx context.Context, prefix string, fn func(MultipartInfo) error) error
25 AbortMultipart(ctx context.Context, key, id string) error // for GC of uploads listed by ListMultipart
26 Check(ctx context.Context) error // startup probe; never creates anything
27}
28type BlobInfo struct {
29 Key string
30 Size int64
31 Digest string
32 ModTime time.Time
33} // Digest "sha256:<hex>" from Put, "" from Stat/List/Open on S3
34type MultipartInfo struct {
35 Key, ID string
36 Started time.Time
37}
38
39// MultipartUpload uploads one object in parts of BlobMinPartSize or more
40// (the last may be smaller), at most BlobMaxParts, numbered from 1 in the
41// order given to Complete. Abort discards everything; Complete or Abort
42// must be called exactly once.
43type MultipartUpload interface {
44 ID() string
45 UploadPart(ctx context.Context, n int, r io.Reader, size int64) error
46 Complete(ctx context.Context) (BlobInfo, error) // parts in number order; Digest "" (the caller hashes)
47 Abort(ctx context.Context) error
48}
49
50const (
51 BlobMinPartSize = 5 << 20
52 BlobPartSize = 8 << 20
53 BlobMaxParts = 10_000
54)
55
56// ValidBlobKey reports whether key follows the rule above.
57func ValidBlobKey(key string) bool {
58 if len(key) == 0 || len(key) > 512 {
59 return false
60 }
61 for _, c := range []byte(key) {
62 if c > 127 {
63 return false
64 }
65 }
66 for _, segment := range strings.Split(key, "/") {
67 if segment == "" || segment == "." || segment == ".." {
68 return false
69 }
70 }
71 return true
72}