Skip to content
quarry Artifact [091aa8a523]
Login

Artifact [091aa8a523]

Artifact 091aa8a5230a3f4fca07d8fb95cd733e9a5682df1b7af81b87d813c4ed6ab085:


// avatar.go is a repository's own mark, when somebody has uploaded one.
//
// The BYTES are not here. They live in the forge's own bucket
// (oltp.ForgeBucket), because that is the storage the operator already
// pays for and configures, and because a database is a poor place for
// binary that is served on every listing. What is here is the little a
// page needs to serve it: the key, the media type, and a strong
// validator so a reader fetches the image once.
//
// Absence is the ordinary case and is not an error: a repository with
// no row keeps the identicon derived from its name, which is what
// every repository had before this existed (RFC 0018 ยง"An uploaded
// avatar", amended).
package oltp

import (
	"database/sql"
	"errors"
	"time"
)

const avatarSchema = `
CREATE TABLE IF NOT EXISTS repo_avatar (
	repo   TEXT PRIMARY KEY,
	key    TEXT NOT NULL,          -- the object in the forge's bucket
	mime   TEXT NOT NULL,          -- what to serve it as; validated before it was stored
	etag   TEXT NOT NULL,          -- sha256 of the bytes, for If-None-Match
	bytes  INTEGER NOT NULL,
	set_by TEXT NOT NULL DEFAULT '',
	at     TEXT NOT NULL
);`

// RepoAvatar is one repository's uploaded mark.
type RepoAvatar struct {
	Repo  string `json:"repo"`
	Key   string `json:"key"`
	Mime  string `json:"mime"`
	ETag  string `json:"etag"`
	Bytes int64  `json:"bytes"`
	SetBy string `json:"setBy,omitempty"`
	At    string `json:"at"`
}

// SetRepoAvatar records a repository's uploaded mark, replacing any it
// had. The caller has already written the object; this is the row that
// makes it findable.
func (d *DB) SetRepoAvatar(a RepoAvatar) error {
	if a.Repo == "" || a.Key == "" || a.Mime == "" {
		return errors.New("oltp: avatar needs a repository, a key and a type")
	}
	if a.At == "" {
		a.At = time.Now().UTC().Format(time.RFC3339)
	}
	_, err := d.db.Exec(`INSERT INTO repo_avatar (repo, key, mime, etag, bytes, set_by, at)
		VALUES (?, ?, ?, ?, ?, ?, ?)
		ON CONFLICT(repo) DO UPDATE SET key = excluded.key, mime = excluded.mime,
			etag = excluded.etag, bytes = excluded.bytes, set_by = excluded.set_by, at = excluded.at`,
		a.Repo, a.Key, a.Mime, a.ETag, a.Bytes, a.SetBy, a.At)
	return err
}

// RepoAvatarOf reads a repository's mark. Absent is not an error.
func (d *DB) RepoAvatarOf(repo string) (RepoAvatar, bool, error) {
	var a RepoAvatar
	err := d.db.QueryRow(`SELECT repo, key, mime, etag, bytes, set_by, at
		FROM repo_avatar WHERE repo = ?`, repo).
		Scan(&a.Repo, &a.Key, &a.Mime, &a.ETag, &a.Bytes, &a.SetBy, &a.At)
	if errors.Is(err, sql.ErrNoRows) {
		return RepoAvatar{}, false, nil
	}
	if err != nil {
		return RepoAvatar{}, false, err
	}
	return a, true, nil
}

// RepoAvatarETags is every repository's validator in one read, for the
// listings that draw many marks at once. A page that asked per row
// would issue one query per repository on the forge's front page.
func (d *DB) RepoAvatarETags() (map[string]string, error) {
	rows, err := d.db.Query(`SELECT repo, etag FROM repo_avatar`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := map[string]string{}
	for rows.Next() {
		var repo, etag string
		if err := rows.Scan(&repo, &etag); err != nil {
			return nil, err
		}
		out[repo] = etag
	}
	return out, rows.Err()
}

// ClearRepoAvatar drops the row. The object is left in the bucket: a
// removal is a decision about the page and a deletion one about the
// bytes, which is the split RFC 0016 already draws for attachments,
// and the operator owns the second.
func (d *DB) ClearRepoAvatar(repo string) error {
	_, err := d.db.Exec(`DELETE FROM repo_avatar WHERE repo = ?`, repo)
	return err
}