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
185 lines
5.4 KiB
Go
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
|
|
}
|