purl_fetcher-client 1.0.0 → 1.2.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: cade5ac0289e6a39d9d9dca571bea47032391fa774d1356e56dbada402e6bb5e
4
- data.tar.gz: 043a7f61d74b733bf2fd12bf26704ff25af6539f10c5731ae4843f11c2376c1a
3
+ metadata.gz: 989a8e0ff7bcb9f39c83e2d5656b759f8a265862e97c0f4964de9ca15ec63be7
4
+ data.tar.gz: 640c89a6ed21223385b8a07950da50b3e7d0dbad00f6b35a4b34707e6af6ad99
5
5
  SHA512:
6
- metadata.gz: fa1bce8d7e6ef5090a19d8cf8ecf42eeeb24f2c5ef59345c77ded4b50752f01e1b18307dd65aafcd0aa8c4873210becf6fcdfacb5cdd6179034c023a609d3218
7
- data.tar.gz: 8f76a7eecfff70435931364f827571733cc6f4d9e7cc90d8f65276580eab0dbcd5cd8eddd5a146b3e7fe016b2f4d69a3dff177e77e941ef7bbe47e49b975d3dc
6
+ metadata.gz: a4155c8e5dce02815e9b9993a6634f2226047ff0c302dc8412451baa4b280892a7539cf56fb64d4dfe45512aacf70497051ac8be5a9fe302d427da97ee932d8c
7
+ data.tar.gz: 7d0e48ee44aa4358a93bccae266190b287bbe4f948f017511696326c7d6a12ec661db38e2d328dcca17e78bee6b8d445e568cb10e299d94cba393568f308896f
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ inherit_gem:
2
+ rubocop-rails-omakase: rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.2
data/Gemfile CHANGED
@@ -1,6 +1,9 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in purl_fetcher-client.gemspec
6
6
  gemspec
7
+
8
+ gem "rubocop-rails-omakase", require: false, group: [ :development ]
9
+ gem "debug"
data/README.md CHANGED
@@ -22,7 +22,25 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ ### Uploading a file
26
+
27
+ ```ruby
28
+ PurlFetcher::Client.configure(url:'http://127.0.0.1:3000', token: 'abc123')
29
+
30
+ PurlFetcher::Client::UploadFiles.upload(
31
+ file_metadata: {
32
+ 'file1.txt' => PurlFetcher::Client::DirectUploadRequest.new(
33
+ checksum: '123',
34
+ byte_size: 10_000,
35
+ content_type: 'image/tiff',
36
+ filename: 'image.tiff'
37
+ )
38
+ },
39
+ filepath_map: {
40
+ 'file1.txt' => File.expand_path('Gemfile.lock')
41
+ }
42
+ )
43
+ ```
26
44
 
27
45
  ## Development
28
46
 
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "purl_fetcher/client"
3
+ require 'bundler/setup'
4
+ require 'purl_fetcher/client'
5
+ require 'debug'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "purl_fetcher/client"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module PurlFetcher
6
+ class Client
7
+ # This models the JSON that we send to the server.
8
+ DirectUploadRequest = Data.define(:checksum, :byte_size, :content_type, :filename) do
9
+ def self.from_file(hexdigest:, byte_size:, file_name:, content_type:)
10
+ new(checksum: hex_to_base64_digest(hexdigest),
11
+ byte_size: byte_size,
12
+ content_type: clean_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_content_type(content_type) }
20
+ }
21
+ end
22
+
23
+ def to_json(*_args)
24
+ JSON.generate(to_h)
25
+ end
26
+
27
+ def self.clean_content_type(content_type)
28
+ return "application/octet-stream" if content_type.blank?
29
+
30
+ # ActiveStorage is expecting "application/x-stata-dta" not "application/x-stata-dta;version=14"
31
+ content_type.split(";").first
32
+ end
33
+
34
+ def self.hex_to_base64_digest(hexdigest)
35
+ [ [ hexdigest ].pack("H*") ].pack("m0")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurlFetcher
4
+ class Client
5
+ DirectUploadResponse = Data.define(:id, :key, :checksum, :byte_size, :content_type,
6
+ :filename, :metadata, :created_at, :direct_upload,
7
+ :signed_id, :service_name) do
8
+ def with_filename(filename)
9
+ self.class.new(**deconstruct_keys(nil).merge(filename:))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurlFetcher
4
+ class Client
5
+ # Publish (metadata-only). This will be replaced with a single publish operation
6
+ class LegacyPublish
7
+ # @param [Cocina::Models::DRO,Cocina::Models::Collection] cocina the Cocina data object
8
+ def self.publish(cocina:)
9
+ new(cocina:).publish
10
+ end
11
+
12
+ # @param [Cocina::Models::DRO,Cocina::Models::Collection] cocina the Cocina data object
13
+ def initialize(cocina:)
14
+ @cocina = cocina
15
+ end
16
+
17
+ def publish
18
+ logger.debug("Starting a legacy publish request for: #{druid}")
19
+ response = client.post(path:, body:)
20
+ logger.debug("Legacy publish request complete")
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :cocina
26
+
27
+ def druid
28
+ cocina.externalIdentifier
29
+ end
30
+
31
+ def body
32
+ cocina.to_json
33
+ end
34
+
35
+ def logger
36
+ Client.config.logger
37
+ end
38
+
39
+ def client
40
+ Client.instance
41
+ end
42
+
43
+ def path
44
+ "/purls/#{druid}"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -2,7 +2,7 @@ class PurlFetcher::Client::Reader
2
2
  include Enumerable
