erplibre/script/qemu_cache/age.go
Mathieu Benoit fb2f71477e [ADD] cache qemu : messages du binaire en anglais ou en français
Le binaire ne parlait que français alors que le menu suit EL_LANG : un
opérateur anglophone lisait un statut qu'il ne comprenait pas. Chaque message
humain passe par T(), dont la clé EST le texte français, et un catalogue
anglais le traduit ; la langue vient de --lang, puis d'EL_LANG, le français
restant le défaut. Le menu demande le français pour ce qu'il RELIT, ses
motifs ne connaissant que cette langue. Un test refuse un message sans
traduction, une traduction orpheline et un verbe de format qui diverge.

--- EN ---

The binary spoke French only while the menu follows EL_LANG: an English
reader got a status he could not read. Every human message goes through T(),
whose key IS the French text, and an English catalogue translates it; the
language comes from --lang, then EL_LANG, French remaining the default. The
menu asks for French for what it READS BACK, its patterns knowing that
language alone. A test refuses a message without translation, an orphan
translation, and a format verb that drifts.

Assisted-by: Claude Opus 5
2026-09-16 02:59:01 -04:00

287 lines
8.1 KiB
Go

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
package main
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// Le cache ne diminue jamais de lui-même — c'est assumé, et la place se
// surveille à la main. Encore faut-il pouvoir la surveiller : savoir CE QUI
// occupe, et depuis QUAND, avant de décider ce qui peut partir.
//
// L'âge retenu est la date du dernier USAGE et non celle du stockage. Un
// paquet servi tous les jours depuis un an n'est pas vieux : le supprimer
// obligerait à le retélécharger le lendemain. C'est ce qu'un opérateur veut
// dire par « nettoyer ce qui ne sert plus », et c'est pour cela que le service
// remet la date d'un objet à chaque fois qu'il le sert.
// Tranche est un groupe d'objets d'un même âge.
type Tranche struct {
// Debut est le premier instant de la tranche ; Libelle la nomme.
Debut time.Time
Libelle string
Objets int
Octets int64
}
// Granularite dit comment les tranches sont découpées.
type Granularite string
const (
ParJour Granularite = "jour"
ParSemaine Granularite = "semaine"
ParMois Granularite = "mois"
)
// LireGranularite tolère l'anglais comme le français : le CLI est bilingue
// partout ailleurs dans ce dépôt.
func LireGranularite(s string) (Granularite, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "jour", "day", "j", "d":
return ParJour, nil
case "semaine", "week", "s", "w":
return ParSemaine, nil
case "mois", "month", "m":
return ParMois, nil
}
return "", fmt.Errorf(T("granularité inconnue %q : jour, semaine ou mois"), s)
}
// debutDeTranche ramène un instant au premier de sa tranche.
//
// La semaine commence le LUNDI : c'est la convention ISO, et celle que
// « %V » et les outils du système emploient. Ramener au dimanche donnerait des
// tranches qui ne correspondent à aucun autre relevé de la machine.
func debutDeTranche(t time.Time, g Granularite) time.Time {
t = t.Local()
jour := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
switch g {
case ParSemaine:
recul := (int(jour.Weekday()) + 6) % 7
return jour.AddDate(0, 0, -recul)
case ParMois:
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
}
return jour
}
func libelleDeTranche(t time.Time, g Granularite) string {
switch g {
case ParSemaine:
an, sem := t.ISOWeek()
return fmt.Sprintf("%d-S%02d", an, sem)
case ParMois:
return t.Format("2006-01")
}
return t.Format("2006-01-02")
}
// TranchesStore groupe les objets du magasin par âge, du plus récent au plus
// ancien.
func (s *Store) Tranches(g Granularite) []Tranche {
par := map[time.Time]*Tranche{}
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) != ".body" {
return nil
}
d := debutDeTranche(info.ModTime(), g)
tr, ok := par[d]
if !ok {
tr = &Tranche{Debut: d, Libelle: libelleDeTranche(d, g)}
par[d] = tr
}
tr.Objets++
tr.Octets += info.Size()
return nil
})
return trier(par)
}
// TranchesMiroirs fait de même pour les dépôts git.
//
// Un dépôt entier par tranche, pas ses fichiers : ce qui s'efface est un
// dépôt, et le compter au fichier donnerait un relevé qu'on ne peut pas
// suivre d'un geste.
func (g *GitMirror) Tranches(gran Granularite) []Tranche {
if g == nil || g.Dir == "" {
return nil
}
par := map[time.Time]*Tranche{}
for _, d := range g.Depots() {
debut := debutDeTranche(d.Maj, gran)
tr, ok := par[debut]
if !ok {
tr = &Tranche{Debut: debut, Libelle: libelleDeTranche(debut, gran)}
par[debut] = tr
}
tr.Objets++
tr.Octets += d.Octets
}
return trier(par)
}
func trier(par map[time.Time]*Tranche) []Tranche {
out := make([]Tranche, 0, len(par))
for _, tr := range par {
out = append(out, *tr)
}
sort.Slice(out, func(i, j int) bool {
return out[i].Debut.After(out[j].Debut)
})
return out
}
// Purger efface les objets dont le dernier usage précède `avant`.
//
// Le méta part AVEC le corps : un méta orphelin ferait croire à une copie
// présente, et la lecture échouerait au moment de servir, c'est-à-dire au pire
// moment. Rend (objets effacés, octets rendus).
func (s *Store) Purger(avant time.Time) (int, int64, error) {
n, octets := 0, int64(0)
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) != ".body" {
return nil
}
if !info.ModTime().Before(avant) {
return nil
}
taille := info.Size()
if e := os.Remove(p); e != nil {
return nil
}
os.Remove(strings.TrimSuffix(p, ".body") + ".meta")
n++
octets += taille
return nil
})
return n, octets, err
}
// PurgerMiroirs efface les dépôts qui n'ont pas été rafraîchis depuis
// `avant`. Rend (dépôts effacés, octets rendus).
func (g *GitMirror) PurgerMiroirs(avant time.Time) (int, int64, error) {
if g == nil || g.Dir == "" {
return 0, 0, nil
}
n, octets := 0, int64(0)
for _, d := range g.Depots() {
if !d.Maj.Before(avant) {
continue
}
if err := g.Retirer(d.Chemin); err != nil {
return n, octets, err
}
n++
octets += d.Octets
}
return n, octets, nil
}
// LireDuree accepte les suffixes de Go et, en plus, « j » pour un jour.
//
// « 30d » n'existe pas dans la bibliothèque, et un opérateur qui nettoie
// raisonne en jours, pas en sept cent vingt heures.
func LireDuree(s string) (time.Duration, error) {
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return 0, fmt.Errorf("%s", T("durée vide"))
}
if n, unite := s[:len(s)-1], s[len(s)-1]; unite == 'j' || unite == 'd' {
var jours float64
if _, err := fmt.Sscanf(n, "%g", &jours); err != nil || jours < 0 {
return 0, fmt.Errorf(T("durée illisible %q"), s)
}
return time.Duration(jours * 24 * float64(time.Hour)), nil
}
d, err := time.ParseDuration(s)
if err != nil || d < 0 {
return 0, fmt.Errorf(
T("durée illisible %q : essayer 30j, 12h, 90m"), s)
}
return d, nil
}
// printAge écrit le relevé par âge : les objets, puis les dépôts.
//
// Les deux SÉPARÉMENT : ils ne s'effacent pas de la même façon — un objet est
// un fichier, un dépôt est un arbre — et un total commun cacherait qu'un seul
// dépôt pèse plus que mille objets.
func printAge(store *Store, miroir *GitMirror, gran Granularite) {
fmt.Printf(T("Âge du dernier usage, par %s.\n"), T(string(gran)))
fmt.Printf(
"%s", T("\nUn objet servi voit sa date remise à jour : « vieux » veut donc "+
"dire\n« n'a plus servi », et non « est entré il y a "+
"longtemps ».\n"))
ecrire := func(titre string, tranches []Tranche, quoi string) {
fmt.Printf("\n%s\n", titre)
if len(tranches) == 0 {
fmt.Println(T(" rien"))
return
}
var n int
var octets int64
for _, tr := range tranches {
fmt.Printf(" %-12s %6d %-8s %10s\n",
tr.Libelle, tr.Objets, quoi, HumanBytes(tr.Octets))
n += tr.Objets
octets += tr.Octets
}
fmt.Printf(" %-12s %6d %-8s %10s\n", "total", n, quoi,
HumanBytes(octets))
}
ecrire(T("Objets du cache :"), store.Tranches(gran), T("objets"))
if miroir.Actif() {
ecrire(T("Dépôts en miroir :"), miroir.Tranches(gran), T("dépôts"))
}
}
// printPurgeABlanc dit ce qu'une purge emporterait, sans rien effacer.
//
// Une purge ne se rattrape pas — les octets sont rendus, il faut les
// retélécharger — et c'est le genre de geste qu'on veut relire avant.
func printPurgeABlanc(store *Store, miroir *GitMirror, avant time.Time) {
var n int
var octets int64
for _, tr := range store.Tranches(ParJour) {
if tr.Debut.Before(avant) {
n += tr.Objets
octets += tr.Octets
}
}
fmt.Printf(T("[à blanc] objets qui partiraient : %d, %s\n"),
n, HumanBytes(octets))
var nd int
var octd int64
for _, d := range miroir.Depots() {
if d.Maj.Before(avant) {
nd++
octd += d.Octets
}
}
fmt.Printf(T("[à blanc] dépôts qui partiraient : %d, %s\n"),
nd, HumanBytes(octd))
fmt.Println(T("[à blanc] rien n'a été effacé."))
}