feat(tree): persist custom node icons and animate folder expansion
This commit is contained in:
1 parent
4a8902a3f6
commit
0861be681b
18 files changed
+1056
-72
No files matched your search
@@ -0,0 +1,124 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// TreeIcon 是文件树的显示属性,不参与 Markdown 内容或版本计算。
|
||||
type TreeIcon struct {
|
||||
Kind string `json:"kind"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
var ErrInvalidIcon = errors.New("非法图标")
|
||||
|
||||
const builtinIcons = " plus eye expand graph calendar tag copy task quote undo redo print grid menu noteAdd import folderAdd folder image note chevron more close pin trash logout restore edit search link key settings palette user outline source command info "
|
||||
|
||||
var emojiPattern = regexp.MustCompile(`^[\p{So}\p{Sk}\p{Sm}\x{2139}\x{203C}\x{2049}\x{3030}\x{303D}\x{200D}\x{FE0F}\x{20E3}0-9#*\x{E0020}-\x{E007F}]+$`)
|
||||
var emojiBase = regexp.MustCompile(`[\p{So}\p{Sm}\x{2139}\x{203C}\x{2049}\x{3030}\x{303D}\x{20E3}]`)
|
||||
|
||||
func (icon TreeIcon) valid() bool {
|
||||
switch icon.Kind {
|
||||
case "builtin":
|
||||
return icon.Value != "" && !strings.ContainsAny(icon.Value, " \t\r\n") && strings.Contains(builtinIcons, " "+icon.Value+" ")
|
||||
case "emoji":
|
||||
return utf8.ValidString(icon.Value) && len(icon.Value) <= 64 && emojiPattern.MatchString(icon.Value) && emojiBase.MatchString(icon.Value)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Vault) readTreeIcons() (map[string]TreeIcon, error) {
|
||||
data, err := os.ReadFile(filepath.Join(v.root, metaDir, "tree-icons.json"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return map[string]TreeIcon{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var icons map[string]TreeIcon
|
||||
if err := json.Unmarshal(data, &icons); err != nil {
|
||||
return nil, fmt.Errorf("文件树图标元数据损坏: %w", err)
|
||||
}
|
||||
if icons == nil {
|
||||
return nil, errors.New("文件树图标元数据必须是对象")
|
||||
}
|
||||
for p, icon := range icons {
|
||||
if _, err := CleanPath(p); err != nil || !icon.valid() {
|
||||
return nil, errors.New("文件树图标元数据含非法路径或图标")
|
||||
}
|
||||
}
|
||||
return icons, nil
|
||||
}
|
||||
|
||||
func (v *Vault) writeTreeIcons(icons map[string]TreeIcon) error {
|
||||
data, err := json.Marshal(icons)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return atomicWrite(filepath.Join(v.root, metaDir, "tree-icons.json"), data)
|
||||
}
|
||||
|
||||
func (v *Vault) SetTreeIcon(rel string, icon *TreeIcon) error {
|
||||
if icon != nil && !icon.valid() {
|
||||
return ErrInvalidIcon
|
||||
}
|
||||
p, err := v.abs(rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
info, err := os.Lstat(p)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && (!info.Mode().IsRegular() || (!IsNote(rel) && !IsImage(rel))) {
|
||||
return ErrInvalidPath
|
||||
}
|
||||
icons, err := v.readTreeIcons()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if icon == nil {
|
||||
if _, ok := icons[rel]; !ok {
|
||||
return nil
|
||||
}
|
||||
delete(icons, rel)
|
||||
} else {
|
||||
if icons[rel] == *icon {
|
||||
return nil
|
||||
}
|
||||
icons[rel] = *icon
|
||||
}
|
||||
return v.writeTreeIcons(icons)
|
||||
}
|
||||
|
||||
// 从所属子树移出属性,供移动和回收站操作在原有写锁内提交。
|
||||
func takeTreeIcons(icons map[string]TreeIcon, root string, directory bool) map[string]TreeIcon {
|
||||
taken := map[string]TreeIcon{}
|
||||
for p, icon := range icons {
|
||||
if p == root || directory && strings.HasPrefix(p, root+"/") {
|
||||
taken[p] = icon
|
||||
delete(icons, p)
|
||||
}
|
||||
}
|
||||
return taken
|
||||
}
|
||||
|
||||
// 回收站保留属性,但普通回收站 API 不暴露内部元数据。
|
||||
type trashMetadata struct {
|
||||
TrashItem
|
||||
Icons map[string]TreeIcon `json:"icons,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setIcon(t *testing.T, v *Vault, path string, icon *TreeIcon) {
|
||||
t.Helper()
|
||||
if err := v.SetTreeIcon(path, icon); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func treeIcons(t *testing.T, v *Vault) map[string]TreeIcon {
|
||||
t.Helper()
|
||||
entries, err := v.Tree()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := map[string]TreeIcon{}
|
||||
for _, entry := range entries {
|
||||
if entry.Icon != nil {
|
||||
result[entry.Path] = *entry.Icon
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestTreeIconsPersistFollowSubtreeAndTrash(t *testing.T) {
|
||||
v := newVault(t)
|
||||
if _, err := os.Stat(filepath.Join(v.root, metaDir, "tree-icons.json")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatal("未修改时不应生成图标文件", err)
|
||||
}
|
||||
if _, err := v.Write("folder/nested/note.md", []byte("# 正文\n"), Precondition{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
folder, note := TreeIcon{"emoji", "📚"}, TreeIcon{"builtin", "task"}
|
||||
setIcon(t, v, "folder", &folder)
|
||||
setIcon(t, v, "folder/nested/note.md", ¬e)
|
||||
body, rev, err := v.Read("folder/nested/note.md")
|
||||
if err != nil || string(body) != "# 正文\n" || rev != Rev([]byte("# 正文\n")) {
|
||||
t.Fatal("图标修改了正文/版本", err)
|
||||
}
|
||||
v, err = Open(v.root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := v.Move("folder/nested/note.md", "folder/nested/renamed.md"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := v.Move("folder", "destination/moved"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]TreeIcon{"destination/moved": folder, "destination/moved/nested/renamed.md": note}
|
||||
if got := treeIcons(t, v); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("改名/移动后图标不符: %v", got)
|
||||
}
|
||||
item, err := v.Delete("destination/moved")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := treeIcons(t, v); len(got) != 0 {
|
||||
t.Fatalf("删除后保留了属性: %v", got)
|
||||
}
|
||||
if _, err := v.Write("destination/moved/nested/renamed.md", []byte("新内容"), Precondition{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := treeIcons(t, v); len(got) != 0 {
|
||||
t.Fatalf("同名新文件继承了属性: %v", got)
|
||||
}
|
||||
if _, err := v.Restore(item.ID); !errors.Is(err, ErrExists) {
|
||||
t.Fatal("恢复冲突应失败", err)
|
||||
}
|
||||
if _, err := v.Move("destination/moved", "replacement"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := v.Restore(item.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := treeIcons(t, v); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("恢复后图标不符: %v", got)
|
||||
}
|
||||
setIcon(t, v, "destination/moved", nil)
|
||||
delete(want, "destination/moved")
|
||||
if got := treeIcons(t, v); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("恢复默认失败: %v", got)
|
||||
}
|
||||
// 原有无图标的回收站条目也可恢复。
|
||||
item, err = v.Delete("replacement")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := v.Restore(item.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeIconValidationAndCorruptMetadata(t *testing.T) {
|
||||
v := newVault(t)
|
||||
if _, err := v.Write("a.md", []byte("body"), Precondition{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, value := range []string{"📂", "👨👩👧👦", "👩🏽💻", "🇨🇳", "1️⃣", "©️", "ℹ️", "↔️", "◼️", "‼️", "〰️", "🏴\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f"} {
|
||||
setIcon(t, v, "a.md", &TreeIcon{"emoji", value})
|
||||
}
|
||||
for _, icon := range []TreeIcon{{"url", "https://example.com"}, {"builtin", "__proto__"}, {"builtin", "folder note"}, {"emoji", ""}, {"emoji", "hello"}, {"emoji", "<svg>"}, {"emoji", "\u200d"}, {"emoji", strings.Repeat("📂", 17)}} {
|
||||
if err := v.SetTreeIcon("a.md", &icon); !errors.Is(err, ErrInvalidIcon) {
|
||||
t.Fatalf("应拒绝 %+v: %v", icon, err)
|
||||
}
|
||||
}
|
||||
if err := v.SetTreeIcon("../a.md", nil); !errors.Is(err, ErrInvalidPath) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := v.SetTreeIcon("missing.md", nil); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(filepath.Join(v.root, "a.md"), filepath.Join(v.root, "link.md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := v.SetTreeIcon("link.md", nil); !errors.Is(err, ErrInvalidPath) {
|
||||
t.Fatal("不应设置符号链接图标", err)
|
||||
}
|
||||
for _, data := range []string{"{", "null", `{"../a.md":{"kind":"emoji","value":"📂"}}`, `{"a.md":{"kind":"builtin","value":"absent"}}`} {
|
||||
if err := os.WriteFile(filepath.Join(v.root, metaDir, "tree-icons.json"), []byte(data), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := v.Tree(); err == nil {
|
||||
t.Fatal("损坏元数据不应静默清空")
|
||||
}
|
||||
if _, err := v.Move("a.md", "b.md"); err == nil {
|
||||
t.Fatal("元数据异常时应停止移动")
|
||||
}
|
||||
if _, err := v.Delete("a.md"); err == nil {
|
||||
t.Fatal("元数据异常时应停止删除")
|
||||
}
|
||||
if _, _, err := v.Read("a.md"); err != nil {
|
||||
t.Fatal("原件应保持可用", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeIconWriteFailureRollsBack(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("目录写权限测试需以普通用户运行;root 可绕过文件权限")
|
||||
}
|
||||
for _, operation := range []string{"set", "move", "delete", "restore"} {
|
||||
t.Run(operation, func(t *testing.T) {
|
||||
v := newVault(t)
|
||||
if _, err := v.Write("a.md", []byte("[link](other.md)"), Precondition{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
legacyAsset(t, v, "a.md", "p.png", "image")
|
||||
icon := TreeIcon{"emoji", "📚"}
|
||||
setIcon(t, v, "a.md", &icon)
|
||||
var item TrashItem
|
||||
var err error
|
||||
if operation == "restore" {
|
||||
item, err = v.Delete("a.md")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
before, err := os.ReadFile(filepath.Join(v.root, metaDir, "tree-icons.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(filepath.Join(v.root, metaDir), 0555); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod(filepath.Join(v.root, metaDir), 0755)
|
||||
switch operation {
|
||||
case "set":
|
||||
err = v.SetTreeIcon("a.md", &TreeIcon{"builtin", "folder"})
|
||||
case "move":
|
||||
_, err = v.Move("a.md", "new/b.md")
|
||||
case "delete":
|
||||
_, err = v.Delete("a.md")
|
||||
case "restore":
|
||||
_, err = v.Restore(item.ID)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("图标元数据写失败却返回成功")
|
||||
}
|
||||
after, err := os.ReadFile(filepath.Join(v.root, metaDir, "tree-icons.json"))
|
||||
if err != nil || string(before) != string(after) {
|
||||
t.Fatal("原有元数据被改变", err)
|
||||
}
|
||||
if operation == "restore" {
|
||||
if _, err := v.Stat("a.md"); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatal("恢复应撤回", err)
|
||||
}
|
||||
metadata, err := v.readTrashMetadata(item.ID)
|
||||
if err != nil || metadata.Icons["a.md"] != icon {
|
||||
t.Fatal("回收站图标丢失", err)
|
||||
}
|
||||
} else {
|
||||
body, _, err := v.Read("a.md")
|
||||
if err != nil || string(body) != "[link](other.md)" {
|
||||
t.Fatal("原件或正文未回滚", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(v.root, "a.assets")); err != nil {
|
||||
t.Fatal("附件未回滚", err)
|
||||
}
|
||||
if _, err := v.Stat("new/b.md"); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatal("移动目标未撤回", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+84
-13
@@ -54,6 +54,7 @@ type Entry struct {
|
||||
Size int64 `json:"size"`
|
||||
ModTime int64 `json:"mtime"` // 毫秒
|
||||
Info fs.FileInfo `json:"-"`
|
||||
Icon *TreeIcon `json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
type TrashItem struct {
|
||||
@@ -306,12 +307,19 @@ func (v *Vault) Tree() ([]Entry, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
icons, err := v.readTreeIcons()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
visible := []Entry{}
|
||||
for _, e := range entries {
|
||||
if e.Dir {
|
||||
e.Size = 0
|
||||
}
|
||||
if e.Dir || IsImage(e.Path) || (IsNote(e.Path) && !strings.Contains(e.Path, AssetsExt+"/")) {
|
||||
if icon, ok := icons[e.Path]; ok {
|
||||
e.Icon = &icon
|
||||
}
|
||||
visible = append(visible, e)
|
||||
}
|
||||
}
|
||||
@@ -396,6 +404,15 @@ func (v *Vault) Move(from, to string) (rewritten bool, err error) {
|
||||
if _, err := os.Lstat(dst); err == nil {
|
||||
return false, ErrExists
|
||||
}
|
||||
icons, err := v.readTreeIcons()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
movedIcons := takeTreeIcons(icons, from, info.IsDir())
|
||||
iconsChanged := len(movedIcons) > 0
|
||||
for p, icon := range movedIcons {
|
||||
icons[movedPath(p, from, to, info.IsDir())] = icon
|
||||
}
|
||||
srcAssets, dstAssets := "", ""
|
||||
if isNote {
|
||||
if st, err := os.Stat(AssetsDir(src)); err == nil && st.IsDir() {
|
||||
@@ -405,6 +422,13 @@ func (v *Vault) Move(from, to string) (rewritten bool, err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if srcAssets != "" {
|
||||
attached := takeTreeIcons(icons, AssetsDir(from), true)
|
||||
iconsChanged = iconsChanged || len(attached) > 0
|
||||
for p, icon := range attached {
|
||||
icons[movedPath(p, AssetsDir(from), AssetsDir(to), true)] = icon
|
||||
}
|
||||
}
|
||||
var images []imageTransfer
|
||||
var imageTargets map[string]string
|
||||
defer func() {
|
||||
@@ -486,6 +510,9 @@ func (v *Vault) Move(from, to string) (rewritten bool, err error) {
|
||||
images[i].removed = true
|
||||
}
|
||||
}
|
||||
if iconsChanged {
|
||||
err = v.writeTreeIcons(icons)
|
||||
}
|
||||
return rewritten, err
|
||||
}
|
||||
|
||||
@@ -531,16 +558,32 @@ func (v *Vault) deleteLocked(rel string) (item TrashItem, err error) {
|
||||
item.Assets = filepath.Base(AssetsDir(p))
|
||||
}
|
||||
}
|
||||
icons, err := v.readTreeIcons()
|
||||
if err != nil {
|
||||
return TrashItem{}, err
|
||||
}
|
||||
metadata := trashMetadata{TrashItem: item, Icons: takeTreeIcons(icons, rel, item.Dir)}
|
||||
if item.Assets != "" {
|
||||
for p, icon := range takeTreeIcons(icons, AssetsDir(rel), true) {
|
||||
metadata.Icons[p] = icon
|
||||
}
|
||||
}
|
||||
dir := filepath.Join(v.trash, item.ID)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return TrashItem{}, err
|
||||
}
|
||||
noteMoved := false
|
||||
noteMoved, assetsMoved := false, false
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
rollbackFailed := false
|
||||
if assetsMoved {
|
||||
if rollback := os.Rename(filepath.Join(dir, item.Assets), AssetsDir(p)); rollback != nil {
|
||||
err = errors.Join(err, fmt.Errorf("回滚回收站图片失败: %w", rollback))
|
||||
rollbackFailed = true
|
||||
}
|
||||
}
|
||||
if noteMoved {
|
||||
if rollback := os.Rename(filepath.Join(dir, filepath.Base(p)), p); rollback != nil {
|
||||
err = errors.Join(err, fmt.Errorf("回滚回收站笔记失败: %w", rollback))
|
||||
@@ -554,7 +597,7 @@ func (v *Vault) deleteLocked(rel string) (item TrashItem, err error) {
|
||||
}
|
||||
}
|
||||
}()
|
||||
meta, _ := json.Marshal(item)
|
||||
meta, _ := json.Marshal(metadata)
|
||||
if err = atomicWrite(filepath.Join(dir, "meta.json"), meta); err != nil {
|
||||
return item, err
|
||||
}
|
||||
@@ -567,25 +610,39 @@ func (v *Vault) deleteLocked(rel string) (item TrashItem, err error) {
|
||||
return item, fmt.Errorf("移动图片目录到回收站失败: %w", err)
|
||||
}
|
||||
}
|
||||
return item, nil
|
||||
assetsMoved = item.Assets != ""
|
||||
if len(metadata.Icons) > 0 {
|
||||
err = v.writeTreeIcons(icons)
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (v *Vault) readTrashItem(id string) (TrashItem, error) {
|
||||
metadata, err := v.readTrashMetadata(id)
|
||||
return metadata.TrashItem, err
|
||||
}
|
||||
|
||||
func (v *Vault) readTrashMetadata(id string) (trashMetadata, error) {
|
||||
if !trashIDPattern.MatchString(id) {
|
||||
return TrashItem{}, ErrInvalidPath
|
||||
return trashMetadata{}, ErrInvalidPath
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(v.trash, id, "meta.json"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return TrashItem{}, ErrNotFound
|
||||
return trashMetadata{}, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return TrashItem{}, err
|
||||
return trashMetadata{}, err
|
||||
}
|
||||
var item TrashItem
|
||||
if err := json.Unmarshal(data, &item); err != nil {
|
||||
return TrashItem{}, fmt.Errorf("回收站条目 %s 元数据损坏: %w", id, err)
|
||||
var metadata trashMetadata
|
||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||
return trashMetadata{}, fmt.Errorf("回收站条目 %s 元数据损坏: %w", id, err)
|
||||
}
|
||||
return item, nil
|
||||
for p, icon := range metadata.Icons {
|
||||
if _, err := CleanPath(p); err != nil || !icon.valid() || !(p == metadata.Path || metadata.Dir && strings.HasPrefix(p, metadata.Path+"/") || metadata.Assets != "" && (p == AssetsDir(metadata.Path) || strings.HasPrefix(p, AssetsDir(metadata.Path)+"/"))) {
|
||||
return trashMetadata{}, fmt.Errorf("回收站条目 %s 图标元数据损坏", id)
|
||||
}
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func (v *Vault) TrashList() ([]TrashItem, error) {
|
||||
@@ -609,7 +666,8 @@ func (v *Vault) TrashList() ([]TrashItem, error) {
|
||||
}
|
||||
|
||||
func (v *Vault) Restore(id string) (item TrashItem, err error) {
|
||||
item, err = v.readTrashItem(id)
|
||||
metadata, err := v.readTrashMetadata(id)
|
||||
item = metadata.TrashItem
|
||||
if err != nil {
|
||||
return TrashItem{}, err
|
||||
}
|
||||
@@ -619,6 +677,13 @@ func (v *Vault) Restore(id string) (item TrashItem, err error) {
|
||||
}
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
icons, err := v.readTreeIcons()
|
||||
if err != nil {
|
||||
return TrashItem{}, err
|
||||
}
|
||||
for p, icon := range metadata.Icons {
|
||||
icons[p] = icon
|
||||
}
|
||||
if _, err := os.Lstat(dst); err == nil {
|
||||
return TrashItem{}, ErrExists
|
||||
}
|
||||
@@ -645,7 +710,7 @@ func (v *Vault) Restore(id string) (item TrashItem, err error) {
|
||||
err = errors.Join(err, rollback)
|
||||
return
|
||||
}
|
||||
meta, _ := json.Marshal(item)
|
||||
meta, _ := json.Marshal(metadata)
|
||||
if rollback := atomicWrite(filepath.Join(dir, "meta.json"), meta); rollback != nil {
|
||||
err = errors.Join(err, rollback)
|
||||
}
|
||||
@@ -666,7 +731,13 @@ func (v *Vault) Restore(id string) (item TrashItem, err error) {
|
||||
assetsMoved = true
|
||||
}
|
||||
cleaning = true
|
||||
return item, os.RemoveAll(dir)
|
||||
if err = os.RemoveAll(dir); err != nil {
|
||||
return item, err
|
||||
}
|
||||
if len(metadata.Icons) > 0 {
|
||||
err = v.writeTreeIcons(icons)
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (v *Vault) Purge(id string) error {
|
||||
|
||||
Reference in new issue
Block a user