-
Notifications
You must be signed in to change notification settings - Fork 370
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
fix: don't fail if ~/.config doesn't exist #1428
Conversation
…affeeullah/succeedIfConfigDoesntExist
Codecov Report
@@ Coverage Diff @@
## master #1428 +/- ##
=======================================
Coverage 99.10% 99.10%
=======================================
Files 14 14
Lines 12163 12187 +24
Branches 604 532 -72
=======================================
+ Hits 12054 12078 +24
Misses 109 109
Continue to review full report at Codecov.
|
src/file.ts
Outdated
@@ -1802,7 +1802,9 @@ class File extends ServiceObject<File> { | |||
const configDir = xdgBasedir.config || os.tmpdir(); | |||
|
|||
fs.access(configDir, fs.constants.W_OK, err => { | |||
if (err) { | |||
if (err && fs.existsSync(configDir)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally you want to avoid calling Sync
methods inside of production level code paths. They block the node.js event loop, and file system access is slow. If we wanted to keep down this path, I think we'd have to make 2 distinct calls to fs.access
with varying flags: one with R_OK
and one with W_OK
.
That having been said ... the pattern of checking for a given directory or file before starting a write is generally frowned upon. This is specifically called out in the fs
docs:
Do not use
fs.access()
to check for the accessibility of a file before callingfs.open()
,fs.readFile()
orfs.writeFile()
. Doing so introduces a race condition, since other processes may change the file's state between the two calls. Instead, user code should open/read/write the file directly and handle the error raised if the file is not accessible.
From here:
https://nodejs.org/api/fs.html#fs_fs_access_path_mode_callback
Should we instead handle errors that come back directly from startSimpleUpload_
, and give a good error message based on the resulting stream?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding to the first point - if we decide to keep going down the pre-check route, it's possible you can do a single fs.stat
and do bit math to play with the resulting mode - I'm out of my depth here though (@bcoe may know more)
…affeeullah/succeedIfConfigDoesntExist
src/file.ts
Outdated
if (options.resumable) { | ||
const error = new ResumableUploadError( | ||
[ | ||
'A resumable upload could not be performed. The directory,', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are two causes of failures now:
- The directory doesn't exist, and can't be created
- The directory exists, but cannot be written to
Should we generalize the error message or maybe do a separate message for each?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great callout! I've created a separate issue to track this here.
src/file.ts
Outdated
this.startResumableUpload_(fileWriteStream, options); | ||
}; | ||
maybeCreateFolder().catch(e => { | ||
throw new Error(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is not covered by code coverage, but I'm not sure how to get to this case. This error shouldn't get thrown. Any ideas for how to write a test for this are welcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe related, do we need the function “maybeCreateFolder”? Can the code inside simply execute when the fs.access callback runs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Fixed!
@@ -1801,26 +1801,30 @@ class File extends ServiceObject<File> { | |||
// https://github.com/yeoman/configstore/blob/f09f067e50e6a636cfc648a6fc36a522062bd49d/index.js#L11 | |||
const configDir = xdgBasedir.config || os.tmpdir(); | |||
|
|||
fs.access(configDir, fs.constants.W_OK, err => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I attempted a bit of a code re-sort and added comments for clarity. Hopefully it's not too much, and it's helpful for future developers. Feel free to take only pieces if there are any parts that you think are useful:
// The logic below attempts to mimic the resumable upload library,
// gcs-resumable-upload. That library requires a writable configuration
// directory in order to work. If we wait for that library to discover any
// issues, we've already started a resumable upload which is difficult to back
// out of. We want to catch any errors first, so we can choose a simple, non-
// resumable upload instead.
// Same as configstore (used by gcs-resumable-upload):
// https://github.com/yeoman/configstore/blob/f09f067e50e6a636cfc648a6fc36a522062bd49d/index.js#L11
const configDir = xdgBasedir.config || os.tmpdir();
fs.access(configDir, fs.constants.W_OK, accessErr => {
if (!accessErr) {
// A configuration directory exists, and it's writable. gcs-resumable-upload
// should have everything it needs to work.
this.startResumableUpload_(fileWriteStream, options);
return;
};
// The configuration directory is either not writable, or it doesn't exist.
// gcs-resumable-upload will attempt to create it for the user, but we'll try
// it now to confirm that it won't have any issues. That way, if we catch the
// issue before we start the resumable upload, we can instead start a simple
// upload.
fs.mkdir(configDir, {mode: 0o0700}, err => {
if (!err) {
// We successfully created a configuration directory that
// gcs-resumable-upload will use.
this.startResumableUpload_(fileWriteStream, options);
return;
}
if (options.resumable) {
// The user wanted a resumable upload, but we couldn't create a
// configuration directory, which means gcs-resumable-upload will fail.
const error = new ResumableUploadError(
[
'A resumable upload could not be performed. The directory,',
`${configDir}, is not writable. You may try another upload,`,
'this time setting `options.resumable` to `false`.',
].join(' ')
);
stream.destroy(error);
} else {
// The user didn't care, resumable or not. Fall back to simple upload.
this.startSimpleUpload_(fileWriteStream, options);
}
});
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this! Thanks for the additions. I like that it notes the extent to which we bleed the requirements of gcs-resumable-upload
into this library.
test/file.ts
Outdated
}); | ||
|
||
it('should succeed if resumable requested but config dir doesnt exist', done => { | ||
assert.strictEqual(fs.existsSync(configDir), false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fs.access(configDir, fs.constants.W_OK, accessErr => {
is not throwing an error for this test and I'm really confused as to why. @stephenplusplus any ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that was because we were stubbing fs.access
earlier in the test file to always pass. Checking into this caused me to get in the editor and write a bit of code as I worked through the issue, and I ended up with a few tests to cover the changes in the PR. I'm sorry I keep doing this. It seems to work out that when I'm investigating an issue, I end up writing a bunch of code to test my theories out. So, here is what I ended up with: https://gist.github.com/stephenplusplus/dccf237542b7a702ebff90f03f2a557a. Don't worry about using any of it if you wanted to go a different way with the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to apologize! Thanks so much for doing this!!! Super super helpful :) Really appreciate it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch noting that fs.access
was being stubbed. I totally missed that.
package.json
Outdated
@@ -111,6 +112,7 @@ | |||
"node-fetch": "^2.2.0", | |||
"normalize-newline": "^3.0.0", | |||
"proxyquire": "^2.1.3", | |||
"rimraf": "^3.0.2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove this dependency now.
Thank you! LGTM! |
Fixes #1420 🦕