shrine-google_cloud_storage 0.2.0 → 1.0.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
  SHA1:
3
- metadata.gz: '08d6183e134fdb573cb7a432c55489f17a59d05a'
4
- data.tar.gz: 4c0604286ce8695f96509d6f332f5b6045c408c4
3
+ metadata.gz: 2e36b4d1de28a447a15aab4f23369f8d580d3486
4
+ data.tar.gz: 8857d5c06e26ec8c96b4cd0e69672db095eab118
5
5
  SHA512:
6
- metadata.gz: fa358fd9e4a7a53ba56da9612860eee38998f544858e3010103b492c6fa9ee1ebb8937e39e42e42572daf29aea0e8a9a1d799ac39ebdf5b31248536e07ba8f1d
7
- data.tar.gz: 963493555038ef7918db7b129a1b5e837edb59212570ae936cc8fa78db4fd99fe9082d7395d87c47b9f6553297f976575523e3a82cbe5d5d4dcfecfa14ad7c0f
6
+ metadata.gz: e68c8763ebc015e2c65a8b523b4b56c26115efd9aaad97cd64cea85928abfce73f54a7cc96f8de189c3f70f3bce5b862b304a5ba16d79a6f50525ef02055f1af
7
+ data.tar.gz: 0e9322682a9934641d37ecfbff19eb9f69318727e989cb6c33eb7e24b6dc36b111c47cd5ec91a818b7e85a4479d54f6d8a87f20e29a7014052ec8032011fc49d
data/README.md CHANGED
@@ -12,13 +12,13 @@ gem "shrine-google_cloud_storage"
12
12
 
13
13
  ## Authentication
14
14
 
15
- The GCS plugin uses Google's [Application Default Credentials]. Please check
15
+ The GCS plugin uses Google's [Project and Credential Lookup]. Please check
16
16
  documentation for the various ways to provide credentials.
17
17
 
18
18
  ## Usage
19
19
 
20
20
  ```rb
21
- require "shrine/storage/gcs"
21
+ require "shrine/storage/google_cloud_storage"
22
22
 
23
23
  Shrine.storages = {
24
24
  cache: Shrine::Storage::GoogleCloudStorage.new(bucket: "cache"),
@@ -41,25 +41,60 @@ Shrine::Storage::GoogleCloudStorage.new(
41
41
 
42
42
  ## Contributing
43
43
 
44
- Firstly you need to create an `.env` file with a dedicated GCS bucket:
44
+ ### Test setup
45
+
46
+ #### Option 1 - use the script
47
+
48
+ Review the script `test/create_test_environment.sh`. It will:
49
+ - create a Google Cloud project
50
+ - associate it with your billing account
51
+ - create a service account
52
+ - add the `roles/storage.admin` iam policy
53
+ - download the json credentials
54
+ - create a test bucket
55
+ - add the needed variables to your `.env` file
56
+
57
+ To run, it assumes you have already run `gcloud auth login`.
58
+ It also needs a `.env` file in the project root containing the project name
59
+ and the billing account to use:
45
60
 
46
61
  ```sh
47
- # .env
48
- GCS_BUCKET="..."
62
+ cp .env.sample .env
63
+ # Edit .env to fill in your project and billing accounts
64
+ ./test/create_test_environment.sh
49
65
  ```
50
66
 
51
- Warning: all content of the bucket is cleared between tests, create a new one only for this usage!
67
+ #### Option 2 - manual setup
52
68
 
53
- Afterwards you can run the tests:
69
+ Create your own bucket and provide variables that allow for [project and credential lookup](http://googlecloudplatform.github.io/google-cloud-ruby/#/docs/google-cloud-storage/v1.6.0/guides/authentication#projectandcredentiallookup).
70
+ For example:
71
+
72
+ ```sh
73
+ GCS_BUCKET=shrine-gcs-test-my-project
74
+ GOOGLE_CLOUD_PROJECT=my-project
75
+ GOOGLE_CLOUD_KEYFILE=/Users/user/.gcp/my-project/shrine-gcs-test.json
76
+ ```
77
+
78
+ **Warning**: all content of the bucket is cleared between tests, create a new one only for this usage!
79
+
80
+ ### Running tests
81
+
82
+ After setting up your bucket, run the tests:
54
83
 
55
84
  ```sh
56
85
  $ bundle exec rake test
