bucket_store 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14fe28facb1db7af27414a3d1ce64ad24974c79e72c5b9faca796860f679c3d8
4
- data.tar.gz: 4b6a4fe4e68bb81b4e7df6d76d2f8e14dc6210508955263fe579b067c7b1af61
3
+ metadata.gz: 4a5f88aa8903c932c2de1c7ed531bf78f58f72e9785ba8732a4cca7e44605549
4
+ data.tar.gz: f2d8e01ad1f54acf7c44a71df268576131df013e656dfced91f700b80927f036
5
5
  SHA512:
6
- metadata.gz: 60b864052d68196fd7bda4a67a67bb66a82ef9bb828f01fb9364ce8aeca67ea414f8f2c34259982fd3dd76a3545b9c72172400e3acbb277fe3cc907d5d449e36
7
- data.tar.gz: 5aab2c949368e330f73470f5a9c705c681662ec7209d8e8600c3ea971f852c79d2ce6f6af52350760c9f585cc25ab94f51431001cbb4ff053276333515bf8fd1
6
+ metadata.gz: 22ba164e42115a064b4aa6f7833d3d5c86931fe7c35d2da873725fa2488ab6849ce2481bf09bc9d93f7eab085a0132d957e1d81fad8855e89060663895aa5428
7
+ data.tar.gz: 9c88e6bf881ecab6686255cabfa416be09985a8f48b200726f89e7589e5925dd5ab13700a865aa69c49b4050d654447c09e802b71852b62ef331c5e80568b369
data/README.md CHANGED
@@ -135,18 +135,34 @@ in mind:
135
135
 
136
136
  ## Examples
137
137
 
138
- ### Uploading a file to a bucket
138
+ ### Uploading a string to a bucket
139
139
  ```ruby
140
140
  BucketStore.for("inmemory://bucket/path/file.xml").upload!("hello world")
141
141
  => "inmemory://bucket/path/file.xml"
142
142
  ```
143
143
 
144
- ### Accessing a file in a bucket
144
+ ### Accessing a string in a bucket
145
145
  ```ruby
146
146
  BucketStore.for("inmemory://bucket/path/file.xml").download
147
147
  => {:bucket=>"bucket", :key=>"path/file.xml", :content=>"hello world"}
148
148
  ```
149
149
 
150
+ ### Uploading a file-like object to a bucket
151
+ ```ruby
152
+ buffer = StringIO.new("This could also be an actual file")
153
+ BucketStore.for("inmemory://bucket/path/file.xml").stream.upload!(file: buffer)
154
+ => "inmemory://bucket/path/file.xml"
155
+ ```
156
+
157
+ ### Downloading to a file-like object from a bucket
158
+ ```ruby
159
+ buffer = StringIO.new
160
+ BucketStore.for("inmemory://bucket/path/file.xml").stream.download(file: buffer)
161
+ => {:bucket=>"bucket", :key=>"path/file.xml", :file=>buffer}
162
+ buffer.string
163
+ => "This could also be an actual file"
164
+ ```
165
+
150
166
  ### Listing all keys under a prefix