3
3
  attr_reader :host, :conn, :range
4
4
 
5
- def initialize(host: 'https://purl-fetcher.stanford.edu', conn: nil)
5
+ def initialize(host: "https://purl-fetcher.stanford.edu", conn: nil)
6
6
  @host = host
7
7
  @conn = conn || Faraday.new(host) do |f|
8
8
  f.response :json
@@ -13,8 +13,8 @@ class PurlFetcher::Client::Reader
13
13
  def collection_members(druid)
14
14
  return to_enum(:collection_members, druid) unless block_given?
15
15
 
16
- paginated_get("/collections/druid:#{druid.delete_prefix('druid:')}/purls", 'purls').each do |obj, _meta|
17
- yield obj['druid'].delete_prefix('druid:')
16
+ paginated_get("/collections/druid:#{druid.delete_prefix('druid:')}/purls", "purls").each do |obj, _meta|
17
+ yield obj["druid"].delete_prefix("druid:")
18
18
  end
19
19
  end
20
20
 
@@ -55,7 +55,7 @@ class PurlFetcher::Client::Reader
55
55
 
56
56
  loop do
57
57
  data = fetch(path, { per_page: per_page, page: page }.merge(params))
58
- @range = data['range']
58
+ @range = data["range"]
59
59
 
60
60
  total += data[accessor].length
61
61
 
@@ -63,7 +63,7 @@ class PurlFetcher::Client::Reader
63
63
  yielder.yield element, self
64
64
  end
65
65
 
66
- page = data['pages']['next_page']
66
+ page = data["pages"]["next_page"]
67
67
 
68
68
  break if page.nil? || total >= max
