sdr-client 2.12.0 → 2.13.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.rubocop_todo.yml +24 -20
  4. data/Gemfile.lock +11 -8
  5. data/exe/sdr_redesigned +10 -0
  6. data/lib/sdr_client/redesigned_client/authenticator.rb +40 -0
  7. data/lib/sdr_client/redesigned_client/cli/config.rb +32 -0
  8. data/lib/sdr_client/redesigned_client/cli/credentials.rb +35 -0
  9. data/lib/sdr_client/redesigned_client/cli/update.rb +186 -0
  10. data/lib/sdr_client/redesigned_client/cli.rb +198 -0
  11. data/lib/sdr_client/redesigned_client/create_resource.rb +71 -0
  12. data/lib/sdr_client/redesigned_client/deposit.rb +115 -0
  13. data/lib/sdr_client/redesigned_client/direct_upload_request.rb +45 -0
  14. data/lib/sdr_client/redesigned_client/direct_upload_response.rb +9 -0
  15. data/lib/sdr_client/redesigned_client/file.rb +100 -0
  16. data/lib/sdr_client/redesigned_client/file_set.rb +53 -0
  17. data/lib/sdr_client/redesigned_client/file_type_file_set_strategy.rb +13 -0
  18. data/lib/sdr_client/redesigned_client/find.rb +42 -0
  19. data/lib/sdr_client/redesigned_client/image_file_set_strategy.rb +13 -0
  20. data/lib/sdr_client/redesigned_client/job_status.rb +74 -0
  21. data/lib/sdr_client/redesigned_client/matching_file_grouping_strategy.rb +19 -0
  22. data/lib/sdr_client/redesigned_client/metadata.rb +64 -0
  23. data/lib/sdr_client/redesigned_client/operations/md5.rb +16 -0
  24. data/lib/sdr_client/redesigned_client/operations/mime_type.rb +17 -0
  25. data/lib/sdr_client/redesigned_client/operations/sha1.rb +16 -0
  26. data/lib/sdr_client/redesigned_client/request_builder.rb +171 -0
  27. data/lib/sdr_client/redesigned_client/single_file_grouping_strategy.rb +14 -0
  28. data/lib/sdr_client/redesigned_client/structural_grouper.rb +72 -0
  29. data/lib/sdr_client/redesigned_client/structural_metadata_builder.rb +51 -0
  30. data/lib/sdr_client/redesigned_client/unexpected_response.rb +25 -0
  31. data/lib/sdr_client/redesigned_client/update_dro_with_file_identifiers.rb +35 -0
  32. data/lib/sdr_client/redesigned_client/update_resource.rb +61 -0
  33. data/lib/sdr_client/redesigned_client/upload_files.rb +71 -0
  34. data/lib/sdr_client/redesigned_client/upload_files_metadata_builder.rb +40 -0
  35. data/lib/sdr_client/redesigned_client.rb +192 -0
  36. data/lib/sdr_client/version.rb +1 -1
  37. data/lib/sdr_client.rb +3 -1
  38. metadata +35 -3
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # The file uploading part of a deposit
6
+ class UploadFiles
7
+ # @param [Hash<String,DirectUploadRequest>] file_metadata map of relative filepaths to file metadata
8
+ # @param [Hash<String,String>] filepath_map map of relative filepaths to absolute filepaths
9
+ def self.upload(file_metadata:, filepath_map:)
10
+ new(file_metadata: file_metadata, filepath_map: filepath_map).upload
11
+ end
12
+
13
+ # @param [Hash<String,DirectUploadRequest>] file_metadata map of relative filepaths to file metadata
14
+ # @param [Hash<String,String>] filepath_map map of relative filepaths to absolute filepaths
15
+ def initialize(file_metadata:, filepath_map:)
16
+ @file_metadata = file_metadata
17
+ @filepath_map = filepath_map
18
+ end
19
+
20
+ # @return [Array<DirectUploadResponse>] the responses from the server for the uploads
21
+ def upload
22
+ file_metadata.map do |filepath, metadata|
23
+ direct_upload(metadata.to_json).tap do |response|
24
+ # ActiveStorage modifies the filename provided in response, so setting here with the relative filename
25
+ response.filename = filepath
26
+ upload_file(response)
27
+ logger.info("Upload of #{filepath} complete")
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :file_metadata, :filepath_map
35
+
36
+ def logger
37
+ SdrClient::RedesignedClient.config.logger
38
+ end
39
+
40
+ def client
41
+ SdrClient::RedesignedClient.instance
42
+ end
43
+
44
+ def path
45
+ '/v1/direct_uploads'
46
+ end
47
+
48
+ def direct_upload(metadata_json)
49
+ logger.info("Starting an upload request: #{metadata_json}")
50
+ response = client.post(path: path, body: metadata_json)
51
+
52
+ logger.info("Response from server: #{response}")
53
+ DirectUploadResponse.new(response)
54
+ end
55
+
56
+ def upload_file(response)
57
+ logger.info("Uploading `#{response.filename}' to #{response.direct_upload.fetch('url')}")
58
+
59
+ client.put(
60
+ path: response.direct_upload.fetch('url'),
61
+ body: ::File.open(filepath_map[response.filename]),
62
+ headers: {
63
+ 'content-type' => response.content_type,
64
+ 'content-length' => response.byte_size.to_s
65
+ },
66
+ expected_status: 204
67
+ )
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Collecting all the metadata about the files for a deposit
6
+ class UploadFilesMetadataBuilder
7
+ # @param [Array<String>] files a list of relative filepaths to upload
8
+ # @param [Hash<String,String>] mime_types a map of filenames to mime types
9
+ # @param [String] basepath path to which files are relative
10
+ # @return [Hash<String, DirectUploadRequest>] the metadata for uploading the files
11
+ def self.build(files:, mime_types:, basepath:)
12
+ new(files: files, mime_types: mime_types, basepath: basepath).build
13
+ end
14
+
15
+ # @param [Array<String>] files a list of absolute filepaths to upload
16
+ # @param [Hash<String,String>] mime_types a map of filenames to mime types
17
+ # @param [String] basepath path to which files are relative
18
+ def initialize(files:, mime_types:, basepath:)
19
+ @files = files
20
+ @mime_types = mime_types
21
+ @basepath = basepath
22
+ end
23
+
24
+ attr_reader :files, :mime_types, :basepath
25
+
26
+ # @return [Hash<String, DirectUploadRequest>] the metadata for uploading the files
27
+ def build
28
+ files.each_with_object({}) do |filepath, obj|
29
+ obj[filepath] = DirectUploadRequest.from_file(absolute_filepath_for(filepath),
30
+ file_name: filepath,
31
+ content_type: mime_types[filepath])
32
+ end
33
+ end
34
+
35
+ def absolute_filepath_for(filepath)
36
+ ::File.join(basepath, filepath)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'logger'
5
+ require 'shellwords'
6
+ require 'singleton'
7
+ require 'timeout'
8
+
9
+ module SdrClient
10
+ # The SDR client reimagined, built using patterns successfully used in other client gems we maintain
11
+ class RedesignedClient
12
+ include Singleton
13
+
14
+ class << self
15
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
16
+ def configure(url:, email: nil, password: nil, token_refresher: nil, token: default_token,
17
+ request_options: default_request_options, logger: default_logger)
18
+ if email.blank? && password.blank? && !token_refresher.respond_to?(:call)
19
+ raise ArgumentError, 'email and password cannot be blank without a custom token refresher callable'
20
+ end
21
+
22
+ instance.config = Config.new(
23
+ token: token,
24
+ url: url,
25
+ email: email,
26
+ password: password,
27
+ request_options: request_options,
28
+ logger: logger,
29
+ token_refresher: token_refresher
30
+ )
31
+
32
+ instance
33
+ end
34
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
35
+
36
+ # For the initial token, use a dummy value to avoid hitting any APIs
37
+ # during configuration, allowing `with_token_refresh_when_unauthorized` to handle
38
+ # auto-magic token refreshing. Why not immediately get a valid token? Our apps
39
+ # commonly invoke client `.configure` methods in the initializer in all
40
+ # application environments, even those that are never expected to
41
+ # connect to production APIs, such as local development machines.
42
+ #
43
+ # NOTE: `nil` and blank string cannot be used as dummy values here as
44
+ # they lead to a malformed request to be sent, which triggers an
45
+ # exception not rescued by `with_token_refresh_when_unauthorized`
46
+ def default_token
47
+ 'a temporary dummy token to avoid hitting the API before it is needed'
48
+ end
49
+
50
+ def default_logger
51
+ Logger.new($stdout)
52
+ end
53
+
54
+ def default_request_options
55
+ {
56
+ read_timeout: default_timeout,
57
+ timeout: default_timeout
58
+ }
59
+ end
60
+
61
+ # NOTE: This is the number of seconds it roughly takes for H2 to
62
+ # successfully shunt ~10GB files over to SDR API
63
+ def default_timeout
64
+ 900
65
+ end
66
+
67
+ delegate :config, :connection, :deposit_model, :job_status, :find, :update_model, :build_and_deposit,
68
+ to: :instance
69
+ end
70
+
71
+ attr_accessor :config
72
+
73
+ def deposit_model(...)
74
+ Deposit.deposit_model(...)
75
+ end
76
+
77
+ def job_status(...)
78
+ JobStatus.new(...)
79
+ end
80
+
81
+ def find(...)
82
+ Find.run(...)
83
+ end
84
+
85
+ def update_model(...)
86
+ UpdateResource.run(...)
87
+ end
88
+
89
+ def build_and_deposit(...)
90
+ Metadata.deposit(...)
91
+ end
92
+
93
+ # Send an authenticated GET request
94
+ # @param path [String] the path to the SDR API request
95
+ def get(path:)
96
+ response = with_token_refresh_when_unauthorized do
97
+ connection.get(path)
98
+ end
99
+
100
+ UnexpectedResponse.call(response) unless response.success?
101
+
102
+ return nil if response.body.blank?
103
+
104
+ JSON.parse(response.body).with_indifferent_access
105
+ end
106
+
107
+ # Send an authenticated POST request
108
+ # @param path [String] the path to the SDR API request
109
+ # @param body [String] the body of the SDR API request
110
+ # @param headers [Hash] extra headers to add to the SDR API request
111
+ # @param expected_status [Integer] override if all 2xx statuses aren't success conditions
112
+ def post(path:, body:, headers: {}, expected_status: nil) # rubocop:disable Metrics/MethodLength
113
+ response = with_token_refresh_when_unauthorized do
114
+ connection.post(path) do |request|
115
+ request.body = body
116
+ request.headers = default_headers.merge(headers)
117
+ end
118
+ end
119
+
120
+ if expected_status
121
+ UnexpectedResponse.call(response) if response.status != expected_status
122
+ elsif !response.success?
123
+ UnexpectedResponse.call(response)
124
+ end
125
+
126
+ return nil if response.body.blank?
127
+
128
+ JSON.parse(response.body).with_indifferent_access
129
+ end
130
+
131
+ # Send an authenticated PUT request
132
+ # @param path [String] the path to the SDR API request
133
+ # @param body [String] the body of the SDR API request
134
+ # @param headers [Hash] extra headers to add to the SDR API request
135
+ # @param params [Hash] query parameters to add to the SDR API request
136
+ # @param expected_status [Integer] override if all 2xx statuses aren't success conditions
137
+ def put(path:, body:, headers: {}, params: {}, expected_status: nil) # rubocop:disable Metrics/MethodLength
138
+ response = with_token_refresh_when_unauthorized do
139
+ connection.put(path) do |request|
140
+ request.body = body
141
+ request.headers = default_headers.merge(headers)
142
+ request.params = params if params.present?
143
+ end
144
+ end
145
+
146
+ if expected_status
147
+ UnexpectedResponse.call(response) if response.status != expected_status
148
+ elsif !response.success?
149
+ UnexpectedResponse.call(response)
150
+ end
151
+
152
+ return nil if response.body.blank?
153
+
154
+ JSON.parse(response.body).with_indifferent_access
155
+ end
156
+
157
+ private
158
+
159
+ Config = Struct.new(:url, :email, :password, :token, :logger,
160
+ :request_options, :token_refresher, keyword_init: true)
161
+
162
+ def connection
163
+ Faraday.new(
164
+ url: SdrClient::RedesignedClient.config.url,
165
+ headers: default_headers,
166
+ request: SdrClient::RedesignedClient.config.request_options
167
+ ) do |conn|
168
+ conn.adapter :net_http
169
+ end
170
+ end
171
+
172
+ def default_headers
173
+ {
174
+ accept: 'application/json',
175
+ content_type: 'application/json',
176
+ Authorization: "Bearer #{config.token}"
177
+ }
178
+ end
179
+
180
+ def with_token_refresh_when_unauthorized
181
+ response = yield
182
+
183
+ # if unauthorized, token has likely expired. try to get a new token and then retry the same request(s).
184
+ if response.status == 401
185
+ config.token = config.token_refresher ? config.token_refresher.call : Authenticator.token
186
+ response = yield
187
+ end
188
+
189
+ response
190
+ end
191
+ end
192
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SdrClient
4
- VERSION = '2.12.0'
4
+ VERSION = '2.13.0.beta1'
5
5
  end
