Files
pages/internal/store/blobs.go
T
2026-08-15 07:13:00 +00:00

95 lines
2.9 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"github.com/iceBear67/simplepages/internal/cas"
)
// Blob is what the database believes about one piece of content: how long it
// is, and whether its bytes have been verified onto disk yet.
type Blob struct {
Digest cas.Digest
Size int64
Present bool
}
// Blob looks up one blob. ErrNotFound means no manifest has ever declared it,
// which is what lets the upload endpoint refuse content nobody asked for.
func (d *DB) Blob(ctx context.Context, digest cas.Digest) (*Blob, error) {
b := Blob{Digest: digest}
err := d.r.QueryRowContext(ctx,
`SELECT size, present FROM blobs WHERE digest = ?`, digest.Bytes()).
Scan(&b.Size, &b.Present)
if err != nil {
return nil, mapErr(err)
}
return &b, nil
}
// MarkBlobPresent records that a blob's content has been verified onto disk.
//
// Idempotent, because a retried `pages deploy` re-uploads blobs that in the
// meantime became present, and because the row may already say so if a previous
// attempt was interrupted between the rename and this update.
//
// The size is part of the WHERE clause rather than something to overwrite: a
// digest determines its content and therefore its length, so a row that
// disagrees means the manifest and the upload cannot both be describing the same
// bytes, and the safe move is to change nothing.
func (d *DB) MarkBlobPresent(ctx context.Context, digest cas.Digest, size int64) error {
return d.Tx(ctx, func(tx *sql.Tx) error {
res, err := tx.ExecContext(ctx,
`UPDATE blobs SET present = 1 WHERE digest = ? AND size = ?`, digest.Bytes(), size)
if err != nil {
return err
}
if n, err := res.RowsAffected(); err != nil {
return err
} else if n > 0 {
return nil
}
// Nothing was updated. Distinguish "no such blob" from "the row says a
// different size" so the caller can report something actionable.
var have int64
err = tx.QueryRowContext(ctx, `SELECT size FROM blobs WHERE digest = ?`, digest.Bytes()).Scan(&have)
if err != nil {
return mapErr(err)
}
return fmt.Errorf("%w: blob %s is %d bytes here, upload declared %d",
cas.ErrSizeMismatch, digest, have, size)
})
}
// MissingBlobs lists the distinct digests a deployment needs whose content has
// not been uploaded yet. It is both the manifest response and the check
// finalize runs before assembling anything.
func (d *DB) MissingBlobs(ctx context.Context, deploymentID int64) ([]cas.Digest, error) {
rows, err := d.r.QueryContext(ctx, `
SELECT DISTINCT f.digest
FROM deployment_files f JOIN blobs b ON b.digest = f.digest
WHERE f.deployment_id = ? AND b.present = 0
ORDER BY f.digest`, deploymentID)
if err != nil {
return nil, err
}
defer rows.Close()
var missing []cas.Digest
for rows.Next() {
var raw []byte
if err := rows.Scan(&raw); err != nil {
return nil, err
}
dg, err := cas.FromBytes(raw)
if err != nil {
return nil, err
}
missing = append(missing, dg)
}
return missing, rows.Err()
}