purl_fetcher-client 1.0.0 → 1.1.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: cb1a08fc0010f80045db352cb2bfb2735dd2b7447513ba366281cc4d488895e4
4
+ data.tar.gz: a2213eda4272694dcf1e889ed9185956b0ba1da24e98056e2360a242e9eb6a0b
5
5
  SHA512:
6
- metadata.gz: fa1bce8d7e6ef5090a19d8cf8ecf42eeeb24f2c5ef59345c77ded4b50752f01e1b18307dd65aafcd0aa8c4873210becf6fcdfacb5cdd6179034c023a609d3218
7
- data.tar.gz: 8f76a7eecfff70435931364f827571733cc6f4d9e7cc90d8f65276580eab0dbcd5cd8eddd5a146b3e7fe016b2f4d69a3dff177e77e941ef7bbe47e49b975d3dc
6
+ metadata.gz: f8a0200ff798158ca1b5c58c120c94b1cb3e8f172cec1c28b30b1a1cae47291483dc5d681467a17f08d7ef650a14bf9fffc0c8d33895fd36565e2b2ea7e70ac9
7
+ data.tar.gz: 4600d53676d35892f3485f9cbc280afb4cc1fd8cedd9028794376cb20b9a49ad6d3981279e84e89b56549b87173fdc106aaa09f459d1d461a72675b2c2c2ca12
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
@@ -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,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.1.0"
4
4
  end
5
5
  end
@@ -1,14 +1,92 @@
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"
3
12
 
4
13
  module PurlFetcher
5
- module Client
6
- require 'purl_fetcher/client/reader'
7
-
14
+ class Client
8
15
  # General error originating in PurlFetcher::Client
9
16
  class Error < StandardError; end
10
17
 
11
18
  # Raised when the response from the server is not successful
12
19
  class ResponseError < Error; end
20
+
21
+ include Singleton
22
+ class << self
23
+ def configure(url:, logger: default_logger, token: nil)
24
+ instance.config = Config.new(
25
+ url: url,
26
+ logger: logger,
27
+ token: token
28
+ )
29
+
30
+ instance
31
+ end
32
+
33
+ def default_logger
34
+ Logger.new($stdout)
35
+ end
36
+
37
+ delegate :config, to: :instance
38
+ end
39
+
40
+ attr_accessor :config
41
+
42
+ # Send an POST request
43
+ # @param path [String] the path for the API request
44
+ # @param body [String] the body of the POST request
45
+ def post(path:, body:)
46
+ response = connection.post(path) do |request|
47
+ request.body = body
48
+ end
49
+
50
+ raise "unexpected response: #{response.status} #{response.body}" unless response.success?
51
+
52
+ response.body
53
+ end
54
+
55
+ # Send an PUT request
56
+ # @param path [String] the path for the API request
57
+ # @param body [String] the body of the POST request
58
+ # @param headers [Hash] extra headers to add to the SDR API request
59
+ def put(path:, body:, headers: {})
60
+ response = connection.put(path) do |request|
61
+ request.body = body
62
+ request.headers = default_headers.merge(headers)
63
+ end
64
+
65
+ raise "unexpected response: #{response.status} #{response.body}" unless response.success?
66
+
67
+ response.body
68
+ end
69
+
70
+ private
71
+
72
+ Config = Data.define(:url, :logger, :token)
73
+
74
+ def connection
75
+ Faraday.new(
76
+ url: config.url,
77
+ headers: default_headers
78
+ ) do |conn|
79
+ conn.response :json
80
+ end
81
+ end
82
+
83
+ def default_headers
84
+ {
85
+ accept: "application/json",
86
+ content_type: "application/json"
87
+ }.tap do |headers|
88
+ headers[:authorization] = "Bearer #{config.token}" if config.token
89
+ end
90
+ end
13
91
  end
14
92
  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.1.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-02 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,10 @@ 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
114
131
  - lib/purl_fetcher/client/reader.rb
132
+ - lib/purl_fetcher/client/upload_files.rb
115
133
  - lib/purl_fetcher/client/version.rb
116
134
  - purl_fetcher-client.gemspec
117
135
  homepage: