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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.rubocop_todo.yml +24 -20
- data/Gemfile.lock +11 -8
- data/exe/sdr_redesigned +10 -0
- data/lib/sdr_client/redesigned_client/authenticator.rb +40 -0
- data/lib/sdr_client/redesigned_client/cli/config.rb +32 -0
- data/lib/sdr_client/redesigned_client/cli/credentials.rb +35 -0
- data/lib/sdr_client/redesigned_client/cli/update.rb +186 -0
- data/lib/sdr_client/redesigned_client/cli.rb +198 -0
- data/lib/sdr_client/redesigned_client/create_resource.rb +71 -0
- data/lib/sdr_client/redesigned_client/deposit.rb +115 -0
- data/lib/sdr_client/redesigned_client/direct_upload_request.rb +45 -0
- data/lib/sdr_client/redesigned_client/direct_upload_response.rb +9 -0
- data/lib/sdr_client/redesigned_client/file.rb +100 -0
- data/lib/sdr_client/redesigned_client/file_set.rb +53 -0
- data/lib/sdr_client/redesigned_client/file_type_file_set_strategy.rb +13 -0
- data/lib/sdr_client/redesigned_client/find.rb +42 -0
- data/lib/sdr_client/redesigned_client/image_file_set_strategy.rb +13 -0
- data/lib/sdr_client/redesigned_client/job_status.rb +74 -0
- data/lib/sdr_client/redesigned_client/matching_file_grouping_strategy.rb +19 -0
- data/lib/sdr_client/redesigned_client/metadata.rb +64 -0
- data/lib/sdr_client/redesigned_client/operations/md5.rb +16 -0
- data/lib/sdr_client/redesigned_client/operations/mime_type.rb +17 -0
- data/lib/sdr_client/redesigned_client/operations/sha1.rb +16 -0
- data/lib/sdr_client/redesigned_client/request_builder.rb +171 -0
- data/lib/sdr_client/redesigned_client/single_file_grouping_strategy.rb +14 -0
- data/lib/sdr_client/redesigned_client/structural_grouper.rb +72 -0
- data/lib/sdr_client/redesigned_client/structural_metadata_builder.rb +51 -0
- data/lib/sdr_client/redesigned_client/unexpected_response.rb +25 -0
- data/lib/sdr_client/redesigned_client/update_dro_with_file_identifiers.rb +35 -0
- data/lib/sdr_client/redesigned_client/update_resource.rb +61 -0
- data/lib/sdr_client/redesigned_client/upload_files.rb +71 -0
- data/lib/sdr_client/redesigned_client/upload_files_metadata_builder.rb +40 -0
- data/lib/sdr_client/redesigned_client.rb +192 -0
- data/lib/sdr_client/version.rb +1 -1
- data/lib/sdr_client.rb +3 -1
- 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
|