sdr-client 2.12.0 → 2.13.0.beta1

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 (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