Skip to content
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

Merged
merged 19 commits into from
Apr 1, 2021

Conversation

shaffeeullah
Copy link
Contributor

Fixes #1420 🦕

@shaffeeullah shaffeeullah requested a review from a team March 23, 2021 20:20
@shaffeeullah shaffeeullah requested a review from a team as a code owner March 23, 2021 20:20
@product-auto-label product-auto-label bot added the api: storage Issues related to the googleapis/nodejs-storage API. label Mar 23, 2021
@google-cla google-cla bot added the cla: yes This human has signed the Contributor License Agreement. label Mar 23, 2021
@codecov
Copy link

codecov bot commented Mar 23, 2021

Codecov Report

Merging #1428 (1aa3608) into master (bd9ef18) will increase coverage by 0.00%.
The diff coverage is 100.00%.

Impacted file tree graph

@@           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           
Impacted Files Coverage Δ
src/file.ts 99.94% <100.00%> (+<0.01%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bd9ef18...1aa3608. Read the comment docs.

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)) {
Copy link
Contributor

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 calling fs.open(), fs.readFile() or fs.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?

Copy link
Contributor

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)

src/file.ts Outdated
if (options.resumable) {
const error = new ResumableUploadError(
[
'A resumable upload could not be performed. The directory,',
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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 => {
Copy link
Contributor

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);
    }
  });
});

Copy link
Contributor Author

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);
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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",
Copy link
Contributor

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.

@stephenplusplus
Copy link
Contributor

Thank you! LGTM!

@shaffeeullah shaffeeullah merged commit 3cfaba1 into master Apr 1, 2021
@shaffeeullah shaffeeullah deleted the shaffeeullah/succeedIfConfigDoesntExist branch April 1, 2021 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: storage Issues related to the googleapis/nodejs-storage API. cla: yes This human has signed the Contributor License Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Resumable Upload fails if ~/.config does not exist
4 participants