{"record":{"id":"67aa783d20f8acdb","repo":"juicedata/juicefs","slug":"inode-d-is-not-a-directory","errorCode":null,"errorMessage":"inode %d is not a directory","messagePattern":"inode (.+?) is not a directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/meta/base.go","lineNumber":2367,"sourceCode":"func (m *baseMeta) GetPaths(ctx Context, inode Ino) []string {\n\tif inode == RootInode {\n\t\treturn []string{\"/\"}\n\t}\n\n\tif inode == TrashInode {\n\t\treturn []string{\"/.trash\"}\n\t}\n\n\toutside := \"path not shown because it's outside of the mounted root\"\n\tgetDirPath := func(ino Ino) (string, error) {\n\t\tvar names []string\n\t\tvar attr Attr\n\t\tfor ino != RootInode && ino != m.root {\n\t\t\tif st := m.en.doGetAttr(ctx, ino, &attr); st != 0 {\n\t\t\t\treturn \"\", fmt.Errorf(\"getattr inode %d: %s\", ino, st)\n\t\t\t}\n\t\t\tif attr.Typ != TypeDirectory {\n\t\t\t\treturn \"\", fmt.Errorf(\"inode %d is not a directory\", ino)\n\t\t\t}\n\t\t\tvar entries []*Entry\n\t\t\tif st := m.en.doReaddir(ctx, attr.Parent, 0, &entries, -1); st != 0 {\n\t\t\t\treturn \"\", fmt.Errorf(\"readdir inode %d: %s\", ino, st)\n\t\t\t}\n\t\t\tvar name string\n\t\t\tfor _, e := range entries {\n\t\t\t\tif e.Inode == ino {\n\t\t\t\t\tname = string(e.Name)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif attr.Parent == RootInode && ino == TrashInode {\n\t\t\t\tname = TrashName\n\t\t\t}\n\t\t\tif name == \"\" {\n\t\t\t\treturn \"\", fmt.Errorf(\"entry %d/%d not found\", attr.Parent, ino)\n\t\t\t}","sourceCodeStart":2349,"sourceCodeEnd":2385,"githubUrl":"https://github.com/juicedata/juicefs/blob/c9a67b23e8e08ec23ec331aa6f1675e2319e921c/pkg/meta/base.go#L2349-L2385","documentation":"Thrown while building an absolute path from an inode (path resolution walking up via attr.Parent): the inode exists but its type is not a directory, so it cannot be treated as a directory entry during the parent walk. The metadata engine enforces the invariant that every node in the ancestor chain of a path must be a directory.","triggerScenarios":"Calling path-building/resolution APIs (e.g. GetPath / getPath via doGetAttr+doReaddir walk) with an inode whose Attr.Typ is not TypeDirectory; typically happens when a stale or corrupted inode is passed, or when the inode was concurrently removed and recreated as a file.","commonSituations":"Applications holding cached inode handles across a delete+recreate where the new inode is a regular file; operating on the trash or subdirectory-mount root with stale inodes; metadata corruption or mismatched metadata backend state.","solutions":["Refresh the inode: re-resolve the path from the current directory tree instead of using a stale cached inode","Check the inode type first (getattr) and only pass directory inodes to path resolution","If it occurs without stale handles, run fsck / verify the metadata engine for corruption"],"exampleFix":"// before\nentries, err := fs.ReadDir(ino) // ino from stale cache\n// after\nattr, err := fs.GetAttr(ino)\nif err == nil && attr.Typ == TypeDirectory {\n    entries, err = fs.ReadDir(ino)\n}","handlingStrategy":"validation","validationCode":"attr, st := meta.GetAttr(ctx, ino)\nif st != 0 { return st }\nif attr.Typ != meta.TypeDirectory { return errors.New(\"not a directory\") }\n// proceed with path resolution","typeGuard":"func isDir(attr meta.Attr) bool { return attr.Typ == meta.TypeDirectory }","tryCatchPattern":null,"preventionTips":["Never cache inodes across unlink/rename operations","Re-stat the inode before path-based operations after long-lived handles","Run fsck periodically to catch dentry/parent inconsistencies"],"tags":["metadata","inode","not-a-directory","filesystem"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"c9a67b23e8e08ec23ec331aa6f1675e2319e921c","analyzedAt":"2026-09-06T17:55:48.476Z","contentChangedAt":"2026-09-06T17:55:48.476Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}