uploadcare-ruby 1.2.1 → 3.0.3

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.
Files changed (98) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/gem-push.yml +20 -0
  3. data/.github/workflows/ruby.yml +52 -0
  4. data/.gitignore +13 -5
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +20 -0
  7. data/.yardopts +4 -0
  8. data/CHANGELOG.md +25 -24
  9. data/DEVELOPMENT.md +18 -0
  10. data/Gemfile +2 -0
  11. data/LICENSE +1 -1
  12. data/README.md +191 -519
  13. data/Rakefile +6 -4
  14. data/bin/console +15 -0
  15. data/bin/setup +8 -0
  16. data/lib/uploadcare.rb +36 -26
  17. data/lib/uploadcare/api/api.rb +25 -0
  18. data/lib/uploadcare/client/file_client.rb +44 -0
  19. data/lib/uploadcare/client/file_list_client.rb +46 -0
  20. data/lib/uploadcare/client/group_client.rb +45 -0
  21. data/lib/uploadcare/client/multipart_upload/chunks_client.rb +46 -0
  22. data/lib/uploadcare/client/multipart_upload_client.rb +62 -0
  23. data/lib/uploadcare/client/project_client.rb +18 -0
  24. data/lib/uploadcare/client/rest_client.rb +73 -0
  25. data/lib/uploadcare/client/rest_group_client.rb +23 -0
  26. data/lib/uploadcare/client/upload_client.rb +35 -0
  27. data/lib/uploadcare/client/uploader_client.rb +93 -0
  28. data/lib/uploadcare/client/webhook_client.rb +43 -0
  29. data/lib/uploadcare/concern/error_handler.rb +54 -0
  30. data/lib/uploadcare/concern/throttle_handler.rb +25 -0
  31. data/lib/uploadcare/concern/upload_error_handler.rb +32 -0
  32. data/lib/uploadcare/entity/decorator/paginator.rb +81 -0
  33. data/lib/uploadcare/entity/entity.rb +18 -0
  34. data/lib/uploadcare/entity/file.rb +81 -0
  35. data/lib/uploadcare/entity/file_list.rb +30 -0
  36. data/lib/uploadcare/entity/group.rb +41 -0
  37. data/lib/uploadcare/entity/group_list.rb +24 -0
  38. data/lib/uploadcare/entity/project.rb +13 -0
  39. data/lib/uploadcare/entity/uploader.rb +73 -0
  40. data/lib/uploadcare/entity/webhook.rb +14 -0
  41. data/lib/uploadcare/exception/request_error.rb +9 -0
  42. data/lib/uploadcare/exception/throttle_error.rb +14 -0
  43. data/lib/uploadcare/param/authentication_header.rb +25 -0
  44. data/lib/uploadcare/param/param.rb +10 -0
  45. data/lib/uploadcare/param/secure_auth_header.rb +37 -0
  46. data/lib/uploadcare/param/simple_auth_header.rb +14 -0
  47. data/lib/uploadcare/param/upload/signature_generator.rb +24 -0
  48. data/lib/uploadcare/param/upload/upload_params_generator.rb +23 -0
  49. data/lib/uploadcare/param/user_agent.rb +21 -0
  50. data/lib/uploadcare/ruby/version.rb +5 -0
  51. data/uploadcare-ruby.gemspec +50 -36
  52. metadata +112 -96
  53. data/.travis.yml +0 -12
  54. data/lib/uploadcare/api.rb +0 -24
  55. data/lib/uploadcare/api/file_api.rb +0 -7
  56. data/lib/uploadcare/api/file_list_api.rb +0 -8
  57. data/lib/uploadcare/api/group_api.rb +0 -38
  58. data/lib/uploadcare/api/group_list_api.rb +0 -8
  59. data/lib/uploadcare/api/project_api.rb +0 -9
  60. data/lib/uploadcare/api/raw_api.rb +0 -44
  61. data/lib/uploadcare/api/uploading_api.rb +0 -110
  62. data/lib/uploadcare/errors/errors.rb +0 -64
  63. data/lib/uploadcare/resources/file.rb +0 -164
  64. data/lib/uploadcare/resources/file_list.rb +0 -41
  65. data/lib/uploadcare/resources/group.rb +0 -115
  66. data/lib/uploadcare/resources/group_list.rb +0 -31
  67. data/lib/uploadcare/resources/project.rb +0 -13
  68. data/lib/uploadcare/rest/auth/auth.rb +0 -31
  69. data/lib/uploadcare/rest/auth/secure.rb +0 -43
  70. data/lib/uploadcare/rest/auth/simple.rb +0 -16
  71. data/lib/uploadcare/rest/connections/api_connection.rb +0 -32
  72. data/lib/uploadcare/rest/connections/upload_connection.rb +0 -21
  73. data/lib/uploadcare/rest/middlewares/auth_middleware.rb +0 -24
  74. data/lib/uploadcare/rest/middlewares/parse_json_middleware.rb +0 -33
  75. data/lib/uploadcare/rest/middlewares/raise_error_middleware.rb +0 -21
  76. data/lib/uploadcare/utils/parser.rb +0 -71
  77. data/lib/uploadcare/utils/user_agent.rb +0 -44
  78. data/lib/uploadcare/version.rb +0 -3
  79. data/spec/api/raw_api_spec.rb +0 -25
  80. data/spec/resources/file_list_spec.rb +0 -64
  81. data/spec/resources/file_spec.rb +0 -222
  82. data/spec/resources/group_list_spec.rb +0 -30
  83. data/spec/resources/group_spec.rb +0 -101
  84. data/spec/resources/operations_spec.rb +0 -59
  85. data/spec/resources/project_spec.rb +0 -21
  86. data/spec/rest/api_connection_spec.rb +0 -68
  87. data/spec/rest/auth/secure_spec.rb +0 -66
  88. data/spec/rest/auth/simple_spec.rb +0 -31
  89. data/spec/rest/errors_spec.rb +0 -75
  90. data/spec/rest/upload_connection_spec.rb +0 -19
  91. data/spec/spec_helper.rb +0 -41
  92. data/spec/uploadcare_spec.rb +0 -16
  93. data/spec/uploading/uploading_multiple_spec.rb +0 -43
  94. data/spec/uploading/uploading_spec.rb +0 -40
  95. data/spec/utils/parser_spec.rb +0 -87
  96. data/spec/utils/user_agent_spec.rb +0 -46
  97. data/spec/view.png +0 -0
  98. data/spec/view2.jpg +0 -0
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rest_client'
4
+
5
+ module Uploadcare
6
+ module Client
7
+ # @see https://uploadcare.com/api-refs/rest-api/v0.5.0/#tag/Group/paths/~1groups~1%3Cuuid%3E~1storage~1/put
8
+ class RestGroupClient < RestClient
9
+ # store all files in a group
10
+ # @see https://uploadcare.com/api-refs/rest-api/v0.5.0/#tag/Group/paths/~1groups~1%3Cuuid%3E~1storage~1/put
11
+ def store(uuid)
12
+ put(uri: "/groups/#{uuid}/storage/")
13
+ end
14
+
15
+ # return paginated list of groups
16
+ # @see https://uploadcare.com/api-refs/rest-api/v0.5.0/#operation/groupsList
17
+ def list(**options)
18
+ query = options.empty? ? '' : '?' + URI.encode_www_form(options)
19
+ get(uri: "/groups/#{query}")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'param/user_agent'
4
+ require 'uploadcare/concern/error_handler'
5
+ require 'uploadcare/concern/throttle_handler'
6
+
7
+ module Uploadcare
8
+ module Client
9
+ # @abstract
10
+ #
11
+ # Headers and helper methods for clients working with upload API
12
+ # @see https://uploadcare.com/docs/api_reference/upload/
13
+ class UploadClient < ApiStruct::Client
14
+ include Concerns::ErrorHandler
15
+ include Concerns::ThrottleHandler
16
+ include Exception
17
+
18
+ def api_root
19
+ Uploadcare.config.upload_api_root
20
+ end
21
+
22
+ def headers
23
+ {
24
+ 'User-Agent': Uploadcare::Param::UserAgent.call
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ def default_params
31
+ {}
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'upload_client'
4
+ require 'retries'
5
+ require 'param/upload/upload_params_generator'
6
+
7
+ module Uploadcare
8
+ module Client
9
+ # This is client for general uploads
10
+ #
11
+ # @see https://uploadcare.com/api-refs/upload-api/#tag/Upload
12
+ class UploaderClient < UploadClient
13
+ # @see https://uploadcare.com/api-refs/upload-api/#operation/baseUpload
14
+
15
+ def upload_many(arr, **options)
16
+ body = upload_many_body(arr, **options)
17
+ post(path: 'base/',
18
+ headers: { 'Content-type': body.content_type },
19
+ body: body)
20
+ end
21
+
22
+ # syntactic sugar for upload_many
23
+ # There is actual upload method for one file, but it is redundant
24
+
25
+ def upload(file, **options)
26
+ upload_many([file], **options)
27
+ end
28
+
29
+ # Upload files from url
30
+ # @see https://uploadcare.com/api-refs/upload-api/#operation/fromURLUpload
31
+ # options:
32
+ # - check_URL_duplicates
33
+ # - filename
34
+ # - save_URL_duplicates
35
+ # - async - returns upload token instead of upload data
36
+ def upload_from_url(url, **options)
37
+ body = upload_from_url_body(url, **options)
38
+ token_response = post(path: 'from_url/', headers: { 'Content-type': body.content_type }, body: body)
39
+ return token_response if options[:async]
40
+
41
+ uploaded_response = poll_upload_response(token_response.success[:token])
42
+ return uploaded_response if uploaded_response.success[:status] == 'error'
43
+
44
+ Dry::Monads::Success(files: [uploaded_response.success])
45
+ end
46
+
47
+ private
48
+
49
+ alias api_struct_post post
50
+ def post(**args)
51
+ handle_throttling { api_struct_post(**args) }
52
+ end
53
+
54
+ def poll_upload_response(token)
55
+ with_retries(max_tries: Uploadcare.config.max_request_tries,
56
+ base_sleep_seconds: Uploadcare.config.base_request_sleep,
57
+ max_sleep_seconds: Uploadcare.config.max_request_sleep) do
58
+ response = get_status_response(token)
59
+ raise RequestError if %w[progress waiting unknown].include?(response.success[:status])
60
+
61
+ response
62
+ end
63
+ end
64
+
65
+ # Check upload status
66
+ #
67
+ # @see https://uploadcare.com/api-refs/upload-api/#operation/fromURLUploadStatus
68
+ def get_status_response(token)
69
+ query_params = { token: token }
70
+ get(path: 'from_url/status/', params: query_params)
71
+ end
72
+
73
+ # Prepares body for upload_many method
74
+ def upload_many_body(arr, **options)
75
+ files_formdata = arr.map do |file|
76
+ [HTTP::FormData::File.new(file).filename,
77
+ HTTP::FormData::File.new(file)]
78
+ end .to_h
79
+ HTTP::FormData::Multipart.new(
80
+ Param::Upload::UploadParamsGenerator.call(options[:store]).merge(files_formdata)
81
+ )
82
+ end
83
+
84
+ # Prepare upload_from_url initial request body
85
+ def upload_from_url_body(url, **options)
86
+ HTTP::FormData::Multipart.new({
87
+ 'pub_key': Uploadcare.config.public_key,
88
+ 'source_url': url
89
+ }.merge(**options))
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rest_client'
4
+
5
+ module Uploadcare
6
+ module Client
7
+ # client for webhook management
8
+ # @see https://uploadcare.com/api-refs/rest-api/v0.5.0/#tag/Webhook
9
+ class WebhookClient < RestClient
10
+ # Create webhook
11
+ # @see https://uploadcare.com/docs/api_reference/rest/webhooks/#subscribe
12
+ def create(target_url, event: 'file.uploaded', is_active: true)
13
+ body = { 'target_url': target_url, 'event': event, 'is_active': is_active }.to_json
14
+ post(uri: '/webhooks/', content: body)
15
+ end
16
+
17
+ # Returns array (not paginated list) of webhooks
18
+ # @see https://uploadcare.com/docs/api_reference/rest/webhooks/#get-list
19
+ def list
20
+ get(uri: '/webhooks/')
21
+ end
22
+
23
+ # Permanently deletes subscription
24
+ # @see https://uploadcare.com/docs/api_reference/rest/webhooks/#unsubscribe
25
+ def delete(name)
26
+ body = { 'name': name }.to_json
27
+ post(uri: '/webhooks/unsubscribe/', content: body)
28
+ end
29
+
30
+ # Updates webhook
31
+ # @see https://uploadcare.com/docs/api_reference/rest/webhooks/#subscribe-update
32
+ def update(id, **options)
33
+ body = options.to_json
34
+ post(uri: "/webhooks/#{id}/", content: body)
35
+ end
36
+
37
+ alias create_webhook create
38
+ alias list_webhooks list
39
+ alias delete_webhook delete
40
+ alias update_webhook update
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uploadcare
4
+ module Concerns
5
+ # Wrapper for responses
6
+ # raises errors instead of returning monads
7
+ module ErrorHandler
8
+ include Exception
9
+
10
+ # Extension of ApiStruct's failure method
11
+ #
12
+ # Raises errors instead of returning falsey objects
13
+ # @see https://github.com/rubygarage/api_struct/blob/master/lib/api_struct/client.rb#L55
14
+ def failure(response)
15
+ catch_upload_errors(response)
16
+ parsed_response = JSON.parse(response.body.to_s)
17
+ raise RequestError, parsed_response['detail']
18
+ rescue JSON::ParserError
19
+ raise RequestError, response.status
20
+ end
21
+
22
+ # Extension of ApiStruct's wrap method
23
+ #
24
+ # Catches throttling errors and Upload API errors
25
+ #
26
+ # @see https://github.com/rubygarage/api_struct/blob/master/lib/api_struct/client.rb#L45
27
+ def wrap(response)
28
+ raise_throttling_error(response) if response.status == 429
29
+ return failure(response) if response.status >= 300
30
+
31
+ catch_upload_errors(response)
32
+ success(response)
33
+ end
34
+
35
+ private
36
+
37
+ # Raise ThrottleError. Also, tells in error when server will be ready for next request
38
+ def raise_throttling_error(response)
39
+ retry_after = response.headers['Retry-After'].to_i + 1 || 11
40
+ raise ThrottleError.new(retry_after), "Response throttled, retry #{retry_after} seconds later"
41
+ end
42
+
43
+ # Upload API returns its errors with code 200, and stores its actual code and details within response message
44
+ # This methods detects that and raises apropriate error
45
+ def catch_upload_errors(response)
46
+ return unless response.code == 200
47
+
48
+ parsed_response = JSON.parse(response.body.to_s)
49
+ error = parsed_response['error'] if parsed_response.is_a?(Hash)
50
+ raise RequestError, error if error
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uploadcare
4
+ module Concerns
5
+ # This module lets clients send request multiple times if request is throttled
6
+ module ThrottleHandler
7
+ # call given block. If ThrottleError is returned, it will wait and attempt again 4 more times
8
+ # @yield executable block (HTTP request that may be throttled)
9
+ def handle_throttling
10
+ (Uploadcare.config.max_throttle_attempts - 1).times do
11
+ # rubocop:disable Style/RedundantBegin
12
+ begin
13
+ return yield
14
+ rescue(Exception::ThrottleError) => e
15
+ wait_time = e.timeout
16
+ sleep(wait_time)
17
+ next
18
+ end
19
+ # rubocop:enable Style/RedundantBegin
20
+ end
21
+ yield
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uploadcare
4
+ module Concerns
5
+ # Wrapper for responses
6
+ # raises errors instead of returning monads
7
+ module UploadErrorHandler
8
+ include Exception
9
+
10
+ # Extension of ApiStruct's failure method
11
+ #
12
+ # Raises errors instead of returning falsey objects
13
+ # @see https://github.com/rubygarage/api_struct/blob/master/lib/api_struct/client.rb#L55
14
+ def failure(response)
15
+ catch_throttling_error(response)
16
+ parsed_response = JSON.parse(response.body.to_s)
17
+ raise RequestError, parsed_response['detail']
18
+ rescue JSON::ParserError
19
+ raise RequestError, response.status
20
+ end
21
+
22
+ private
23
+
24
+ def catch_throttling_error(response)
25
+ return unless response.code == 429
26
+
27
+ retry_after = response.headers['Retry-After'].to_i + 1 || 11
28
+ raise ThrottleError.new(retry_after), "Response throttled, retry #{retry_after} seconds later"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uploadcare
4
+ module Entity
5
+ # @abstract
6
+ module Decorator
7
+ # provides pagination methods for things in Uploadcare that paginate,
8
+ # namely [FileList] and [Group]
9
+ #
10
+ # Requirements:
11
+ # - Should be Entity with Client
12
+ # - Associated Client should have `list` method that returns objects with pagination
13
+ # - Response should have :next, :previous, :total, :per_page params and :results fields
14
+ module Paginator
15
+ @entity ||= Hashie::Mash.new
16
+
17
+ # meta data of a pagination object
18
+ def meta
19
+ Hashie::Mash.new(next: @entity[:next], previous: @entity[:previous],
20
+ total: @entity[:total], per_page: @entity[:per_page])
21
+ end
22
+
23
+ # Returns new instance of current object on next page
24
+ def next_page
25
+ url = @entity[:next]
26
+ return unless url
27
+
28
+ query = URI.decode_www_form(URI(url).query).to_h
29
+ query = Hash[query.map { |k, v| [k.to_sym, v] }]
30
+ self.class.list(**query)
31
+ end
32
+
33
+ # Returns new instance of current object on previous page
34
+ def previous_page
35
+ url = @entity[:previous]
36
+ return unless url
37
+
38
+ query = URI.decode_www_form(URI(url).query).to_h
39
+ query = Hash[query.map { |k, v| [k.to_sym, v] }]
40
+ self.class.list(**query)
41
+ end
42
+
43
+ # Attempts to load the entire list after offset into results of current object
44
+ #
45
+ # It's possible to avoid loading objects on previous pages by offsetting them first
46
+ def load
47
+ return if @entity[:next].nil? || @entity[:results].length == @entity[:total]
48
+
49
+ np = self
50
+ until np.next.nil?
51
+ np = np.next_page
52
+ @entity[:results].concat(np.results.map(&:to_h))
53
+ end
54
+ @entity[:next] = nil
55
+ @entity[:per_page] = @entity[:total]
56
+ self
57
+ end
58
+
59
+ # iterate through pages, starting with current one
60
+ #
61
+ # @yield [Block]
62
+ def each
63
+ current_page = self
64
+ while current_page
65
+ current_page.results.each do |obj|
66
+ yield obj
67
+ end
68
+ current_page = current_page.next_page
69
+ end
70
+ end
71
+
72
+ # Load and return all objects in list
73
+ #
74
+ # @return [Array]
75
+ def all
76
+ load[:results]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem.find_files('client/**/*.rb').each { |path| require path }
4
+
5
+ module Uploadcare
6
+ # Entities represent objects existing in Uploadcare cloud
7
+ #
8
+ # Typically, Entities inherit class methods from {Client} instance methods
9
+ # @see Client
10
+ module Entity
11
+ # @abstract
12
+ class Entity < ApiStruct::Entity
13
+ include Client
14
+ end
15
+ end
16
+
17
+ include Entity
18
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uploadcare
4
+ module Entity
5
+ # This serializer returns a single file
6
+ #
7
+ # @see https://uploadcare.com/docs/api_reference/rest/handling_projects/
8
+ class File < Entity
9
+ client_service FileClient
10
+
11
+ attr_entity :datetime_removed, :datetime_stored, :datetime_uploaded, :image_info, :is_image, :is_ready,
12
+ :mime_type, :original_file_url, :original_filename, :size, :url, :uuid
13
+
14
+ # gets file's uuid - even if it's only initialized with url
15
+ # @return [String]
16
+ def uuid
17
+ return @entity.uuid if @entity.uuid
18
+
19
+ uuid = @entity.url.gsub('https://ucarecdn.com/', '')
20
+ uuid = uuid.gsub(%r{\/.*}, '')
21
+ uuid
22
+ end
23
+
24
+ # loads file metadata, if it's initialized with url or uuid
25
+ def load
26
+ initialize(File.info(uuid).entity)
27
+ end
28
+
29
+ # 'copy' method is used to copy original files or their modified versions to default storage.
30
+ #
31
+ # Source files MAY either be stored or just uploaded and MUST NOT be deleted.
32
+ #
33
+ # @param [String] source uuid or uploadcare link to file.
34
+ # @param [Hash] args
35
+ # @option args [Boolean] :store Whether to store the file
36
+ # @option args [Boolean] :strip_operations Copies file without transformations (if source has them)
37
+ # @option args [String] :target points to a target custom storage.
38
+ # @option args [Boolean] :make_public make files on custom storage available via public links.
39
+ # @option args [String] :pattern define file naming pattern for the custom storage scenario.
40
+ #
41
+ # @see https://uploadcare.com/api-refs/rest-api/v0.5.0/#operation/copyFile
42
+ def self.copy(source, **args)
43
+ response = FileClient.new.copy(source: source, **args).success[:result]
44
+ File.new(response)
45
+ end
46
+
47
+ # Copies file to current project
48
+ #
49
+ # source can be UID or full CDN link
50
+ #
51
+ # @see .copy
52
+ def self.local_copy(source, **args)
53
+ File.copy(source, **args)
54
+ end
55
+
56
+ # copy file to different project
57
+ #
58
+ # source can be UID or full CDN link
59
+ #
60
+ # @see .copy
61
+ def self.remote_copy(source, target, **args)
62
+ File.copy(source: source, target: target, **args)
63
+ end
64
+
65
+ # Instance version of #{copy}. Copies current file.
66
+ def copy(**args)
67
+ File.copy(uuid, **args)
68
+ end
69
+
70
+ # Instance version of {internal_copy}
71
+ def local_copy(**args)
72
+ File.local_copy(uuid, **args)
73
+ end
74
+
75
+ # Instance version of {external_copy}
76
+ def remote_copy(target, **args)
77
+ File.copy(uuid, target: target, **args)
78
+ end
79
+ end
80
+ end
81
+ end