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.
@@ -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