57
86
  ```
58
87
 
88
+ For additional debug, add the following to your `.env` file:
89
+
90
+ ```sh
91
+ GCS_DEBUG=true
92
+ ```
93
+
59
94
  ## License
60
95
 
61
96
  [MIT](http://opensource.org/licenses/MIT)
62
97
 
63
98
  [Google Cloud Storage]: https://cloud.google.com/storage/
64
99
  [Shrine]: https://github.com/janko-m/shrine
65
- [Application Default Credentials]: https://developers.google.com/identity/protocols/application-default-credentials
100
+ [Project and Credential Lookup]: http://googlecloudplatform.github.io/google-cloud-ruby/#/docs/google-cloud-storage/master/guides/authentication#projectandcredentiallookup
@@ -1,129 +1,124 @@
1
1
  require "shrine"
2
- require "googleauth"
3
- require "google/apis/storage_v1"
2
+ require "google/cloud/storage"
3
+ require "down/chunked_io"
4
4
 
5
5
  class Shrine
6
6
  module Storage
7
7
  class GoogleCloudStorage
8
8
  attr_reader :bucket, :prefix, :host
9
9
 
10
- def initialize(bucket:, prefix: nil, host: nil, default_acl: nil, object_options: {})
10
+ # Initialize a Shrine::Storage for GCS allowing for auto-discovery of the Google::Cloud::Storage client.
11
+ # @param [String] project Provide if not using auto discovery
12
+ # @see http://googlecloudplatform.github.io/google-cloud-ruby/#/docs/google-cloud-storage/v1.6.0/guides/authentication#environmentvariables for information on discovery
13
+ def initialize(project: nil, bucket:, prefix: nil, host: nil, default_acl: nil, object_options: {})
14
+ @project = project
11
15
  @bucket = bucket
12
16
  @prefix = prefix
13
17
  @host = host
14
18
  @default_acl = default_acl
15
19
  @object_options = object_options
20
+ @storage = nil
16
21
  end
17
22
 
18
- def upload(io, id, shrine_metadata: {}, **_options)
19
- # uploads `io` to the location `id`
20
-
21
- object = Google::Apis::StorageV1::Object.new @object_options.merge(bucket: @bucket, name: object_name(id))
22
-
23
+ # If the file is an UploadFile from GCS, issues a copy command, otherwise it uploads a file.
24
+ # @param [IO] io - io like object
25
+ # @param [String] id - location
26
+ def upload(io, id, shrine_metadata: {}, **options)
23
27
  if copyable?(io)
24
- storage_api.copy_object(
25
- io.storage.bucket,
26
- io.storage.object_name(io.id),
27
- @bucket,
28
- object_name(id),
29
- object,
30
- destination_predefined_acl: @default_acl,
31
- )
28
+ existing_file = get_bucket(io.storage.bucket).file(io.storage.object_name(io.id))
29
+ file = existing_file.copy(
30
+ @bucket, # dest_bucket_or_path - the bucket to copy the file to
31
+ object_name(id), # dest_path - the path to copy the file to in the given bucket
32
+ acl: @default_acl
33
+ ) do |f|
34
+ # update the additional options
35
+ @object_options.merge(options).each_pair do |key, value|
36
+ f.send("#{key}=", value)
37
+ end
38
+ end
39
+ file
32
40
  else
33
- storage_api.insert_object(
34
- @bucket,
35
- object,
36
- content_type: shrine_metadata["mime_type"],
37
- upload_source: io.to_io,
38
- options: { uploadType: 'multipart' },
39
- predefined_acl: @default_acl,
41
+ get_bucket.create_file(
42
+ prepare_io(io), # file - IO object, or IO-ish object like StringIO
43
+ object_name(id), # path
44
+ @object_options.merge(
45
+ content_type: shrine_metadata["mime_type"],
46
+ acl: @default_acl
47
+ ).merge(options)
40
48
  )
41
49
  end
42
50
  end
43
51
 
44
- def url(id, **_options)
45
- # URL to the remote file, accepts options for customizing the URL
46
- host = @host || "storage.googleapis.com/#{@bucket}"
47
-
48
- "https://#{host}/#{object_name(id)}"
52
+ # URL to the remote file, accepts options for customizing the URL
53
+ def url(id, **options)
54
+ if(options.key? :expires)
55
+ signed_url = presign(id, options).url
56
+ if @host.nil?
57
+ signed_url
58
+ else
59
+ signed_url.gsub(/storage.googleapis.com\/#{@bucket}/, @host)
60
+ end
61
+ else
62
+ host = @host || "storage.googleapis.com/#{@bucket}"
63
+ "https://#{host}/#{object_name(id)}"
64
+ end
49
65
  end
50
66
 
67
+ # Downloads the file from GCS, and returns a `Tempfile`.
51
68
  def download(id)
52
69
  tempfile = Tempfile.new(["googlestorage", File.extname(id)], binmode: true)
53
- storage_api.get_object(@bucket, object_name(id), download_dest: tempfile)
70
+ get_file(id).download tempfile.path
54
71
  tempfile.tap(&:open)
55
72
  end
56
73
 
74
+ # Opens the remote file and returns it as `Down::ChunkedIO` object.
75
+ # @return [Down::ChunkedIO] object
76
+ # @see https://github.com/janko-m/down#downchunkedio
57
77
  def open(id)
58
- # returns the remote file as an IO-like object
59
- io = storage_api.get_object(@bucket, object_name(id), download_dest: StringIO.new)
60
- io.rewind
61
- io
62
- end
78
+ file = get_file(id)
63
79
 
64
- def exists?(id)
65
- # checks if the file exists on the storage
66
- storage_api.get_object(@bucket, object_name(id)) do |_, err|
67
- if err
68
- if err.status_code == 404
69
- false
70
- else
71
- raise err
72
- end
73
- else
74
- true
75
- end
80
+ # create enumerator which lazily yields chunks of downloaded content
81
+ chunks = Enumerator.new do |yielder|
82
+ # trick to get google client to stream the download
83
+ proc_io = ProcIO.new { |data| yielder << data }
84
+ file.download(proc_io, verify: :none)
76
85
  end
77
- end
78
86
 
79
- def delete(id)
80
- # deletes the file from the storage
81
- storage_api.delete_object(@bucket, object_name(id))
82
-
83
- rescue Google::Apis::ClientError => e
84
- # The object does not exist, Shrine expects us to be ok
85
- return true if e.status_code == 404
87
+ # wrap chunks in an IO-like object which downloads when needed
88
+ Down::ChunkedIO.new(
89
+ chunks: chunks,
90
+ size: file.size,
91
+ data: { file: file }
92
+ )
93
+ end
86
94
 
87
- raise e
95
+ # checks if the file exists on the storage
96
+ def exists?(id)
97
+ file = get_file(id)
98
+ return false if file.nil?
99
+ file.exists?
88
100
  end
89
101
 
90
- def multi_delete(ids)
91
- batch_delete(ids.map { |i| object_name(i) })
102
+ # deletes the file from the storage
103
+ def delete(id)
104
+ file = get_file(id)
105
+ file.delete unless file.nil?
92
106
  end
93
107
 
108
+ # Otherwise deletes all objects from the storage.
94
109
  def clear!
95
- all_objects = storage_api.fetch_all do |token, s|
96
- prefix = "#{@prefix}/" if @prefix
97
- s.list_objects(
98
- @bucket,
99
- prefix: prefix,
100
- fields: "items/name",
101
- page_token: token,
102
- )
110
+ prefix = "#{@prefix}/" if @prefix
111
+ files = get_bucket.files prefix: prefix
112
+ batch_delete(files.lazy.map(&:name))
113
+ loop do
114
+ break if !files.next?
115
+ batch_delete(files.next.lazy.map(&:name))
103
116
  end
104
-
105
- batch_delete(all_objects.lazy.map(&:name))
106
117
  end
107
118
 
108
119
  def presign(id, **options)
109
- method = options[:method] || "GET"
110
- content_md5 = options[:content_md5] || ""
111
- content_type = options[:content_type] || ""
112
- expires = (Time.now.utc + (options[:expires] || 300)).to_i
113
- headers = nil
114
- path = "/#{@bucket}/" + object_name(id)
115
-
116
- to_sign = [method, content_md5, content_type, expires, headers, path].compact.join("\n")
117
-
118
- signing_key = options[:signing_key]
119
- signing_key = OpenSSL::PKey::RSA.new(signing_key) unless signing_key.respond_to?(:sign)
120
- signature = Base64.strict_encode64(signing_key.sign(OpenSSL::Digest::SHA256.new, to_sign)).delete("\n")
121
-
122
- signed_url = "https://storage.googleapis.com#{path}?GoogleAccessId=#{options[:issuer]}" \
123
- "&Expires=#{expires}&Signature=#{CGI.escape(signature)}"
124
-
125
120
  OpenStruct.new(
126
- url: signed_url,
121
+ url: storage.signed_url(@bucket, object_name(id), options),
127
122
  fields: {},
128
123
  )
129
124
  end
@@ -134,33 +129,70 @@ class Shrine
134
129
 
135
130
  private
136
131
 
132
+ def get_file(id)
133
+ get_bucket.file(object_name(id))
134
+ end
135
+
136
+ def get_bucket(bucket_name = @bucket)
137
+ storage.bucket(bucket_name, skip_lookup: true)
138
+ end
139
+
140
+ # @see http://googlecloudplatform.github.io/google-cloud-ruby/#/docs/google-cloud-storage/v1.6.0/guides/authentication
141
+ def storage
142
+ @storage ||= if @project.nil?
143
+ Google::Cloud::Storage.new
144
+ else
145
+ Google::Cloud::Storage.new(project: @project)
146
+ end
147
+ end
148
+
137
149
  def copyable?(io)
138
150
  io.is_a?(UploadedFile) &&
139
151
  io.storage.is_a?(Storage::GoogleCloudStorage)
140
152
  # TODO: add a check for the credentials
141
153
  end
142
154
 
155
+ # Google cloud storage client only accepts IO|IOString|Tempfile instances
156
+ # or else it will raise a "Google::Apis::ClientError, 'Invalid upload source"
157
+ #
158
+ # We need to convert our file to an IO.
159
+ j
160
+ # see:
161
+ # https://github.com/google/google-api-ruby-client/blob/1c2cf5d57fd5e606c03f2aecab88469f46b8f3b2/lib/google/apis/core/upload.rb#L77
162
+ def prepare_io(io)
163
+ if io.respond_to?(:to_io)
164
+ io.to_io
165
+ elsif io.respond_to?(:tempfile)
166
+ io.tempfile
167
+ else
168
+ io
169
+ end
170
+ end
171
+
143
172
  def batch_delete(object_names)
144
- # Batches are limited to 100 operations
145
- object_names.each_slice(100) do |names|
146
- storage_api.batch do |storage|
147
- names.each do |name|
148
- storage.delete_object(@bucket, name)
149
- end
150
- end
173
+ bucket = get_bucket
174
+ object_names.each do |name|
175
+ bucket.file(name).delete
151
176
  end
152
177
  end
153
178
 
154
- def storage_api
155
- if !@storage_api || @storage_api.authorization.expired?
156
- service = Google::Apis::StorageV1::StorageService.new
157
- scopes = ['https://www.googleapis.com/auth/devstorage.read_write']
158
- authorization = Google::Auth.get_application_default(scopes)
159
- authorization.fetch_access_token!
160
- service.authorization = authorization
161
- @storage_api = service
179
+ # This class provides a writable IO wrapper around a proc object, with
180
+ # #write simply calling the proc, which we can pass in as the destination
181
+ # IO for download.
182
+ class ProcIO
183
+ def initialize(&proc)
184
+ @proc = proc
185
+ end
186
+
187
+ def write(data)
188
+ @proc.call(data)
189
+ data.bytesize # match return value of other IO objects
190
+ end
191
+
192
+ # TODO: Remove this once google/google-api-ruby-client#638 is merged.
193
+ def flush
194
+ # google-api-client calls this method
162
195
  end
163
- @storage_api
164
196
  end
165
197
  end
166
198
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "shrine-google_cloud_storage"
3
- gem.version = "0.2.0"
3
+ gem.version = "1.0.0"
4
4
 
5
5
  gem.required_ruby_version = ">= 2.1"
6
6
 
@@ -14,7 +14,7 @@ Gem::Specification.new do |gem|
14
14
  gem.require_path = "lib"
15
15
 
16
16
  gem.add_dependency "shrine", "~> 2.0"
17
- gem.add_dependency "google-api-client", "~> 0.13.0"
17
+ gem.add_dependency "google-cloud-storage", "~> 1.6"
18
18
 
19
19
  gem.add_development_dependency "rake"
20
20
  gem.add_development_dependency "minitest"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shrine-google_cloud_storage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renaud Chaput
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-06-23 00:00:00.000000000 Z
11
+ date: 2017-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: shrine
@@ -25,19 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: google-api-client
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: 0.13.0
33
+ version: '1.6'
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: 0.13.0
40
+ version: '1.6'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -110,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
110
  version: '0'
111
111
  requirements: []
112
112
  rubyforge_project:
113
- rubygems_version: 2.5.2
113
+ rubygems_version: 2.6.14
114
114
  signing_key:
115
115
  specification_version: 4
116
116
  summary: Provides Google Cloud Storage storage for Shrine.