Skip to content

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:

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:

python3 -m http.server 8080

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:

  1. Lowercase the text after the last dot in the object's name, and look for a viewer that lists it in extensions.
  2. 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:
    1. Open the viewer URL directly and confirm that it loads. It will not display object content outside lakeFS.
    2. 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.
    3. 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

{
  type: "viewer-ready";
  metadataOnly?: boolean; // default false
}

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

{
  type: "set-iframe-height";
  height: string; // CSS length, such as "600px"
}

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.