Additionner déplacés, fondus et inchangés dépasse le nombre d'objets lus, et la passe paraît alors se contredire. Elle ne se trompe pas : 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. Le commentaire le dit désormais, pour que personne ne prenne cette somme pour un inventaire. --- EN --- Adding moved, merged and unchanged exceeds the number of objects read, and the pass then seems to contradict itself. It does not: a copy already stored under the current key is counted unchanged when the walk visits it, then merged if a more recent copy takes its place. The comment now says so, lest anyone take that sum for an inventory. Assisted-by: Claude Opus 5
191 lines
5.8 KiB
Go
191 lines
5.8 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.
|
|
//
|
|
// 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
|
|
}
|