atlas_rb 1.5.0 → 1.6.1
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/Gemfile.lock +2 -2
- data/lib/atlas_rb/blob.rb +47 -23
- data/lib/atlas_rb/compilation.rb +31 -9
- data/lib/atlas_rb/errors.rb +35 -0
- data/lib/atlas_rb/faraday_helper.rb +27 -0
- data/lib/atlas_rb/file_set.rb +34 -12
- data/lib/atlas_rb/middleware/raise_on_resource_error.rb +28 -8
- 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: f0b6def06432136a4f0fda238a5c8ff3599de63234325e8609d0fcc49012d42d
|
|
4
|
+
data.tar.gz: a50c8fbbcaa1776a171f199e86e5ab90a8034f8c9415547c97893f52dc26a449
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19578a460cb38f49fd97a7513cc2ba6d782fa260a82f3c1071cc88e01a6a21ad6026969422033af5ad013f3e81d2d4d400fab42c72b300bd410582dc806d7f29
|
|
7
|
+
data.tar.gz: 941ce0caf062890a63891e5a458d83e1300e9240240f8adaf39a9e7ed7879ddaf4ef8168c566b052c5c7f7fde50c66e00e93baa66f71462a5464f2ea1b4a03ad
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.6.1
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
atlas_rb (1.
|
|
4
|
+
atlas_rb (1.6.1)
|
|
5
5
|
faraday (~> 2.7)
|
|
6
6
|
faraday-follow_redirects (~> 0.3.0)
|
|
7
7
|
faraday-multipart (~> 1)
|
|
@@ -13,7 +13,7 @@ GEM
|
|
|
13
13
|
specs:
|
|
14
14
|
base64 (0.3.0)
|
|
15
15
|
diff-lcs (1.6.1)
|
|
16
|
-
faraday (2.14.
|
|
16
|
+
faraday (2.14.3)
|
|
17
17
|
faraday-net_http (>= 2.0, < 3.5)
|
|
18
18
|
json
|
|
19
19
|
logger
|
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/compilation.rb
CHANGED
|
@@ -53,16 +53,31 @@ module AtlasRb
|
|
|
53
53
|
))["compilation"]
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# List Compilations,
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# user's —
|
|
60
|
-
# anyone else
|
|
61
|
-
#
|
|
62
|
-
#
|
|
56
|
+
# List Compilations, paginated (newest first), in one of three modes.
|
|
57
|
+
#
|
|
58
|
+
# Default (no `scope:`) is owner-scoped: the acting user's own Sets. Pass
|
|
59
|
+
# `owner:` to list another user's — admin-only, raising
|
|
60
|
+
# {AtlasRb::ForbiddenError} for anyone else; there is no public browse
|
|
61
|
+
# surface.
|
|
62
|
+
#
|
|
63
|
+
# Pass `scope:` for grant-scoped discovery — Sets where the acting user is
|
|
64
|
+
# a *grantee* but **not** the owner (owned Sets are always excluded; list
|
|
65
|
+
# those with the default mode):
|
|
66
|
+
# - `scope: :editable` — Sets the caller may edit (`edit_users` /
|
|
67
|
+
# `edit_groups` grants).
|
|
68
|
+
# - `scope: :shared` — Sets shared with the caller (`read_groups` grants,
|
|
69
|
+
# plus the edit grants that imply read).
|
|
70
|
+
# Grant-scoped modes are keyed on the acting user; `owner:` is ignored and
|
|
71
|
+
# group membership is resolved server-side. An unknown `scope:` is a 400.
|
|
72
|
+
#
|
|
73
|
+
# Pass `q:` to narrow by case-insensitive title substring in any mode; the
|
|
74
|
+
# filter applies before pagination, so the `"pagination"` block describes
|
|
75
|
+
# the filtered result.
|
|
63
76
|
#
|
|
64
77
|
# @param owner [String, nil] NUID whose Sets to list (admin-only when it
|
|
65
|
-
# isn't the acting user). Omit for "my Sets".
|
|
78
|
+
# isn't the acting user). Omit for "my Sets". Ignored when `scope:` is set.
|
|
79
|
+
# @param scope [Symbol, String, nil] grant-scoped mode — `:editable` or
|
|
80
|
+
# `:shared`. Omit for the owner-scoped default.
|
|
66
81
|
# @param q [String, nil] case-insensitive title substring filter.
|
|
67
82
|
# @param page [Integer, nil] 1-indexed page number.
|
|
68
83
|
# @param per_page [Integer, nil] page size override.
|
|
@@ -82,11 +97,18 @@ module AtlasRb
|
|
|
82
97
|
# @example Another user's Sets (admin)
|
|
83
98
|
# AtlasRb::Compilation.list(owner: "000000002", nuid: "000000004")
|
|
84
99
|
#
|
|
100
|
+
# @example Sets shared with me that I can edit (not owned)
|
|
101
|
+
# AtlasRb::Compilation.list(scope: :editable, nuid: "000000002")
|
|
102
|
+
#
|
|
103
|
+
# @example Sets shared with me to view (read + edit grants, not owned)
|
|
104
|
+
# AtlasRb::Compilation.list(scope: :shared, nuid: "000000002")
|
|
105
|
+
#
|
|
85
106
|
# @example Title typeahead
|
|
86
107
|
# AtlasRb::Compilation.list(q: "course", nuid: "000000002")
|
|
87
|
-
def self.list(owner: nil, q: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
|
|
108
|
+
def self.list(owner: nil, scope: nil, q: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil)
|
|
88
109
|
params = {}
|
|
89
110
|
params[:owner] = owner if owner
|
|
111
|
+
params[:scope] = scope if scope
|
|
90
112
|
params[:q] = q if q
|
|
91
113
|
params[:page] = page if page
|
|
92
114
|
params[:per_page] = per_page if per_page
|
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
|
|
@@ -124,11 +124,38 @@ module AtlasRb
|
|
|
124
124
|
headers: headers
|
|
125
125
|
) do |f|
|
|
126
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
|
|
127
131
|
f.request :multipart
|
|
128
132
|
f.request :url_encoded
|
|
129
133
|
end
|
|
130
134
|
end
|
|
131
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
|
+
|
|
132
159
|
# Build a Faraday connection authenticated as the Atlas `:system`
|
|
133
160
|
# fixture for system-context calls (SSO user provisioning, etc.).
|
|
134
161
|
#
|
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
|
|
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.1
|
|
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
|