siyuan-note/siyuan · warning

remove empty plugin storage directory [%s]: %w

Error message

remove empty plugin storage directory [%s]: %w

What it means

cleanupEmptyPluginStorageDirs walks <workspace>/data/storage/petals, and for directories containing neither files nor subdirectories calls removeEmptyDirectoryTree. Any removal failure (permissions, busy, symlink/IO errors) is collected as 'remove empty plugin storage directory [%s]: %w' and returned joined via errors.Join. Cleanup continues past failures, so the error reports one or more aggregated failures, not a fatal abort.

Source

Thrown at kernel/model/plugin_storage.go:95

		if infoErr != nil {
			cleanupErrors = append(cleanupErrors, fmt.Errorf("inspect plugin storage directory [%s]: %w", entry.Name(), infoErr))
			continue
		}
		if !isDir || !bazaar.IsValidPackageName(entry.Name()) {
			continue
		}

		dirPath := filepath.Join(storageRoot, entry.Name())
		hasFile, readErr := containsFile(dirPath)
		if readErr != nil {
			cleanupErrors = append(cleanupErrors, fmt.Errorf("inspect plugin storage directory [%s]: %w", dirPath, readErr))
			continue
		}
		if hasFile {
			continue
		}
		if _, removeErr := removeEmptyDirectoryTree(dirPath); removeErr != nil {
			cleanupErrors = append(cleanupErrors, fmt.Errorf("remove empty plugin storage directory [%s]: %w", dirPath, removeErr))
		}
	}
	return errors.Join(cleanupErrors...)
}

func isRegularDirectoryEntry(entry os.DirEntry) (bool, error) {
	if entry.Type()&os.ModeSymlink != 0 {
		return false, nil
	}
	info, err := entry.Info()
	if err != nil {
		return false, err
	}
	return info.IsDir() && info.Mode()&os.ModeSymlink == 0, nil
}

func removeEmptyDirectoryTree(dirPath string) (removed bool, err error) {
	entries, err := os.ReadDir(dirPath)

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Re-run cleanup after closing other SiYuan instances/plugins that may hold handles to the directory
  2. Fix permissions on data/storage/petals subdirectories so the kernel can delete them
  3. Inspect the wrapped %w error to identify the specific failing path and remove it manually
  4. Move cleanup to startup/shutdown windows when plugins are inactive

Example fix

// before: treat joined cleanup errors as fatal
if err := model.CleanupEmptyPluginStorageDirs(); err != nil { return err }
// after: log aggregated failures and continue
if errs := model.CleanupEmptyPluginStorageDirs(); errs != nil {
    logging.LogWarnf("plugin storage cleanup incomplete: %s", errs)
}
Defensive patterns

Strategy: try-catch

Validate before calling

const storageRoot = path.join(workspaceDir, "data", "storage", "petals");
try { fs.accessSync(storageRoot, fs.constants.W_OK); } catch (e) {
  console.warn("plugin storage root not writable, cleanup may fail:", e.message);
}

Try / catch

try {
  await fetchPost("/api/petal/cleanupEmptyPluginStorageDirs", {});
} catch (e) {
  logging.LogWarn("some empty plugin storage dirs could not be removed: " + e);
  // non-fatal: retry later when no plugins are active
}

Prevention

When it happens

Trigger: Calling CleanupEmptyPluginStorageDirs when an empty plugin storage directory cannot be deleted — the directory was made read-only, a file appeared concurrently inside it, the OS reports it busy (open handle on Windows), or an unreadable/locked entry confuses the tree removal.

Common situations: Another running SiYuan instance or a plugin still writing into the directory during cleanup; Windows Explorer/AV holding a handle; permission drift after copying the workspace; removing dirs while the plugin is active.

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/5a7f7ad3adab928d. Report an issue: GitHub.