resizing 0.1.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 +7 -0
- data/.circleci/config.yml +71 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +74 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +69 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +13 -0
- data/lib/resizing.rb +60 -0
- data/lib/resizing/active_storage/service/resizing_service.rb +57 -0
- data/lib/resizing/carrier_wave.rb +126 -0
- data/lib/resizing/carrier_wave/storage/file.rb +212 -0
- data/lib/resizing/carrier_wave/storage/remote.rb +51 -0
- data/lib/resizing/client.rb +148 -0
- data/lib/resizing/configuration.rb +100 -0
- data/lib/resizing/mock_client.rb +23 -0
- data/lib/resizing/version.rb +5 -0
- data/resizing.gemspec +43 -0
- metadata +234 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resizing
|
4
|
+
module CarrierWave
|
5
|
+
module Storage
|
6
|
+
class File
|
7
|
+
include ::CarrierWave::Utilities::Uri
|
8
|
+
|
9
|
+
def initialize(uploader, identifier = nil)
|
10
|
+
@uploader = uploader
|
11
|
+
@content_type = nil
|
12
|
+
@identifier = identifier
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes
|
16
|
+
file.attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
def authenticated_url(_options = {})
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def content_type
|
24
|
+
@content_type || file.try(:content_type)
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_writer :content_type
|
28
|
+
|
29
|
+
def delete
|
30
|
+
public_id = model.send :read_attribute, serialization_column
|
31
|
+
# TODO:
|
32
|
+
# refactoring
|
33
|
+
separted = Resizing.separate_public_id(public_id)
|
34
|
+
image_id = separted[:image_id]
|
35
|
+
resp = client.delete(image_id)
|
36
|
+
if resp.nil? # 404 not found
|
37
|
+
model.send :write_attribute, serialization_column, nil
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
if public_id == resp['public_id']
|
42
|
+
model.send :write_attribute, serialization_column, nil
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
raise APIError, "raise someone error:#{resp.inspect}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def extension
|
50
|
+
raise NotImplementedError, 'this method is do not used. maybe'
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Read content of file from service
|
55
|
+
#
|
56
|
+
# === Returns
|
57
|
+
#
|
58
|
+
# [String] contents of file
|
59
|
+
def read
|
60
|
+
file_body = file.body
|
61
|
+
|
62
|
+
return if file_body.nil?
|
63
|
+
return file_body unless file_body.is_a?(::File)
|
64
|
+
|
65
|
+
# Fog::Storage::XXX::File#body could return the source file which was upoloaded to the remote server.
|
66
|
+
read_source_file(file_body) if ::File.exist?(file_body.path)
|
67
|
+
|
68
|
+
# If the source file doesn't exist, the remote content is read
|
69
|
+
@file = nil
|
70
|
+
file.body
|
71
|
+
end
|
72
|
+
|
73
|
+
def size
|
74
|
+
file.nil? ? 0 : file.content_length
|
75
|
+
end
|
76
|
+
|
77
|
+
def exists?
|
78
|
+
!!file
|
79
|
+
end
|
80
|
+
|
81
|
+
def store(new_file)
|
82
|
+
if new_file.is_a?(self.class)
|
83
|
+
# new_file.copy_to(path)
|
84
|
+
raise NotImplementedError, 'new file is required duplicating'
|
85
|
+
end
|
86
|
+
|
87
|
+
@content_type ||= new_file.content_type
|
88
|
+
@response = Resizing.put(identifier, new_file.read, { content_type: @content_type })
|
89
|
+
@public_id = @response['public_id']
|
90
|
+
|
91
|
+
# force update column
|
92
|
+
# model_class
|
93
|
+
# .where(primary_key_name => model.send(primary_key_name))
|
94
|
+
# .update_all(serialization_column=>@public_id)
|
95
|
+
|
96
|
+
# save new value to model class
|
97
|
+
model.send :write_attribute, serialization_column, @public_id
|
98
|
+
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
attr_reader :public_id
|
103
|
+
|
104
|
+
def identifier
|
105
|
+
if public_id.present?
|
106
|
+
public_id
|
107
|
+
else
|
108
|
+
@identifier = SecureRandom.uuid
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def filename(options = {})
|
113
|
+
file_url = url(options)
|
114
|
+
return unless file_url
|
115
|
+
|
116
|
+
CGI.unescape(file_url.split('?').first).gsub(%r{.*/(.*?$)}, '\1')
|
117
|
+
end
|
118
|
+
|
119
|
+
# def copy_to(new_path)
|
120
|
+
# CarrierWave::Storage::Fog::File.new(@uploader, @base, new_path)
|
121
|
+
# end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
attr_reader :uploader
|
126
|
+
|
127
|
+
def model
|
128
|
+
@model ||= uploader.model
|
129
|
+
end
|
130
|
+
|
131
|
+
def model_class
|
132
|
+
@model_class ||= model.class
|
133
|
+
end
|
134
|
+
|
135
|
+
def primary_key_name
|
136
|
+
@primary_key_name ||= model_class.primary_key.to_sym
|
137
|
+
end
|
138
|
+
|
139
|
+
def serialization_column
|
140
|
+
@serialization_column ||= model.send(:_mounter, uploader.mounted_as).send(:serialization_column)
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# client of Resizing
|
145
|
+
def client
|
146
|
+
@client ||= if Resizing.configure.enable_mock
|
147
|
+
Resizing::MockClient.new
|
148
|
+
else
|
149
|
+
Resizing::Client.new
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# lookup file
|
155
|
+
#
|
156
|
+
# === Returns
|
157
|
+
#
|
158
|
+
# [Fog::#{provider}::File] file data from remote service
|
159
|
+
#
|
160
|
+
# def file
|
161
|
+
# @file ||= directory.files.head(path)
|
162
|
+
# end
|
163
|
+
|
164
|
+
def read_source_file(file_body)
|
165
|
+
return unless ::File.exist?(file_body.path)
|
166
|
+
|
167
|
+
begin
|
168
|
+
file_body = ::File.open(file_body.path) if file_body.closed? # Reopen if it's already closed
|
169
|
+
file_body.read
|
170
|
+
ensure
|
171
|
+
file_body.close
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def url_options_supported?(local_file)
|
176
|
+
parameters = local_file.method(:url).parameters
|
177
|
+
parameters.count == 2 && parameters[1].include?(:options)
|
178
|
+
end
|
179
|
+
|
180
|
+
# def store! sanitized_file
|
181
|
+
# puts sanitized_file.inspect
|
182
|
+
# client = Resizing::Client.new
|
183
|
+
# @store_response = client.post(sanitized_file.to_file, {content_type: sanitized_file.content_type})
|
184
|
+
# end
|
185
|
+
|
186
|
+
def retrieve!(identifier)
|
187
|
+
raise NotImplementedError, "retrieve! #{identifier}"
|
188
|
+
end
|
189
|
+
|
190
|
+
# def cache!(new_file)
|
191
|
+
# raise NotImplementedError,
|
192
|
+
# "Need to implement #cache! if you want to use #{self.class.name} as a cache storage."
|
193
|
+
# end
|
194
|
+
|
195
|
+
# def retrieve_from_cache!(identifier)
|
196
|
+
# raise NotImplementedError,
|
197
|
+
# "Need to implement #retrieve_from_cache! if you want to use #{self.class.name} as a cache storage."
|
198
|
+
# end
|
199
|
+
|
200
|
+
# def delete_dir!(path)
|
201
|
+
# raise NotImplementedError,
|
202
|
+
# "Need to implement #delete_dir! if you want to use #{self.class.name} as a cache storage."
|
203
|
+
# end
|
204
|
+
|
205
|
+
# def clean_cache!(seconds)
|
206
|
+
# raise NotImplementedError,
|
207
|
+
# "Need to implement #clean_cache! if you want to use #{self.class.name} as a cache storage."
|
208
|
+
# end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resizing
|
4
|
+
module CarrierWave
|
5
|
+
module Storage
|
6
|
+
# ref.
|
7
|
+
# https://github.com/carrierwaveuploader/carrierwave/blob/master/lib/carrierwave/storage/abstract.rb
|
8
|
+
class Remote < ::CarrierWave::Storage::Abstract
|
9
|
+
def store!(file)
|
10
|
+
f = Resizing::CarrierWave::Storage::File.new(uploader)
|
11
|
+
f.store(file)
|
12
|
+
@filename = f.public_id
|
13
|
+
f
|
14
|
+
end
|
15
|
+
|
16
|
+
def retrieve!(identifier)
|
17
|
+
Resizing::CarrierWave::Storage::File.new(uploader, identifier)
|
18
|
+
end
|
19
|
+
|
20
|
+
def cache!(new_file)
|
21
|
+
f = Resizing::CarrierWave::Storage::File.new(uploader)
|
22
|
+
f.store(new_file)
|
23
|
+
f
|
24
|
+
end
|
25
|
+
|
26
|
+
def retrieve_from_cache!(identifier)
|
27
|
+
# NOP
|
28
|
+
# Resizing::CarrierWave::Storage::File..new(uploader, uploader.cache_path(identifier))
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete_dir!(path)
|
32
|
+
# do nothing, because there's no such things as 'empty directory'
|
33
|
+
end
|
34
|
+
|
35
|
+
def clean_cache!(seconds)
|
36
|
+
# do nothing
|
37
|
+
#
|
38
|
+
# connection.directories.new(
|
39
|
+
# :key => uploader.fog_directory,
|
40
|
+
# :public => uploader.fog_public
|
41
|
+
# ).files.all(:prefix => uploader.cache_dir).each do |file|
|
42
|
+
# # generate_cache_id returns key formated TIMEINT-PID(-COUNTER)-RND
|
43
|
+
# time = file.key.scan(/(\d+)-\d+-\d+(?:-\d+)?/).first.map { |t| t.to_i }
|
44
|
+
# time = Time.at(*time)
|
45
|
+
# file.destroy if time < (Time.now.utc - seconds)
|
46
|
+
# end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Resizing
|
4
|
+
#= Client class for Resizing
|
5
|
+
#--
|
6
|
+
# usage.
|
7
|
+
# options = {
|
8
|
+
# host: 'https://www.resizing.net',
|
9
|
+
# project_id: '098a2a0d-0000-0000-0000-000000000000',
|
10
|
+
# secret_token: '4g1cshg......rbs6'
|
11
|
+
# }
|
12
|
+
# client = Resizing::Client.new(options)
|
13
|
+
# file = File.open('sample.jpg', 'r')
|
14
|
+
# response = client.post(file)
|
15
|
+
# {
|
16
|
+
# "id"=>"fde443bb-0b29-4be2-a04e-2da8f19716ac",
|
17
|
+
# "project_id"=>"098a2a0d-0000-0000-0000-000000000000",
|
18
|
+
# "content_type"=>"image/jpeg",
|
19
|
+
# "latest_version_id"=>"Ot0NL4rptk6XxQNFP2kVojn5yKG44cYH",
|
20
|
+
# "latest_etag"=>"\"069ec178a367089c3f0306dd716facf2\"",
|
21
|
+
# "created_at"=>"2020-05-17T15:02:30.548Z",
|
22
|
+
# "updated_at"=>"2020-05-17T15:02:30.548Z"
|
23
|
+
# }
|
24
|
+
#
|
25
|
+
#++
|
26
|
+
class Client
|
27
|
+
# TODO
|
28
|
+
# to use standard constants
|
29
|
+
HTTP_STATUS_OK = 200
|
30
|
+
HTTP_STATUS_CREATED = 201
|
31
|
+
|
32
|
+
attr_reader :config
|
33
|
+
def initialize(*attrs)
|
34
|
+
@config = if attrs.first.is_a? Configuration
|
35
|
+
attrs.first
|
36
|
+
elsif attrs.first.nil?
|
37
|
+
Resizing.configure
|
38
|
+
else
|
39
|
+
Configuration.new(*attrs)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def get(name)
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
def post(file_or_binary, options = {})
|
48
|
+
ensure_content_type(options)
|
49
|
+
|
50
|
+
url = build_post_url
|
51
|
+
|
52
|
+
body = to_io(file_or_binary)
|
53
|
+
params = {
|
54
|
+
image: Faraday::UploadIO.new(body, options[:content_type])
|
55
|
+
}
|
56
|
+
|
57
|
+
response = http_client.post(url, params) do |request|
|
58
|
+
request.headers['X-ResizingToken'] = config.generate_auth_header
|
59
|
+
end
|
60
|
+
|
61
|
+
result = handle_response(response)
|
62
|
+
result
|
63
|
+
end
|
64
|
+
|
65
|
+
def put(name, file_or_binary, options)
|
66
|
+
ensure_content_type(options)
|
67
|
+
|
68
|
+
url = build_put_url(name)
|
69
|
+
|
70
|
+
body = to_io(file_or_binary)
|
71
|
+
params = {
|
72
|
+
image: Faraday::UploadIO.new(body, options[:content_type])
|
73
|
+
}
|
74
|
+
|
75
|
+
response = http_client.put(url, params) do |request|
|
76
|
+
request.headers['X-ResizingToken'] = config.generate_auth_header
|
77
|
+
end
|
78
|
+
|
79
|
+
result = handle_response(response)
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete(name)
|
84
|
+
url = build_delete_url(name)
|
85
|
+
|
86
|
+
response = http_client.delete(url) do |request|
|
87
|
+
request.headers['X-ResizingToken'] = config.generate_auth_header
|
88
|
+
end
|
89
|
+
|
90
|
+
result = handle_response(response)
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def build_get_url(name)
|
97
|
+
"#{config.host}/projects/#{config.project_id}/upload/images/#{name}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def build_post_url
|
101
|
+
"#{config.host}/projects/#{config.project_id}/upload/images/"
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_put_url(name)
|
105
|
+
"#{config.host}/projects/#{config.project_id}/upload/images/#{name}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_delete_url(name)
|
109
|
+
"#{config.host}/projects/#{config.project_id}/upload/images/#{name}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def http_client
|
113
|
+
@http_client ||= Faraday.new(url: config.host) do |builder|
|
114
|
+
builder.options[:open_timeout] = config.open_timeout
|
115
|
+
builder.options[:timeout] = config.response_timeout
|
116
|
+
builder.request :multipart
|
117
|
+
builder.request :url_encoded
|
118
|
+
builder.adapter Faraday.default_adapter
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_io(data)
|
123
|
+
case data
|
124
|
+
when IO
|
125
|
+
data
|
126
|
+
when String
|
127
|
+
StringIO.new(data)
|
128
|
+
else
|
129
|
+
raise ArgumentError, 'file_or_binary is required IO class or String'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def ensure_content_type(options)
|
134
|
+
raise ArgumentError, "need options[:content_type] for #{options.inspect}" unless options[:content_type]
|
135
|
+
end
|
136
|
+
|
137
|
+
def handle_response(response)
|
138
|
+
raise APIError, "no response is returned" if response.nil?
|
139
|
+
|
140
|
+
case response.status
|
141
|
+
when HTTP_STATUS_OK, HTTP_STATUS_CREATED
|
142
|
+
JSON.parse(response.body)
|
143
|
+
else
|
144
|
+
raise APIError, "invalid http status code #{resp.status}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/sha2'
|
4
|
+
require 'securerandom'
|
5
|
+
module Resizing
|
6
|
+
#= Configuration class for Resizing client
|
7
|
+
#--
|
8
|
+
# usage.
|
9
|
+
# options = {
|
10
|
+
# host: 'https://www.resizing.net',
|
11
|
+
# project_id: '098a2a0d-0000-0000-0000-000000000000',
|
12
|
+
# secret_token: '4g1cshg......rbs6'
|
13
|
+
# }
|
14
|
+
# configuration = Resizing::Configuration.new(options)
|
15
|
+
# Resizing::Client.new(configuration)
|
16
|
+
#++
|
17
|
+
class Configuration
|
18
|
+
attr_reader :host, :project_id, :secret_token, :open_timeout, :response_timeout, :enable_mock
|
19
|
+
DEFAULT_HOST = 'https://www.resizing.net'
|
20
|
+
DEFAULT_OPEN_TIMEOUT = 2
|
21
|
+
DEFAULT_RESPONSE_TIMEOUT = 10
|
22
|
+
|
23
|
+
TRANSFORM_OPTIONS = %i[w width h height f format c crop q quality].freeze
|
24
|
+
|
25
|
+
def initialize(*attrs)
|
26
|
+
case attr = attrs.first
|
27
|
+
when Hash
|
28
|
+
raise_configiration_error if attr[:project_id].nil? || attr[:secret_token].nil?
|
29
|
+
|
30
|
+
initialize_by_hash attr
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
raise_configiration_error
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_auth_header
|
38
|
+
current_timestamp = Time.now.to_i
|
39
|
+
data = [current_timestamp, secret_token].join('|')
|
40
|
+
token = Digest::SHA2.hexdigest(data)
|
41
|
+
version = 'v1'
|
42
|
+
[version, current_timestamp, token].join(',')
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate_image_url(image_id, version_id = nil, transforms = [])
|
46
|
+
path = transformation_path(transforms)
|
47
|
+
version = if version_id.nil?
|
48
|
+
nil
|
49
|
+
else
|
50
|
+
"v#{version_id}"
|
51
|
+
end
|
52
|
+
|
53
|
+
parts = []
|
54
|
+
parts << image_id
|
55
|
+
parts << version if version
|
56
|
+
parts << path unless path.empty?
|
57
|
+
"#{host}/projects/#{project_id}/upload/images/#{parts.join('/')}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# this method should be divided other class
|
61
|
+
def transformation_path(transformations)
|
62
|
+
transformations = [transformations] if transformations.is_a? Hash
|
63
|
+
|
64
|
+
transformations.map do |transform|
|
65
|
+
transform.slice(*TRANSFORM_OPTIONS).map { |key, value| [key, value].join('_') }.join(',')
|
66
|
+
end.join('/')
|
67
|
+
end
|
68
|
+
|
69
|
+
# たぶんここにおくものではない
|
70
|
+
# もしくはキャッシュしない
|
71
|
+
def generate_identifier
|
72
|
+
@image_id ||= ::SecureRandom.uuid
|
73
|
+
|
74
|
+
"/projects/#{project_id}/upload/images/#{@image_id}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def ==(other)
|
78
|
+
return false unless self.class == other.class
|
79
|
+
|
80
|
+
%i[host project_id secret_token open_timeout response_timeout].all? do |name|
|
81
|
+
send(name) == other.send(name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def raise_configiration_error
|
88
|
+
raise ConfigurationError, 'need hash and some keys like :host, :project_id, :secret_token'
|
89
|
+
end
|
90
|
+
|
91
|
+
def initialize_by_hash(attr)
|
92
|
+
@host = attr[:host].dup.freeze || DEFAULT_HOST
|
93
|
+
@project_id = attr[:project_id].dup.freeze
|
94
|
+
@secret_token = attr[:secret_token].dup.freeze
|
95
|
+
@open_timeout = attr[:open_timeout] || DEFAULT_OPEN_TIMEOUT
|
96
|
+
@response_timeout = attr[:response_timeout] || DEFAULT_RESPONSE_TIMEOUT
|
97
|
+
@enable_mock = attr[:enable_mock] || false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|