diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c5cad77..26495ca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 4.4.0 — 2024-03-09 + +### Breaking + +* Drop support of unmaintainable Ruby versions < 3.x. + +### Fixed + +* Update locations where Dry::Monads structure has changed. +* Sign URL uploads if configured (#139). +* Start returning proper error message when raising RequestError in poll_upload_response, to hint to users what is going on. Fixes #141. +* When polling, raise if an error is returned (#142). +* Fix documentation about original file url on simple file upload. + +### Changed +* Support params in Rest client and in file info method, to allow passing custom params like "include=appdata" in `Uploadcare::File.file` calls. Closes #132. + + ## 4.3.6 — 2023-11-18 ### Fixed diff --git a/lib/uploadcare.rb b/lib/uploadcare.rb index 72db20eb..60e84c0d 100644 --- a/lib/uploadcare.rb +++ b/lib/uploadcare.rb @@ -6,6 +6,7 @@ # Exceptions require 'exception/throttle_error' require 'exception/request_error' +require 'exception/retry_error' # Entities require 'entity/entity' diff --git a/lib/uploadcare/client/uploader_client.rb b/lib/uploadcare/client/uploader_client.rb index 51b04a4a..b0424c33 100644 --- a/lib/uploadcare/client/uploader_client.rb +++ b/lib/uploadcare/client/uploader_client.rb @@ -3,6 +3,7 @@ require_relative 'upload_client' require 'retries' require 'param/upload/upload_params_generator' +require 'param/upload/signature_generator' module Uploadcare module Client @@ -75,15 +76,22 @@ def post(args = {}) def poll_upload_response(token) with_retries(max_tries: Uploadcare.config.max_request_tries, base_sleep_seconds: Uploadcare.config.base_request_sleep, - max_sleep_seconds: Uploadcare.config.max_request_sleep) do + max_sleep_seconds: Uploadcare.config.max_request_sleep, + rescue: RetryError) do response = get_upload_from_url_status(token) + handle_polling_response(response) + end + end - if %w[progress waiting unknown].include?(response.success[:status]) - raise RequestError, 'Upload is taking longer than expected. Try increasing the max_request_tries config if you know your file uploads will take more time.' # rubocop:disable Layout/LineLength - end - - response + def handle_polling_response(response) + case response.success[:status] + when 'error' + raise RequestError, response.success[:error] + when 'progress', 'waiting', 'unknown' + raise RetryError, response.success[:error] || 'Upload is taking longer than expected. Try increasing the max_request_tries config if you know your file uploads will take more time.' # rubocop:disable Layout/LineLength end + + response end # Prepares body for upload_many method @@ -99,13 +107,13 @@ def upload_many_body(arr, options = {}) # Prepare upload_from_url initial request body def upload_from_url_body(url, options = {}) - HTTP::FormData::Multipart.new( - options.merge( - 'pub_key' => Uploadcare.config.public_key, - 'source_url' => url, - 'store' => store_value(options[:store]) - ) - ) + opts = { + 'pub_key' => Uploadcare.config.public_key, + 'source_url' => url, + 'store' => store_value(options[:store]) + } + opts.merge!(Param::Upload::SignatureGenerator.call) if Uploadcare.config.sign_uploads + HTTP::FormData::Multipart.new(options.merge(opts)) end def store_value(store) diff --git a/lib/uploadcare/exception/request_error.rb b/lib/uploadcare/exception/request_error.rb index a949e44a..ca23bfc3 100644 --- a/lib/uploadcare/exception/request_error.rb +++ b/lib/uploadcare/exception/request_error.rb @@ -3,7 +3,6 @@ module Uploadcare module Exception # Standard error for invalid API responses - class RequestError < StandardError - end + class RequestError < StandardError; end end end diff --git a/lib/uploadcare/exception/retry_error.rb b/lib/uploadcare/exception/retry_error.rb new file mode 100644 index 00000000..ac05439b --- /dev/null +++ b/lib/uploadcare/exception/retry_error.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Uploadcare + module Exception + # Standard error to raise when needing to retry a request + class RetryError < StandardError; end + end +end diff --git a/lib/uploadcare/ruby/version.rb b/lib/uploadcare/ruby/version.rb index c3c4de84..ad4db9c6 100644 --- a/lib/uploadcare/ruby/version.rb +++ b/lib/uploadcare/ruby/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Uploadcare - VERSION = '4.3.6' + VERSION = '4.4.0' end diff --git a/spec/fixtures/vcr_cassettes/upload_upload_from_url_with_signature.yml b/spec/fixtures/vcr_cassettes/upload_upload_from_url_with_signature.yml new file mode 100644 index 00000000..0e05e641 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/upload_upload_from_url_with_signature.yml @@ -0,0 +1,116 @@ +--- +http_interactions: +- request: + method: post + uri: https://upload.uploadcare.com/from_url/ + body: + encoding: ASCII-8BIT + string: "-----------------------c5f0754b5a5d1bfb797b6944cc4c653cdf2b5c20ab\r\nContent-Disposition: + form-data; name=\"pub_key\"\r\n\r\ndemopublickey\r\n-----------------------c5f0754b5a5d1bfb797b6944cc4c653cdf2b5c20ab\r\nContent-Disposition: + form-data; name=\"source_url\"\r\n\r\nhttps://placekitten.com/2250/2250\r\n-----------------------c5f0754b5a5d1bfb797b6944cc4c653cdf2b5c20ab\r\nContent-Disposition: + form-data; name=\"store\"\r\n\r\nauto\r\n-----------------------c5f0754b5a5d1bfb797b6944cc4c653cdf2b5c20ab\r\nContent-Disposition: + form-data; name=\"signature\"\r\n\r\nf6b1a41383cb2179c57cd1baf967ace8\r\n-----------------------c5f0754b5a5d1bfb797b6944cc4c653cdf2b5c20ab\r\nContent-Disposition: + form-data; name=\"expire\"\r\n\r\n1701130306\r\n-----------------------c5f0754b5a5d1bfb797b6944cc4c653cdf2b5c20ab--\r\n" + headers: + User-Agent: + - UploadcareRuby/4.3.6/demopublickey (Ruby/3.0.5) + Content-Type: + - multipart/form-data; boundary=---------------------c5f0754b5a5d1bfb797b6944cc4c653cdf2b5c20ab + Connection: + - close + Host: + - upload.uploadcare.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Mon, 27 Nov 2023 23:41:47 GMT + Content-Type: + - application/json + Content-Length: + - '63' + Connection: + - close + Server: + - nginx + Vary: + - Accept-Encoding + - Origin + Access-Control-Allow-Origin: + - "*" + Access-Control-Allow-Methods: + - OPTIONS, GET, POST + Access-Control-Allow-Headers: + - DNT, X-UC-User-Agent, X-PINGOTHER + Access-Control-Max-Age: + - '1' + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Expose-Headers: + - Warning, Retry-After + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Uploadcare-Request-Id: + - ffc67f72-2b8f-46c9-8239-1da1de4c869a + body: + encoding: UTF-8 + string: '{"type":"token","token":"ff450eec-48ad-491c-bfb7-804698c78951"}' + recorded_at: Mon, 27 Nov 2023 23:41:47 GMT +- request: + method: get + uri: https://upload.uploadcare.com/from_url/status/?token=ff450eec-48ad-491c-bfb7-804698c78951 + body: + encoding: ASCII-8BIT + string: '' + headers: + User-Agent: + - UploadcareRuby/4.3.6/demopublickey (Ruby/3.0.5) + Connection: + - close + Host: + - upload.uploadcare.com + response: + status: + code: 200 + message: OK + headers: + Date: + - Mon, 27 Nov 2023 23:41:48 GMT + Content-Type: + - application/json + Content-Length: + - '726' + Connection: + - close + Server: + - nginx + Vary: + - Accept-Encoding + - Origin + Access-Control-Allow-Origin: + - "*" + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS + Access-Control-Allow-Headers: + - DNT, X-PINGOTHER, X-UC-User-Agent + Access-Control-Max-Age: + - '1' + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Expose-Headers: + - Warning, Retry-After + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Uploadcare-Request-Id: + - 176cc03a-0df5-4a45-8ff1-b5a394af8b7d + body: + encoding: UTF-8 + string: '{"size":297261,"total":297261,"done":297261,"uuid":"1a4844c6-2463-440a-80a8-219e3d00e22a","file_id":"1a4844c6-2463-440a-80a8-219e3d00e22a","original_filename":"2250","is_image":true,"is_stored":true,"image_info":{"dpi":[96,96],"width":2250,"format":"JPEG","height":2250,"sequence":false,"color_mode":"RGB","orientation":null,"geo_location":null,"datetime_original":null},"video_info":null,"content_info":{"mime":{"mime":"image/jpeg","type":"image","subtype":"jpeg"},"image":{"dpi":[96,96],"width":2250,"format":"JPEG","height":2250,"sequence":false,"color_mode":"RGB","orientation":null,"geo_location":null,"datetime_original":null}},"is_ready":true,"filename":"2250","mime_type":"image/jpeg","metadata":{},"status":"success"}' + recorded_at: Mon, 27 Nov 2023 23:41:48 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/uploadcare/client/uploader_client_spec.rb b/spec/uploadcare/client/uploader_client_spec.rb index 7f2c57e8..d4bc797f 100644 --- a/spec/uploadcare/client/uploader_client_spec.rb +++ b/spec/uploadcare/client/uploader_client_spec.rb @@ -5,11 +5,12 @@ module Uploadcare module Client RSpec.describe UploaderClient do - subject { UploaderClient.new } - let!(:file) { ::File.open('spec/fixtures/kitten.jpeg') } - let!(:another_file) { ::File.open('spec/fixtures/another_kitten.jpeg') } + subject { described_class.new } describe 'upload' do + let(:file) { ::File.open('spec/fixtures/kitten.jpeg') } + let(:another_file) { ::File.open('spec/fixtures/another_kitten.jpeg') } + it 'uploads a file' do VCR.use_cassette('upload_upload') do response = subject.upload(file, metadata: { subsystem: 'test' }) diff --git a/spec/uploadcare/entity/uploader_spec.rb b/spec/uploadcare/entity/uploader_spec.rb index c475ca15..80f683a2 100644 --- a/spec/uploadcare/entity/uploader_spec.rb +++ b/spec/uploadcare/entity/uploader_spec.rb @@ -2,106 +2,131 @@ require 'spec_helper' -module Uploadcare - module Entity - RSpec.describe Uploader do - subject { Uploadcare::Entity::Uploader } - let!(:file) { ::File.open('spec/fixtures/kitten.jpeg') } - let!(:another_file) { ::File.open('spec/fixtures/another_kitten.jpeg') } - let!(:big_file) { ::File.open('spec/fixtures/big.jpeg') } - - describe 'upload_many' do - it 'returns a hash of filenames and uids', :aggregate_failures do - VCR.use_cassette('upload_upload_many') do - uploads_list = subject.upload([file, another_file]) - expect(uploads_list.length).to eq 2 - first_upload = uploads_list.first - expect(first_upload.original_filename).not_to be_empty - expect(first_upload.uuid).not_to be_empty - end +RSpec.describe Uploadcare::Entity::Uploader do + subject { Uploadcare::Entity::Uploader } + let!(:file) { File.open('spec/fixtures/kitten.jpeg') } + let!(:another_file) { File.open('spec/fixtures/another_kitten.jpeg') } + let!(:big_file) { File.open('spec/fixtures/big.jpeg') } + + describe 'upload_many' do + it 'returns a hash of filenames and uids', :aggregate_failures do + VCR.use_cassette('upload_upload_many') do + uploads_list = subject.upload([file, another_file]) + expect(uploads_list.length).to eq 2 + first_upload = uploads_list.first + expect(first_upload.original_filename).not_to be_empty + expect(first_upload.uuid).not_to be_empty + end + end + + describe 'upload_one' do + it 'returns a file', :aggregate_failures do + VCR.use_cassette('upload_upload_one') do + upload = subject.upload(file) + expect(upload).to be_kind_of(Uploadcare::Entity::File) + expect(file.path).to end_with(upload.original_filename.to_s) + expect(file.size).to eq(upload.size) end + end - describe 'upload_one' do - it 'returns a file', :aggregate_failures do - VCR.use_cassette('upload_upload_one') do - upload = subject.upload(file) - expect(upload).to be_kind_of(Uploadcare::Entity::File) - expect(file.path).to end_with(upload.original_filename.to_s) - expect(file.size).to eq(upload.size) - end - end + context 'when the secret key is missing' do + it 'returns a file without details', :aggregate_failures do + Uploadcare.config.secret_key = nil - context 'when the secret key is missing' do - it 'returns a file without details', :aggregate_failures do - Uploadcare.config.secret_key = nil - - VCR.use_cassette('upload_upload_one_without_secret_key') do - upload = subject.upload(file) - expect(upload).to be_kind_of(Uploadcare::Entity::File) - expect(file.path).to end_with(upload.original_filename.to_s) - expect(file.size).to eq(upload.size) - end - end + VCR.use_cassette('upload_upload_one_without_secret_key') do + upload = subject.upload(file) + expect(upload).to be_kind_of(Uploadcare::Entity::File) + expect(file.path).to end_with(upload.original_filename.to_s) + expect(file.size).to eq(upload.size) end end + end + end - describe 'upload_from_url' do - it 'polls server and returns array of files' do - VCR.use_cassette('upload_upload_from_url') do - url = 'https://placekitten.com/2250/2250' - upload = subject.upload(url) - expect(upload[0]).to be_kind_of(Uploadcare::Entity::File) - end - end + describe 'upload_from_url' do + let(:url) { 'https://placekitten.com/2250/2250' } - it 'raises error with information if file upload takes time' do - Uploadcare.config.max_request_tries = 1 - VCR.use_cassette('upload_upload_from_url') do - url = 'https://placekitten.com/2250/2250' - error_str = 'Upload is taking longer than expected. Try increasing the max_request_tries config if you know your file uploads will take more time.' # rubocop:disable Layout/LineLength - expect { subject.upload(url) }.to raise_error(RequestError, error_str) - end - end + before do + allow(HTTP::FormData::Multipart).to receive(:new).and_call_original + end + + it 'polls server and returns array of files' do + VCR.use_cassette('upload_upload_from_url') do + upload = subject.upload(url) + expect(upload[0]).to be_kind_of(Uploadcare::Entity::File) + expect(HTTP::FormData::Multipart).to have_received(:new).with( + a_hash_including( + 'source_url' => url + ) + ) end + end - describe 'multipart_upload' do - let!(:some_var) { nil } - - it 'uploads a file', :aggregate_failures do - VCR.use_cassette('upload_multipart_upload') do - # Minimal size for file to be valid for multipart upload is 10 mb - Uploadcare.config.multipart_size_threshold = 10 * 1024 * 1024 - expect(some_var).to receive(:to_s).at_least(:once).and_call_original - file = subject.multipart_upload(big_file) { some_var } - expect(file).to be_kind_of(Uploadcare::Entity::File) - expect(file.uuid).not_to be_empty - end - end + context 'when signed uploads are enabled' do + before do + allow(Uploadcare.config).to receive(:sign_uploads).and_return(true) end - describe 'get_upload_from_url_status' do - it 'gets a status of upload-from-URL' do - VCR.use_cassette('upload_get_upload_from_url_status') do - token = '0313e4e2-f2ca-4564-833b-4f71bc8cba27' - status_info = subject.get_upload_from_url_status(token).success - expect(status_info[:status]).to eq 'success' - end + it 'includes signature' do + VCR.use_cassette('upload_upload_from_url_with_signature') do + upload = subject.upload(url) + expect(upload[0]).to be_kind_of(Uploadcare::Entity::File) + expect(HTTP::FormData::Multipart).to have_received(:new).with( + a_hash_including( + signature: instance_of(String), + expire: instance_of(Integer) + ) + ) end end end - describe 'file_info' do - it 'returns file info without the secret key', :aggregate_failures do - uuid = 'a7f9751a-432b-4b05-936c-2f62d51d255d' + it 'raises error with information if file upload takes time' do + Uploadcare.config.max_request_tries = 1 + VCR.use_cassette('upload_upload_from_url') do + url = 'https://placekitten.com/2250/2250' + error_str = 'Upload is taking longer than expected. Try increasing the max_request_tries config if you know your file uploads will take more time.' # rubocop:disable Layout/LineLength + expect { subject.upload(url) }.to raise_error(RetryError, error_str) + end + end + end - VCR.use_cassette('upload_file_info') do - file_info = subject.file_info(uuid).success - expect(file_info[:original_filename]).not_to be_empty - expect(file_info[:size]).to be >= 0 - expect(file_info[:uuid]).to eq uuid - end + describe 'multipart_upload' do + let!(:some_var) { nil } + + it 'uploads a file', :aggregate_failures do + VCR.use_cassette('upload_multipart_upload') do + # Minimal size for file to be valid for multipart upload is 10 mb + Uploadcare.config.multipart_size_threshold = 10 * 1024 * 1024 + expect(some_var).to receive(:to_s).at_least(:once).and_call_original + file = subject.multipart_upload(big_file) { some_var } + expect(file).to be_kind_of(Uploadcare::Entity::File) + expect(file.uuid).not_to be_empty + end + end + end + + describe 'get_upload_from_url_status' do + it 'gets a status of upload-from-URL' do + VCR.use_cassette('upload_get_upload_from_url_status') do + token = '0313e4e2-f2ca-4564-833b-4f71bc8cba27' + status_info = subject.get_upload_from_url_status(token).success + expect(status_info[:status]).to eq 'success' end end end end + + describe 'file_info' do + it 'returns file info without the secret key', :aggregate_failures do + uuid = 'a7f9751a-432b-4b05-936c-2f62d51d255d' + + VCR.use_cassette('upload_file_info') do + file_info = subject.file_info(uuid).success + expect(file_info[:original_filename]).not_to be_empty + expect(file_info[:size]).to be >= 0 + expect(file_info[:uuid]).to eq uuid + end + end + end end