Add image listing and asset management
Show images in asset directories with preview, rename, replacement, and trash recovery. Update local Markdown references when images move and restore original files if related metadata cannot be committed. Refresh image previews after replacement and compare actual contents when deduplicating subsequent uploads.
This commit is contained in:
1 parent
8b54ccba18
commit
d253536009
25 files changed
+801
-103
No files matched your search
@@ -0,0 +1,58 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReplaceImageAPI(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
f.write("assets/a.png", "original")
|
||||
f.write("a.md", "")
|
||||
var body bytes.Buffer
|
||||
form := multipart.NewWriter(&body)
|
||||
file, err := form.CreateFormFile("file", "replacement.png")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.WriteString(file, "replacement"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := form.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
headers := map[string]string{"Content-Type": form.FormDataContentType()}
|
||||
if code := f.status(newClient(), "PUT", "/api/v1/assets/assets/a.png", body.String(), headers); code != 401 {
|
||||
t.Fatalf("未登录应被拒绝:%d", code)
|
||||
}
|
||||
readToken := decode[struct{ Token string }](t, f.do(f.admin, "POST", "/api/v1/tokens", `{"name":"read","scope":"read"}`, nil)).Token
|
||||
if code := f.status(newClient(), "PUT", "/api/v1/assets/assets/a.png", body.String(), map[string]string{"Content-Type": form.FormDataContentType(), "Authorization": "Bearer " + readToken}); code != 403 {
|
||||
t.Fatalf("只读令牌不能替换图片:%d", code)
|
||||
}
|
||||
for route, want := range map[string]int{"/api/v1/assets/assets/a.png": 200, "/api/v1/assets/missing.png": 404, "/api/v1/assets/a.md": 400} {
|
||||
if code := f.status(f.admin, "PUT", route, body.String(), headers); code != want {
|
||||
t.Fatalf("%s:得到 %d,期望 %d", route, code, want)
|
||||
}
|
||||
}
|
||||
res := f.do(f.admin, "GET", "/api/v1/raw/assets/a.png", "", nil)
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil || string(data) != "replacement" || res.Header.Get("Content-Security-Policy") == "" || res.Header.Get("Cache-Control") != "no-store" {
|
||||
t.Fatalf("替换内容或安全边界错误:%q %v", data, err)
|
||||
}
|
||||
note := f.do(f.admin, "GET", "/api/v1/files/a.md", "", nil)
|
||||
data, err = io.ReadAll(note.Body)
|
||||
if err != nil || string(data) != "" {
|
||||
t.Fatalf("替换不应改变引用:%q %v", data, err)
|
||||
}
|
||||
// 复用现有移动与回收站接口,图片不会被当作 Markdown 正文读取。
|
||||
if code := f.status(f.admin, "POST", "/api/v1/move", `{"from":"assets/a.png","to":"assets/b.png"}`, nil); code != 200 {
|
||||
t.Fatal(code)
|
||||
}
|
||||
deleted := decode[struct{ ID string }](t, f.do(f.admin, "DELETE", "/api/v1/files/assets/b.png", "", nil))
|
||||
if code := f.status(f.admin, "POST", "/api/v1/trash/"+deleted.ID+"/restore", "", nil); code != http.StatusOK {
|
||||
t.Fatal(code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user