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 withuser_idor an OAuth user token.
Scopes
| Scope | Grants |
|---|---|
drive:read | Browse, download, list shares, list versions, delta sync |
drive:write | Upload, 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"Search
GET /api/v1/drive/search?q=quarterly+reportReturns matched files and folders across the user’s Drive.
Storage usage
GET /api/v1/drive/storageReturns { used_bytes, quota_bytes, file_count }.
File operations
Get file metadata
GET /api/v1/drive/{file_id}/detailDownload a file
GET /api/v1/drive/{file_id}/downloadReturns { 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.pdfUpload 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.pdfStep 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 trashDelete permanently
DELETE /api/v1/drive/{file_id} # Permanent deletion — 204 No ContentSharing
Create a share link
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}/sharesDelete a share
DELETE /api/v1/drive/{file_id}/shares/{share_id}Access a shared link (no auth)
GET /api/v1/drive/shared-link/{token}Versions
List versions
GET /api/v1/drive/{file_id}/versionsDownload a specific version
GET /api/v1/drive/{file_id}/versions/{version_id}/downloadReturns a presigned URL (same as regular download).
Restore a version
POST /api/v1/drive/{file_id}/versions/{version_id}/restoreDelta 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
| Method | Behaviour |
|---|---|
OPTIONS | Advertises DAV: 1, 2 |
PROPFIND | List folders and files (Depth 0 and 1) |
GET / HEAD | Download a file / fetch metadata |
PUT | Upload or overwrite (512 MB per file) |
MKCOL | Create a folder |
DELETE | Move to Drive Trash (recoverable, not a hard delete) |
MOVE | Move or rename |
COPY | Copy |
LOCK / UNLOCK | Exclusive write locks, shared with Collabora (30 min TTL) |
PROPPATCH | Acknowledged (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.comSign 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 mefolder; writes into that subtree return403. 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. GETstreams 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_idto your PAT. - Real-time file change subscriptions via long-poll or SSE — use webhooks + delta sync instead.