afero
afero copied to clipboard
ReadDir is not setting FileInfo up correctly for symlinks
TLDR: When one calls IsDir()
on a FileInfo returned back from afero.ReadDir
, and that FileInfo is for a symlink to another directory, the IsDir()
function returns false
. When one gets a FileInfo from os.Stat
for the same symlink, IsDir()
returns true
.
Example Dir:
$ ll /tmp/foo
total 8
drwxr-xr-x 5 anand wheel 160B Feb 2 16:48 .
drwxrwxrwt 13 root wheel 416B Feb 2 17:28 ..
drwxr-xr-x 2 anand wheel 64B Feb 2 16:47 bar
lrwxr-xr-x 1 anand wheel 3B Feb 2 16:47 baz -> bar
-rw-r--r-- 1 anand wheel 731B Feb 2 17:05 main.go
Example code (from main.go
):
package main
import (
"fmt"
"os"
"github.com/spf13/afero"
)
func main() {
fmt.Println("os: bar is a dir:", isDir("bar"))
fmt.Println("os: baz is a dir:", isDir("baz"))
fs := afero.NewOsFs()
fmt.Println("afero: bar is a dir:", isDirAfero(fs, "bar"))
fmt.Println("afero: baz is a dir:", isDirAfero(fs, "baz"))
infos, err := afero.ReadDir(fs, "/tmp/foo")
if err != nil {
panic(err)
}
for _, info := range infos {
fmt.Println("afero.ReadDir", info.Name(), "is a dir:", info.IsDir())
}
}
func isDir(p string) bool {
i, err := os.Stat(p)
if err != nil {
panic(err)
}
return i.IsDir()
}
func isDirAfero(fs afero.Fs, p string) bool {
i, err := fs.Stat(p)
if err != nil {
panic(err)
}
return i.IsDir()
}
Output of main.go
:
$ cd /tmp/foo
$ go run main.go
os: bar is a dir: true
os: baz is a dir: true
afero: bar is a dir: true
afero: baz is a dir: true
afero.ReadDir bar is a dir: true
afero.ReadDir baz is a dir: false
afero.ReadDir main.go is a dir: false
As we can see the following line from the output was unexpected:
afero.ReadDir baz is a dir: false
What I expected
I expected Info.IsDir()
to return true for baz
.
This doesn't seem to be a library problem, this is due to the functioning of de 'os'.
infos, _ := os.ReadDir("/tmp/foo")
for _, info := range infos {
fmt.Println("", info.Name(), "is a dir:", info.IsDir())
}
the output is:
bar is a dir: true
baz is a dir: false
The error seems to be caused by using a different function to fill in the stat data: os.ReadDir, will call Lstat and not Stat function, in their subdirectories.
go/src/os/dir_unix.go
You can check this by looking for the function func (f *File) readdir
and see that the stats are filled by calling
info, err := lstat(f.name + "/" + string(name))
This has an impact on afero.Walk
as it relies on IsDir()
. If you only care about following the symlink of the root directory and you're using OsFs
, then using EvalSymlinks is a viable workaround. This, however, does not help my case as I must support different filesystems.