-
Notifications
You must be signed in to change notification settings - Fork 653
bug(fs): fs.exists/fs.existsSync throws when querying subpath of existing file. #1216
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
Comments
Related: nodejs/node#39960 Implementing |
Deprecating fs.exists might make sense. It always causes TOCTOU issue and fs.exists can be considered as an anti-pattern. What do you think? @bnoordhuis @piscisaureus @ry |
I've never seen actually valid uses of Let me enumerate what its flaws are:
Go through nodejs/node#1592 if you have 30 minutes to spare and observe how pretty much all the people arguing for un-deprecation manage to screw up their examples. Peak facepalm! |
@bnoordhuis @kt3k A valid use-case is to check if a specific file exists that a third-party binary needs before starting that binary eventually. node introduced fs.accessSync() for that case. |
@martin-braun Sounds curious to me. Can you link to more specific context?
This still causes TOCTOU bugs. So I think the ideal solution to that situation is that the 3rd party binary handles the situation correctly and propagate the error to the user. |
@kt3k You are right. In theory the binary that needs the file should at least exit with an error code when it catches such exception, I just like to point out that in real life scenarios things are not always in control of us. I do not have a specific case to explore, but I know that I had to check for file existences in the past, because foreign binaries failed to do so or to provide a seamless experience and give more details, i.e. which file is missing for the external task which is about to trigger. At the end the external binary should and often will take care of TOCTOU, but checking the existence of those files still might be useful for gateways like wizard or wrapper applications etc. It's really an edge case and I agree that we should teach about TOCTOU by deprecating "exists", but node provides an alternative for special cases which I would appreciate in Deno as well. |
Thanks for the further input
This is sort of understandable scenario of using fs.exists. The point is probably how common that kind of situation is... I try to bring up this topic again at least before actually deleting these APIs. |
@kt3k Thank you and yes I agree, it's a rare situation, yet not avoidable in all cases. |
I've come across another possible use case for this API, a templating helper script (like you might find in some framework CLIs) with this logic:
The advice given above...
...assumes there is some action that the script wants to perform on the file, but in this case there is nothing to do with the file if it already exists. The script just needs to know that its not there. Sure, you can Alternatively (and this might be better approach), options could be added to |
@kt3k After a while I figured out that Deno has a way to check for file existence and uses this approach itself in the std lib.
@dev-nicolaos Use fs.ensureFile to create a file if it does not exist. Whatsoever, I agree that TOCTOU is not solved, but we ended up with an uglier version of Since checking for a file/folder is more annoying, since I have to use I know this is subjective, but this mindset made the lack of |
@martin-braun Do you now agree with the deprecation? |
@kt3k Yes, but it still feels subjective. The deprecation of The deprecation warning should be longer and more detailed, listing up all common scenarios and how to solve them, i.e. |
@martin-braun this still doesn't cover the scenario I outlined above. Using |
@dev-nicolaos Sorry, I have problems to understand what you try to achieve. Please quote what you "outlined above", so it's easier to follow. Above you said:
And If you really need to know if the file exists beforehand or you want to create a file X when file Y exists, then yes, you have to use But think it this way: @kt3k I think it would be nice if |
The sudo code for that example is roughly...
or...
This could be modifying how the existing options ( Deno.writeTextFileSync('path/to/maybe-file', data, { errorIfExists: true } Bottom LineFor the use case where you want to create a file only if no file already exists at a path, |
@dev-nicolaos Now I got what you mean, you mean the case: Create file X with content Y if it does not exist Using
So use neither of those methods. Instead open the file using It also seems that you still don't acknowledge the potential issue of having a directory instead of a file at your given path in your example. If you would use const x = "myfile.txt"
const y = "Hello World"
if(fs.exists(x)) { // first race condition
const stat = await Deno.lstat(x) // redundancy + 2nd race condition
if(stat.isDirectory) {
console.error("File does not exist, but path is blocked by folder")
}
} else {
await Deno.writeTextFile(x, y)
} (untested, probably bugged, but you get the idea) This code above is terrible. Deprecating Using const x = "myfile.txt"
const y = "Hello World"
const encoder = new TextDecoder("utf-8") // to write the file contents
const file = await Deno.open(x, { read: true, write: true, create: true }) // "create" will only create a new file when it does not exist
const stat = await file.stat()
if(stat.isDirectory) {
console.error("Path is blocked by a folder.")
} else if(stat.size == 0) { // file is empty?
await file.write(encoder.encode(y))
}
file.close() (untested, probably bugged, but you get the idea) This has exactly 0 race conditions / TOCTOU bugs. As soon as you open the file, its handler stays consistent. It should be save to move the file around on your system while the code runs. It also enables you to handle the "directory, not file" case. All that for just one line more code, compared to the full @kt3k @bnoordhuis @piscisaureus @ry I think we all would really welcome to have a flag on |
What API (option name) do you suggest more exactly? I feel it'd be a little too complex and too specific to certain scenario as a Deno namespace API. In general deno_std is more open to the addition of functions which are specific to some scenarios. |
@kt3k I'm not sure, playing with my thoughts, at the moment. I agree though, altering When I suggest with question marks, I really want to poke, maybe one of you have some idea how to cover this case properly. But, when it doubt, leave it out .. |
The "create file only when not already there" case would benefit from an |
Which maps directly to |
is basically |
Hi. I feel that "making For example, I need to check presence of a So please re-consider this deprecation again. P.S. I totally agree that documenting "file operations after |
It doesn't matter if you do something with the folder or not. Another process in the background could do something with it between your check and your action.
So why not using |
Yes. That's why I'm trying to say "race condition" is not the matter what Deno library should care.
It's just an example case so I don't dig it deeper but I don't check if Additionally, using export async function exists(filePath: string): Promise<boolean> {
try {
await Deno.lstat(filePath);
return true;
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
return false;
}
throw err;
}
} And this code is already exist in |
@lambdalisue So you say git should handle the error that Anyway, for the sake of dealing with an example in your favor, let's give you a proper case that supports your stance: If you have a situation in which it does not matter if you have a file or a folder but need to spawn a process if either of those exists under a given path/name, but for performance reasons avoid spawning such process if neither of those exist under a given path/name, then - so far I see - The official counter argument is that you should spawn such process without checking with But, the honest truth is that this situation is: It is very rare. If you check for a path for good reason you also should check if you are dealing with a file or folder, because in 99% of the time you expect one or another, but not any of them. In my personal subjective opinion for the 1% I'd say that you should just bite the bullet and use |
It's off topic so I don't want to dig it deeper but see https://git-scm.com/docs/git-init#Documentation/git-init.txt---separate-git-dirltgit-dirgt if you need some evidence.
I'm sorry but my point of view is not an exact case what I mentioned. I'd like to point out that there is situation that we need to check presence of a file/directory and we need to use deprecated code in that case because there is no other way.
In my opinion, using Ok, another example. I'm sorry but git again. Git create a file |
@lambdalisue Thanks for the evidence in regards of git.
I would like to know: Can you confirm It feels I would suggest you open a new issue to push a potential change. Best is to explain the very specific case in which you want to check for the existence of a file system entry and don't care about if it's a file or a folder to launch a sub process that handles that file system entry properly. I think your example in which you want to check if If it comes back, it should not be named Just saying |
Of course, people write problematic code from time to time, myself included, but I don't understand why const info = await Deno.lstat("hello.txt");
// Something...
await delay(1000);
if (info.isFile) {
// Maybe `hello.txt` become a directory at this point.
await Deno.run(["rm", "-rf", "hello.txt"]).output();
}
I think
In addition, the above link also stated the following
I think this paragraph is the correct way to alert users that improper use of the function can cause TOCTOU issues. In my opinion, there should be a distinction between documentation and deprecation.
I totally agree with this.
If
Since git itself does not care whether it is a file or a directory, in order to mimic it's behavior, it must not care wheter it is a file or a directory.
People like me would touch the contents in |
Thanks for the suggestion. I added a discussion for this https://github.com/denoland/deno_std/discussions/2102 |
Uh oh!
There was an error while loading. Please reload this page.
Describe the bug A clear and concise description of what the bug is.
fs.exists/fs.existsSync throws when querying subpath of existing file.
To Reproduce Steps to reproduce the behavior:
touch a.txt
deno --unstable
import * as fs from "https://deno.land/[email protected]/fs/mod.ts";
await fs.exists("a.txt/b");
Expected behavior A clear and concise description of what you expected to
happen.
Return
false
.Screenshots If applicable, add screenshots to help explain your problem.
Desktop (please complete the following information):
Additional context Add any other context about the problem here.
Other implementations:
os.path.exists
: Return false.require("fs").exists
: Resolves false.File.exist?
: Return false.Simple alternative implementation suggest:
fs/exists_test.ts doesn't include any throws/rejects test.
The text was updated successfully, but these errors were encountered: