// mirroruv.go is RFC 0020 rung 6's half of the mirror row: whether a
// mirror imports its upstream's unversioned files, the credential the
// import round signs with, and what the last round did.
//
// The password is sealed the way a bucket secret is (secret.go): the
// round replays it into a login card on every pull, so a one-way hash
// cannot hold it, and a copy of oltp.db alone must not yield it.
// MirrorUVCredential is its one reader, and no op returns it.
package oltp
import (
"database/sql"
"errors"
"fmt"
"time"
)
// SetMirrorUV turns the import round on or off for repo's mirror.
func (d *DB) SetMirrorUV(repo string, on bool) error {
res, err := d.w.Exec(`UPDATE mirror SET uv = ? WHERE repo = ?`, boolInt(on), repo)
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return fmt.Errorf("oltp: %s is not a mirror", repo)
}
return nil
}
// SetMirrorUVCredential stores the login and password the import round
// signs with, sealing the password; an empty user clears both, and the
// round then reads as nobody. The upstream that the credential belongs
// to is the row's URL, and SetMirror drops it when the URL changes.
func (d *DB) SetMirrorUVCredential(repo, user, password string) error {
sealed := ""
if user != "" {
var err error
if sealed, err = d.seal.seal(password); err != nil {
return err
}
}
res, err := d.w.Exec(`UPDATE mirror SET uv_user = ?, uv_secret_enc = ? WHERE repo = ?`, user, sealed, repo)
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return fmt.Errorf("oltp: %s is not a mirror", repo)
}
return nil
}
// MirrorUVCredential answers the import round's login with its password
// unsealed; ok is false when none is held.
func (d *DB) MirrorUVCredential(repo string) (user, password string, ok bool, err error) {
var sealed string
err = d.db.QueryRow(`SELECT uv_user, uv_secret_enc FROM mirror WHERE repo = ?`, repo).Scan(&user, &sealed)
if errors.Is(err, sql.ErrNoRows) {
return "", "", false, nil
}
if err != nil || user == "" || sealed == "" {
return "", "", false, err
}
if password, err = d.seal.open(sealed); err != nil {
return "", "", false, err
}
return user, password, true, nil
}
// RecordMirrorUV stamps an import round: on the mirror row as the last
// result, and on the pull it followed, named by that pull's at (another
// pull may have been recorded while the round ran). changed is how many
// files it applied; msg is what it could not do, empty when nothing.
func (d *DB) RecordMirrorUV(repo, pullAt string, at time.Time, changed int, msg string) error {
if _, err := d.w.Exec(`UPDATE mirror SET uv_at = ?, uv_changed = ?, uv_err = ? WHERE repo = ?`,
at.UTC().Format(time.RFC3339), changed, msg, repo); err != nil {
return err
}
_, err := d.w.Exec(`UPDATE mirror_pull SET uv_ran = 1, uv_changed = ?, uv_err = ?
WHERE repo = ? AND at = ?`, changed, msg, repo, pullAt)
return err
}