siyuan-note/siyuan · error
Access to encrypted notebook data is not supported via this…
Error message
Access to encrypted notebook data is not supported via this API
What it means
prepareFileAssets validates paths requested through the file API and calls rejectEncryptedBoxPath for each matched asset path. If the requested file or a sub-path belongs to an encrypted notebook's data, the API refuses access with the localized message 321: encrypted notebook data must not be read or written through generic file APIs. This preserves the encryption boundary: plaintext access only through unlock-aware code paths.
Solutions
- Restrict the requested path to non-encrypted notebooks; check the target box's encryption state first.
- Use the encrypted-notebook-aware APIs (with unlock flow, e.g. copyDecryptedAsset) for assets in encrypted notebooks.
- Filter the file list so no entry resolves under an encrypted box's assets directory before calling the API.
- Catch message 321 in the UI and explain that encrypted notebook files need the unlock flow.
Example fix
// before
files := [{"path": "/notebook-enc/assets/a.png"}]
await fetchPost("/api/file/copyFiles", {files})
// after
if rejectEncryptedBoxPath(assetPath) {
await copyDecryptedAsset(srcPath, dest) // unlock-aware path
} else {
await fetchPost("/api/file/copyFiles", {files})
} Defensive patterns
Strategy: validation
Validate before calling
if (files.some(f => rejectEncryptedBoxPath(join(dataDir, f.path)))) throw new Error("use unlock-aware API for encrypted notebooks") Type guard
function isEncryptedBoxAsset(p, encryptedBoxIDs) {
const rel = require("path").relative(dataDir, p)
const box = rel.split(require("path").sep)[0]
return encryptedBoxIDs.includes(box) && rel.split(require("path").sep)[1] === "assets"
} Try / catch
try { await fetchPost("/api/file/copyFiles", {files}) }
catch (e) { if (e.msg.includes("encrypted")) notify("Encrypted notebook data needs the unlock flow") else throw e } Prevention
- Filter file lists against encrypted box ids before generic file APIs
- Route encrypted-notebook assets through dedicated unlock-aware endpoints
- Keep an up-to-date set of encrypted box ids client-side
When it happens
Trigger: Calling globalCopyFiles / file download APIs whose absPath equals or is a parent of an asset path inside an encrypted notebook (data/<encryptedBoxID>/assets/...).
Common situations: Scripts bulk-copy files from data/ without knowing which notebooks are encrypted; sync/backup tools targeting the whole data directory; plugins using the generic file API on an encrypted notebook's assets.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- cannot swap blocks across encrypted notebook boundaries
- Conf.Language(314)
- Conf.Language(314)
- encrypted notebook is not accessible
- path belongs to encrypted notebook
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/d63b72ddc552cc45.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/api/file.go:128
})
// prepareFileAssets 在原始文件 API 完成权限校验后补齐目录或文件的资源内容。
func prepareFileAssets(absPath string) error {
absPath = filepath.Clean(absPath)
dataPath := filepath.Clean(util.DataDir)
if gulu.File.IsSubPath(absPath, dataPath) {
absPath = dataPath
} else if absPath != dataPath && !gulu.File.IsSubPath(dataPath, absPath) {
return nil
}
files, err := model.DeferredSyncAssets()
if err != nil {
return err
}
for _, file := range files {
assetPath := filepath.Join(util.DataDir, filepath.FromSlash(strings.TrimPrefix(file.Path, "/")))
if (absPath == assetPath || gulu.File.IsSubPath(absPath, assetPath)) && rejectEncryptedBoxPath(assetPath) {
return fmt.Errorf("%s", model.Conf.Language(321))
}
}
return model.EnsureAssetPrefixLocal(absPath)
}
var globalCopyFiles = contractHandler(apicontract.GlobalCopyFiles, func(c *gin.Context, request apicontract.CopyFilesRequest) apicontract.Response[apicontract.Null] {
ret := gulu.Ret.NewResult()
var changedPaths []string
defer func() {
model.IncSyncIfNeeded(changedPaths...)
}()
srcs, destDirArg := request.Srcs, request.DestDir
for i, src := range srcs {
if !filepath.IsAbs(src) {
logging.LogErrorf("global copy files src [%s] is not an absolute path", src)
ret.Code = -1
ret.Msg = "Field [srcs]: each path must be absolute"View on GitHub (pinned to 9f775e8a12)