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,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'byebug'
4
+ require 'launchy'
5
+ require 'thor'
6
+ require_relative 'cli/config'
7
+ require_relative 'cli/credentials'
8
+ require_relative 'cli/update'
9
+
10
+ module SdrClient
11
+ class RedesignedClient
12
+ # The SDR command-line interface
13
+ class CLI < Thor
14
+ include Thor::Actions
15
+
16
+ # Make sure Thor commands preserve exit statuses
17
+ # @see https://github.com/rails/thor/wiki/Making-An-Executable
18
+ def self.exit_on_failure?
19
+ true
20
+ end
21
+
22
+ # Print out help and exit with error code if command not found
23
+ def self.handle_no_command_error(command)
24
+ puts "Command '#{command}' not found, displaying help:"
25
+ puts
26
+ puts help
27
+ exit(1)
28
+ end
29
+
30
+ def self.default_url
31
+ 'https://sdr-api-prod.stanford.edu'
32
+ end
33
+
34
+ package_name 'sdr'
35
+
36
+ class_option :url, desc: 'URL of SDR API endpoint', type: :string, default: default_url
37
+
38
+ desc 'get DRUID', 'Retrieve an object from the SDR'
39
+ def get(druid)
40
+ say client.find(object_id: druid)
41
+ end
42
+
43
+ desc 'version', 'Display the SDR CLI version'
44
+ def version
45
+ say VERSION
46
+ end
47
+
48
+ desc 'update DRUID', 'Update an object in the SDR'
49
+ option :skip_polling, type: :boolean, default: false, aliases: '-s',
50
+ desc: 'Print out job ID instead of polling for result'
51
+ option :apo, desc: 'Druid identifier of the admin policy object', aliases: '--admin-policy'
52
+ option :collection, desc: 'Druid identifier of the collection object'
53
+ option :copyright, desc: 'Copyright statement'
54
+ option :use_and_reproduction, desc: 'Use and reproduction statement'
55
+ option :license, desc: 'License URI'
56
+ option :view, enum: %w[world stanford location-based citation-only dark], desc: 'Access view level for the object'
57
+ option :download, enum: %w[world stanford location-based none], desc: 'Access download level for the object'
58
+ option :location, enum: %w[spec music ars art hoover m&m], desc: 'Access location for the object'
59
+ option :cdl, type: :boolean, default: false, desc: 'Controlled digital lending'
60
+ option :cocina_file, desc: 'Path to a file containing Cocina JSON'
61
+ option :cocina_pipe, type: :boolean, default: false, desc: 'Indicate Cocina JSON is being piped in'
62
+ option :basepath, default: Dir.getwd, desc: 'Base path for the files'
63
+ def update(druid)
64
+ validate_druid!(druid)
65
+ # Make sure client is configured
66
+ client
67
+ job_id = CLI::Update.run(druid, **options)
68
+ if options[:skip_polling]
69
+ say "job ID #{job_id} queued (not polling because `-s` flag was supplied)"
70
+ return
71
+ end
72
+
73
+ job_status = client.job_status(job_id: job_id)
74
+ if job_status.wait_until_complete
75
+ say "success! (druid: #{job_status.druid})"
76
+ else
77
+ say_error "errored! #{job_status.errors}"
78
+ end
79
+ end
80
+
81
+ desc 'deposit OPTIONAL_FILES', 'Deposit (accession) an object into the SDR'
82
+ option :skip_polling, type: :boolean, default: false, aliases: '-s',
83
+ desc: 'Print out job ID instead of polling for result'
84
+ option :apo, required: true, desc: 'Druid identifier of the admin policy object', aliases: '--admin-policy'
85
+ option :source_id, required: true, desc: 'Source ID for this object'
86
+ option :label, desc: 'Object label'
87
+ option :type, enum: %w[image book document map manuscript media three_dimensional object collection admin_policy],
88
+ desc: 'The object type'
89
+ option :collection, desc: 'Druid identifier of the collection object'
90
+ option :catkey, desc: 'Symphony catkey for this item'
91
+ option :folio_instance_hrid, desc: 'Folio instance HRID for this item'
92
+ option :copyright, desc: 'Copyright statement'
93
+ option :use_and_reproduction, desc: 'Use and reproduction statement'
94
+ option :viewing_direction, enum: %w[left-to-right right-to-left], desc: 'Viewing direction (if a book)'
95
+ option :view, enum: %w[world stanford location-based citation-only dark], desc: 'Access view level for the object'
96
+ option :files_metadata, desc: 'JSON string representing per-file metadata'
97
+ option :grouping_strategy, enum: %w[default filename], desc: 'Strategy for grouping files into filesets'
98
+ option :basepath, default: Dir.getwd, desc: 'Base path for the files'
99
+ def deposit(*files)
100
+ register_or_deposit(files: files, accession: true)
101
+ end
102
+
103
+ desc 'register OPTIONAL_FILES', 'Create a draft object in the SDR and retrieve a Druid identifier'
104
+ option :skip_polling, type: :boolean, default: false, aliases: '-s',
105
+ desc: 'Print out job ID instead of polling for result'
106
+ option :apo, required: true, desc: 'Druid identifier of the admin policy object', aliases: '--admin-policy'
107
+ option :source_id, required: true, desc: 'Source ID for this object'
108
+ option :label, desc: 'Object label'
109
+ option :type, enum: %w[image book document map manuscript media three_dimensional object collection admin_policy],
110
+ desc: 'The object type'
111
+ option :collection, desc: 'Druid identifier of the collection object'
112
+ option :catkey, desc: 'Symphony catkey for this item'
113
+ option :folio_instance_hrid, desc: 'Folio instance HRID for this item'
114
+ option :copyright, desc: 'Copyright statement'
115
+ option :use_and_reproduction, desc: 'Use and reproduction statement'
116
+ option :viewing_direction, enum: %w[left-to-right right-to-left], desc: 'Viewing direction (if a book)'
117
+ option :view, enum: %w[world stanford location-based citation-only dark], desc: 'Access view level for the object'
118
+ option :files_metadata, desc: 'JSON string representing per-file metadata'
119
+ option :grouping_strategy, enum: %w[default filename], desc: 'Strategy for grouping files into filesets'
120
+ option :basepath, default: Dir.getwd, desc: 'Base path for the files'
121
+ def register(*files)
122
+ register_or_deposit(files: files, accession: false)
123
+ end
124
+
125
+ private
126
+
127
+ def client
128
+ SdrClient::RedesignedClient.configure(
129
+ url: options[:url],
130
+ token: Credentials.read || SdrClient::RedesignedClient.default_token,
131
+ token_refresher: -> { login_via_proxy }
132
+ )
133
+ end
134
+
135
+ def login_via_proxy
136
+ say 'Opened the configured authentication proxy in your browser. ' \
137
+ 'Once there, generate a new token and copy the entire value.'
138
+ Launchy.open(authentication_proxy_url)
139
+ # Some CLI environments will pop up a message about opening the URL in
140
+ # an existing browse. Since this is OS-dependency, and not something
141
+ # we can control via Launchy, just wait a bit before rendering the
142
+ # `ask` prompt so it's clearer to the user what's happening
143
+ sleep 0.5
144
+ token_string = ask('Paste token here:')
145
+ expiry = JSON.parse(token_string)['exp']
146
+ CLI::Credentials.write(token_string)
147
+ say "You are now authenticated for #{options[:url]} until #{expiry}"
148
+ token_string
149
+ end
150
+
151
+ def authentication_proxy_url
152
+ Settings.authentication_proxy_url[options[:url]]
153
+ end
154
+
155
+ def register_or_deposit(files:, accession:)
156
+ opts = munge_options(options, files, accession)
157
+ job_id = client.build_and_deposit(apo: options[:apo], source_id: options[:source_id], **opts)
158
+ if opts.delete(:skip_polling)
159
+ say "job ID #{job_id} queued (not polling because `-s` flag was supplied)"
160
+ return
161
+ end
162
+
163
+ job_status = client.job_status(job_id: job_id)
164
+ if job_status.wait_until_complete
165
+ say "success! (druid: #{job_status.druid})"
166
+ else
167
+ say_error "errored! #{job_status.errors}"
168
+ end
169
+ end
170
+
171
+ def munge_options(options, files, accession)
172
+ options.to_h.symbolize_keys.tap do |opts|
173
+ opts[:access] = accession
174
+ opts[:type] = Cocina::Models::ObjectType.public_send(options[:type]) if options[:type]
175
+ opts[:files] = expand_files(files) if files.present?
176
+ opts[:files_metadata] = JSON.parse(options[:files_metadata]) if options[:files_metadata]
177
+ opts.delete(:apo)
178
+ opts.delete(:source_id)
179
+ end
180
+ end
181
+
182
+ def expand_files(files)
183
+ files.flat_map do |file|
184
+ next file unless Dir.exist?(file)
185
+
186
+ Dir.glob("#{file}/**/*").select { |f| File.file?(f) }
187
+ end
188
+ end
189
+
190
+ def validate_druid!(druid)
191
+ return if druid.present?
192
+
193
+ say_error "Not a valid druid: #{druid.inspect}"
194
+ exit(1)
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Creates a resource (metadata) in SDR
6
+ class CreateResource
7
+ def self.run(...)
8
+ new(...).run
9
+ end
10
+
11
+ # @param [Boolean] accession should the accessionWF be started
12
+ # @param [Boolean] assign_doi should a DOI be assigned to this item
13
+ # @param [Cocina::Models::RequestDRO, Cocina::Models::RequestCollection] metadata
14
+ # @param [Hash<Symbol,String>] the result of the metadata call
15
+ # @param [String] priority what processing priority should be used
16
+ # either 'low' or 'default'
17
+ def initialize(accession:, metadata:, assign_doi: false, priority: nil)
18
+ @accession = accession
19
+ @priority = priority
20
+ @assign_doi = assign_doi
21
+ @metadata = metadata
22
+ end
23
+
24
+ # @param [Hash<Symbol,String>] the result of the metadata call
25
+ # @return [String] job id for the background job result
26
+ def run
27
+ json = metadata.to_json
28
+ logger.debug("Starting upload metadata: #{json}")
29
+
30
+ response_hash = client.post(
31
+ path: path,
32
+ body: json,
33
+ headers: { 'X-Cocina-Models-Version' => Cocina::Models::VERSION },
34
+ expected_status: 201
35
+ )
36
+
37
+ logger.info("Response from server: #{response_hash.to_json}")
38
+
39
+ response_hash.fetch(:jobId)
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :metadata, :priority
45
+
46
+ def logger
47
+ SdrClient::RedesignedClient.config.logger
48
+ end
49
+
50
+ def client
51
+ SdrClient::RedesignedClient.instance
52
+ end
53
+
54
+ def accession?
55
+ @accession
56
+ end
57
+
58
+ def assign_doi?
59
+ @assign_doi
60
+ end
61
+
62
+ def path
63
+ params = { accession: accession? }
64
+ params[:priority] = priority if priority
65
+ params[:assign_doi] = true if assign_doi? # false is default
66
+ query_string = params.map { |k, v| "#{k}=#{v}" }.join('&')
67
+ "/v1/resources?#{query_string}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Deposit into the SDR API
6
+ class Deposit
7
+ def self.deposit_model(...)
8
+ new(...).deposit_model
9
+ end
10
+
11
+ # @param [Cocina::Model::RequestDRO] model for depositing
12
+ # @param [Boolean] accession should the accessionWF be started
13
+ # @param [String] basepath filepath to which filepaths are relative
14
+ # @param [Array<String>] files a list of relative filepaths to upload
15
+ # @param [Hash] options optional parameters
16
+ # @option options [Boolean] assign_doi should a DOI be assigned to this item
17
+ # @option options [String] priority what processing priority should be used ('low', 'default')
18
+ # @option options [String] grouping_strategy what strategy will be used to group files
19
+ # @option options [String] file_set_strategy what strategy will be used to group file sets
20
+ # @option options [RequestBuilder] request_builder a request builder instance
21
+ def initialize(model:, accession:, basepath:, files: [], **options)
22
+ @model = model
23
+ @accession = accession
24
+ @basepath = basepath
25
+ @files = files
26
+ @options = options
27
+ end
28
+
29
+ def deposit_model # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
30
+ check_files_exist!
31
+ child_files_match! unless options[:request_builder]
32
+
33
+ file_metadata = UploadFilesMetadataBuilder.build(files: files, mime_types: mime_types, basepath: basepath)
34
+ upload_responses = UploadFiles.upload(file_metadata: file_metadata,
35
+ filepath_map: filepath_map)
36
+ if options[:request_builder]
37
+ @model = StructuralGrouper.group(
38
+ request_builder: options[:request_builder],
39
+ upload_responses: upload_responses,
40
+ grouping_strategy: options[:grouping_strategy],
41
+ file_set_strategy: options[:file_set_strategy]
42
+ )
43
+ child_files_match!
44
+ end
45
+
46
+ new_request_dro = UpdateDroWithFileIdentifiers.update(request_dro: model,
47
+ upload_responses: upload_responses)
48
+ CreateResource.run(accession: accession,
49
+ priority: options[:priority],
50
+ assign_doi: options[:assign_doi],
51
+ metadata: new_request_dro)
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :model, :files, :basepath, :accession, :options
57
+
58
+ def check_files_exist!
59
+ SdrClient::RedesignedClient.config.logger.info('checking to see if files exist')
60
+ files.each do |filepath|
61
+ raise Errno::ENOENT, filepath unless ::File.exist?(absolute_filepath_for(filepath))
62
+ end
63
+ end
64
+
65
+ def child_files_match!
66
+ # Files without request files.
67
+ files.each do |filepath|
68
+ raise "Request file not provided for #{filepath}" if request_files[filepath].nil?
69
+ end
70
+
71
+ # Request files without files
72
+ request_files.each_key do |request_filename|
73
+ raise "File not provided for request file #{request_filename}" unless files.include?(request_filename)
74
+ end
75
+ end
76
+
77
+ # Map of relative filepaths to mimetypes
78
+ def mime_types
79
+ return mime_types_from_request_builder if options[:request_builder]
80
+
81
+ request_files.transform_values { |file| file.hasMimeType || 'application/octet-stream' }
82
+ end
83
+
84
+ def mime_types_from_request_builder
85
+ files.to_h do |filepath|
86
+ [filepath, options[:request_builder].for(filepath)['mime_type']]
87
+ end
88
+ end
89
+
90
+ # Map of absolute filepaths to Cocina::Models::RequestFiles
91
+ def request_files
92
+ @request_files ||=
93
+ if model.structural
94
+ model.structural.contains.map do |file_set|
95
+ file_set.structural.contains.map do |file|
96
+ [file.filename, file]
97
+ end
98
+ end.flatten(1).to_h
99
+ else
100
+ {}
101
+ end
102
+ end
103
+
104
+ def absolute_filepath_for(filename)
105
+ ::File.join(basepath, filename)
106
+ end
107
+
108
+ def filepath_map
109
+ @filepath_map ||= files.each_with_object({}) do |filepath, obj|
110
+ obj[filepath] = absolute_filepath_for(filepath)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module SdrClient
6
+ class RedesignedClient
7
+ DirectUploadRequest = Struct.new(:checksum, :byte_size, :content_type, :filename, keyword_init: true) do
8
+ def self.from_file(path, file_name:, content_type:)
9
+ checksum = Digest::MD5.file(path).base64digest
10
+ new(checksum: checksum,
11
+ byte_size: ::File.size(path),
12
+ content_type: clean_and_translate_content_type(content_type),
13
+ filename: file_name)
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ blob: { filename: filename, byte_size: byte_size, checksum: checksum,
19
+ content_type: self.class.clean_and_translate_content_type(content_type) }
20
+ }
21
+ end
22
+
23
+ def to_json(*_args)
24
+ JSON.generate(to_h)
25
+ end
26
+
27
+ # Invalid JSON files with a content type of application/json will trigger 400 errors in sdr-api
28
+ # since they are parsed and rejected (not clear why and what part of the stack is doing this).
29
+ # The work around is to change the content_type for any JSON files to a custom stand-in and
30
+ # specific to avoid the parsing, and then have this translated back to application/json after .
31
+ # upload is complete. There is a corresponding change in sdr-api to translate the content_type back
32
+ # before the Cocina is saved.
33
+ # See https://github.com/sul-dlss/happy-heron/issues/3075 for the original bug report
34
+ # See https://github.com/sul-dlss/sdr-api/pull/585 for the change in sdr-api
35
+ def self.clean_and_translate_content_type(content_type)
36
+ return 'application/octet-stream' if content_type.blank?
37
+
38
+ # ActiveStorage is expecting "application/x-stata-dta" not "application/x-stata-dta;version=14"
39
+ content_type = content_type.split(';')&.first
40
+
41
+ content_type == 'application/json' ? 'application/x-stanford-json' : content_type
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ DirectUploadResponse = Struct.new(:id, :key, :checksum, :byte_size, :content_type,
6
+ :filename, :metadata, :created_at, :direct_upload,
7
+ :signed_id, :service_name, keyword_init: true)
8
+ end
9
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # This represents the File metadata that we send to the server for doing a deposit
6
+ class File
7
+ # @param [String] external_identifier used for object IDs (e.g., druids)
8
+ # @param [String] label the required object label
9
+ # @param [String] filename a filename
10
+ # @param [Hash] options optional parameters
11
+ # @option options [String] view the access level for viewing the object
12
+ # @option options [String] download the access level for downloading the object
13
+ # @option options [Boolean] preserve whether to preserve the file or not
14
+ # @option options [Boolean] shelve whether to shelve the file or not
15
+ # @option options [Boolean] publish whether to publish the file or not
16
+ # @option options [String] mime_type the MIME type of the file
17
+ # @option options [String] md5 the MD5 digest of the file
18
+ # @option options [String] sha1 the SHA1 digest of the file
19
+ # @option options [String] use the use and reproduction statement
20
+ def initialize(external_identifier:, label:, filename:, **options)
21
+ @external_identifier = external_identifier
22
+ @label = label
23
+ @filename = filename
24
+ @options = options
25
+ end
26
+
27
+ def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
28
+ {
29
+ type: Cocina::Models::ObjectType.file,
30
+ label: label,
31
+ filename: filename,
32
+ externalIdentifier: external_identifier,
33
+ access: {
34
+ view: view,
35
+ download: download
36
+ },
37
+ administrative: {
38
+ sdrPreserve: preserve,
39
+ shelve: shelve,
40
+ publish: publish
41
+ },
42
+ version: 1,
43
+ hasMessageDigests: message_digests
44
+ }.tap do |json|
45
+ json['hasMimeType'] = mime_type if mime_type
46
+ json['use'] = use if use
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :external_identifier, :label, :filename, :options
53
+
54
+ def message_digests
55
+ [].tap do |message_digests|
56
+ message_digests << { type: 'md5', digest: md5 } if md5
57
+ message_digests << { type: 'sha1', digest: sha1 } if sha1
58
+ end
59
+ end
60
+
61
+ def view
62
+ options.fetch(:view, 'dark')
63
+ end
64
+
65
+ def download
66
+ options.fetch(:download, 'none')
67
+ end
68
+
69
+ def preserve
70
+ options.fetch(:preserve, true)
71
+ end
72
+
73
+ def shelve
74
+ return false if view == 'dark'
75
+
76
+ options.fetch(:shelve, true)
77
+ end
78
+
79
+ def publish
80
+ options.fetch(:publish, true)
81
+ end
82
+
83
+ def mime_type
84
+ options[:mime_type]
85
+ end
86
+
87
+ def md5
88
+ options[:md5]
89
+ end
90
+
91
+ def sha1
92
+ options[:sha1]
93
+ end
94
+
95
+ def use
96
+ options[:use]
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # This represents the FileSet metadata that we send to the server for doing a deposit
6
+ class FileSet
7
+ # @param [String] label
8
+ # @param [Array] uploads
9
+ # @param [Hash<String,Hash<String,String>>] uploads_metadata the file level metadata
10
+ # @param [Array] files
11
+ # @param [Class] type_strategy (FileTypeFileSetStrategy) a class that helps us determine how to type the fileset
12
+ def initialize(label:, type_strategy:, uploads: [], uploads_metadata: {}, files: [])
13
+ @label = label
14
+ @type_strategy = type_strategy
15
+ @files = uploads.empty? ? files : files_from(uploads, uploads_metadata)
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ type: type_strategy.run(files: files),
21
+ label: label,
22
+ structural: {
23
+ contains: files.map(&:to_h)
24
+ },
25
+ version: 1
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :files, :label, :type_strategy
32
+
33
+ def files_from(uploads, uploads_metadata)
34
+ uploads.map do |upload|
35
+ SdrClient::RedesignedClient::File.new(**file_args(upload, uploads_metadata.fetch(upload.filename, {})))
36
+ end
37
+ end
38
+
39
+ # This creates the metadata for each file and symbolizes the keys
40
+ # @return [Hash<Symbol,String>]
41
+ def file_args(upload, upload_metadata)
42
+ args = {
43
+ external_identifier: upload.signed_id,
44
+ label: ::File.basename(upload.filename),
45
+ filename: upload.filename
46
+ }
47
+ args.merge!(upload_metadata)
48
+ # Symbolize
49
+ args.transform_keys(&:to_sym)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # This strategy is for building the type of a fileset
6
+ class FileTypeFileSetStrategy
7
+ # @return [String] The URI that represents the type of file_set
8
+ def self.run(...)
9
+ Cocina::Models::FileSetType.file
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module SdrClient
6
+ class RedesignedClient
7
+ # Find an object
8
+ class Find
9
+ def self.run(...)
10
+ new(...).run
11
+ end
12
+
13
+ # @param [String] object_id an ID for an object
14
+ def initialize(object_id:)
15
+ @object_id = object_id
16
+ end
17
+
18
+ # @raise [Failed] if the find operation fails
19
+ # @return [String] JSON for the given Cocina object or an error
20
+ def run
21
+ logger.info("Retrieving metadata from: #{path}")
22
+ client.get(path: path)
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :object_id
28
+
29
+ def logger
30
+ SdrClient::RedesignedClient.config.logger
31
+ end
32
+
33
+ def client
34
+ SdrClient::RedesignedClient.instance
35
+ end
36
+
37
+ def path
38
+ format('/v1/resources/%<object_id>s', object_id: object_id)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # This strategy is for building the type of a fileset
6
+ class ImageFileSetStrategy
7
+ # @return [String] The URI that represents the type of file_set
8
+ def self.run(...)
9
+ Cocina::Models::FileSetType.image
10
+ end
11
+ end
12
+ end
13
+ end