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 +4 -4
- data/README.md +43 -8
- data/lib/shrine/storage/google_cloud_storage.rb +132 -100
- data/shrine-google_cloud_storage.gemspec +2 -2
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e36b4d1de28a447a15aab4f23369f8d580d3486
|
4
|
+
data.tar.gz: 8857d5c06e26ec8c96b4cd0e69672db095eab118
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 [
|
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/
|
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
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
67
|
+
#### Option 2 - manual setup
|
52
68
|
|
53
|
-
|
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
|
-
[
|
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 "
|
3
|
-
require "
|
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
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
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
|
-
|
91
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
145
|
-
object_names.
|
146
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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.
|
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-
|
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.
|
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-
|
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-
|
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:
|
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:
|
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.
|
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.
|