Le cache ne diminuait jamais de lui-même et vit sur le disque de l'orchestrateur : un parc qui déploie chaque jour finissait par le remplir, sans que rien ne le signale. Deux règles existent désormais, éteintes par défaut et réglées depuis le menu : l'âge du dernier usage, et un plafond de taille qui efface le moins récemment servi jusqu'à tenir dessous, objets et dépôts git confondus. Un minuteur systemd quotidien les applique, et une réinstallation garde les valeurs choisies. Le menu montre d'abord ce qui partirait. --- EN --- The cache never shrank by itself and lives on the orchestrator's disk: a site deploying every day filled it up with nothing to say so. Two rules now exist, off by default and set from the menu: the age of last use, and a size ceiling that erases the least recently served until the cache fits under it, objects and git mirrors alike. A daily systemd timer applies them, and a reinstall keeps the chosen values. The menu first shows what would go. Assisted-by: Claude Opus 5
142 lines
3.7 KiB
Go
142 lines
3.7 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"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// LireTaille lit une taille : un nombre, suivi ou non d'une unité binaire.
|
|
//
|
|
// « 50G », « 50Gio », « 50GiB », « 1.5 G », « 500M », « 2T » ou un nombre
|
|
// d'octets. Les unités valent des puissances de 1024 : c'est ce qu'affiche
|
|
// HumanBytes, et un plafond lu autrement que la taille affichée ne se
|
|
// comparerait pas à elle.
|
|
func LireTaille(s string) (int64, error) {
|
|
brut := strings.TrimSpace(s)
|
|
if brut == "" {
|
|
return 0, fmt.Errorf("%s", T("taille vide"))
|
|
}
|
|
t := strings.ToUpper(strings.ReplaceAll(brut, " ", ""))
|
|
for _, suffixe := range []string{"IB", "IO", "B", "O"} {
|
|
if strings.HasSuffix(t, suffixe) && len(t) > len(suffixe) {
|
|
t = strings.TrimSuffix(t, suffixe)
|
|
break
|
|
}
|
|
}
|
|
facteur := int64(1)
|
|
if n := len(t); n > 0 {
|
|
switch t[n-1] {
|
|
case 'K':
|
|
facteur = 1 << 10
|
|
case 'M':
|
|
facteur = 1 << 20
|
|
case 'G':
|
|
facteur = 1 << 30
|
|
case 'T':
|
|
facteur = 1 << 40
|
|
}
|
|
if facteur > 1 {
|
|
t = t[:n-1]
|
|
}
|
|
}
|
|
v, err := strconv.ParseFloat(t, 64)
|
|
if err != nil || v <= 0 {
|
|
return 0, fmt.Errorf(T("taille illisible %q : essayer 50G, 500M, 2T"), brut)
|
|
}
|
|
return int64(v * float64(facteur)), nil
|
|
}
|
|
|
|
// BilanPlafond dit ce qu'une purge au plafond a effacé, ou effacerait.
|
|
type BilanPlafond struct {
|
|
Objets int
|
|
OctetsObjets int64
|
|
Depots int
|
|
OctetsDepots int64
|
|
Avant int64
|
|
Apres int64
|
|
}
|
|
|
|
// candidat est un objet du magasin ou un dépôt en miroir, avec son dernier
|
|
// usage : ce qui décide de l'ordre d'éviction.
|
|
type candidat struct {
|
|
chemin string
|
|
octets int64
|
|
usage time.Time
|
|
depot bool
|
|
}
|
|
|
|
// PurgerJusqua efface le moins récemment servi, objets et dépôts confondus,
|
|
// jusqu'à ce que l'ensemble tienne sous le plafond. aBlanc compte sans rien
|
|
// effacer.
|
|
//
|
|
// Confondus, parce que le plafond porte sur le disque et que les deux s'y
|
|
// disputent la place : un dépôt oublié depuis des mois pèse plus que mille
|
|
// paquets servis récemment. L'usage d'un objet est la date de son corps, remise à
|
|
// jour à chaque service ; celle d'un dépôt, son dernier rafraîchissement.
|
|
// Rien ne part quand le cache tient déjà sous le plafond.
|
|
func PurgerJusqua(store *Store, miroir *GitMirror, plafond int64, aBlanc bool) (BilanPlafond, error) {
|
|
var b BilanPlafond
|
|
var tous []candidat
|
|
if store != nil && store.Dir != "" {
|
|
filepath.Walk(store.Dir, func(p string, info os.FileInfo, err error) error {
|
|
if err != nil || info == nil {
|
|
return nil
|
|
}
|
|
if horsCasier(store.Dir, p, info) {
|
|
return filepath.SkipDir
|
|
}
|
|
if info.IsDir() || filepath.Ext(p) != ".body" {
|
|
return nil
|
|
}
|
|
tous = append(tous, candidat{p, info.Size(), info.ModTime(), false})
|
|
return nil
|
|
})
|
|
}
|
|
for _, d := range miroir.Depots() {
|
|
tous = append(tous, candidat{d.Chemin, d.Octets, d.Maj, true})
|
|
}
|
|
for _, c := range tous {
|
|
b.Avant += c.octets
|
|
}
|
|
b.Apres = b.Avant
|
|
if b.Avant <= plafond {
|
|
return b, nil
|
|
}
|
|
sort.SliceStable(tous, func(i, j int) bool { return tous[i].usage.Before(tous[j].usage) })
|
|
for _, c := range tous {
|
|
if b.Apres <= plafond {
|
|
break
|
|
}
|
|
if !aBlanc {
|
|
if c.depot {
|
|
if err := miroir.Retirer(c.chemin); err != nil {
|
|
return b, err
|
|
}
|
|
} else {
|
|
if err := os.Remove(c.chemin); err != nil {
|
|
continue
|
|
}
|
|
// Le méta part AVEC le corps : orphelin, il ferait croire à
|
|
// une copie présente.
|
|
os.Remove(strings.TrimSuffix(c.chemin, ".body") + ".meta")
|
|
}
|
|
}
|
|
b.Apres -= c.octets
|
|
if c.depot {
|
|
b.Depots++
|
|
b.OctetsDepots += c.octets
|
|
} else {
|
|
b.Objets++
|
|
b.OctetsObjets += c.octets
|
|
}
|
|
}
|
|
return b, nil
|
|
}
|