{"record":{"id":"5a7f7ad3adab928d","repo":"siyuan-note/siyuan","slug":"remove-empty-plugin-storage-directory-s-w","errorCode":null,"errorMessage":"remove empty plugin storage directory [%s]: %w","messagePattern":"remove empty plugin storage directory \\[(.+?)\\]: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/plugin_storage.go","lineNumber":95,"sourceCode":"\t\tif infoErr != nil {\n\t\t\tcleanupErrors = append(cleanupErrors, fmt.Errorf(\"inspect plugin storage directory [%s]: %w\", entry.Name(), infoErr))\n\t\t\tcontinue\n\t\t}\n\t\tif !isDir || !bazaar.IsValidPackageName(entry.Name()) {\n\t\t\tcontinue\n\t\t}\n\n\t\tdirPath := filepath.Join(storageRoot, entry.Name())\n\t\thasFile, readErr := containsFile(dirPath)\n\t\tif readErr != nil {\n\t\t\tcleanupErrors = append(cleanupErrors, fmt.Errorf(\"inspect plugin storage directory [%s]: %w\", dirPath, readErr))\n\t\t\tcontinue\n\t\t}\n\t\tif hasFile {\n\t\t\tcontinue\n\t\t}\n\t\tif _, removeErr := removeEmptyDirectoryTree(dirPath); removeErr != nil {\n\t\t\tcleanupErrors = append(cleanupErrors, fmt.Errorf(\"remove empty plugin storage directory [%s]: %w\", dirPath, removeErr))\n\t\t}\n\t}\n\treturn errors.Join(cleanupErrors...)\n}\n\nfunc isRegularDirectoryEntry(entry os.DirEntry) (bool, error) {\n\tif entry.Type()&os.ModeSymlink != 0 {\n\t\treturn false, nil\n\t}\n\tinfo, err := entry.Info()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn info.IsDir() && info.Mode()&os.ModeSymlink == 0, nil\n}\n\nfunc removeEmptyDirectoryTree(dirPath string) (removed bool, err error) {\n\tentries, err := os.ReadDir(dirPath)","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/plugin_storage.go#L77-L113","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-run cleanup after closing other SiYuan instances/plugins that may hold handles to the directory","Fix permissions on data/storage/petals subdirectories so the kernel can delete them","Inspect the wrapped %w error to identify the specific failing path and remove it manually","Move cleanup to startup/shutdown windows when plugins are inactive"],"exampleFix":"// before: treat joined cleanup errors as fatal\nif err := model.CleanupEmptyPluginStorageDirs(); err != nil { return err }\n// after: log aggregated failures and continue\nif errs := model.CleanupEmptyPluginStorageDirs(); errs != nil {\n    logging.LogWarnf(\"plugin storage cleanup incomplete: %s\", errs)\n}","handlingStrategy":"try-catch","validationCode":"const storageRoot = path.join(workspaceDir, \"data\", \"storage\", \"petals\");\ntry { fs.accessSync(storageRoot, fs.constants.W_OK); } catch (e) {\n  console.warn(\"plugin storage root not writable, cleanup may fail:\", e.message);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await fetchPost(\"/api/petal/cleanupEmptyPluginStorageDirs\", {});\n} catch (e) {\n  logging.LogWarn(\"some empty plugin storage dirs could not be removed: \" + e);\n  // non-fatal: retry later when no plugins are active\n}","preventionTips":["Run cleanup at kernel startup/shutdown, not while plugins are active","Keep data/storage/petals subdirectories writable by the kernel user","Close other SiYuan instances sharing the workspace before cleanup","Treat joined cleanup errors as warnings and inspect the wrapped per-path causes"],"tags":["go","filesystem","cleanup","plugin-storage"],"backgroundTag":"file-write-failed","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}