69
69
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurlFetcher
4
+ class Client
5
+ # Transfer release tags to purl-fetcher
6
+ class ReleaseTags
7
+ # @param [String] druid the identifier of the object
8
+ # @param [Array<String>] index ([]) list of properties to index to
9
+ # @param [Array<String>] delete ([]) list of properties to delete from
10
+ def self.release(druid:, index: [], delete: [])
11
+ new(index:, delete:).release
12
+ end
13
+
14
+ # @param [String] druid the identifier of the object
15
+ # @param [Array<String>] index ([]) list of properties to index to
16
+ # @param [Array<String>] delete ([]) list of properties to delete from
17
+ def initialize(druid:, index: [], delete: [])
18
+ @druid = druid
19
+ @index = index
20
+ @delete = delete
21
+ end
22
+
23
+ def release
24
+ logger.debug("Starting an release request for: #{druid}")
25
+ response = client.put(path:, body:)
26
+ logger.debug("Release request complete")
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :druid, :index, :delete
32
+
33
+ def body
34
+ { index:, delete: }.to_json
35
+ end
36
+
37
+ def logger
38
+ Client.config.logger
39
+ end
40
+
41
+ def client
42
+ Client.instance
43
+ end
44
+
45
+ def path
46
+ "/v1/released/#{druid}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurlFetcher
4
+ class Client
5
+ # Delete an item from the purl-fetcher cache
6
+ class Unpublish
7
+ # @param [String] druid the identifier of the item
8
+ def self.unpublish(druid:)
9
+ new(druid:).unpublish
10
+ end
11
+
12
+ # @param [String] druid the identifier of the item
13
+ def initialize(druid:)
14
+ @druid = druid
15
+ end
16
+
17
+ def unpublish
18
+ logger.debug("Starting a unpublish request for: #{druid}")
19
+ response = client.delete(path:)
20
+ logger.debug("Unpublish request complete")
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :druid
26
+
27
+ def logger
28
+ Client.config.logger
29
+ end
30
+
31
+ def client
32
+ Client.instance
33
+ end
34
+
35
+ def path
36
+ "/purls/#{druid}"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurlFetcher
4
+ class Client
5
+ # The file uploading part of a transfer
6
+ class UploadFiles
7
+ # @param [Hash<String,DirectUploadRequest>] file_metadata map of relative filepaths to file metadata
8
+ # @param [Hash<String,String>] filepath_map map of relative filepaths to absolute filepaths
9
+ def self.upload(file_metadata:, filepath_map:)
10
+ new(file_metadata: file_metadata, filepath_map: filepath_map).upload
11
+ end
12
+
13
+ # @param [Hash<String,DirectUploadRequest>] file_metadata map of relative filepaths to file metadata
14
+ # @param [Hash<String,String>] filepath_map map of relative filepaths to absolute filepaths
15
+ def initialize(file_metadata:, filepath_map:)
16
+ @file_metadata = file_metadata
17
+ @filepath_map = filepath_map
18
+ end
19
+
20
+ # @return [Array<DirectUploadResponse>] the responses from the server for the uploads
21
+ def upload
22
+ file_metadata.map do |filepath, metadata|
23
+ direct_upload(metadata.to_json).tap do |response|
24
+ # ActiveStorage modifies the filename provided in response, so setting here with the relative filename
25
+ response = response.with_filename(filepath)
26
+ upload_file(response)
27
+ logger.info("Upload of #{filepath} complete")
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :file_metadata, :filepath_map
35
+
36
+ def logger
37
+ Client.config.logger
38
+ end
39
+
40
+ def client
41
+ Client.instance
42
+ end
43
+
44
+ def path
45
+ "/v1/direct_uploads"
46
+ end
47
+
48
+ def direct_upload(metadata_json)
49
+ logger.info("Starting an upload request: #{metadata_json}")
50
+ response = client.post(path: path, body: metadata_json)
51
+
52
+ logger.info("Response from server: #{response}")
53
+ DirectUploadResponse.new(**response.symbolize_keys)
54
+ end
55
+
56
+ def upload_file(response)
57
+ logger.info("Uploading `#{response.filename}' to #{response.direct_upload.fetch('url')}")
58
+
59
+ client.put(
60
+ path: response.direct_upload.fetch("url"),
61
+ body: ::File.open(filepath_map[response.filename]),
62
+ headers: {
63
+ "content-type" => response.content_type,
64
+ "content-length" => response.byte_size.to_s
65
+ }
66
+ )
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,5 +1,5 @@
1
1
  module PurlFetcher
2
- module Client
3
- VERSION = "1.0.0"
2
+ class Client
3
+ VERSION = "1.2.0"
4
4
  end
5
5
  end
@@ -1,14 +1,108 @@
1
+ require "active_support"
2
+ require "active_support/core_ext"
3
+ require "faraday"
4
+ require "singleton"
5
+ require "logger"
6
+
1
7
  require "purl_fetcher/client/version"
2
- require 'faraday'
8
+ require "purl_fetcher/client/reader"
9
+ require "purl_fetcher/client/upload_files"
10
+ require "purl_fetcher/client/direct_upload_request"
11
+ require "purl_fetcher/client/direct_upload_response"
12
+ require "purl_fetcher/client/legacy_publish"
13
+ require "purl_fetcher/client/release_tags"
14
+ require "purl_fetcher/client/unpublish"
3
15
 
4
16
  module PurlFetcher
5
- module Client
6
- require 'purl_fetcher/client/reader'
7
-
17
+ class Client
8
18
  # General error originating in PurlFetcher::Client
9
19
  class Error < StandardError; end
10
20
 
11
21
  # Raised when the response from the server is not successful
12
22
  class ResponseError < Error; end
