Skip to Content
Developer APIDrive API

Drive API

Base path: /api/v1/drive/
Plan feature required: drive
Auth: PAT bound to a user, or Keycloak user-delegated JWT

Drive operates on per-user data. Every request must resolve to an acting user. A pure service account token (client credentials without a bound user) is rejected with 403 Forbidden. Use a PAT with user_id or an OAuth user token.


Scopes

ScopeGrants
drive:readBrowse, download, list shares, list versions, delta sync
drive:writeUpload, create folders, rename, move, copy, share, trash, restore, permanently delete

Browse

# Root folder GET /api/v1/drive/browse # Specific folder GET /api/v1/drive/browse/{folder_id}

Query parameters: limit, offset

Response: FolderContents with breadcrumbs, pagination, and a list of FileItem objects.

curl https://api.eunifyer.com/api/v1/drive/browse \ -H "Authorization: Bearer $TOKEN"

GET /api/v1/drive/search?q=quarterly+report

Returns matched files and folders across the user’s Drive.


Storage usage

GET /api/v1/drive/storage

Returns { used_bytes, quota_bytes, file_count }.


File operations

Get file metadata

GET /api/v1/drive/{file_id}/detail

Download a file

GET /api/v1/drive/{file_id}/download

Returns { url: string } — a short-lived presigned S3 URL. Redirect to this URL to download the file directly (no auth needed for the S3 URL itself).

