purl_fetcher-client 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: