sdr-client 0.2.0 → 0.5.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a995f725226eef2f793d8b0b45e487c37a6e34899b223a1f2c6864f32204325
4
- data.tar.gz: dd947253f76fe8dcfd20565537e89fa86c4b6b13a4e6c68b2ecf285d30406c08
3
+ metadata.gz: 565e7d34958ad818fcc048440816b0a360fa723ea6e62dc9437c6832ba8877c1
4
+ data.tar.gz: 14dfae8ecef2b6d05a256a8bdc7aebebfe801a278546c98bd3066169f7c98899
5
5
  SHA512:
6
- metadata.gz: f3f0c8d2c9994c4a0408438199105dfdcccb0901e4905220cbf99fd69b3d5e9a735752c3de638264c1397986985ecb2a9f992ab1f1fad2e3245025e78eed1dd0
7
- data.tar.gz: 300591272660ad09e74bc992742c3c26c14251bd107252b441a2e2856ed981749dfd5ff298704b13c360a0b3dab0cc89fceb2282a9f301fe24909eaac0b8de0d
6
+ metadata.gz: bc629b8837567ee95b01ce673a1f1b49596c3665810ab8d59573012b3b9b6a674db12dde7d15461a279f87a4ef88ed23d65d2457bbc7c0479330f45b3f2c1649
7
+ data.tar.gz: fe176665dadd92ae291b7dbf5b461bb40606600383ad81c9824a8a4bad83344db5e74b6568a10eced02f4fa14894188122605ac2b7ecd5176d7b25b241963da4
@@ -13,3 +13,5 @@ Layout/LineLength:
13
13
  Metrics/BlockLength:
14
14
  Exclude:
15
15
  - 'spec/**/*'
16
+ ExcludedMethods:
17
+ - 'OptionParser.new'
data/README.md CHANGED
@@ -8,6 +8,10 @@
8
8
  This is a CLI for interacting with the Stanford Digital Repository API.
9
9
  The code for the SDR API server is at https://github.com/sul-dlss/sdr-api
10
10
 
11
+ ## Install
12
+
13
+ `gem install sdr-client`
14
+
11
15
  ## Usage
12
16
 
13
17
  Log in:
data/exe/sdr CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  $LOAD_PATH.unshift 'lib'
5
5
  require 'optparse'
6
+ require 'sdr_client'
6
7
 
7
8
  options = {}
8
9
  global = OptionParser.new do |opts|
@@ -60,6 +61,20 @@ subcommands = {
60
61
  options[:source_id] = source_id
61
62
  end
62
63
 
64
+ opts.on('--strategy STRATEGY',
65
+ 'The strategy to use for distributing files into filesets. Either "default" or "filename"') do |strategy|
66
+ strategy_class = case strategy
67
+ when 'filename'
68
+ SdrClient::Deposit::MatchingFileGroupingStrategy
69
+ when 'default'
70
+ SdrClient::Deposit::SingleFileGroupingStrategy
71
+ else
72
+ warn "Unknown strategy #{strategy}"
73
+ exit(1)
74
+ end
75
+ options[:grouping_strategy] = strategy_class
76
+ end
77
+
63
78
  opts.on('-h', '--help', 'Display this screen') do
64
79
  puts opts
65
80
  exit
@@ -75,6 +90,5 @@ end
75
90
 
76
91
  subcommands[command].order!
77
92
 
78
- require 'sdr_client'
79
93
  options[:files] = ARGV unless ARGV.empty?
80
94
  SdrClient::CLI.start(command, options)
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dry/monads'
4
+ require 'faraday'
5
+
3
6
  require 'sdr_client/version'
4
7
  require 'sdr_client/deposit'
5
8
  require 'sdr_client/credentials'
6
9
  require 'sdr_client/login'
7
10
  require 'sdr_client/login_prompt'
8
11
  require 'sdr_client/cli'
9
- require 'faraday'
10
12
 
11
13
  module SdrClient
12
14
  class Error < StandardError; end
@@ -8,7 +8,8 @@ module SdrClient
8
8
  when 'deposit'
9
9
  SdrClient::Deposit.run(options)
10
10
  when 'login'
11
- SdrClient::Login.run(options)
11
+ status = SdrClient::Login.run(options)
12
+ puts status.value if status.failure?
12
13
  else
13
14
  raise "Unknown command #{command}"
14
15
  end
@@ -14,7 +14,7 @@ module SdrClient
14
14
  end
15
15
 
16
16
  def self.read
17
- return IO.readlines(credentials_file, chomp: true).first if File.exist?(credentials_file)
17
+ return IO.readlines(credentials_file, chomp: true).first if ::File.exist?(credentials_file)
18
18
 
19
19
  puts 'Log in first'
20
20
  exit(1)
@@ -3,13 +3,16 @@
3
3
  module SdrClient
4
4
  # The namespace for the "deposit" command
5
5
  module Deposit
6
+ # rubocop:disable Metrics/ParameterLists
6
7
  def self.run(label: nil,
7
8
  type: 'http://cocina.sul.stanford.edu/models/book.jsonld',
8
9
  apo:,
9
10
  collection:,
10
11
  catkey: nil,
11
12
  source_id:,
12
- url:, files: [])
13
+ url:,
14
+ files: [],
15
+ grouping_strategy: SingleFileGroupingStrategy)
13
16
  token = Credentials.read
