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
167 lines
5.5 KiB
Go
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()
|
|
}
|