DOWNLOAD_URL=$(curl -s https://api.eunifyer.com/api/v1/drive/$FILE_ID/download \ -H "Authorization: Bearer $TOKEN" | jq -r .url) curl -L "$DOWNLOAD_URL" -o output.pdf

Upload a file (3-step)

Step 1 — Request an upload URL:

curl -X POST https://api.eunifyer.com/api/v1/drive/upload \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "report.pdf", "mime_type": "application/pdf", "size": 204800, "parent_id": "optional-folder-uuid" }'

Response: { "upload_url": "https://…", "file": { "id": "uuid", … } }

Step 2 — Upload the file bytes directly to S3 (no auth needed):

curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: application/pdf" \ --data-binary @report.pdf

Step 3 — Confirm the upload:

curl -X POST https://api.eunifyer.com/api/v1/drive/$FILE_ID/confirm \ -H "Authorization: Bearer $TOKEN"

Returns the final FileItem. The file is now indexed and visible in Drive.

Resumable uploads (TUS)

For large files or unreliable connections, use the TUS protocol  at /api/v1/drive/tus/.

import * as tus from "tus-js-client"; const upload = new tus.Upload(file, { endpoint: "https://api.eunifyer.com/api/v1/drive/tus/", headers: { Authorization: `Bearer ${token}` }, metadata: { filename: file.name, filetype: file.type, parent_id: parentFolderId, }, chunkSize: 10 * 1024 * 1024, // 10 MB onProgress(bytesUploaded, bytesTotal) { console.log(`${Math.round((bytesUploaded / bytesTotal) * 100)}%`); }, onSuccess() { console.log("Upload complete:", upload.url); }, }); upload.start();

TUS requires drive:write on create/PATCH/DELETE and drive:read on HEAD.

Create a folder

curl -X POST https://api.eunifyer.com/api/v1/drive/folders \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Q1 Reports", "parent_id": "optional-folder-uuid" }'

Rename

PATCH /api/v1/drive/{file_id}/rename { "name": "Q1 Reports Final.pdf" }

Move

PATCH /api/v1/drive/{file_id}/move { "target_folder_id": "destination-folder-uuid" }

Copy

POST /api/v1/drive/{file_id}/copy { "target_folder_id": "optional-destination-folder-uuid" }

Trash and restore

POST /api/v1/drive/{file_id}/trash # Move to trash (recoverable) POST /api/v1/drive/{file_id}/restore # Restore from trash

Delete permanently

DELETE /api/v1/drive/{file_id} # Permanent deletion — 204 No Content

Sharing

curl -X POST https://api.eunifyer.com/api/v1/drive/$FILE_ID/shares \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "create_link": true, "permission": "view", "expires_at": "2026-12-31T23:59:59Z" }'

Response: ShareInfo with a share_link URL (public, no auth required to access).

List shares

GET /api/v1/drive/{file_id}/shares

Delete a share

DELETE /api/v1/drive/{file_id}/shares/{share_id}
GET /api/v1/drive/shared-link/{token}

Versions

List versions

GET /api/v1/drive/{file_id}/versions

Download a specific version

GET /api/v1/drive/{file_id}/versions/{version_id}/download

Returns a presigned URL (same as regular download).

Restore a version

POST /api/v1/drive/{file_id}/versions/{version_id}/restore

Delta sync

See Pagination & Delta Sync for a full guide.

Quick reference:

# Initial full snapshot GET /api/v1/drive/sync/delta?device_id=MY_DEVICE_ID # Incremental changes since cursor GET /api/v1/drive/sync/delta?device_id=MY_DEVICE_ID&cursor=opaque-cursor # Commit sync progress server-side POST /api/v1/drive/sync/commit { "device_id": "MY_DEVICE_ID", "cursor": "latest-cursor" }

WebDAV (mount Drive as a network drive)

Drive also speaks WebDAV (RFC 4918), so an operating system or file manager can mount it and work on files locally instead of calling the REST API.

  • Short client URL: https://web.eunifyer.com
  • Canonical endpoint: https://web.eunifyer.com/api/v1/webdav/
  • Auth: HTTP Basic with an app-specific password (Settings → App Passwords) — the same credential CalDAV, CardDAV and IMAP use. Account passwords live in Keycloak and are not accepted. Bearer JWT also works for programmatic clients.
  • Compliance: DAV classes 1 and 2 (locking).

Both URLs reach the same Drive root; https://web.eunifyer.com exists so users have something short to type during manual setup, matching cal. for CalDAV and car. for CardDAV.

Methods

MethodBehaviour
OPTIONSAdvertises DAV: 1, 2
PROPFINDList folders and files (Depth 0 and 1)
GET / HEADDownload a file / fetch metadata
PUTUpload or overwrite (512 MB per file)
MKCOLCreate a folder
DELETEMove to Drive Trash (recoverable, not a hard delete)
MOVEMove or rename
COPYCopy
LOCK / UNLOCKExclusive write locks, shared with Collabora (30 min TTL)
PROPPATCHAcknowledged (clients set timestamps)

Every operation runs through the same permission, quota, sync-event and activity machinery as the web UI, so a WebDAV change is indistinguishable from one made in the browser.

Connecting

macOS Finder Go → Connect to Server (⌘K) → https://web.eunifyer.com Windows Explorer Map Network Drive → https://web.eunifyer.com Linux (GNOME) Other Locations → davs://web.eunifyer.com

Sign in with your email address and an app-specific password.

Client compatibility

Any WebDAV-capable client works. Worth knowing on mobile:

  • iOS/iPadOS Files — WebDAV is built in (Connect to Server).
  • Android — the stock Samsung My Files app cannot connect. Its network storage supports SMB, FTP and SFTP only, with no WebDAV option. Use a file manager that speaks WebDAV (Solid Explorer, CX File Explorer, X-plore, Total Commander with its WebDAV plugin, or rclone) and point it at https://web.eunifyer.com/api/v1/webdav/.

Limitations

  • The mount root is your own Drive. Files shared with you, org-wide shares and ShareHub site files appear under a read-only Shared with me folder; writes into that subtree return 403. Edit shared files in the web UI.
  • A file locked over WebDAV or open in Collabora cannot be overwritten, moved, copied over or deleted by another user until the lock is released or expires. Pass the lock token in the If: header to mutate your own locked file.
  • GET streams the whole object through the API. Very large files are better handled in the web UI or the desktop sync app.

Full upload + list example

import httpx, pathlib API = "https://api.eunifyer.com/api/v1" def upload_file(token: str, path: str, parent_id: str | None = None) -> dict: client = httpx.Client(base_url=API, headers={"Authorization": f"Bearer {token}"}) p = pathlib.Path(path) # Step 1: request upload URL r = client.post("/drive/upload", json={ "name": p.name, "mime_type": "application/octet-stream", "size": p.stat().st_size, **({"parent_id": parent_id} if parent_id else {}), }).raise_for_status().json() file_id = r["file"]["id"] # Step 2: PUT to S3 with open(path, "rb") as f: httpx.put(r["upload_url"], content=f.read()).raise_for_status() # Step 3: confirm return client.post(f"/drive/{file_id}/confirm").raise_for_status().json() def list_files(token: str, folder_id: str | None = None) -> list: client = httpx.Client(base_url=API, headers={"Authorization": f"Bearer {token}"}) path = f"/drive/browse/{folder_id}" if folder_id else "/drive/browse" return client.get(path).raise_for_status().json()["items"]

What’s not in v1

  • Machine-level Drive access without a bound user — bind a user_id to your PAT.
  • Real-time file change subscriptions via long-poll or SSE — use webhooks + delta sync instead.