siyuan-note/siyuan · error

stat encrypted notebook history conf [%s] failed: %w

Error message

stat encrypted notebook history conf [%s] failed: %w

What it means

After the crypto-backup marker is absent, isEncryptedHistoryBoxDir falls back to reading .siyuan/conf.json and checking BoxConf.Encrypted. A failed os.Stat on conf.json that is not NotExist is wrapped as 'stat encrypted notebook history conf [%s] failed'. It is thrown because the unencrypted-marker fallback path must fail loudly rather than classify an encrypted box as unencrypted.

Source

Thrown at kernel/model/crypto.go:946

}

// isEncryptedHistoryBoxDir 判断历史目录中的 boxID 子目录是否属于加密笔记本。
// 优先看 notebook-crypto-backup.json(删除前随 box 目录整体备份,是加密身份的权威标识),
// 再 fallback 到 conf.json 的 Encrypted 标志。
func isEncryptedHistoryBoxDir(boxDir string) (bool, error) {
	siyuanDir := filepath.Join(boxDir, ".siyuan")
	backupPath := filepath.Join(siyuanDir, "notebook-crypto-backup.json")
	if _, err := os.Stat(backupPath); err == nil {
		return true, nil
	} else if !os.IsNotExist(err) {
		return false, fmt.Errorf("stat encrypted notebook history backup [%s] failed: %w", boxDir, err)
	}
	confPath := filepath.Join(siyuanDir, "conf.json")
	if _, err := os.Stat(confPath); err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, fmt.Errorf("stat encrypted notebook history conf [%s] failed: %w", boxDir, err)
	}
	data, err := filelock.ReadFile(confPath)
	if err != nil {
		return false, fmt.Errorf("read encrypted notebook history conf [%s] failed: %w", boxDir, err)
	}
	var boxConf conf.BoxConf
	if err = gulu.JSON.UnmarshalJSON(data, &boxConf); err != nil {
		return false, fmt.Errorf("parse encrypted notebook history conf [%s] failed: %w", boxDir, err)
	}
	return boxConf.Encrypted, nil
}

// cachedDEKs 缓存已解锁加密笔记本的 DEK,按 boxID 索引。
// KEK 不全局缓存("严格每笔记本单独解锁"语义):UnlockBox 临时派生 KEK 解出 DEK 后即丢弃 KEK,
// 仅保留 per-box DEK 供后续读写加解密。
var (
	cachedDEKs     = map[string][]byte{}
	cachedDEKsLock sync.RWMutex

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Inspect the wrapped OS error and repair permissions on <boxDir>/.siyuan/conf.json
  2. Confirm conf.json is a regular readable file; replace it from backup or the live notebook's .siyuan/conf.json if it is corrupt or a special file
  3. Check for sync-conflict artifacts and resolve them
  4. Re-run the history scan once the file is stat-able

Example fix

// before: conf.json is a zero-byte sync conflict stub
mv data/history/<snap>/<box>/.siyuan/conf.json conf.json.conflict
// after: restore from the live notebook
cp data/notebooks/<boxID>/.siyuan/conf.json data/history/<snap>/<box>/.siyuan/conf.json
Defensive patterns

Strategy: type-guard

Validate before calling

fi, err := os.Stat(filepath.Join(boxDir, ".siyuan", "conf.json"))
if err != nil && !os.IsNotExist(err) { return err }
if err == nil && !fi.Mode().IsRegular() { return fmt.Errorf("conf.json is not a regular file") }

Type guard

func confFileRegular(boxDir string) bool {
    fi, err := os.Stat(filepath.Join(boxDir, ".siyuan", "conf.json"))
    return err == nil && fi.Mode().IsRegular()
}

Try / catch

enc, err := isEncryptedHistoryBoxDir(boxDir)
if err != nil {
    log.Printf("cannot classify history box %s: %v", boxDir, err)
    return err // abort scan; do not classify as unencrypted
}

Prevention

When it happens

Trigger: Callers (encryptedNotebookHistoryBoxDirs, hasEncryptedNotebookDeleteHistory, IsEncryptedHistoryPath) classifying a box dir where .siyuan/conf.json exists but cannot be stat'ed: permission denied, .siyuan or conf.json replaced by a non-directory, dangling state after partial restore, or I/O errors on network storage.

Common situations: Restored snapshots with mismatched ownership/ACLs; sync clients (e.g. Syncthing) leaving conflict files like conf.json (sync-conflict); antivirus quarantine touching conf.json on Windows.

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


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/738f3748fde1739f. Report an issue: GitHub.