Tags et favoris, filtres dans l'explorateur
- DATA_DIR/.meta.json : tags et favoris par chemin, suivis aux renommages, déplacements, copies (tags seuls) et suppressions. - API : GET /api/tags, POST /api/meta/favorite, POST /api/meta/tags ; Entry porte tags et favorite ; /api/search accepte tag= et fav=true (q facultatif). - Tags comparés sans casse ni accents, écriture existante conservée ; ils participent à la recherche texte. - Explorateur : ligne de filtres (Favoris, tags avec leur nombre), étoile et tags sur les éléments, menu Favori / Tags…, actions sur la sélection, dialogue Tags (ajout, retrait, suggestions). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H6J7vKwXx7uopxKhWfFJpm
This commit is contained in:
1 parent
c78df092c1
commit
c1ea1e84a7
13 files changed
+880
-33
No files matched your search
@@ -0,0 +1,286 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Tags et favoris des fichiers et dossiers, dans .meta.json à la racine :
|
||||
// {"items":{"a/b.excalidraw":{"tags":["NSX"],"favorite":true}}}. Ils suivent
|
||||
// les renommages, déplacements et suppressions.
|
||||
|
||||
const metaFile = ".meta.json"
|
||||
|
||||
const (
|
||||
// MaxTagLen borne la longueur d'un tag (en caractères).
|
||||
MaxTagLen = 40
|
||||
// MaxTags borne le nombre de tags d'un élément.
|
||||
MaxTags = 20
|
||||
)
|
||||
|
||||
type itemMeta struct {
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Favorite bool `json:"favorite,omitempty"`
|
||||
}
|
||||
|
||||
func (m itemMeta) empty() bool { return len(m.Tags) == 0 && !m.Favorite }
|
||||
|
||||
// TagCount est un tag utilisé et son nombre d'éléments.
|
||||
type TagCount struct {
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// loadMeta charge .meta.json au premier accès ; appelé sous metaMu.
|
||||
func (s *Store) loadMeta() error {
|
||||
if s.meta != nil {
|
||||
return nil
|
||||
}
|
||||
var d struct {
|
||||
Items map[string]itemMeta `json:"items"`
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(s.root, metaFile))
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
if err := json.Unmarshal(b, &d); err != nil {
|
||||
return fmt.Errorf("%s : %w", metaFile, err)
|
||||
}
|
||||
}
|
||||
if d.Items == nil {
|
||||
d.Items = map[string]itemMeta{}
|
||||
}
|
||||
s.meta = d.Items
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveMeta écrit .meta.json ; appelé sous metaMu.
|
||||
func (s *Store) saveMeta() error {
|
||||
for k, m := range s.meta {
|
||||
if m.empty() {
|
||||
delete(s.meta, k)
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(map[string]any{"items": s.meta})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return atomicWrite(filepath.Join(s.root, metaFile), b)
|
||||
}
|
||||
|
||||
// metaSnapshot renvoie une copie des métadonnées (pour List et Search).
|
||||
func (s *Store) metaSnapshot() map[string]itemMeta {
|
||||
s.metaMu.Lock()
|
||||
defer s.metaMu.Unlock()
|
||||
if s.loadMeta() != nil {
|
||||
return map[string]itemMeta{}
|
||||
}
|
||||
out := make(map[string]itemMeta, len(s.meta))
|
||||
for k, v := range s.meta {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func decorate(e *Entry, meta map[string]itemMeta) {
|
||||
if m, ok := meta[e.Path]; ok {
|
||||
e.Tags, e.Favorite = m.Tags, m.Favorite
|
||||
}
|
||||
}
|
||||
|
||||
func normTag(t string) (string, error) {
|
||||
t = strings.Join(strings.Fields(strings.TrimPrefix(strings.TrimSpace(t), "#")), " ")
|
||||
switch {
|
||||
case t == "", !utf8.ValidString(t), strings.ContainsAny(t, ",\x00"):
|
||||
return "", fmt.Errorf("%w : tag %q", ErrInvalidPath, t)
|
||||
case utf8.RuneCountInString(t) > MaxTagLen:
|
||||
return "", fmt.Errorf("%w : tag de %d caractères max", ErrInvalidPath, MaxTagLen)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// editMeta vérifie que chaque chemin existe puis applique edit à ses
|
||||
// métadonnées, et enregistre le tout.
|
||||
func (s *Store) editMeta(paths []string, edit func(*itemMeta) error) error {
|
||||
cs := make([]string, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
c, abs, err := s.resolve(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c == "" {
|
||||
return ErrInvalidPath
|
||||
}
|
||||
if _, err := os.Lstat(abs); err != nil {
|
||||
return mapErr(err)
|
||||
}
|
||||
cs = append(cs, c)
|
||||
}
|
||||
s.metaMu.Lock()
|
||||
defer s.metaMu.Unlock()
|
||||
if err := s.loadMeta(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range cs {
|
||||
m := s.meta[c]
|
||||
m.Tags = append([]string(nil), m.Tags...)
|
||||
if err := edit(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
s.meta[c] = m
|
||||
}
|
||||
return s.saveMeta()
|
||||
}
|
||||
|
||||
// SetFavorite ajoute ou retire des éléments des favoris.
|
||||
func (s *Store) SetFavorite(paths []string, favorite bool) error {
|
||||
return s.editMeta(paths, func(m *itemMeta) error {
|
||||
m.Favorite = favorite
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// EditTags ajoute et retire des tags sur des éléments. Les tags sont comparés
|
||||
// sans casse ni accents ; un tag déjà utilisé garde son écriture d'origine.
|
||||
func (s *Store) EditTags(paths, add, remove []string) error {
|
||||
var adds []string
|
||||
for _, t := range add {
|
||||
n, err := normTag(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adds = append(adds, n)
|
||||
}
|
||||
drop := map[string]bool{}
|
||||
for _, t := range remove {
|
||||
drop[fold(strings.TrimSpace(t))] = true
|
||||
}
|
||||
canon := map[string]string{} // écriture existante de chaque tag
|
||||
s.metaMu.Lock()
|
||||
err := s.loadMeta()
|
||||
for _, m := range s.meta {
|
||||
for _, t := range m.Tags {
|
||||
if _, ok := canon[fold(t)]; !ok {
|
||||
canon[fold(t)] = t
|
||||
}
|
||||
}
|
||||
}
|
||||
s.metaMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.editMeta(paths, func(m *itemMeta) error {
|
||||
kept := m.Tags[:0]
|
||||
has := map[string]bool{}
|
||||
for _, t := range m.Tags {
|
||||
if !drop[fold(t)] && !has[fold(t)] {
|
||||
kept = append(kept, t)
|
||||
has[fold(t)] = true
|
||||
}
|
||||
}
|
||||
for _, t := range adds {
|
||||
if has[fold(t)] {
|
||||
continue
|
||||
}
|
||||
if c, ok := canon[fold(t)]; ok {
|
||||
t = c
|
||||
}
|
||||
kept = append(kept, t)
|
||||
has[fold(t)] = true
|
||||
}
|
||||
if len(kept) > MaxTags {
|
||||
return fmt.Errorf("%w : %d tags max par élément", ErrInvalidPath, MaxTags)
|
||||
}
|
||||
sort.Slice(kept, func(i, j int) bool { return fold(kept[i]) < fold(kept[j]) })
|
||||
m.Tags = kept
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Tags liste les tags utilisés, triés par nom, avec leur nombre d'éléments.
|
||||
func (s *Store) Tags() ([]TagCount, error) {
|
||||
s.metaMu.Lock()
|
||||
defer s.metaMu.Unlock()
|
||||
if err := s.loadMeta(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idx := map[string]int{}
|
||||
out := []TagCount{}
|
||||
for _, m := range s.meta {
|
||||
for _, t := range m.Tags {
|
||||
k := fold(t)
|
||||
if i, ok := idx[k]; ok {
|
||||
out[i].Count++
|
||||
} else {
|
||||
idx[k] = len(out)
|
||||
out = append(out, TagCount{Name: t, Count: 1})
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return fold(out[i].Name) < fold(out[j].Name) })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Les opérations suivantes accompagnent Move/Copy/Delete (au mieux).
|
||||
|
||||
func under(k, c string) bool { return k == c || strings.HasPrefix(k, c+"/") }
|
||||
|
||||
func (s *Store) moveMeta(from, to string) {
|
||||
s.metaMu.Lock()
|
||||
defer s.metaMu.Unlock()
|
||||
if s.loadMeta() != nil {
|
||||
return
|
||||
}
|
||||
moved := map[string]itemMeta{}
|
||||
for k, m := range s.meta {
|
||||
if under(k, from) {
|
||||
delete(s.meta, k)
|
||||
moved[to+k[len(from):]] = m
|
||||
}
|
||||
}
|
||||
for k, m := range moved {
|
||||
s.meta[k] = m
|
||||
}
|
||||
if len(moved) > 0 {
|
||||
_ = s.saveMeta()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) copyMeta(from, to string) {
|
||||
s.metaMu.Lock()
|
||||
defer s.metaMu.Unlock()
|
||||
if s.loadMeta() != nil {
|
||||
return
|
||||
}
|
||||
if m, ok := s.meta[from]; ok && len(m.Tags) > 0 {
|
||||
s.meta[to] = itemMeta{Tags: append([]string(nil), m.Tags...)} // pas le favori
|
||||
_ = s.saveMeta()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) removeMeta(c string) {
|
||||
s.metaMu.Lock()
|
||||
defer s.metaMu.Unlock()
|
||||
if s.loadMeta() != nil {
|
||||
return
|
||||
}
|
||||
changed := false
|
||||
for k := range s.meta {
|
||||
if under(k, c) {
|
||||
delete(s.meta, k)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
_ = s.saveMeta()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTagsFavorites(t *testing.T) {
|
||||
s := newStore(t)
|
||||
s.Write("d/a.excalidraw", []byte(doc), "", true)
|
||||
s.Write("d/b.excalidraw", []byte(doc), "", true)
|
||||
s.Write("c.excalidraw", []byte(doc), "", true)
|
||||
|
||||
if err := s.EditTags([]string{"d/a.excalidraw", "d/b.excalidraw"}, []string{"#NSX", " Réseau cœur "}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Même tag sans casse ni accents : l'écriture existante est reprise.
|
||||
if err := s.EditTags([]string{"c.excalidraw"}, []string{"nsx", "reseau cœur"}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tags, _ := s.Tags()
|
||||
if len(tags) != 2 || tags[0] != (TagCount{"NSX", 3}) || tags[1] != (TagCount{"Réseau cœur", 3}) {
|
||||
t.Fatalf("Tags = %+v", tags)
|
||||
}
|
||||
if err := s.EditTags([]string{"d/b.excalidraw"}, nil, []string{"NSX"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.SetFavorite([]string{"d/b.excalidraw", "d"}, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, bad := range [][]string{{""}, {"absent.excalidraw"}, {"../x"}} {
|
||||
if err := s.SetFavorite(bad, true); err == nil {
|
||||
t.Errorf("SetFavorite(%q) aurait dû échouer", bad)
|
||||
}
|
||||
}
|
||||
if err := s.EditTags([]string{"c.excalidraw"}, []string{"a,b"}, nil); !errors.Is(err, ErrInvalidPath) {
|
||||
t.Fatalf("tag invalide : %v", err)
|
||||
}
|
||||
|
||||
l, _ := s.List("d")
|
||||
if len(l[0].Tags) != 2 || l[0].Favorite || len(l[1].Tags) != 1 || !l[1].Favorite {
|
||||
t.Fatalf("List = %+v", l)
|
||||
}
|
||||
|
||||
// Filtres de recherche.
|
||||
if h, _ := s.Search("", Filter{Tag: "nsx"}, 10); len(h) != 2 {
|
||||
t.Fatalf("tag : %+v", h)
|
||||
}
|
||||
if h, _ := s.Search("", Filter{Favorite: true}, 10); len(h) != 2 {
|
||||
t.Fatalf("favoris : %+v", h)
|
||||
}
|
||||
if h, _ := s.Search("reseau", Filter{Favorite: true}, 10); len(h) != 1 || h[0].Path != "d/b.excalidraw" {
|
||||
t.Fatalf("mot (tag) + favoris : %+v", h)
|
||||
}
|
||||
|
||||
// Les métadonnées suivent déplacement, copie (tags seuls) et suppression,
|
||||
// et survivent au rechargement.
|
||||
s.Move("d", "e")
|
||||
s.Copy("e/b.excalidraw", "copie.excalidraw")
|
||||
s.Delete("c.excalidraw", false)
|
||||
s2, _ := New(s.root)
|
||||
l, _ = s2.List("")
|
||||
got := map[string]Entry{}
|
||||
for _, e := range l {
|
||||
got[e.Path] = e
|
||||
}
|
||||
if !got["e"].Favorite || got["copie.excalidraw"].Favorite || len(got["copie.excalidraw"].Tags) != 1 {
|
||||
t.Fatalf("après déplacement/copie : %+v", l)
|
||||
}
|
||||
if tags, _ := s2.Tags(); len(tags) != 2 || tags[0] != (TagCount{"NSX", 1}) {
|
||||
t.Fatalf("après suppression : %+v", tags)
|
||||
}
|
||||
}
|
||||
@@ -139,14 +139,23 @@ func snippet(d indexDoc, i, n int) string {
|
||||
return pre + string(runes[lo:hi]) + post
|
||||
}
|
||||
|
||||
// Search renvoie les entrées dont le nom ou, pour un schéma, le contenu
|
||||
// contient tous les mots de q (casse et accents ignorés). Les correspondances
|
||||
// sur le nom viennent en premier.
|
||||
func (s *Store) Search(q string, limit int) ([]Hit, error) {
|
||||
// Filter restreint une recherche aux éléments portant un tag ou favoris.
|
||||
type Filter struct {
|
||||
Tag string
|
||||
Favorite bool
|
||||
}
|
||||
|
||||
// Search renvoie les entrées dont le nom, les tags ou, pour un schéma, le
|
||||
// contenu contiennent tous les mots de q (casse et accents ignorés), et qui
|
||||
// passent le filtre. Les correspondances sur le nom viennent en premier.
|
||||
// q peut être vide si un filtre est donné.
|
||||
func (s *Store) Search(q string, f Filter, limit int) ([]Hit, error) {
|
||||
terms := strings.Fields(fold(q))
|
||||
if len(terms) == 0 {
|
||||
tag := fold(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(f.Tag), "#")))
|
||||
if len(terms) == 0 && tag == "" && !f.Favorite {
|
||||
return []Hit{}, nil
|
||||
}
|
||||
meta := s.metaSnapshot()
|
||||
s.index.mu.Lock()
|
||||
defer s.index.mu.Unlock()
|
||||
if s.index.docs == nil {
|
||||
@@ -176,13 +185,23 @@ func (s *Store) Search(q string, limit int) ([]Hit, error) {
|
||||
return nil
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
name := fold(d.Name())
|
||||
hit := Hit{Entry: toEntry(rel, fi)}
|
||||
decorate(&hit.Entry, meta)
|
||||
if f.Favorite && !hit.Favorite {
|
||||
return nil
|
||||
}
|
||||
tags := fold(strings.Join(hit.Tags, "\n"))
|
||||
if tag != "" && !strings.Contains("\n"+tags+"\n", "\n"+tag+"\n") {
|
||||
return nil
|
||||
}
|
||||
name := fold(d.Name()) + "\n" + tags
|
||||
var doc indexDoc
|
||||
if !d.IsDir() {
|
||||
seen[rel] = true
|
||||
doc = s.content(rel, p, fi)
|
||||
if len(terms) > 0 {
|
||||
doc = s.content(rel, p, fi)
|
||||
}
|
||||
}
|
||||
hit := Hit{Entry: toEntry(rel, fi)}
|
||||
inName := true
|
||||
for _, t := range terms {
|
||||
if strings.Contains(name, t) {
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestSearchContent(t *testing.T) {
|
||||
s.Write("réplication.excalidraw", scene(`rien`), "", true)
|
||||
s.Write("supprimé.excalidraw", []byte(`{"elements":[{"type":"text","text":"172.27.207.99","isDeleted":true}]}`), "", true)
|
||||
|
||||
hits, err := s.Search("172.27.207", 10)
|
||||
hits, err := s.Search("172.27.207", Filter{}, 10)
|
||||
if err != nil || len(hits) != 1 || hits[0].Path != "réseau/sdn.excalidraw" {
|
||||
t.Fatalf("IP : %+v, %v", hits, err)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func TestSearchContent(t *testing.T) {
|
||||
}
|
||||
|
||||
// Accents et casse ignorés ; le nom passe avant le contenu.
|
||||
hits, _ = s.Search("REPLICATION", 10)
|
||||
hits, _ = s.Search("REPLICATION", Filter{}, 10)
|
||||
if len(hits) != 2 || hits[0].Path != "réplication.excalidraw" || hits[1].Path != "stockage/vsan.excalidraw" {
|
||||
t.Fatalf("accents : %+v", hits)
|
||||
}
|
||||
@@ -39,24 +39,24 @@ func TestSearchContent(t *testing.T) {
|
||||
}
|
||||
|
||||
// Tous les mots doivent correspondre, dans le nom ou le contenu.
|
||||
if hits, _ := s.Search("sdn overlay", 10); len(hits) != 1 {
|
||||
if hits, _ := s.Search("sdn overlay", Filter{}, 10); len(hits) != 1 {
|
||||
t.Fatalf("nom + contenu : %+v", hits)
|
||||
}
|
||||
if hits, _ := s.Search("sdn vsan", 10); len(hits) != 0 {
|
||||
if hits, _ := s.Search("sdn vsan", Filter{}, 10); len(hits) != 0 {
|
||||
t.Fatalf("ET logique : %+v", hits)
|
||||
}
|
||||
// Les dossiers correspondent par leur nom.
|
||||
if hits, _ := s.Search("stock", 10); len(hits) != 1 || !hits[0].IsDir {
|
||||
if hits, _ := s.Search("stock", Filter{}, 10); len(hits) != 1 || !hits[0].IsDir {
|
||||
t.Fatalf("dossier : %+v", hits)
|
||||
}
|
||||
|
||||
// Une modification est prise en compte (date ou taille changée).
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
s.Write("réseau/sdn.excalidraw", scene(`Edge NSX 10.0.0.1`), "", false)
|
||||
if hits, _ := s.Search("172.27.207", 10); len(hits) != 0 {
|
||||
if hits, _ := s.Search("172.27.207", Filter{}, 10); len(hits) != 0 {
|
||||
t.Fatalf("index périmé : %+v", hits)
|
||||
}
|
||||
if hits, _ := s.Search("10.0.0", 10); len(hits) != 1 {
|
||||
if hits, _ := s.Search("10.0.0", Filter{}, 10); len(hits) != 1 {
|
||||
t.Fatalf("index non mis à jour : %+v", hits)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ type Entry struct {
|
||||
IsDir bool `json:"isDir"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime time.Time `json:"modTime"`
|
||||
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Favorite bool `json:"favorite,omitempty"`
|
||||
}
|
||||
|
||||
// Store est un dépôt de schémas enraciné dans un répertoire.
|
||||
@@ -58,6 +61,9 @@ type Store struct {
|
||||
|
||||
settingsMu sync.Mutex // sérialise les lectures-modifications de .settings.json
|
||||
index textIndex // texte des schémas pour la recherche (search.go)
|
||||
|
||||
metaMu sync.Mutex // tags et favoris (meta.go)
|
||||
meta map[string]itemMeta // nil : pas encore chargé
|
||||
}
|
||||
|
||||
// New crée le répertoire racine si besoin et renvoie un Store.
|
||||
@@ -165,6 +171,7 @@ func (s *Store) List(rel string) ([]Entry, error) {
|
||||
return nil, mapErr(err)
|
||||
}
|
||||
out := make([]Entry, 0, len(des))
|
||||
meta := s.metaSnapshot()
|
||||
for _, de := range des {
|
||||
if !visible(de.Name(), de.IsDir()) || de.Type()&fs.ModeSymlink != 0 {
|
||||
continue
|
||||
@@ -173,7 +180,9 @@ func (s *Store) List(rel string) ([]Entry, error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, toEntry(path.Join(c, de.Name()), fi))
|
||||
e := toEntry(path.Join(c, de.Name()), fi)
|
||||
decorate(&e, meta)
|
||||
out = append(out, e)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].IsDir != out[j].IsDir {
|
||||
@@ -335,6 +344,7 @@ func (s *Store) Move(from, to string) (Entry, error) {
|
||||
}
|
||||
s.moveThumb(cf, ct, fi.IsDir())
|
||||
s.moveHistory(cf, ct)
|
||||
s.moveMeta(cf, ct)
|
||||
fi, err = os.Stat(at)
|
||||
if err != nil {
|
||||
return Entry{}, err
|
||||
@@ -400,6 +410,9 @@ func (s *Store) Copy(from, to string) (Entry, error) {
|
||||
e, _, err := s.Write(to, data, "", true)
|
||||
if err == nil {
|
||||
s.copyThumb(from, e.Path)
|
||||
if cf, err := Clean(from); err == nil {
|
||||
s.copyMeta(cf, e.Path)
|
||||
}
|
||||
}
|
||||
return e, err
|
||||
}
|
||||
@@ -436,6 +449,7 @@ func (s *Store) Delete(rel string, recursive bool) error {
|
||||
}
|
||||
s.removeThumb(c, fi.IsDir())
|
||||
s.trashHistory(c, trash)
|
||||
s.removeMeta(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestListMoveDelete(t *testing.T) {
|
||||
if _, err := s.Move("dir", "dir/sub"); !errors.Is(err, ErrInvalidPath) {
|
||||
t.Fatalf("déplacement dans soi-même : %v", err)
|
||||
}
|
||||
if res, _ := s.Search("Y.EXCAL", 10); len(res) != 1 || res[0].Path != "dir/y.excalidraw" {
|
||||
if res, _ := s.Search("Y.EXCAL", Filter{}, 10); len(res) != 1 || res[0].Path != "dir/y.excalidraw" {
|
||||
t.Fatalf("Search = %+v", res)
|
||||
}
|
||||
if err := s.Delete("dir", false); !errors.Is(err, ErrNotEmpty) {
|
||||
|
||||
Reference in new issue
Block a user