Skip to content

Latest commit

 

History

History
692 lines (548 loc) · 21.1 KB

File metadata and controls

692 lines (548 loc) · 21.1 KB

Lazync HTTP API

This document describes protocol version 1 as implemented by the current server.

Transport And Authentication

GET /api/v1/health, POST /api/v1/auth/login, and the device refresh/revoke routes do not require a session Bearer header. Login accepts a Lazync account username and password and returns an opaque session token. Device refresh and revoke authenticate with their opaque device secret in the JSON body. Every other /api/v1/* endpoint requires a session token:

Authorization: Bearer SESSION_TOKEN

Session tokens are random, expire after the configured idle lifetime, and are not persisted across a server restart. Device tokens are long-lived and revocable; only their SHA-256 hashes are persisted by the server. Do not place a password, device token, or session token in a URL, log, or configuration file.

Use HTTPS for every non-loopback connection. A reverse proxy may terminate TLS only when the backend server listens on loopback. API responses include X-Request-ID; include this value when reporting a server error.

The Web UI uses the same accounts through a form login and an HttpOnly, SameSite=Strict session cookie. Set web_secure_cookie=1 when HTTPS is terminated by a reverse proxy, because the backend listener cannot otherwise detect the external scheme.

Error Format

API errors use JSON:

{
  "ok": false,
  "code": "not_found",
  "message": "File not found.",
  "request_id": "a1b2c3..."
}

Internal errors return a generic message. Details and the same request ID are written to the server log.

Common status codes are 400, 401, 403, 404, 405, 409, 413, 422, 429, and 500.

Health

GET /api/v1/health

No authentication is required. Example response:

{
  "status": "ok",
  "app": "Lazync",
  "version": "0.1.0-dev",
  "protocol_version": 1,
  "instance_id": "0123456789abcdef0123456789abcdef"
}

instance_id is a stable, non-secret identity stored in the managed library. Clients use it to recognize one server behind different LAN and WAN URLs. The response intentionally does not expose storage paths or configuration.

Login

POST /api/v1/auth/login
Content-Type: application/json

{"username":"alice","password":"correct horse battery staple"}

Successful response:

{
  "ok": true,
  "session_token": "OPAQUE_RANDOM_VALUE",
  "expires_unix": 1785600000,
  "instance_id": "0123456789abcdef0123456789abcdef",
  "account_id": "fedcba9876543210fedcba9876543210",
  "username": "alice",
  "administrator": false,
  "server_version": "0.1.0-dev",
  "protocol_version": 1
}

Invalid credentials return 401 with the same message for unknown users and wrong passwords. Five failures for one normalized username and source address within five minutes return 429 rate_limited and Retry-After: 300.

Passwords created or changed in the Web UI must contain at least 8 valid UTF-8 characters and no more than 512 UTF-8 bytes. An 8-character ASCII password meets the minimum.

Session

GET /api/v1/session
Authorization: Bearer SESSION_TOKEN

Validates and refreshes the idle session and returns its account context:

{
  "ok": true,
  "server_version": "0.1.0-dev",
  "protocol_version": 1,
  "https": true,
  "instance_id": "0123456789abcdef0123456789abcdef",
  "account_id": "fedcba9876543210fedcba9876543210",
  "username": "alice",
  "administrator": false,
  "expires_unix": 1785600000
}

https describes the server's own listener. It is false when TLS terminates at a reverse proxy.

End a session explicitly with:

POST /api/v1/auth/logout
Authorization: Bearer SESSION_TOKEN

Sessions and device credentials are invalidated when an administrator changes the account password, disables the account, or deletes it. Permission changes apply to existing sessions on their next request.

Device Credentials

Create a persistent device credential after password login:

POST /api/v1/auth/device
Authorization: Bearer SESSION_TOKEN
Content-Type: application/json

{"device_name":"WORKSTATION-01"}

The optional UTF-8 device name is limited to 128 bytes. A user may have up to 32 devices; issuing another removes the least recently used device and any short-lived sessions issued from it. Successful creation returns HTTP 201:

{
  "ok": true,
  "instance_id": "0123456789abcdef0123456789abcdef",
  "account_id": "fedcba9876543210fedcba9876543210",
  "device_id": "11111111111111111111111111111111",
  "device_token": "OPAQUE_LONG_LIVED_SECRET"
}

The token is returned only once. A client stores it in the operating system's credential store, never in its INI file.

Obtain a new memory-only session after a client or server restart:

POST /api/v1/auth/device/refresh
Content-Type: application/json

{"device_id":"11111111111111111111111111111111","device_token":"OPAQUE_LONG_LIVED_SECRET"}

The response contains the same session and identity fields as password login. An unknown, invalid, disabled, or revoked device returns 401.

Explicit sign-out revokes the device credential:

POST /api/v1/auth/device/revoke
Authorization: Bearer SESSION_TOKEN
Content-Type: application/json

{"device_id":"11111111111111111111111111111111","device_token":"OPAQUE_LONG_LIVED_SECRET"}

The Bearer header is optional; when present, that session is also removed. A matching device ID with the wrong token returns 401; an already absent device is treated as successfully revoked. Successful revocation also removes short-lived sessions previously issued from that device credential. Clients must delete their local credential only after the server responds, then retain local files while marking the folder unbound.

Directory Permissions

Administrators have read/write access to the whole library. A regular account has zero or more directory grants. A grant applies to the named relative directory and all descendants; * denotes the whole library. Read-only grants allow listing, download, path validation, and version history. Read/write grants additionally allow upload, move, delete, and version restore.

File and directory lists are filtered before pagination, so they contain only accessible paths and report an accessible total. Directory listings also include existing ancestors needed to reach a granted subtree. Direct access outside the account's grants returns 403 forbidden. A move requires write access to both source and destination.

Path Validation

GET /api/v1/path/validate?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKEN

Path rules:

  • Relative paths only; normalized separator is /.
  • No absolute paths or parent (..) segments.
  • No control characters or Windows-incompatible characters/endings.
  • No Windows reserved device names.
  • Maximum relative path: 2048 UTF-8 bytes.
  • Maximum segment: 240 UTF-8 bytes.
  • Maximum depth: 64 segments.
  • Maximum resolved physical path: 3800 bytes.

Invalid paths receive HTTP 400 and a stable path error code.

List Files

GET /api/v1/files/list?offset=0&limit=200&include_hash=0
Authorization: Bearer SESSION_TOKEN

Parameters:

Name Default Limit Meaning
offset 0 non-negative integer Sorted result offset
limit 200 1..1000 Page size
include_hash 0 0, 1, true Include SHA-256; adds file I/O

Example:

{
  "ok": true,
  "offset": 0,
  "limit": 200,
  "total": 1,
  "sync_scope_version": 1,
  "read_roots": ["*"],
  "files": [
    {
      "path": "Documents/readme.txt",
      "size": 125,
      "modified_unix": 1784304000
    }
  ]
}

The server aborts the listing if the configured max_list_entries or scan safety limit is exceeded.

The first page after a server start or library mutation rebuilds a shared, sorted file/directory path cache with one tree walk. Subsequent pages use index slices, and permission-filtered pages use merged path ranges. Clients should still page sequentially and must not assume that an offset snapshot remains stable across concurrent writes.

sync_scope_version: 1 declares that the paginated result is complete for the listed read_roots. A regular account receives its normalized readable grant roots; an administrator receives *. Sync clients use this scope to distinguish a deleted file from a file that merely became invisible after a permission change. They still confirm a missing path through the metadata endpoint before deleting local content.

Browse Directory

GET /api/v1/directories/entries?path=Documents
Authorization: Bearer SESSION_TOKEN

path is optional; omit it to browse the file-library root. The response contains only the immediate children of that directory, sorted with folders before files. Empty folders therefore appear without requiring a placeholder file.

{
  "ok": true,
  "path": "Documents",
  "total": 2,
  "entries": [
    {
      "path": "Documents/Empty folder",
      "name": "Empty folder",
      "type": "directory",
      "size": 0,
      "modified_unix": 1784303900
    },
    {
      "path": "Documents/readme.txt",
      "name": "readme.txt",
      "type": "file",
      "size": 125,
      "modified_unix": 1784304000
    }
  ]
}

The requested directory and every returned entry are permission-filtered. A directory needed as an ancestor of a granted path is visible, but files outside the account's read grants and unrelated sibling directories are not. Browsing an inaccessible directory returns HTTP 403; a missing directory returns HTTP 404. The configured max_list_entries applies to each directory.

List Directories

GET /api/v1/directories/list?offset=0&limit=200
Authorization: Bearer SESSION_TOKEN

offset and limit use the same bounds as the file list. Results contain all live directories, including empty directories, in sorted relative-path order:

{
  "ok": true,
  "offset": 0,
  "limit": 200,
  "total": 2,
  "directories": [
    {"path": "Documents"},
    {"path": "Documents/Empty folder"}
  ]
}

Permission filtering happens before pagination. A regular account receives its granted directories, their descendants, and existing ancestors needed to construct the visible tree; sibling and unrelated directories are omitted.

Incremental Change Feed

GET /api/v1/changes?cursor=EPOCH%3ASEQUENCE&limit=500
Authorization: Bearer SESSION_TOKEN

limit defaults to 500 and accepts 1..1000. A cursor is opaque to clients even though the current representation is <32-hex epoch>:<sequence>.

{
  "ok": true,
  "cursor": "0123456789abcdef0123456789abcdef:42",
  "has_more": false,
  "reset_required": false,
  "journal_healthy": true,
  "changes": [
    {
      "sequence": 42,
      "path": "Documents/readme.txt",
      "entry_type": "file",
      "action": "upsert"
    }
  ]
}

entry_type is file or directory; action is upsert or delete. A file move is represented by a delete for the old path followed by an upsert for the new path. Events outside the account's visible read scope are omitted, while the returned cursor still advances past them.

An absent, malformed, foreign, future, or expired cursor returns HTTP 200 with reset_required: true, an empty change list, and the current cursor. The client must take a complete permission-filtered file and directory snapshot, then continue from that cursor. Permission changes are also reconciled through that periodic snapshot because they do not create file-change events.

The append-only journal compacts after 1,000,000 records and retains the newest 500,000. journal_healthy: false requires complete snapshot fallback; clients must rate-limit that fallback rather than repeatedly requesting full lists.

Create Directory

POST /api/v1/directories/create?path=Documents%2FEmpty%20folder
Authorization: Bearer SESSION_TOKEN
Content-Type: application/json

{}

The caller needs write access to path, and its parent directory must already exist. Creation is idempotent: a new directory returns 201 with "created": true; an existing directory returns 200 with "created": false. A file or unsupported filesystem entry at the same path returns 409.

{
  "ok": true,
  "path": "Documents/Empty folder",
  "created": true
}

Protocol version 1 has no public directory-delete API. The Web UI may delete an empty directory, which emits a change event. The Windows client removes the corresponding local directory only if it was synchronized and is still empty.

Get File Metadata

GET /api/v1/files/metadata?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKEN

Returns metadata without returning file content:

{
  "ok": true,
  "path": "Documents/readme.txt",
  "size": 125,
  "sha256": "LOWERCASE_SHA256"
}

The caller needs read access. An inaccessible path returns 403; a missing live file returns 404. The sync client uses this distinction to confirm a server-side deletion after a path disappears from a complete scoped listing.

Download File

GET /api/v1/files/content?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKEN

The response body is application/octet-stream. The ETag is the quoted lowercase SHA-256 hash of the returned content and Accept-Ranges is bytes.

Conditional download:

If-None-Match: "CURRENT_SHA256"

Returns 304 if unchanged.

Resumable download uses one explicit byte range and pins the source version:

Range: bytes=8388608-
If-Match: "EXPECTED_SHA256"

The server returns 206, Content-Range, the same ETag, and the requested suffix. Multiple ranges and suffix-only ranges are not supported; malformed or unsatisfiable ranges return 416. A changed If-Match version returns 412. The client appends only a valid 206 response whose range start and ETag match its local partial file, then verifies final size and SHA-256 before atomic installation.

Resumable Upload

Start or recover an upload session:

POST /api/v1/uploads/start
Authorization: Bearer SESSION_TOKEN
Content-Type: application/json

{
  "path": "Documents/archive.bin",
  "size": 12582912,
  "sha256": "LOWERCASE_SHA256",
  "expected_current_hash": "!absent"
}

expected_current_hash may be empty for an unconditional write, !absent for create-only behavior, or the current lowercase SHA-256 for an overwrite. A new session returns 201; repeating the same account/path/size/hash/precondition returns the existing session with 200:

{
  "ok": true,
  "created": true,
  "upload_id": "11111111111111111111111111111111",
  "path": "Documents/archive.bin",
  "size": 12582912,
  "sha256": "LOWERCASE_SHA256",
  "chunk_size": 8388608,
  "received_bytes": 0,
  "created_unix": 1785600000,
  "updated_unix": 1785600000
}

Send sequential chunks at the acknowledged offset:

PUT /api/v1/uploads/chunk?id=UPLOAD_ID&offset=0&hash=CHUNK_SHA256
Authorization: Bearer SESSION_TOKEN
Content-Type: application/octet-stream

...one chunk...

The hash is required. A hash mismatch returns 422; an offset other than received_bytes returns 409; a chunk above configured max_chunk_bytes or beyond the declared file size returns 413. Each accepted chunk and manifest is flushed before its new offset is acknowledged.

Inspect, commit, or cancel the account-owned session:

GET  /api/v1/uploads/status?id=UPLOAD_ID
POST /api/v1/uploads/commit?id=UPLOAD_ID
POST /api/v1/uploads/cancel?id=UPLOAD_ID

Status returns the same fields as start. Commit requires all declared bytes, verifies the complete SHA-256 and optimistic precondition, then uses the normal versioned atomic file commit. It returns file metadata with 201 for a create or 200 for an overwrite. Cancel deletes the manifest and partial content.

Sessions and partial bytes survive a server restart. Idle sessions expire after seven days and the server admits at most 1,024 active sessions. If a commit succeeded but its response was lost, a repeated start recognizes the matching live size and SHA-256, reports all bytes received, and allows a no-op commit without retransmitting the file.

Compatibility Whole-File Upload

PUT /api/v1/files/content?path=Documents%2Freadme.txt&hash=NEW_SHA256
Authorization: Bearer SESSION_TOKEN
Content-Type: application/octet-stream

...whole file body...
  • PUT is the only accepted method on this compatibility endpoint.
  • hash is optional. If supplied and the body differs, the server returns 422 hash_mismatch without changing the current file.
  • A successful create returns 201; an overwrite returns 200.
  • The response and ETag contain the committed SHA-256.
  • The request body must not exceed configured max_upload_bytes.
  • The listener rejects connections above configured max_connections.

Optimistic concurrency options:

If-Match: "EXPECTED_CURRENT_SHA256"

Writes only if the current content has that hash. If-Match: * requires the file to exist. The value must be one quoted or unquoted hexadecimal SHA-256 (or *); empty, malformed, or multi-value conditions return 400.

If-None-Match: *

Creates only if the path is absent. A failed precondition returns 409 and does not change the file. Upload requests must not combine If-Match and If-None-Match; malformed combinations return 400 before content is committed.

Move File

POST /api/v1/files/move
Authorization: Bearer SESSION_TOKEN
Content-Type: application/x-www-form-urlencoded

source_path=Documents%2Fold.txt&target_path=Documents%2Fnew.txt

The two parameters may also be passed in the query string. The target must be absent. A move preserves the immutable file identity and all version history.

Delete File

DELETE /api/v1/files/content?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKEN

The server creates a historical version, moves the live content into internal trash, and returns an opaque trash_id. There is no public trash restore API in protocol version 1.

List Versions

GET /api/v1/files/versions?path=Documents%2Freadme.txt
Authorization: Bearer SESSION_TOKEN

Returns sorted opaque version IDs for the file's immutable identity.

Restore Version

POST /api/v1/files/versions/restore
Authorization: Bearer SESSION_TOKEN
Content-Type: application/x-www-form-urlencoded

path=Documents%2Freadme.txt&version_id=VERSION_ID

Parameters may also be in the query string. If a current file exists, it is versioned before the selected historical content becomes current. The response contains the restored SHA-256.

Web Routes

GET  /web/login?lang=en|zh
POST /web/login?lang=en|zh
GET  /web/lazync.js
POST /web/logout?lang=en|zh
GET  /web/files?dir=...&lang=en|zh
GET  /web/files/download?path=...&lang=en|zh
POST /web/files/upload?lang=en|zh
POST /web/files/create-directory?lang=en|zh
POST /web/files/delete?lang=en|zh
GET  /web/files/history?path=...&lang=en|zh
POST /web/files/restore-version?lang=en|zh
GET  /web/admin/users?lang=en|zh
POST /web/admin/users/create?lang=en|zh
GET  /web/admin/users/edit?username=...&lang=en|zh
POST /web/admin/users/update?lang=en|zh
POST /web/admin/users/delete?lang=en|zh

All routes except the login page and its static script require a valid session cookie. Administrator user-management routes require an administrator account. Every authenticated POST contains a per-session CSRF token; login has a separate process-local CSRF token. The account named by security.admin_username is root-managed: its edit, update, and delete routes return 409, and its controls are omitted from the user list. Change that account in the server INI and restart instead.

The file page browses one directory at a time; an omitted dir denotes the library root. Directory ancestors needed to reach an account's grants are visible without granting access to siblings. Upload uses multipart/form-data fields upload_file, directory, csrf_token, and an optional overwrite=1. Existing files are rejected unless overwrite is explicit; replacement creates a historical version. Directory creation uses name and directory. Deletion uses path, directory, and kind=file|directory; files follow the normal version/trash path and a directory must be empty.

Current Protocol Limits

  • Desktop resumable transfers accept files through configured max_file_bytes, which defaults to 2,000,000,000,000 bytes. Each upload request is bounded by max_chunk_bytes, default 8 MiB.
  • Compatibility whole-file API and Web multipart requests remain buffered by FCL-Web and are separately bounded by max_upload_bytes, default 256 MiB. Multipart framing counts toward that request-body limit.
  • Management-client text responses are capped at 8 MiB.
  • Short sessions are server-memory state, but bound clients obtain a new one from their persistent revocable device credential after a server restart. There is not yet a device/session inventory or selective revocation UI.
  • The change feed has path-level tombstones but no batch mutation API. The client periodically reconciles full scoped lists to cover permission changes, expired cursors, watcher overflow, and offline edits. Local deletion and rename propagation remain disabled; remote deletions are applied only when the local item still matches synchronized state.
  • The server is intended to be the exclusive writer of its managed library.