Revert "Add encrypted note storage and keyring endpoints"
This reverts commit 35d3138c7d.
This commit is contained in:
1 parent
b26c5aa693
commit
3c98a6bbd9
5 files changed
+19
-316
No files matched your search
+5
-55
@@ -56,9 +56,6 @@ func (s *Server) Handler(static fs.FS) http.Handler {
|
|||||||
"POST /api/v1/move": s.move,
|
"POST /api/v1/move": s.move,
|
||||||
"POST /api/v1/folders": s.mkdir,
|
"POST /api/v1/folders": s.mkdir,
|
||||||
"POST /api/v1/assets": s.uploadAsset,
|
"POST /api/v1/assets": s.uploadAsset,
|
||||||
"DELETE /api/v1/assets/{path...}": s.deleteAsset,
|
|
||||||
"GET /api/v1/keyring": s.readKeyring,
|
|
||||||
"PUT /api/v1/keyring": s.writeKeyring,
|
|
||||||
"GET /api/v1/raw/{path...}": s.raw,
|
"GET /api/v1/raw/{path...}": s.raw,
|
||||||
"GET /api/v1/trash": s.trashList,
|
"GET /api/v1/trash": s.trashList,
|
||||||
"POST /api/v1/trash/{id}/restore": s.trashRestore,
|
"POST /api/v1/trash/{id}/restore": s.trashRestore,
|
||||||
@@ -240,21 +237,18 @@ func (s *Server) readFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeFile 以请求体整体作为笔记内容。If-Match: "rev" 要求版本一致;If-None-Match: * 要求文件不存在;都不带则直接覆盖。
|
// writeFile 以请求体整体作为笔记内容。If-Match: "rev" 要求版本一致;If-None-Match: * 要求文件不存在;都不带则直接覆盖
|
||||||
// 带 ?convertFrom=同名笔记 时为加密/取消加密:写入本笔记并永久删除原笔记,If-Match 校验原笔记的版本
|
|
||||||
func (s *Server) writeFile(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) writeFile(w http.ResponseWriter, r *http.Request) {
|
||||||
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxNoteBytes))
|
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxNoteBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, 413, "笔记过大")
|
writeError(w, 413, "笔记过大")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pre := preconditionOf(r)
|
pre := vault.Precondition{
|
||||||
var rev string
|
IfMatch: strings.Trim(r.Header.Get("If-Match"), `"`),
|
||||||
if from := r.URL.Query().Get("convertFrom"); from != "" {
|
IfNoneMatch: r.Header.Get("If-None-Match") == "*",
|
||||||
rev, err = s.vault.Convert(from, r.PathValue("path"), data, pre)
|
|
||||||
} else {
|
|
||||||
rev, err = s.vault.Write(r.PathValue("path"), data, pre)
|
|
||||||
}
|
}
|
||||||
|
rev, err := s.vault.Write(r.PathValue("path"), data, pre)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vaultError(w, r, err)
|
vaultError(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -263,13 +257,6 @@ func (s *Server) writeFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, 200, map[string]string{"rev": rev})
|
writeJSON(w, 200, map[string]string{"rev": rev})
|
||||||
}
|
}
|
||||||
|
|
||||||
func preconditionOf(r *http.Request) vault.Precondition {
|
|
||||||
return vault.Precondition{
|
|
||||||
IfMatch: strings.Trim(r.Header.Get("If-Match"), `"`),
|
|
||||||
IfNoneMatch: r.Header.Get("If-None-Match") == "*",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) deleteFile(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) deleteFile(w http.ResponseWriter, r *http.Request) {
|
||||||
item, err := s.vault.Delete(r.PathValue("path"))
|
item, err := s.vault.Delete(r.PathValue("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -320,43 +307,6 @@ func (s *Server) uploadAsset(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, 200, map[string]string{"link": link})
|
writeJSON(w, 200, map[string]string{"link": link})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) deleteAsset(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if err := s.vault.DeleteAsset(r.PathValue("path")); err != nil {
|
|
||||||
vaultError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeJSON(w, 200, map[string]bool{"ok": true})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 加密密钥环(内容由浏览器生成,服务端只保存) ----
|
|
||||||
|
|
||||||
func (s *Server) readKeyring(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data, rev, err := s.vault.ReadKeyring()
|
|
||||||
if err != nil {
|
|
||||||
vaultError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("ETag", `"`+rev+`"`)
|
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) writeKeyring(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxJSONBytes))
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 413, "密钥环过大")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rev, err := s.vault.WriteKeyring(data, preconditionOf(r))
|
|
||||||
if err != nil {
|
|
||||||
vaultError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("ETag", `"`+rev+`"`)
|
|
||||||
writeJSON(w, 200, map[string]string{"rev": rev})
|
|
||||||
}
|
|
||||||
|
|
||||||
// raw 输出仓库内任意非隐藏文件(主要是图片)。以沙箱 CSP 返回,防止 SVG/HTML 中的脚本在本站执行
|
// raw 输出仓库内任意非隐藏文件(主要是图片)。以沙箱 CSP 返回,防止 SVG/HTML 中的脚本在本站执行
|
||||||
func (s *Server) raw(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) raw(w http.ResponseWriter, r *http.Request) {
|
||||||
f, info, err := s.vault.OpenRaw(r.PathValue("path"))
|
f, info, err := s.vault.OpenRaw(r.PathValue("path"))
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func (x *Index) sync(files map[string]string) error {
|
|||||||
next[p] = d
|
next[p] = d
|
||||||
}
|
}
|
||||||
for p, d := range x.known {
|
for p, d := range x.known {
|
||||||
if _, ok := files[p]; !ok || vault.IsEncrypted(p) {
|
if _, ok := files[p]; !ok {
|
||||||
if err := remove(tx, d.id); err != nil {
|
if err := remove(tx, d.id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -152,10 +152,6 @@ func (x *Index) sync(files map[string]string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for p, rev := range files {
|
for p, rev := range files {
|
||||||
// 加密笔记的内容服务端无法读取,不入索引
|
|
||||||
if vault.IsEncrypted(p) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
old, ok := x.known[p]
|
old, ok := x.known[p]
|
||||||
if ok && old.rev == rev {
|
if ok && old.rev == rev {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -179,13 +179,3 @@ func TestSyncTracksChanges(t *testing.T) {
|
|||||||
t.Errorf("重启后搜索 = %q", got)
|
t.Errorf("重启后搜索 = %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncryptedNotesNotIndexed(t *testing.T) {
|
|
||||||
f := newFixture(t)
|
|
||||||
f.write("明文.md", "公开内容")
|
|
||||||
f.write("秘密.md.enc", `{"data":"公开内容"}`)
|
|
||||||
f.sync()
|
|
||||||
if got := f.paths("公开内容"); got != "明文.md" {
|
|
||||||
t.Errorf("加密笔记不应入索引: %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+13
-141
@@ -36,13 +36,8 @@ func (e *ConflictError) Error() string { return "版本冲突" }
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
NoteExt = ".md"
|
NoteExt = ".md"
|
||||||
// 加密笔记:内容为浏览器端加密的信封,服务端只做不透明存储
|
|
||||||
EncryptedNoteExt = ".md.enc"
|
|
||||||
AssetsExt = ".assets"
|
AssetsExt = ".assets"
|
||||||
// 加密笔记的图片(密文)统一使用此扩展名
|
|
||||||
encryptedAssetExt = ".enc"
|
|
||||||
metaDir = ".rawmark"
|
metaDir = ".rawmark"
|
||||||
keyringFile = "keyring.json"
|
|
||||||
tmpPrefix = ".rawmark-tmp-"
|
tmpPrefix = ".rawmark-tmp-"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,35 +105,10 @@ func CleanPath(p string) (string, error) {
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsNote(p string) bool { return noteBase(p) != "" }
|
func IsNote(p string) bool { return strings.HasSuffix(p, NoteExt) && len(p) > len(NoteExt) }
|
||||||
|
|
||||||
func IsEncrypted(p string) bool { return strings.HasSuffix(p, EncryptedNoteExt) }
|
// assetsDir 返回笔记对应的 Typora 风格图片目录:a.md -> a.assets
|
||||||
|
func assetsDir(note string) string { return strings.TrimSuffix(note, NoteExt) + AssetsExt }
|
||||||
// noteBase 去掉笔记扩展名(.md 或 .md.enc);不是笔记时返回空串
|
|
||||||
func noteBase(p string) string {
|
|
||||||
for _, ext := range []string{EncryptedNoteExt, NoteExt} {
|
|
||||||
if strings.HasSuffix(p, ext) && len(p) > len(ext) && !strings.HasSuffix(p, "/"+ext) {
|
|
||||||
return strings.TrimSuffix(p, ext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// twin 返回同名的另一种笔记:a.md <-> a.md.enc。二者共用 a.assets,不允许同时存在
|
|
||||||
func twin(note string) string {
|
|
||||||
if IsEncrypted(note) {
|
|
||||||
return noteBase(note) + NoteExt
|
|
||||||
}
|
|
||||||
return noteBase(note) + EncryptedNoteExt
|
|
||||||
}
|
|
||||||
|
|
||||||
// assetsDir 返回笔记对应的 Typora 风格图片目录:a.md、a.md.enc -> a.assets
|
|
||||||
func assetsDir(note string) string { return noteBase(note) + AssetsExt }
|
|
||||||
|
|
||||||
func exists(p string) bool {
|
|
||||||
_, err := os.Lstat(p)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vault) abs(rel string) (string, error) {
|
func (v *Vault) abs(rel string) (string, error) {
|
||||||
rel, err := CleanPath(rel)
|
rel, err := CleanPath(rel)
|
||||||
@@ -214,24 +184,16 @@ func (v *Vault) Write(rel string, content []byte, pre Precondition) (string, err
|
|||||||
}
|
}
|
||||||
v.mu.Lock()
|
v.mu.Lock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.Unlock()
|
||||||
if !exists(p) && exists(twin(p)) {
|
|
||||||
return "", ErrExists
|
|
||||||
}
|
|
||||||
return writeChecked(p, content, pre)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeChecked 按前置条件写入文件,调用方须持有 v.mu
|
|
||||||
func writeChecked(p string, content []byte, pre Precondition) (string, error) {
|
|
||||||
cur, err := os.ReadFile(p)
|
cur, err := os.ReadFile(p)
|
||||||
found := err == nil
|
exists := err == nil
|
||||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if pre.IfNoneMatch && found {
|
if pre.IfNoneMatch && exists {
|
||||||
return "", ErrExists
|
return "", ErrExists
|
||||||
}
|
}
|
||||||
if pre.IfMatch != "" {
|
if pre.IfMatch != "" {
|
||||||
if !found {
|
if !exists {
|
||||||
return "", &ConflictError{}
|
return "", &ConflictError{}
|
||||||
}
|
}
|
||||||
if Rev(cur) != pre.IfMatch {
|
if Rev(cur) != pre.IfMatch {
|
||||||
@@ -360,7 +322,6 @@ func (v *Vault) Tree() ([]Entry, error) {
|
|||||||
// ---- 移动 / 重命名 ----
|
// ---- 移动 / 重命名 ----
|
||||||
|
|
||||||
// Move 移动或重命名笔记/文件夹。笔记的 .assets 目录随之移动;笔记改名时同步改写正文中指向自身图片目录的链接
|
// Move 移动或重命名笔记/文件夹。笔记的 .assets 目录随之移动;笔记改名时同步改写正文中指向自身图片目录的链接
|
||||||
// (加密笔记的正文服务端无法改写,由浏览器解密后处理)。不能借移动在普通笔记与加密笔记之间转换
|
|
||||||
func (v *Vault) Move(from, to string) error {
|
func (v *Vault) Move(from, to string) error {
|
||||||
src, err := v.abs(from)
|
src, err := v.abs(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -383,10 +344,10 @@ func (v *Vault) Move(from, to string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
isNote := !info.IsDir()
|
isNote := !info.IsDir()
|
||||||
if isNote && (!IsNote(from) || !IsNote(to) || IsEncrypted(from) != IsEncrypted(to)) {
|
if isNote && (!IsNote(from) || !IsNote(to)) {
|
||||||
return ErrInvalidPath
|
return ErrInvalidPath
|
||||||
}
|
}
|
||||||
if exists(dst) || (isNote && exists(twin(dst))) {
|
if _, err := os.Lstat(dst); err == nil {
|
||||||
return ErrExists
|
return ErrExists
|
||||||
}
|
}
|
||||||
srcAssets, dstAssets := "", ""
|
srcAssets, dstAssets := "", ""
|
||||||
@@ -408,9 +369,9 @@ func (v *Vault) Move(from, to string) error {
|
|||||||
if err := os.Rename(srcAssets, dstAssets); err != nil {
|
if err := os.Rename(srcAssets, dstAssets); err != nil {
|
||||||
return fmt.Errorf("移动图片目录失败: %w", err)
|
return fmt.Errorf("移动图片目录失败: %w", err)
|
||||||
}
|
}
|
||||||
oldBase := path.Base(noteBase(from))
|
oldBase := strings.TrimSuffix(path.Base(from), NoteExt)
|
||||||
newBase := path.Base(noteBase(to))
|
newBase := strings.TrimSuffix(path.Base(to), NoteExt)
|
||||||
if oldBase != newBase && !IsEncrypted(to) {
|
if oldBase != newBase {
|
||||||
data, err := os.ReadFile(dst)
|
data, err := os.ReadFile(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -521,7 +482,7 @@ func (v *Vault) Restore(id string) (TrashItem, error) {
|
|||||||
}
|
}
|
||||||
v.mu.Lock()
|
v.mu.Lock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.Unlock()
|
||||||
if exists(dst) || (!item.Dir && exists(twin(dst))) {
|
if _, err := os.Lstat(dst); err == nil {
|
||||||
return TrashItem{}, ErrExists
|
return TrashItem{}, ErrExists
|
||||||
}
|
}
|
||||||
if item.Assets != "" {
|
if item.Assets != "" {
|
||||||
@@ -573,9 +534,7 @@ func (v *Vault) SaveAsset(note, origName string, r io.Reader, limit int64) (stri
|
|||||||
return "", ErrTooLarge
|
return "", ErrTooLarge
|
||||||
}
|
}
|
||||||
ext := strings.ToLower(filepath.Ext(origName))
|
ext := strings.ToLower(filepath.Ext(origName))
|
||||||
if IsEncrypted(note) {
|
if !extPattern.MatchString(ext) {
|
||||||
ext = encryptedAssetExt
|
|
||||||
} else if !extPattern.MatchString(ext) {
|
|
||||||
ext = sniffExt(data)
|
ext = sniffExt(data)
|
||||||
}
|
}
|
||||||
sum := sha256.Sum256(data)
|
sum := sha256.Sum256(data)
|
||||||
@@ -618,90 +577,3 @@ func EscapeLinkPath(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var linkEscaper = strings.NewReplacer(" ", "%20", "(", "%28", ")", "%29", "<", "%3C", ">", "%3E")
|
var linkEscaper = strings.NewReplacer(" ", "%20", "(", "%28", ")", "%29", "<", "%3C", ">", "%3E")
|
||||||
|
|
||||||
// DeleteAsset 永久删除笔记 .assets 目录中的一个文件。用于加密/取消加密笔记后清理转换前的图片:
|
|
||||||
// 加密前的明文图片不应留在回收站中
|
|
||||||
func (v *Vault) DeleteAsset(rel string) error {
|
|
||||||
if !strings.HasSuffix(path.Dir(rel), AssetsExt) {
|
|
||||||
return ErrInvalidPath
|
|
||||||
}
|
|
||||||
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.Mode().IsRegular() {
|
|
||||||
return ErrInvalidPath
|
|
||||||
}
|
|
||||||
return os.Remove(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 加密 ----
|
|
||||||
|
|
||||||
// Convert 在普通笔记与同名加密笔记之间转换:写入 to 并永久删除 from(明文不进回收站)。
|
|
||||||
// pre 校验的是 from 的版本,确保转换的是最新内容
|
|
||||||
func (v *Vault) Convert(from, to string, content []byte, pre Precondition) (string, error) {
|
|
||||||
if !IsNote(from) || twin(from) != to {
|
|
||||||
return "", ErrInvalidPath
|
|
||||||
}
|
|
||||||
src, err := v.abs(from)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
dst, err := v.abs(to)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
v.mu.Lock()
|
|
||||||
defer v.mu.Unlock()
|
|
||||||
cur, err := os.ReadFile(src)
|
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return "", ErrNotFound
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if pre.IfMatch != "" && Rev(cur) != pre.IfMatch {
|
|
||||||
return "", &ConflictError{Rev: Rev(cur)}
|
|
||||||
}
|
|
||||||
if exists(dst) {
|
|
||||||
return "", ErrExists
|
|
||||||
}
|
|
||||||
if err := atomicWrite(dst, content); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := os.Remove(src); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return Rev(content), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadKeyring 读取加密密钥环(被加密密码包裹的主密钥,浏览器端生成和解开);尚未设置时返回 ErrNotFound
|
|
||||||
func (v *Vault) ReadKeyring() ([]byte, string, error) {
|
|
||||||
data, err := os.ReadFile(filepath.Join(v.root, metaDir, keyringFile))
|
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return nil, "", ErrNotFound
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
return data, Rev(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteKeyring 写入密钥环。首次设置用 IfNoneMatch,修改加密密码用 IfMatch,防止覆盖另一端的修改
|
|
||||||
func (v *Vault) WriteKeyring(data []byte, pre Precondition) (string, error) {
|
|
||||||
if !json.Valid(data) {
|
|
||||||
return "", ErrInvalidPath
|
|
||||||
}
|
|
||||||
v.mu.Lock()
|
|
||||||
defer v.mu.Unlock()
|
|
||||||
return writeChecked(filepath.Join(v.root, metaDir, keyringFile), data, pre)
|
|
||||||
}
|
|
||||||
@@ -152,108 +152,3 @@ func TestTreeHidesMetaAndAssets(t *testing.T) {
|
|||||||
t.Fatalf("目录树不符: %v", paths)
|
t.Fatalf("目录树不符: %v", paths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncryptedNotes(t *testing.T) {
|
|
||||||
v := newVault(t)
|
|
||||||
if _, err := v.Write("a.md", []byte("明文"), Precondition{}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
link, err := v.SaveAsset("a.md", "x.png", strings.NewReader("png"), 1<<20)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// 同名的普通笔记与加密笔记不能同时存在(二者共用 a.assets)
|
|
||||||
if _, err := v.Write("a.md.enc", []byte("{}"), Precondition{}); !errors.Is(err, ErrExists) {
|
|
||||||
t.Fatal("同名加密笔记不应能直接创建:", err)
|
|
||||||
}
|
|
||||||
// 转换校验原笔记版本,写入新笔记并永久删除原笔记
|
|
||||||
if _, err := v.Convert("a.md", "a.md.enc", []byte(`{"v":1}`), Precondition{IfMatch: "stale"}); !isConflict(err) {
|
|
||||||
t.Fatal("版本不符时应拒绝转换:", err)
|
|
||||||
}
|
|
||||||
if _, err := v.Convert("a.md", "b.md.enc", []byte(`{}`), Precondition{}); !errors.Is(err, ErrInvalidPath) {
|
|
||||||
t.Fatal("只能转换为同名笔记:", err)
|
|
||||||
}
|
|
||||||
if _, err := v.Convert("a.md", "a.md.enc", []byte(`{"v":1}`), Precondition{IfMatch: Rev([]byte("明文"))}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, _, err := v.Read("a.md"); !errors.Is(err, ErrNotFound) {
|
|
||||||
t.Fatal("原笔记应被删除")
|
|
||||||
}
|
|
||||||
if items, _ := v.TrashList(); len(items) != 0 {
|
|
||||||
t.Fatal("转换前的明文不应进入回收站")
|
|
||||||
}
|
|
||||||
// 加密笔记的图片统一为 .enc,与普通笔记共用图片目录
|
|
||||||
encLink, err := v.SaveAsset("a.md.enc", "y.png", strings.NewReader("密文"), 1<<20)
|
|
||||||
if err != nil || !strings.HasPrefix(encLink, "a.assets/") || !strings.HasSuffix(encLink, ".enc") {
|
|
||||||
t.Fatal("加密图片链接不符:", encLink, err)
|
|
||||||
}
|
|
||||||
// 清理转换前的明文图片:只允许删除 .assets 目录中的文件
|
|
||||||
if err := v.DeleteAsset(link); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := v.DeleteAsset("a.md.enc"); !errors.Is(err, ErrInvalidPath) {
|
|
||||||
t.Fatal("不应能经此删除笔记")
|
|
||||||
}
|
|
||||||
// 改名:图片目录随之移动,密文不改写;不能借移动转换类型,也不能与同名普通笔记冲突
|
|
||||||
if err := v.Move("a.md.enc", "c.md"); !errors.Is(err, ErrInvalidPath) {
|
|
||||||
t.Fatal("移动不能改变加密类型:", err)
|
|
||||||
}
|
|
||||||
if _, err := v.Write("c.md", []byte("x"), Precondition{}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := v.Move("a.md.enc", "c.md.enc"); !errors.Is(err, ErrExists) {
|
|
||||||
t.Fatal("不能移动到与普通笔记同名:", err)
|
|
||||||
}
|
|
||||||
if err := v.Move("a.md.enc", "d.md.enc"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if data, _, _ := v.Read("d.md.enc"); string(data) != `{"v":1}` {
|
|
||||||
t.Fatal("加密笔记内容不应被改写:", string(data))
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(filepath.Join(v.Root(), strings.Replace(encLink, "a.assets", "d.assets", 1))); err != nil {
|
|
||||||
t.Fatal("图片目录未随加密笔记移动:", err)
|
|
||||||
}
|
|
||||||
entries, _ := v.Tree()
|
|
||||||
var names []string
|
|
||||||
for _, e := range entries {
|
|
||||||
names = append(names, e.Path)
|
|
||||||
}
|
|
||||||
if strings.Join(names, ",") != "c.md,d.md.enc" {
|
|
||||||
t.Fatal("目录树应包含加密笔记:", names)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyring(t *testing.T) {
|
|
||||||
v := newVault(t)
|
|
||||||
if _, _, err := v.ReadKeyring(); !errors.Is(err, ErrNotFound) {
|
|
||||||
t.Fatal("尚未设置时应返回 ErrNotFound")
|
|
||||||
}
|
|
||||||
if _, err := v.WriteKeyring([]byte("not json"), Precondition{IfNoneMatch: true}); !errors.Is(err, ErrInvalidPath) {
|
|
||||||
t.Fatal("应拒绝非 JSON")
|
|
||||||
}
|
|
||||||
rev, err := v.WriteKeyring([]byte(`{"v":1}`), Precondition{IfNoneMatch: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := v.WriteKeyring([]byte(`{"v":2}`), Precondition{IfNoneMatch: true}); !errors.Is(err, ErrExists) {
|
|
||||||
t.Fatal("已设置时不能再次初始化")
|
|
||||||
}
|
|
||||||
if _, err := v.WriteKeyring([]byte(`{"v":2}`), Precondition{IfMatch: "stale"}); !isConflict(err) {
|
|
||||||
t.Fatal("版本不符时应拒绝")
|
|
||||||
}
|
|
||||||
if _, err := v.WriteKeyring([]byte(`{"v":2}`), Precondition{IfMatch: rev}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if data, _, _ := v.ReadKeyring(); string(data) != `{"v":2}` {
|
|
||||||
t.Fatal("密钥环内容不符:", string(data))
|
|
||||||
}
|
|
||||||
// 密钥环在隐藏目录中,不经文件接口暴露
|
|
||||||
if _, _, err := v.Read(".rawmark/keyring.json"); err == nil {
|
|
||||||
t.Fatal("不应能经文件接口读取密钥环")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isConflict(err error) bool {
|
|
||||||
var c *ConflictError
|
|
||||||
return errors.As(err, &c)
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user