diff --git a/api/tangled/cbor_gen.go b/api/tangled/cbor_gen.go --- a/api/tangled/cbor_gen.go +++ b/api/tangled/cbor_gen.go @@ -8,6 +8,7 @@ "io" "math" "sort" + util "github.com/bluesky-social/indigo/lex/util" cid "github.com/ipfs/go-cid" cbg "github.com/whyrusleeping/cbor-gen" xerrors "golang.org/x/xerrors" @@ -3098,3 +3099,293 @@ } return nil } +func (t *RepoArtifact) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 6 + + if t.Tag == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Tag (util.LexBytes) (slice) + if t.Tag != nil { + + if len("tag") > 1000000 { + return xerrors.Errorf("Value in field \"tag\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("tag"))); err != nil { + return err + } + if _, err := cw.WriteString(string("tag")); err != nil { + return err + } + + if len(t.Tag) > 2097152 { + return xerrors.Errorf("Byte array in field t.Tag was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Tag))); err != nil { + return err + } + + if _, err := cw.Write(t.Tag); err != nil { + return err + } + + } + + // t.Name (string) (string) + if len("name") > 1000000 { + return xerrors.Errorf("Value in field \"name\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil { + return err + } + if _, err := cw.WriteString(string("name")); err != nil { + return err + } + + if len(t.Name) > 1000000 { + return xerrors.Errorf("Value in field t.Name was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Name)); err != nil { + return err + } + + // t.Repo (string) (string) + if len("repo") > 1000000 { + return xerrors.Errorf("Value in field \"repo\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { + return err + } + if _, err := cw.WriteString(string("repo")); err != nil { + return err + } + + if len(t.Repo) > 1000000 { + return xerrors.Errorf("Value in field t.Repo was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Repo)); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.artifact"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.repo.artifact")); err != nil { + return err + } + + // t.Artifact (util.LexBlob) (struct) + if len("artifact") > 1000000 { + return xerrors.Errorf("Value in field \"artifact\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artifact"))); err != nil { + return err + } + if _, err := cw.WriteString(string("artifact")); err != nil { + return err + } + + if err := t.Artifact.MarshalCBOR(cw); err != nil { + return err + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + return nil +} + +func (t *RepoArtifact) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoArtifact{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoArtifact: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Tag (util.LexBytes) (slice) + case "tag": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Tag: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Tag = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Tag); err != nil { + return err + } + + // t.Name (string) (string) + case "name": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Name = string(sval) + } + // t.Repo (string) (string) + case "repo": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Repo = string(sval) + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Artifact (util.LexBlob) (struct) + case "artifact": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Artifact = new(util.LexBlob) + if err := t.Artifact.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Artifact pointer: %w", err) + } + } + + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/api/tangled/repoartifact.go b/api/tangled/repoartifact.go new file mode 100644 --- /dev/null +++ b/api/tangled/repoartifact.go @@ -0,0 +1,31 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package tangled + +// schema: sh.tangled.repo.artifact + +import ( + "github.com/bluesky-social/indigo/lex/util" +) + +const ( + RepoArtifactNSID = "sh.tangled.repo.artifact" +) + +func init() { + util.RegisterType("sh.tangled.repo.artifact", &RepoArtifact{}) +} // +// RECORDTYPE: RepoArtifact +type RepoArtifact struct { + LexiconTypeID string `json:"$type,const=sh.tangled.repo.artifact" cborgen:"$type,const=sh.tangled.repo.artifact"` + // artifact: the artifact + Artifact *util.LexBlob `json:"artifact" cborgen:"artifact"` + // createdAt: time of creation of this artifact + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + // name: name of the artifact + Name string `json:"name" cborgen:"name"` + // repo: repo that this artifact is being uploaded to + Repo string `json:"repo" cborgen:"repo"` + // tag: hash of the tag object that this artifact is attached to (only annotated tags are supported) + Tag util.LexBytes `json:"tag,omitempty" cborgen:"tag,omitempty"` +} diff --git a/appview/db/artifact.go b/appview/db/artifact.go new file mode 100644 --- /dev/null +++ b/appview/db/artifact.go @@ -0,0 +1,166 @@ +package db + +import ( + "fmt" + "strings" + "time" + + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/go-git/go-git/v5/plumbing" + "github.com/ipfs/go-cid" + "tangled.sh/tangled.sh/core/api/tangled" +) + +type Artifact struct { + Id uint64 + Did string + Rkey string + + RepoAt syntax.ATURI + Tag plumbing.Hash + CreatedAt time.Time + + BlobCid cid.Cid + Name string + Size uint64 + Mimetype string +} + +func (a *Artifact) ArtifactAt() syntax.ATURI { + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", a.Did, tangled.RepoPullNSID, a.Rkey)) +} + +func AddArtifact(e Execer, artifact Artifact) error { + _, err := e.Exec( + `insert or ignore into artifacts ( + did, + rkey, + repo_at, + tag, + created, + blob_cid, + name, + size, + mimetype + ) + values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + artifact.Did, + artifact.Rkey, + artifact.RepoAt, + artifact.Tag[:], + artifact.CreatedAt.Format(time.RFC3339), + artifact.BlobCid.String(), + artifact.Name, + artifact.Size, + artifact.Mimetype, + ) + return err +} + +type Filter struct { + key string + arg any +} + +func NewFilter(key string, arg any) Filter { + return Filter{ + key: key, + arg: arg, + } +} + +func (f Filter) Condition() string { + return fmt.Sprintf("%s = ?", f.key) +} + +func GetArtifact(e Execer, filters ...Filter) ([]Artifact, error) { + var artifacts []Artifact + + var conditions []string + var args []any + for _, filter := range filters { + conditions = append(conditions, filter.Condition()) + args = append(args, filter.arg) + } + + whereClause := "" + if conditions != nil { + whereClause = " where " + strings.Join(conditions, " and ") + } + + query := fmt.Sprintf(`select + did, + rkey, + repo_at, + tag, + created, + blob_cid, + name, + size, + mimetype + from artifacts %s`, + whereClause, + ) + + rows, err := e.Query(query, args...) + + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var artifact Artifact + var createdAt string + var tag []byte + var blobCid string + + if err := rows.Scan( + &artifact.Did, + &artifact.Rkey, + &artifact.RepoAt, + &tag, + &createdAt, + &blobCid, + &artifact.Name, + &artifact.Size, + &artifact.Mimetype, + ); err != nil { + return nil, err + } + + artifact.CreatedAt, err = time.Parse(time.RFC3339, createdAt) + if err != nil { + artifact.CreatedAt = time.Now() + } + artifact.Tag = plumbing.Hash(tag) + artifact.BlobCid = cid.MustParse(blobCid) + + artifacts = append(artifacts, artifact) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return artifacts, nil +} + +func RemoveArtifact(e Execer, filters ...Filter) error { + var conditions []string + var args []any + for _, filter := range filters { + conditions = append(conditions, filter.Condition()) + args = append(args, filter.arg) + } + + whereClause := "" + if conditions != nil { + whereClause = " where " + strings.Join(conditions, " and ") + } + + query := fmt.Sprintf(`delete from artifacts %s`, whereClause) + + _, err := e.Exec(query, args...) + return err +} diff --git a/appview/db/db.go b/appview/db/db.go --- a/appview/db/db.go +++ b/appview/db/db.go @@ -208,6 +208,29 @@ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), unique(did, email) ); + create table if not exists artifacts ( + -- id + id integer primary key autoincrement, + did text not null, + rkey text not null, + + -- meta + repo_at text not null, + tag binary(20) not null, + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + + -- data + blob_cid text not null, + name text not null, + size integer not null default 0, + mimetype string not null default "*/*", + + -- constraints + unique(did, rkey), -- record must be unique + unique(repo_at, tag, name), -- for a given tag object, each file must be unique + foreign key (repo_at) references repos(at_uri) on delete cascade + ); + create table if not exists migrations ( id integer primary key autoincrement, name text unique diff --git a/appview/db/pulls.go b/appview/db/pulls.go --- a/appview/db/pulls.go +++ b/appview/db/pulls.go @@ -10,7 +10,7 @@ "time" "github.com/bluekeyes/go-gitdiff/gitdiff" "github.com/bluesky-social/indigo/atproto/syntax" - tangled "tangled.sh/tangled.sh/core/api/tangled" + "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/patchutil" "tangled.sh/tangled.sh/core/types" ) diff --git a/appview/pages/pages.go b/appview/pages/pages.go --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -28,6 +28,7 @@ chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/microcosm-cc/bluemonday" ) @@ -484,11 +485,23 @@ LoggedInUser *auth.User RepoInfo repoinfo.RepoInfo Active string types.RepoTagsResponse + ArtifactMap map[plumbing.Hash][]db.Artifact + DanglingArtifacts []db.Artifact } func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { params.Active = "overview" return p.executeRepo("repo/tags", w, params) +} + +type RepoArtifactParams struct { + LoggedInUser *auth.User + RepoInfo RepoInfo + Artifact db.Artifact +} + +func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { + return p.executePlain("repo/fragments/artifact", w, params) } type RepoBlobParams struct { diff --git a/appview/pages/templates/repo/fragments/artifact.html b/appview/pages/templates/repo/fragments/artifact.html new file mode 100644 --- /dev/null +++ b/appview/pages/templates/repo/fragments/artifact.html @@ -0,0 +1,34 @@ +{{ define "repo/fragments/artifact" }} +{{ $unique := .Artifact.BlobCid.String }} +
{{ index $messageParts 0 }}
+{{ index $messageParts 0 }}
{{ if gt (len $messageParts) 1 }} -{{ nl2br (index $messageParts 1) }}
+{{ nl2br (index $messageParts 1) }}
{{ end }} + {{ block "artifacts" (list $ .) }} {{ end }} {{ else }}no message
{{ end }} @@ -74,3 +75,91 @@ {{ end }}The tags that these artifacts were attached to have been deleted. These artifacts are only visible to collaborators.
+