151
167
  ```ruby
152
168
  BucketStore.for("inmemory://bucket/path/").list
@@ -163,10 +179,14 @@ BucketStore.for("inmemory://bucket/path/file.xml").delete!
163
179
 
164
180
  ### Running tests
165
181
  BucketStore comes with both unit and integration tests. While unit tests can be run by simply
166
- executing `bundle exec rspec`, integration tests require running minio locally. We provide an
167
- helper script (`scripts/run-minio.sh`) that spins up a pre-configured docker container with
168
- a single test bucket. Once minio has started, integration tests can be executed with
169
- `bundle exec rspec --tag integration`.
182
+ executing `bundle exec rspec`, integration tests require running minio locally. We provide a
183
+ docker-compose file that spins up pre-configured simulator instances for S3 and GCS with
184
+ test buckets. Running the integration tests is as easy as:
185
+
186
+ ```
187
+ docker-compose up
188
+ bundle exec rspec --tag integration
189
+ ```
170
190
 
171
191
  ## License & Contributing
172
192
 
@@ -13,23 +13,22 @@ module BucketStore
13
13
  @base_dir = File.expand_path(base_dir)
14
14
  end
15
15
 
16
- def upload!(bucket:, key:, content:)
17
- File.open(key_path(bucket, key), "w") do |file|
18
- file.write(content)
16
+ def upload!(bucket:, key:, file:)
17
+ File.open(key_path(bucket, key), "w") do |output_file|
18
+ output_file.write(file.read)
19
+ output_file.rewind
19
20
  end
21
+
20
22
  {
21
23
  bucket: bucket,
22
24
  key: key,
23
25
  }
24
26
  end
25
27
 
26
- def download(bucket:, key:)
27
- File.open(key_path(bucket, key), "r") do |file|
28
- {
29
- bucket: bucket,
30
- key: key,
31
- content: file.read,
32
- }
28
+ def download(bucket:, key:, file:)
29
+ File.open(key_path(bucket, key), "r") do |saved_file|
30
+ file.write(saved_file.read)
31
+ file.rewind
33
32
  end
34
33
  end
35
34
 
@@ -14,14 +14,27 @@ module BucketStore
14
14
  end
15
15
 
16
16
  def initialize(timeout_seconds)
17
- @storage = Google::Cloud::Storage.new(
17
+ # Ruby's GCS library does not natively support setting up a simulator, but it allows
18
+ # for a specific endpoint to be passed down which has the same effect. The simulator
19
+ # needs to be special cased as in that case we want to bypass authentication,
20
+ # which we can only do by accessing the `.anonymous` version of the Storage class.
21
+ simulator_endpoint = ENV["STORAGE_EMULATOR_HOST"]
22
+ is_simulator = !simulator_endpoint.nil?
23
+
24
+ args = {
25
+ endpoint: simulator_endpoint,
18
26
  timeout: timeout_seconds,
19
- )
27
+ }.compact
28
+
29
+ @storage = if is_simulator
30
+ Google::Cloud::Storage.anonymous(**args)
31
+ else
32
+ Google::Cloud::Storage.new(**args)
33
+ end
20
34
  end
21
35
 
22
- def upload!(bucket:, key:, content:)
23
- buffer = StringIO.new(content)
24
- get_bucket(bucket).create_file(buffer, key)
36
+ def upload!(bucket:, key:, file:)
37
+ get_bucket(bucket).create_file(file, key)
25
38
 
26
39
  {
27
40
  bucket: bucket,
@@ -29,17 +42,14 @@ module BucketStore
29
42
  }
30
43
  end
31
44
 
32
- def download(bucket:, key:)
33
- file = get_bucket(bucket).file(key)
34
-
35
- buffer = StringIO.new
36
- file.download(buffer)
45
+ def download(bucket:, key:, file:)
46
+ file.tap do |f|
47
+ get_bucket(bucket).
48
+ file(key).
49
+ download(f)
37
50
 
38
- {
39
- bucket: bucket,
40
- key: key,
41
- content: buffer.string,
42
- }
51
+ f.rewind
52
+ end
43
53
  end
44
54
 
45
55
  def list(bucket:, key:, page_size:)
@@ -24,8 +24,10 @@ module BucketStore
24
24
  @buckets = Hash.new { |hash, key| hash[key] = {} }
25
25
  end
26
26
 
27
- def upload!(bucket:, key:, content:)
28
- @buckets[bucket][key] = content
27
+ def upload!(bucket:, key:, file:)
28
+ file.tap do |f|
29
+ @buckets[bucket][key] = f.read
30
+ end
29
31
 
30
32
  {
31
33
  bucket: bucket,
@@ -33,12 +35,11 @@ module BucketStore
33
35
  }
34
36
  end
35
37
 
36
- def download(bucket:, key:)
37
- {
38
- bucket: bucket,
39
- key: key,
40
- content: @buckets[bucket].fetch(key),
41
- }
38
+ def download(bucket:, key:, file:)
39
+ file.tap do |f|
40
+ f.write(@buckets[bucket].fetch(key))
41
+ f.rewind
42
+ end
42
43
  end
43
44
 
44
45
  def list(bucket:, key:, page_size:)
@@ -15,6 +15,90 @@ module BucketStore
15
15
  disk: Disk,
16
16
  }.freeze
17
17
 
18
+ # Defines a streaming interface for download and upload operations.
19
+ #
20
+ # Note that individual adapters may require additional configuration for the correct
21
+ # behavior of the streaming interface.
22
+ class KeyStreamer
23
+ attr_reader :bucket, :key, :adapter_type
24
+
25
+ def initialize(adapter:, adapter_type:, bucket:, key:)
26
+ @adapter = adapter
27
+ @adapter_type = adapter_type
28
+ @bucket = bucket
29
+ @key = key
30
+ end
31
+
32
+ # Streams the content of the reference key into a file-like object
33
+ # @param [IO] file a writeable IO instance, or a file-like object such as `StringIO`
34
+ # @return hash containing the bucket, the key and file like object passed in as input
35
+ #
36
+ # @see KeyStorage#download
37
+ # @example Download a key
38
+ # buffer = StringIO.new
39
+ # BucketStore.for("inmemory://bucket/file.xml").stream.download(file: buffer)
40
+ # buffer.string == "Imagine I'm a 2GB file"
41
+ def download(file:)
42
+ BucketStore.logger.info(event: "key_storage.download_started")
43
+
44
+ start = BucketStore::Timing.monotonic_now
45
+ adapter.download(
46
+ bucket: bucket,
47
+ key: key,
48
+ file: file,
49
+ )
50
+
51
+ BucketStore.logger.info(event: "key_storage.download_finished",
52
+ duration: BucketStore::Timing.monotonic_now - start)
53
+
54
+ {
55
+ bucket: bucket,
56
+ key: key,
57
+ file: file,
58
+ }
59
+ end
60
+
61
+ # Performs a streaming upload to the backing object store
62
+ # @param [IO] file a readable IO instance, or a file-like object such as `StringIO`
63
+ # @return the generated key for the new object
64
+ #
65
+ # @see KeyStorage#upload!
66
+ # @example Upload a key
67
+ # buffer = StringIO.new("Imagine I'm a 2GB file")
68
+ # BucketStore.for("inmemory://bucket/file.xml").stream.upload!(file: buffer)
69
+ def upload!(file:)
70
+ raise ArgumentError, "Key cannot be empty" if key.empty?
71
+
72
+ BucketStore.logger.info(event: "key_storage.upload_started",
73
+ **log_context)
74
+
75
+ start = BucketStore::Timing.monotonic_now
76
+ adapter.upload!(
77
+ bucket: bucket,
78
+ key: key,
79
+ file: file,
80
+ )
81
+
82
+ BucketStore.logger.info(event: "key_storage.upload_finished",
83
+ duration: BucketStore::Timing.monotonic_now - start,
84
+ **log_context)
85
+
86
+ "#{adapter_type}://#{bucket}/#{key}"
87
+ end
88
+
89
+ private
90
+
91
+ attr_reader :adapter
92
+
93
+ def log_context
94
+ {
95
+ bucket: bucket,
96
+ key: key,
97
+ adapter_type: adapter_type,
98
+ }.compact
99
+ end
100
+ end
101
+
18
102
  attr_reader :bucket, :key, :adapter_type
19
103
 
20
104
  def initialize(adapter:, bucket:, key:)
@@ -40,45 +124,32 @@ module BucketStore
40
124
  # @example Download a key
41
125
  # BucketStore.for("inmemory://bucket/file.xml").download
42
126
  def download
43
- raise ArgumentError, "Key cannot be empty" if key.empty?
44
-
45
- BucketStore.logger.info(event: "key_storage.download_started")
46
-
47
- start = BucketStore::Timing.monotonic_now
48
- result = adapter.download(bucket: bucket, key: key)
49
-
50
- BucketStore.logger.info(event: "key_storage.download_finished",
51
- duration: BucketStore::Timing.monotonic_now - start)
52
-
53
- result
127
+ buffer = StringIO.new
128
+ stream.download(file: buffer).tap do |result|
129
+ result.delete(:file)
130
+ result[:content] = buffer.string
131
+ end
54
132
  end
55
133
 
56
- # Uploads the given content to the reference key location.
134
+ # Uploads the given file to the reference key location.
57
135
  #
58
136
  # If the `key` already exists, its content will be replaced by the one in input.
59
137
  #
60
- # @param [String] content The content to upload
138
+ # @param [String] content Contents of the file
61
139
  # @return [String] The final `key` where the content has been uploaded
62
140
  # @example Upload a file
63
- # BucketStore.for("inmemory://bucket/file.xml").upload("hello world")
141
+ # BucketStore.for("inmemory://bucket/file.xml").upload!("hello world")
64
142
  def upload!(content)
65
- raise ArgumentError, "Key cannot be empty" if key.empty?
66
-
67
- BucketStore.logger.info(event: "key_storage.upload_started",
68
- **log_context)
69
-
70
- start = BucketStore::Timing.monotonic_now
71
- result = adapter.upload!(
72
- bucket: bucket,
73
- key: key,
74
- content: content,
75
- )
143
+ stream.upload!(file: StringIO.new(content))
144
+ end
76
145
 
77
- BucketStore.logger.info(event: "key_storage.upload_finished",
78
- duration: BucketStore::Timing.monotonic_now - start,
79
- **log_context)
146
+ # Returns an interface for streaming operations
147
+ #
148
+ # @return [KeyStreamer] An interface for streaming operations
149
+ def stream
150
+ raise ArgumentError, "Key cannot be empty" if key.empty?
80
151
 
81
- "#{adapter_type}://#{result[:bucket]}/#{result[:key]}"
152
+ KeyStreamer.new(adapter: adapter, adapter_type: adapter_type, bucket: bucket, key: key)
82
153
  end
83
154
 
84
155
  # Lists all keys for the current adapter that have the reference key as prefix
@@ -159,13 +230,5 @@ module BucketStore
159
230
  private
160
231
 
161
232
  attr_reader :adapter
162
-
163
- def log_context
164
- {
165
- bucket: bucket,
166
- key: key,
167
- adapter_type: adapter_type,
168
- }.compact
169
- end
170
233
  end
171
234
  end
@@ -20,11 +20,11 @@ module BucketStore
20
20
  )
21
21
  end
22
22
 
23
- def upload!(bucket:, key:, content:)
23
+ def upload!(bucket:, key:, file:)
24
24
  storage.put_object(
25
25
  bucket: bucket,
26
26
  key: key,
27
- body: content,
27
+ body: file,
28
28
  )
29
29
 
30
30
  {
@@ -33,17 +33,12 @@ module BucketStore
33
33
  }
34
34
  end
35
35
 
36
- def download(bucket:, key:)
37
- file = storage.get_object(
36
+ def download(bucket:, key:, file:)
37
+ storage.get_object(
38
+ response_target: file,
38
39
  bucket: bucket,
39
40
  key: key,
40
41
  )
41
-
42
- {
43
- bucket: bucket,
44
- key: key,
45
- content: file.body.read,
46
- }
47
42
  end
48
43
 
49
44
  def list(bucket:, key:, page_size:)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BucketStore
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bucket_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless Engineering
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-09 00:00:00.000000000 Z
11
+ date: 2023-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-s3
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.29'
47
+ version: 4.3.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.29'
54
+ version: 4.3.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: pry-byebug
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -81,61 +81,33 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '3.10'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rspec_junit_formatter
84
+ name: rspec-github
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 0.4.1
89
+ version: 2.4.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 0.4.1
96
+ version: 2.4.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: rubocop
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '1.22'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '1.22'
111
- - !ruby/object:Gem::Dependency
112
- name: rubocop-performance
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: '1.11'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: '1.11'
125
- - !ruby/object:Gem::Dependency
126
- name: rubocop-rspec
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - "~>"
101
+ - - ">="
130
102
  - !ruby/object:Gem::Version
131
- version: '2.5'
103
+ version: '1.49'
132
104
  type: :development
133
105
  prerelease: false
134
106
  version_requirements: !ruby/object:Gem::Requirement
135
107
  requirements:
136
- - - "~>"
108
+ - - ">="
137
109
  - !ruby/object:Gem::Version
138
- version: '2.5'
110
+ version: '1.49'
139
111
  description: " A helper library to access cloud storage services such as Google
140
112
  Cloud Storage or S3.\n"
141
113
  email:
@@ -159,7 +131,8 @@ files:
159
131
  homepage: https://github.com/gocardless/bucket-store
160
132
  licenses:
161
133
  - MIT
162
- metadata: {}
134
+ metadata:
135
+ rubygems_mfa_required: 'true'
163
136
  post_install_message:
164
137
  rdoc_options: []
165
138
  require_paths:
@@ -168,14 +141,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
141
  requirements:
169
142
  - - ">="
170
143
  - !ruby/object:Gem::Version
171
- version: '2.6'
144
+ version: '2.7'
172
145
  required_rubygems_version: !ruby/object:Gem::Requirement
173
146
  requirements:
174
147
  - - ">="
175
148
  - !ruby/object:Gem::Version
176
149
  version: '0'
177
150
  requirements: []
178
- rubygems_version: 3.2.22
151
+ rubygems_version: 3.3.3
179
152
  signing_key:
180
153
  specification_version: 4
181
154
  summary: A helper library to access cloud storage services