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 SymlinkFS 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 |
Thank you for considering Lstat even though I arrived late to the proposal process :) |
Based on the discussion above, this proposal seems like a likely accept. |
Can someone clarify for me why the proposed readlink function would modify the symlinks at all (or rather, why that's mandated)? In the filesystem, symlinks are mostly effectively just special files, and dangling symlinks are a valid use case, for example. As a more concrete example, I've got an implementation of FS for a Git repo for which symlinks have been a pain, but an interface that puts extra constraints on what I can return about symlinks doesn't really resolve the problem for me (symlinks might point outside the repo, to non-existent files, etc, and those are all valid and often useful to the consumer; in those cases, Lstat would succeed but Stat would error and that's reasonable and matches the behavior on a normal system). |
@tianon where do you see that modification being documented or implemented? |
I guess I'm probably just misunderstanding the discussions here around the use of Rel and Clean (and absolute path restrictions) and now I realize those are probably specific to a particular implementation of the interface 🤦 apologies! |
No change in consensus, so accepted. 🎉 |
The CL is ready for review with the new changes. |
@zombiezen Is that https://go.dev/cl/385534 ? It seems to have picked up a merge conflict. |
Yes, that's the one. I'll go fix that. |
I apologize for bikeshedding now, but should we declare two interfaces instead of one? type ReadLinkFS interface {
ReadLink(name string) (string, error)
}
type StatLinkFS interface {
StatLink(name string) (FileInfo, error)
} Arguments for this:
|
My thinking was to have both in the same interface because, on the reading side, I can't think why an implementation would be able to provide one method but not the other. They implement two sides of the same feature, so I wouldn't split them up unless we have good reason to. Your arguments about following precedent are interesting. I can't say I disagree with them, but I'm also not terribly concerned if we aren't consistent. Speaking of late bikeshedding... I just noticed that the accepted design at #49580 (comment) uses the names |
I assumed yes since there was a light precedent set forth by |
The relevance of my concern arises because I'm working on a package that extends |
@ianlancetaylor @bcmills With the release freeze coming, what's left to do for this CL? IMO this is ready to go, barring any changes based on @dsnet's comments. |
Looking forward to SymlinkFS support; I have had a case recently writing a routine to copy some contents from an io/fs.FS to another format, and due to the lack of symlink support, needed to port it to a different filesystem abstraction. |
I was chatting with @mvdan and I was emphasizing that my main concern was point 2 in my earlier comment (which I should have listed first):
To elaborate further, this arises as I was working on a project that needed writable virtual FS interfaces. While working on that, I had a clone of the "io/fs" API but with additional interfaces to support writing. One of my declarations was @mvdan and I propose: type ReadLinkFS interface {
ReadLink(name string) (string, error)
StatLink(name string) (FileInfo, error)
} which:
|
@dsnet LGTM. I can make that change to the CL. |
@ianlancetaylor @rsc #49580 (comment) tweaks the names of the interface of one of the methods compared to the accepted version as of #49580 (comment); does that sound good? It's unclear to me if we want to re-open the proposal as a "likely accept" for a week or two to see if anyone disagrees. In any case I'd love to have final agreement in the next week or two so that we can merge the updated CL before the 1.23 freeze. |
Another point in favor of calling this new interface |
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: