chore: standardize project comments in English

This commit is contained in:
eezd committed 2026-10-26 22:52:12 +08:00
1 parent a874a39ea5
commit ccb16acbea
167 files changed
+820 -820

No files matched your search

+21 -21
View File
@@ -1,4 +1,4 @@
// Package api 提供 REST API(/api/v1)、事件推送与前端静态资源。前端只通过这套 API 访问数据。
// Package api serves the REST API (/api/v1), events, and frontend assets. The frontend accesses data only through this API.
package api
import (
@@ -37,9 +37,9 @@ type Server struct {
store *store.Store
hub *events.Hub
index *search.Index
// 串行化设置的"读取-合并-写入"
// Serialize settings read/merge/write operations.
settingsMu sync.Mutex
// ThemesDir 指向数据目录中的主题目录;为空时只有内置主题。
// ThemesDir points to the data directory's themes folder; an empty value enables only built-in themes.
DataDir string
ThemesDir string
PluginsDir string
@@ -62,14 +62,14 @@ func New(v *vault.Vault, a *auth.Manager, st *store.Store, hub *events.Hub, inde
return &Server{vault: v, auth: a, store: st, hub: hub, index: index, captchas: newCaptchas()}
}
// access 为接口的访问要求
// access defines an endpoint's access requirement.
type access int
const (
public access = iota
// 登录会话或 API 令牌(只读令牌可用 GET 和只读导出接口)
// Login session or API token (read-only tokens may use GET and read-only exports).
authed
// 只允许登录会话:管理 API 令牌,避免令牌给自己扩权
// Login sessions only: prevent API tokens from expanding their own permissions through token management.
sessionOnly
)
@@ -79,7 +79,7 @@ type route struct {
handler http.HandlerFunc
}
// routes 为全部接口;openapi.json 须与之一致(见 TestOpenAPICoversRoutes)
// routes lists all endpoints; openapi.json must match it (see TestOpenAPICoversRoutes).
func (s *Server) routes() []route {
return []route{
{"GET /api/v1/health", public, func(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, map[string]bool{"ok": true}) }},
@@ -170,7 +170,7 @@ func (s *Server) Handler(static fs.FS) http.Handler {
return localized(securityHeaders(sameOrigin(s.guardAPI(mux))))
}
// ---- 中间件 ----
// ---- Middleware ----
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -185,8 +185,8 @@ func securityHeaders(next http.Handler) http.Handler {
})
}
// sameOrigin 保持写请求的同源边界;Bearer 鉴权不依赖浏览器 Cookie。
// 没有 Origin 头的请求来自非浏览器客户端,放行
// sameOrigin preserves the same-origin boundary for writes; Bearer authentication does not rely on browser Cookies.
// Requests without Origin come from non-browser clients and are allowed.
func sameOrigin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
@@ -201,7 +201,7 @@ func sameOrigin(next http.Handler) http.Handler {
})
}
// ---- 鉴权 ----
// ---- Authentication ----
func (s *Server) authState(w http.ResponseWriter, r *http.Request) {
setUp, err := s.auth.IsSetUp()
@@ -276,7 +276,7 @@ func (s *Server) me(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]any{"permissions": []string{"vault:read", "vault:write", "instance:manage"}, "expiresAt": identity(r).ExpiresAt})
}
// ---- 文件 ----
// ---- Files ----
func (s *Server) tree(w http.ResponseWriter, r *http.Request) {
entries, err := s.vault.Tree()
@@ -331,7 +331,7 @@ func (s *Server) readFile(w http.ResponseWriter, r *http.Request) {
w.Write(data)
}
// writeFile 以请求体整体作为笔记内容。If-Match: "rev" 要求版本一致;If-None-Match: * 要求文件不存在;都不带则直接覆盖
// writeFile uses the entire request body as note content. If-Match: "rev" requires a matching revision; If-None-Match: * requires a new file; omitting both overwrites unconditionally.
func (s *Server) writeFile(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxNoteBytes))
if err != nil {
@@ -371,10 +371,10 @@ func (s *Server) move(w http.ResponseWriter, r *http.Request) {
return
}
if rewritten {
// 原子改写替换了文件身份,监听器无法再从快照中识别这次移动。
// Atomic rewriting replaced the file identity, so the watcher can no longer identify this move from its snapshot.
s.hub.Broadcast(events.Event{Type: "moved", From: body.From, Path: body.To}, events.Event{Type: "tree"})
} else {
// 图标提交在文件移动之后;不能仅依赖文件监听器较早的通知。
// Icons commit after the file move; the watcher's earlier notification alone is insufficient.
s.hub.Broadcast(events.Event{Type: "tree"})
}
writeJSON(w, 200, map[string]bool{"ok": true})
@@ -392,7 +392,7 @@ func (s *Server) mkdir(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]bool{"ok": true})
}
// uploadAsset 接收 multipart 表单中的 file 字段,存入指定笔记或目录下的共享 assets 目录。
// uploadAsset accepts the multipart file field and saves it in the shared assets directory for the selected note or folder.
func (s *Server) uploadAsset(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxAssetBytes+1<<20)
file, header, err := r.FormFile("file")
@@ -418,7 +418,7 @@ func (s *Server) uploadAsset(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]string{"link": link})
}
// replaceImage 仅替换已存在的图片;路径与引用保持不变。
// replaceImage replaces only an existing image, preserving its path and references.
func (s *Server) replaceImage(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxAssetBytes+1<<20)
if err := r.ParseMultipartForm(1 << 20); err != nil {
@@ -444,7 +444,7 @@ func (s *Server) replaceImage(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]bool{"ok": true})
}
// raw 输出仓库内任意非隐藏文件(主要是图片)。以沙箱 CSP 返回,防止 SVG/HTML 中的脚本在本站执行
// raw serves any non-hidden vault file (mainly images). A sandbox CSP prevents SVG/HTML scripts from running on this origin.
func (s *Server) raw(w http.ResponseWriter, r *http.Request) {
s.serveRaw(w, r, r.PathValue("path"))
}
@@ -465,7 +465,7 @@ func (s *Server) serveRaw(w http.ResponseWriter, r *http.Request, rel string) {
http.ServeContent(w, r, info.Name(), info.ModTime(), f)
}
// ---- 回收站 ----
// ---- Recycle bin ----
func (s *Server) trashList(w http.ResponseWriter, r *http.Request) {
items, err := s.vault.TrashList()
@@ -494,9 +494,9 @@ func (s *Server) trashPurge(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]bool{"ok": true})
}
// ---- 响应工具 ----
// ---- Response helpers ----
// clientIP 返回限速用的来源地址(未经反向代理头部,避免伪造)
// clientIP returns the source address for rate limiting, ignoring proxy headers to prevent spoofing.
func clientIP(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
+5 -5
View File
@@ -33,7 +33,7 @@ type fixture struct {
root string
store *store.Store
hub *events.Hub
// 已登录的客户端
// Authenticated client.
admin *http.Client
}
@@ -88,7 +88,7 @@ func TestEventsRejectRevokedCredentials(t *testing.T) {
t.Fatal(err)
}
defer conn.CloseNow()
// 首条事件确认订阅已经建立,避免把握手和广播的时序当作鉴权结果。
// The first event confirms subscription before testing authorization, avoiding handshake/broadcast timing races.
f.hub.Broadcast(events.Event{Type: "tree"})
if _, _, err := conn.Read(ctx); err != nil {
t.Fatal(err)
@@ -238,7 +238,7 @@ func decode[T any](t *testing.T, res *http.Response) T {
return envelope.Data
}
// 文档与实际接口一一对应:新增接口忘记写文档、文档中残留已删除的接口都会失败
// Documentation must match actual endpoints: missing new routes and obsolete documented routes both fail.
func TestOpenAPICoversRoutes(t *testing.T) {
var spec struct {
Paths map[string]map[string]json.RawMessage
@@ -271,7 +271,7 @@ func TestOpenAPICoversRoutes(t *testing.T) {
func TestAPIResponsesAreNotCached(t *testing.T) {
f := newFixture(t)
f.write("笔记.md", "# 正文")
// 登录状态与目录曾在 Firefox 离线时命中 HTTP 缓存,误进入无法加载的工作区。
// Firefox once cached identity and tree responses offline, incorrectly opening an unloadable workspace.
for _, path := range []string{"/api/v1/auth/state", "/api/v1/tree", "/api/v1/files/笔记.md", "/api/v1/raw/笔记.md"} {
res := f.do(f.admin, "GET", path, "", nil)
if res.StatusCode != http.StatusOK || res.Header.Get("Cache-Control") != "no-store" {
@@ -443,7 +443,7 @@ func TestSetupWithoutCode(t *testing.T) {
if status := f.status(newClient(), "POST", "/api/v1/auth/setup", `{"password":"replacement"}`, nil); status != 409 {
t.Fatalf("重复初始化必须拒绝: %d", status)
}
// 新建 Manager 模拟重启,不能重新开放密码设置。
// A new Manager simulates a restart and must not reopen password setup.
reopened := auth.New(st)
if err := reopened.Setup("replacement"); !errors.Is(err, auth.ErrAlreadySetUp) {
t.Fatalf("重启后不能覆盖密码: %v", err)
+5 -5
View File
@@ -9,11 +9,11 @@ import (
"github.com/coder/websocket"
)
// events 通过 WebSocket 推送仓库变更。websocket.Accept 默认校验 Origin 与 Host 一致
// events pushes vault changes over WebSocket. websocket.Accept checks that Origin matches Host by default.
func (s *Server) events(w http.ResponseWriter, r *http.Request) {
s.eventWG.Add(1)
defer s.eventWG.Done()
// 先订阅再完成握手,避免连接建立后首条变更落在订阅空窗中。
// Subscribe before completing the handshake so the first change cannot fall into a subscription gap.
msgs, unsubscribe := s.hub.Subscribe()
defer unsubscribe()
conn, err := websocket.Accept(w, r, nil)
@@ -36,9 +36,9 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
}
return true
}
// 客户端不发消息;CloseRead 负责处理控制帧并在连接断开时取消 ctx
// Clients send no messages; CloseRead handles control frames and cancels ctx on disconnect.
ctx := conn.CloseRead(r.Context())
// 定期 ping,避免反向代理因空闲断开连接
// Ping periodically to prevent idle disconnects by reverse proxies.
ping := time.NewTicker(30 * time.Second)
defer ping.Stop()
for {
@@ -47,7 +47,7 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
return
case msg, ok := <-msgs:
if !ok {
// 客户端消费过慢被移出广播列表,断开后由客户端重连并全量同步
// Disconnect slow subscribers removed from the hub; clients reconnect and perform a full sync.
conn.Close(websocket.StatusTryAgainLater, "too slow")
return
}
+1 -1
View File
@@ -76,7 +76,7 @@ func TestPortableNotesExcludeApplicationData(t *testing.T) {
t.Fatal(err)
}
}
// 与真实进程一致,导出暂存文件位于应用数据目录内。
// As in the real process, export temporary files live inside the application data directory.
t.Setenv("TMPDIR", filepath.Join(data, "temp"))
res = f.do(f.admin, "POST", "/api/v1/maintenance/notes/export", "", nil)
defer res.Body.Close()
+1 -1
View File
@@ -235,7 +235,7 @@ func (s *Server) deleteFont(w http.ResponseWriter, r *http.Request) {
return
}
defer root.Close()
// 先撤下资源;设置写失败时恢复,避免留下指向已删除字体的设置。
// Remove the resource first and restore it if settings fail to save, avoiding references to a deleted font.
removed := ".remove-" + id
if err := root.Rename(id, removed); err != nil {
internalError(w, r, err)
+1 -1
View File
@@ -79,7 +79,7 @@ func TestReplaceImageAPI(t *testing.T) {
if err != nil || string(data) != "![](assets/a.png)" {
t.Fatalf("替换不应改变引用:%q %v", data, err)
}
// 复用现有移动与回收站接口,图片不会被当作 Markdown 正文读取。
// Reuse move and recycle-bin endpoints without reading images as Markdown text.
if code := f.status(f.admin, "POST", "/api/v1/move", `{"from":"assets/a.png","to":"assets/b.png"}`, nil); code != 200 {
t.Fatal(code)
}
+5 -5
View File
@@ -42,7 +42,7 @@ func publicImageIP(ip netip.Addr) bool {
if !ip.IsGlobalUnicast() || ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() {
return false
}
// 不接受 IPv6 地址转换前缀,避免其中嵌入的 IPv4 地址访问内网。
// Reject IPv6 translation prefixes so embedded IPv4 addresses cannot reach private networks.
if ip.Is6() && !netip.MustParsePrefix("2000::/3").Contains(ip) {
return false
}
@@ -64,7 +64,7 @@ func checkImageURL(u *url.URL) error {
var remoteImageClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
// 不使用环境代理,确保每个实际连接都经过目标地址校验。
// Ignore environment proxies so every actual connection passes destination-address validation.
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
ControlContext: func(_ context.Context, _, address string, _ syscall.RawConn) error {
@@ -105,7 +105,7 @@ var remoteImageClient = &http.Client{
},
}
// readImageDownload 按实际文件签名确定类型,不信任来源的扩展名或 Content-Type。
// readImageDownload determines type from file signatures, not the source extension or Content-Type.
func readImageDownload(body io.Reader) ([]byte, string, error) {
data, err := io.ReadAll(io.LimitReader(body, maxAssetBytes+1))
if err != nil {
@@ -174,7 +174,7 @@ func (s *Server) importAsset(w http.ResponseWriter, r *http.Request) {
writeError(w, 400, i18n.RenderError(errImageAddress, responseLocale(w)))
return
}
// url.Error 含完整来源 URL,其中可能有临时签名,日志只保留底层原因。
// url.Error includes the full source URL, potentially with temporary signatures; log only its underlying cause.
var urlError *url.Error
if errors.As(err, &urlError) {
err = urlError.Err
@@ -208,7 +208,7 @@ func (s *Server) importAsset(w http.ResponseWriter, r *http.Request) {
}
return
}
// 下载期间笔记可能被外部程序移动或删除;不能在旧路径下创建孤立资源。
// External programs may move or delete the note during download; do not create orphaned resources at the old path.
info, err := s.vault.Stat(note)
if err != nil {
vaultError(w, r, err)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
)
func (s *Server) importNotes(w http.ResponseWriter, r *http.Request) {
// 总请求上限包含表单开销,文件总量另由 vault 校验。
// The total request limit includes form overhead; vault validates the aggregate file size separately.
r.Body = http.MaxBytesReader(w, r.Body, vault.MaxImportBytes+(1<<20))
if err := r.ParseMultipartForm(8 << 20); err != nil {
var limit *http.MaxBytesError
+1 -1
View File
@@ -95,7 +95,7 @@ func TestImportImageDownloadAndRedirects(t *testing.T) {
transport := original.Transport.(*http.Transport).Clone()
dial := transport.DialContext
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
// 只把固定的测试来源映射到本地真实 HTTP 服务,其他目标仍走生产地址校验。
// Map only the fixed test origin to a real local HTTP server; other destinations retain production address validation.
if address == "images.example:80" {
return (&net.Dialer{}).DialContext(ctx, network, source.Listener.Addr().String())
}
+1 -1
View File
@@ -1,4 +1,4 @@
package api
// CloseEvents 在数据库关闭前断开并等待全部 WebSocket 处理结束。
// CloseEvents disconnects and waits for all WebSocket handlers before databases close.
func (s *Server) CloseEvents() { s.hub.Close(); s.eventWG.Wait() }
+2 -2
View File
@@ -29,7 +29,7 @@ type maintenanceStatus struct {
cause error
}
// gate 排空普通 API 操作后再执行维护;WebSocket 不持有长期读锁。
// gate drains ordinary API operations before maintenance; WebSockets do not hold long-lived read locks.
func (s *Server) guardAPI(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/api/v1/health" {
@@ -324,7 +324,7 @@ func readPreferences(stage string) (map[string]json.RawMessage, error) {
return values, nil
}
// importSettings 在确认后合并资源;原资源与数据库值都保留到成功提交为止。
// importSettings merges resources after confirmation, retaining original resources and database values until commit succeeds.
func (s *Server) applySettings(w http.ResponseWriter, r *http.Request) {
var body struct {
ID string
+3 -3
View File
@@ -71,7 +71,7 @@ func uploadedPackage(t *testing.T, f *fixture, kind string, data []byte) string
t.Fatalf("创建上传失败: %d", res.StatusCode)
}
id := decode[struct{ ID string }](t, res).ID
// 跨多个分块验证偏移,不引入测试开关。
// Verify offsets across multiple chunks without introducing test switches.
offset := 0
for offset < len(data) {
end := min(offset+137, len(data))
@@ -192,7 +192,7 @@ func TestFullBackupRestoresFilesDatabaseAndInvalidatesSessions(t *testing.T) {
root := t.TempDir()
vault := filepath.Join(root, "vault")
os.MkdirAll(vault, 0700)
// 解包后落入目标实例,目标的环境路径与源实例不同。
// Extract into the target instance, whose environment paths differ from the source.
if err = backup.CopyTree(filepath.Join(stage, "vault"), vault); err != nil {
t.Fatal(err)
}
@@ -273,7 +273,7 @@ func TestSettingsPackageDatabaseFailureRollsBack(t *testing.T) {
f.store.SetSetting(settingsKey, `{"fontSize":24}`)
data := exportedPackage(t, f, "settings")
f.store.SetSetting(settingsKey, `{"fontSize":18}`)
// 通过实际 SQLite 约束制造提交失败,验证备份和事务边界。
// Use an actual SQLite constraint to force commit failure and verify backup and transaction boundaries.
raw, err := sql.Open("sqlite", filepath.Join(filepath.Dir(f.root), "rawmark.db"))
if err != nil {
t.Fatal(err)
+4 -4
View File
@@ -181,7 +181,7 @@ func (s *Server) appendMaintenanceUpload(w http.ResponseWriter, r *http.Request)
}
func backupResourceError(err error) error {
var pathError *os.PathError
// 缺失文件或文件/目录类型错误来自归档内容;其他磁盘故障仍由服务端处理。
// Missing files or file/directory type mismatches are archive errors; other disk failures remain server errors.
if errors.As(err, &pathError) && !errors.Is(pathError, os.ErrNotExist) && !errors.Is(pathError, syscall.ENOTDIR) && !errors.Is(pathError, syscall.EISDIR) {
return err
}
@@ -368,7 +368,7 @@ type cleanupResult struct {
func (s *Server) clearTemp(w http.ResponseWriter, r *http.Request) {
result := cleanupResult{Failed: []string{}}
err := s.maintain("clean_temporary_files_af91ae", func() error {
// temp 为进程专属目录,普通 API 与维护已排空;恢复包和回退备份在其他目录。
// temp belongs to this process; ordinary APIs and maintenance have drained. Restore archives and rollback backups live elsewhere.
temp, err := s.tempDir()
if err != nil {
return err
@@ -381,7 +381,7 @@ func (s *Server) clearTemp(w http.ResponseWriter, r *http.Request) {
for _, entry := range entries {
targets = append(targets, filepath.Join(temp, entry.Name()))
}
// 只清理过期未使用上传;当前会话新上传和待恢复内容不会被清除。
// Clean only expired unused uploads; retain new uploads from the current session and pending restore contents.
uploads := filepath.Join(s.DataDir, ".maintenance", "uploads")
entries, err = os.ReadDir(uploads)
if err != nil && !errors.Is(err, os.ErrNotExist) {
@@ -450,7 +450,7 @@ func (s *Server) clearTemp(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, result)
}
// 仅用于服务启动前读取恢复结果,不以空响应伪装已恢复。
// Read restore results only before service startup; never disguise an empty response as a completed restore.
func (s *Server) SetRestoreFailure(err error) {
s.maintenanceStatusMu.Lock()
defer s.maintenanceStatusMu.Unlock()
+2 -2
View File
@@ -38,7 +38,7 @@ func TestOpenAPIResponseContracts(t *testing.T) {
}
return op
}
// 下载、原文、资源代理与 OpenAPI 本身不使用业务 JSON 外壳。
// Downloads, raw files, resource proxies, and OpenAPI itself do not use the business JSON envelope.
raw := map[string]bool{
"get /openapi.json": true, "get /files/{path}": true, "get /raw/{path}": true,
"get /plugins/{id}/assets/{revision}/{path}": true, "get /fonts/{id}/file": true,
@@ -133,7 +133,7 @@ func TestOpenAPIResponseContracts(t *testing.T) {
check("get", "/maintenance/status", f.do(f.admin, "GET", "/api/v1/maintenance/status", "", nil))
check("post", "/maintenance/uploads", f.do(f.admin, "POST", "/api/v1/maintenance/uploads", `{"kind":"settings","size":10}`, nil))
id := stagedPackage(t, f, "full", exportedPackage(t, f, "full"))
// 仅验证提交契约;合成实例不触发真实进程重启或恢复执行。
// Verify only the submission contract; the synthetic instance must not trigger a real process restart or restore.
f.server.Restart = func() {}
check("post", "/maintenance/restore", f.do(f.admin, "POST", "/api/v1/maintenance/restore", `{"id":"`+id+`","confirmed":true}`, nil))
}
+1 -1
View File
@@ -124,7 +124,7 @@ func TestPluginInstallBoundariesAndData(t *testing.T) {
if res := pluginUpload(t, f, f.admin, large); res.StatusCode != 400 {
t.Fatalf("解压超限得到 %d", res.StatusCode)
}
// 令牌即使有写权限,也不能植入或读取插件代码及私有配置。
// Even write-capable API tokens cannot install or read plugin code or private configuration.
token := decode[struct{ Token string }](t, f.do(f.admin, "POST", "/api/v1/tokens", `{"name":"测试","scope":"write"}`, nil)).Token
for _, target := range []string{"/api/v1/plugins", base + "/data", asset} {
if code := f.status(guest, "GET", target, "", map[string]string{"Authorization": "Bearer " + token}); code != 403 {
+4 -4
View File
@@ -12,7 +12,7 @@ import (
"rawmark/internal/events"
)
// 应用设置:一份 JSON 对象,全部设备共用。内容由前端定义,服务端合并与保存,并校验由服务端执行的清理期限
// Application settings are one JSON object shared across devices. The frontend defines contents; the server merges, saves, and validates server-enforced retention.
const settingsKey = "app.settings"
func (s *Server) readSettings() (map[string]json.RawMessage, error) {
@@ -34,8 +34,8 @@ func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, settings)
}
// patchSettings 按顶层键合并:请求中的键覆盖原值,值为 null 的键被删除。
// 不同设备同时修改不同的设置不会互相覆盖;合并后通知其他设备重新读取
// patchSettings merges top-level keys: supplied values replace existing values, and null removes a key.
// Concurrent changes to different settings do not overwrite each other; notify other devices to reload after merging.
func (s *Server) patchSettings(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxJSONBytes))
if err != nil {
@@ -86,7 +86,7 @@ func (s *Server) patchSettings(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, settings)
}
// changePassword 修改登录密码;当前会话保留,其他设备需重新登录
// changePassword changes the login password, retaining the current session and requiring other devices to sign in again.
func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
var body struct{ Current, Next string }
if !readJSON(w, r, &body) {
+8 -8
View File
@@ -12,19 +12,19 @@ import (
"time"
)
// 应用页面的 CSP:CodeMirror 通过 <style> 注入样式,需要 'unsafe-inline';
// 笔记中可引用外部图片,img-src 放开 http/https
// Application CSP: CodeMirror injects <style> elements and requires 'unsafe-inline';
// notes may reference external images, so img-src allows http/https.
const appCSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: blob: https: http:; connect-src 'self'; worker-src 'self' blob:; font-src 'self' data:; " +
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
type staticFile struct {
data []byte
gzipped []byte // 压缩后不变小时为 nil
gzipped []byte // nil when compression does not reduce size.
}
// staticHandler 提供前端构建产物:带哈希的 assets/ 长期缓存,其余每次校验;
// 文本资源首次请求时压缩并缓存在内存(嵌入的文件不会变化)
// staticHandler serves frontend builds: hashed assets/ files use long-lived caching; other files revalidate on every request.
// Text resources are compressed and cached in memory on first request (embedded files never change).
type staticHandler struct {
fsys fs.FS
started time.Time
@@ -42,14 +42,14 @@ func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
name := strings.TrimPrefix(path.Clean("/"+r.URL.Path), "/")
// 已移除的公开入口不能回退到应用首页。
// Removed public entry points must not fall back to the application home page.
if name == "s" || strings.HasPrefix(name, "s/") || name == "share.html" {
writeError(w, 404, message(w, "page_not_found_e75737"))
return
}
f, err := h.load(name)
if err != nil {
// 前端路由:未知路径一律返回入口页面
// Frontend routing: unknown paths return the entry page.
name = "index.html"
if f, err = h.load(name); err != nil {
writeError(w, 404, message(w, "frontend_resources_are_missing_2c9b3e"))
@@ -66,7 +66,7 @@ func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
hd.Set("Content-Security-Policy", appCSP)
}
if path.Ext(name) == ".webmanifest" {
// Go 的 MIME 表未必包含此扩展名,不能让 ServeContent 对压缩后的数据嗅探类型。
// Go's MIME table may lack this extension; do not let ServeContent sniff the compressed bytes.
hd.Set("Content-Type", "application/manifest+json")
} else if ct := mime.TypeByExtension(path.Ext(name)); ct != "" {
hd.Set("Content-Type", ct)
+3 -3
View File
@@ -39,8 +39,8 @@ func (s *Server) getStorage(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, usage)
}
// 只统计普通文件逻辑字节,不跟随内部符号链接;外置仓库单独扫描。
// 两个根互相嵌套时,各自由更具体的根负责,避免重复计算。
// Count logical bytes of regular files only, without following internal symlinks; scan external vaults separately.
// For nested roots, assign each file to the more specific root to avoid double counting.
func scanStorage(ctx context.Context, dataDir, vaultDir string) (storageUsage, error) {
usage := storageUsage{Categories: []storageCategory{{ID: "vault"}, {ID: "databases"}, {ID: "fonts"}, {ID: "themes"}, {ID: "plugins"}, {ID: "other"}}}
if dataDir == "" {
@@ -80,7 +80,7 @@ func scanStorage(ctx context.Context, dataDir, vaultDir string) (storageUsage, e
}
if walkErr != nil {
if path != root && errors.Is(walkErr, fs.ErrNotExist) {
return nil // 文件可能在扫描期间被移动或删除。
return nil // Files may move or disappear during the scan.
}
return walkErr
}
+1 -1
View File
@@ -19,7 +19,7 @@
--rm-highlight-bg: rgba(232, 196, 119, 0.25);
}
/* 绿色按钮使用深色文字,保持按钮的文字对比度。 */
/* Use dark text on green buttons to preserve text contrast. */
.rm-btn-primary,
.rm-btn-primary:hover,
.rm-segmented button.on {
+1 -1
View File
@@ -40,7 +40,7 @@ func TestThemeBoundaries(t *testing.T) {
if !strings.Contains(theme["css"], "#faf6ed") {
t.Fatal("未读取到主题 CSS")
}
// CSS 符号链接不能读取主题目录之外的应用数据。
// CSS symlinks must not expose application data outside the theme directory.
if err := os.Remove(filepath.Join(dir, "theme.css")); err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -19,7 +19,7 @@ func (s *Server) listTokens(w http.ResponseWriter, r *http.Request) {
writeJSON(w, 200, map[string]any{"tokens": tokens})
}
// createToken 返回的 token 明文只出现这一次,服务器只保存哈希
// createToken returns the plaintext token only once; the server stores only its hash.
func (s *Server) createToken(w http.ResponseWriter, r *http.Request) {
var body struct{ Name, Scope string }
if !readJSON(w, r, &body) {
+3 -3
View File
@@ -10,7 +10,7 @@ import (
"time"
)
// JSON/JavaScript 共同精确表示的非负整数范围;不乘天数,避免期限溢出。
// Nonnegative integers exactly representable in both JSON and JavaScript; avoid multiplying by days, which could overflow.
const maxRetentionDays = 1<<53 - 1
func trashRetentionDays(settings map[string]json.RawMessage) (int64, error) {
@@ -40,7 +40,7 @@ func (s *Server) trashClear(w http.ResponseWriter, r *http.Request) {
func (s *Server) cleanupTrash(now time.Time) error {
s.activity.RLock()
defer s.activity.RUnlock()
// 与设置写入串行,避免在“永不删除”已保存后使用旧期限开始清理。
// Serialize with settings writes so cleanup cannot start with stale retention after "never delete" is saved.
s.settingsMu.Lock()
defer s.settingsMu.Unlock()
settings, err := s.readSettings()
@@ -64,7 +64,7 @@ func (s *Server) cleanupTrash(now time.Time) error {
return nil
}
// RunTrashCleanup 启动时执行一次,其后每小时执行;不依赖浏览器在线。
// RunTrashCleanup runs at startup and hourly thereafter, independently of browser connections.
func (s *Server) RunTrashCleanup(ctx context.Context) {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()