atlas_rb 1.6.5 → 1.7.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: 19c01cf68a6a94d94dceff16d5e8ea00e147b8db2c6fd51f8d6c1e9b8ec36fac
4
- data.tar.gz: 457092385fc63ed58a2c2e871983bfb2a62d2d69448a74c78796d6a6f14506be
3
+ metadata.gz: 1a9ee4ab4b57221618d8ad13ab874acbfce61a452efc8e9562559fcb0215bef8
4
+ data.tar.gz: 22c8c48404af609784177ba0f157380bc1cef435f9b6b37b76f6e65b95cef1a2
5
5
  SHA512:
6
- metadata.gz: 2008dd0082b8de90ca4016a8a66c378bc88a6a6cc61b44070d9fcf9c9fd13ed5d21f81787dbd1b0131194b8edbd10eaf4904366c5e496452fd689fe2eedab429
7
- data.tar.gz: 801b406750972c2dc014c4e202fcb53a2d204e554197c5594015f4dd0c0b60d58839ad6aab8b7c8659399082618483be9bc918543355f06d6a7ad3d125ed7107
6
+ metadata.gz: 68ddbb93d8ad4049f16d01786d860f3ccc7bd610e5f57144f79ba0e30dd1bb8e66fd4c98adf17e9847b8551e9b42265b649650acd6c23f3a6dad3ffa3479879b
7
+ data.tar.gz: bdfa9d364af363d50466158d13a944c3062b6d8649e5de57fdbb350d87e30e2c0fbf4dcc72c6ff515b536b40358456dc7359d449e1b2926149ff5f489f3ba9d5
data/.version CHANGED
@@ -1 +1 @@
1
- 1.6.5
1
+ 1.7.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.0
4
+
5
+ ### Added — binary version read surface (`Blob.versions` / `version_content` / `rollback`)
6
+
7
+ The binary counterpart to `Resource.mods_versions` / `mods_version`. Replacing a
8
+ file (`Blob.update`) already retains prior bytes in OCFL; these bindings make the
9
+ retained versions addressable:
10
+
11
+ - `Blob.versions(id)` → `GET /files/:id/versions` — reverse-chronological
12
+ envelope (`{ "blob_id", "versions" }`), one descriptor per retained content
13
+ revision (`version_id`, `file_identifier`, `created`, `digest`, `size`,
14
+ `original_filename`, plus correlated `actor_nuid` / `on_behalf_of_nuid`).
15
+ Admin-gated by the server.
16
+ - `Blob.version_content(id, version_id, &chunk_handler)` →
17
+ `GET /files/:id/versions/:version_id/content` — streams a prior version's
18
+ bytes through a block, exactly like `Blob.content`.
19
+ - `Blob.rollback(id, version_id)` → `POST /files/:id/rollback` — reinstates a
20
+ prior version by appending its bytes as a new revision (non-destructive; NOID
21
+ preserved).
22
+
23
+ ### Added — `Blob.update` accepts `idempotency_key:`
24
+
25
+ `Blob.update` (`PATCH /files/:id`) now takes an optional `idempotency_key:`,
26
+ threaded as the `Idempotency-Key` header (same semantics as `Blob.create` /
27
+ `FileSet.create`). A double-submitted replace sharing a key returns the existing
28
+ Blob instead of minting a second OCFL version.
29
+
3
30
  ## 1.5.0
4
31
 
5
32
  ### Added — optional auth for `Reset.clean`
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.6.5)
4
+ atlas_rb (1.7.0)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
@@ -25,7 +25,7 @@ GEM
25
25
  net-http (~> 0.5)
26
26
  hashie (5.1.0)
27
27
  logger
28
- json (2.19.9)
28
+ json (2.20.0)
29
29
  jwt (2.10.3)
30
30
  base64
31
31
  logger (1.7.0)
data/lib/atlas_rb/blob.rb CHANGED
@@ -153,6 +153,10 @@ module AtlasRb
153
153
  # @param blob_path [String] path to the replacement binary on disk.
154
154
  # @param expected_digest [String, nil] optional verify-on-ingest checksum,
155
155
  # `"<algorithm>:<hexvalue>"`. 422 ({AtlasRb::FixityMismatchError}) on mismatch.
156
+ # @param idempotency_key [String, nil] optional UUID. A double-submit of the
157
+ # replace with the same key returns the existing Blob instead of minting a
158
+ # second OCFL version — without it a retried replace appends a duplicate
159
+ # revision. See {AtlasRb::Work.create} for full semantics.
156
160
  # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
157
161
  # path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
158
162
  # path it is ignored (identity lives in the token).
@@ -168,15 +172,123 @@ module AtlasRb
168
172
  #
169
173
  # @example
170
174
  # AtlasRb::Blob.update("b-321", "/tmp/revised.pdf")
