erplibre/script/qemu_cache/recle.go
Mathieu Benoit ab57a49d22 [ADD] cache qemu : ranger à nouveau le magasin sous la clé courante
La règle de clé d'un fichier portable a changé : seule la fin du chemin compte
désormais. Les objets écrits sous l'ancienne règle restent sur le disque mais
deviennent introuvables — le service les redemande à l'amont, et la place
qu'ils tiennent ne sert plus personne. Sur un magasin de 12 764 objets, 5 419
étaient dans ce cas, soit 9,11 Gio. La passe les range sous la clé courante
sans rien retélécharger, et fond au passage les copies qu'un miroir portait
sous plusieurs chemins — 1 422 groupes, environ 3,37 Gio rendus. Un statut
seul n'est pas touché : sa clé porte l'hôte. Vérifié : cinq tests, trois
mutations attrapées.

--- EN ---

The key rule for a portable file changed: only the end of the path now counts.
Objects written under the old rule stay on disk but become unreachable — the
service asks upstream again, and the space they hold serves no one. On a store
of 12 764 objects, 5 419 were in that case, that is 9.11 GiB. The pass stores
them again under the current key without downloading anything, and merges on
the way the copies a mirror carried under several paths — 1 422 groups, about
3.37 GiB returned. A status-only entry is left alone: its key carries the
host. Checked: five tests, three mutations caught.

Assisted-by: Claude Opus 5
2026-09-21 05:39:38 -04:00

185 lines
5.4 KiB
Go

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
package main
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
)
// « --recle » range à nouveau les objets sous la clé que le service calcule
// AUJOURD'HUI.
//
// LE MANQUE QU'IL COMBLE. La clé d'un fichier portable a changé : seule la fin
// du chemin compte désormais (voir SegmentsDeCle), là où le chemin entier
// comptait. Les objets écrits sous l'ancienne règle restent sur le disque mais
// deviennent introuvables : le service les redemande à l'amont, et l'espace
// qu'ils occupent ne sert plus personne. La passe les range sous la clé
// courante, sans rien retélécharger.
//
// ELLE FOND AUSSI LES COPIES. Un même fichier vivait sous plusieurs chemins de
// miroir, donc sous plusieurs clés ; la clé courante les réunit, et deux
// objets tombent alors au même endroit. Le plus récemment rangé est gardé —
// les octets sont les mêmes, seule la date de service les sépare — et l'autre
// rend sa place.
//
// CE QU'ELLE NE TOUCHE PAS. Un statut seul est rangé sous CleStatut, qui hache
// l'URL ENTIÈRE : la règle du chemin ne l'atteint pas, et recalculer sa clé
// avec CleDe la déplacerait là où personne ne la lit.
//
// QUAND LA LANCER. Le service doit être arrêté : la passe renomme des fichiers
// qu'il sert, et un objet déplacé entre la lecture du méta et celle du corps
// ressort en défaut de cache. Rien n'est perdu dans ce cas, mais la mesure
// d'une campagne qui tournerait en même temps ne voudrait rien dire.
// Reclassement compte ce qu'une passe a fait.
type Reclassement struct {
Lus int
Deplaces int
Fondus int
Inchanges int
Illisibles int
Refus int
// OctetsRendus : ce que les copies fondues rendent au disque.
OctetsRendus int64
}
// Ligne rend le compte en une ligne lisible.
func (r Reclassement) Ligne() string {
return fmt.Sprintf(
"lus %d, déplacés %d, fondus %d (%s rendus), inchangés %d,"+
" illisibles %d, refus %d",
r.Lus, r.Deplaces, r.Fondus, HumanBytes(r.OctetsRendus),
r.Inchanges, r.Illisibles, r.Refus)
}
// Recler range chaque objet sous la clé courante. À blanc, rien n'est écrit et
// le compte dit ce qui bougerait.
func (s *Store) Recler(aBlanc bool) (Reclassement, error) {
var r Reclassement
// La liste est faite AVANT de renommer : déplacer un fichier sous un
// répertoire que le parcours n'a pas encore visité le lui ferait
// rencontrer deux fois.
var metas []string
err := filepath.Walk(s.Dir, func(p string, info os.FileInfo, err error) error {
if err != nil || info == nil {
return nil
}
if horsCasier(s.Dir, p, info) {
return filepath.SkipDir
}
if !info.IsDir() && filepath.Ext(p) == ".meta" {
metas = append(metas, p)
}
return nil
})
if err != nil && !os.IsNotExist(err) {
return r, err
}
for _, chemin := range metas {
r.Lus++
ancienne := strings.TrimSuffix(filepath.Base(chemin), ".meta")
m, err := s.LireMeta(ancienne)
if err != nil {
r.Illisibles++
continue
}
// Un statut seul vit sous une clé qui porte l'hôte : la règle du
// chemin ne le concerne pas.
if m.StatutSeul() {
r.Inchanges++
continue
}
u, err := url.Parse(m.URL)
if err != nil || !u.IsAbs() || u.Host == "" {
r.Illisibles++
continue
}
nouvelle := CleDe(m.Method, u)
if nouvelle == ancienne {
r.Inchanges++
continue
}
s.deplacer(&r, ancienne, nouvelle, m, aBlanc)
}
return r, nil
}
// deplacer range un objet sous sa nouvelle clé, ou le fond dans la copie qui
// s'y trouve déjà.
//
// Le CORPS part en premier et le méta ensuite : c'est l'ordre qui publie un
// objet, et l'interrompre laisse au pire un corps sans méta — que le magasin
// ignore — plutôt qu'un méta qui promet un corps absent.
func (s *Store) deplacer(
r *Reclassement, ancienne, nouvelle string, m *Meta, aBlanc bool,
) {
ancienMeta, ancienCorps := s.paths(ancienne)
nouveauMeta, nouveauCorps := s.paths(nouvelle)
if autre, err := s.LireMeta(nouvelle); err == nil {
// Deux chemins de miroir pour le même fichier. Le plus récemment
// rangé reste : les octets sont les mêmes, seule leur date diffère.
garde, rendus := autre.StoredAt, m.Size
if m.StoredAt.After(garde) {
rendus = autre.Size
if !aBlanc {
if err := s.retirerPaire(nouveauMeta, nouveauCorps); err != nil {
r.Refus++
return
}
if err := s.renommerPaire(
ancienMeta, ancienCorps, nouveauMeta, nouveauCorps,
); err != nil {
r.Refus++
return
}
}
r.Deplaces++
} else if !aBlanc {
if err := s.retirerPaire(ancienMeta, ancienCorps); err != nil {
r.Refus++
return
}
}
r.Fondus++
r.OctetsRendus += rendus
return
}
if !aBlanc {
if err := os.MkdirAll(filepath.Dir(nouveauMeta), 0o755); err != nil {
r.Refus++
return
}
if err := s.renommerPaire(
ancienMeta, ancienCorps, nouveauMeta, nouveauCorps,
); err != nil {
r.Refus++
return
}
}
r.Deplaces++
}
func (s *Store) renommerPaire(ancienMeta, ancienCorps, nouveauMeta, nouveauCorps string) error {
if err := os.Rename(ancienCorps, nouveauCorps); err != nil {
return err
}
return os.Rename(ancienMeta, nouveauMeta)
}
func (s *Store) retirerPaire(meta, corps string) error {
if err := os.Remove(corps); err != nil && !os.IsNotExist(err) {
return err
}
if err := os.Remove(meta); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}