bucket_store 0.5.0 → 0.7.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: 0a1d87ab8bc67271f2a7dcc656d78a8b0a57083d2251534a851a882067efc48d
4
+ data.tar.gz: 32d35e884c672c6b45d815bc022c2c2937ff741686ce52ea24b022b493f0b65a
5
5
  SHA512:
6
- metadata.gz: 60b864052d68196fd7bda4a67a67bb66a82ef9bb828f01fb9364ce8aeca67ea414f8f2c34259982fd3dd76a3545b9c72172400e3acbb277fe3cc907d5d449e36
7
- data.tar.gz: 5aab2c949368e330f73470f5a9c705c681662ec7209d8e8600c3ea971f852c79d2ce6f6af52350760c9f585cc25ab94f51431001cbb4ff053276333515bf8fd1
6
+ metadata.gz: f3b67bfee44e2b627522acd7dc6b068038ebed5e472cdd82f32f5cac1774b762e165d65107c5faf2f5bda7926e4e630206323d293ee40ad2537f6114680da523
7
+ data.tar.gz: ba9679d3ed934f439195893ad8d5d6b9d18b4a638d167bb385416c3b12c3f4da28df74cf076cd2a5cd6065e982ec80356fd3893c0d645e396eec1ee45e47ae00
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
 
@@ -3,25 +3,42 @@
3
3
  require "stringio"
4
4
  require "uri"
5
5
 
6
- require "google/cloud/storage"
7
-
8
6
  module BucketStore
9
7
  class Gcs
10
8
  DEFAULT_TIMEOUT_SECONDS = 30
11
9
 
10
+ def self.load_client_library
11
+ @load_client_library ||= require "google/cloud/storage"
12
+ end
13
+
12
14
  def self.build(timeout_seconds = DEFAULT_TIMEOUT_SECONDS)
13
- Gcs.new(timeout_seconds)
15
+ new(timeout_seconds)
14
16
  end
15
17
 
16
18
  def initialize(timeout_seconds)
17
- @storage = Google::Cloud::Storage.new(
19
+ self.class.load_client_library
20
+
21
+ # Ruby's GCS library does not natively support setting up a simulator, but it allows
22
+ # for a specific endpoint to be passed down which has the same effect. The simulator
23
+ # needs to be special cased as in that case we want to bypass authentication,
24
+ # which we can only do by accessing the `.anonymous` version of the Storage class.
25
+ simulator_endpoint = ENV["STORAGE_EMULATOR_HOST"]
26
+ is_simulator = !simulator_endpoint.nil?
27
+
28
+ args = {
29
+ endpoint: simulator_endpoint,
18
30
  timeout: timeout_seconds,
19
- )
31
+ }.compact
32
+
33
+ @storage = if is_simulator
34
+ Google::Cloud::Storage.anonymous(**args)
35
+ else
36
+ Google::Cloud::Storage.new(**args)
37
+ end
20
38
  end
21
39
 
22
- def upload!(bucket:, key:, content:)
23
- buffer = StringIO.new(content)
24
- get_bucket(bucket).create_file(buffer, key)
40
+ def upload!(bucket:, key:, file:)
41
+ get_bucket(bucket).create_file(file, key)
25
42
 
26
43
  {
27
44
  bucket: bucket,
@@ -29,17 +46,14 @@ module BucketStore
29
46
  }
30
47
  end
31
48
 
32
- def download(bucket:, key:)
33
- file = get_bucket(bucket).file(key)
49
+ def download(bucket:, key:, file:)
50
+ file.tap do |f|
51
+ get_bucket(bucket).
52
+ file(key).
53
+ download(f)
34
54
 
35
- buffer = StringIO.new
36
- file.download(buffer)
37
-
38
- {
39
- bucket: bucket,
40
- key: key,
41
- content: buffer.string,
42
- }
55
+ f.rewind
56
+ end
43
57
  end
44
58
 
45
59
  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
@@ -2,29 +2,33 @@
2
2
 
3
3
  require "uri"
4
4
 
5
- require "aws-sdk-s3"
6
-
7
5
  module BucketStore
8
6
  class S3
9
7
  DEFAULT_TIMEOUT_SECONDS = 30
10
8
 
