95 lines
2.9 KiB
Go
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()
|
|
}
|