init
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user