171
- def self.update(id, blob_path, expected_digest: nil, nuid: nil, on_behalf_of: nil)
175
+ #
176
+ # @example Retry-safe replace
177
+ # AtlasRb::Blob.update("b-321", "/tmp/revised.pdf", idempotency_key: SecureRandom.uuid)
178
+ def self.update(id, blob_path, expected_digest: nil, idempotency_key: nil, nuid: nil, on_behalf_of: nil)
172
179
  with_file_part(blob_path) do |part|
173
180
  payload = { binary: part }
174
181
  payload[:expected_digest] = expected_digest if expected_digest
175
182
 
176
183
  AtlasRb::Mash.new(JSON.parse(
177
- multipart(nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id, payload)&.body
184
+ multipart(nuid, on_behalf_of: on_behalf_of, idempotency_key: idempotency_key)
185
+ .patch(ROUTE + id, payload)&.body
178
186
  ))
179
187
  end
180
188
  end
189
+
190
+ # List a Blob's retained binary version history.
191
+ #
192
+ # Wraps Atlas's `GET /files/<id>/versions` — the binary counterpart to
193
+ # {Resource.mods_versions}. Returns a reverse-chronological (newest first)
194
+ # envelope: one descriptor per retained content revision, each carrying its
195
+ # OCFL `version_id` label, the `file_identifier` appended for that revision,
196
+ # the `created` timestamp, the `digest`/`size` recorded at that version,
197
+ # the stable `original_filename`, and actor attribution (`actor_nuid` /
198
+ # `on_behalf_of_nuid`, null when no audit event correlates).
199
+ #
200
+ # Server admin-gates this endpoint (it exposes edit attribution), so
201
+ # `401` / `403` surface as raw Faraday responses, matching
202
+ # {Resource.mods_versions}. An unknown Blob id yields a `404`.
203
+ #
204
+ # @param id [String] the Blob ID.
205
+ # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
206
+ # path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
207
+ # path it is ignored (identity lives in the token).
208
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
209
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
210
+ # omitted.
211
+ # @return [AtlasRb::Mash] the parsed envelope, with `"blob_id"` and a
212
+ # `"versions"` array (reverse chronological).
213
+ #
214
+ # @example
215
+ # history = AtlasRb::Blob.versions("b-321")
216
+ # history["versions"].first["version_id"] # => "v5"
217
+ # history["versions"].first["digest"] # => "sha512:9f86d0…"
218
+ def self.versions(id, nuid: nil, on_behalf_of: nil)
219
+ AtlasRb::Mash.new(JSON.parse(
220
+ connection({}, nuid, on_behalf_of: on_behalf_of).get("#{ROUTE}#{id}/versions")&.body
221
+ ))
222
+ end
223
+
224
+ # Stream the bytes of a *prior* version of a Blob through a block.
225
+ #
226
+ # Wraps `GET /files/<id>/versions/<version_id>/content` — the version-pinned
227
+ # twin of {.content}, and the read half of "download the superseded file".
228
+ # Like {.content}, the body is **not** buffered: each chunk is yielded to
229
+ # `chunk_handler` immediately (safe for files larger than memory), and the
230
+ # response headers are captured and returned.
231
+ #
232
+ # Pass a `version_id` obtained from {.versions} (an opaque OCFL `vN` label);
233
+ # only labels the history surfaced are addressable. An unknown id or version
234
+ # yields a `404` (raw Faraday response).
235
+ #
236
+ # @param id [String] the Blob ID.
237
+ # @param version_id [String] an OCFL version label from {.versions}, e.g. `"v1"`.
238
+ # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
239
+ # path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
240
+ # path it is ignored (identity lives in the token).
241
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
242
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
243
+ # omitted.
244
+ # @yieldparam chunk [String] the next chunk of binary data.
245
+ # @return [Hash] the response headers from the version-content request.
246
+ #
247
+ # @example Download a superseded version to disk
248
+ # File.open("/tmp/old.pdf", "wb") do |f|
249
+ # AtlasRb::Blob.version_content("b-321", "v1") { |chunk| f.write(chunk) }
250
+ # end
251
+ def self.version_content(id, version_id, nuid: nil, on_behalf_of: nil, &chunk_handler)
252
+ headers = {}
253
+ connection({}, nuid, on_behalf_of: on_behalf_of).get("#{ROUTE}#{id}/versions/#{version_id}/content") do |req|
254
+ req.options.on_data = proc do |chunk, _bytes_received, env|
255
+ headers = env.response_headers if headers.empty? && env
256
+ chunk_handler.call(chunk)
257
+ end
258
+ end
259
+ headers
260
+ end
261
+
262
+ # Roll a Blob back to a prior version.
263
+ #
264
+ # Wraps `POST /files/<id>/rollback`. Atlas promotes the given version to
265
+ # current by appending its bytes again as a NEW revision — so rollback is
266
+ # itself non-destructive (it becomes vN+1 with the bytes of vN) and the Blob
267
+ # NOID is preserved. OCFL dedups the identical content, so no bytes are
268
+ # recopied. Avoids a full round-trip of the bytes back through the caller
269
+ # (vs. re-streaming {.version_content} into {.update}).
270
+ #
271
+ # Pass a `version_id` obtained from {.versions}. An unknown id or version
272
+ # yields a `404` (raw Faraday response).
273
+ #
274
+ # @param id [String] the Blob ID.
275
+ # @param version_id [String] the OCFL version label to reinstate, e.g. `"v1"`.
276
+ # @param nuid [String, nil] optional acting user's NUID. On the relay-signing
277
+ # path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
278
+ # path it is ignored (identity lives in the token).
279
+ # @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
280
+ # header. Falls through to {AtlasRb.config}.default_on_behalf_of when
281
+ # omitted.
282
+ # @return [AtlasRb::Mash] the updated `"blob"` payload (NOID unchanged,
283
+ # `"digest"` refreshed to the reinstated bytes).
284
+ #
285
+ # @example
286
+ # AtlasRb::Blob.rollback("b-321", "v1")
287
+ def self.rollback(id, version_id, nuid: nil, on_behalf_of: nil)
288
+ AtlasRb::Mash.new(JSON.parse(
289
+ connection({}, nuid, on_behalf_of: on_behalf_of)
290
+ .post("#{ROUTE}#{id}/rollback", JSON.dump(version_id: version_id))&.body
291
+ ))['blob']
292
+ end
181
293
  end
