globus_client 0.3.1 → 0.4.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: 37387fc87518bb7b41b617d8bd5bd43e10e3a90ac5a301d35ce276a7fd16d0df
4
- data.tar.gz: f8a624b5d07f3d1f140af118bcda125a779e7c4650f1da54c04217525734d8b4
3
+ metadata.gz: 3a9de93796688ebf5d55d620e0cb290ed7e6b86f67ed1212e7847281ea5ac44d
4
+ data.tar.gz: b298b6a3744471cb4199e9c9f4ae2c69ece63dd9a3898dc058844d6bad22279c
5
5
  SHA512:
6
- metadata.gz: 973dcfc397df77f417ab70e30c5ef1da9b1b681a6f37a97ffd38c26a735af470f862840ca17e3bbfd5423f7417332578f4ff078a87728f94dd144213a488b187
7
- data.tar.gz: 74869d43ab436dc23a5fb3aa191e7e696ccf4d2f20ea590aeeaa966f059732a17eb8e00cb6299573ab4d5557848a0ac90bda9fb78ce46e1944417e150adbc277
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.1)
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 +1,73 @@
1
- require "globus/client"
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: 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
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.1
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.1"
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