+291
api/tangled/cbor_gen.go
+291
api/tangled/cbor_gen.go
···
8
8
"math"
9
9
"sort"
10
10
11
+
util "github.com/bluesky-social/indigo/lex/util"
11
12
cid "github.com/ipfs/go-cid"
12
13
cbg "github.com/whyrusleeping/cbor-gen"
13
14
xerrors "golang.org/x/xerrors"
···
3098
3099
3099
3100
return nil
3100
3101
}
3102
+
func (t *RepoArtifact) MarshalCBOR(w io.Writer) error {
3103
+
if t == nil {
3104
+
_, err := w.Write(cbg.CborNull)
3105
+
return err
3106
+
}
3107
+
3108
+
cw := cbg.NewCborWriter(w)
3109
+
fieldCount := 6
3110
+
3111
+
if t.Tag == nil {
3112
+
fieldCount--
3113
+
}
3114
+
3115
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
3116
+
return err
3117
+
}
3118
+
3119
+
// t.Tag (util.LexBytes) (slice)
3120
+
if t.Tag != nil {
3121
+
3122
+
if len("tag") > 1000000 {
3123
+
return xerrors.Errorf("Value in field \"tag\" was too long")
3124
+
}
3125
+
3126
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("tag"))); err != nil {
3127
+
return err
3128
+
}
3129
+
if _, err := cw.WriteString(string("tag")); err != nil {
3130
+
return err
3131
+
}
3132
+
3133
+
if len(t.Tag) > 2097152 {
3134
+
return xerrors.Errorf("Byte array in field t.Tag was too long")
3135
+
}
3136
+
3137
+
if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Tag))); err != nil {
3138
+
return err
3139
+
}
3140
+
3141
+
if _, err := cw.Write(t.Tag); err != nil {
3142
+
return err
3143
+
}
3144
+
3145
+
}
3146
+
3147
+
// t.Name (string) (string)
3148
+
if len("name") > 1000000 {
3149
+
return xerrors.Errorf("Value in field \"name\" was too long")
3150
+
}
3151
+
3152
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil {
3153
+
return err
3154
+
}
3155
+
if _, err := cw.WriteString(string("name")); err != nil {
3156
+
return err
3157
+
}
3158
+
3159
+
if len(t.Name) > 1000000 {
3160
+
return xerrors.Errorf("Value in field t.Name was too long")
3161
+
}
3162
+
3163
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil {
3164
+
return err
3165
+
}
3166
+
if _, err := cw.WriteString(string(t.Name)); err != nil {
3167
+
return err
3168
+
}
3169
+
3170
+
// t.Repo (string) (string)
3171
+
if len("repo") > 1000000 {
3172
+
return xerrors.Errorf("Value in field \"repo\" was too long")
3173
+
}
3174
+
3175
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil {
3176
+
return err
3177
+
}
3178
+
if _, err := cw.WriteString(string("repo")); err != nil {
3179
+
return err
3180
+
}
3181
+
3182
+
if len(t.Repo) > 1000000 {
3183
+
return xerrors.Errorf("Value in field t.Repo was too long")
3184
+
}
3185
+
3186
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil {
3187
+
return err
3188
+
}
3189
+
if _, err := cw.WriteString(string(t.Repo)); err != nil {
3190
+
return err
3191
+
}
3192
+
3193
+
// t.LexiconTypeID (string) (string)
3194
+
if len("$type") > 1000000 {
3195
+
return xerrors.Errorf("Value in field \"$type\" was too long")
3196
+
}
3197
+
3198
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
3199
+
return err
3200
+
}
3201
+
if _, err := cw.WriteString(string("$type")); err != nil {
3202
+
return err
3203
+
}
3204
+
3205
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.artifact"))); err != nil {
3206
+
return err
3207
+
}
3208
+
if _, err := cw.WriteString(string("sh.tangled.repo.artifact")); err != nil {
3209
+
return err
3210
+
}
3211
+
3212
+
// t.Artifact (util.LexBlob) (struct)
3213
+
if len("artifact") > 1000000 {
3214
+
return xerrors.Errorf("Value in field \"artifact\" was too long")
3215
+
}
3216
+
3217
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artifact"))); err != nil {
3218
+
return err
3219
+
}
3220
+
if _, err := cw.WriteString(string("artifact")); err != nil {
3221
+
return err
3222
+
}
3223
+
3224
+
if err := t.Artifact.MarshalCBOR(cw); err != nil {
3225
+
return err
3226
+
}
3227
+
3228
+
// t.CreatedAt (string) (string)
3229
+
if len("createdAt") > 1000000 {
3230
+
return xerrors.Errorf("Value in field \"createdAt\" was too long")
3231
+
}
3232
+
3233
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
3234
+
return err
3235
+
}
3236
+
if _, err := cw.WriteString(string("createdAt")); err != nil {
3237
+
return err
3238
+
}
3239
+
3240
+
if len(t.CreatedAt) > 1000000 {
3241
+
return xerrors.Errorf("Value in field t.CreatedAt was too long")
3242
+
}
3243
+
3244
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil {
3245
+
return err
3246
+
}
3247
+
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
3248
+
return err
3249
+
}
3250
+
return nil
3251
+
}
3252
+
3253
+
func (t *RepoArtifact) UnmarshalCBOR(r io.Reader) (err error) {
3254
+
*t = RepoArtifact{}
3255
+
3256
+
cr := cbg.NewCborReader(r)
3257
+
3258
+
maj, extra, err := cr.ReadHeader()
3259
+
if err != nil {
3260
+
return err
3261
+
}
3262
+
defer func() {
3263
+
if err == io.EOF {
3264
+
err = io.ErrUnexpectedEOF
3265
+
}
3266
+
}()
3267
+
3268
+
if maj != cbg.MajMap {
3269
+
return fmt.Errorf("cbor input should be of type map")
3270
+
}
3271
+
3272
+
if extra > cbg.MaxLength {
3273
+
return fmt.Errorf("RepoArtifact: map struct too large (%d)", extra)
3274
+
}
3275
+
3276
+
n := extra
3277
+
3278
+
nameBuf := make([]byte, 9)
3279
+
for i := uint64(0); i < n; i++ {
3280
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
3281
+
if err != nil {
3282
+
return err
3283
+
}
3284
+
3285
+
if !ok {
3286
+
// Field doesn't exist on this type, so ignore it
3287
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
3288
+
return err
3289
+
}
3290
+
continue
3291
+
}
3292
+
3293
+
switch string(nameBuf[:nameLen]) {
3294
+
// t.Tag (util.LexBytes) (slice)
3295
+
case "tag":
3296
+
3297
+
maj, extra, err = cr.ReadHeader()
3298
+
if err != nil {
3299
+
return err
3300
+
}
3301
+
3302
+
if extra > 2097152 {
3303
+
return fmt.Errorf("t.Tag: byte array too large (%d)", extra)
3304
+
}
3305
+
if maj != cbg.MajByteString {
3306
+
return fmt.Errorf("expected byte array")
3307
+
}
3308
+
3309
+
if extra > 0 {
3310
+
t.Tag = make([]uint8, extra)
3311
+
}
3312
+
3313
+
if _, err := io.ReadFull(cr, t.Tag); err != nil {
3314
+
return err
3315
+
}
3316
+
3317
+
// t.Name (string) (string)
3318
+
case "name":
3319
+
3320
+
{
3321
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3322
+
if err != nil {
3323
+
return err
3324
+
}
3325
+
3326
+
t.Name = string(sval)
3327
+
}
3328
+
// t.Repo (string) (string)
3329
+
case "repo":
3330
+
3331
+
{
3332
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3333
+
if err != nil {
3334
+
return err
3335
+
}
3336
+
3337
+
t.Repo = string(sval)
3338
+
}
3339
+
// t.LexiconTypeID (string) (string)
3340
+
case "$type":
3341
+
3342
+
{
3343
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3344
+
if err != nil {
3345
+
return err
3346
+
}
3347
+
3348
+
t.LexiconTypeID = string(sval)
3349
+
}
3350
+
// t.Artifact (util.LexBlob) (struct)
3351
+
case "artifact":
3352
+
3353
+
{
3354
+
3355
+
b, err := cr.ReadByte()
3356
+
if err != nil {
3357
+
return err
3358
+
}
3359
+
if b != cbg.CborNull[0] {
3360
+
if err := cr.UnreadByte(); err != nil {
3361
+
return err
3362
+
}
3363
+
t.Artifact = new(util.LexBlob)
3364
+
if err := t.Artifact.UnmarshalCBOR(cr); err != nil {
3365
+
return xerrors.Errorf("unmarshaling t.Artifact pointer: %w", err)
3366
+
}
3367
+
}
3368
+
3369
+
}
3370
+
// t.CreatedAt (string) (string)
3371
+
case "createdAt":
3372
+
3373
+
{
3374
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3375
+
if err != nil {
3376
+
return err
3377
+
}
3378
+
3379
+
t.CreatedAt = string(sval)
3380
+
}
3381
+
3382
+
default:
3383
+
// Field doesn't exist on this type, so ignore it
3384
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
3385
+
return err
3386
+
}
3387
+
}
3388
+
}
3389
+
3390
+
return nil
3391
+
}
+31
api/tangled/repoartifact.go
+31
api/tangled/repoartifact.go
···
1
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
+
3
+
package tangled
4
+
5
+
// schema: sh.tangled.repo.artifact
6
+
7
+
import (
8
+
"github.com/bluesky-social/indigo/lex/util"
9
+
)
10
+
11
+
const (
12
+
RepoArtifactNSID = "sh.tangled.repo.artifact"
13
+
)
14
+
15
+
func init() {
16
+
util.RegisterType("sh.tangled.repo.artifact", &RepoArtifact{})
17
+
} //
18
+
// RECORDTYPE: RepoArtifact
19
+
type RepoArtifact struct {
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.artifact" cborgen:"$type,const=sh.tangled.repo.artifact"`
21
+
// artifact: the artifact
22
+
Artifact *util.LexBlob `json:"artifact" cborgen:"artifact"`
23
+
// createdAt: time of creation of this artifact
24
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
25
+
// name: name of the artifact
26
+
Name string `json:"name" cborgen:"name"`
27
+
// repo: repo that this artifact is being uploaded to
28
+
Repo string `json:"repo" cborgen:"repo"`
29
+
// tag: hash of the tag object that this artifact is attached to (only annotated tags are supported)
30
+
Tag util.LexBytes `json:"tag,omitempty" cborgen:"tag,omitempty"`
31
+
}
+166
appview/db/artifact.go
+166
appview/db/artifact.go
···
1
+
package db
2
+
3
+
import (
4
+
"fmt"
5
+
"strings"
6
+
"time"
7
+
8
+
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
"github.com/go-git/go-git/v5/plumbing"
10
+
"github.com/ipfs/go-cid"
11
+
"tangled.sh/tangled.sh/core/api/tangled"
12
+
)
13
+
14
+
type Artifact struct {
15
+
Id uint64
16
+
Did string
17
+
Rkey string
18
+
19
+
RepoAt syntax.ATURI
20
+
Tag plumbing.Hash
21
+
CreatedAt time.Time
22
+
23
+
BlobCid cid.Cid
24
+
Name string
25
+
Size uint64
26
+
Mimetype string
27
+
}
28
+
29
+
func (a *Artifact) ArtifactAt() syntax.ATURI {
30
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", a.Did, tangled.RepoPullNSID, a.Rkey))
31
+
}
32
+
33
+
func AddArtifact(e Execer, artifact Artifact) error {
34
+
_, err := e.Exec(
35
+
`insert or ignore into artifacts (
36
+
did,
37
+
rkey,
38
+
repo_at,
39
+
tag,
40
+
created,
41
+
blob_cid,
42
+
name,
43
+
size,
44
+
mimetype
45
+
)
46
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
47
+
artifact.Did,
48
+
artifact.Rkey,
49
+
artifact.RepoAt,
50
+
artifact.Tag[:],
51
+
artifact.CreatedAt.Format(time.RFC3339),
52
+
artifact.BlobCid.String(),
53
+
artifact.Name,
54
+
artifact.Size,
55
+
artifact.Mimetype,
56
+
)
57
+
return err
58
+
}
59
+
60
+
type Filter struct {
61
+
key string
62
+
arg any
63
+
}
64
+
65
+
func NewFilter(key string, arg any) Filter {
66
+
return Filter{
67
+
key: key,
68
+
arg: arg,
69
+
}
70
+
}
71
+
72
+
func (f Filter) Condition() string {
73
+
return fmt.Sprintf("%s = ?", f.key)
74
+
}
75
+
76
+
func GetArtifact(e Execer, filters ...Filter) ([]Artifact, error) {
77
+
var artifacts []Artifact
78
+
79
+
var conditions []string
80
+
var args []any
81
+
for _, filter := range filters {
82
+
conditions = append(conditions, filter.Condition())
83
+
args = append(args, filter.arg)
84
+
}
85
+
86
+
whereClause := ""
87
+
if conditions != nil {
88
+
whereClause = " where " + strings.Join(conditions, " and ")
89
+
}
90
+
91
+
query := fmt.Sprintf(`select
92
+
did,
93
+
rkey,
94
+
repo_at,
95
+
tag,
96
+
created,
97
+
blob_cid,
98
+
name,
99
+
size,
100
+
mimetype
101
+
from artifacts %s`,
102
+
whereClause,
103
+
)
104
+
105
+
rows, err := e.Query(query, args...)
106
+
107
+
if err != nil {
108
+
return nil, err
109
+
}
110
+
defer rows.Close()
111
+
112
+
for rows.Next() {
113
+
var artifact Artifact
114
+
var createdAt string
115
+
var tag []byte
116
+
var blobCid string
117
+
118
+
if err := rows.Scan(
119
+
&artifact.Did,
120
+
&artifact.Rkey,
121
+
&artifact.RepoAt,
122
+
&tag,
123
+
&createdAt,
124
+
&blobCid,
125
+
&artifact.Name,
126
+
&artifact.Size,
127
+
&artifact.Mimetype,
128
+
); err != nil {
129
+
return nil, err
130
+
}
131
+
132
+
artifact.CreatedAt, err = time.Parse(time.RFC3339, createdAt)
133
+
if err != nil {
134
+
artifact.CreatedAt = time.Now()
135
+
}
136
+
artifact.Tag = plumbing.Hash(tag)
137
+
artifact.BlobCid = cid.MustParse(blobCid)
138
+
139
+
artifacts = append(artifacts, artifact)
140
+
}
141
+
142
+
if err := rows.Err(); err != nil {
143
+
return nil, err
144
+
}
145
+
146
+
return artifacts, nil
147
+
}
148
+
149
+
func RemoveArtifact(e Execer, filters ...Filter) error {
150
+
var conditions []string
151
+
var args []any
152
+
for _, filter := range filters {
153
+
conditions = append(conditions, filter.Condition())
154
+
args = append(args, filter.arg)
155
+
}
156
+
157
+
whereClause := ""
158
+
if conditions != nil {
159
+
whereClause = " where " + strings.Join(conditions, " and ")
160
+
}
161
+
162
+
query := fmt.Sprintf(`delete from artifacts %s`, whereClause)
163
+
164
+
_, err := e.Exec(query, args...)
165
+
return err
166
+
}
+23
appview/db/db.go
+23
appview/db/db.go
···
208
208
unique(did, email)
209
209
);
210
210
211
+
create table if not exists artifacts (
212
+
-- id
213
+
id integer primary key autoincrement,
214
+
did text not null,
215
+
rkey text not null,
216
+
217
+
-- meta
218
+
repo_at text not null,
219
+
tag binary(20) not null,
220
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
221
+
222
+
-- data
223
+
blob_cid text not null,
224
+
name text not null,
225
+
size integer not null default 0,
226
+
mimetype string not null default "*/*",
227
+
228
+
-- constraints
229
+
unique(did, rkey), -- record must be unique
230
+
unique(repo_at, tag, name), -- for a given tag object, each file must be unique
231
+
foreign key (repo_at) references repos(at_uri) on delete cascade
232
+
);
233
+
211
234
create table if not exists migrations (
212
235
id integer primary key autoincrement,
213
236
name text unique
+1
-1
appview/db/pulls.go
+1
-1
appview/db/pulls.go
···
10
10
11
11
"github.com/bluekeyes/go-gitdiff/gitdiff"
12
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
-
tangled "tangled.sh/tangled.sh/core/api/tangled"
13
+
"tangled.sh/tangled.sh/core/api/tangled"
14
14
"tangled.sh/tangled.sh/core/patchutil"
15
15
"tangled.sh/tangled.sh/core/types"
16
16
)
+13
appview/pages/pages.go
+13
appview/pages/pages.go
···
28
28
"github.com/alecthomas/chroma/v2/lexers"
29
29
"github.com/alecthomas/chroma/v2/styles"
30
30
"github.com/bluesky-social/indigo/atproto/syntax"
31
+
"github.com/go-git/go-git/v5/plumbing"
31
32
"github.com/go-git/go-git/v5/plumbing/object"
32
33
"github.com/microcosm-cc/bluemonday"
33
34
)
···
484
485
RepoInfo repoinfo.RepoInfo
485
486
Active string
486
487
types.RepoTagsResponse
488
+
ArtifactMap map[plumbing.Hash][]db.Artifact
489
+
DanglingArtifacts []db.Artifact
487
490
}
488
491
489
492
func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
490
493
params.Active = "overview"
491
494
return p.executeRepo("repo/tags", w, params)
495
+
}
496
+
497
+
type RepoArtifactParams struct {
498
+
LoggedInUser *auth.User
499
+
RepoInfo RepoInfo
500
+
Artifact db.Artifact
501
+
}
502
+
503
+
func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error {
504
+
return p.executePlain("repo/fragments/artifact", w, params)
492
505
}
493
506
494
507
type RepoBlobParams struct {
+34
appview/pages/templates/repo/fragments/artifact.html
+34
appview/pages/templates/repo/fragments/artifact.html
···
1
+
{{ define "repo/fragments/artifact" }}
2
+
{{ $unique := .Artifact.BlobCid.String }}
3
+
<div id="artifact-{{ $unique }}" class="flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-700">
4
+
<div id="left-side" class="flex items-center gap-2 min-w-0 max-w-[60%]">
5
+
{{ i "box" "w-4 h-4" }}
6
+
<a href="/{{ .RepoInfo.FullName }}/tags/{{ .Artifact.Tag.String }}/download/{{ .Artifact.Name | urlquery }}" class="no-underline hover:no-underline">
7
+
{{ .Artifact.Name }}
8
+
</a>
9
+
<span class="text-gray-500 dark:text-gray-400 pl-2">{{ byteFmt .Artifact.Size }}</span>
10
+
</div>
11
+
12
+
<div id="right-side" class="text-gray-500 dark:text-gray-400 flex items-center flex-shrink-0 gap-2">
13
+
<span title="{{ longTimeFmt .Artifact.CreatedAt }}" class="hidden md:inline">{{ timeFmt .Artifact.CreatedAt }}</span>
14
+
<span title="{{ longTimeFmt .Artifact.CreatedAt }}" class=" md:hidden">{{ shortTimeFmt .Artifact.CreatedAt }}</span>
15
+
16
+
<span class="select-none after:content-['·'] hidden md:inline"></span>
17
+
<span class="truncate max-w-[100px] hidden md:inline">{{ .Artifact.Mimetype }}</span>
18
+
19
+
{{ if and (.LoggedInUser) (eq .LoggedInUser.Did .Artifact.Did) }}
20
+
<button
21
+
id="delete-{{ $unique }}"
22
+
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2"
23
+
title="Delete artifact"
24
+
hx-delete="/{{ .RepoInfo.FullName }}/tags/{{ .Artifact.Tag.String }}/{{ .Artifact.Name | urlquery }}"
25
+
hx-swap="outerHTML"
26
+
hx-target="#artifact-{{ $unique }}"
27
+
hx-disabled-elt="#delete-{{ $unique }}"
28
+
hx-confirm="Are you sure you want to delete the artifact '{{ .Artifact.Name }}'?">
29
+
{{ i "trash-2" "w-4 h-4" }}
30
+
</button>
31
+
{{ end }}
32
+
</div>
33
+
</div>
34
+
{{ end }}
+280
appview/state/artifact.go
+280
appview/state/artifact.go
···
1
+
package state
2
+
3
+
import (
4
+
"fmt"
5
+
"log"
6
+
"net/http"
7
+
"time"
8
+
9
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
10
+
lexutil "github.com/bluesky-social/indigo/lex/util"
11
+
"github.com/dustin/go-humanize"
12
+
"github.com/go-chi/chi/v5"
13
+
"github.com/go-git/go-git/v5/plumbing"
14
+
"github.com/ipfs/go-cid"
15
+
"tangled.sh/tangled.sh/core/api/tangled"
16
+
"tangled.sh/tangled.sh/core/appview"
17
+
"tangled.sh/tangled.sh/core/appview/db"
18
+
"tangled.sh/tangled.sh/core/appview/pages"
19
+
"tangled.sh/tangled.sh/core/types"
20
+
)
21
+
22
+
// TODO: proper statuses here on early exit
23
+
func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) {
24
+
user := s.auth.GetUser(r)
25
+
tagParam := chi.URLParam(r, "tag")
26
+
f, err := fullyResolvedRepo(r)
27
+
if err != nil {
28
+
log.Println("failed to get repo and knot", err)
29
+
s.pages.Notice(w, "upload", "failed to upload artifact, error in repo resolution")
30
+
return
31
+
}
32
+
33
+
tag, err := s.resolveTag(f, tagParam)
34
+
if err != nil {
35
+
log.Println("failed to resolve tag", err)
36
+
s.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
37
+
return
38
+
}
39
+
40
+
file, handler, err := r.FormFile("artifact")
41
+
if err != nil {
42
+
log.Println("failed to upload artifact", err)
43
+
s.pages.Notice(w, "upload", "failed to upload artifact")
44
+
return
45
+
}
46
+
defer file.Close()
47
+
48
+
client, _ := s.auth.AuthorizedClient(r)
49
+
50
+
uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file)
51
+
if err != nil {
52
+
log.Println("failed to upload blob", err)
53
+
s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.")
54
+
return
55
+
}
56
+
57
+
log.Println("uploaded blob", humanize.Bytes(uint64(uploadBlobResp.Blob.Size)), uploadBlobResp.Blob.Ref.String())
58
+
59
+
rkey := appview.TID()
60
+
createdAt := time.Now()
61
+
62
+
putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
63
+
Collection: tangled.RepoArtifactNSID,
64
+
Repo: user.Did,
65
+
Rkey: rkey,
66
+
Record: &lexutil.LexiconTypeDecoder{
67
+
Val: &tangled.RepoArtifact{
68
+
Artifact: uploadBlobResp.Blob,
69
+
CreatedAt: createdAt.Format(time.RFC3339),
70
+
Name: handler.Filename,
71
+
Repo: f.RepoAt.String(),
72
+
Tag: tag.Tag.Hash[:],
73
+
},
74
+
},
75
+
})
76
+
if err != nil {
77
+
log.Println("failed to create record", err)
78
+
s.pages.Notice(w, "upload", "Failed to create artifact record. Try again later.")
79
+
return
80
+
}
81
+
82
+
log.Println(putRecordResp.Uri)
83
+
84
+
tx, err := s.db.BeginTx(r.Context(), nil)
85
+
if err != nil {
86
+
log.Println("failed to start tx")
87
+
s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
88
+
return
89
+
}
90
+
defer tx.Rollback()
91
+
92
+
artifact := db.Artifact{
93
+
Did: user.Did,
94
+
Rkey: rkey,
95
+
RepoAt: f.RepoAt,
96
+
Tag: tag.Tag.Hash,
97
+
CreatedAt: createdAt,
98
+
BlobCid: cid.Cid(uploadBlobResp.Blob.Ref),
99
+
Name: handler.Filename,
100
+
Size: uint64(uploadBlobResp.Blob.Size),
101
+
Mimetype: uploadBlobResp.Blob.MimeType,
102
+
}
103
+
104
+
err = db.AddArtifact(tx, artifact)
105
+
if err != nil {
106
+
log.Println("failed to add artifact record to db", err)
107
+
s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
108
+
return
109
+
}
110
+
111
+
err = tx.Commit()
112
+
if err != nil {
113
+
log.Println("failed to add artifact record to db")
114
+
s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
115
+
return
116
+
}
117
+
118
+
s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
119
+
LoggedInUser: user,
120
+
RepoInfo: f.RepoInfo(s, user),
121
+
Artifact: artifact,
122
+
})
123
+
}
124
+
125
+
// TODO: proper statuses here on early exit
126
+
func (s *State) DownloadArtifact(w http.ResponseWriter, r *http.Request) {
127
+
tagParam := chi.URLParam(r, "tag")
128
+
filename := chi.URLParam(r, "file")
129
+
f, err := fullyResolvedRepo(r)
130
+
if err != nil {
131
+
log.Println("failed to get repo and knot", err)
132
+
return
133
+
}
134
+
135
+
tag, err := s.resolveTag(f, tagParam)
136
+
if err != nil {
137
+
log.Println("failed to resolve tag", err)
138
+
s.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
139
+
return
140
+
}
141
+
142
+
client, _ := s.auth.AuthorizedClient(r)
143
+
144
+
artifacts, err := db.GetArtifact(
145
+
s.db,
146
+
db.NewFilter("repo_at", f.RepoAt),
147
+
db.NewFilter("tag", tag.Tag.Hash[:]),
148
+
db.NewFilter("name", filename),
149
+
)
150
+
if err != nil {
151
+
log.Println("failed to get artifacts", err)
152
+
return
153
+
}
154
+
if len(artifacts) != 1 {
155
+
log.Printf("too many or too little artifacts found")
156
+
return
157
+
}
158
+
159
+
artifact := artifacts[0]
160
+
161
+
getBlobResp, err := comatproto.SyncGetBlob(r.Context(), client, artifact.BlobCid.String(), artifact.Did)
162
+
if err != nil {
163
+
log.Println("failed to get blob from pds", err)
164
+
return
165
+
}
166
+
167
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
168
+
w.Write(getBlobResp)
169
+
}
170
+
171
+
// TODO: proper statuses here on early exit
172
+
func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
173
+
user := s.auth.GetUser(r)
174
+
tagParam := chi.URLParam(r, "tag")
175
+
filename := chi.URLParam(r, "file")
176
+
f, err := fullyResolvedRepo(r)
177
+
if err != nil {
178
+
log.Println("failed to get repo and knot", err)
179
+
return
180
+
}
181
+
182
+
client, _ := s.auth.AuthorizedClient(r)
183
+
184
+
tag := plumbing.NewHash(tagParam)
185
+
186
+
artifacts, err := db.GetArtifact(
187
+
s.db,
188
+
db.NewFilter("repo_at", f.RepoAt),
189
+
db.NewFilter("tag", tag[:]),
190
+
db.NewFilter("name", filename),
191
+
)
192
+
if err != nil {
193
+
log.Println("failed to get artifacts", err)
194
+
s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
195
+
return
196
+
}
197
+
if len(artifacts) != 1 {
198
+
s.pages.Notice(w, "remove", "Unable to find artifact.")
199
+
return
200
+
}
201
+
202
+
artifact := artifacts[0]
203
+
204
+
if user.Did != artifact.Did {
205
+
log.Println("user not authorized to delete artifact", err)
206
+
s.pages.Notice(w, "remove", "Unauthorized deletion of artifact.")
207
+
return
208
+
}
209
+
210
+
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
211
+
Collection: tangled.RepoArtifactNSID,
212
+
Repo: user.Did,
213
+
Rkey: artifact.Rkey,
214
+
})
215
+
if err != nil {
216
+
log.Println("failed to get blob from pds", err)
217
+
s.pages.Notice(w, "remove", "Failed to remove blob from PDS.")
218
+
return
219
+
}
220
+
221
+
tx, err := s.db.BeginTx(r.Context(), nil)
222
+
if err != nil {
223
+
log.Println("failed to start tx")
224
+
s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
225
+
return
226
+
}
227
+
defer tx.Rollback()
228
+
229
+
err = db.RemoveArtifact(tx,
230
+
db.NewFilter("repo_at", f.RepoAt),
231
+
db.NewFilter("tag", artifact.Tag[:]),
232
+
db.NewFilter("name", filename),
233
+
)
234
+
if err != nil {
235
+
log.Println("failed to remove artifact record from db", err)
236
+
s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
237
+
return
238
+
}
239
+
240
+
err = tx.Commit()
241
+
if err != nil {
242
+
log.Println("failed to remove artifact record from db")
243
+
s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
244
+
return
245
+
}
246
+
247
+
w.Write([]byte{})
248
+
}
249
+
250
+
func (s *State) resolveTag(f *FullyResolvedRepo, tagParam string) (*types.TagReference, error) {
251
+
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
252
+
if err != nil {
253
+
return nil, err
254
+
}
255
+
256
+
result, err := us.Tags(f.OwnerDid(), f.RepoName)
257
+
if err != nil {
258
+
log.Println("failed to reach knotserver", err)
259
+
return nil, err
260
+
}
261
+
262
+
var tag *types.TagReference
263
+
for _, t := range result.Tags {
264
+
if t.Tag != nil {
265
+
if t.Reference.Name == tagParam || t.Reference.Hash == tagParam {
266
+
tag = t
267
+
}
268
+
}
269
+
}
270
+
271
+
if tag == nil {
272
+
return nil, fmt.Errorf("invalid tag, only annotated tags are supported for artifacts")
273
+
}
274
+
275
+
if tag.Tag.Target.IsZero() {
276
+
return nil, fmt.Errorf("invalid tag, only annotated tags are supported for artifacts")
277
+
}
278
+
279
+
return tag, nil
280
+
}
+1
-1
appview/state/follow.go
+1
-1
appview/state/follow.go
···
7
7
8
8
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
9
lexutil "github.com/bluesky-social/indigo/lex/util"
10
-
tangled "tangled.sh/tangled.sh/core/api/tangled"
10
+
"tangled.sh/tangled.sh/core/api/tangled"
11
11
"tangled.sh/tangled.sh/core/appview"
12
12
"tangled.sh/tangled.sh/core/appview/db"
13
13
"tangled.sh/tangled.sh/core/appview/pages"
+70
appview/state/jetstream.go
+70
appview/state/jetstream.go
···
1
+
package state
2
+
3
+
import (
4
+
"context"
5
+
"encoding/json"
6
+
"fmt"
7
+
"log"
8
+
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
"github.com/bluesky-social/jetstream/pkg/models"
11
+
"tangled.sh/tangled.sh/core/api/tangled"
12
+
"tangled.sh/tangled.sh/core/appview/db"
13
+
)
14
+
15
+
type Ingester func(ctx context.Context, e *models.Event) error
16
+
17
+
func jetstreamIngester(d db.DbWrapper) Ingester {
18
+
return func(ctx context.Context, e *models.Event) error {
19
+
var err error
20
+
defer func() {
21
+
eventTime := e.TimeUS
22
+
lastTimeUs := eventTime + 1
23
+
if err := d.SaveLastTimeUs(lastTimeUs); err != nil {
24
+
err = fmt.Errorf("(deferred) failed to save last time us: %w", err)
25
+
}
26
+
}()
27
+
28
+
if e.Kind != models.EventKindCommit {
29
+
return nil
30
+
}
31
+
32
+
did := e.Did
33
+
raw := json.RawMessage(e.Commit.Record)
34
+
35
+
switch e.Commit.Collection {
36
+
case tangled.GraphFollowNSID:
37
+
record := tangled.GraphFollow{}
38
+
err := json.Unmarshal(raw, &record)
39
+
if err != nil {
40
+
log.Println("invalid record")
41
+
return err
42
+
}
43
+
err = db.AddFollow(d, did, record.Subject, e.Commit.RKey)
44
+
if err != nil {
45
+
return fmt.Errorf("failed to add follow to db: %w", err)
46
+
}
47
+
case tangled.FeedStarNSID:
48
+
record := tangled.FeedStar{}
49
+
err := json.Unmarshal(raw, &record)
50
+
if err != nil {
51
+
log.Println("invalid record")
52
+
return err
53
+
}
54
+
55
+
subjectUri, err := syntax.ParseATURI(record.Subject)
56
+
57
+
if err != nil {
58
+
log.Println("invalid record")
59
+
return err
60
+
}
61
+
62
+
err = db.AddStar(d, did, subjectUri, e.Commit.RKey)
63
+
if err != nil {
64
+
return fmt.Errorf("failed to add follow to db: %w", err)
65
+
}
66
+
}
67
+
68
+
return err
69
+
}
70
+
}
+36
-31
appview/state/repo.go
+36
-31
appview/state/repo.go
···
16
16
"strings"
17
17
"time"
18
18
19
-
"github.com/bluesky-social/indigo/atproto/data"
20
-
"github.com/bluesky-social/indigo/atproto/identity"
21
-
"github.com/bluesky-social/indigo/atproto/syntax"
22
-
securejoin "github.com/cyphar/filepath-securejoin"
23
-
"github.com/go-chi/chi/v5"
24
-
"github.com/go-git/go-git/v5/plumbing"
25
19
"tangled.sh/tangled.sh/core/api/tangled"
26
20
"tangled.sh/tangled.sh/core/appview"
27
21
"tangled.sh/tangled.sh/core/appview/auth"
···
31
25
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
32
26
"tangled.sh/tangled.sh/core/appview/pagination"
33
27
"tangled.sh/tangled.sh/core/types"
28
+
29
+
"github.com/bluesky-social/indigo/atproto/data"
30
+
"github.com/bluesky-social/indigo/atproto/identity"
31
+
"github.com/bluesky-social/indigo/atproto/syntax"
32
+
securejoin "github.com/cyphar/filepath-securejoin"
33
+
"github.com/go-chi/chi/v5"
34
+
"github.com/go-git/go-git/v5/plumbing"
34
35
35
36
comatproto "github.com/bluesky-social/indigo/api/atproto"
36
37
lexutil "github.com/bluesky-social/indigo/lex/util"
···
171
172
return
172
173
}
173
174
174
-
resp, err = us.Tags(f.OwnerDid(), f.RepoName)
175
+
result, err := us.Tags(f.OwnerDid(), f.RepoName)
175
176
if err != nil {
176
177
log.Println("failed to reach knotserver", err)
177
-
return
178
-
}
179
-
180
-
body, err = io.ReadAll(resp.Body)
181
-
if err != nil {
182
-
log.Printf("error reading response body: %v", err)
183
-
return
184
-
}
185
-
186
-
var result types.RepoTagsResponse
187
-
err = json.Unmarshal(body, &result)
188
-
if err != nil {
189
-
log.Printf("Error unmarshalling response body: %v", err)
190
178
return
191
179
}
192
180
···
426
414
return
427
415
}
428
416
429
-
resp, err := us.Tags(f.OwnerDid(), f.RepoName)
417
+
result, err := us.Tags(f.OwnerDid(), f.RepoName)
430
418
if err != nil {
431
419
log.Println("failed to reach knotserver", err)
432
420
return
433
421
}
434
422
435
-
body, err := io.ReadAll(resp.Body)
423
+
artifacts, err := db.GetArtifact(s.db, db.NewFilter("repo_at", f.RepoAt))
436
424
if err != nil {
437
-
log.Printf("Error reading response body: %v", err)
425
+
log.Println("failed grab artifacts", err)
438
426
return
439
427
}
440
428
441
-
var result types.RepoTagsResponse
442
-
err = json.Unmarshal(body, &result)
443
-
if err != nil {
444
-
log.Println("failed to parse response:", err)
445
-
return
429
+
// convert artifacts to map for easy UI building
430
+
artifactMap := make(map[plumbing.Hash][]db.Artifact)
431
+
for _, a := range artifacts {
432
+
artifactMap[a.Tag] = append(artifactMap[a.Tag], a)
433
+
}
434
+
435
+
var danglingArtifacts []db.Artifact
436
+
for _, a := range artifacts {
437
+
found := false
438
+
for _, t := range result.Tags {
439
+
if t.Tag != nil {
440
+
if t.Tag.Hash == a.Tag {
441
+
found = true
442
+
}
443
+
}
444
+
}
445
+
446
+
if !found {
447
+
danglingArtifacts = append(danglingArtifacts, a)
448
+
}
446
449
}
447
450
448
451
user := s.auth.GetUser(r)
449
452
s.pages.RepoTags(w, pages.RepoTagsParams{
450
-
LoggedInUser: user,
451
-
RepoInfo: f.RepoInfo(s, user),
452
-
RepoTagsResponse: result,
453
+
LoggedInUser: user,
454
+
RepoInfo: f.RepoInfo(s, user),
455
+
RepoTagsResponse: *result,
456
+
ArtifactMap: artifactMap,
457
+
DanglingArtifacts: danglingArtifacts,
453
458
})
454
459
return
455
460
}
+18
-1
appview/state/router.go
+18
-1
appview/state/router.go
···
63
63
})
64
64
r.Get("/commit/{ref}", s.RepoCommit)
65
65
r.Get("/branches", s.RepoBranches)
66
-
r.Get("/tags", s.RepoTags)
66
+
r.Route("/tags", func(r chi.Router) {
67
+
r.Get("/", s.RepoTags)
68
+
r.Route("/{tag}", func(r chi.Router) {
69
+
r.Use(middleware.AuthMiddleware(s.auth))
70
+
// require auth to download for now
71
+
r.Get("/download/{file}", s.DownloadArtifact)
72
+
73
+
// require repo:push to upload or delete artifacts
74
+
//
75
+
// additionally: only the uploader can truly delete an artifact
76
+
// (record+blob will live on their pds)
77
+
r.Group(func(r chi.Router) {
78
+
r.With(RepoPermissionMiddleware(s, "repo:push"))
79
+
r.Post("/upload", s.AttachArtifact)
80
+
r.Delete("/{file}", s.DeleteArtifact)
81
+
})
82
+
})
83
+
})
67
84
r.Get("/blob/{ref}/*", s.RepoBlob)
68
85
r.Get("/raw/{ref}/*", s.RepoBlobRaw)
69
86
+18
-2
appview/state/signer.go
+18
-2
appview/state/signer.go
···
350
350
return us.client.Do(req)
351
351
}
352
352
353
-
func (us *UnsignedClient) Tags(ownerDid, repoName string) (*http.Response, error) {
353
+
func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) {
354
354
const (
355
355
Method = "GET"
356
356
)
···
362
362
return nil, err
363
363
}
364
364
365
-
return us.client.Do(req)
365
+
resp, err := us.client.Do(req)
366
+
if err != nil {
367
+
return nil, err
368
+
}
369
+
370
+
body, err := io.ReadAll(resp.Body)
371
+
if err != nil {
372
+
return nil, err
373
+
}
374
+
375
+
var result types.RepoTagsResponse
376
+
err = json.Unmarshal(body, &result)
377
+
if err != nil {
378
+
return nil, err
379
+
}
380
+
381
+
return &result, nil
366
382
}
367
383
368
384
func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
+1
-1
appview/state/star.go
+1
-1
appview/state/star.go
···
8
8
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
10
lexutil "github.com/bluesky-social/indigo/lex/util"
11
-
tangled "tangled.sh/tangled.sh/core/api/tangled"
11
+
"tangled.sh/tangled.sh/core/api/tangled"
12
12
"tangled.sh/tangled.sh/core/appview"
13
13
"tangled.sh/tangled.sh/core/appview/db"
14
14
"tangled.sh/tangled.sh/core/appview/pages"
+1
-1
appview/state/state.go
+1
-1
appview/state/state.go
···
17
17
lexutil "github.com/bluesky-social/indigo/lex/util"
18
18
securejoin "github.com/cyphar/filepath-securejoin"
19
19
"github.com/go-chi/chi/v5"
20
-
tangled "tangled.sh/tangled.sh/core/api/tangled"
20
+
"tangled.sh/tangled.sh/core/api/tangled"
21
21
"tangled.sh/tangled.sh/core/appview"
22
22
"tangled.sh/tangled.sh/core/appview/auth"
23
23
"tangled.sh/tangled.sh/core/appview/db"
+14
-13
cmd/gen.go
+14
-13
cmd/gen.go
···
2
2
3
3
import (
4
4
cbg "github.com/whyrusleeping/cbor-gen"
5
-
shtangled "tangled.sh/tangled.sh/core/api/tangled"
5
+
"tangled.sh/tangled.sh/core/api/tangled"
6
6
)
7
7
8
8
func main() {
···
14
14
if err := genCfg.WriteMapEncodersToFile(
15
15
"api/tangled/cbor_gen.go",
16
16
"tangled",
17
-
shtangled.FeedStar{},
18
-
shtangled.GraphFollow{},
19
-
shtangled.KnotMember{},
20
-
shtangled.PublicKey{},
21
-
shtangled.RepoIssueComment{},
22
-
shtangled.RepoIssueState{},
23
-
shtangled.RepoIssue{},
24
-
shtangled.Repo{},
25
-
shtangled.RepoPull{},
26
-
shtangled.RepoPull_Source{},
27
-
shtangled.RepoPullStatus{},
28
-
shtangled.RepoPullComment{},
17
+
tangled.FeedStar{},
18
+
tangled.GraphFollow{},
19
+
tangled.KnotMember{},
20
+
tangled.PublicKey{},
21
+
tangled.RepoIssueComment{},
22
+
tangled.RepoIssueState{},
23
+
tangled.RepoIssue{},
24
+
tangled.Repo{},
25
+
tangled.RepoPull{},
26
+
tangled.RepoPull_Source{},
27
+
tangled.RepoPullStatus{},
28
+
tangled.RepoPullComment{},
29
+
tangled.RepoArtifact{},
29
30
); err != nil {
30
31
panic(err)
31
32
}
+1
-1
go.mod
+1
-1
go.mod
···
19
19
github.com/go-git/go-git/v5 v5.14.0
20
20
github.com/google/uuid v1.6.0
21
21
github.com/gorilla/sessions v1.4.0
22
-
github.com/ipfs/go-cid v0.4.1
22
+
github.com/ipfs/go-cid v0.5.0
23
23
github.com/mattn/go-sqlite3 v1.14.24
24
24
github.com/microcosm-cc/bluemonday v1.0.27
25
25
github.com/resend/resend-go/v2 v2.15.0
+2
go.sum
+2
go.sum
···
132
132
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
133
133
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
134
134
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
135
+
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
136
+
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
135
137
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
136
138
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
137
139
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
+52
lexicons/artifact.json
+52
lexicons/artifact.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "sh.tangled.repo.artifact",
4
+
"needsCbor": true,
5
+
"needsType": true,
6
+
"defs": {
7
+
"main": {
8
+
"type": "record",
9
+
"key": "tid",
10
+
"record": {
11
+
"type": "object",
12
+
"required": [
13
+
"name",
14
+
"repo",
15
+
"tag",
16
+
"createdAt",
17
+
"artifact"
18
+
],
19
+
"properties": {
20
+
"name": {
21
+
"type": "string",
22
+
"description": "name of the artifact"
23
+
},
24
+
"repo": {
25
+
"type": "string",
26
+
"format": "at-uri",
27
+
"description": "repo that this artifact is being uploaded to"
28
+
},
29
+
"tag": {
30
+
"type": "bytes",
31
+
"description": "hash of the tag object that this artifact is attached to (only annotated tags are supported)",
32
+
"minLength": 20,
33
+
"maxLength": 20
34
+
},
35
+
"createdAt": {
36
+
"type": "string",
37
+
"format": "datetime",
38
+
"description": "time of creation of this artifact"
39
+
},
40
+
"artifact": {
41
+
"type": "blob",
42
+
"description": "the artifact",
43
+
"accept": [
44
+
"*/*"
45
+
],
46
+
"maxSize": 1000000
47
+
}
48
+
}
49
+
}
50
+
}
51
+
}
52
+
}