Custom Object Viewer¶
Available in lakeFS Enterprise. Start a free trial.
A custom object viewer is a web page that lakeFS embeds when a user opens a matching object. Use one to display a file type lakeFS does not render, or to replace a built-in viewer with your own. Viewers match objects by extension or content type; see Matching rules.
Note
The messaging protocol is experimental and may change between releases.
Prerequisites¶
Before you start, you need:
- lakeFS Enterprise version 1.93.0 or higher
- Permission to edit the lakeFS server configuration, where you register the viewer.
- Permission to restart lakeFS, which loads the new configuration.
- Somewhere to host a static HTML page, reachable from each user's browser and meeting the hosting requirements.
The walkthrough below serves the page locally with the HTTP server built into Python 3, but any static HTTP server works.
Building a text viewer¶
1. Create the page¶
Save this as index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Text viewer</title>
</head>
<body>
<pre id="output"></pre>
<script>
window.addEventListener("message", (event) => {
// Ignore messages that are not from the embedding lakeFS page.
if (event.source !== window.parent) return;
if (event.data?.type !== "object-response") return;
const text = new TextDecoder().decode(event.data.content);
document.getElementById("output").textContent = text;
const height = document.documentElement.scrollHeight + "px";
window.parent.postMessage({ type: "set-iframe-height", height }, "*");
});
// Register the listener before telling lakeFS that the viewer is ready.
window.parent.postMessage({ type: "viewer-ready" }, "*");
</script>
</body>
</html>
After the viewer sends viewer-ready, lakeFS returns the
object in object-response. The example decodes its
content as UTF-8 and resizes the frame to fit the text.
2. Serve the page¶
Run this command from the directory containing index.html:
The page is served at http://localhost:8080/. Alternatively, copy
index.html to any static file server and use that URL.
3. Register the viewer¶
Add the viewer to
ui.custom_viewers in the lakeFS
server configuration, setting url to the address where you serve the page:
ui:
custom_viewers:
- name: Text viewer
url: http://localhost:8080/
extensions:
- txt
content_types:
- text/plain
Restart lakeFS to load the new configuration.
4. Test the viewer¶
Upload a file named hello.txt containing known text, then open it from the
repository's objects page. Its contents should appear in the custom viewer.
If the built-in viewer opens or the frame stays blank, see Troubleshooting.
Before making the viewer available to other users, review Hosting requirements and Security.
Displaying an image¶
Replace the <script> element in the text viewer with
this script. It creates a Blob from the ArrayBuffer and displays it through
an object URL:
window.addEventListener("message", (event) => {
if (event.source !== window.parent) return;
if (event.data?.type !== "object-response") return;
const blob = new Blob([event.data.content], { type: event.data.contentType });
const image = document.createElement("img");
image.style.maxWidth = "100%";
image.onload = () => {
const height = document.documentElement.scrollHeight + "px";
window.parent.postMessage({ type: "set-iframe-height", height }, "*");
};
image.src = URL.createObjectURL(blob);
document.body.appendChild(image);
});
window.parent.postMessage({ type: "viewer-ready" }, "*");
Update the viewer configuration to match the image's content type or extension.
Reading part of a large object¶
By default, lakeFS sends the entire object. To read selected bytes instead, send
viewer-ready with metadataOnly: true, then use
range-request messages.
Replace the <script> element in the text viewer with
this script. It shows the end of a large text object, like tail:
const output = document.getElementById("output");
window.addEventListener("message", (event) => {
if (event.source !== window.parent) return;
switch (event.data?.type) {
case "object-response": {
const rangeSize = Math.min(1024, event.data.size);
if (rangeSize > 0) {
const start = event.data.size - rangeSize;
const message = { type: "range-request", id: "tail", start, rangeSize };
window.parent.postMessage(message, "*");
}
break;
}
case "range-response": {
const text = new TextDecoder().decode(event.data.content);
// A range starts mid-line, so drop the partial first line.
output.textContent = text.slice(text.indexOf("\n") + 1);
break;
}
case "range-error":
output.textContent = event.data.error;
break;
}
});
window.parent.postMessage({ type: "viewer-ready", metadataOnly: true }, "*");
Give each request a unique id when you send more than one. Range reads
suit formats such as Parquet, whose metadata sits in the file footer.
How it works¶
lakeFS loads the page in a new sandboxed iframe each time a user opens an
object. The page and lakeFS exchange object data through window.postMessage.
The protocol reference documents every message.
Matching rules¶
When a user opens an object, lakeFS picks a viewer in two steps, using the
values configured in ui.custom_viewers:
- Lowercase the text after the last dot in the object's name, and look for a
viewer that lists it in
extensions. - If none matched, look for a viewer that lists the object's content type in
content_types.
A content type or extension can belong to only one viewer. If no viewer matches, lakeFS falls back to its built-in rendering.
Sandboxing¶
The frame carries
sandbox="allow-scripts",
so a viewer can run scripts but cannot read the cookies or browser storage of
the host that serves it. The sandbox also gives the page an
opaque origin, so
even requests to its own host are cross-origin.
Hosting requirements¶
The viewer URL must be reachable from each user's browser. If the lakeFS UI
uses HTTPS, serve the viewer over HTTPS too; browsers block an HTTP viewer
inside an HTTPS page. http://localhost is exempt.
Any Content-Security-Policy or X-Frame-Options headers on the host must
allow the lakeFS origin to embed the page.
Allowing CORS on the viewer's host¶
Return Access-Control-Allow-Origin: * from the viewer's host if the viewer
loads module scripts or fonts, or reads responses with fetch or
XMLHttpRequest. Those requests are cross-origin because the viewer runs on an
opaque origin. If you cannot change the host's
CORS
configuration, relax the sandbox
instead.
Security¶
A viewer can run scripts and receives the full contents of every matching object a user opens, so register only viewers whose code and hosting you trust. It holds no lakeFS credentials and by default cannot read or change anything through the lakeFS API.
Since object content is user data, render it with safe DOM APIs such as
textContent or an <img> element.
Do not navigate away after startup: lakeFS sends replies to the frame, so a page that replaces it can receive object data and request more.
Relaxing the sandbox with allow_same_origin¶
With allow_same_origin: true,
the viewer keeps its real origin, so requests to its own host are no longer
cross-origin and it can load resources without CORS. lakeFS refuses to load such
a viewer from the lakeFS origin itself.
Prefer CORS where you can configure it on the viewer's host.
Warning
Host the viewer on its own domain, not a sibling subdomain of lakeFS:
viewer.example.com and lakefs.example.com are the same site, so the
viewer's requests to lakeFS could carry the user's session cookie.
Troubleshooting¶
lakeFS shows viewer errors above the frame. It does not send these errors to the viewer.
- The built-in viewer opens. The object did not match a configured content type or extension; see Matching rules.
- The frame stays blank. Check the following:
- Open the viewer URL directly and confirm that it loads. It will not display object content outside lakeFS.
- Open the browser developer tools for the lakeFS tab and check the Console panel for blocked framing, an HTTP viewer inside an HTTPS page, or CORS errors; see Hosting requirements.
- Confirm that the viewer registers its message listener before sending
viewer-ready.
- Changes do not appear. Refresh the repository's objects page to load changes to the viewer page. Configuration changes need a lakeFS restart.
Protocol reference¶
Every message is a plain object with a required type field. Each heading
below gives that value; the code block under it shows the message's shape.
Numeric fields must be finite safe integers.
lakeFS silently ignores messages whose data is not an object. Unknown message types and known types with invalid fields are reported only in the lakeFS UI, never to the viewer; see Troubleshooting.
Message flow¶
A typical exchange starts when the viewer sends
viewer-ready.
sequenceDiagram
participant V as Viewer
participant L as lakeFS
L->>V: Load viewer page
V->>L: viewer-ready { metadataOnly? }
L->>V: object-response { content, contentType, size, path }
opt Read a range
V->>L: range-request { id?, start, rangeSize }
alt Read succeeds
L->>V: range-response { id, content, start }
else Read fails
L->>V: range-error { id, start, error }
end
end
opt Set minimum height
V->>L: set-iframe-height { height }
end
Responses can arrive in a different order from their requests: a viewer that
sends range requests immediately after viewer-ready can receive a
range-response before the
object-response. Match range responses to range
requests with id.
Messages sent by the viewer¶
viewer-ready¶
Send this once, after registering the message listener. lakeFS replies with
object-response containing the entire object; there is
no size limit, and the content is buffered in memory.
Set metadataOnly: true to skip the download and read the bytes you need with
range requests, as shown in
Reading part of a large object.
range-request¶
{
type: "range-request";
id?: unknown; // echoed in the reply; use a unique value
start: number; // 0 or greater, below the object size
rangeSize: number; // 1 or greater
}
Read rangeSize bytes from offset start. lakeFS replies with
range-response, or with range-error if
the read fails. Requests with invalid values get no reply.
Omit id only when at most one request is outstanding; the reply's id is
then undefined.
set-iframe-height¶
Set the frame's minimum height. Send again whenever the content height changes.
Messages sent by lakeFS¶
object-response¶
{
type: "object-response";
content: ArrayBuffer | null; // the whole object; null when metadataOnly was set
contentType: string; // content type from the object's metadata
size: number; // full object size in bytes
path: string; // object path in the repository
}
Sent in reply to viewer-ready. content is null only
when the viewer requested metadataOnly.
If lakeFS cannot load the object, it reports the error in its UI and sends no response.
range-response¶
{
type: "range-response";
id: unknown; // id from the matching request
content: ArrayBuffer; // bytes read from the object
start: number; // start from the matching request
}
Sent in reply to a successful range-request. If a valid
range extends beyond the end of the object, content contains only the
remaining bytes. You have reached the end when start + content.byteLength
equals size from
object-response.
range-error¶
{
type: "range-error";
id: unknown; // id from the matching request
start: number; // start from the matching request
error: string; // description of the failure
}
Sent when fetching a requested range fails.