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() }