9
+ def self.load_client_library
10
+ @load_client_library ||= require "aws-sdk-s3"
11
+ end
12
+
11
13
  def self.build(open_timeout_seconds = DEFAULT_TIMEOUT_SECONDS,
12
14
  read_timeout_seconds = DEFAULT_TIMEOUT_SECONDS)
13
- S3.new(open_timeout_seconds, read_timeout_seconds)
15
+ new(open_timeout_seconds, read_timeout_seconds)
14
16
  end
15
17
 
16
18
  def initialize(open_timeout_seconds, read_timeout_seconds)
19
+ self.class.load_client_library
20
+
17
21
  @storage = Aws::S3::Client.new(
18
22
  http_open_timeout: open_timeout_seconds,
19
23
  http_read_timeout: read_timeout_seconds,
20
24
  )
21
25
  end
22
26
 
23
- def upload!(bucket:, key:, content:)
27
+ def upload!(bucket:, key:, file:)
24
28
  storage.put_object(
25
29
  bucket: bucket,
26
30
  key: key,
27
- body: content,
31
+ body: file,
28
32
  )
29
33
 
30
34
  {
@@ -33,17 +37,12 @@ module BucketStore
33
37
  }
34
38
  end
35
39
 
36
- def download(bucket:, key:)
37
- file = storage.get_object(
40
+ def download(bucket:, key:, file:)
41
+ storage.get_object(
42
+ response_target: file,
38
43
  bucket: bucket,
39
44
  key: key,
40
45
  )
41
-
42
- {
43
- bucket: bucket,
44
- key: key,
45
- content: file.body.read,
46
- }
47
46
  end
48
47
 
49
48
  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.7.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.7.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: 2024-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-s3
@@ -16,58 +16,44 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.104'
19
+ version: '1.147'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.104'
26
+ version: '1.147'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: google-cloud-storage
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.34'
33
+ version: '1.50'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.34'
40
+ version: '1.50'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: gc_ruboconfig
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.29'
47
+ version: 4.4.2
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.4.2
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: pry-byebug
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '3.9'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '3.9'
69
- - !ruby/object:Gem::Dependency
70
- name: rspec
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
59
  - - "~>"
@@ -81,61 +67,47 @@ dependencies:
81
67
  - !ruby/object:Gem::Version
82
68
  version: '3.10'
83
69
  - !ruby/object:Gem::Dependency
84
- name: rspec_junit_formatter
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: 0.4.1
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: 0.4.1
97
- - !ruby/object:Gem::Dependency
98
- name: rubocop
70
+ name: rspec
99
71
  requirement: !ruby/object:Gem::Requirement
100
72
  requirements:
101
73
  - - "~>"
102
74
  - !ruby/object:Gem::Version
103
- version: '1.22'
75
+ version: '3.13'
104
76
  type: :development
105
77
  prerelease: false
106
78
  version_requirements: !ruby/object:Gem::Requirement
107
79
  requirements:
108
80
  - - "~>"
109
81
  - !ruby/object:Gem::Version
110
- version: '1.22'
82
+ version: '3.13'
111
83
  - !ruby/object:Gem::Dependency
112
- name: rubocop-performance
84
+ name: rspec-github
113
85
  requirement: !ruby/object:Gem::Requirement
114
86
  requirements:
115
87
  - - "~>"
116
88
  - !ruby/object:Gem::Version
117
- version: '1.11'
89
+ version: 2.4.0
118
90
  type: :development
119
91
  prerelease: false
120
92
  version_requirements: !ruby/object:Gem::Requirement
121
93
  requirements:
122
94
  - - "~>"
123
95
  - !ruby/object:Gem::Version
124
- version: '1.11'
96
+ version: 2.4.0
125
97
  - !ruby/object:Gem::Dependency
126
- name: rubocop-rspec
98
+ name: rubocop
127
99
  requirement: !ruby/object:Gem::Requirement
128
100
  requirements:
129
- - - "~>"
101
+ - - ">="
130
102
  - !ruby/object:Gem::Version
131
- version: '2.5'
103
+ version: '1.63'
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.63'
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.4.10
179
152
  signing_key:
180
153
  specification_version: 4
181
154
  summary: A helper library to access cloud storage services