erplibre/script/qemu_cache/age.go

288 lines
8.1 KiB
Go
Raw Permalink 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"
"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é."))
}