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
+30
-30
@@ -9,11 +9,11 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -24,7 +24,7 @@ const MaxExpandedBytes int64 = 8 << 30
|
||||
const MaxEntries = 100000
|
||||
|
||||
// ErrInvalidArchive 表示归档内容不满足备份契约,不包含磁盘或文件系统故障。
|
||||
var ErrInvalidArchive = errors.New("备份内容无效")
|
||||
var ErrInvalidArchive = i18n.New("invalid_backup_contents_ecff07")
|
||||
|
||||
type Entry struct {
|
||||
Path string `json:"path"`
|
||||
@@ -62,7 +62,7 @@ func validEntry(kind, name string) bool {
|
||||
// Create 不追踪符号链接,任何无法读取的文件都使备份明确失败。
|
||||
func Create(ctx context.Context, dest, kind string, sources []Source, skip func(string) bool) (err error) {
|
||||
if kind != "full" && kind != "settings" {
|
||||
return fmt.Errorf("备份类型无效")
|
||||
return i18n.New("invalid_backup_type_f5ff70")
|
||||
}
|
||||
f, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
@@ -101,7 +101,7 @@ func Create(ctx context.Context, dest, kind string, sources []Source, skip func(
|
||||
return nil
|
||||
}
|
||||
if !validEntry(kind, name) || names[name] {
|
||||
return fmt.Errorf("备份路径无效或重复: %s", name)
|
||||
return i18n.New("invalid_or_duplicate_backup_path_ce9c5b", name)
|
||||
}
|
||||
names[name] = true
|
||||
info, err := d.Info()
|
||||
@@ -109,11 +109,11 @@ func Create(ctx context.Context, dest, kind string, sources []Source, skip func(
|
||||
return err
|
||||
}
|
||||
if !info.Mode().IsRegular() && !info.IsDir() {
|
||||
return fmt.Errorf("备份不支持符号链接或特殊文件: %s", name)
|
||||
return i18n.New("backups_do_not_support_symlinks_872adf", name)
|
||||
}
|
||||
e := Entry{Path: name, Dir: info.IsDir(), Modified: info.ModTime().UnixMilli()}
|
||||
if len(manifest.Entries) >= MaxEntries {
|
||||
return fmt.Errorf("备份文件数超过上限")
|
||||
return i18n.New("backup_file_count_exceeds_the_77ac37")
|
||||
}
|
||||
h, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
@@ -141,17 +141,17 @@ func Create(ctx context.Context, dest, kind string, sources []Source, skip func(
|
||||
}
|
||||
total += e.Size
|
||||
if total > MaxExpandedBytes {
|
||||
return fmt.Errorf("备份展开大小超过 8 GiB")
|
||||
return i18n.New("expanded_backup_exceeds_gib_b38d18")
|
||||
}
|
||||
if e.Size != info.Size() {
|
||||
return fmt.Errorf("备份期间文件发生变化: %s", name)
|
||||
return i18n.New("a_file_changed_during_backup_a85244", name)
|
||||
}
|
||||
after, err := os.Stat(full)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !os.SameFile(info, after) || !info.ModTime().Equal(after.ModTime()) || info.Size() != after.Size() {
|
||||
return fmt.Errorf("备份期间文件发生变化: %s", name)
|
||||
return i18n.New("a_file_changed_during_backup_a85244", name)
|
||||
}
|
||||
e.SHA256 = hex.EncodeToString(sum.Sum(nil))
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func Create(ctx context.Context, dest, kind string, sources []Source, skip func(
|
||||
return err
|
||||
}
|
||||
if info.Size() > MaxArchiveBytes {
|
||||
return fmt.Errorf("备份压缩大小超过 2 GiB")
|
||||
return i18n.New("compressed_backup_exceeds_gib_3962d5")
|
||||
}
|
||||
return f.Sync()
|
||||
}
|
||||
@@ -188,52 +188,52 @@ func Extract(ctx context.Context, archive, dest string) (Manifest, error) {
|
||||
z, err := zip.OpenReader(archive)
|
||||
if errors.Is(err, zip.ErrInsecurePath) {
|
||||
z.Close()
|
||||
return manifest, fmt.Errorf("%w:%w", ErrInvalidArchive, err)
|
||||
return manifest, i18n.New("w_w_f0fd7d", ErrInvalidArchive, err)
|
||||
}
|
||||
if errors.Is(err, zip.ErrFormat) {
|
||||
return manifest, fmt.Errorf("%w:%w", ErrInvalidArchive, err)
|
||||
return manifest, i18n.New("w_w_f0fd7d", ErrInvalidArchive, err)
|
||||
}
|
||||
if err != nil {
|
||||
return manifest, err
|
||||
}
|
||||
defer z.Close()
|
||||
if len(z.File) > MaxEntries+1 {
|
||||
return manifest, fmt.Errorf("%w:备份文件数超过上限", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_backup_file_count_exceeds_5ba1d4", ErrInvalidArchive)
|
||||
}
|
||||
entries := map[string]*zip.File{}
|
||||
for _, f := range z.File {
|
||||
name := strings.TrimSuffix(f.Name, "/")
|
||||
if !validPath(name) || entries[name] != nil {
|
||||
return manifest, fmt.Errorf("%w:归档路径无效或重复", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_invalid_or_duplicate_archive_c11ac2", ErrInvalidArchive)
|
||||
}
|
||||
if !f.Mode().IsRegular() && !f.Mode().IsDir() {
|
||||
return manifest, fmt.Errorf("%w:归档不允许符号链接或特殊文件", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_archives_cannot_contain_symlinks_fd836b", ErrInvalidArchive)
|
||||
}
|
||||
entries[name] = f
|
||||
}
|
||||
m := entries["manifest.json"]
|
||||
if m == nil || m.UncompressedSize64 > 32<<20 {
|
||||
return manifest, fmt.Errorf("%w:缺少有效备份清单", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_a_valid_backup_manifest_8905b1", ErrInvalidArchive)
|
||||
}
|
||||
r, err := m.Open()
|
||||
if err != nil {
|
||||
return manifest, fmt.Errorf("%w:%w", ErrInvalidArchive, err)
|
||||
return manifest, i18n.New("w_w_f0fd7d", ErrInvalidArchive, err)
|
||||
}
|
||||
err = json.NewDecoder(io.LimitReader(r, 32<<20)).Decode(&manifest)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w:%w", ErrInvalidArchive, err)
|
||||
err = i18n.New("w_w_f0fd7d", ErrInvalidArchive, err)
|
||||
}
|
||||
err = errors.Join(err, r.Close())
|
||||
if err != nil {
|
||||
return manifest, err
|
||||
}
|
||||
if manifest.Format != 1 || (manifest.Kind != "full" && manifest.Kind != "settings") || len(manifest.Entries) != len(entries)-1 {
|
||||
return manifest, fmt.Errorf("%w:备份版本、类型或清单无效", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_invalid_backup_version_type_342220", ErrInvalidArchive)
|
||||
}
|
||||
var expanded int64
|
||||
for _, e := range manifest.Entries {
|
||||
if e.Size < 0 || e.Size > MaxExpandedBytes-expanded {
|
||||
return manifest, fmt.Errorf("%w:备份展开大小超过 8 GiB", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_expanded_backup_exceeds_gib_8e4c5b", ErrInvalidArchive)
|
||||
}
|
||||
expanded += e.Size
|
||||
}
|
||||
@@ -242,7 +242,7 @@ func Extract(ctx context.Context, archive, dest string) (Manifest, error) {
|
||||
return manifest, err
|
||||
}
|
||||
if uint64(expanded)+(32<<20) > disk.Bavail*uint64(disk.Bsize) {
|
||||
return manifest, fmt.Errorf("磁盘空间不足:%w", syscall.ENOSPC)
|
||||
return manifest, i18n.New("not_enough_disk_space_w_a39b20", syscall.ENOSPC)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
var total int64
|
||||
@@ -252,17 +252,17 @@ func Extract(ctx context.Context, archive, dest string) (Manifest, error) {
|
||||
}
|
||||
f := entries[e.Path]
|
||||
if !validEntry(manifest.Kind, e.Path) || seen[e.Path] || f == nil || e.Size < 0 || e.Dir != f.FileInfo().IsDir() || uint64(e.Size) != f.UncompressedSize64 {
|
||||
return manifest, fmt.Errorf("%w:备份清单与文件不符", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_backup_manifest_does_not_69c8ba", ErrInvalidArchive)
|
||||
}
|
||||
seen[e.Path] = true
|
||||
total += e.Size
|
||||
if total > MaxExpandedBytes {
|
||||
return manifest, fmt.Errorf("%w:备份展开大小超过 8 GiB", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_expanded_backup_exceeds_gib_8e4c5b", ErrInvalidArchive)
|
||||
}
|
||||
target := filepath.Join(dest, filepath.FromSlash(e.Path))
|
||||
if e.Dir {
|
||||
if e.Size != 0 {
|
||||
return manifest, fmt.Errorf("%w:目录大小无效", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_invalid_directory_size_3ec5c4", ErrInvalidArchive)
|
||||
}
|
||||
if err := os.MkdirAll(target, 0700); err != nil {
|
||||
return manifest, err
|
||||
@@ -270,14 +270,14 @@ func Extract(ctx context.Context, archive, dest string) (Manifest, error) {
|
||||
continue
|
||||
}
|
||||
if len(e.SHA256) != 64 {
|
||||
return manifest, fmt.Errorf("%w:文件校验值无效", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_invalid_file_checksum_ec0b61", ErrInvalidArchive)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil {
|
||||
return manifest, err
|
||||
}
|
||||
in, err := f.Open()
|
||||
if err != nil {
|
||||
return manifest, fmt.Errorf("%w:%w", ErrInvalidArchive, err)
|
||||
return manifest, i18n.New("w_w_f0fd7d", ErrInvalidArchive, err)
|
||||
}
|
||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
@@ -288,14 +288,14 @@ func Extract(ctx context.Context, archive, dest string) (Manifest, error) {
|
||||
n, copyErr := io.Copy(io.MultiWriter(out, sum), io.LimitReader(in, e.Size+1))
|
||||
var corruptDeflate flate.CorruptInputError
|
||||
if errors.Is(copyErr, zip.ErrChecksum) || errors.Is(copyErr, zip.ErrFormat) || errors.Is(copyErr, io.ErrUnexpectedEOF) || errors.As(copyErr, &corruptDeflate) {
|
||||
copyErr = fmt.Errorf("%w:%w", ErrInvalidArchive, copyErr)
|
||||
copyErr = i18n.New("w_w_f0fd7d", ErrInvalidArchive, copyErr)
|
||||
}
|
||||
err = errors.Join(copyErr, in.Close(), out.Close())
|
||||
if err != nil {
|
||||
return manifest, err
|
||||
}
|
||||
if n != e.Size || hex.EncodeToString(sum.Sum(nil)) != e.SHA256 {
|
||||
return manifest, fmt.Errorf("%w:文件校验失败: %s", ErrInvalidArchive, e.Path)
|
||||
return manifest, i18n.New("w_file_verification_failed_s_46b024", ErrInvalidArchive, e.Path)
|
||||
}
|
||||
t := time.UnixMilli(e.Modified)
|
||||
if err := os.Chtimes(target, t, t); err != nil {
|
||||
@@ -303,10 +303,10 @@ func Extract(ctx context.Context, archive, dest string) (Manifest, error) {
|
||||
}
|
||||
}
|
||||
if manifest.Kind == "full" && (!seen["app/rawmark.db"] || !seen["vault"] || entries["app/rawmark.db"].FileInfo().IsDir() || !entries["vault"].FileInfo().IsDir()) {
|
||||
return manifest, fmt.Errorf("%w:实例备份缺少数据库或笔记库", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_instance_backup_is_missing_4e260c", ErrInvalidArchive)
|
||||
}
|
||||
if manifest.Kind == "settings" && (!seen["settings.json"] || entries["settings.json"].FileInfo().IsDir()) {
|
||||
return manifest, fmt.Errorf("%w:设置备份缺少设置", ErrInvalidArchive)
|
||||
return manifest, i18n.New("w_settings_backup_is_missing_46056b", ErrInvalidArchive)
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ func (l *LogWriter) Write(p []byte) (int, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if len(p) > 10<<20 {
|
||||
return 0, errors.New("单条日志超过大小上限")
|
||||
return 0, i18n.New("a_single_log_entry_exceeds_b1cdf6")
|
||||
}
|
||||
if l.size+int64(len(p)) > 10<<20 {
|
||||
if err := l.file.Close(); err != nil {
|
||||
|
||||
+15
-15
@@ -5,10 +5,10 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rawmark/internal/i18n"
|
||||
"strings"
|
||||
|
||||
"rawmark/internal/store"
|
||||
@@ -78,14 +78,14 @@ func CheckDatabase(file string) error {
|
||||
header := make([]byte, 16)
|
||||
_, err = io.ReadFull(f, header)
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
err = fmt.Errorf("%w:%w", ErrInvalidArchive, err)
|
||||
err = i18n.New("w_w_f0fd7d", ErrInvalidArchive, err)
|
||||
}
|
||||
err = errors.Join(err, f.Close())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(header) != "SQLite format 3\x00" {
|
||||
return fmt.Errorf("%w:备份数据库格式无效", ErrInvalidArchive)
|
||||
return i18n.New("w_invalid_backup_database_format_d45d60", ErrInvalidArchive)
|
||||
}
|
||||
st, err := store.OpenExisting(file)
|
||||
if err != nil {
|
||||
@@ -101,7 +101,7 @@ func CheckDatabase(file string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%w:%w", ErrInvalidArchive, err)
|
||||
return i18n.New("w_w_f0fd7d", ErrInvalidArchive, err)
|
||||
}
|
||||
password, ok, err := st.Setting("auth.password")
|
||||
if err != nil {
|
||||
@@ -109,15 +109,15 @@ func CheckDatabase(file string) error {
|
||||
}
|
||||
parts := strings.Split(password, "$")
|
||||
if !ok || len(parts) != 6 || parts[1] != "argon2id" || parts[2] != "v=19" || parts[3] != "m=65536,t=3,p=4" {
|
||||
return fmt.Errorf("%w:备份密码格式不受支持", ErrInvalidArchive)
|
||||
return i18n.New("w_unsupported_backup_password_format_184d32", ErrInvalidArchive)
|
||||
}
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil || len(salt) != 16 {
|
||||
return fmt.Errorf("%w:备份密码盐无效", ErrInvalidArchive)
|
||||
return i18n.New("w_invalid_backup_password_salt_315eca", ErrInvalidArchive)
|
||||
}
|
||||
key, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||
if err != nil || len(key) != 32 {
|
||||
return fmt.Errorf("%w:备份密码校验值无效", ErrInvalidArchive)
|
||||
return i18n.New("w_invalid_backup_password_verification_17d1c1", ErrInvalidArchive)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func CopyTree(src, dest string) error {
|
||||
return err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("不允许复制特殊文件")
|
||||
return i18n.New("special_files_cannot_be_copied_441836")
|
||||
}
|
||||
if err = copyFile(p, target, 0600); err != nil {
|
||||
return err
|
||||
@@ -272,17 +272,17 @@ func Recover(data, vault string) (err error) {
|
||||
return err
|
||||
}
|
||||
if record.State != "prepared" && record.State != "applying" && record.State != "verifying" {
|
||||
return fmt.Errorf("恢复记录状态无效")
|
||||
return i18n.New("invalid_restore_record_state_eee063")
|
||||
}
|
||||
// 记录由本机生成;仍限制其读取范围,避免错误记录读取任意文件。
|
||||
if !strings.HasPrefix(record.Stage, filepath.Join(data, ".maintenance", "uploads")+string(filepath.Separator)) || !strings.HasPrefix(record.Backup, filepath.Join(data, "backups")+string(filepath.Separator)) {
|
||||
return fmt.Errorf("恢复记录路径无效")
|
||||
return i18n.New("invalid_restore_record_path_611881")
|
||||
}
|
||||
if record.Kind == "settings" {
|
||||
return recoverSettings(data, record)
|
||||
}
|
||||
if record.State != "prepared" {
|
||||
err = fmt.Errorf("上次实例恢复被中断")
|
||||
err = i18n.New("the_previous_instance_restore_was_a9d480")
|
||||
}
|
||||
if record.State == "prepared" {
|
||||
record.State = "applying"
|
||||
@@ -304,13 +304,13 @@ func Recover(data, vault string) (err error) {
|
||||
restoreErr = apply(data, vault, stage)
|
||||
}
|
||||
if restoreErr != nil {
|
||||
return errors.Join(err, fmt.Errorf("实例回退失败: %w", restoreErr))
|
||||
return errors.Join(err, i18n.New("instance_rollback_failed_w_80ec77", restoreErr))
|
||||
}
|
||||
if removeErr := os.Remove(recordPath(data)); removeErr != nil {
|
||||
return errors.Join(err, removeErr)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("导入恢复失败,已回退原实例: %w", err)
|
||||
return i18n.New("restore_failed_original_instance_restored_c03846", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -329,7 +329,7 @@ func FinishRecovery(data string) error {
|
||||
return err
|
||||
}
|
||||
if record.State != "verifying" {
|
||||
return fmt.Errorf("恢复尚未完成初始化")
|
||||
return i18n.New("the_restored_instance_has_not_5ca7fb")
|
||||
}
|
||||
if err = os.RemoveAll(filepath.Dir(record.Stage)); err != nil {
|
||||
return err
|
||||
@@ -383,5 +383,5 @@ func recoverSettings(data string, record RestoreRecord) error {
|
||||
if err = os.Remove(recordPath(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("设置导入被中断,已回退原设置和资源")
|
||||
return i18n.New("settings_import_was_interrupted_original_417ef5")
|
||||
}
|
||||
Reference in new issue
Block a user