globus_client 0.3.2 → 0.4.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: 5209cc407cc6ecd58dde455757a44ec9ceefb0a1301688c4af7d9b5b21f6952f
4
- data.tar.gz: 76439856106a60b60f0ed6aa4e9072c5b85084581d589ceb407267b5b1e97d21
3
+ metadata.gz: 3a9de93796688ebf5d55d620e0cb290ed7e6b86f67ed1212e7847281ea5ac44d
4
+ data.tar.gz: b298b6a3744471cb4199e9c9f4ae2c69ece63dd9a3898dc058844d6bad22279c
5
5
  SHA512:
6
- metadata.gz: 57bbeaf0041224ff2c451dd85cf663ef59ef281bdf707976c4c1ddad0bff1b8b55630cb5890ee82fb275de5a7896f3e44606046e8122136783faa1b839116e3e
7
- data.tar.gz: 302a49a520934b6d68eee8e3a909834b8ec43468d263d241ccb5aaf8137ca994f5ceb86d036f747ccd699f8d250231c951ee1b2457f7652cd755cb498cc5dfa9
6
+ metadata.gz: ad0fe7f847270e9534458577dda9a75dad2e83298f0d8232ee8ccd7eca6e127a7290db944ac4dac31cad2df7b42f3e2b7ecc4f273a75059c1c39de2cdf820b09
7
+ data.tar.gz: 3488ec985fee144991bbe90c29345a7717cae4e371a843bdb0947a6042128391bf7442e522c03494340acfb1ce31b61dc23ca7a30f05bedc667fc459bcfeaea8
@@ -2,7 +2,7 @@
2
2
 
3
3
  # This script is called by our weekly dependency update job in Jenkins after updating Ruby and other deps
4
4
 
5
- # Switch to Ruby 3.1 for Globus::Client (3.0 is default in Jenkinsfile)
5
+ # Switch to Ruby 3.1 for GlobusClient (3.0 is default in Jenkinsfile)
6
6
  rvm use 3.1.2@globus_client --create &&
7
7
  gem install bundler &&
8
8
  bundle install --gemfile Gemfile
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- globus_client (0.3.2)
4
+ globus_client (0.4.0)
5
5
  activesupport (>= 4.2, < 8)
6
6
  faraday
7
7
  zeitwerk
