Include note filenames in new share links

This commit is contained in:
eezd committed 2026-10-20 14:41:43 +08:00
1 parent ac81544e03
commit e66fee9dac
4 files changed
+52 -4

No files matched your search

+48
View File
@@ -3,11 +3,13 @@ package api
import (
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"regexp"
@@ -350,6 +352,52 @@ func TestShareAccess(t *testing.T) {
}
}
func TestShareFilenameToken(t *testing.T) {
f := newFixture(t)
name := "计划 #1? 50% +;\""
p := "目录/" + name + ".md"
f.write(p, "正文")
f.write("目录/"+name+".assets/图.png", "PNG")
body, err := json.Marshal(map[string]string{"path": p, "password": "访问密码"})
if err != nil {
t.Fatal(err)
}
sh := decode[shareView](t, f.do(f.admin, "POST", "/api/v1/shares", string(body), nil))
suffix, ok := strings.CutPrefix(sh.Token, name+"-")
random, err := base64.RawURLEncoding.DecodeString(suffix)
if !ok || err != nil || len(random) != 16 {
t.Fatal("分享应包含完整文件名和 128 位随机字符,不包含目录")
}
other := decode[shareView](t, f.do(f.admin, "POST", "/api/v1/shares", string(body), nil))
if other.Token == sh.Token {
t.Fatal("同一笔记的分享链接不能重复")
}
base := "/api/v1/public/shares/" + url.PathEscape(sh.Token)
guest := newClient()
if code := f.status(guest, "GET", base, "", nil); code != 401 {
t.Fatalf("未解锁应拒绝访问,得到 %d", code)
}
if code := f.status(guest, "POST", base+"/unlock", `{"password":"访问密码"}`, nil); code != 200 {
t.Fatalf("解锁失败:%d", code)
}
if code := f.status(guest, "GET", base, "", nil); code != 200 {
t.Fatalf("带保留字符的链接应可读取正文,得到 %d", code)
}
asset := base + "/assets/" + url.PathEscape(name+".assets") + "/图.png"
if code := f.status(guest, "GET", asset, "", nil); code != 200 {
t.Fatalf("解锁凭据应覆盖图片路径,得到 %d", code)
}
if code := f.status(guest, "GET", "/api/v1/public/shares/"+url.PathEscape(other.Token), "", nil); code != 401 {
t.Fatalf("凭据不能解锁另一分享,得到 %d", code)
}
if code := f.status(f.admin, "DELETE", "/api/v1/shares/"+url.PathEscape(sh.Token), "", nil); code != 200 {
t.Fatalf("撤销带保留字符的链接失败:%d", code)
}
if code := f.status(guest, "GET", base, "", nil); code != 404 {
t.Fatalf("撤销后应返回 404,得到 %d", code)
}
}
func TestShareWithoutPasswordAndExpiry(t *testing.T) {
f := newFixture(t)
f.write("a.md", "公开")
+1 -1
View File
@@ -1083,7 +1083,7 @@
},
"responses": {
"200": {
"description": "新分享,访问地址为 /s/{token}",
"description": "新分享,token 为文件名(不含 .md)加连字符和随机字符,访问地址为 /s/{URL 编码后的 token};文件移动或改名不改变已生成的链接",
"content": {
"application/json": {
"schema": {
+2 -2
View File
@@ -76,7 +76,7 @@ func (s *Server) createShare(w http.ResponseWriter, r *http.Request) {
}
now := time.Now()
sh := store.Share{
Token: auth.NewShareToken(),
Token: strings.TrimSuffix(path.Base(p), vault.NoteExt) + "-" + auth.NewShareToken(),
Path: p,
PasswordHash: auth.HashSharePassword(body.Password),
CreatedAt: now.UnixMilli(),
@@ -214,7 +214,7 @@ func (s *Server) unlockShare(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: shareCookieName,
Value: value,
Path: "/api/v1/public/shares/" + sh.Token,
// 默认 Path 为 /unlock 所在目录,避免非 ASCII 文件名违反 Cookie Path 格式。
MaxAge: int(ttl / time.Second),
HttpOnly: true,
Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
+1 -1
View File
@@ -22,7 +22,7 @@ const busy = ref(false)
const failed = ref(false)
const fmt = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short', timeStyle: 'short' })
const linkOf = (s: Share) => `${location.origin}/s/${s.token}`
const linkOf = (s: Share) => `${location.origin}/s/${encodeURIComponent(s.token)}`
const expired = (s: Share) => s.expiresAt !== 0 && s.expiresAt <= Date.now()
function errorText(err: unknown) {