atlas_rb 1.4.0 → 1.6.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 +4 -4
- data/.version +1 -1
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +1 -1
- data/lib/atlas_rb/blob.rb +47 -23
- data/lib/atlas_rb/errors.rb +35 -0
- data/lib/atlas_rb/faraday_helper.rb +50 -8
- data/lib/atlas_rb/file_set.rb +34 -12
- data/lib/atlas_rb/middleware/raise_on_resource_error.rb +28 -8
- data/lib/atlas_rb.rb +13 -10
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: decab10cfb0c67c68615a9695a8c5084aa1bd349e199dccd77a76d024a3e7ba3
|
|
4
|
+
data.tar.gz: 90c4db9a3ffd80e9f226334f4862b9982534e431a3a6eb58746bfa96442ee701
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7fc789c2d47882f6d93664de27f7ab2f2a0d7d43957f4244436600e4247a332c5f8b3df030a5b11fb064187bd55439c0417a04974ca321df88861d47e0da2f15
|
|
7
|
+
data.tar.gz: 7189e8bcc3b4d483f623cfdc7a0881f41735646ef085f05a1418436b927fc84c2be514844ea44572c0d2c85f48e057be3a86d5774ac55bf8e09ae3ac4238fc4a
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.6.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Added — optional auth for `Reset.clean`
|
|
6
|
+
|
|
7
|
+
`AtlasRb::Reset.clean` now uses **optional auth**: it signs an assertion when a
|
|
8
|
+
credential is available and sends no `Authorization` header otherwise, instead
|
|
9
|
+
of raising `AtlasRb::ConfigurationError`. Atlas serves `GET /reset` with
|
|
10
|
+
`require_auth` skipped (env-gated), so the call no longer needs an acting nuid
|
|
11
|
+
or a configured signer just to satisfy the client-side header builder — fixing
|
|
12
|
+
test `before(:suite)` resets that run before any acting principal is set.
|
|
13
|
+
|
|
14
|
+
`FaradayHelper#connection` gains an `auth:` keyword (`:required` default,
|
|
15
|
+
`:optional`) to support this; every other endpoint stays strict and still
|
|
16
|
+
raises on a missing credential.
|
|
17
|
+
|
|
3
18
|
## 1.4.0
|
|
4
19
|
|
|
5
20
|
### Removed — legacy `ATLAS_TOKEN` relay
|
data/Gemfile.lock
CHANGED
data/lib/atlas_rb/blob.rb
CHANGED
|
@@ -24,11 +24,15 @@ module AtlasRb
|
|
|
24
24
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
25
25
|
# omitted.
|
|
26
26
|
# @return [Hash] the `"blob"` object, already unwrapped — typically
|
|
27
|
-
# includes `"id"`, `"original_filename"`, `"size"`,
|
|
27
|
+
# includes `"id"`, `"original_filename"`, `"size"`, `"digest"` (the
|
|
28
|
+
# recorded fixity digest `"sha512:<hex>"`, or `nil` for a Blob with no
|
|
29
|
+
# held bytes — reconciliation compares this against the v1 manifest
|
|
30
|
+
# without re-downloading), and a download URL.
|
|
28
31
|
#
|
|
29
32
|
# @example
|
|
30
33
|
# AtlasRb::Blob.find("b-321")
|
|
31
|
-
# # => { "id" => "b-321", "original_filename" => "scan.pdf",
|
|
34
|
+
# # => { "id" => "b-321", "original_filename" => "scan.pdf",
|
|
35
|
+
# # "digest" => "sha512:9f86d0…", ... }
|
|
32
36
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
33
37
|
AtlasRb::Mash.new(JSON.parse(
|
|
34
38
|
connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
|
|
@@ -82,33 +86,44 @@ module AtlasRb
|
|
|
82
86
|
# @param idempotency_key [String, nil] optional UUID. A repeat call with
|
|
83
87
|
# the same key returns the originally-created Blob instead of creating
|
|
84
88
|
# a new one. See {AtlasRb::Work.create} for full semantics.
|
|
89
|
+
# @param expected_digest [String, nil] optional verify-on-ingest checksum,
|
|
90
|
+
# `"<algorithm>:<hexvalue>"` (sha512/sha256/sha1/md5, e.g.
|
|
91
|
+
# `"sha256:abc…"`). Atlas hashes the uploaded bytes **before** persisting
|
|
92
|
+
# and raises {AtlasRb::FixityMismatchError} (HTTP 422) on a mismatch or an
|
|
93
|
+
# unsupported algorithm — nothing is left behind on rejection.
|
|
85
94
|
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
86
95
|
# path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
|
|
87
96
|
# path it is ignored (identity lives in the token).
|
|
88
97
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
89
98
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
90
99
|
# omitted.
|
|
91
|
-
# @return [Hash] the created `"blob"` payload, including its `"id"
|
|
100
|
+
# @return [Hash] the created `"blob"` payload, including its `"id"` and
|
|
101
|
+
# `"digest"` (the recorded fixity digest, `"sha512:<hex>"`).
|
|
102
|
+
# @raise [AtlasRb::FixityMismatchError] if `expected_digest` was supplied and
|
|
103
|
+
# the uploaded bytes did not match (or the algorithm is unsupported).
|
|
104
|
+
#
|
|
105
|
+
# @note Streams the file (FD closed deterministically); a multi-GB upload is
|
|
106
|
+
# not buffered in memory. See {AtlasRb::FaradayHelper#with_file_part}.
|
|
92
107
|
#
|
|
93
108
|
# @example
|
|
94
109
|
# AtlasRb::Blob.create("w-789", "/tmp/upload.tmp", "final_thesis.pdf")
|
|
95
110
|
# # => { "id" => "b-321", "original_filename" => "final_thesis.pdf", ... }
|
|
96
111
|
#
|
|
97
|
-
# @example Retry-safe bulk-deposit create
|
|
112
|
+
# @example Retry-safe bulk-deposit create with fixity verification
|
|
98
113
|
# key = SecureRandom.uuid
|
|
99
114
|
# AtlasRb::Blob.create("w-789", "/tmp/upload.tmp", "thesis.pdf",
|
|
100
|
-
# idempotency_key: key)
|
|
101
|
-
def self.create(id, blob_path, original_filename,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
File.basename(blob_path)) }
|
|
115
|
+
# idempotency_key: key, expected_digest: "sha256:#{sha}")
|
|
116
|
+
def self.create(id, blob_path, original_filename, expected_digest: nil,
|
|
117
|
+
idempotency_key: nil, nuid: nil, on_behalf_of: nil)
|
|
118
|
+
with_file_part(blob_path) do |part|
|
|
119
|
+
payload = { work_id: id, original_filename: original_filename, binary: part }
|
|
120
|
+
payload[:expected_digest] = expected_digest if expected_digest
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
123
|
+
multipart(nuid, on_behalf_of: on_behalf_of, idempotency_key: idempotency_key)
|
|
124
|
+
.post(ROUTE, payload)&.body
|
|
125
|
+
))['blob']
|
|
126
|
+
end
|
|
112
127
|
end
|
|
113
128
|
|
|
114
129
|
# Delete a Blob (the bytes *and* the metadata record).
|
|
@@ -136,23 +151,32 @@ module AtlasRb
|
|
|
136
151
|
#
|
|
137
152
|
# @param id [String] the Blob ID.
|
|
138
153
|
# @param blob_path [String] path to the replacement binary on disk.
|
|
154
|
+
# @param expected_digest [String, nil] optional verify-on-ingest checksum,
|
|
155
|
+
# `"<algorithm>:<hexvalue>"`. 422 ({AtlasRb::FixityMismatchError}) on mismatch.
|
|
139
156
|
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
140
157
|
# path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
|
|
141
158
|
# path it is ignored (identity lives in the token).
|
|
142
159
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
143
160
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
144
161
|
# omitted.
|
|
145
|
-
# @return [Hash] the parsed JSON response from the patch
|
|
162
|
+
# @return [Hash] the parsed JSON response from the patch (the updated
|
|
163
|
+
# `"blob"`, with a refreshed `"digest"` for the new revision).
|
|
164
|
+
# @raise [AtlasRb::FixityMismatchError] if `expected_digest` was supplied and
|
|
165
|
+
# the uploaded bytes did not match (or the algorithm is unsupported).
|
|
166
|
+
#
|
|
167
|
+
# @note Streams the file with the FD closed deterministically — see {.create}.
|
|
146
168
|
#
|
|
147
169
|
# @example
|
|
148
170
|
# AtlasRb::Blob.update("b-321", "/tmp/revised.pdf")
|
|
149
|
-
def self.update(id, blob_path, nuid: nil, on_behalf_of: nil)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
def self.update(id, blob_path, expected_digest: nil, nuid: nil, on_behalf_of: nil)
|
|
172
|
+
with_file_part(blob_path) do |part|
|
|
173
|
+
payload = { binary: part }
|
|
174
|
+
payload[:expected_digest] = expected_digest if expected_digest
|
|
175
|
+
|
|
176
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
177
|
+
multipart(nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id, payload)&.body
|
|
178
|
+
))
|
|
179
|
+
end
|
|
156
180
|
end
|
|
157
181
|
end
|
|
158
182
|
end
|
data/lib/atlas_rb/errors.rb
CHANGED
|
@@ -134,6 +134,41 @@ module AtlasRb
|
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
# Raised when Atlas rejects a binary upload's verify-on-ingest check with a
|
|
138
|
+
# `422` carrying a fixity discriminator — `fixity_mismatch` (the uploaded
|
|
139
|
+
# bytes don't match the supplied `expected_digest`) or
|
|
140
|
+
# `unsupported_digest_algorithm` (a malformed/unknown `expected_digest`).
|
|
141
|
+
# Fires on `POST /files`, `PATCH /files/:id`, and `PATCH /file_sets/:id`.
|
|
142
|
+
#
|
|
143
|
+
# The upload sibling of {ReparentError} / {LinkedMemberError}; same shape,
|
|
144
|
+
# same rationale — without it the `["blob"]` / `["file_set"]` unwrap would
|
|
145
|
+
# return `nil` on the 422 and discard the signal a migration needs to tell a
|
|
146
|
+
# corrupted transfer from a clean one. Atlas rejects *before* persisting, so
|
|
147
|
+
# nothing is left behind to clean up.
|
|
148
|
+
#
|
|
149
|
+
# rescue AtlasRb::FixityMismatchError => e
|
|
150
|
+
# # e.code == "fixity_mismatch": re-fetch the source, retry, or quarantine
|
|
151
|
+
#
|
|
152
|
+
# @note Authorization failures surface as {ForbiddenError} (HTTP 403).
|
|
153
|
+
class FixityMismatchError < Error
|
|
154
|
+
# @return [String, nil] the machine-readable error code from the envelope
|
|
155
|
+
# (`"fixity_mismatch"` or `"unsupported_digest_algorithm"`).
|
|
156
|
+
attr_reader :code
|
|
157
|
+
|
|
158
|
+
# @return [String, nil] the rejected resource's ID, from the envelope (the
|
|
159
|
+
# FileSet on the attach path; may be nil on `POST /files`).
|
|
160
|
+
attr_reader :resource_id
|
|
161
|
+
|
|
162
|
+
# @param message [String] human-readable rejection description.
|
|
163
|
+
# @param code [String, nil] the envelope's `error` discriminator.
|
|
164
|
+
# @param resource_id [String, nil] the rejected resource's ID.
|
|
165
|
+
def initialize(message, code: nil, resource_id: nil)
|
|
166
|
+
super(message)
|
|
167
|
+
@code = code
|
|
168
|
+
@resource_id = resource_id
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
137
172
|
# Raised when Atlas refuses a re-parent, linked-member, or Compilation
|
|
138
173
|
# request with an HTTP `403`, whose envelope is
|
|
139
174
|
# `{ "error", "action", "subject" }`. Lets callers distinguish "you may
|
|
@@ -62,13 +62,20 @@ module AtlasRb
|
|
|
62
62
|
# `POST /works`, `POST /file_sets`, `POST /files`) to deduplicate replays
|
|
63
63
|
# against the originally-created resource. Generated by the caller —
|
|
64
64
|
# this gem does not mint keys.
|
|
65
|
+
# @param auth [:required, :optional] auth strictness. `:required` (default)
|
|
66
|
+
# raises {AtlasRb::ConfigurationError} when no credential can be built —
|
|
67
|
+
# the right behaviour for every endpoint behind `require_auth`. `:optional`
|
|
68
|
+
# signs when it can but sends no `Authorization` header otherwise, for the
|
|
69
|
+
# handful of endpoints Atlas serves with auth skipped (currently only
|
|
70
|
+
# `GET /reset`).
|
|
65
71
|
# @return [Faraday::Connection] a connection that follows redirects and
|
|
66
72
|
# uses Faraday's default adapter.
|
|
67
73
|
#
|
|
68
74
|
# @example Fetching a community
|
|
69
75
|
# AtlasRb::Community.connection({}).get('/communities/abc123')
|
|
70
|
-
def connection(params, nuid=nil, on_behalf_of: nil, idempotency_key: nil)
|
|
71
|
-
headers = auth_headers(nuid, on_behalf_of
|
|
76
|
+
def connection(params, nuid=nil, on_behalf_of: nil, idempotency_key: nil, auth: :required)
|
|
77
|
+
headers = auth_headers(nuid, on_behalf_of, optional: auth == :optional)
|
|
78
|
+
.merge("Content-Type" => "application/json")
|
|
72
79
|
headers["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
73
80
|
|
|
74
81
|
Faraday.new(
|
|
@@ -117,11 +124,38 @@ module AtlasRb
|
|
|
117
124
|
headers: headers
|
|
118
125
|
) do |f|
|
|
119
126
|
f.use AtlasRb::Middleware::RaiseOnStaleResource
|
|
127
|
+
# Translate Atlas's verify-on-ingest 422 (fixity_mismatch /
|
|
128
|
+
# unsupported_digest_algorithm) into a typed FixityMismatchError —
|
|
129
|
+
# the JSON-connection path already carries this; uploads need it too.
|
|
130
|
+
f.use AtlasRb::Middleware::RaiseOnResourceError
|
|
120
131
|
f.request :multipart
|
|
121
132
|
f.request :url_encoded
|
|
122
133
|
end
|
|
123
134
|
end
|
|
124
135
|
|
|
136
|
+
# Build a streaming multipart FilePart for `blob_path`, run the request
|
|
137
|
+
# inside the block, and close the underlying File handle deterministically
|
|
138
|
+
# afterward (on success or exception). The handle must stay open *during*
|
|
139
|
+
# the request — Faraday reads it while posting — so it can't be closed
|
|
140
|
+
# before the call; an unclosed handle leaks a descriptor per upload, which
|
|
141
|
+
# exhausts FDs across a TB migration of millions of files.
|
|
142
|
+
#
|
|
143
|
+
# Streaming/memory: faraday-multipart wraps the part in a streaming
|
|
144
|
+
# CompositeReadIO and the default net_http adapter sends it via
|
|
145
|
+
# `request.body_stream` (Content-Length known), so a multi-GB file uploads
|
|
146
|
+
# without being buffered into a String in memory. (Swapping the host app's
|
|
147
|
+
# default Faraday adapter to a buffering one would regress this.)
|
|
148
|
+
#
|
|
149
|
+
# @param blob_path [String] path to the binary on disk.
|
|
150
|
+
# @yieldparam part [Faraday::Multipart::FilePart] the streaming part.
|
|
151
|
+
# @return the block's return value.
|
|
152
|
+
def with_file_part(blob_path)
|
|
153
|
+
File.open(blob_path, "rb") do |io|
|
|
154
|
+
yield Faraday::Multipart::FilePart.new(io, "application/octet-stream",
|
|
155
|
+
File.basename(blob_path))
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
125
159
|
# Build a Faraday connection authenticated as the Atlas `:system`
|
|
126
160
|
# fixture for system-context calls (SSO user provisioning, etc.).
|
|
127
161
|
#
|
|
@@ -167,18 +201,26 @@ module AtlasRb
|
|
|
167
201
|
# Precedence: ATLAS_JWT (BYO-JWT) > relay-signing. The acting nuid /
|
|
168
202
|
# on_behalf_of fall through to the configured `default_nuid` /
|
|
169
203
|
# `default_on_behalf_of` callables here, once, for whichever mode applies.
|
|
170
|
-
#
|
|
171
|
-
|
|
204
|
+
#
|
|
205
|
+
# Raises {ConfigurationError} when no credential can be built — unless
|
|
206
|
+
# `optional:` is set, in which case it returns no auth headers instead. That
|
|
207
|
+
# is only for endpoints Atlas serves with `require_auth` skipped (`GET
|
|
208
|
+
# /reset`); every normal endpoint leaves `optional` false so a
|
|
209
|
+
# misconfiguration fails loudly rather than silently going unauthenticated.
|
|
210
|
+
def auth_headers(nuid, on_behalf_of, optional: false)
|
|
172
211
|
jwt = ENV.fetch("ATLAS_JWT", nil)
|
|
173
212
|
return { "Authorization" => "Bearer #{jwt}" } if jwt
|
|
174
213
|
|
|
175
214
|
nuid ||= AtlasRb.config.default_nuid&.call
|
|
176
215
|
on_behalf_of ||= AtlasRb.config.default_on_behalf_of&.call
|
|
177
216
|
|
|
178
|
-
signed_relay_headers(nuid, on_behalf_of)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
217
|
+
headers = signed_relay_headers(nuid, on_behalf_of)
|
|
218
|
+
return headers if headers
|
|
219
|
+
return {} if optional
|
|
220
|
+
|
|
221
|
+
raise(ConfigurationError,
|
|
222
|
+
"atlas_rb: no auth configured — set ATLAS_JWT or " \
|
|
223
|
+
"AtlasRb.config.assertion_signing_key (with an acting nuid to sign)")
|
|
182
224
|
end
|
|
183
225
|
|
|
184
226
|
# A signed-assertion Authorization header (sub = acting nuid), or nil when
|
data/lib/atlas_rb/file_set.rb
CHANGED
|
@@ -98,30 +98,52 @@ module AtlasRb
|
|
|
98
98
|
# Attach (or replace) the binary content backing this FileSet.
|
|
99
99
|
#
|
|
100
100
|
# The body is uploaded as `application/octet-stream` regardless of the
|
|
101
|
-
# file's true type — Atlas inspects the content server-side.
|
|
102
|
-
#
|
|
103
|
-
# against the underlying `/files/` endpoint.
|
|
101
|
+
# file's true type — Atlas inspects the content server-side. This is the
|
|
102
|
+
# ordered/classified-slot attach used after {.create} cuts the slot.
|
|
104
103
|
#
|
|
105
104
|
# @param id [String] the FileSet ID.
|
|
106
105
|
# @param blob_path [String] path to the binary file on disk.
|
|
106
|
+
# @param original_filename [String, nil] the user-facing filename Atlas
|
|
107
|
+
# should record on the resulting Blob (e.g. the v1 `"page-0001.tif"`);
|
|
108
|
+
# preserved separately from the temp `File.basename(blob_path)`.
|
|
109
|
+
# @param expected_digest [String, nil] optional verify-on-ingest checksum,
|
|
110
|
+
# `"<algorithm>:<hexvalue>"`. 422 ({AtlasRb::FixityMismatchError}) on mismatch.
|
|
111
|
+
# @param idempotency_key [String, nil] optional UUID. A repeat call with the
|
|
112
|
+
# same key returns the FileSet with its already-attached Blob **without
|
|
113
|
+
# recopying the bytes** (and 410 if it was tombstoned in the interim). See
|
|
114
|
+
# {AtlasRb::Work.create} for full semantics.
|
|
107
115
|
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
108
116
|
# path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
|
|
109
117
|
# path it is ignored (identity lives in the token).
|
|
110
118
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
111
119
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
112
120
|
# omitted.
|
|
113
|
-
# @return [Hash] the parsed JSON response from the patch.
|
|
121
|
+
# @return [Hash] the parsed JSON response from the patch (the `"file_set"`).
|
|
122
|
+
# @raise [AtlasRb::FixityMismatchError] if `expected_digest` was supplied and
|
|
123
|
+
# the uploaded bytes did not match (or the algorithm is unsupported).
|
|
124
|
+
#
|
|
125
|
+
# @note Streams the file with the FD closed deterministically — see
|
|
126
|
+
# {Blob.create} / {AtlasRb::FaradayHelper#with_file_part}.
|
|
114
127
|
#
|
|
115
128
|
# @example
|
|
116
129
|
# AtlasRb::FileSet.update("fs-001", "/tmp/article.pdf")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
130
|
+
#
|
|
131
|
+
# @example Resumable, filename-preserving migration attach
|
|
132
|
+
# AtlasRb::FileSet.update(page["id"], "/tmp/p1.tif",
|
|
133
|
+
# original_filename: "page-0001.tif",
|
|
134
|
+
# idempotency_key: key)
|
|
135
|
+
def self.update(id, blob_path, original_filename: nil, expected_digest: nil,
|
|
136
|
+
idempotency_key: nil, nuid: nil, on_behalf_of: nil)
|
|
137
|
+
with_file_part(blob_path) do |part|
|
|
138
|
+
payload = { binary: part }
|
|
139
|
+
payload[:original_filename] = original_filename if original_filename
|
|
140
|
+
payload[:expected_digest] = expected_digest if expected_digest
|
|
141
|
+
|
|
142
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
143
|
+
multipart(nuid, on_behalf_of: on_behalf_of, idempotency_key: idempotency_key)
|
|
144
|
+
.patch(ROUTE + id, payload)&.body
|
|
145
|
+
))
|
|
146
|
+
end
|
|
125
147
|
end
|
|
126
148
|
|
|
127
149
|
# Persist the per-page IIIF image-service pointer on a FileSet.
|
|
@@ -15,39 +15,53 @@ module AtlasRb
|
|
|
15
15
|
# {RaiseOnStaleResource}.
|
|
16
16
|
#
|
|
17
17
|
# It is intentionally narrow — it only fires on the re-parent
|
|
18
|
-
# (`.../parent`) and linked-member (`.../linked_members...`) write paths
|
|
19
|
-
#
|
|
20
|
-
# `403` / `422` bodies carrying
|
|
18
|
+
# (`.../parent`) and linked-member (`.../linked_members...`) write paths,
|
|
19
|
+
# the Compilation surface (`/compilations...`), and binary uploads
|
|
20
|
+
# (`/files...`, `/file_sets...`), and only on `403` / `422` bodies carrying
|
|
21
|
+
# an `error` discriminator. The upload branch is further gated on a fixity
|
|
22
|
+
# discriminator ({FIXITY_CODES}), so a `422` on those paths with any other
|
|
23
|
+
# `error` (or `403`s on uploads, which stay raw) passes through untouched.
|
|
21
24
|
# Everything else (other paths, other statuses, a `422` whose body uses a
|
|
22
25
|
# different discriminator such as `tombstone`'s `code: "has_live_children"`)
|
|
23
26
|
# passes through untouched, so atlas_rb stays a thin Faraday binding that
|
|
24
27
|
# translates only the wire signals callers genuinely need to discriminate.
|
|
25
28
|
#
|
|
26
29
|
# Mapping:
|
|
27
|
-
# - `403`
|
|
30
|
+
# - `403` on a re-parent/linked/Compilation path → {AtlasRb::ForbiddenError}
|
|
28
31
|
# - `422` on `.../parent` → {AtlasRb::ReparentError} (`error`/`resource_id`)
|
|
29
32
|
# - `422` on `.../linked_members...` → {AtlasRb::LinkedMemberError}
|
|
30
33
|
# - `422` on `/compilations...` → {AtlasRb::CompilationError}
|
|
34
|
+
# - `422` + a fixity discriminator on `/files...` / `/file_sets...` →
|
|
35
|
+
# {AtlasRb::FixityMismatchError}
|
|
31
36
|
class RaiseOnResourceError < Faraday::Middleware
|
|
37
|
+
# Upload-path `422` discriminators this middleware translates; any other
|
|
38
|
+
# `error` on those paths passes through (Atlas owns these as a wire contract).
|
|
39
|
+
FIXITY_CODES = %w[fixity_mismatch unsupported_digest_algorithm].freeze
|
|
40
|
+
|
|
32
41
|
# @param env [Faraday::Env] the completed response environment.
|
|
33
|
-
# @raise [AtlasRb::ForbiddenError] on a 403 to a
|
|
42
|
+
# @raise [AtlasRb::ForbiddenError] on a 403 to a re-parent/linked/Compilation path.
|
|
34
43
|
# @raise [AtlasRb::ReparentError] on a 422 to a re-parent path.
|
|
35
44
|
# @raise [AtlasRb::LinkedMemberError] on a 422 to a linked-member path.
|
|
36
45
|
# @raise [AtlasRb::CompilationError] on a 422 to a Compilation path.
|
|
46
|
+
# @raise [AtlasRb::FixityMismatchError] on a 422 + fixity discriminator to an upload path.
|
|
37
47
|
# @return [void]
|
|
38
48
|
def on_complete(env)
|
|
39
|
-
return unless
|
|
49
|
+
return unless [403, 422].include?(env.status)
|
|
40
50
|
|
|
41
51
|
path = env.url&.path.to_s
|
|
42
52
|
reparent = path.end_with?("/parent")
|
|
43
53
|
linked = path.include?("/linked_members")
|
|
44
54
|
compilation = path.start_with?("/compilations")
|
|
45
|
-
|
|
55
|
+
upload = path.start_with?("/files") || path.start_with?("/file_sets")
|
|
56
|
+
return unless reparent || linked || compilation || upload
|
|
46
57
|
|
|
47
58
|
body = parse_json(env.body)
|
|
48
59
|
return unless body.is_a?(Hash) && body["error"]
|
|
49
60
|
|
|
50
61
|
if env.status == 403
|
|
62
|
+
# 403s on upload paths stay raw — acting-as/authz isn't an upload concern here.
|
|
63
|
+
return unless reparent || linked || compilation
|
|
64
|
+
|
|
51
65
|
raise AtlasRb::ForbiddenError.new(
|
|
52
66
|
body["message"] || "Atlas refused the request",
|
|
53
67
|
code: body["error"],
|
|
@@ -66,12 +80,18 @@ module AtlasRb
|
|
|
66
80
|
code: body["error"],
|
|
67
81
|
resource_id: body["resource_id"]
|
|
68
82
|
)
|
|
69
|
-
|
|
83
|
+
elsif compilation
|
|
70
84
|
raise AtlasRb::CompilationError.new(
|
|
71
85
|
body["message"] || "Atlas rejected the compilation write",
|
|
72
86
|
code: body["error"],
|
|
73
87
|
resource_id: body["resource_id"]
|
|
74
88
|
)
|
|
89
|
+
elsif FIXITY_CODES.include?(body["error"])
|
|
90
|
+
raise AtlasRb::FixityMismatchError.new(
|
|
91
|
+
body["message"] || "Atlas rejected the upload (fixity)",
|
|
92
|
+
code: body["error"],
|
|
93
|
+
resource_id: body["resource_id"]
|
|
94
|
+
)
|
|
75
95
|
end
|
|
76
96
|
end
|
|
77
97
|
|
data/lib/atlas_rb.rb
CHANGED
|
@@ -123,20 +123,23 @@ module AtlasRb
|
|
|
123
123
|
|
|
124
124
|
# Reset the connected Atlas instance to a clean state.
|
|
125
125
|
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
126
|
+
# Atlas serves `GET /reset` with `require_auth` **skipped** (it is env-gated,
|
|
127
|
+
# not principal-gated), so this call uses **optional auth**: it signs an
|
|
128
|
+
# assertion when a credential is available, and sends no `Authorization`
|
|
129
|
+
# header otherwise — never raising {AtlasRb::ConfigurationError} for lack of
|
|
130
|
+
# one. That lets a test `before(:suite)` reset before any acting nuid is set.
|
|
131
|
+
#
|
|
132
|
+
# @param nuid [String, nil] optional acting user's NUID. When a signing key
|
|
133
|
+
# is configured it is signed into the assertion `sub`; otherwise it is
|
|
134
|
+
# unused (Atlas ignores it on this endpoint). Mostly here for symmetry.
|
|
135
|
+
# @param on_behalf_of [String, nil] optional NUID. Falls through to
|
|
136
|
+
# {AtlasRb.config}.default_on_behalf_of when omitted.
|
|
134
137
|
# @return [String, nil] the raw response body from `GET /reset`.
|
|
135
138
|
#
|
|
136
139
|
# @example
|
|
137
|
-
# AtlasRb::Reset.clean
|
|
140
|
+
# AtlasRb::Reset.clean
|
|
138
141
|
def self.clean(nuid: nil, on_behalf_of: nil)
|
|
139
|
-
connection({}, nuid, on_behalf_of: on_behalf_of).get("/reset")&.body
|
|
142
|
+
connection({}, nuid, on_behalf_of: on_behalf_of, auth: :optional).get("/reset")&.body
|
|
140
143
|
end
|
|
141
144
|
end
|
|
142
145
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: atlas_rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cliff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|