Skip to content

Commit

Permalink
Support moving files between uris
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeSouthan committed May 26, 2021
1 parent 3ad68aa commit 084dfbf
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ tmp/

.bundle/
.rspec_status
.yardoc/
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ FileStorage.for("inmemory://bucket/path/").list
=> ["inmemory://bucket/path/file.xml"]
```

### Moving a file
```ruby
FileStorage.for("inmemory://bucket/path/file.xml").move!("inmemory://bucket/path/file2.xml")
=> "inmemory://bucket/path/file2.xml"
```

### Delete a file
```ruby
FileStorage.for("inmemory://bucket/path/file.xml").delete!
Expand Down
9 changes: 9 additions & 0 deletions lib/file_storage/disk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ def delete!(bucket:, key:)
true
end

def move!(bucket:, key:, new_bucket:, new_key:)
FileUtils.mv(key_path(bucket, key), key_path(new_bucket, new_key))

{
bucket: new_bucket,
key: new_key,
}
end

private

attr_reader :base_dir
Expand Down
12 changes: 12 additions & 0 deletions lib/file_storage/gcs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ def delete!(bucket:, key:)
true
end

def move!(bucket:, key:, new_bucket:, new_key:)
old_file = get_bucket(bucket).file(key)
destination_bucket = get_bucket(new_bucket)
file.copy(destination_bucket.name, new_key)
old_file.delete

{
bucket: new_bucket,
key: new_key,
}
end

private

attr_reader :storage
Expand Down
8 changes: 8 additions & 0 deletions lib/file_storage/in_memory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,13 @@ def delete!(bucket:, key:)

true
end

def move!(bucket:, key:, new_bucket:, new_key:)
@buckets[new_bucket][new_key] = @buckets.fetch(bucket).delete(key)
{
bucket: new_bucket,
key: new_key,
}
end
end
end
32 changes: 32 additions & 0 deletions lib/file_storage/key_storage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,38 @@ def list(page_size: 1000)
end
end

# Moves the existing file to a new file path
#
# @param [String] new_key The new key to move the file to
# @return [String] A URI to the file's new path
# @example Move a file
# FileStorage.for("inmemory://bucket1/foo").move!("inmemory://bucket2/bar")
def move!(new_key)
raise ArgumentError, "Key cannot be empty" if key.empty?

new_key_ctx = FileStorage.for(new_key)

unless new_key_ctx.adapter_type == adapter_type
raise ArgumentError, "Adapter type must be the same"
end
raise ArgumentError, "Destination key cannot be empty" if new_key_ctx.key.empty?

start = FileStorage::Timing.monotonic_now
result = adapter.move!(
bucket: bucket,
key: key,
new_bucket: new_key_ctx.bucket,
new_key: new_key_ctx.key,
)

FileStorage.logger.info(
event: "key_storage.moved",
duration: FileStorage::Timing.monotonic_now - start,
)

"#{adapter_type}://#{result[:bucket]}/#{result[:key]}"
end

# Deletes the referenced key.
#
# Note that this method will always return true.
Expand Down
40 changes: 40 additions & 0 deletions spec/file_storage/disk_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,44 @@
to raise_error(Errno::ENOENT, /No such file or directory/)
end
end

describe "#move!" do
subject(:move) do
instance.move!(bucket: bucket, key: key, new_bucket: new_bucket, new_key: new_key)
end

let(:new_bucket) { "cake" }

context "when the 'existing' file doesn't exist" do
let(:key) { "foobar" }
let(:new_key) { "barbaz" }

it "raises a File error" do
expect { move }.to raise_error(Errno::ENOENT, /No such file or directory/)
end
end

context "when the file does exist" do
let(:key) { "2021-02-08/hello1" }
let(:new_key) { "2021-02-08/hello2" }
let(:content) { "world" }

before { instance.upload!(bucket: bucket, key: key, content: content) }

it "moves the file" do
move

expect(instance.download(bucket: new_bucket, key: new_key)[:content]).to eq(content)
expect { instance.download(bucket: bucket, key: key)[:content] }.
to raise_error(Errno::ENOENT, /No such file or directory/)
end

it "returns the expected payload" do
expect(move).to eq(
bucket: new_bucket,
key: new_key,
)
end
end
end
end
40 changes: 40 additions & 0 deletions spec/file_storage/in_memory_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,46 @@
end
end

describe "#move!" do
subject(:move) do
instance.move!(bucket: bucket, key: key, new_bucket: new_bucket, new_key: new_key)
end

let(:new_bucket) { "cake" }

context "when the 'existing' file doesn't exist" do
let(:key) { "foobar" }
let(:new_key) { "barbaz" }

it "raises a KeyError" do
expect { move }.to raise_error(KeyError, /#{bucket}/)
end
end

context "when the file does exist" do
let(:key) { "2021-02-08/hello1" }
let(:new_key) { "2021-02-08/hello2" }
let(:content) { "world" }

before { instance.upload!(bucket: bucket, key: key, content: content) }

it "moves the file" do
move

expect(instance.download(bucket: new_bucket, key: new_key)[:content]).to eq(content)
expect { instance.download(bucket: bucket, key: key)[:content] }.
to raise_error(KeyError, /key not found/)
end

it "returns the expected payload" do
expect(move).to eq(
bucket: new_bucket,
key: new_key,
)
end
end
end

describe "#reset!" do
let(:bucket2) { "bucket2" }

Expand Down
27 changes: 27 additions & 0 deletions spec/file_storage/key_storage_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,33 @@ def build_for(key)
end
end

describe "#move!" do
subject(:move) { build_for(old_key).move!(new_key) }

let(:old_key) { "inmemory://bucket/file1" }
let(:new_key) { "inmemory://bucket2/file2" }

before { build_for(old_key).upload!("hello") }

it "returns the new path's uri" do
expect(move).to eq(new_key)
end

it "moves the file" do
move

expect(build_for(new_key).download[:content]).to eq("hello")
end

context "with a different adapter" do
let(:new_key) { "disk://bucket2/file2" }

it "raises an error" do
expect { move }.to raise_error(ArgumentError, /Adapter type/)
end
end
end

describe "delete!" do
before do
build_for("inmemory://bucket/file1").upload!("content1")
Expand Down

0 comments on commit 084dfbf

Please sign in to comment.