bucket_store 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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