Cover byte reclamation: one guarded helper, file first, covers row last (#154)
This commit is contained in:
@@ -746,6 +746,42 @@ func (s *Store) putCover(sourceURL string, body []byte, contentType string) (str
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// ReclaimCover permanently removes a Cover nothing references: the sharded
|
||||
// file first, the covers row last. A blank address is a no-op, and so is any
|
||||
// address a Series row still points at — byte-identical artwork is one row by
|
||||
// construction (ADR-0014), so reclaiming one Series' stranded bytes must not
|
||||
// blank another's. The file goes first because the covers row is the handle:
|
||||
// an interrupted run stays findable in SQL — covers rows unreferenced by any
|
||||
// series cover_address — and re-running finishes the job, whereas deleting
|
||||
// the row first would leave a file nothing names. A concurrent Forced Poll
|
||||
// repointing a live Series at this address between the guard and the unlink
|
||||
// is the repairable case: the missing file reads as ok=false and the next
|
||||
// pass re-installs it. Failures are returned, never logged here — the caller
|
||||
// logs and carries on — and a failed unlink leaves the row in place for a
|
||||
// retry. A whole-table sweep, if ever wanted, is one SQL query over covers,
|
||||
// not a tree walk and not this function.
|
||||
func (s *Store) ReclaimCover(address string) error {
|
||||
if address == "" {
|
||||
return nil
|
||||
}
|
||||
var referenced int
|
||||
err := s.db.QueryRow(`SELECT 1 FROM series WHERE cover_address = $1 LIMIT 1`, address).Scan(&referenced)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("guard reclaim of cover %q: %w", address, err)
|
||||
}
|
||||
coverPath := filepath.Join(s.coverDir, filepath.FromSlash(coverRelativePath(address)))
|
||||
if err := os.Remove(coverPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("remove cover file %q: %w", address, err)
|
||||
}
|
||||
if _, err := s.db.Exec(`DELETE FROM covers WHERE address = $1`, address); err != nil {
|
||||
return fmt.Errorf("delete cover row %q: %w", address, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCover returns the immutable object a source URL's own hash names. Rows
|
||||
// written before byte addressing (ADR-0014) are the only ones that ever reach
|
||||
// it; it hashes the URL, so a byte-addressed Cover is invisible to it. Missing
|
||||
@@ -825,9 +861,10 @@ func (s *Store) SetSeriesCover(site, seriesID, sourceURL string, body []byte, co
|
||||
// ("" if it had none) and current the address of the bytes just stored; both
|
||||
// are read and written in one transaction, so a concurrent replacement reports
|
||||
// the exact displacement. previous == current means the Site served identical
|
||||
// artwork, an honest no-op; otherwise previous is stranded — its bytes stay
|
||||
// served under the covers table (ADR-0014), the row just no longer points at
|
||||
// them.
|
||||
// artwork, an honest no-op; otherwise previous is stranded — the row no
|
||||
// longer points at it, and reclaiming its bytes is the caller's separate act
|
||||
// (the poller's replace path calls ReclaimCover on it). This write itself
|
||||
// removes nothing.
|
||||
func (s *Store) ReplaceSeriesCover(site, seriesID, sourceURL string, body []byte, contentType string) (previous, current string, err error) {
|
||||
current, err = s.putCover(sourceURL, body, contentType)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user