14
17
 
15
18
  metadata = Request.new(label: label,
@@ -18,12 +21,17 @@ module SdrClient
18
21
  collection: collection,
19
22
  source_id: source_id,
20
23
  catkey: catkey)
21
- Process.new(metadata: metadata, url: url, token: token, files: files).run
24
+ Process.new(metadata: metadata, url: url, token: token, files: files, grouping_strategy: grouping_strategy).run
22
25
  end
26
+ # rubocop:enable Metrics/ParameterLists
23
27
  end
24
28
  end
25
29
  require 'json'
30
+ require 'sdr_client/deposit/single_file_grouping_strategy'
31
+ require 'sdr_client/deposit/matching_file_grouping_strategy'
26
32
  require 'sdr_client/deposit/files/direct_upload_request'
27
33
  require 'sdr_client/deposit/files/direct_upload_response'
34
+ require 'sdr_client/deposit/file'
35
+ require 'sdr_client/deposit/file_set'
28
36
  require 'sdr_client/deposit/request'
29
37
  require 'sdr_client/deposit/process'
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ module Deposit
5
+ # This represents the File metadata that we send to the server for doing a deposit
6
+ class File
7
+ def initialize(external_identifier:, label:, filename:, access: 'dark', preserve: false, shelve: false,
8
+ md5: nil, sha1: nil)
9
+ @external_identifier = external_identifier
10
+ @label = label
11
+ @filename = filename
12
+ @access = access
13
+ @preserve = preserve
14
+ @shelve = shelve
15
+ @md5 = md5
16
+ @sha1 = sha1
17
+ end
18
+
19
+ # rubocop:disable Metrics/MethodLength
20
+ def as_json
21
+ json = {
22
+ "type": 'http://cocina.sul.stanford.edu/models/file.jsonld',
23
+ label: @label,
24
+ filename: @filename,
25
+ externalIdentifier: @external_identifier,
26
+ access: {
27
+ access: @access
28
+ },
29
+ administrative: {
30
+ sdrPreserve: @preserve,
31
+ shelve: @shelve
32
+ }
33
+ }
34
+ json['hasMessageDigests'] = message_digests unless message_digests.empty?
35
+ json
36
+ end
37
+ # rubocop:enable Metrics/MethodLength
38
+
39
+ private
40
+
41
+ def message_digests
42
+ @message_digests ||= [].tap do |message_digests|
43
+ message_digests << create_message_digest('md5', @md5) unless @md5.nil?
44
+ message_digests << create_message_digest('sha1', @sha1) unless @sha1.nil?
45
+ end
46
+ end
47
+
48
+ def create_message_digest(algorithm, digest)
49
+ {
50
+ "type": algorithm,
51
+ digest: digest
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ module Deposit
5
+ # This represents the FileSet metadata that we send to the server for doing a deposit
6
+ class FileSet
7
+ def initialize(uploads: [], uploads_metadata: {}, files: [], label:)
8
+ @label = label
9
+ @files = if !uploads.empty?
10
+ uploads.map do |upload|
11
+ File.new(file_args(upload, uploads_metadata.fetch(upload.filename, {})))
12
+ end
13
+ else
14
+ files
15
+ end
16
+ end
17
+
18
+ def as_json
19
+ {
20
+ "type": 'http://cocina.sul.stanford.edu/models/fileset.jsonld',
21
+ "label": label,
22
+ structural: {
23
+ hasMember: files.map(&:as_json)
24
+ }
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :files, :label
31
+
32
+ def file_args(upload, upload_metadata)
33
+ args = {
34
+ external_identifier: upload.signed_id,
35
+ label: upload.filename,
36
+ filename: upload.filename
37
+ }
38
+ args.merge(upload_metadata)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -9,9 +9,9 @@ module SdrClient
9
9
  def self.from_file(filename)
10
10
  checksum = Digest::MD5.file(filename).base64digest
11
11
  new(checksum: checksum,
12
- byte_size: File.size(filename),
12
+ byte_size: ::File.size(filename),
13
13
  content_type: 'text/html',
14
- filename: File.basename(filename))
14
+ filename: ::File.basename(filename))
15
15
  end
16
16
 
17
17
  def as_json
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ module Deposit
5
+ # This strategy is for building one file set per set of similarly prefixed uploaded files
6
+ class MatchingFileGroupingStrategy
7
+ # @param [Array<SdrClient::Deposit::Files::DirectUploadResponse>] uploads the uploaded files to attach.
8
+ # @return [Array<Array<SdrClient::Deposit::Files::DirectUploadResponse>>] uploads the grouped uploaded files.
9
+ def self.run(uploads: [])
10
+ uploads.group_by { |ul| ::File.basename(ul.filename, '.*') }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -9,16 +9,22 @@ module SdrClient
9
9
  BLOB_PATH = '/v1/direct_uploads'
10
10
  DRO_PATH = '/v1/resources'
11
11
  # @param [Request] metadata information about the object
12
+ # @param [Class] grouping_strategy class whose run method groups an array of uploads
12
13
  # @param [String] url the server to send to
13
14
  # @param [String] token the bearer auth token for the server
14
15
  # @param [Array<String>] files a list of file names to upload
16
+ # @param [Hash<String, Hash<String, String>>] files_metadata file name, hash of additional file metadata
17
+ # Additional metadata includes access, preserve, shelve, md5, sha1
15
18
  # @param [Logger] logger the logger to use
16
- def initialize(metadata:, url:, token:, files: [], logger: Logger.new(STDOUT))
19
+ def initialize(metadata:, grouping_strategy: SingleFileGroupingStrategy, url:,
20
+ token:, files: [], files_metadata: {}, logger: Logger.new(STDOUT))
17
21
  @files = files
18
22
  @url = url
19
23
  @token = token
20
24
  @metadata = metadata
21
25
  @logger = logger
26
+ @grouping_strategy = grouping_strategy
27
+ @files_metadata = files_metadata
22
28
  end
23
29
 
24
30
  def run
@@ -26,18 +32,19 @@ module SdrClient
26
32
  file_metadata = collect_file_metadata
27
33
  upload_responses = upload_file_metadata(file_metadata)
28
34
  upload_files(upload_responses)
29
- request = metadata.with_uploads(upload_responses.values)
35
+ file_sets = build_filesets(uploads: upload_responses.values, files_metadata: files_metadata)
36
+ request = metadata.with_file_sets(file_sets)
30
37
  upload_metadata(request.as_json)
31
38
  end
32
39
 
33
40
  private
34
41
 
35
- attr_reader :metadata, :files, :url, :token, :logger
42
+ attr_reader :metadata, :files, :url, :token, :logger, :grouping_strategy, :files_metadata
36
43
 
37
44
  def check_files_exist
38
45
  logger.info('checking to see if files exist')
39
46
  files.each do |file_name|
40
- raise Errno::ENOENT, file_name unless File.exist?(file_name)
47
+ raise Errno::ENOENT, file_name unless ::File.exist?(file_name)
41
48
  end
42
49
  end
43
50
 
@@ -78,7 +85,7 @@ module SdrClient
78
85
  logger.info("Uploading `#{filename}' to #{url}")
79
86
 
80
87
  upload_response = connection.put(url) do |req|
81
- req.body = File.open(filename)
88
+ req.body = ::File.open(filename)
82
89
  req.headers['Content-Type'] = content_type
83
90
  req.headers['Content-Length'] = content_length.to_s
84
91
  end
@@ -111,6 +118,18 @@ module SdrClient
111
118
  conn.adapter :net_http
112
119
  end
113
120
  end
121
+
122
+ # @param [Array<SdrClient::Deposit::Files::DirectUploadResponse>] uploads the uploaded files to attach.
123
+ # @param [Hash<String,Hash<String, String>>] files_metadata filename, hash of additional file metadata.
124
+ # @return [Array<SdrClient::Deposit::FileSet>] the uploads transformed to filesets
125
+ def build_filesets(uploads:, files_metadata:)
126
+ grouped_uploads = grouping_strategy.run(uploads: uploads)
127
+ grouped_uploads.each_with_index.map do |upload_group, i|
128
+ metadata_group = {}
129
+ upload_group.each { |upload| metadata_group[upload.filename] = files_metadata.fetch(upload.filename, {}) }
130
+ FileSet.new(uploads: upload_group, uploads_metadata: metadata_group, label: "Object #{i + 1}")
131
+ end
132
+ end
114
133
  end
115
134
  end
116
135
  end
@@ -6,21 +6,21 @@ module SdrClient
6
6
  class Request
7
7
  # @param [String] label the required object label
8
8
  # @param [String] type (http://cocina.sul.stanford.edu/models/object.jsonld) the required object type.
9
- # @param [Array<SdrClient::Deposit::Files::DirectUploadResponse>] uploads the uploaded files to attach.
9
+ # @param [Array<FileSet>] file_sets the file sets to attach.
10
10
  def initialize(label: nil,
11
11
  apo:,
12
12
  collection:,
13
13
  source_id:,
14
14
  catkey: nil,
15
15
  type: 'http://cocina.sul.stanford.edu/models/object.jsonld',
16
- uploads: [])
16
+ file_sets: [])
17
17
  @label = label
