feat(assets): store note images in shared sibling assets directories

This commit is contained in:
eezd committed 2026-10-24 18:12:23 +08:00
1 parent 5abc99b89e
commit 08f4e4aa7d
14 files changed
+507 -75

No files matched your search

+83 -18
View File
@@ -405,8 +405,35 @@ func (v *Vault) Move(from, to string) (rewritten bool, err error) {
}
}
}
var images []imageTransfer
var imageTargets map[string]string
defer func() {
if err == nil {
return
}
for _, image := range images {
if image.removed {
if rollback := atomicWrite(filepath.Join(v.root, filepath.FromSlash(image.source)), image.data); rollback != nil {
err = errors.Join(err, fmt.Errorf("回滚图片原件失败: %w", rollback))
}
}
}
for _, image := range images {
if image.created {
if rollback := os.Remove(filepath.Join(v.root, filepath.FromSlash(image.target))); rollback != nil {
err = errors.Join(err, fmt.Errorf("撤回图片副本失败: %w", rollback))
}
}
}
}()
if isNote && path.Dir(from) != path.Dir(to) {
images, imageTargets, err = v.copyNoteImages(from, to)
if err != nil {
return false, err
}
}
// 笔记、目录和图片的移动都可能改变跨文件相对链接。
links, err := v.moveLinkChanges(from, to, info.IsDir(), srcAssets != "")
links, err := v.moveLinkChanges(from, to, info.IsDir(), srcAssets != "", imageTargets)
if err != nil {
return false, err
}
@@ -417,10 +444,16 @@ func (v *Vault) Move(from, to string) (rewritten bool, err error) {
return false, err
}
writtenLinks := 0
assetsMoved := false
defer func() {
if err == nil {
return
}
if assetsMoved {
if rollback := os.Rename(dstAssets, srcAssets); rollback != nil {
err = errors.Join(err, fmt.Errorf("回滚旧附件目录失败: %w", rollback))
}
}
for _, change := range links[:writtenLinks] {
if rollback := atomicWrite(change.path, change.before); rollback != nil {
err = errors.Join(err, fmt.Errorf("回滚笔记引用失败: %w", rollback))
@@ -443,6 +476,15 @@ func (v *Vault) Move(from, to string) (rewritten bool, err error) {
if err := os.Rename(srcAssets, dstAssets); err != nil {
return false, fmt.Errorf("移动图片目录失败: %w", err)
}
assetsMoved = true
}
for i := range images {
if images[i].removeSource {
if err := os.Remove(filepath.Join(v.root, filepath.FromSlash(images[i].source))); err != nil {
return false, err
}
images[i].removed = true
}
}
return rewritten, err
}
@@ -638,13 +680,16 @@ func (v *Vault) Purge(id string) error {
var extPattern = regexp.MustCompile(`^\.[a-z0-9]{1,8}$`)
// SaveAsset 把上传的文件存入笔记的 .assets 目录,以内容哈希命名(相同内容只存一份),
// SaveAsset 把上传的文件存入笔记同级的 assets 目录,以内容哈希命名(相同内容只存一份),
// 返回可直接写入 Markdown 的相对链接
func (v *Vault) SaveAsset(note, origName string, r io.Reader, limit int64) (string, error) {
if !IsNote(note) {
return "", ErrInvalidPath
}
return v.saveAssetAt(AssetsDir(note), origName, r, limit)
if _, err := CleanPath(note); err != nil {
return "", err
}
return v.saveAssetAt(path.Join(path.Dir(note), "assets"), origName, r, limit)
}
// SaveFolderAsset 为图片管理上传到当前目录的共享 assets 目录,沿用哈希去重。
@@ -685,28 +730,48 @@ func (v *Vault) saveAssetAt(relativeDir, origName string, r io.Reader, limit int
if err := os.MkdirAll(dir, 0o755); err != nil {
return "", err
}
// 管理界面允许替换图片,哈希名称不再保证内容未变;不能把旧内容上传误判为重复。
base := strings.TrimSuffix(name, path.Ext(name))
target, exists, err := v.assetDestination(relativeDir, name, data)
if err != nil {
return "", err
}
if !exists {
if err := atomicWrite(filepath.Join(v.root, filepath.FromSlash(target)), data); err != nil {
return "", err
}
}
return "./" + EscapeLinkPath(path.Base(relativeDir)) + "/" + EscapeLinkPath(path.Base(target)), nil
}
// 管理界面允许替换图片,文件名不保证内容未变;只复用实际内容相同的普通文件。
func (v *Vault) assetDestination(dir, name string, data []byte) (string, bool, error) {
base, ext := strings.TrimSuffix(name, path.Ext(name)), path.Ext(name)
for i := 0; ; i++ {
candidate := name
if i > 0 {
candidate = fmt.Sprintf("%s-%d%s", base, i, path.Ext(name))
candidate = fmt.Sprintf("%s-%d%s", base, i, ext)
}
target := filepath.Join(dir, candidate)
if err := v.checkInside(target); err != nil {
return "", err
rel := path.Join(dir, candidate)
full, err := v.abs(rel)
if err != nil {
return "", false, err
}
existing, err := os.ReadFile(target)
info, err := os.Lstat(full)
if errors.Is(err, fs.ErrNotExist) {
if err := atomicWrite(target, data); err != nil {
return "", err
}
} else if err != nil {
return "", err
} else if !bytes.Equal(existing, data) {
continue
return rel, false, nil
}
if err != nil {
return "", false, err
}
if !info.Mode().IsRegular() {
return "", false, ErrInvalidPath
}
existing, err := os.ReadFile(full)
if err != nil {
return "", false, err
}
if bytes.Equal(existing, data) {
return rel, true, nil
}
return EscapeLinkPath(path.Base(relativeDir)) + "/" + candidate, nil
}
}