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 22c8e90..f79da2f 100644 --- a/README.md +++ b/README.md @@ -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! diff --git a/lib/file_storage/disk.rb b/lib/file_storage/disk.rb index 16543f4..470f563 100644 --- a/lib/file_storage/disk.rb +++ b/lib/file_storage/disk.rb @@ -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 diff --git a/lib/file_storage/gcs.rb b/lib/file_storage/gcs.rb index 4d8c969..076d56e 100644 --- a/lib/file_storage/gcs.rb +++ b/lib/file_storage/gcs.rb @@ -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 diff --git a/lib/file_storage/in_memory.rb b/lib/file_storage/in_memory.rb index 58759a2..43b08ba 100644 --- a/lib/file_storage/in_memory.rb +++ b/lib/file_storage/in_memory.rb @@ -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 diff --git a/lib/file_storage/key_storage.rb b/lib/file_storage/key_storage.rb index c8180ae..c302da7 100644 --- a/lib/file_storage/key_storage.rb +++ b/lib/file_storage/key_storage.rb @@ -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. diff --git a/spec/file_storage/disk_spec.rb b/spec/file_storage/disk_spec.rb index c9b2ab3..4b6ecee 100644 --- a/spec/file_storage/disk_spec.rb +++ b/spec/file_storage/disk_spec.rb @@ -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 diff --git a/spec/file_storage/in_memory_spec.rb b/spec/file_storage/in_memory_spec.rb index 6588143..77a9c03 100644 --- a/spec/file_storage/in_memory_spec.rb +++ b/spec/file_storage/in_memory_spec.rb @@ -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" } diff --git a/spec/file_storage/key_storage_spec.rb b/spec/file_storage/key_storage_spec.rb index f111b2d..d3670e4 100644 --- a/spec/file_storage/key_storage_spec.rb +++ b/spec/file_storage/key_storage_spec.rb @@ -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")