18
18
  @type = type
19
19
  @source_id = source_id
20
20
  @collection = collection
21
21
  @catkey = catkey
22
22
  @apo = apo
23
- @uploads = uploads
23
+ @file_sets = file_sets
24
24
  end
25
25
 
26
26
  def as_json
@@ -35,20 +35,20 @@ module SdrClient
35
35
  end
36
36
  end
37
37
 
38
- # @return [Request] a clone of this request with the uploads added
39
- def with_uploads(uploads)
38
+ # @return [Request] a clone of this request with the file_sets added
39
+ def with_file_sets(file_sets)
40
40
  Request.new(label: label,
41
41
  apo: apo,
42
42
  collection: collection,
43
43
  source_id: source_id,
44
44
  catkey: catkey,
45
45
  type: type,
46
- uploads: uploads)
46
+ file_sets: file_sets)
47
47
  end
48
48
 
49
49
  private
50
50
 
51
- attr_reader :label, :uploads, :source_id, :catkey, :apo, :collection, :type
51
+ attr_reader :label, :file_sets, :source_id, :catkey, :apo, :collection, :type
52
52
 
53
53
  def administrative
54
54
  {
@@ -65,24 +65,9 @@ module SdrClient
65
65
  def structural
66
66
  {
67
67
  isMemberOf: collection,
68
- hasMember: file_sets_as_json
68
+ hasMember: file_sets.map(&:as_json)
69
69
  }
70
70
  end
71
-
72
- # In this case there is a 1-1 mapping between Files and FileSets,
73
- # but this doesn't always have to be the case. We could change this in the
74
- # future so that we have one FileSet that has an Image and its OCR file.
75
- def file_sets_as_json
76
- uploads.map do |upload|
77
- {
78
- "type": 'http://cocina.sul.stanford.edu/models/fileset.jsonld',
79
- label: upload.filename,
80
- structural: {
81
- hasMember: [upload.signed_id]
82
- }
83
- }
84
- end
85
- end
86
71
  end
87
72
  end
88
73
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ module Deposit
5
+ # This strategy is for building one file set per uploaded file
6
+ class SingleFileGroupingStrategy
7
+ # @param [Array<SdrClient::Deposit::Files::DirectUploadResponse>] uploads the uploaded files to attach.
8
+ # @return [Array<Array<SdrClient::Deposit::Files::DirectUploadResponse>>] uploads the grouped uploaded files.
9
+ def self.run(uploads: [])
10
+ uploads.map { |upload| [upload] }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -4,19 +4,22 @@ module SdrClient
4
4
  # The namespace for the "login" command
5
5
  module Login
6
6
  LOGIN_PATH = '/v1/auth/login'
7
+ extend Dry::Monads[:result]
8
+
9
+ # @return [Result] the status of the call
7
10
  def self.run(url:, login_service: LoginPrompt)
8
11
  request_json = JSON.generate(login_service.run)
9
12
  response = Faraday.post(url + LOGIN_PATH, request_json, 'Content-Type' => 'application/json')
10
13
  case response.status
11
14
  when 200
12
15
  Credentials.write(response.body)
16
+ Success()
13
17
  when 400
14
- puts 'Email address is not a valid email'
18
+ Failure('Email address is not a valid email')
15
19
  when 401
16
- puts 'Invalid username or password'
20
+ Failure('Invalid username or password')
17
21
  else
18
- puts "Status: #{response.status}"
19
- puts response.body
22
+ Failure("Status: #{response.status}\n#{response.body}")
20
23
  end
21
24
  end
22
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SdrClient
4
- VERSION = '0.2.0'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -27,7 +27,8 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ['lib']
29
29
 
30
- spec.add_dependency 'faraday', '~> 0.17.0'
30
+ spec.add_dependency 'dry-monads'
31
+ spec.add_dependency 'faraday', '>= 0.16'
31
32
 
32
33
  spec.add_development_dependency 'bundler', '~> 2.0'
33
34
  spec.add_development_dependency 'rake', '~> 13.0'
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sdr-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-01-13 00:00:00.000000000 Z
11
+ date: 2020-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-monads
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: faraday
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
- - - "~>"
31
+ - - ">="
18
32
  - !ruby/object:Gem::Version
19
- version: 0.17.0
33
+ version: '0.16'
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
- - - "~>"
38
+ - - ">="
25
39
  - !ruby/object:Gem::Version
26
- version: 0.17.0
40
+ version: '0.16'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -148,10 +162,14 @@ files:
148
162
  - lib/sdr_client/cli.rb
149
163
  - lib/sdr_client/credentials.rb
150
164
  - lib/sdr_client/deposit.rb
165
+ - lib/sdr_client/deposit/file.rb
166
+ - lib/sdr_client/deposit/file_set.rb
151
167
  - lib/sdr_client/deposit/files/direct_upload_request.rb
152
168
  - lib/sdr_client/deposit/files/direct_upload_response.rb
169
+ - lib/sdr_client/deposit/matching_file_grouping_strategy.rb
153
170
  - lib/sdr_client/deposit/process.rb
154
171
  - lib/sdr_client/deposit/request.rb
172
+ - lib/sdr_client/deposit/single_file_grouping_strategy.rb
155
173
  - lib/sdr_client/login.rb
156
174
  - lib/sdr_client/login_prompt.rb
157
175
  - lib/sdr_client/version.rb