data/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  [![Code Climate](https://codeclimate.com/github/sul-dlss/globus_client/badges/gpa.svg)](https://codeclimate.com/github/sul-dlss/globus_client)
4
4
  [![Code Climate Test Coverage](https://api.codeclimate.com/v1/badges/8e6bf1a5d3fc86a6fbd0/test_coverage)](https://codeclimate.com/github/sul-dlss/globus_client/test_coverage)
5
5
 
6
- # Globus::Client
6
+ # GlobusClient
7
7
 
8
- Globus::Client is a Ruby gem that acts as a client to the RESTful HTTP APIs provided by the [Globus service](https://docs.globus.org/api/).
8
+ GlobusClient is a Ruby gem that acts as a client to the RESTful HTTP APIs provided by the [Globus service](https://docs.globus.org/api/).
9
9
 
10
10
  ## Installation
11
11
 
@@ -25,7 +25,7 @@ For one-off requests:
25
25
  require 'globus_client'
26
26
 
27
27
  # NOTE: The settings below live in the consumer, not in the gem.
28
- client = Globus::Client.configure(
28
+ client = GlobusClient.configure(
29
29
  client_id: Settings.globus.client_id,
30
30
  client_secret: Settings.globus.client_secret,
31
31
  uploads_directory: Settings.globus.uploads_directory,
@@ -43,7 +43,7 @@ to be sure configuration has already occurred, e.g.:
43
43
 
44
44
  ```ruby
45
45
  # config/initializers/globus_client.rb
46
- Globus::Client.configure(
46
+ GlobusClient.configure(
47
47
  client_id: Settings.globus.client_id,
48
48
  client_secret: Settings.globus.client_secret,
49
49
  uploads_directory: Settings.globus.uploads_directory,
@@ -53,7 +53,7 @@ Globus::Client.configure(
53
53
  # app/services/my_globus_service.rb
54
54
  # ...
55
55
  def create_user_directory
56
- Globus::Client.mkdir(user_id: 'mjgiarlo', work_id: 1234, work_version: 1)
56
+ GlobusClient.mkdir(user_id: 'mjgiarlo', work_id: 1234, work_version: 1)
57
57
  end
58
58
  # ...
59
59
  ```
data/api_test.rb CHANGED
@@ -4,7 +4,7 @@
4
4
  require "bundler/setup"
5
5
  require "globus_client"
6
6
 
7
- Globus::Client.configure(
7
+ GlobusClient.configure(
8
8
  client_id: ENV["GLOBUS_CLIENT_ID"],
9
9
  client_secret: ENV["GLOBUS_CLIENT_SECRET"],
10
10
  uploads_directory: ENV["GLOBUS_UPLOADS_DIRECTORY"],
@@ -14,21 +14,21 @@ Globus::Client.configure(
14
14
  user_id, work_id, work_version = *ARGV
15
15
 
16
16
  # Test public API methods here.
17
- Globus::Client.mkdir(user_id:, work_id:, work_version:)
17
+ GlobusClient.mkdir(user_id:, work_id:, work_version:)
18
18
 
19
- user_exists = Globus::Client.user_exists?(user_id)
19
+ user_exists = GlobusClient.user_exists?(user_id)
20
20
 
21
21
  # Not part of the public API but this allows us to test access changes
22
- before_permissions = Globus::Client::Endpoint.new(Globus::Client.config, user_id: user_id, work_id: work_id, work_version: work_version).send(:access_rule)["permissions"]
22
+ before_permissions = GlobusClient::Endpoint.new(GlobusClient.config, user_id: user_id, work_id: work_id, work_version: work_version).send(:access_rule)["permissions"]
23
23
 
24
- files_count = Globus::Client.file_count(user_id:, work_id:, work_version:)
24
+ files_count = GlobusClient.file_count(user_id:, work_id:, work_version:)
25
25
 
26
- total_size = Globus::Client.total_size(user_id:, work_id:, work_version:)
26
+ total_size = GlobusClient.total_size(user_id:, work_id:, work_version:)
27
27
 
28
- Globus::Client.disallow_writes(user_id:, work_id:, work_version:)
28
+ GlobusClient.disallow_writes(user_id:, work_id:, work_version:)
29
29
 
30
30
  # Not part of the public API but this allows us to test access changes
31
- after_permissions = Globus::Client::Endpoint.new(Globus::Client.config, user_id: user_id, work_id: work_id, work_version: work_version).send(:access_rule)["permissions"]
31
+ after_permissions = GlobusClient::Endpoint.new(GlobusClient.config, user_id: user_id, work_id: work_id, work_version: work_version).send(:access_rule)["permissions"]
32
32
 
33
33
  puts "User #{user_id} exists: #{user_exists}"
34
34
  puts "Initial directory permissions: #{before_permissions}"
@@ -2,11 +2,11 @@
2
2
 
3
3
  lib = File.expand_path("lib", __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require "globus/client/version"
5
+ require "globus_client/version"
6
6
 
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = "globus_client"
9
- spec.version = Globus::Client::VERSION
9
+ spec.version = GlobusClient::VERSION
10
10
  spec.authors = ["Aaron Collier", "Laura Wrubel", "Mike Giarlo"]
11
11
  spec.email = ["aaron.collier@stanford.edu", "lwrubel@stanford.edu", "mjgiarlo@stanford.edu"]
12
12
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GlobusClient
4
+ # The namespace for the "login" command
5
+ class Authenticator
6
+ def self.token(client_id, client_secret, auth_url)
7
+ new(client_id, client_secret, auth_url).token
8
+ end
9
+
10
+ def initialize(client_id, client_secret, auth_url)
11
+ @client_id = client_id
12
+ @client_secret = client_secret
13
+ @auth_url = auth_url
14
+ end
15
+
16
+ # Request an access_token
17
+ def token
18
+ response = connection.post("/v2/oauth2/token", form_data)
19
+
20
+ JSON.parse(response.body)["access_token"]
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :client_id, :client_secret, :auth_url
26
+
27
+ def connection
28
+ Faraday.new(url: auth_url)
29
+ end
30
+
31
+ def form_data
32
+ {
33
+ client_id:,
34
+ client_secret:,
35
+ encoding: "form",
36
+ grant_type: "client_credentials",
37
+ scope: "urn:globus:auth:scope:transfer.api.globus.org:all"
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GlobusClient
4
+ # The namespace for endpoint API operations
5
+ class Endpoint
6
+ # @param config [#token, #uploads_directory, #transfer_endpoint_id, #transfer_url, #auth_url] configuration for the gem
7
+ # @param user_id [String] conventionally, we use the SUNet ID, not an email address
8
+ # @param work_id [#to_s] the identifier of the work (e.g., an H2 work)
9
+ # @param work_version [#to_s] the version of the work (e.g., an H2 version)
10
+ def initialize(config, user_id:, work_id:, work_version:)
11
+ @config = config
12
+ @user_id = user_id
13
+ @work_id = work_id
14
+ @work_version = work_version
15
+ end
16
+
17
+ def file_count
18
+ objects["total"]
19
+ end
20
+
21
+ def total_size
22
+ files.sum { |file| file["size"] }
23
+ end
24
+
25
+ # Create a directory https://docs.globus.org/api/transfer/file_operations/#make_directory
26
+ def mkdir
27
+ # transfer API does not support recursive directory creation
28
+ paths.each do |path|
29
+ response = connection.post("#{transfer_path}/mkdir") do |req|
30
+ req.headers["Content-Type"] = "application/json"
31
+ req.body = {
32
+ DATA_TYPE: "mkdir",
33
+ path:
34
+ }.to_json
35
+ end
36
+
37
+ next if response.success?
38
+
39
+ # Ignore error if directory already exists
40
+ if response.status == 502
41
+ error = JSON.parse(response.body)
42
+ next if error["code"] == "ExternalError.MkdirFailed.Exists"
43
+ end
44
+
45
+ UnexpectedResponse.call(response)
46
+ end
47
+ end
48
+
49
+ # Assign a user read/write permissions for a directory https://docs.globus.org/api/transfer/acl/#rest_access_create
50
+ def allow_writes
51
+ access_request(permissions: "rw")
52
+ end
53
+
54
+ # Assign a user read-only permissions for a directory https://docs.globus.org/api/transfer/acl/#rest_access_create
55
+ def disallow_writes
56
+ access_request(permissions: "r")
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :config, :user_id, :work_id, :work_version
62
+
63
+ def connection
64
+ # Transfer API connection
65
+ Faraday.new(
66
+ url: config.transfer_url,
67
+ headers: {Authorization: "Bearer #{config.token}"}
68
+ )
69
+ end
70
+
71
+ def user
72
+ Identity.new(config).get_identity_id(user_id)
73
+ end
74
+
75
+ # Builds up a path from a list of path elements. E.g., input would look like:
76
+ # ["mjgiarlo", "work123", "version1"]
77
+ # And this method returns:
78
+ # ["/uploads/mjgiarlo/", "/uploads/mjgiarlo/work123/", "/uploads/mjgiarlo/work123/version1/"]
79
+ def paths
80
+ path_segments.map.with_index do |_segment, index|
81
+ File.join(config.uploads_directory, path_segments.slice(..index)).concat("/")
82
+ end
83
+ end
84
+
85
+ # @see #paths
86
+ def full_path
87
+ paths.last
88
+ end
89
+
90
+ def path_segments
91
+ [user_id, "work#{work_id}", "version#{work_version}"]
92
+ end
93
+
94
+ def objects
95
+ # List files at an endpoint https://docs.globus.org/api/transfer/file_operations/#list_directory_contents
96
+ response = connection.get("#{transfer_path}/ls?path=#{full_path}")
97
+ return JSON.parse(response.body) if response.success?
98
+
99
+ UnexpectedResponse.call(response)
100
+ end
101
+
102
+ def files
103
+ objects["DATA"].select { |object| object["DATA_TYPE"] == "file" }
104
+ end
105
+
106
+ def access_request(permissions:)
107
+ response = if access_rule_id
108
+ connection.put("#{access_path}/#{access_rule_id}") do |req|
109
+ req.body = {
110
+ DATA_TYPE: "access",
111
+ permissions:
112
+ }.to_json
113
+ req.headers["Content-Type"] = "application/json"
114
+ end
115
+ else
116
+ connection.post(access_path) do |req|
117
+ req.body = {
118
+ DATA_TYPE: "access",
119
+ principal_type: "identity",
120
+ principal: user,
121
+ path: full_path,
122
+ permissions:,
123
+ notify_email: "#{user_id}@stanford.edu"
124
+ }.to_json
125
+ req.headers["Content-Type"] = "application/json"
126
+ end
127
+ end
128
+
129
+ return response if response.success?
130
+
131
+ UnexpectedResponse.call(response)
132
+ end
133
+
134
+ def access_rule
135
+ response = connection.get(access_list_path) do |req|
136
+ req.headers["Content-Type"] = "application/json"
137
+ end
138
+
139
+ JSON
140
+ .parse(response.body)["DATA"]
141
+ .find { |acl| acl["path"] == full_path }
142
+ end
143
+
144
+ def access_rule_id
145
+ access_rule&.fetch("id")
146
+ end
147
+
148
+ def transfer_path
149
+ "/v0.10/operation/endpoint/#{config.transfer_endpoint_id}"
150
+ end
151
+
152
+ def access_path
153
+ "/v0.10/endpoint/#{config.transfer_endpoint_id}/access"
154
+ end
155
+
156
+ def access_list_path
157
+ "/v0.10/endpoint/#{config.transfer_endpoint_id}/access_list"
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GlobusClient
4
+ # Lookup of a Globus identity ID
5
+ class Identity
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def get_identity_id(sunetid)
11
+ @email = "#{sunetid}@stanford.edu"
12
+
13
+ response = lookup_identity
14
+ UnexpectedResponse.call(response) unless response.success?
15
+
16
+ data = JSON.parse(response.body)
17
+ extract_id(data)
18
+ end
19
+
20
+ def exists?(sunetid)
21
+ get_identity_id(sunetid)
22
+ true
23
+ rescue
24
+ false
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :config
30
+
31
+ def connection
32
+ Faraday.new(url: config.auth_url)
33
+ end
34
+
35
+ def lookup_identity
36
+ id_endpoint = "/v2/api/identities"
37
+ connection.get(id_endpoint) do |req|
38
+ req.params["usernames"] = @email
39
+ req.headers["Authorization"] = "Bearer #{config.token}"
40
+ end
41
+ end
42
+
43
+ def extract_id(data)
44
+ identities = data["identities"]
45
+ # Select identity with "used" or "private" status
46
+ matching_users = identities.select { |id| id["username"] == @email }
47
+ active_users = matching_users.select { |user| (user["status"] == "used" || user["status"] == "private") }
48
+ raise "No matching active Globus user found for #{@email}." if active_users.empty?
49
+
50
+ active_users.first["id"]
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GlobusClient
4
+ # Handles unexpected responses when communicating with Globus
5
+ class UnexpectedResponse
6
+ # Error raised when the Globus Auth or Transfer API returns a 400 error
7
+ class BadRequestError < StandardError; end
8
+
9
+ # Error raised by the Globus Auth API returns a 401 Unauthorized
10
+ class UnauthorizedError < StandardError; end
11
+
12
+ # Error raised when the Globus Auth or Transfer API returns a 403 Forbidden
13
+ class ForbiddenError < StandardError; end
14
+
15
+ # Error raised when the Globus Auth or Transfer API returns a 404 NotFound
16
+ class ResourceNotFound < StandardError; end
17
+
18
+ # Error raised when the Globus Transfer API returns a 502 Bad Gateway
19
+ class EndpointError < StandardError; end
20
+
21
+ # Error raised when the remote server returns a 503 Bad Gateway
22
+ class ServiceUnavailable < StandardError; end
23
+
24
+ # @param [Faraday::Response] response
25
+ # https://docs.globus.org/api/transfer/file_operations/#common_errors
26
+ # https://docs.globus.org/api/transfer/file_operations/#errors
27
+ # https://docs.globus.org/api/transfer/acl/#common_errors
28
+ # https://docs.globus.org/api/auth/reference/
29
+ def self.call(response)
30
+ case response.status
31
+ when 400
32
+ raise BadRequestError, "Invalid path or another error with the request: #{response.body}"
33
+ when 401
34
+ raise UnauthorizedError, "There was a problem with the access token: #{response.body} "
35
+ when 403
36
+ raise ForbiddenError, "The operation requires privileges which the client does not have: #{response.body}"
37
+ when 404
38
+ raise ResourceNotFound, "Endpoint ID not found or resource does not exist: #{response.body}"
39
+ when 502
40
+ raise EndpointError, "Other error with endpoint: #{response.body}"
41
+ when 503
42
+ raise ServiceUnavailable, "The service is down for maintenance."
43
+ else
44
+ raise StandardError, "Unexpected response: #{response.status} #{response.body}"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GlobusClient
4
+ VERSION = "0.4.0"
5
+ end
data/lib/globus_client.rb CHANGED
@@ -1,5 +1,73 @@
1
- require "globus/client"
1
+ # frozen_string_literal: true
2
2
 
3
- # This is here to make Zeitwerk happy
4
- module GlobusClient
3
+ require "active_support/core_ext/module/delegation"
4
+ require "faraday"
5
+ require "ostruct"
6
+ require "singleton"
7
+ require "zeitwerk"
8
+
9
+ # Load the gem's internal dependencies: use Zeitwerk instead of needing to manually require classes
10
+ Zeitwerk::Loader.for_gem.setup
11
+
12
+ # Client for interacting with the Globus API
13
+ class GlobusClient
14
+ include Singleton
15
+
16
+ class << self
17
+ # @param client_id [String] the client identifier registered with Globus
18
+ # @param client_secret [String] the client secret to authenticate with Globus
19
+ # @param uploads_directory [String] where to upload files
20
+ # @param transfer_endpoint_id [String] the transfer API endpoint ID supplied by Globus
21
+ # @param transfer_url [String] the transfer API URL
22
+ # @param auth_url [String] the authentication API URL
23
+ def configure(client_id:, client_secret:, uploads_directory:, transfer_endpoint_id:, transfer_url: default_transfer_url, auth_url: default_auth_url)
24
+ instance.config = OpenStruct.new(
25
+ token: Authenticator.token(client_id, client_secret, auth_url),
26
+ uploads_directory:,
27
+ transfer_endpoint_id:,
28
+ transfer_url:,
29
+ auth_url:
30
+ )
31
+
32
+ self
33
+ end
34
+
35
+ delegate :config, :disallow_writes, :file_count, :mkdir, :total_size, :user_exists?, to: :instance
36
+
37
+ def default_transfer_url
38
+ "https://transfer.api.globusonline.org"
39
+ end
40
+
41
+ def default_auth_url
42
+ "https://auth.globus.org"
43
+ end
44
+ end
45
+
46
+ attr_accessor :config
47
+
48
+ def mkdir(...)
49
+ endpoint = Endpoint.new(config, ...)
50
+ endpoint.mkdir
51
+ endpoint.allow_writes
52
+ end
53
+
54
+ def disallow_writes(...)
55
+ endpoint = Endpoint.new(config, ...)
56
+ endpoint.disallow_writes
57
+ end
58
+
59
+ def file_count(...)
60
+ endpoint = Endpoint.new(config, ...)
61
+ endpoint.file_count
62
+ end
63
+
64
+ def total_size(...)
65
+ endpoint = Endpoint.new(config, ...)
66
+ endpoint.total_size
67
+ end
68
+
69
+ def user_exists?(...)
70
+ identity = Identity.new(config)
71
+ identity.exists?(...)
72
+ end
5
73
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: globus_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Collier
@@ -166,14 +166,12 @@ files:
166
166
  - Rakefile
167
167
  - api_test.rb
168
168
  - globus_client.gemspec
169
- - lib/globus/client.rb
170
- - lib/globus/client/authenticator.rb
171
- - lib/globus/client/endpoint.rb
172
- - lib/globus/client/identity.rb
173
- - lib/globus/client/unexpected_response.rb
174
- - lib/globus/client/version.rb
175
169
  - lib/globus_client.rb
176
- - sig/globus_client.rbs
170
+ - lib/globus_client/authenticator.rb
171
+ - lib/globus_client/endpoint.rb
172
+ - lib/globus_client/identity.rb
173
+ - lib/globus_client/unexpected_response.rb
174
+ - lib/globus_client/version.rb
177
175
  homepage: https://github.com/sul-dlss/globus_client
178
176
  licenses: []
179
177
  metadata:
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Globus
4
- class Client
5
- # The namespace for the "login" command
6
- class Authenticator
7
- def self.token(client_id, client_secret, auth_url)
8
- new(client_id, client_secret, auth_url).token
9
- end
10
-
11
- def initialize(client_id, client_secret, auth_url)
12
- @client_id = client_id
13
- @client_secret = client_secret
14
- @auth_url = auth_url
15
- end
16
-
17
- # Request an access_token
18
- def token
19
- response = connection.post("/v2/oauth2/token", form_data)
20
-
21
- JSON.parse(response.body)["access_token"]
22
- end
23
-
24
- private
25
-
26
- attr_reader :client_id, :client_secret, :auth_url
27
-
28
- def connection
29
- Faraday.new(url: auth_url)
30
- end
31
-
32
- def form_data
33
- {
34
- client_id:,
35
- client_secret:,
36
- encoding: "form",
37
- grant_type: "client_credentials",
38
- scope: "urn:globus:auth:scope:transfer.api.globus.org:all"
39
- }
40
- end
41
- end
42
- end
43
- end
@@ -1,162 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Globus
4
- class Client
5
- # The namespace for endpoint API operations
6
- class Endpoint
7
- # @param config [#token, #uploads_directory, #transfer_endpoint_id, #transfer_url, #auth_url] configuration for the gem
8
- # @param user_id [String] conventionally, we use the SUNet ID, not an email address
9
- # @param work_id [#to_s] the identifier of the work (e.g., an H2 work)
10
- # @param work_version [#to_s] the version of the work (e.g., an H2 version)
11
- def initialize(config, user_id:, work_id:, work_version:)
12
- @config = config
13
- @user_id = user_id
14
- @work_id = work_id
15
- @work_version = work_version
16
- end
17
-
18
- def file_count
19
- objects["total"]
20
- end
21
-
22
- def total_size
23
- files.sum { |file| file["size"] }
24
- end
25
-
26
- # Create a directory https://docs.globus.org/api/transfer/file_operations/#make_directory
27
- def mkdir
28
- # transfer API does not support recursive directory creation
29
- paths.each do |path|
30
- response = connection.post("#{transfer_path}/mkdir") do |req|
31
- req.headers["Content-Type"] = "application/json"
32
- req.body = {
33
- DATA_TYPE: "mkdir",
34
- path:
35
- }.to_json
36
- end
37
-
38
- next if response.success?
39
-
40
- # Ignore error if directory already exists
41
- if response.status == 502
42
- error = JSON.parse(response.body)
43
- next if error["code"] == "ExternalError.MkdirFailed.Exists"
44
- end
45
-
46
- UnexpectedResponse.call(response)
47
- end
48
- end
49
-
50
- # Assign a user read/write permissions for a directory https://docs.globus.org/api/transfer/acl/#rest_access_create
51
- def allow_writes
52
- access_request(permissions: "rw")
53
- end
54
-
55
- # Assign a user read-only permissions for a directory https://docs.globus.org/api/transfer/acl/#rest_access_create
56
- def disallow_writes
57
- access_request(permissions: "r")
58
- end
59
-
60
- private
61
-
62
- attr_reader :config, :user_id, :work_id, :work_version
63
-
64
- def connection
65
- # Transfer API connection
66
- Faraday.new(
67
- url: config.transfer_url,
68
- headers: {Authorization: "Bearer #{config.token}"}
69
- )
70
- end
71
-
72
- def user
73
- Identity.new(config).get_identity_id(user_id)
74
- end
75
-
76
- # Builds up a path from a list of path elements. E.g., input would look like:
77
- # ["mjgiarlo", "work123", "version1"]
78
- # And this method returns:
79
- # ["/uploads/mjgiarlo/", "/uploads/mjgiarlo/work123/", "/uploads/mjgiarlo/work123/version1/"]
80
- def paths
81
- path_segments.map.with_index do |_segment, index|
82
- File.join(config.uploads_directory, path_segments.slice(..index)).concat("/")
83
- end
84
- end
85
-
86
- # @see #paths
87
- def full_path
88
- paths.last
89
- end
90
-
91
- def path_segments
92
- [user_id, "work#{work_id}", "version#{work_version}"]
93
- end
94
-
95
- def objects
96
- # List files at an endpoint https://docs.globus.org/api/transfer/file_operations/#list_directory_contents
97
- response = connection.get("#{transfer_path}/ls?path=#{full_path}")
98
- return JSON.parse(response.body) if response.success?
99
-
100
- UnexpectedResponse.call(response)
101
- end
102
-
103
- def files
104
- objects["DATA"].select { |object| object["DATA_TYPE"] == "file" }
105
- end
106
-
107
- def access_request(permissions:)
108
- response = if access_rule_id
109
- connection.put("#{access_path}/#{access_rule_id}") do |req|
110
- req.body = {
111
- DATA_TYPE: "access",
112
- permissions:
113
- }.to_json
114
- req.headers["Content-Type"] = "application/json"
115
- end
116
- else
117
- connection.post(access_path) do |req|
118
- req.body = {
119
- DATA_TYPE: "access",
120
- principal_type: "identity",
121
- principal: user,
122
- path: full_path,
123
- permissions:,
124
- notify_email: "#{user_id}@stanford.edu"
125
- }.to_json
126
- req.headers["Content-Type"] = "application/json"
127
- end
128
- end
129
-
130
- return response if response.success?
131
-
132
- UnexpectedResponse.call(response)
133
- end
134
-
135
- def access_rule
136
- response = connection.get(access_list_path) do |req|
137
- req.headers["Content-Type"] = "application/json"
138
- end
139
-
140
- JSON
141
- .parse(response.body)["DATA"]
142
- .find { |acl| acl["path"] == full_path }
143
- end
144
-
145
- def access_rule_id
146
- access_rule&.fetch("id")
147
- end
148
-
149
- def transfer_path
150
- "/v0.10/operation/endpoint/#{config.transfer_endpoint_id}"
151
- end
152
-
153
- def access_path
154
- "/v0.10/endpoint/#{config.transfer_endpoint_id}/access"
155
- end
156
-
157
- def access_list_path
158
- "/v0.10/endpoint/#{config.transfer_endpoint_id}/access_list"
159
- end
160
- end
161
- end
162
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Globus
4
- class Client
5
- # Lookup of a Globus identity ID
6
- class Identity
7
- def initialize(config)
8
- @config = config
9
- end
10
-
11
- def get_identity_id(sunetid)
12
- @email = "#{sunetid}@stanford.edu"
13
-
14
- response = lookup_identity
15
- UnexpectedResponse.call(response) unless response.success?
16
-
17
- data = JSON.parse(response.body)
18
- extract_id(data)
19
- end
20
-
21
- def exists?(sunetid)
22
- get_identity_id(sunetid)
23
- true
24
- rescue
25
- false
26
- end
27
-
28
- private
29
-
30
- attr_reader :config
31
-
32
- def connection
33
- Faraday.new(url: config.auth_url)
34
- end
35
-
36
- def lookup_identity
37
- id_endpoint = "/v2/api/identities"
38
- connection.get(id_endpoint) do |req|
39
- req.params["usernames"] = @email
40
- req.headers["Authorization"] = "Bearer #{config.token}"
41
- end
42
- end
43
-
44
- def extract_id(data)
45
- identities = data["identities"]
46
- # Select identity with "used" or "private" status
47
- matching_users = identities.select { |id| id["username"] == @email }
48
- active_users = matching_users.select { |user| (user["status"] == "used" || user["status"] == "private") }
49
- raise "No matching active Globus user found for #{@email}." if active_users.empty?
50
-
51
- active_users.first["id"]
52
- end
53
- end
54
- end
55
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Globus
4
- class Client
5
- # Handles unexpected responses when communicating with Globus
6
- class UnexpectedResponse
7
- # Error raised when the Globus Auth or Transfer API returns a 400 error
8
- class BadRequestError < StandardError; end
9
-
10
- # Error raised by the Globus Auth API returns a 401 Unauthorized
11
- class UnauthorizedError < StandardError; end
12
-
13
- # Error raised when the Globus Auth or Transfer API returns a 403 Forbidden
14
- class ForbiddenError < StandardError; end
15
-
16
- # Error raised when the Globus Auth or Transfer API returns a 404 NotFound
17
- class ResourceNotFound < StandardError; end
18
-
19
- # Error raised when the Globus Transfer API returns a 502 Bad Gateway
20
- class EndpointError < StandardError; end
21
-
22
- # Error raised when the remote server returns a 503 Bad Gateway
23
- class ServiceUnavailable < StandardError; end
24
-
25
- # @param [Faraday::Response] response
26
- # https://docs.globus.org/api/transfer/file_operations/#common_errors
27
- # https://docs.globus.org/api/transfer/file_operations/#errors
28
- # https://docs.globus.org/api/transfer/acl/#common_errors
29
- # https://docs.globus.org/api/auth/reference/
30
- def self.call(response)
31
- case response.status
32
- when 400
33
- raise BadRequestError, "Invalid path or another error with the request: #{response.body}"
34
- when 401
35
- raise UnauthorizedError, "There was a problem with the access token: #{response.body} "
36
- when 403
37
- raise ForbiddenError, "The operation requires privileges which the client does not have: #{response.body}"
38
- when 404
39
- raise ResourceNotFound, "Endpoint ID not found or resource does not exist: #{response.body}"
40
- when 502
41
- raise EndpointError, "Other error with endpoint: #{response.body}"
42
- when 503
43
- raise ServiceUnavailable, "The service is down for maintenance."
44
- else
45
- raise StandardError, "Unexpected response: #{response.status} #{response.body}"
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Globus
4
- class Client
5
- VERSION = "0.3.2"
6
- end
7
- end
data/lib/globus/client.rb DELETED
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/core_ext/module/delegation"
4
- require "faraday"
5
- require "ostruct"
6
- require "singleton"
7
- require "zeitwerk"
8
-
9
- # Load the gem's internal dependencies
10
- loader = Zeitwerk::Loader.new
11
- loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
12
- loader.push_dir(File.absolute_path("#{__FILE__}/../.."))
13
- loader.setup
14
-
15
- module Globus
16
- # Client for interacting with the Globus API
17
- class Client
18
- include Singleton
19
-
20
- class << self
21
- # @param client_id [String] the client identifier registered with Globus
22
- # @param client_secret [String] the client secret to authenticate with Globus
23
- # @param uploads_directory [String] where to upload files
24
- # @param transfer_endpoint_id [String] the transfer API endpoint ID supplied by Globus
25
- # @param transfer_url [String] the transfer API URL
26
- # @param auth_url [String] the authentication API URL
27
- def configure(client_id:, client_secret:, uploads_directory:, transfer_endpoint_id:, transfer_url: default_transfer_url, auth_url: default_auth_url)
28
- instance.config = OpenStruct.new(
29
- token: Globus::Client::Authenticator.token(client_id, client_secret, auth_url),
30
- uploads_directory:,
31
- transfer_endpoint_id:,
32
- transfer_url:,
33
- auth_url:
34
- )
35
-
36
- self
37
- end
38
-
39
- delegate :config, :disallow_writes, :file_count, :mkdir, :total_size, :user_exists?, to: :instance
40
-
41
- def default_transfer_url
42
- "https://transfer.api.globusonline.org"
43
- end
44
-
45
- def default_auth_url
46
- "https://auth.globus.org"
47
- end
48
- end
49
-
50
- attr_accessor :config
51
-
52
- def mkdir(...)
53
- endpoint = Globus::Client::Endpoint.new(config, ...)
54
- endpoint.mkdir
55
- endpoint.allow_writes
56
- end
57
-
58
- def disallow_writes(...)
59
- endpoint = Globus::Client::Endpoint.new(config, ...)
60
- endpoint.disallow_writes
61
- end
62
-
63
- def file_count(...)
64
- endpoint = Globus::Client::Endpoint.new(config, ...)
65
- endpoint.file_count
66
- end
67
-
68
- def total_size(...)
69
- endpoint = Globus::Client::Endpoint.new(config, ...)
70
- endpoint.total_size
71
- end
72
-
73
- def user_exists?(...)
74
- identity = Globus::Client::Identity.new(config)
75
- identity.exists?(...)
76
- end
77
- end
78
- end
@@ -1,4 +0,0 @@
1
- module GlobusClient
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end