siyuan-note/siyuan · error

source is not an encrypted asset

Error message

source is not an encrypted asset

What it means

copyDecryptedAsset extracts the notebook (box) ID from the source assets path via model.ExtractBoxIDFromAssetsPath and requires it to refer to an encrypted notebook. If the path does not resolve to an assets path of an encrypted box (empty box ID, or box not marked encrypted), the function refuses to decrypt-copy with this error.

Solutions

  1. Verify the source path is of the form <workspace>/data/<boxID>/assets/... for an encrypted notebook.
  2. Check model.IsEncryptedBox(model.ExtractBoxIDFromAssetsPath(src)) before calling the API and use the normal file-read path for unencrypted assets.
  3. Pass the asset path exactly as returned by kernel asset APIs rather than constructing it manually.
  4. If the notebook should be encrypted, confirm the box's encryption state/config; a recently migrated notebook may no longer be flagged encrypted.

Example fix

// before
boxID := model.ExtractBoxIDFromAssetsPath(src)
_ = copyDecryptedAsset(src, dest) // errors for plain notebooks

// after
boxID := model.ExtractBoxIDFromAssetsPath(src)
if boxID == "" || !model.IsEncryptedBox(boxID) {
    return model.EnsureAssetLocal(src) // plain asset: use normal copy path
}
err := copyDecryptedAsset(src, dest)
Defensive patterns

Strategy: validation

Validate before calling

const boxID = extractBoxIDFromAssetsPath(src)
if (!boxID || !isEncryptedBox(boxID)) usePlainCopyPath(src) else copyDecryptedAsset(src, dest)

Type guard

function isEncryptedAssetPath(p) {
  const m = p.match(/[\\/]data[\\/]([^\\/]+)[\\/]assets[\\/]/)
  return !!m && isEncryptedBox(m[1])
}

Try / catch

try { await copyDecryptedAsset(src, dest) }
catch (e) { if (e.message === "source is not an encrypted asset") await plainCopy(src, dest) else throw e }

Prevention

When it happens

Trigger: Calling the decrypt-copy API with src pointing to an asset in a plain (unencrypted) notebook, or a path that is not a valid assets path (e.g. outside data/<boxID>/assets/, or a notebook ID extracted as empty string).

Common situations: Caller passes a global /assets/ path or a hand-built path not matching data/<boxID>/assets/; the notebook was converted to unencrypted; the asset belongs to a different feature (e.g. temp or emb folder).

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/590cbef7a47e5087. Report an issue: GitHub.

Appendix: source

Thrown at kernel/api/file.go:72

// rejectEncryptedBoxPath 检查 absPath 是否落在加密笔记本目录下(含 symlink 绕过),是则返回 true。
// 原始文件 API(getFile/putFile/copyFile/renameFile/removeFile)是绕过加密层的逃生口,
// 对加密笔记本的任何文件读写都应拒绝——合法读写走专用 API(upload/getBlockKramdown 等,已加密感知),
// 避免密文泄漏给插件或明文破坏加密格式。
// 防止 symlink 绕过:找到最长已存在的父路径,解析 symlink 后拼回剩余路径,再检查是否落入加密 box。
func rejectEncryptedBoxPath(absPath string) bool {
	return model.EncryptedRawPathBoxID(absPath) != ""
}

// copyDecryptedAsset 将加密 asset 解密后复制到目标路径(dest 必须在工作区外)。
func copyDecryptedAsset(src, dest string) error {
	// 安全守卫:dest 必须在工作区外,防止解密后的明文落入工作区普通目录
	if gulu.File.IsSubPath(util.WorkspaceDir, dest) {
		return fmt.Errorf("refuse to write decrypted asset inside workspace")
	}
	boxID := model.ExtractBoxIDFromAssetsPath(src)
	if boxID == "" || !model.IsEncryptedBox(boxID) {
		return fmt.Errorf("source is not an encrypted asset")
	}
	if !model.IsBoxUnlocked(boxID) {
		return fmt.Errorf("%s", model.Conf.Language(314))
	}
	if err := model.EnsureAssetLocal(src); err != nil {
		return err
	}
	model.HoldBoxReadLock(boxID)
	defer model.ReleaseBoxReadLock(boxID)
	dek, dekErr := model.GetDEKIfUnlocked(boxID)
	if dekErr != nil {
		return dekErr
	}
	diskName := filepath.Base(src)
	data, readErr := os.ReadFile(src)
	if readErr != nil {
		return readErr
	}

View on GitHub (pinned to 9f775e8a12)