forked from tangled.org/core
this repo has no description

appview: introduce release artifacts

+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 }}
+93 -4
appview/pages/templates/repo/tags.html
··· 11 11 <!-- Header column (top on mobile, left on md+) --> 12 12 <div class="md:col-span-2 md:border-r border-b md:border-b-0 border-gray-200 dark:border-gray-700 w-full md:h-full"> 13 13 <!-- Mobile layout: horizontal --> 14 - <div class="flex md:hidden flex-col py-2 px-2"> 14 + <div class="flex md:hidden flex-col py-2 px-2 text-xl"> 15 15 <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Name | urlquery }}" class="no-underline hover:underline flex items-center gap-2 font-bold"> 16 16 {{ i "tag" "w-4 h-4" }} 17 17 {{ .Name }} ··· 54 54 </div> 55 55 56 56 <!-- Content column (bottom on mobile, right on md+) --> 57 - <div class="md:col-span-9 px-2 py-3 md:py-0 md:pb-6"> 57 + <div class="md:col-span-10 px-2 py-3 md:py-0 md:pb-6"> 58 58 {{ if .Tag }} 59 59 {{ $messageParts := splitN .Tag.Message "\n\n" 2 }} 60 - <p class="font-bold">{{ index $messageParts 0 }}</p> 60 + <p class="font-bold text-lg">{{ index $messageParts 0 }}</p> 61 61 {{ if gt (len $messageParts) 1 }} 62 - <p class="cursor-text pb-2 text-sm">{{ nl2br (index $messageParts 1) }}</p> 62 + <p class="cursor-text py-2">{{ nl2br (index $messageParts 1) }}</p> 63 63 {{ end }} 64 + {{ block "artifacts" (list $ .) }} {{ end }} 64 65 {{ else }} 65 66 <p class="italic text-gray-500 dark:text-gray-400">no message</p> 66 67 {{ end }} ··· 74 75 </div> 75 76 </section> 76 77 {{ end }} 78 + 79 + {{ define "repoAfter" }} 80 + {{ if gt (len .DanglingArtifacts) 0 }} 81 + <section class="bg-white dark:bg-gray-800 p-6 mt-4"> 82 + {{ block "dangling" . }} {{ end }} 83 + </section> 84 + {{ end }} 85 + {{ end }} 86 + 87 + {{ define "artifacts" }} 88 + {{ $root := index . 0 }} 89 + {{ $tag := index . 1 }} 90 + {{ $isPushAllowed := $root.RepoInfo.Roles.IsPushAllowed }} 91 + {{ $artifacts := index $root.ArtifactMap $tag.Tag.Hash }} 92 + 93 + {{ if or (gt (len $artifacts) 0) $isPushAllowed }} 94 + <h2 class="my-4 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">artifacts</h2> 95 + <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700"> 96 + {{ range $artifact := $artifacts }} 97 + {{ $args := dict "LoggedInUser" $root.LoggedInUser "RepoInfo" $root.RepoInfo "Artifact" $artifact }} 98 + {{ template "repo/fragments/artifact" $args }} 99 + {{ end }} 100 + {{ if $isPushAllowed }} 101 + {{ block "uploadArtifact" (list $root $tag) }} {{ end }} 102 + {{ end }} 103 + </div> 104 + {{ end }} 105 + {{ end }} 106 + 107 + {{ define "uploadArtifact" }} 108 + {{ $root := index . 0 }} 109 + {{ $tag := index . 1 }} 110 + {{ $unique := $tag.Tag.Target.String }} 111 + <form 112 + id="upload-{{$unique}}" 113 + method="post" 114 + enctype="multipart/form-data" 115 + hx-post="/{{ $root.RepoInfo.FullName }}/tags/{{ $tag.Name | urlquery }}/upload" 116 + hx-on::after-request="if(event.detail.successful) this.reset()" 117 + hx-disabled-elt="#upload-btn-{{$unique}}" 118 + hx-swap="beforebegin" 119 + hx-target="this" 120 + class="flex items-center gap-2 px-2"> 121 + <div class="flex-grow"> 122 + <input type="file" 123 + name="artifact" 124 + required 125 + class="block py-2 px-0 w-full border-none 126 + text-black dark:text-white 127 + bg-white dark:bg-gray-800 128 + file:mr-4 file:px-2 file:py-2 129 + file:rounded file:border-0 130 + file:text-sm file:font-medium 131 + file:text-gray-700 file:dark:text-gray-300 132 + file:bg-gray-200 file:dark:bg-gray-700 133 + file:hover:bg-gray-100 file:hover:dark:bg-gray-600 134 + "> 135 + </input> 136 + </div> 137 + <div class="flex justify-end"> 138 + <button 139 + type="submit" 140 + class="btn gap-2" 141 + id="upload-btn-{{$unique}}" 142 + title="Upload artifact"> 143 + {{ i "upload" "w-4 h-4" }} 144 + <span class="hidden md:inline">upload</span> 145 + </button> 146 + </div> 147 + </form> 148 + {{ end }} 149 + 150 + {{ define "dangling" }} 151 + {{ $root := . }} 152 + {{ $isPushAllowed := $root.RepoInfo.Roles.IsPushAllowed }} 153 + {{ $artifacts := $root.DanglingArtifacts }} 154 + 155 + {{ if and (gt (len $artifacts) 0) $isPushAllowed }} 156 + <h2 class="mb-2 text-sm text-left text-red-700 dark:text-red-400 uppercase font-bold">dangling artifacts</h2> 157 + <p class="mb-4">The tags that these artifacts were attached to have been deleted. These artifacts are only visible to collaborators.</p> 158 + <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700"> 159 + {{ range $artifact := $artifacts }} 160 + {{ $args := dict "LoggedInUser" $root.LoggedInUser "RepoInfo" $root.RepoInfo "Artifact" $artifact }} 161 + {{ template "repo/fragments/artifact" $args }} 162 + {{ end }} 163 + </div> 164 + {{ end }} 165 + {{ end }}
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
flake.nix
··· 446 446 }; 447 447 }; 448 448 } 449 +
+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
··· 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
··· 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 + }