-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
io/fs: add ReadLinkFS interface #49580
Comments
I would say it should be slash-separated and also relative to the same |
Rewriting the link is tricky and not rewriting the link is also tricky. It's unclear to me what we should do here, if anything. |
This proposal has been added to the active column of the proposals project |
As a data point, for the application I was writing, rewriting the link to be relative is effectively what I did anyway. I wanted to create a zip archive of an on-disk directory, so absolute paths were rewritten to be relative to the |
So the API for ReadLink is that it must return a path relative to the link, and if it can't do that, then it returns an error? I'm trying to understand how useful this will be in practice, to balance against the cost. Will it be useful in practice? Or will people just be frustrated that 99% of symlinks aren't usable? |
Yes.
I would expect the So, for example, os.DirFS("/") on Unix would be able to resolve symlinks anywhere on the filesystem, even if those symlinks are absolute. |
Does this extend to non local-disk filesystems? |
@rsc:
That was sufficient for my application. The root of my
I'm not 100% convinced of that, but I don't have evidence to dispute your claim. FWIW the cases that I was concerned with in a DevTools context:
Agreed, I think weighing this tradeoff is the trickiest part of this proposal. Relative links are IMO the most useful for a consumer of this API. I could imagine an implementation of You're probably already considering this, but it's just
I'm proposing the slash-separated relative path restriction would extend to non-local-disk filesystems, yes. How each implementation meets this contract is up to the individual filesystem. |
@zombiezen, thanks for the use cases. It seems fairly unobtrusive to add ReadLinkFS and fs.ReadLink, so the cost seems low and the benefit > 0.
Rewriting symlinks may run into problems. I've been burned enough that I'm a bit wary about that. Should we start with just erroring out on the absolute ones? |
I think it would be fine to start by erroring out on absolute links, and perhaps define a specific error (or error type) for symlinks that refer to locations outside of the From the perspective of |
OK, so it sounds like we agree on ReadLink and ReadLinkFS but with the restriction that the returned link must be relative, and absolute symlinks return errors instead. Do I have that right? |
IIUC the full constraint for the returned link is relative and underneath the root of |
Based on the discussion above, this proposal seems like a likely accept. |
No change in consensus, so accepted. 🎉 |
Excellent! I'm working on a CL now. Implementation question: I would very much like to be able to use |
In principle you could move the functions into an internal package imported by both the os package and the path/filepath package. But try to avoid that if you can. Better to keep the os package focused on the simplest possible approach. In particular I don't see why the os package would need to use |
Blocked by: golang/go#49580
Change https://go.dev/cl/385534 mentions this issue: |
I have use-cases which require the ability to have an absolute path returned, namely, taking an fs.FS and taring it up (tons of symlinks to |
Sorry for being late to the party, I've just returned from a long paternity leave. My only concern is the interface methods docs, IMO they are too Unix specific. Windows supports other types of links that are not symbolic links. For example, on Windows, My suggestion would be to remove the word |
Swapping "symbolic link" for "link" could make the docs a bit ambiguous with hard links though: https://pkg.go.dev/os#Link Does Windows use any term for ReadLink and Lstat other than "link"? The OS in general does seem to have the notion of "hard links" as well as "symbolic links", which this proposal is mainly aimed at: https://learn.microsoft.com/en-us/windows/win32/fileio/hard-links-and-junctions |
Could we use "soft link"? It is not a well established term on Windows, but it appears in Microsoft's docs and it should work well on Unix too. We could mention symlinks, and then clarify that other types of soft links are also supported, such as Windows mount points. |
@aclements SGTM. I can draft a new version of the CL within the next week or so. |
I have a new patchset of https://go.dev/cl/385534 ready for review that incorporates the discussed changes. |
I'm happy to use the term "soft link". It's a bit tricky to document in detail because this is an interface. It might not be backed by any sort of OS file system at all, so the exact meaning is kind of left up to each implementation of the interface. I'm open to suggestions, though. |
Based on the discussion above, this proposal seems like a likely accept. The proposal is: package fs
type ReadLinkFS interface {
FS
// ReadLink returns the destination of the named soft link.
ReadLink(name string) (string, error)
// Lstat returns a FileInfo describing the file without following any soft links.
// If there is an error, it should be of type *PathError.
Lstat(name string) (FileInfo, error)
} along with toplevel func ReadLink(fsys FS, name string) (string, error)
func Lstat(fsys FS, name string) (FileInfo, error) There are no guarantees or requirements about what ReadLink returns. We can continue to hash out what the exact documentation should look like to cover both Unix and Windows. (This time for sure!) |
On Windows, can the current interface distinguish between Junction and Symlink, which are two different types? The return value of ReadLink lacks a rigorous definition. According to this wiki page from ntfs-3g, Junctions may also contain volume id. If the target volume does not exist, what should ReadLink return? A failure or NT Object Manager path beginning with |
@zhangyoufu It seems to me like the same questions apply to the documented behavior for https://pkg.go.dev/os#Readlink. Does |
@mvdan Yes. Current behavior of
Go standard library did a good job handling/testing all these cases. |
@zhangyoufu given that |
Agree. |
During my test on Windows,
Here is my test result:
To make
Test Code: test.bat@echo off
cd /D "%~dp0"
for /f "tokens=*" %%i in ('mountvol %SystemDrive% /L') do set VolumeName=%%i
REM prepare
md mountpoint
mountvol mountpoint %VolumeName%
mklink /J junction %SystemDrive%\junction-target
mklink /D symlinkd X:\symlinkd-target
mklink symlink X:\symlink-target
pause
go run main.go
pause
REM cleanup
mountvol mountpoint /D
rd mountpoint
rd junction
rd symlinkd
del symlink main.gopackage main
import (
"fmt"
"io/fs"
"os"
"reflect"
"syscall"
)
func main() {
for _, source := range []string{"mountpoint", "junction", "symlinkd", "symlink"} {
target, err := os.Readlink(source)
if err != nil {
fmt.Println(err)
continue
}
fi, err := os.Lstat(source)
if err != nil {
fmt.Println(err)
continue
}
modeSymlink := fi.Mode() & fs.ModeSymlink != 0
modeIrregular := fi.Mode() & fs.ModeIrregular != 0
fileAttributeDir := reflect.ValueOf(fi).Elem().FieldByName("FileAttributes").Uint() & syscall.FILE_ATTRIBUTE_DIRECTORY != 0
fmt.Printf("%s => %s, modeSymlink=%v, modeIrregular=%v, isDir=%v, fileAttributeDir=%v\n", source, target, modeSymlink, modeIrregular, fi.IsDir(), fileAttributeDir)
}
} |
This is the expected behavior since Go 1.23 (see #61893).
This is also the expected behavior. I don't know the exact reason to not set
I wouldn't limit
This is very related to #67002. It probably deserves a separate proposal. |
IEEE Std 1003.1 defined readlink(2), the API, as I understand that Go may not follow aforementioned spec/standard. But I would be surprised if Go define IMHO, Windows junctions (incl. mount points) are also symlinks. |
I think none of this affects the ReadLinkFS interface directly, though it certainly affects implementations. It seems like we should improve the documentation on os.Readlink, but that can be done separately. |
No change in consensus, so accepted. 🎉 The proposal is: package fs
type ReadLinkFS interface {
FS
// ReadLink returns the destination of the named soft link.
ReadLink(name string) (string, error)
// Lstat returns a FileInfo describing the file without following any soft links.
// If there is an error, it should be of type *PathError.
Lstat(name string) (FileInfo, error)
} along with toplevel func ReadLink(fsys FS, name string) (string, error)
func Lstat(fsys FS, name string) (FileInfo, error) There are no guarantees or requirements about what ReadLink returns. We can continue to hash out what the exact documentation should look like to cover both Unix and Windows. |
In preparation for golang/go#49580, this commit adds an own simplified implementation of ReadLinkFS. It adds helpers to extend a simple fstest.MapFS into a ReadLinkFS.
Code freeze is today and CL 385534 hasn't been properly reviewed yet. Any chance to put this into the fast line so it can land in Go 1.24? |
One last minute comment came in before the submit happened, which only affects |
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes
What did you do?
Walked a directory with
fs.WalkDir
and encountered a symlink that I wanted to read.What did you expect to see?
A function
fs.ReadLink
that behaves likeos.Readlink
, but operates on anfs.FS
. Design sketch:I would also want the file system returned by
os.DirFS
to have an implementation that callsos.Readlink
. IIUCarchive/zip.Reader
would probably also benefit from an implementation.An open question in my mind is whether the returned destination should be a slash-separated path or kept as-is. I think for consistency it probably should convert to a slash-separated path, but I'm not sure if this has problems on Windows.
What did you see instead?
No such API exists.
Other details
I have bandwidth to contribute an implementation of this, but I understand we're in the freeze and the earliest this could go in is Go 1.19.
This is somewhat related to #45470, but I'm not proposing changing any existing semantics, just adding a new method.
The text was updated successfully, but these errors were encountered: