Espaces personnels et favoris par utilisateur (stockage)

- DATA_DIR/.users/<id>/home : espace personnel, Store complet (historique,
  miniatures, corbeille, tags, partages), exposé sous le préfixe virtuel .perso.
- DATA_DIR/.users/<id>/favoris : un lien symbolique relatif par favori, exposé
  sous .favoris. Les liens suivent renommages et déplacements (y compris entre
  espaces) pour tous les utilisateurs et disparaissent à la suppression.
- Identifiant : email OIDC normalisé (UserID), utilisateur « local » sinon.
- Reprise unique des favoris de .meta.json à la création d'un espace.
- Les espaces personnels signent leurs liens de partage avec la clé du Store
  principal ; Tout révoquer couvre aussi les espaces personnels.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174sFqehj4uGqJHQZvWy1gn
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-08 10:24:55 +02:00
1 parent c7a43ff12b
commit 781fc8f5f5
3 files changed
+944 -2

No files matched your search

+12 -2
View File
@@ -63,6 +63,9 @@ func (s *Store) SetShareSecret(secret string) {
// key renvoie la clé de signature, créée au besoin ; appelé sous sharesMu. // key renvoie la clé de signature, créée au besoin ; appelé sous sharesMu.
func (s *Store) key() ([]byte, error) { func (s *Store) key() ([]byte, error) {
if s.keyFrom != nil {
return s.keyFrom.lockedKey()
}
if s.shareKey != nil { if s.shareKey != nil {
return s.shareKey, nil return s.shareKey, nil
} }
@@ -79,6 +82,13 @@ func (s *Store) key() ([]byte, error) {
return s.newKey() return s.newKey()
} }
// lockedKey renvoie la clé de signature pour un espace personnel.
func (s *Store) lockedKey() ([]byte, error) {
s.sharesMu.Lock()
defer s.sharesMu.Unlock()
return s.key()
}
func (s *Store) newKey() ([]byte, error) { func (s *Store) newKey() ([]byte, error) {
k := make([]byte, 32) k := make([]byte, 32)
if _, err := rand.Read(k); err != nil { if _, err := rand.Read(k); err != nil {
@@ -230,11 +240,11 @@ func (s *Store) RevokeShare(id string) error {
} }
// RevokeAllShares supprime tous les liens et change la clé de signature // RevokeAllShares supprime tous les liens et change la clé de signature
// (sauf si elle est imposée par SHARE_SECRET). // (sauf si elle est imposée par SHARE_SECRET ou reprise du Store principal).
func (s *Store) RevokeAllShares() error { func (s *Store) RevokeAllShares() error {
return s.editShares(func(m map[string]shareDisk) (bool, error) { return s.editShares(func(m map[string]shareDisk) (bool, error) {
clear(m) clear(m)
if !s.fixedKey { if !s.fixedKey && s.keyFrom == nil {
if _, err := s.newKey(); err != nil { if _, err := s.newKey(); err != nil {
return false, err return false, err
} }
+926
View File
@@ -0,0 +1,926 @@
package storage
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
// Espaces personnels et favoris par utilisateur :
//
// DATA_DIR/.users/<id>/home espace personnel, un Store complet (historique,
// miniatures, corbeille, tags, partages)
// DATA_DIR/.users/<id>/favoris liens symboliques relatifs vers des schémas
//
// Côté API, l'arborescence commune garde ses chemins. L'espace personnel est
// exposé sous le préfixe virtuel ".perso" et les favoris sous ".favoris" : un
// préfixe caché ne peut pas entrer en conflit avec un dossier réel. Un favori
// pointe vers le schéma d'origine ; il suit ses renommages et déplacements et
// disparaît quand le schéma est supprimé.
const (
// PersoPrefix est la racine virtuelle de l'espace personnel.
PersoPrefix = ".perso"
// FavPrefix est le dossier virtuel des favoris.
FavPrefix = ".favoris"
usersDir = ".users"
homeName = "home"
favName = "favoris"
seededFile = ".seeded" // favoris de .meta.json déjà repris pour cet utilisateur
)
// LocalUser est l'utilisateur unique hors OIDC.
const LocalUser = "local"
// UserID transforme un identifiant (email, sub) en nom de dossier : minuscules,
// caractères hors [a-z0-9._@+-] remplacés par « _ », jamais caché.
func UserID(raw string) string {
s := strings.ToLower(strings.TrimSpace(raw))
var b strings.Builder
for _, r := range s {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || strings.ContainsRune("._@+-", r) {
b.WriteRune(r)
} else {
b.WriteByte('_')
}
}
id := b.String()
if strings.HasPrefix(id, ".") {
id = "_" + id[1:]
}
if id == "" {
return LocalUser
}
if len(id) > 80 {
h := sha256.Sum256([]byte(s))
id = id[:60] + "-" + hex.EncodeToString(h[:8])
}
return id
}
// Spaces réunit l'arborescence commune et les espaces des utilisateurs.
type Spaces struct {
Main *Store
mu sync.Mutex
homes map[string]*Store
favMu sync.Mutex // dossiers de favoris (tous utilisateurs)
}
// NewSpaces crée le gestionnaire d'espaces autour du Store principal.
func NewSpaces(main *Store) *Spaces {
return &Spaces{Main: main, homes: map[string]*Store{}}
}
func (sp *Spaces) usersRoot() string { return filepath.Join(sp.Main.root, usersDir) }
func (sp *Spaces) userDir(id string) string { return filepath.Join(sp.usersRoot(), id) }
func (sp *Spaces) favDir(id string) string { return filepath.Join(sp.userDir(id), favName) }
// home renvoie l'espace personnel de id, créé au premier accès.
func (sp *Spaces) home(id string) (*Store, error) {
if id == "" || UserID(id) != id {
return nil, fmt.Errorf("%w : utilisateur %q", ErrInvalidPath, id)
}
sp.mu.Lock()
defer sp.mu.Unlock()
if st := sp.homes[id]; st != nil {
return st, nil
}
if err := os.MkdirAll(sp.favDir(id), 0o755); err != nil {
return nil, err
}
st, err := New(filepath.Join(sp.userDir(id), homeName))
if err != nil {
return nil, err
}
st.History = sp.Main.History
st.keyFrom = sp.Main
sp.homes[id] = st
sp.seedFavorites(id)
return st, nil
}
// seedFavorites reprend une fois les favoris de .meta.json (avant les espaces
// personnels, ils étaient communs à tous).
func (sp *Spaces) seedFavorites(id string) {
mark := filepath.Join(sp.userDir(id), seededFile)
if _, err := os.Stat(mark); err == nil {
return
}
var paths []string
for p, m := range sp.Main.metaSnapshot() {
if m.Favorite && requireSchemaPath(p) == nil {
paths = append(paths, p)
}
}
sort.Strings(paths)
sp.favMu.Lock()
for _, p := range paths {
if _, abs, err := sp.Main.schema(p); err == nil {
if fi, err := os.Lstat(abs); err == nil && fi.Mode().IsRegular() {
_ = sp.addLink(id, abs)
}
}
}
sp.favMu.Unlock()
_ = os.WriteFile(mark, []byte(time.Now().UTC().Format(time.RFC3339)+"\n"), 0o644)
}
// For renvoie la vue de l'utilisateur id.
func (sp *Spaces) For(id string) (*Space, error) {
h, err := sp.home(id)
if err != nil {
return nil, err
}
return &Space{sp: sp, id: id, home: h}, nil
}
// users liste les utilisateurs qui ont un espace.
func (sp *Spaces) users() []string {
des, _ := os.ReadDir(sp.usersRoot())
var out []string
for _, de := range des {
if de.IsDir() && UserID(de.Name()) == de.Name() {
out = append(out, de.Name())
}
}
return out
}
// ReadShared résout un lien de partage, de l'arborescence commune ou d'un
// espace personnel.
func (sp *Spaces) ReadShared(token string) (Share, []byte, error) {
sh, data, err := sp.Main.ReadShared(token)
if !errors.Is(err, ErrNotFound) {
return sh, data, err
}
for _, id := range sp.users() {
st, herr := sp.home(id)
if herr != nil {
continue
}
if sh, data, herr := st.ReadShared(token); !errors.Is(herr, ErrNotFound) {
return sh, data, herr
}
}
return Share{}, nil, err
}
// --- Liens des favoris -------------------------------------------------------
type favLink struct {
name string // nom du lien dans le dossier des favoris
target string // chemin absolu visé
}
// links lit les liens du dossier des favoris de id ; appelé sous favMu.
func (sp *Spaces) links(id string) []favLink {
dir := sp.favDir(id)
des, _ := os.ReadDir(dir)
var out []favLink
for _, de := range des {
if de.Type()&fs.ModeSymlink == 0 {
continue
}
t, err := os.Readlink(filepath.Join(dir, de.Name()))
if err != nil {
continue
}
if !filepath.IsAbs(t) {
t = filepath.Join(dir, t)
}
out = append(out, favLink{name: de.Name(), target: filepath.Clean(t)})
}
return out
}
// visibleTo indique si l'utilisateur id peut viser abs : arborescence commune
// ou son propre espace personnel.
func (sp *Spaces) visibleTo(id, abs string) bool {
sep := string(os.PathSeparator)
if strings.HasPrefix(abs, sp.usersRoot()+sep) {
return strings.HasPrefix(abs, filepath.Join(sp.userDir(id), homeName)+sep)
}
return strings.HasPrefix(abs, sp.Main.root+sep)
}
// addLink crée un lien vers abs (nom du schéma, suffixé en cas de doublon) ;
// appelé sous favMu.
func (sp *Spaces) addLink(id, abs string) error {
for _, l := range sp.links(id) {
if l.target == abs {
return nil
}
}
dir := sp.favDir(id)
rel, err := filepath.Rel(dir, abs)
if err != nil {
return err
}
base := filepath.Base(abs)
stem, ext := strings.TrimSuffix(base, filepath.Ext(base)), filepath.Ext(base)
name := base
for i := 2; ; i++ {
if _, err := os.Lstat(filepath.Join(dir, name)); errors.Is(err, fs.ErrNotExist) {
break
}
name = fmt.Sprintf("%s (%d)%s", stem, i, ext)
}
return os.Symlink(rel, filepath.Join(dir, name))
}
// relink fait suivre aux favoris de tous les utilisateurs un élément déplacé
// de from vers to (chemins absolus, fichier ou dossier). Un favori qui
// deviendrait invisible pour son utilisateur (schéma passé dans l'espace
// personnel d'un autre) est retiré.
func (sp *Spaces) relink(from, to string) {
sp.favMu.Lock()
defer sp.favMu.Unlock()
for _, id := range sp.users() {
dir := sp.favDir(id)
for _, l := range sp.links(id) {
if l.target != from && !strings.HasPrefix(l.target, from+string(os.PathSeparator)) {
continue
}
nt := to + l.target[len(from):]
lp := filepath.Join(dir, l.name)
_ = os.Remove(lp)
if sp.visibleTo(id, nt) {
if rel, err := filepath.Rel(dir, nt); err == nil {
_ = os.Symlink(rel, lp)
}
}
}
}
}
// unlink retire, chez tous les utilisateurs, les favoris visant abs ou son contenu.
func (sp *Spaces) unlink(abs string) {
sp.favMu.Lock()
defer sp.favMu.Unlock()
for _, id := range sp.users() {
for _, l := range sp.links(id) {
if l.target == abs || strings.HasPrefix(l.target, abs+string(os.PathSeparator)) {
_ = os.Remove(filepath.Join(sp.favDir(id), l.name))
}
}
}
}
// --- Vue d'un utilisateur ------------------------------------------------------
// Space est la vue d'un utilisateur : arborescence commune, espace personnel
// (préfixe .perso) et favoris (.favoris). Ses méthodes prennent et renvoient
// des chemins virtuels.
type Space struct {
sp *Spaces
id string
home *Store
}
// ID renvoie l'identifiant de l'utilisateur.
func (u *Space) ID() string { return u.id }
func cleanVirtual(p string) string { return strings.TrimPrefix(path.Clean("/"+p), "/") }
func favLinkName(p string) (string, bool) {
name, ok := strings.CutPrefix(cleanVirtual(p), FavPrefix+"/")
return name, ok && name != "" && !strings.Contains(name, "/")
}
// route traduit un chemin virtuel en Store et chemin relatif à ce Store.
func (u *Space) route(p string) (*Store, string, error) {
c := cleanVirtual(p)
switch {
case c == PersoPrefix:
return u.home, "", nil
case strings.HasPrefix(c, PersoPrefix+"/"):
return u.home, c[len(PersoPrefix)+1:], nil
case c == FavPrefix || strings.HasPrefix(c, FavPrefix+"/"):
return nil, "", fmt.Errorf("%w : le dossier Favoris ne contient que des liens", ErrInvalidPath)
}
return u.sp.Main, p, nil
}
// virt traduit un chemin relatif à st en chemin virtuel.
func (u *Space) virt(st *Store, rel string) string {
if st == u.sp.Main {
return rel
}
if rel == "" {
return PersoPrefix
}
return PersoPrefix + "/" + rel
}
// fromAbs traduit un chemin absolu en chemin virtuel, s'il est visible.
func (u *Space) fromAbs(abs string) (string, bool) {
for _, st := range []*Store{u.home, u.sp.Main} {
rel, err := filepath.Rel(st.root, abs)
if err != nil || rel == "." || strings.HasPrefix(rel, "..") {
continue
}
c, err := Clean(filepath.ToSlash(rel))
if err != nil || c == "" {
return "", false // .users, .trash, .history...
}
return u.virt(st, c), true
}
return "", false
}
func absOf(st *Store, c string) string { return filepath.Join(st.root, filepath.FromSlash(c)) }
func (u *Space) favSet() map[string]bool {
u.sp.favMu.Lock()
links := u.sp.links(u.id)
u.sp.favMu.Unlock()
out := make(map[string]bool, len(links))
for _, l := range links {
if p, ok := u.fromAbs(l.target); ok {
out[p] = true
}
}
return out
}
func (u *Space) entry(st *Store, e Entry, favs map[string]bool) Entry {
e.Path = u.virt(st, e.Path)
e.Favorite = favs[e.Path]
return e
}
// Favorites liste les favoris de l'utilisateur ; Path est le chemin du schéma
// d'origine (ou .favoris/<lien> si l'original a disparu).
func (u *Space) Favorites() ([]Entry, error) {
u.sp.favMu.Lock()
links := u.sp.links(u.id)
u.sp.favMu.Unlock()
meta := map[*Store]map[string]itemMeta{u.sp.Main: u.sp.Main.metaSnapshot(), u.home: u.home.metaSnapshot()}
out := make([]Entry, 0, len(links))
for _, l := range links {
e := Entry{Name: l.name, Path: FavPrefix + "/" + l.name, Link: l.name, Broken: true}
if p, ok := u.fromAbs(l.target); ok {
if fi, err := os.Stat(l.target); err == nil && fi.Mode().IsRegular() {
st, rel, _ := u.route(p)
e = toEntry(rel, fi)
decorate(&e, meta[st])
e.Path, e.Link, e.Favorite = p, l.name, true
}
}
out = append(out, e)
}
sort.Slice(out, func(i, j int) bool { return fold(out[i].Name) < fold(out[j].Name) })
return out, nil
}
// SetFavorite ajoute ou retire des schémas des favoris. Un chemin
// .favoris/<lien> désigne directement un lien (favori dont l'original a disparu).
func (u *Space) SetFavorite(paths []string, favorite bool) error {
var targets []string
var names []string
for _, p := range paths {
if name, ok := favLinkName(p); ok {
if !favorite {
names = append(names, name)
}
continue
}
st, rel, err := u.route(p)
if err != nil {
return err
}
_, abs, err := st.schema(rel)
if err != nil {
return fmt.Errorf("%w (seuls les schémas peuvent être favoris)", err)
}
fi, err := os.Lstat(abs)
if err != nil {
return mapErr(err)
}
if !fi.Mode().IsRegular() {
return fmt.Errorf("%w : %s n'est pas un schéma", ErrInvalidPath, p)
}
targets = append(targets, abs)
}
u.sp.favMu.Lock()
defer u.sp.favMu.Unlock()
dir := u.sp.favDir(u.id)
for _, name := range names {
if fi, err := os.Lstat(filepath.Join(dir, name)); err == nil && fi.Mode()&fs.ModeSymlink != 0 {
_ = os.Remove(filepath.Join(dir, name))
}
}
for _, abs := range targets {
if favorite {
if err := u.sp.addLink(u.id, abs); err != nil {
return err
}
continue
}
for _, l := range u.sp.links(u.id) {
if l.target == abs {
_ = os.Remove(filepath.Join(dir, l.name))
}
}
}
return nil
}
// --- Opérations sur les schémas, en chemins virtuels -----------------------------
func isRoot(rel string) bool {
c, err := Clean(rel)
return err == nil && c == ""
}
// List renvoie le contenu d'un dossier. La racine commune commence par les
// dossiers virtuels Favoris et Personnel.
func (u *Space) List(p string) ([]Entry, error) {
if cleanVirtual(p) == FavPrefix {
return u.Favorites()
}
st, rel, err := u.route(p)
if err != nil {
return nil, err
}
es, err := st.List(rel)
if err != nil {
return nil, err
}
favs := u.favSet()
out := make([]Entry, 0, len(es)+2)
if st == u.sp.Main && isRoot(rel) {
out = append(out,
Entry{Name: "Favoris", Path: FavPrefix, IsDir: true, Special: "favoris"},
Entry{Name: "Personnel", Path: PersoPrefix, IsDir: true, Special: "perso"})
}
for _, e := range es {
out = append(out, u.entry(st, e, favs))
}
return out, nil
}
// Search cherche dans l'espace personnel puis l'arborescence commune.
func (u *Space) Search(q string, f Filter, limit int) ([]Hit, error) {
favs := u.favSet()
inner := Filter{Tag: f.Tag}
tag := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(f.Tag), "#"))
if f.Favorite && strings.TrimSpace(q) == "" && tag == "" {
es, err := u.Favorites()
out := []Hit{}
for _, e := range es {
if !e.Broken && len(out) < limit {
out = append(out, Hit{Entry: e})
}
}
return out, err
}
lim := limit
if f.Favorite {
lim = 1 << 30 // filtrés ensuite
}
var all []Hit
for _, st := range []*Store{u.home, u.sp.Main} {
hs, err := st.Search(q, inner, lim)
if err != nil {
return nil, err
}
for _, h := range hs {
h.Entry = u.entry(st, h.Entry, favs)
if !f.Favorite || h.Favorite {
all = append(all, h)
}
}
}
// Correspondances sur le nom (sans extrait) d'abord, comme Store.Search.
sort.SliceStable(all, func(i, j int) bool { return all[i].Snippet == "" && all[j].Snippet != "" })
if len(all) > limit {
all = all[:limit]
}
if all == nil {
all = []Hit{}
}
return all, nil
}
// Tags réunit les tags de l'arborescence commune et de l'espace personnel.
func (u *Space) Tags() ([]TagCount, error) {
idx := map[string]int{}
out := []TagCount{}
for _, st := range []*Store{u.sp.Main, u.home} {
ts, err := st.Tags()
if err != nil {
return nil, err
}
for _, t := range ts {
if i, ok := idx[fold(t.Name)]; ok {
out[i].Count += t.Count
} else {
idx[fold(t.Name)] = len(out)
out = append(out, t)
}
}
}
sort.Slice(out, func(i, j int) bool { return fold(out[i].Name) < fold(out[j].Name) })
return out, nil
}
// EditTags ajoute et retire des tags (voir Store.EditTags).
func (u *Space) EditTags(paths, add, remove []string) error {
groups := map[*Store][]string{}
for _, p := range paths {
st, rel, err := u.route(p)
if err != nil {
return err
}
groups[st] = append(groups[st], rel)
}
for st, rels := range groups {
if err := st.EditTags(rels, add, remove); err != nil {
return err
}
}
return nil
}
func (u *Space) Read(p string) ([]byte, string, error) {
st, rel, err := u.route(p)
if err != nil {
return nil, "", err
}
return st.Read(rel)
}
func (u *Space) Write(p string, data []byte, ifMatch string, create bool) (Entry, string, error) {
st, rel, err := u.route(p)
if err != nil {
return Entry{}, "", err
}
e, etag, err := st.Write(rel, data, ifMatch, create)
return u.entry(st, e, nil), etag, err
}
func (u *Space) Mkdir(p string) (Entry, error) {
st, rel, err := u.route(p)
if err != nil {
return Entry{}, err
}
e, err := st.Mkdir(rel)
return u.entry(st, e, nil), err
}
// Move renomme ou déplace un élément, y compris entre l'arborescence commune
// et l'espace personnel. Les favoris suivent.
func (u *Space) Move(from, to string) (Entry, error) {
sf, rf, err := u.route(from)
if err != nil {
return Entry{}, err
}
st, rt, err := u.route(to)
if err != nil {
return Entry{}, err
}
if sf != st {
e, err := u.sp.moveAcross(sf, rf, st, rt)
return u.entry(st, e, nil), err
}
cf, _ := Clean(rf)
e, err := sf.Move(rf, rt)
if err != nil {
return Entry{}, err
}
u.sp.relink(absOf(sf, cf), absOf(sf, e.Path))
return u.entry(st, e, nil), nil
}
// moveAcross déplace un élément d'un Store à un autre (même volume) avec ses
// miniatures, son historique, ses tags et ses liens de partage.
func (sp *Spaces) moveAcross(src *Store, from string, dst *Store, to string) (Entry, error) {
cf, af, err := src.resolve(from)
if err != nil {
return Entry{}, err
}
ct, at, err := dst.resolve(to)
if err != nil {
return Entry{}, err
}
if cf == "" || ct == "" {
return Entry{}, ErrInvalidPath
}
fi, err := os.Lstat(af)
if err != nil {
return Entry{}, mapErr(err)
}
if !fi.IsDir() {
if err := requireSchemaPath(ct); err != nil {
return Entry{}, err
}
}
if _, err := os.Lstat(at); err == nil {
return Entry{}, ErrExists
}
if err := os.MkdirAll(filepath.Dir(at), 0o755); err != nil {
return Entry{}, err
}
if err := os.Rename(af, at); err != nil {
return Entry{}, mapErr(err)
}
moveAside(src.thumbAbs(cf, fi.IsDir()), dst.thumbAbs(ct, fi.IsDir()))
moveAside(src.historyAbs(cf), dst.historyAbs(ct))
dst.putMeta(ct, src.takeMeta(cf))
dst.putShares(ct, src.takeShares(cf))
sp.relink(af, at)
if fi, err = os.Stat(at); err != nil {
return Entry{}, err
}
return toEntry(ct, fi), nil
}
// moveAside déplace src vers dst s'il existe (miniatures, historique).
func moveAside(src, dst string) {
if _, err := os.Stat(src); err != nil {
return
}
_ = os.RemoveAll(dst)
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err == nil {
_ = os.Rename(src, dst)
}
}
// MoveMany déplace plusieurs éléments dans le dossier dir (voir Store.MoveMany).
func (u *Space) MoveMany(paths []string, dir string) ([]MoveResult, error) {
sd, rd, err := u.route(dir)
if err != nil {
return nil, err
}
cd, ad, err := sd.resolve(rd)
if err != nil {
return nil, err
}
fi, err := os.Stat(ad)
if err != nil {
return nil, mapErr(err)
}
if !fi.IsDir() {
return nil, fmt.Errorf("%w : la destination n'est pas un dossier", ErrInvalidPath)
}
vd := u.virt(sd, cd)
out := make([]MoveResult, 0, len(paths))
for _, p := range paths {
r := MoveResult{From: p}
sf, rf, err := u.route(p)
var c string
if err == nil {
c, err = Clean(rf)
}
switch {
case err != nil || c == "":
r.err = ErrInvalidPath
case sf == sd && strings.TrimPrefix(path.Dir("/"+c), "/") == cd:
r.From = u.virt(sf, c)
r.To = r.From // déjà dans le dossier cible
default:
r.From = u.virt(sf, c)
var e Entry
if e, r.err = u.Move(r.From, path.Join(vd, path.Base(c))); r.err == nil {
r.To = e.Path
}
}
if r.err != nil {
r.Error = r.err.Error()
}
out = append(out, r)
}
return out, nil
}
// Copy duplique un schéma, éventuellement vers l'autre espace.
func (u *Space) Copy(from, to string) (Entry, error) {
sf, rf, err := u.route(from)
if err != nil {
return Entry{}, err
}
st, rt, err := u.route(to)
if err != nil {
return Entry{}, err
}
if sf == st {
e, err := sf.Copy(rf, rt)
return u.entry(st, e, nil), err
}
data, _, err := sf.Read(rf)
if err != nil {
return Entry{}, err
}
e, _, err := st.Write(rt, data, "", true)
return u.entry(st, e, nil), err
}
// Delete met un élément à la corbeille de son espace ; les favoris qui le
// visent disparaissent. Sur .favoris/<lien>, seul le lien est retiré.
func (u *Space) Delete(p string, recursive bool) error {
if _, ok := favLinkName(p); ok {
return u.SetFavorite([]string{p}, false)
}
st, rel, err := u.route(p)
if err != nil {
return err
}
_, abs, err := st.resolve(rel)
if err != nil {
return err
}
if err := st.Delete(rel, recursive); err != nil {
return err
}
u.sp.unlink(abs)
return nil
}
func (u *Space) ReadThumb(p string) ([]byte, time.Time, error) {
st, rel, err := u.route(p)
if err != nil {
return nil, time.Time{}, err
}
return st.ReadThumb(rel)
}
func (u *Space) WriteThumb(p string, png []byte) error {
st, rel, err := u.route(p)
if err != nil {
return err
}
return st.WriteThumb(rel, png)
}
func (u *Space) Versions(p string) ([]Version, error) {
st, rel, err := u.route(p)
if err != nil {
return nil, err
}
return st.Versions(rel)
}
func (u *Space) ReadVersion(p, id string) ([]byte, error) {
st, rel, err := u.route(p)
if err != nil {
return nil, err
}
return st.ReadVersion(rel, id)
}
func (u *Space) Snapshot(p string) (Version, error) {
st, rel, err := u.route(p)
if err != nil {
return Version{}, err
}
return st.Snapshot(rel)
}
func (u *Space) Restore(p, id string) (Entry, string, error) {
st, rel, err := u.route(p)
if err != nil {
return Entry{}, "", err
}
e, etag, err := st.Restore(rel, id)
return u.entry(st, e, nil), etag, err
}
// Shares liste les liens d'un schéma, ou tous ceux visibles (arborescence
// commune et espace personnel) si p est vide.
func (u *Space) Shares(p string) ([]Share, error) {
stores := []*Store{u.sp.Main, u.home}
rel := ""
if p != "" {
st, r, err := u.route(p)
if err != nil {
return nil, err
}
stores, rel = []*Store{st}, r
}
out := []Share{}
for _, st := range stores {
l, err := st.Shares(rel)
if err != nil {
return nil, err
}
for _, sh := range l {
sh.Path = u.virt(st, sh.Path)
out = append(out, sh)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Created.After(out[j].Created) })
return out, nil
}
func (u *Space) CreateShare(p string, ttl time.Duration) (Share, error) {
st, rel, err := u.route(p)
if err != nil {
return Share{}, err
}
sh, err := st.CreateShare(rel, ttl)
sh.Path = u.virt(st, sh.Path)
return sh, err
}
// RevokeShare supprime un lien de l'arborescence commune ou de l'espace personnel.
func (u *Space) RevokeShare(id string) error {
err := u.sp.Main.RevokeShare(id)
if errors.Is(err, ErrNotFound) {
err = u.home.RevokeShare(id)
}
return err
}
// RevokeAllShares révoque tous les liens, espaces personnels compris.
func (u *Space) RevokeAllShares() error {
if err := u.sp.Main.RevokeAllShares(); err != nil {
return err
}
for _, id := range u.sp.users() {
if st, err := u.sp.home(id); err == nil {
if err := st.RevokeAllShares(); err != nil {
return err
}
}
}
return nil
}
// --- Transferts de métadonnées entre Stores ----------------------------------------
// takeMeta retire et renvoie les métadonnées de c et de son contenu (clés
// relatives à c).
func (s *Store) takeMeta(c string) map[string]itemMeta {
s.metaMu.Lock()
defer s.metaMu.Unlock()
if s.loadMeta() != nil {
return nil
}
out := map[string]itemMeta{}
for k, m := range s.meta {
if under(k, c) {
out[k[len(c):]] = m
delete(s.meta, k)
}
}
if len(out) > 0 {
_ = s.saveMeta()
}
return out
}
func (s *Store) putMeta(c string, m map[string]itemMeta) {
if len(m) == 0 {
return
}
s.metaMu.Lock()
defer s.metaMu.Unlock()
if s.loadMeta() != nil {
return
}
for suffix, v := range m {
s.meta[c+suffix] = v
}
_ = s.saveMeta()
}
func (s *Store) takeShares(c string) map[string]shareDisk {
out := map[string]shareDisk{}
_ = s.editShares(func(m map[string]shareDisk) (bool, error) {
for id, d := range m {
if under(d.Path, c) {
d.Path = d.Path[len(c):] // suffixe relatif à c
out[id] = d
delete(m, id)
}
}
return len(out) > 0, nil
})
return out
}
func (s *Store) putShares(c string, add map[string]shareDisk) {
if len(add) == 0 {
return
}
_ = s.editShares(func(m map[string]shareDisk) (bool, error) {
for id, d := range add {
d.Path = c + d.Path
m[id] = d
}
return true, nil
})
}
+6
View File
@@ -50,6 +50,11 @@ type Entry struct {
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
Favorite bool `json:"favorite,omitempty"` Favorite bool `json:"favorite,omitempty"`
// Dossiers virtuels et favoris (spaces.go).
Special string `json:"special,omitempty"` // "favoris" ou "perso" : entrée de la racine
Link string `json:"link,omitempty"` // nom du lien dans le dossier Favoris
Broken bool `json:"broken,omitempty"` // favori dont le schéma d'origine a disparu
} }
// Store est un dépôt de schémas enraciné dans un répertoire. // Store est un dépôt de schémas enraciné dans un répertoire.
@@ -68,6 +73,7 @@ type Store struct {
sharesMu sync.Mutex // liens de partage (share.go) sharesMu sync.Mutex // liens de partage (share.go)
shareKey []byte // clé HMAC ; nil : pas encore chargée shareKey []byte // clé HMAC ; nil : pas encore chargée
fixedKey bool // clé imposée par SHARE_SECRET fixedKey bool // clé imposée par SHARE_SECRET
keyFrom *Store // espace personnel : clé de signature du Store principal
} }
// New crée le répertoire racine si besoin et renvoie un Store. // New crée le répertoire racine si besoin et renvoie un Store.