182
294
  end
@@ -5,7 +5,14 @@ module AtlasRb
5
5
  # directory. A Person correlates the several `users` rows that share a NUID
6
6
  # and carries the authoritative, librarian-editable `display_name` (the SSO
7
7
  # `users.name` is frequently wrong and is clobbered on every login), plus
8
- # community affiliations.
8
+ # community affiliations and a stable **personal-root Collection**.
9
+ #
10
+ # The returned Mash carries `personal_root_id` — the **NOID** of that personal
11
+ # root, the structural parent the weighted-deposit publish conduit writes the
12
+ # person's own Works under (then linked into a community showcase via
13
+ # {Work.add_linked_member}). Atlas mints it eagerly at create, so it is always
14
+ # present on a Person; read it straight off the resolve/find Mash, e.g.
15
+ # `AtlasRb::Work.create(person["personal_root_id"], depositor: person["nuid"])`.
9
16
  #
10
17
  # Addressed by **NOID** (like Work/Collection) — the staff-facing NUID is kept
11
18
  # server-side and never put in a public URL. So the positional `id` argument
@@ -27,7 +34,8 @@ module AtlasRb
27
34
  # @param nuid [String, nil] acting principal (signed into the assertion sub).
28
35
  # @param on_behalf_of [String, nil] acting-as target.
29
36
  # @return [AtlasRb::Mash] the unwrapped `"person"` object (carries the
30
- # server-side `nuid` for callers that need it, e.g. depositor gating).
37
+ # server-side `nuid` for callers that need it, e.g. depositor gating, and
38
+ # `personal_root_id` for the publish-conduit parent).
31
39
  def self.find(id, nuid: nil, on_behalf_of: nil)
32
40
  AtlasRb::Mash.new(JSON.parse(
33
41
  connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
@@ -35,8 +43,9 @@ module AtlasRb
35
43
  end
36
44
 
37
45
  # List people — the NOID-keyed People-index source. Returns the page's
38
- # Persons (each with `noid`, `display_name`, and the server-side `nuid`), so
39
- # a consumer builds the index and profiles entirely through atlas_rb without
46
+ # Persons (each with `noid`, `display_name`, the server-side `nuid`, and
47
+ # `personal_root_id`), so a consumer builds the index and profiles entirely
48
+ # through atlas_rb without
40
49
  # routing People through the catalog/Solr or exposing a NUID publicly.
41
50
  #
42
51
  # @param page [Integer, nil] 1-based page (server default when nil).
@@ -54,11 +63,14 @@ module AtlasRb
54
63
 
55
64
  # Batch-resolve people to their authoritative display_name in one call
56
65
  # (supersedes the SSO users directory's resolve). Unresolved NUIDs drop.
66
+ # The deposit fork reads `affiliated_community_ids` and `personal_root_id`
67
+ # off the same resolve Mash it already makes for the depositor's Person.
57
68
  #
58
69
  # @param nuids [Array<String>] the NUIDs to resolve.
59
70
  # @param nuid [String, nil] acting principal.
60
71
  # @param on_behalf_of [String, nil] acting-as target.
61
- # @return [Array<AtlasRb::Mash>] one unwrapped `"person"` per resolved NUID.
72
+ # @return [Array<AtlasRb::Mash>] one unwrapped `"person"` per resolved NUID
73
+ # (each carries `nuid`, `affiliated_community_ids`, and `personal_root_id`).
62
74
  def self.resolve(nuids, nuid: nil, on_behalf_of: nil)
63
75
  JSON.parse(
64
76
  connection({ nuids: Array(nuids).join(",") }, nuid, on_behalf_of: on_behalf_of).get(ROUTE)&.body
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.6.5
4
+ version: 1.7.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-18 00:00:00.000000000 Z
11
+ date: 2026-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday