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(fsi): qri remove --all --force should not fail on low value files #1203

Merged
merged 6 commits into from
Mar 23, 2020

Conversation

Arqu
Copy link
Contributor

@Arqu Arqu commented Mar 19, 2020

Addresses issue #1190.

@Arqu Arqu requested a review from b5 March 19, 2020 10:58
@Arqu Arqu added the fix A bug fix label Mar 19, 2020
@Arqu Arqu changed the title fix(cmd): qri remove --all --force should not fail on low value files fix(fsi): qri remove --all --force should not fail on low value files Mar 19, 2020
@b5 b5 assigned Arqu Mar 19, 2020
fsi/fsi.go Outdated
err := removeLowValueFiles(dirPath)
if err != nil {
log.Errorf("removing directory: %s", err.Error())
}
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 this would be better if it lived here: https://github.com/qri-io/qri/blob/master/lib/datasets.go#L793. For one, this behavior isn't directly needed by fsi.Unlink, only by lib.Remove. If a user runs qri fsi unlink then this function also gets called, via the Unlink function in lib/fsi.go, but that shouldn't be deleting files. Furthermore, having this functionality here means that it doesn't respect the --keep-files flag, which if true shouldn't modify any files at all.

Would also be great to have a test for this in cmd/, which creates a directory, runs qri init, runs save, writes a .DS_Store file to that directory, then runs remove --all to make sure the whole thing is deleted. Copying TestRemoveOneRevisionFromWorkingDirectory then modifying it would be a great start.

Copy link
Member

@b5 b5 Mar 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on moving the call site to https://github.com/qri-io/qri/blob/master/lib/datasets.go#L793, but I think the logic of what we mean by RemoveAll should live in the fsi package. To me the best of both worlds is if fsi had an exported function:

package fsi

// RemoveAll is like os.RemoveAll, but it also removes low-value files
func RemoveAll(path string) error {
  // ...
}

Then call that function from lib.

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 took a bit of a split approach with datastore retaining control but moving the execution logic to fsi.

fsi/fsi.go Outdated
os.Remove(path)
}
}
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only other question here: should we also be removing directories if they are empty after removing low-value files? Personally, I think we should. when I run qri remove --all --force I'm trying to blow the directory away.

We could save that for another PR, but if we can get it knocked out now, I'd be all for 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.

Got covered as part of the update :)

@Arqu
Copy link
Contributor Author

Arqu commented Mar 20, 2020

Still pending on adding the tests.

Copy link
Contributor

@dustmop dustmop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test TestRemoveAllVersionsWorkingDirectoryLowValueFiles is awesome, excellent work. I have a design concern with --force, however, that I think we need to figure out.

subDirPath := filepath.Join(run.WorkPath, subdir)
err := os.MkdirAll(subDirPath, os.ModePerm)
if err != nil {
panic(err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of panic'ing, pass a testing.T to CreateSubDir and call t.Fatal(err) here. See how MustWriteFile does 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.

Yeah, lot's of these small bits we're just taken out of other examples. I'll look over it and appropriate this + if I find this setup anywhere else.

}

// Remove all versions
run.MustExec(t, "qri remove --all=1")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small style suggestion, just do --all without the =1. It's just a boolean flag, this makes it read as though it requires an integer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the above, seen this in several places. For this PR will fix just this case but will open another to clean up the rest.

run.MustWriteFile(t, "test.sh", "echo test\n")

// Remove all with force
run.MustExec(t, "qri remove --revisions=all --force")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest that, even with --force, we shouldn't try and delete files that we don't own. See my comments on the fsi.RemoveAll function for full justification.

fsi/fsi.go Outdated
if err := fsi.Unlink(dirPath, refStr); err != nil {
return err
}
if err := os.RemoveAll(dirPath); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wary of ever calling RemoveAll, because it's potentially very destructive, especially when taking unknown / arbitrary input as a parameter. The only places we use it in the codebase right now are for tests that construct temporary directories, and one or two places that are part of rarely used commands, like a user deleting their whole repository. Since we're a data storage system, when it comes to deletes, I'd rather err on the side of being cautious over convenient, and whitelist the things we delete, rather than doing a RemoveAll.

In fact, this makes me realize, if a user at some point runs qri init in their home directory, and then later run remove --all --force, this codepath would try and delete their entire home directory. That alone is reason enough why we shouldn't make --force mean "remove even unknown files". The current semantics of --force are "delete things even if we get errors, including the working directory not being clean", and I think it should stay that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on RemoveAll being very agressive and some bad input could wreak havoc.
On the alternative side, doing just RemoveAll and Remove will essentially result in the same behavior, unless Remove no longer removes low value files and it just triggers on --force which technically is the purpose of this PR.

Think the above might be a best blend between the two things while remaining cautious.

lib/datasets.go Outdated
if p.Force {
err = r.inst.fsi.RemoveAll(ref.FSIPath, ref.AliasString())
} else if !p.KeepFiles {
err = r.inst.fsi.Remove(ref.FSIPath, ref.AliasString())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call is happening before fsi.DeleteComponentFiles below, which is what removes "body.csv", "structure.json", "meta.json" etc. That means in nearly all cases, the working directory will not be empty, and the call to os.Remove within fsi.Remove will fail.

There's already a call to os.Remove below on the next line (after fsi.DeleteComponentFiles). I think the logic of removing low-value files should be happening over there, instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Currently only works due to os.RemoveAll from above.

fsi/fsi.go Outdated
"^__MACOSX$", // Resource fork

// linux specific files
"~$", // Backup file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fan of this one pattern. Any file ending with a ~ will be considered low-value, that's too broad and easy to get false positives.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to remove it, mostly picked it up from some other list of "low value files"

}

func getLowValueFiles(files *[]string) filepath.WalkFunc {
return func(path string, f os.FileInfo, err error) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to run go fmt ./... in order to apply the standard golang code style guide.

Copy link
Contributor

@dustmop dustmop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@Arqu Arqu merged commit 398e0ac into master Mar 23, 2020
@Arqu Arqu deleted the qri-remove-force-1190 branch March 23, 2020 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fix A bug fix
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants