erplibre/script/qemu_cache/recle.go

192 lines
5.8 KiB
Go
Raw Normal View History

// © 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.
//
// Ce sont des ÉVÉNEMENTS, non des objets, et leur somme dépasse donc le nombre
// d'objets lus : une copie déjà rangée sous la clé courante est comptée
// « inchangée » quand le parcours la visite, puis « fondue » si une copie plus
// récente vient prendre sa place. Les additionner pour retrouver « lus » est
// une erreur de lecture, pas une erreur de compte.
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
}