erplibre/script/qemu_cache/oublie.go
Mathieu Benoit e0624d9aaa [ADD] cache : retirer une seule entrée du magasin, par URL
Rien n'invalidait une entrée dont la somme ne correspond pas : le magasin
continuait de la servir, et retélécharger ne changeait rien puisque c'est
lui qui répond. Les deux purges existantes ne l'atteignent pas — l'une
efface tout, l'autre saute ce qui est récent alors que chaque service
remet cette date, si bien qu'un objet empoisonné qui sert ne vieillit
jamais.

Symétrique de « --detient » : mêmes lignes, mêmes fonctions de clé, mêmes
refus, et un objet présent qui résiste est dit refusé plutôt qu'oublié.
Mesuré : garde, puis oublié 53080, puis absent.

--- EN ---

Nothing invalidated an entry whose checksum does not match: the store kept
serving it, and re-downloading changed nothing since the store is what
answers. Neither existing purge reaches it — one erases everything, the
other skips what is recent while every service resets that date, so a
poisoned object that serves never ages.

Symmetric to "--detient": same lines, same key functions, same refusals,
and a present object that resists is reported refused rather than
forgotten. Measured: held, then forgotten 53080, then absent.

Assisted-by: Claude Opus 5
2026-09-16 21:32:03 -04:00

167 lines
5.5 KiB
Go

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
package main
import (
"bufio"
"fmt"
"io"
"net/url"
"os"
"strings"
)
// « --oublie » efface du magasin ce qu'une méthode et une URL désignent.
//
// LE MANQUE QU'IL COMBLE. Rien n'invalide une entrée quand la somme d'un
// fichier servi ne correspond pas : le magasin continue de le rendre, et
// retélécharger ne change rien puisque c'est lui qui répond. Les deux purges
// existantes ne l'atteignent pas — « --purge » efface TOUT, et
// « --purge-older-than » saute ce dont la date est récente, or Store.Get remet
// cette date à maintenant à CHAQUE service. Un objet empoisonné qui continue
// d'être servi rajeunit donc à chaque VM et ne vieillit jamais : aucun délai
// ne le rattrape.
//
// SYMÉTRIQUE DE « --detient », et c'est ce qui le rend sûr : mêmes lignes en
// entrée, mêmes fonctions de classement et de clé, mêmes verdicts de refus.
// Ce que l'un dit « garde », l'autre l'efface ; ce que l'un dit
// « non-cachable », l'autre n'a rien à effacer. Deux calculs de clé finiraient
// par diverger, et l'on croirait avoir effacé ce que le service sert encore.
//
// ÉCRITURE : le service écrit ses objets en root, et les effacer demande le
// même droit. Un refus est DIT plutôt que compté comme un succès — croire un
// magasin nettoyé qui ne l'est pas est la seule issue vraiment mauvaise.
// Verdicts de « --oublie ».
const (
// OubliEfface : le magasin tenait quelque chose, il ne l'a plus.
OubliEfface = "oublié"
// OubliAbsent : la requête est cachable, le magasin ne tenait rien.
OubliAbsent = "absent"
// OubliNonCachable : le magasin ne tiendra jamais cette requête, il n'y
// a donc rien à y effacer.
OubliNonCachable = "non-cachable"
// OubliRefus : un objet était là et n'a pas pu être retiré — droits,
// système de fichiers en lecture seule. Surtout pas « oublié ».
OubliRefus = "refus"
)
// Oubli dit ce que le magasin a cessé de tenir pour une méthode et une URL.
type Oubli struct {
Verdict string
// Octets rendus au disque. Nul quand rien n'a été effacé.
Octets int64
Classe string
Methode string
URL string
}
// Ligne rend l'oubli en une ligne séparée par des tabulations : verdict,
// octets, classe, méthode, URL. Un champ sans valeur vaut « - », pour que
// chaque ligne ait cinq champs.
func (o Oubli) Ligne() string {
octets, classe := "-", "-"
if o.Octets != 0 {
octets = fmt.Sprintf("%d", o.Octets)
}
if o.Classe != "" {
classe = o.Classe
}
return strings.Join(
[]string{o.Verdict, octets, classe, o.Methode, o.URL}, "\t")
}
// Oublier efface du magasin ce que la requête désigne, et dit ce qu'il en est.
//
// LES DEUX CLÉS, toujours : le corps (CleDe) et le statut seul (CleStatut)
// vivent dans des espaces séparés, et n'en effacer qu'une laisserait le rejeu
// resservir une redirection vers l'octet qu'on vient de retirer.
//
// Le méta part AVEC le corps, comme dans Purger : un méta orphelin ferait
// croire à une copie présente, et la lecture échouerait au moment de servir.
func (s *Store) Oublier(methode, brut string) Oubli {
methode = strings.ToUpper(methode)
o := Oubli{Verdict: OubliNonCachable, Methode: methode, URL: brut}
u, err := url.Parse(brut)
if err != nil || !u.IsAbs() || u.Host == "" {
return o
}
class := Classify(u)
o.Classe = class.String()
if !CacheableMethod(methode) || class == ClassNoStore {
return o
}
if methode == "HEAD" && (class != ClassVolatile || PortableParChemin(u)) {
return o
}
o.Verdict = OubliAbsent
for _, key := range []string{
CleDe(methode, u), CleStatut(methode, u),
} {
octets, etat := s.effacer(key)
o.Octets += octets
switch etat {
case OubliRefus:
o.Verdict = OubliRefus
case OubliEfface:
if o.Verdict != OubliRefus {
o.Verdict = OubliEfface
}
}
}
return o
}
// effacer retire le corps et le méta d'une clé. Rend (octets rendus, verdict
// de cette clé seule).
//
// Un fichier absent n'est pas une erreur : une clé sans objet est le cas
// ordinaire, et deux clés sont essayées pour chaque requête. Un fichier
// PRÉSENT qui résiste en est une, et elle remonte.
func (s *Store) effacer(key string) (int64, string) {
metaPath, bodyPath := s.paths(key)
octets := int64(0)
etat := OubliAbsent
if fi, err := os.Stat(bodyPath); err == nil {
if e := os.Remove(bodyPath); e != nil {
return 0, OubliRefus
}
octets = fi.Size()
etat = OubliEfface
}
if _, err := os.Stat(metaPath); err == nil {
if e := os.Remove(metaPath); e != nil {
return octets, OubliRefus
}
etat = OubliEfface
}
return octets, etat
}
// EcrireOublis lit des lignes « MÉTHODE URL » et écrit une ligne d'oubli pour
// chacune, dans l'ordre. Une ligne vide ou commençant par « # » est sautée ;
// une ligne d'un seul champ vaut « GET <champ> ». Chaque réponse part aussitôt
// écrite, comme pour « --detient ».
func EcrireOublis(s *Store, entree io.Reader, sortie io.Writer) error {
sc := bufio.NewScanner(entree)
// Une URL signée dépasse volontiers les 64 Kio de la ligne par défaut.
sc.Buffer(make([]byte, 64*1024), 1<<20)
w := bufio.NewWriter(sortie)
for sc.Scan() {
ligne := strings.TrimSpace(sc.Text())
if ligne == "" || strings.HasPrefix(ligne, "#") {
continue
}
champs := strings.Fields(ligne)
methode, brut := "GET", champs[0]
if len(champs) >= 2 {
methode, brut = champs[0], champs[1]
}
fmt.Fprintln(w, s.Oublier(methode, brut).Ligne())
if err := w.Flush(); err != nil {
return err
}
}
return sc.Err()
}