data/lib/sdr_client.rb CHANGED
@@ -12,7 +12,9 @@ loader = Zeitwerk::Loader.for_gem
12
12
  loader.ignore(
13
13
  "#{__dir__}/sdr-client.rb",
14
14
  "#{__dir__}/sdr_client/cli.rb",
15
- "#{__dir__}/sdr_client/cli/config.rb"
15
+ "#{__dir__}/sdr_client/cli/config.rb",
16
+ "#{__dir__}/sdr_client/redesigned_client/cli.rb",
17
+ "#{__dir__}/sdr_client/redesigned_client/cli/config.rb"
16
18
  )
17
19
  loader.inflector.inflect(
18
20
  'md5' => 'MD5',
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sdr-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.0
4
+ version: 2.13.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-29 00:00:00.000000000 Z
11
+ date: 2024-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -115,6 +115,7 @@ email:
115
115
  executables:
116
116
  - remove_w3cdtf_encoding_from_event_dates
117
117
  - sdr
118
+ - sdr_redesigned
118
119
  extensions: []
119
120
  extra_rdoc_files: []
120
121
  files:
@@ -134,6 +135,7 @@ files:
134
135
  - config/settings.yml
135
136
  - exe/remove_w3cdtf_encoding_from_event_dates
136
137
  - exe/sdr
138
+ - exe/sdr_redesigned
137
139
  - lib/sdr-client.rb
138
140
  - lib/sdr_client.rb
139
141
  - lib/sdr_client/background_job_results.rb
@@ -166,6 +168,36 @@ files:
166
168
  - lib/sdr_client/find.rb
167
169
  - lib/sdr_client/login.rb
168
170
  - lib/sdr_client/login_prompt.rb
171
+ - lib/sdr_client/redesigned_client.rb
172
+ - lib/sdr_client/redesigned_client/authenticator.rb
173
+ - lib/sdr_client/redesigned_client/cli.rb
174
+ - lib/sdr_client/redesigned_client/cli/config.rb
175
+ - lib/sdr_client/redesigned_client/cli/credentials.rb
176
+ - lib/sdr_client/redesigned_client/cli/update.rb
177
+ - lib/sdr_client/redesigned_client/create_resource.rb
178
+ - lib/sdr_client/redesigned_client/deposit.rb
179
+ - lib/sdr_client/redesigned_client/direct_upload_request.rb
180
+ - lib/sdr_client/redesigned_client/direct_upload_response.rb
181
+ - lib/sdr_client/redesigned_client/file.rb
182
+ - lib/sdr_client/redesigned_client/file_set.rb
183
+ - lib/sdr_client/redesigned_client/file_type_file_set_strategy.rb
184
+ - lib/sdr_client/redesigned_client/find.rb
185
+ - lib/sdr_client/redesigned_client/image_file_set_strategy.rb
186
+ - lib/sdr_client/redesigned_client/job_status.rb
187
+ - lib/sdr_client/redesigned_client/matching_file_grouping_strategy.rb
188
+ - lib/sdr_client/redesigned_client/metadata.rb
189
+ - lib/sdr_client/redesigned_client/operations/md5.rb
190
+ - lib/sdr_client/redesigned_client/operations/mime_type.rb
191
+ - lib/sdr_client/redesigned_client/operations/sha1.rb
192
+ - lib/sdr_client/redesigned_client/request_builder.rb
193
+ - lib/sdr_client/redesigned_client/single_file_grouping_strategy.rb
194
+ - lib/sdr_client/redesigned_client/structural_grouper.rb
195
+ - lib/sdr_client/redesigned_client/structural_metadata_builder.rb
196
+ - lib/sdr_client/redesigned_client/unexpected_response.rb
197
+ - lib/sdr_client/redesigned_client/update_dro_with_file_identifiers.rb
198
+ - lib/sdr_client/redesigned_client/update_resource.rb
199
+ - lib/sdr_client/redesigned_client/upload_files.rb
200
+ - lib/sdr_client/redesigned_client/upload_files_metadata_builder.rb
169
201
  - lib/sdr_client/unexpected_response.rb
170
202
  - lib/sdr_client/update.rb
171
203
  - lib/sdr_client/version.rb
@@ -192,7 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
224
  - !ruby/object:Gem::Version
193
225
  version: '0'
194
226
  requirements: []
195
- rubygems_version: 3.5.6
227
+ rubygems_version: 3.5.7
196
228
  signing_key:
197
229
  specification_version: 4
198
230
  summary: The CLI for https://github.com/sul-dlss/sdr-api