diff --git a/.gitignore b/.gitignore index 4a45838..0752cb4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ tmp/ .bundle/ .rspec_status +.yardoc/ diff --git a/README.md b/README.md index 5d1ef71..734f930 100644 --- a/README.md +++ b/README.md @@ -138,3 +138,9 @@ FileStorage.for("inmemory://bucket/path/").list FileStorage.for("inmemory://bucket/path/").delete! => true ``` + +### Moving a file +```ruby +FileStorage.for("inmemory://bucket/path/file.xml").move!("inmemory://bucket/path/file2.xml") +=> "inmemory://bucket/path/file2.xml" +``` diff --git a/lib/file_storage.rb b/lib/file_storage.rb index 85dab7f..73b70ed 100644 --- a/lib/file_storage.rb +++ b/lib/file_storage.rb @@ -178,6 +178,36 @@ def delete! true 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) + + raise ArgumentError, "Destination key cannot be empty" if new_key_ctx.key.empty? + + info("Moving file", new_key: new_key, event: "move") + + start = FileStorage::Timing.monotonic_now + result = adapter.move!( + bucket: bucket, + key: key, + new_bucket: new_key_ctx.bucket, + new_key: new_key_ctx.key, + ) + + info("Moved file", + event: "move_finished", + duration: FileStorage::Timing.monotonic_now - start) + + "#{adapter_type}://#{result[:bucket]}/#{result[:key]}" + end + private attr_reader :adapter diff --git a/lib/file_storage/disk.rb b/lib/file_storage/disk.rb index f976914..986c3fc 100644 --- a/lib/file_storage/disk.rb +++ b/lib/file_storage/disk.rb @@ -53,6 +53,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 diff --git a/lib/file_storage/gcs.rb b/lib/file_storage/gcs.rb index 6d18361..e47525d 100644 --- a/lib/file_storage/gcs.rb +++ b/lib/file_storage/gcs.rb @@ -56,6 +56,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 diff --git a/lib/file_storage/in_memory.rb b/lib/file_storage/in_memory.rb index 47fb9fe..69301fb 100644 --- a/lib/file_storage/in_memory.rb +++ b/lib/file_storage/in_memory.rb @@ -54,5 +54,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 diff --git a/spec/file_storage/disk_spec.rb b/spec/file_storage/disk_spec.rb index e3663e0..f653b24 100644 --- a/spec/file_storage/disk_spec.rb +++ b/spec/file_storage/disk_spec.rb @@ -141,4 +141,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 diff --git a/spec/file_storage/in_memory_spec.rb b/spec/file_storage/in_memory_spec.rb index aa5feba..269da75 100644 --- a/spec/file_storage/in_memory_spec.rb +++ b/spec/file_storage/in_memory_spec.rb @@ -104,6 +104,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" } diff --git a/spec/file_storage_spec.rb b/spec/file_storage_spec.rb index b19d31d..5c21e23 100644 --- a/spec/file_storage_spec.rb +++ b/spec/file_storage_spec.rb @@ -153,5 +153,24 @@ expect(described_class.for("inmemory://bucket/file1").delete!).to eq(true) end end + + describe "#move!" do + subject(:move) { described_class.for(old_key).move!(new_key) } + + let(:old_key) { "inmemory://bucket/file1" } + let(:new_key) { "inmemory://bucket2/file2" } + + before { described_class.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(described_class.for(new_key).download[:content]).to eq("hello") + end + end end end