Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
| [`chat`](./chat.md) | 1 |
| [`database`](./database.md) | 18 |
| [`echo`](./echo.md) | 1 |
| [`files`](./files.md) | 9 |
| [`membership`](./membership.md) | 18 |
76 changes: 76 additions & 0 deletions docs/api/files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# API · Files

Access with `norbix.api.files`.

| Method | Verb | Path | Scope |
| --- | --- | --- | --- |
| `list_files` | `GET` | `/{version}/files/{filesIntegrationId}` | `project` |
| `get_file_info` | `GET` | `/{version}/files/{filesIntegrationId}/info` | `project` |
| `get_signed_url` | `GET` | `/{version}/files/{filesIntegrationId}/sign` | `project` |
| `request_upload_url` | `POST` | `/{version}/files/{filesIntegrationId}/upload-url` | `project` |
| `commit_upload` | `POST` | `/{version}/files/{filesIntegrationId}/commit` | `project` |
| `download_file_api` | `GET` | `/{version}/files/{filesIntegrationId}/download` | `project` |
| `delete_file_api` | `DELETE` | `/{version}/files/{filesIntegrationId}` | `project` |
| `delete_many_files_api` | `DELETE` | `/{version}/files/{filesIntegrationId}/bulk` | `project` |

`filesIntegrationId` names the storage integration to work with. Get it from
`norbix.hub.files.get_files_integrations()`.

## Uploading a file

Uploading takes two steps, so the file bytes never pass through Norbix:

1. `request_upload_url` returns a short-lived address to upload to.
2. Send the bytes to that address with a plain `PUT`.
3. `commit_upload` tells Norbix the upload finished.

```python
upload = client.api.files.request_upload_url(
integration_id,
path="invoices/invoice.pdf",
contentType="application/pdf",
)
httpx.put(upload["url"], content=pdf_bytes, headers={"Content-Type": "application/pdf"})
client.api.files.commit_upload(
integration_id,
path="invoices/invoice.pdf",
contentType="application/pdf",
sizeBytes=len(pdf_bytes),
)
```

## Reading a file

`get_signed_url` returns a short-lived address anyone can open, which suits a
browser preview or a download link. `download_file_api` streams the bytes
through Norbix instead.

## Reading a public link

