feat(i18n): add English and Simplified Chinese
This commit is contained in:
1 parent
38c4c8b781
commit
1e9e0678ee
149 files changed
+5356
-1144
No files matched your search
+11
-10
@@ -1,6 +1,6 @@
|
||||
package vault
|
||||
|
||||
import "fmt"
|
||||
import "rawmark/internal/i18n"
|
||||
|
||||
type CleanupResult struct {
|
||||
Deleted []TrashItem `json:"deleted"`
|
||||
@@ -9,6 +9,7 @@ type CleanupResult struct {
|
||||
type CleanupSkip struct {
|
||||
Path string `json:"path"`
|
||||
Reason string `json:"reason"`
|
||||
Cause error `json:"-"`
|
||||
}
|
||||
|
||||
// CleanupImages 在写锁内重新扫描引用;派生索引不作为删除依据。
|
||||
@@ -31,7 +32,7 @@ func (v *Vault) CleanupImages(paths []string) (CleanupResult, error) {
|
||||
}
|
||||
metadata := Analyze(entry.Path, content)
|
||||
if len(metadata.Warnings) > 0 {
|
||||
return result, fmt.Errorf("%w:%s 的元数据异常,无法确认全库引用:%s", ErrInvalidPath, entry.Path, metadata.Warnings[0])
|
||||
return result, i18n.New("w_invalid_metadata_in_s_478c93", ErrInvalidPath, entry.Path, metadata.Diagnostics[0])
|
||||
}
|
||||
for _, ref := range metadata.Assets {
|
||||
references[ref.Target] = true
|
||||
@@ -43,27 +44,27 @@ func (v *Vault) CleanupImages(paths []string) (CleanupResult, error) {
|
||||
continue
|
||||
}
|
||||
seen[p] = true
|
||||
reason := ""
|
||||
var reason error
|
||||
switch {
|
||||
case !IsImage(p):
|
||||
reason = "不是图片文件"
|
||||
reason = i18n.New("image_not_file")
|
||||
case references[p]:
|
||||
reason = "仍被笔记引用"
|
||||
reason = i18n.New("image_in_use")
|
||||
default:
|
||||
info, err := v.Stat(p)
|
||||
if err != nil {
|
||||
reason = err.Error()
|
||||
reason = err
|
||||
} else if !info.Mode().IsRegular() {
|
||||
reason = "不是图片文件"
|
||||
reason = i18n.New("image_not_file")
|
||||
}
|
||||
}
|
||||
if reason != "" {
|
||||
result.Skipped = append(result.Skipped, CleanupSkip{p, reason})
|
||||
if reason != nil {
|
||||
result.Skipped = append(result.Skipped, CleanupSkip{p, reason.Error(), reason})
|
||||
continue
|
||||
}
|
||||
item, err := v.deleteLocked(p)
|
||||
if err != nil {
|
||||
result.Skipped = append(result.Skipped, CleanupSkip{p, err.Error()})
|
||||
result.Skipped = append(result.Skipped, CleanupSkip{p, err.Error(), err})
|
||||
continue
|
||||
}
|
||||
result.Deleted = append(result.Deleted, item)
|
||||
|
||||
@@ -3,13 +3,13 @@ package vault
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// ZIP 保留仓库相对路径,正文不改写;只导出原始文件,不解析或下载外部资源。
|
||||
func (v *Vault) Export(ctx context.Context, paths []string) (result *os.File, name string, err error) {
|
||||
if len(paths) == 0 {
|
||||
return nil, "", fmt.Errorf("%w:请选择导出内容", ErrInvalidPath)
|
||||
return nil, "", i18n.New("w_choose_content_to_export_170d5e", ErrInvalidPath)
|
||||
}
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
@@ -70,7 +70,7 @@ func (v *Vault) Export(ctx context.Context, paths []string) (result *os.File, na
|
||||
}
|
||||
u, e := url.Parse(target)
|
||||
if e != nil {
|
||||
return nil, "", fmt.Errorf("%w:附件地址无效 %s", ErrInvalidPath, target)
|
||||
return nil, "", i18n.New("w_invalid_attachment_address_s_8b6ace", ErrInvalidPath, target)
|
||||
}
|
||||
if u.IsAbs() || u.Host != "" || strings.HasPrefix(target, "/") || u.Path == "" {
|
||||
continue
|
||||
@@ -80,11 +80,11 @@ func (v *Vault) Export(ctx context.Context, paths []string) (result *os.File, na
|
||||
continue
|
||||
}
|
||||
if e = v.exportPath(rel); e != nil {
|
||||
return nil, "", fmt.Errorf("附件 %s:%w", rel, e)
|
||||
return nil, "", i18n.New("attachment_s_w_7bc269", rel, e)
|
||||
}
|
||||
info, e := v.Stat(rel)
|
||||
if e != nil {
|
||||
return nil, "", fmt.Errorf("附件 %s:%w", rel, e)
|
||||
return nil, "", i18n.New("attachment_s_w_7bc269", rel, e)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
files[rel] = false
|
||||
|
||||
@@ -3,20 +3,20 @@ package vault
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReplaceImage 原位替换已有图片,保留所有引用路径;不允许借此创建文件或改变格式。
|
||||
func (v *Vault) ReplaceImage(rel, filename string, r io.Reader, limit int64) error {
|
||||
if !IsImage(rel) || !strings.EqualFold(path.Ext(rel), path.Ext(filename)) {
|
||||
return fmt.Errorf("%w:请选择与原图片扩展名相同的文件", ErrInvalidPath)
|
||||
return i18n.New("w_choose_a_file_with_8410e9", ErrInvalidPath)
|
||||
}
|
||||
p, err := v.abs(rel)
|
||||
if err != nil {
|
||||
@@ -30,7 +30,7 @@ func (v *Vault) ReplaceImage(rel, filename string, r io.Reader, limit int64) err
|
||||
return ErrTooLarge
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("%w:图片不能为空", ErrInvalidPath)
|
||||
return i18n.New("w_image_cannot_be_empty_a9a272", ErrInvalidPath)
|
||||
}
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
+32
-28
@@ -6,12 +6,12 @@ import (
|
||||
"compress/flate"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -26,7 +26,7 @@ const (
|
||||
importAssetLimit = 50 << 20
|
||||
)
|
||||
|
||||
var ErrInvalidImport = errors.New("导入内容无效")
|
||||
var ErrInvalidImport = i18n.New("invalid_import_contents_ed556a")
|
||||
|
||||
// ImportFile 的读取范围是单个上传文件,ZIP 与普通 Markdown 共用这一输入边界。
|
||||
type ImportFile struct {
|
||||
@@ -36,25 +36,26 @@ type ImportFile struct {
|
||||
}
|
||||
|
||||
type ImportResult struct {
|
||||
Paths []string `json:"paths"`
|
||||
Notes int `json:"notes"`
|
||||
Assets int `json:"assets"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Paths []string `json:"paths"`
|
||||
Notes int `json:"notes"`
|
||||
Assets int `json:"assets"`
|
||||
Warnings []string `json:"warnings"`
|
||||
WarningMessages []*i18n.Error `json:"-"`
|
||||
}
|
||||
|
||||
// Import 在隐藏目录内整理完整内容,提交前统一检查冲突,绝不覆盖已有文件。
|
||||
func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFile) (result ImportResult, err error) {
|
||||
result = ImportResult{Paths: []string{}, Warnings: []string{}}
|
||||
if len(files) == 0 || len(files) > maxImportEntries {
|
||||
return result, fmt.Errorf("%w:请选择 Markdown 文件或一个 ZIP", ErrInvalidImport)
|
||||
return result, i18n.New("w_choose_markdown_files_or_bcaa52", ErrInvalidImport)
|
||||
}
|
||||
zipped := len(files) == 1 && strings.EqualFold(path.Ext(files[0].Name), ".zip")
|
||||
if zipped {
|
||||
if _, err := CleanPath(folder); err != nil || strings.Contains(folder, "/") || strings.HasSuffix(folder, AssetsExt) {
|
||||
return result, fmt.Errorf("%w:导入文件夹名称无效", ErrInvalidImport)
|
||||
return result, i18n.New("w_invalid_import_folder_name_a10885", ErrInvalidImport)
|
||||
}
|
||||
} else if folder != "" {
|
||||
return result, fmt.Errorf("%w:普通 Markdown 不使用外层文件夹", ErrInvalidImport)
|
||||
return result, i18n.New("w_plain_markdown_files_do_08b920", ErrInvalidImport)
|
||||
}
|
||||
if dir != "" {
|
||||
if _, err := CleanPath(dir); err != nil {
|
||||
@@ -74,7 +75,7 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
}
|
||||
defer func() {
|
||||
if cleanup := os.RemoveAll(stage); cleanup != nil {
|
||||
err = errors.Join(err, fmt.Errorf("清理导入临时目录失败:%w", cleanup))
|
||||
err = errors.Join(err, i18n.New("could_not_clean_the_temporary_212214", cleanup))
|
||||
}
|
||||
}()
|
||||
staged := &Vault{root: stage}
|
||||
@@ -87,30 +88,30 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
return err
|
||||
}
|
||||
if _, err := CleanPath(name); err != nil || !utf8.ValidString(name) {
|
||||
return fmt.Errorf("%w:ZIP 含有非法路径", ErrInvalidImport)
|
||||
return i18n.New("w_zip_contains_an_invalid_7bcf46", ErrInvalidImport)
|
||||
}
|
||||
if _, exists := entries[name]; exists {
|
||||
return fmt.Errorf("%w:存在重复路径", ErrInvalidImport)
|
||||
return i18n.New("w_duplicate_path_408694", ErrInvalidImport)
|
||||
}
|
||||
entries[name] = struct{}{}
|
||||
dest := filepath.Join(stage, filepath.FromSlash(name))
|
||||
if isDir {
|
||||
err := os.MkdirAll(dest, 0o755)
|
||||
if errors.Is(err, fs.ErrExist) || errors.Is(err, syscall.ENOTDIR) {
|
||||
return fmt.Errorf("%w:文件与目录路径冲突", ErrInvalidImport)
|
||||
return i18n.New("w_file_and_directory_paths_eb0b28", ErrInvalidImport)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||
if errors.Is(err, syscall.ENOTDIR) || errors.Is(err, fs.ErrExist) {
|
||||
return fmt.Errorf("%w:文件与目录路径冲突", ErrInvalidImport)
|
||||
return i18n.New("w_file_and_directory_paths_eb0b28", ErrInvalidImport)
|
||||
}
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrExist) || errors.Is(err, syscall.ENOTDIR) {
|
||||
return fmt.Errorf("%w:文件与目录路径冲突", ErrInvalidImport)
|
||||
return i18n.New("w_file_and_directory_paths_eb0b28", ErrInvalidImport)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -131,7 +132,7 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
if zipped {
|
||||
z, err := zip.NewReader(files[0].Reader, files[0].Size)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("%w:无法读取 ZIP:%w", ErrInvalidImport, err)
|
||||
return result, i18n.New("w_could_not_read_zip_d4a777", ErrInvalidImport, err)
|
||||
}
|
||||
if len(z.File) > maxImportEntries {
|
||||
return result, ErrTooLarge
|
||||
@@ -141,19 +142,19 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
continue
|
||||
} // 本程序笔记导出的缺失附件报告,不作为笔记导入。
|
||||
if !f.Mode().IsRegular() && !f.Mode().IsDir() {
|
||||
return result, fmt.Errorf("%w:ZIP 不允许符号链接或特殊文件", ErrInvalidImport)
|
||||
return result, i18n.New("w_zip_cannot_contain_symlinks_5f596f", ErrInvalidImport)
|
||||
}
|
||||
name := strings.TrimSuffix(f.Name, "/")
|
||||
r, err := f.Open()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("%w:无法读取 ZIP 条目:%w", ErrInvalidImport, err)
|
||||
return result, i18n.New("w_could_not_read_zip_8158b9", ErrInvalidImport, err)
|
||||
}
|
||||
err = write(name, f.FileInfo().IsDir(), r)
|
||||
err = errors.Join(err, r.Close())
|
||||
if err != nil {
|
||||
var corrupt flate.CorruptInputError
|
||||
if errors.Is(err, zip.ErrChecksum) || errors.Is(err, io.ErrUnexpectedEOF) || errors.As(err, &corrupt) {
|
||||
err = fmt.Errorf("%w:ZIP 内容损坏:%w", ErrInvalidImport, err)
|
||||
err = i18n.New("w_zip_content_is_damaged_6323c7", ErrInvalidImport, err)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
@@ -161,7 +162,7 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
} else {
|
||||
for _, f := range files {
|
||||
if !IsNote(f.Name) || strings.Contains(f.Name, "/") {
|
||||
return result, fmt.Errorf("%w:只接受 .md 文件或单个 ZIP", ErrInvalidImport)
|
||||
return result, i18n.New("w_only_md_files_or_6f1a4f", ErrInvalidImport)
|
||||
}
|
||||
if err := write(f.Name, false, io.NewSectionReader(f.Reader, 0, f.Size)); err != nil {
|
||||
return result, err
|
||||
@@ -169,7 +170,7 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
}
|
||||
}
|
||||
if len(notes) == 0 {
|
||||
return result, fmt.Errorf("%w:没有可导入的 Markdown 笔记", ErrInvalidImport)
|
||||
return result, i18n.New("w_no_importable_markdown_notes_3ad0b8", ErrInvalidImport)
|
||||
}
|
||||
sort.Strings(notes)
|
||||
migrated := map[string]bool{}
|
||||
@@ -179,7 +180,7 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
}
|
||||
for _, seg := range strings.Split(path.Dir(note), "/") {
|
||||
if strings.HasSuffix(seg, AssetsExt) {
|
||||
return result, fmt.Errorf("%w:笔记不能放在 .assets 附件目录内", ErrInvalidImport)
|
||||
return result, i18n.New("w_notes_cannot_be_inside_744972", ErrInvalidImport)
|
||||
}
|
||||
}
|
||||
content, _, err := staged.Read(note)
|
||||
@@ -187,14 +188,17 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
return result, err
|
||||
}
|
||||
if !utf8.Valid(content) || bytes.IndexByte(content, 0) >= 0 {
|
||||
return result, fmt.Errorf("%w:Markdown 必须是 UTF-8 文本", ErrInvalidImport)
|
||||
return result, i18n.New("w_markdown_must_be_utf_ce0d80", ErrInvalidImport)
|
||||
}
|
||||
content = bytes.TrimPrefix(content, []byte{0xef, 0xbb, 0xbf})
|
||||
updated, warnings, err := staged.importLinks(ctx, note, content, &total, migrated)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Warnings = append(result.Warnings, warnings...)
|
||||
result.WarningMessages = append(result.WarningMessages, warnings...)
|
||||
for _, warning := range warnings {
|
||||
result.Warnings = append(result.Warnings, warning.Error())
|
||||
}
|
||||
if len(updated) > importNoteLimit || total+int64(len(updated)-len(content)) > maxImportExpanded {
|
||||
return result, ErrTooLarge
|
||||
}
|
||||
@@ -213,7 +217,7 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
}
|
||||
metadata := Analyze(note, body)
|
||||
if len(metadata.Warnings) > 0 {
|
||||
return result, fmt.Errorf("%w:%s 的元数据异常,无法确认图片引用", ErrInvalidImport, note)
|
||||
return result, i18n.New("w_invalid_metadata_in_s_cc1d10", ErrInvalidImport, note)
|
||||
}
|
||||
for _, ref := range metadata.Assets {
|
||||
used[ref.Target] = true
|
||||
@@ -275,7 +279,7 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
return result, err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return result, fmt.Errorf("%w:目标不是文件夹", ErrInvalidImport)
|
||||
return result, i18n.New("w_destination_is_not_a_f9de1c", ErrInvalidImport)
|
||||
}
|
||||
for _, target := range targets {
|
||||
dest, err := v.abs(target)
|
||||
@@ -283,7 +287,7 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
return result, err
|
||||
}
|
||||
if _, err := os.Lstat(dest); err == nil {
|
||||
return result, fmt.Errorf("%w:%s,请修改名称", ErrExists, target)
|
||||
return result, i18n.New("w_s_choose_a_different_ac4d15", ErrExists, target)
|
||||
} else if !errors.Is(err, fs.ErrNotExist) {
|
||||
return result, err
|
||||
}
|
||||
@@ -301,7 +305,7 @@ func (v *Vault) Import(ctx context.Context, dir, folder string, files []ImportFi
|
||||
if err = os.Rename(filepath.Join(stage, name), filepath.Join(parent, name)); err != nil {
|
||||
for _, prev := range published {
|
||||
if rollback := os.Rename(filepath.Join(parent, prev), filepath.Join(stage, prev)); rollback != nil {
|
||||
err = errors.Join(err, fmt.Errorf("回滚导入失败:%w", rollback))
|
||||
err = errors.Join(err, i18n.New("import_rollback_failed_w_50748f", rollback))
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -228,7 +228,7 @@ func importLinkSpans(content []byte) []importLink {
|
||||
return spans
|
||||
}
|
||||
|
||||
func (v *Vault) importLinks(ctx context.Context, note string, content []byte, total *int64, migrated map[string]bool) ([]byte, []string, error) {
|
||||
func (v *Vault) importLinks(ctx context.Context, note string, content []byte, total *int64, migrated map[string]bool) ([]byte, []*i18n.Error, error) {
|
||||
missing := map[string]bool{}
|
||||
replacements := map[string]string{}
|
||||
spans := importLinkSpans(content)
|
||||
@@ -312,9 +312,9 @@ func (v *Vault) importLinks(ctx context.Context, note string, content []byte, to
|
||||
last = span.end
|
||||
}
|
||||
updated := append(out, content[last:]...)
|
||||
warnings := []string{}
|
||||
warnings := []*i18n.Error{}
|
||||
if len(missing) > 0 {
|
||||
warnings = append(warnings, note+":"+strconv.Itoa(len(missing))+" 个本地链接的目标不在导入内容中,已保留原链接")
|
||||
warnings = append(warnings, i18n.New("import_missing_links", note, len(missing)))
|
||||
}
|
||||
return updated, warnings, nil
|
||||
}
|
||||
+17
-11
@@ -2,10 +2,10 @@ package vault
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
"path"
|
||||
"rawmark/internal/i18n"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -22,11 +22,12 @@ type Reference struct {
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Title string `json:"title"`
|
||||
Tags []string `json:"tags"`
|
||||
Links []Reference `json:"links"`
|
||||
Assets []Reference `json:"assets"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Title string `json:"title"`
|
||||
Tags []string `json:"tags"`
|
||||
Links []Reference `json:"links"`
|
||||
Assets []Reference `json:"assets"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Diagnostics []*i18n.Error `json:"-"`
|
||||
}
|
||||
|
||||
var inlineTag = regexp.MustCompile(`#[\p{L}\p{N}_][\p{L}\p{N}_/-]*`)
|
||||
@@ -48,6 +49,11 @@ func LocalTarget(note, raw string) (string, *url.URL, bool) {
|
||||
// Analyze 提取语义关系而不重新序列化正文;源位置保留为原文行号。
|
||||
func Analyze(note string, content []byte) Metadata {
|
||||
m := Metadata{Title: strings.TrimSuffix(path.Base(note), NoteExt), Tags: []string{}, Links: []Reference{}, Assets: []Reference{}, Warnings: []string{}}
|
||||
addWarning := func(key string, args ...any) {
|
||||
diagnostic := i18n.New(key, args...)
|
||||
m.Diagnostics = append(m.Diagnostics, diagnostic)
|
||||
m.Warnings = append(m.Warnings, diagnostic.Error())
|
||||
}
|
||||
tags := map[string]bool{}
|
||||
addTag := func(s string) {
|
||||
s = strings.TrimPrefix(strings.TrimSpace(s), "#")
|
||||
@@ -68,7 +74,7 @@ func Analyze(note string, content []byte) Metadata {
|
||||
offset += len(lines[i])
|
||||
}
|
||||
if end < 0 {
|
||||
m.Warnings = append(m.Warnings, "YAML 元数据未闭合")
|
||||
addWarning("metadata_unclosed")
|
||||
end = len(source)
|
||||
} else {
|
||||
var front struct {
|
||||
@@ -76,7 +82,7 @@ func Analyze(note string, content []byte) Metadata {
|
||||
Title string `yaml:"title"`
|
||||
}
|
||||
if err := yaml.Unmarshal(source[len(lines[0]):bodyEnd], &front); err != nil {
|
||||
m.Warnings = append(m.Warnings, fmt.Sprintf("YAML 元数据无效:%v", err))
|
||||
addWarning("metadata_invalid", err)
|
||||
} else {
|
||||
if front.Title != "" {
|
||||
m.Title = front.Title
|
||||
@@ -89,18 +95,18 @@ func Analyze(note string, content []byte) Metadata {
|
||||
addTag(tag)
|
||||
}
|
||||
} else {
|
||||
m.Warnings = append(m.Warnings, "YAML tags 必须是字符串或字符串列表")
|
||||
addWarning("metadata_tags")
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
for _, tag := range front.Tags.Content {
|
||||
if tag.Kind != yaml.ScalarNode || tag.Tag != "!!str" {
|
||||
m.Warnings = append(m.Warnings, "YAML tags 必须是字符串列表")
|
||||
addWarning("metadata_tag_list")
|
||||
break
|
||||
}
|
||||
addTag(tag.Value)
|
||||
}
|
||||
default:
|
||||
m.Warnings = append(m.Warnings, "YAML tags 必须是字符串或字符串列表")
|
||||
addWarning("metadata_tags")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ func (v *Vault) copyNoteImages(from, to string) (transfers []imageTransfer, targ
|
||||
}
|
||||
metadata := Analyze(from, content)
|
||||
if len(metadata.Warnings) != 0 {
|
||||
return nil, nil, fmt.Errorf("%w:%s 的元数据异常,无法确认图片引用", ErrInvalidPath, from)
|
||||
return nil, nil, i18n.New("w_invalid_metadata_in_s_cc1d10", ErrInvalidPath, from)
|
||||
}
|
||||
var images []Reference
|
||||
for _, ref := range metadata.Assets {
|
||||
@@ -51,7 +51,7 @@ func (v *Vault) copyNoteImages(from, to string) (transfers []imageTransfer, targ
|
||||
}
|
||||
m := Analyze(entry.Path, body)
|
||||
if len(m.Warnings) != 0 {
|
||||
return nil, nil, fmt.Errorf("%w:%s 的元数据异常,无法确认图片引用", ErrInvalidPath, entry.Path)
|
||||
return nil, nil, i18n.New("w_invalid_metadata_in_s_cc1d10", ErrInvalidPath, entry.Path)
|
||||
}
|
||||
for _, ref := range m.Assets {
|
||||
used[ref.Target] = true
|
||||
|
||||
@@ -3,9 +3,9 @@ package vault
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -39,6 +39,7 @@ func (v *Vault) purgeTrashItemLocked(metadata trashMetadata) error {
|
||||
type TrashFailure struct {
|
||||
ID string `json:"id"`
|
||||
Error string `json:"error"`
|
||||
Cause error `json:"-"`
|
||||
}
|
||||
|
||||
type TrashPurgeResult struct {
|
||||
@@ -53,7 +54,7 @@ func (v *Vault) PurgeTrash(days int64, now time.Time) (TrashPurgeResult, error)
|
||||
defer v.mu.Unlock()
|
||||
result := TrashPurgeResult{Deleted: []string{}, Failed: []TrashFailure{}}
|
||||
if days < 0 {
|
||||
return result, fmt.Errorf("保留天数不能为负数")
|
||||
return result, i18n.New("retention_days_cannot_be_negative_f37c9b")
|
||||
}
|
||||
dirs, err := os.ReadDir(v.trash)
|
||||
if err != nil {
|
||||
@@ -72,7 +73,7 @@ func (v *Vault) PurgeTrash(days int64, now time.Time) (TrashPurgeResult, error)
|
||||
err = v.purgeTrashItemLocked(metadata)
|
||||
}
|
||||
if err != nil {
|
||||
result.Failed = append(result.Failed, TrashFailure{ID: id, Error: err.Error()})
|
||||
result.Failed = append(result.Failed, TrashFailure{ID: id, Error: err.Error(), Cause: err})
|
||||
} else {
|
||||
result.Deleted = append(result.Deleted, id)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package vault
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -18,7 +18,7 @@ type TreeIcon struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
var ErrInvalidIcon = errors.New("非法图标")
|
||||
var ErrInvalidIcon = i18n.New("invalid_icon_59d610")
|
||||
|
||||
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 "
|
||||
|
||||
@@ -46,14 +46,14 @@ func (v *Vault) readTreeIcons() (map[string]TreeIcon, error) {
|
||||
}
|
||||
var icons map[string]TreeIcon
|
||||
if err := json.Unmarshal(data, &icons); err != nil {
|
||||
return nil, fmt.Errorf("文件树图标元数据损坏: %w", err)
|
||||
return nil, i18n.New("file_tree_icon_metadata_is_49b5aa", err)
|
||||
}
|
||||
if icons == nil {
|
||||
return nil, errors.New("文件树图标元数据必须是对象")
|
||||
return nil, i18n.New("file_tree_icon_metadata_must_54f104")
|
||||
}
|
||||
for p, icon := range icons {
|
||||
if _, err := CleanPath(p); err != nil || !icon.valid() {
|
||||
return nil, errors.New("文件树图标元数据含非法路径或图标")
|
||||
return nil, i18n.New("file_tree_icon_metadata_contains_eb66b6")
|
||||
}
|
||||
}
|
||||
return icons, nil
|
||||
|
||||
+22
-21
@@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -23,16 +24,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidPath = errors.New("非法路径")
|
||||
ErrNotFound = errors.New("文件不存在")
|
||||
ErrExists = errors.New("目标已存在")
|
||||
ErrTooLarge = errors.New("文件过大")
|
||||
ErrInvalidPath = i18n.New("invalid_path_ec93bf")
|
||||
ErrNotFound = i18n.New("file_not_found_3703e4")
|
||||
ErrExists = i18n.New("destination_already_exists_9511c3")
|
||||
ErrTooLarge = i18n.New("file_is_too_large_5bfcab")
|
||||
)
|
||||
|
||||
// ConflictError 表示 If-Match 校验失败,Rev 为服务端当前版本(文件不存在时为空)
|
||||
type ConflictError struct{ Rev string }
|
||||
|
||||
func (e *ConflictError) Error() string { return "版本冲突" }
|
||||
func (e *ConflictError) Error() string { return i18n.New("version_conflict").Error() }
|
||||
|
||||
const (
|
||||
NoteExt = ".md"
|
||||
@@ -438,14 +439,14 @@ func (v *Vault) Move(from, to string) (rewritten bool, err error) {
|
||||
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))
|
||||
err = errors.Join(err, i18n.New("could_not_roll_back_the_d7c4fc", 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))
|
||||
err = errors.Join(err, i18n.New("could_not_remove_the_copied_e96efd", rollback))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -475,16 +476,16 @@ func (v *Vault) Move(from, to string) (rewritten bool, err error) {
|
||||
}
|
||||
if assetsMoved {
|
||||
if rollback := os.Rename(dstAssets, srcAssets); rollback != nil {
|
||||
err = errors.Join(err, fmt.Errorf("回滚旧附件目录失败: %w", rollback))
|
||||
err = errors.Join(err, i18n.New("could_not_roll_back_the_f89084", rollback))
|
||||
}
|
||||
}
|
||||
for _, change := range links[:writtenLinks] {
|
||||
if rollback := atomicWrite(change.path, change.before); rollback != nil {
|
||||
err = errors.Join(err, fmt.Errorf("回滚笔记引用失败: %w", rollback))
|
||||
err = errors.Join(err, i18n.New("could_not_roll_back_note_8554a3", rollback))
|
||||
}
|
||||
}
|
||||
if rollback := os.Rename(dst, src); rollback != nil {
|
||||
err = errors.Join(err, fmt.Errorf("回滚笔记移动失败: %w", rollback))
|
||||
err = errors.Join(err, i18n.New("could_not_roll_back_the_36ec22", rollback))
|
||||
}
|
||||
}()
|
||||
for _, change := range links {
|
||||
@@ -498,7 +499,7 @@ func (v *Vault) Move(from, to string) (rewritten bool, err error) {
|
||||
}
|
||||
if srcAssets != "" {
|
||||
if err := os.Rename(srcAssets, dstAssets); err != nil {
|
||||
return false, fmt.Errorf("移动图片目录失败: %w", err)
|
||||
return false, i18n.New("could_not_move_image_folder_c39928", err)
|
||||
}
|
||||
assetsMoved = true
|
||||
}
|
||||
@@ -580,13 +581,13 @@ func (v *Vault) deleteLocked(rel string) (item TrashItem, err error) {
|
||||
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))
|
||||
err = errors.Join(err, i18n.New("could_not_roll_back_trashed_b37d4f", 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))
|
||||
err = errors.Join(err, i18n.New("could_not_roll_back_trashed_3758a4", rollback))
|
||||
rollbackFailed = true
|
||||
}
|
||||
}
|
||||
@@ -607,7 +608,7 @@ func (v *Vault) deleteLocked(rel string) (item TrashItem, err error) {
|
||||
noteMoved = true
|
||||
if item.Assets != "" {
|
||||
if err = os.Rename(AssetsDir(p), filepath.Join(dir, item.Assets)); err != nil {
|
||||
return item, fmt.Errorf("移动图片目录到回收站失败: %w", err)
|
||||
return item, i18n.New("could_not_move_image_folder_7c4620", err)
|
||||
}
|
||||
}
|
||||
assetsMoved = item.Assets != ""
|
||||
@@ -635,14 +636,14 @@ func (v *Vault) readTrashMetadata(id string) (trashMetadata, error) {
|
||||
}
|
||||
var metadata trashMetadata
|
||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||
return trashMetadata{}, fmt.Errorf("回收站条目 %s 元数据损坏: %w", id, err)
|
||||
return trashMetadata{}, i18n.New("trash_item_s_has_damaged_ba382a", id, err)
|
||||
}
|
||||
if _, err := CleanPath(metadata.Path); err != nil || metadata.ID != id || metadata.DeletedAt <= 0 || (metadata.Assets != "" && metadata.Assets != filepath.Base(AssetsDir(metadata.Path))) {
|
||||
return trashMetadata{}, fmt.Errorf("回收站条目 %s 元数据无效", id)
|
||||
return trashMetadata{}, i18n.New("trash_item_s_has_invalid_006492", id)
|
||||
}
|
||||
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 trashMetadata{}, i18n.New("trash_item_s_has_damaged_856237", id)
|
||||
}
|
||||
}
|
||||
return metadata, nil
|
||||
@@ -722,16 +723,16 @@ func (v *Vault) Restore(id string) (item TrashItem, err error) {
|
||||
}
|
||||
if assetsMoved {
|
||||
if rollback := os.Rename(AssetsDir(dst), filepath.Join(dir, item.Assets)); rollback != nil {
|
||||
err = errors.Join(err, fmt.Errorf("回滚恢复图片失败: %w", rollback))
|
||||
err = errors.Join(err, i18n.New("could_not_roll_back_restored_952073", rollback))
|
||||
}
|
||||
}
|
||||
if rollback := os.Rename(dst, filepath.Join(dir, filepath.Base(dst))); rollback != nil {
|
||||
err = errors.Join(err, fmt.Errorf("回滚恢复笔记失败: %w", rollback))
|
||||
err = errors.Join(err, i18n.New("could_not_roll_back_restored_16b803", rollback))
|
||||
}
|
||||
}()
|
||||
if item.Assets != "" {
|
||||
if err := os.Rename(filepath.Join(dir, item.Assets), AssetsDir(dst)); err != nil {
|
||||
return item, fmt.Errorf("恢复图片目录失败: %w", err)
|
||||
return item, i18n.New("could_not_restore_image_folder_f6e087", err)
|
||||
}
|
||||
assetsMoved = true
|
||||
}
|
||||
@@ -774,7 +775,7 @@ func (v *Vault) SaveAsset(note, origName string, r io.Reader, limit int64) (stri
|
||||
// SaveFolderAsset 为图片管理上传到当前目录的共享 assets 目录,沿用哈希去重。
|
||||
func (v *Vault) SaveFolderAsset(folder, origName string, r io.Reader, limit int64) (string, error) {
|
||||
if !IsImage(origName) {
|
||||
return "", fmt.Errorf("%w:请选择图片文件", ErrInvalidPath)
|
||||
return "", i18n.New("w_choose_an_image_file_544069", ErrInvalidPath)
|
||||
}
|
||||
if folder != "" {
|
||||
info, err := v.Stat(folder)
|
||||
|
||||
Reference in new issue
Block a user