siyuan-note/siyuan · error

path belongs to encrypted notebook [%s]

Error message

path belongs to encrypted notebook [%s]

What it means

Thrown by rejectEncryptedArchivePath (kernel/api/archive.go), a security guard on the archive/zip API. Before packaging anything, the kernel maps the absolute path to a notebook id with model.ExtractBoxIDFromAssetsPath and checks model.IsEncryptedBox. If the path sits inside an encrypted notebook, the zip request is refused outright because the archive API is not encryption-aware and would copy ciphertext out of the protected store.

Source

Thrown at kernel/api/archive.go:35

package api

import (
	"fmt"
	"net/http"
	"path/filepath"

	"github.com/88250/gulu"
	"github.com/gin-gonic/gin"
	"github.com/siyuan-note/logging"
	"github.com/siyuan-note/siyuan/kernel/model"
	"github.com/siyuan-note/siyuan/kernel/util"
)

// rejectEncryptedArchivePath 检查路径是否落入加密笔记本目录(含 symlink 绕过),是则返回错误。
func rejectEncryptedArchivePath(absPath string) error {
	boxID := model.ExtractBoxIDFromAssetsPath(absPath)
	if boxID != "" && model.IsEncryptedBox(boxID) {
		return fmt.Errorf("path belongs to encrypted notebook [%s]", boxID)
	}
	if resolved := util.ResolveLongestExistingParent(absPath); resolved != absPath {
		boxID = model.ExtractBoxIDFromAssetsPath(resolved)
		if boxID != "" && model.IsEncryptedBox(boxID) {
			return fmt.Errorf("symlink resolves into encrypted notebook [%s]", boxID)
		}
	}
	return nil
}

func zip(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	arg, ok := util.JsonArg(c, ret)
	if !ok {
		return
	}

View on GitHub (pinned to afa823b6b4)

Solutions

  1. Export the encrypted notebook through the dedicated export APIs, which acquire the box lease and decrypt content properly (they route through holdEncryptedBoxRequest)
  2. Move or keep the files you need to archive in a non-encrypted notebook
  3. Scope the zip path to exclude the encrypted notebook's directory and archive the remaining notebooks
  4. Do not attempt to zip encrypted notebook paths directly; the guard is intentional and has no override flag
Defensive patterns

Strategy: validation

Validate before calling

function isEncryptedBoxId(boxId: string, notebooks: { id: string; encrypted: boolean }[]): boolean {
  return notebooks.some(n => n.id === boxId && n.encrypted);
}
// before archiving a path:
const box = boxIdFromAssetsPath(absPath);
const nb = await fetchGet('/api/notebook/lsNotebooks');
if (box && isEncryptedBoxId(box, nb.data.notebooks)) {
  throw new Error('skip encrypted notebook in archive job');
}

Type guard

const isEncryptedNotebookPath = (absPath: string, encryptedBoxIds: Set<string>): boolean => {
  const m = absPath.match(/data\/([0-9]{14}[^\/]*)\/assets\//); // box id from assets path
  return !!m && encryptedBoxIds.has(m[1]);
};

Try / catch

if (res.code === -1 && res.msg.startsWith('path belongs to encrypted notebook')) {
  // exclude the encrypted notebook's subtree from the archive scope and re-run
}

Prevention

When it happens

Trigger: Calling the archive zip endpoint with a path under an encrypted notebook's directory (its assets or notebook folder), regardless of whether the notebook is currently unlocked; scripting a bulk zip of the whole workspace while at least one encrypted notebook exists under the tree.

Common situations: Encrypted-notebook feature enabled and an export/backup tool or plugin zips workspace paths; automated backup scripts that walk data/ and hit the encrypted notebook dir; users trying to hand-archive an encrypted notebook via the generic archive API instead of the dedicated export flow.

Related errors


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/d21750adeba9cd22. Report an issue: GitHub.