Skip to content

Activity, content and search

The read-only parts of the Storage API: what has changed, the bytes of a preserved file, where those bytes really are, and a crude path search.

The Storage API publishes an activity stream in the same IIIF Change Discovery shape as the Preservation API’s, but about a different subject: it is a record of the Storage API’s own Import Job activity, not of Archival Groups. The Preservation API consumes it and, from it, produces the stream of created and updated Archival Groups that everything else reads.

GET /activity/importjobs/collection
{
"@context": "http://iiif.io/api/discovery/1/context.json",
"id": "https://storage-api.example/activity/importjobs/collection",
"type": "OrderedCollection",
"totalItems": 3914,
"first": {
"id": "https://storage-api.example/activity/importjobs/pages/1",
"type": "OrderedCollectionPage"
},
"last": {
"id": "https://storage-api.example/activity/importjobs/pages/40",
"type": "OrderedCollectionPage"
}
}

Pages hold 100 entries and are numbered from 1. A client wanting the events since it last read would load the last page:

GET /activity/importjobs/pages/40
{
"@context": "http://iiif.io/api/discovery/1/context.json",
"id": "https://storage-api.example/activity/importjobs/pages/40",
"type": "OrderedCollectionPage",
"startIndex": 3900,
"partOf": {
"id": "https://storage-api.example/activity/importjobs/collection",
"type": "OrderedCollection"
},
"prev": {
"id": "https://storage-api.example/activity/importjobs/pages/39",
"type": "OrderedCollectionPage"
},
"orderedItems": [
{
"type": "Update",
"object": {
"id": "https://storage-api.example/import/results/m3cnbckjmtd9/cc/lqj7mqhg",
"type": "ImportJob",
"seeAlso": [
{
"id": "https://storage-api.example/repository/cc/lqj7mqhg",
"type": "ArchivalGroup"
}
]
},
"startTime": "2025-10-06T10:31:02Z",
"endTime": "2025-10-06T10:34:19Z"
}
]
// ... 99 more
}

…and then work from the end of orderedItems backwards until it reaches an endTime earlier than the last time it read the stream, following prev to the page before if it needs to. It will then have gathered all the new events. A page that is not the last one also has a next.

Property Description
type (of the activity) Always Update. The Storage API does not distinguish creating an Archival Group from changing one.
object.id The Import Job Result URI.
object.type ImportJob, even though the id is that of a result.
object.seeAlso An array with one entry: the Archival Group the job applied to.
startTime When the job was received.
endTime When the job finished. This is what the stream is ordered by, and what a reader keeps track of.

Only jobs that have finished appear; a job that is still waiting or running is not in the stream and is not counted in totalItems.

If you can call the Storage API you can retrieve the bytes of any Binary.

GET /content/library/manuscripts/ms-342/objects/34r.tiff

The path is the same as the repository path apart from the first segment. This URI is exposed as the content property of a Binary in both APIs; it is only honoured here.

The response streams the file with the Binary’s recorded contentType. Anything at the path that is not a Binary - a Container, an Archival Group, nothing at all - is an error, and the body is a problem document saying 404. Be careful how you detect it: the controller does not set the response status, so what you actually receive is HTTP 200 carrying that document, and a client that branches on the status code will read the failure as success. Check the content type, or the body, rather than the status (issue #262).

Callers that are trusted with direct access to the OCFL bucket do not need this endpoint at all: a Binary’s origin property gives the real S3 location of the bytes.

GET /ocfl/storagemap/{archivalGroupPathUnderRoot}
GET /ocfl/storagemap/{archivalGroupPathUnderRoot}?version=v3

The same resource, at the same path, as the Preservation API’s - the Preservation API passes it straight through. It maps each logical path in an Archival Group to the S3 key that path resolves to in a given version, which is what makes it possible to read an old version’s files without asking Fedora for them.

Property Description
archivalGroup The Archival Group this map is of.
version The version the map describes: ocflVersion, mementoTimestamp, mementoDateTime.
headVersion The current version of the object.
allVersions Every version, in order.
storageType The kind of storage - S3.
root The bucket.
objectPath The key prefix of the OCFL object within the bucket.
files A map of logical path to hash (SHA-256) and fullPath (the key, relative to objectPath, of the version of the file this version points at).
hashes The reverse lookup: SHA-256 to path.

Asking for a version the map does not resolve to is an error rather than a silently different answer.

GET /FedoraSearch?text=ms-342&page=0&pageSize=50

A deliberately simple search, and quite unlike the Preservation API’s. It is a case-insensitive substring match over repository paths, run directly against Fedora’s own database, restricted to rows that have a media type - which is to say, to Binaries. It does not look at names (dc:title), at METS, or at file content.

Parameter Description
text Required. The substring to look for in the path. An empty or whitespace value is a 400.
page Zero-based. page=0 is the first page.
pageSize Between 1 and 500.
{
"total": 128,
"count": 50,
"page": 0,
"pageSize": 50,
"results": [
{
"fedoraId": "info:fedora/library/manuscripts/ms-342/objects/34r.tiff",
"created": "2025-03-11T11:19:13Z",
"lastModified": "2025-03-11T11:19:13Z",
"contentSize": 15986,
"mimeType": "image/tiff"
}
]
}

total is the number of matches, count the number returned on this page.

Note that the page numbering here starts at 0, while the activity stream’s pages start at 1.

Path What it tells you
/health The service is up. This is what the load balancer uses.
/fedora Whether the Storage API can reach Fedora.
/storagecheck Whether the Storage API can read its S3 bucket.
/whoami Who the API resolved you as. See Authentication.