siyuan-note/siyuan · critical

write notebook crypt backup failed

Error message

write notebook crypt backup failed: %w

What it means

The final step of writeNotebookCryptBackup writes the marshaled JSON to the backup path via filelock.WriteFile, wrapping any failure with this message. The backup file holds the notebook's key-envelope material, so a failure here means the encrypted notebook's recovery backup was not persisted.

Solutions

  1. Inspect the wrapped error and fix the underlying write failure (permissions, disk space, lock contention)
  2. Ensure only one SiYuan instance uses the workspace at a time
  3. Verify the backup directory exists, is writable, and that backupPath is not a directory
  4. Retry the operation after resolving the I/O issue — the notebook encryption change should not be considered complete without this backup

Example fix

// before
err := model.ChangeMasterPassword(box, old, new) // fails: write notebook crypt backup failed
// after
# free disk space / fix permissions, ensure single instance, then:
err := model.ChangeMasterPassword(box, old, new)
# verify the backup file exists under the notebook crypt backup path
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check backup dir is writable
info, err := os.Stat(backupDir)
if err != nil || !info.IsDir() { return fmt.Errorf("backup dir missing") }
if err := os.WriteFile(filepath.Join(backupDir, ".probe"), nil, 0644); err != nil {
    return fmt.Errorf("backup dir not writable: %w", err)
}
os.Remove(filepath.Join(backupDir, ".probe"))

Try / catch

if err := model.ChangeMasterPassword(box, old, new); err != nil {
    if strings.Contains(err.Error(), "write notebook crypt backup") {
        // recovery backup missing: surface loudly, do not treat as success
        return fmt.Errorf("password changed but recovery backup not written: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: filelock.WriteFile(backupPath, data) fails during CreateEncryptedBox or ChangeMasterPassword — read-only directory, disk full, the backup path locked by another process holding the filelock, antivirus interference, or the target existing as a directory.

Common situations: Docker volume mounted read-only; two SiYuan instances sharing one workspace contending on the file lock; disk quota exceeded; enterprise antivirus blocking writes to newly created files.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


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

Appendix: source

Thrown at kernel/model/crypto.go:2536

// writeNotebookCryptBackup 写入加密笔记本的 BoxCrypt 备份。
// 仅在 Encrypted=true 的笔记本上调用,配合 CreateEncryptedBox / ChangeMasterPassword 写入。
func writeNotebookCryptBackup(boxID string, crypt *conf.BoxEncryption) error {
	if !ast.IsNodeIDPattern(boxID) {
		return errors.New("invalid notebook ID")
	}
	if err := validateBoxEncryption(crypt); err != nil {
		return err
	}
	backupPath := notebookCryptoBackupPath(boxID)
	if err := os.MkdirAll(filepath.Dir(backupPath), 0755); err != nil {
		return fmt.Errorf("mkdir notebook crypt backup dir failed: %w", err)
	}
	data, err := gulu.JSON.MarshalIndentJSON(crypt, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal notebook crypt backup failed: %w", err)
	}
	if err := filelock.WriteFile(backupPath, data); err != nil {
		return fmt.Errorf("write notebook crypt backup failed: %w", err)
	}
	return nil
}

// readNotebookCryptBackup 读取加密笔记本的 BoxCrypt 备份。
// 备份文件不存在时返回 (nil, nil),调用方据此区分"非加密笔记本"和"备份不存在"。
func readNotebookCryptBackup(boxID string) (*conf.BoxEncryption, error) {
	if !ast.IsNodeIDPattern(boxID) {
		return nil, errors.New("invalid notebook ID")
	}
	backupPath := notebookCryptoBackupPath(boxID)
	if !filelock.IsExist(backupPath) {
		return nil, nil
	}
	return readBoxEncryptionFile(backupPath)
}

func readBoxEncryptionFile(backupPath string) (*conf.BoxEncryption, error) {

View on GitHub (pinned to 8641553a1f)