`get_public_file` reads a file somebody made public with
[`hub.files.make_file_public`](../hub/files.md#public-file-links).

**This call carries no sign-in and no project id.** The SDK deliberately sends
no `Authorization` header for it, even when the client you call it on is signed
in. That is what public means: the link has to work in an e-mail, in an
`<img src>`, or in a browser on a stranger's phone, and the unguessable
`nbpf_…` id is the whole credential.

```python
pdf_bytes = client.api.files.get_public_file("nbpf_7hK2abc", "invoice.pdf")

# a file inside a published folder — the path keeps its slashes
report = client.api.files.get_public_file("nbpf_folder1", "2026/q1/report.pdf")
```

It gives back raw `bytes`. When the storage provider signs its own links
(Amazon S3, Azure Blob, Google Cloud Storage) Norbix answers `302` and the call
follows the redirect, so the bytes come straight from the provider.

Every miss is the same plain `404` — an unknown id, a name that does not match,
a file made private again, a file gone from storage. A more precise answer
would tell a stranger that the file is there.

The link is a plain HTTP address, so anything that can do a `GET` can read it —
`httpx`, `curl`, an `<img>` tag. You do not need this SDK, or a Norbix client
at all, to open one; the method is here for code that already has a client.
2 changes: 1 addition & 1 deletion docs/hub/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
| [`database`](./database.md) | 41 |
| [`echo`](./echo.md) | 1 |
| [`email`](./email.md) | 1 |
| [`files`](./files.md) | 15 |
| [`files`](./files.md) | 22 |
| [`internal`](./internal.md) | 1 |
| [`logs`](./logs.md) | 9 |
| [`membership`](./membership.md) | 25 |
Expand Down
73 changes: 73 additions & 0 deletions docs/hub/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Access with `norbix.hub.files`.
| --- | --- | --- | --- |
| `disable_files` | `GET` | `/{version}/files/disable` | `project` |
| `enable_files` | `GET` | `/{version}/files/enable` | `project` |
| `get_folder_files` | `GET` | `/{version}/files/folder` | `project` |
| `get_file` | `GET` | `/{version}/files/item` | `project` |
| `delete_files_trigger` | `DELETE` | `/{version}/files/triggers/{triggerId}` | `project` |
| `disable_files_trigger` | `PATCH` | `/{version}/files/triggers/{triggerId}/disable` | `project` |
| `enable_files_trigger` | `PATCH` | `/{version}/files/triggers/{triggerId}/enable` | `project` |
Expand All @@ -19,3 +21,74 @@ Access with `norbix.hub.files`.
| `get_files_integrations` | `GET` | `/{version}/files/integrations` | `project` |
| `save_files_integration` | `POST` | `/{version}/files/integrations` | `project` |
| `set_files_integration_as_default` | `PUT` | `/{version}/files/integrations/{Id}/default` | `project` |
| `test_files_integration` | `POST` | `/{version}/files/integrations/test` | `project` |
| `make_file_public` | `POST` | `/{version}/files/item/public` | `project` |
| `make_file_private` | `POST` | `/{version}/files/item/private` | `project` |
| `make_folder_public` | `POST` | `/{version}/files/folder/public` | `project` |
| `make_folder_private` | `POST` | `/{version}/files/folder/private` | `project` |

## What the parts are

* An **integration** is one storage place — an Amazon S3 bucket, an Azure
container, a folder on the server. A project can have several; one is the
default.
* `get_folder_files` lists what is under a path. `get_file` reads the details of
one file.
* `test_files_integration` checks the settings really work before you save them.

Uploading and downloading file bytes is on the public API side — see
[API · Files](../api/files.md).

## Public file links

A file, or a whole folder, can be made readable by anyone holding a link — no
sign-in, no project id, no account. Norbix keeps a record and mints an
unguessable id that looks like `nbpf_7hK2…`; the link is then

```
https://<your api host>/v3/files/public/nbpf_7hK2…/invoice.pdf
```

```python
# one file
result = client.hub.files.make_file_public(
filesIntegrationId=integration_id,
path="invoices/invoice.pdf",
)
public_id = result["id"] # "nbpf_7hK2abc"

# take it back
client.hub.files.make_file_private(
filesIntegrationId=integration_id,
path="invoices/invoice.pdf",
)

# a whole folder — one record, however many files are under it
folder = client.hub.files.make_folder_public(
filesIntegrationId=integration_id,
path="invoices",
)
# every file under invoices/ is now readable at
# https://<api host>/v3/files/public/<folder["id"]>/<path inside the folder>

client.hub.files.make_folder_private(
filesIntegrationId=integration_id,
path="invoices",
)
```

Four rules worth knowing:

* **Publishing a folder is one record**, whatever is under it, at any depth.
* **Asking twice gives the same id back.** The first link is already in
somebody's hands; a second id would leave it live and invisible.
* **A file cannot be made private on its own while a folder above it is
public.** The call is refused (`CM-ERRORS-FILES-021`) and the message names
the folder to switch off.
* **The root cannot be published**, and a folder link with nothing after it is
a `404` — publishing a prefix must not publish its listing.

After any of these, `get_file` and `list_files` report `isPublic` and
`publicUrl` on the file, and a listing carries `publicFolders`. Opening the
link itself is on the public API side — see
[API · Files](../api/files.md#reading-a-public-link).
88 changes: 88 additions & 0 deletions src/norbix_python/api/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,50 @@ def request_upload_url(self, files_integration_id: str, *, timeout: float | None
bearer_token=bearer_token,
)

# ------------------------------------------------------------------
# The public file link (10b-files slice PUB). Hand-added by slice SDK-2.
# ------------------------------------------------------------------

def get_public_file(self, public_id: str, name: str, *, timeout: float | None = None, **request: Any) -> Any:
"""GET /{version}/files/public/{publicId}/{name}

The gateway declares the route as /{version}/files/public/{PublicId}/{Name*} —
written out here exactly as the gateway spells it, so the SDK coverage
matrix can see that the two agree (knowledge.md K23).

Reads a file somebody made public.

**No sign-in and no project id.** The SDK deliberately sends no
``Authorization`` header for this call, even when the client is signed
in: the link has to work in an e-mail, in an ``<img src>``, or in a
browser on a stranger's phone. The unguessable ``nbpf_…`` id is the
whole credential.

Gives back the file's raw ``bytes``. When the storage provider signs
its own links (Amazon S3, Azure Blob, Google Cloud Storage) the gateway
answers 302 and this call follows the redirect, so the bytes come from
the provider and never pass through Norbix.

``name`` is the file's name for a file link, or the path inside the
folder for a folder link (``2026/q1/report.pdf``); its slashes stay
slashes.

Every miss is the same plain 404 — an unknown id, a name that does not
match, a file made private again, a file gone from storage. A more
precise answer would tell a stranger the file is there.
"""
return self._transport.send(
target="api",
path="/{version}/files/public/{publicId}/{name}",
method="GET",
path_params={"publicId": public_id, "name": name},
request=request,
scope="unauthenticated",
timeout=timeout,
response_type="binary",
follow_redirects=True,
)


class AsyncFilesModule:
def __init__(self, transport: AsyncTransport) -> None:
Expand Down Expand Up @@ -221,3 +265,47 @@ async def request_upload_url(self, files_integration_id: str, *, timeout: float
timeout=timeout,
bearer_token=bearer_token,
)

# ------------------------------------------------------------------
# The public file link (10b-files slice PUB). Hand-added by slice SDK-2.
# ------------------------------------------------------------------

async def get_public_file(self, public_id: str, name: str, *, timeout: float | None = None, **request: Any) -> Any:
"""GET /{version}/files/public/{publicId}/{name}

The gateway declares the route as /{version}/files/public/{PublicId}/{Name*} —
written out here exactly as the gateway spells it, so the SDK coverage
matrix can see that the two agree (knowledge.md K23).

Reads a file somebody made public.

**No sign-in and no project id.** The SDK deliberately sends no
``Authorization`` header for this call, even when the client is signed
in: the link has to work in an e-mail, in an ``<img src>``, or in a
browser on a stranger's phone. The unguessable ``nbpf_…`` id is the
whole credential.

Gives back the file's raw ``bytes``. When the storage provider signs
its own links (Amazon S3, Azure Blob, Google Cloud Storage) the gateway
answers 302 and this call follows the redirect, so the bytes come from
the provider and never pass through Norbix.

``name`` is the file's name for a file link, or the path inside the
folder for a folder link (``2026/q1/report.pdf``); its slashes stay
slashes.

Every miss is the same plain 404 — an unknown id, a name that does not
match, a file made private again, a file gone from storage. A more
precise answer would tell a stranger the file is there.
"""
return await self._transport.send(
target="api",
path="/{version}/files/public/{publicId}/{name}",
method="GET",
path_params={"publicId": public_id, "name": name},
request=request,
scope="unauthenticated",
timeout=timeout,
response_type="binary",
follow_redirects=True,
)
Loading
Loading