shrine-google_cloud_storage 0.2.0 → 1.0.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
  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.