23
+
24
+ include Singleton
25
+ class << self
26
+ def configure(url:, logger: default_logger, token: nil)
27
+ instance.config = Config.new(
28
+ url: url,
29
+ logger: logger,
30
+ token: token
31
+ )
32
+
33
+ instance
34
+ end
35
+
36
+ def default_logger
37
+ Logger.new($stdout)
38
+ end
39
+
40
+ delegate :config, to: :instance
41
+ end
42
+
43
+ attr_accessor :config
44
+
45
+ # Send an DELETE request
46
+ # @param path [String] the path for the API request
47
+ def delete(path:)
48
+ response = connection.delete(path)
49
+
50
+ raise "unexpected response: #{response.status} #{response.body}" unless response.success?
51
+
52
+ response.body
53
+ end
54
+
55
+ # Send an POST request
56
+ # @param path [String] the path for the API request
57
+ # @param body [String] the body of the POST request
58
+ def post(path:, body:)
59
+ response = connection.post(path) do |request|
60
+ request.body = body
61
+ end
62
+
63
+ raise "unexpected response: #{response.status} #{response.body}" unless response.success?
64
+
65
+ response.body
66
+ end
67
+
68
+ # Send an PUT request
69
+ # @param path [String] the path for the API request
70
+ # @param body [String] the body of the POST request
71
+ # @param headers [Hash] extra headers to add to the SDR API request
72
+ def put(path:, body:, headers: {})
73
+ response = connection.put(path) do |request|
74
+ request.body = body
75
+ request.headers = default_headers.merge(headers)
76
+ end
77
+
78
+ raise "unexpected response: #{response.status} #{response.body}" unless response.success?
79
+
80
+ response.body
81
+ end
82
+
83
+ private
84
+
85
+ Config = Data.define(:url, :logger, :token)
86
+
87
+ def connection
88
+ Faraday.new(
89
+ url: config.url,
90
+ headers: default_headers
91
+ ) do |conn|
92
+ conn.response :json
93
+ end
94
+ end
95
+
96
+ def default_headers
97
+ {
98
+ accept: "application/json",
99
+ content_type: "application/json",
100
+ authorization: auth_header
101
+ }.compact
102
+ end
103
+
104
+ def auth_header
105
+ "Bearer #{config.token}" if config.token
106
+ end
13
107
  end
14
108
  end
@@ -1,30 +1,30 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "purl_fetcher/client/version"
3
+ require 'purl_fetcher/client/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "purl_fetcher-client"
6
+ spec.name = 'purl_fetcher-client'
8
7
  spec.version = PurlFetcher::Client::VERSION
9
- spec.authors = ["Chris Beer"]
10
- spec.email = ["cabeer@stanford.edu"]
8
+ spec.authors = [ "Chris Beer" ]
9
+ spec.email = [ "cabeer@stanford.edu" ]
11
10
 
12
11
  spec.summary = 'Traject-compatible reader implementation for streaming data from purl-fetcher'
13
12
 
14
13
  # Specify which files should be added to the gem when it is released.
15
14
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
16
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
15
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
17
16
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
17
  end
19
- spec.bindir = "exe"
18
+ spec.bindir = 'exe'
20
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
- spec.require_paths = ["lib"]
20
+ spec.require_paths = [ "lib" ]
22
21
 
22
+ spec.add_dependency 'activesupport'
23
23
  spec.add_dependency 'faraday', '~> 2.1'
24
24
 
25
- spec.add_development_dependency "bundler"
26
- spec.add_development_dependency "debug"
27
- spec.add_development_dependency "rake"
28
- spec.add_development_dependency "rspec", "~> 3.0"
29
- spec.add_development_dependency "webmock"
25
+ spec.add_development_dependency 'bundler'
26
+ spec.add_development_dependency 'debug'
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec', '~> 3.0'
29
+ spec.add_development_dependency 'webmock'
30
30
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: purl_fetcher-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Beer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-24 00:00:00.000000000 Z
11
+ date: 2024-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
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
@@ -104,6 +118,7 @@ files:
104
118
  - ".github/workflows/ruby.yml"
105
119
  - ".gitignore"
106
120
  - ".rspec"
121
+ - ".rubocop.yml"
107
122
  - CODE_OF_CONDUCT.md
108
123
  - Gemfile
109
124
  - README.md
@@ -111,7 +126,13 @@ files:
111
126
  - bin/console
112
127
  - bin/setup
113
128
  - lib/purl_fetcher/client.rb
129
+ - lib/purl_fetcher/client/direct_upload_request.rb
130
+ - lib/purl_fetcher/client/direct_upload_response.rb
131
+ - lib/purl_fetcher/client/legacy_publish.rb
114
132
  - lib/purl_fetcher/client/reader.rb
133
+ - lib/purl_fetcher/client/release_tags.rb
134
+ - lib/purl_fetcher/client/unpublish.rb
135
+ - lib/purl_fetcher/client/upload_files.rb
115
136
  - lib/purl_fetcher/client/version.rb
116
137
  - purl_fetcher-client.gemspec
117
138
  homepage: