Skip to content

API Documentation

Complete reference for the ocmonica REST and gRPC APIs.

Table of Contents

Overview

ocmonica provides two API interfaces:

  • REST API: Standard HTTP JSON API for web and mobile clients
  • gRPC API: High-performance RPC API with streaming support via ConnectRPC

Both APIs share the same business logic and data models, providing consistent behavior across interfaces.

API Versions

  • Current: v1
  • Stability: Alpha
  • Base Path: /api/v1 (REST), /ocmonica.v1.* (gRPC)

Base URL

Development

http://localhost:8080

Production

https://your-domain.com

Docker Container

http://localhost:8080

Authentication

Current Status: ✅ Fully Implemented

ocmonica supports two authentication methods: - JWT Tokens: Bearer token authentication with RS256 (4096-bit RSA keys) - API Keys: Long-lived API keys with scoped permissions

Authentication Methods

JWT Bearer Tokens

After registration or login, you'll receive an access token and refresh token:

# Register or login
curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"SecurePass123!"}'

# Use access token
curl http://localhost:8080/api/v1/files \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

Token Details: - Access Token: Short-lived (15 minutes default) - Refresh Token: Long-lived (7 days default) - Algorithm: RS256 with 4096-bit RSA keys - Claims: user_id, organization_id, role, permissions

API Keys

API keys provide programmatic access with scoped permissions:

# Create API key (requires authentication)
curl -X POST http://localhost:8080/api/v1/auth/api-keys \
  -H "Authorization: Bearer <access-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "production-key",
    "organization_id": "org_...",
    "permissions": ["file:read", "file:write"]
  }'

# Use API key
curl http://localhost:8080/api/v1/files \
  -H "X-API-Key: key_..."

API Key Features: - Scoped permissions (subset of user's permissions) - Expiration dates (optional) - Revocable at any time - Max 10 keys per user (configurable)

Multi-tenancy

All authenticated requests are scoped to the user's organization. Users belong to one or more organizations, and all file operations are isolated within the active organization context.

REST API

Health Check

GET /health

Check if the server is running and healthy.

Response:

{
  "status": "healthy",
  "version": "0.1.0"
}

Status Codes: - 200 OK: Server is healthy - 503 Service Unavailable: Server is unhealthy


Authentication Endpoints

All authentication endpoints are prefixed with /api/v1/auth.

POST /api/v1/auth/register

Register a new user account.

Request Body:

{
  "username": "newuser",
  "email": "user@example.com",
  "password": "SecurePass123!"
}

Example:

curl -X POST http://localhost:8080/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","email":"admin@example.com","password":"SecurePass123!"}'

Response (201 Created):

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIs...",
  "user": {
    "id": "usr_...",
    "username": "admin",
    "email": "admin@example.com",
    "is_active": true,
    "created_at": "2025-10-09T12:00:00Z"
  }
}

Error Responses: - 400 Bad Request: Invalid input or weak password - 409 Conflict: Username or email already exists - 403 Forbidden: Registration disabled


POST /api/v1/auth/login

Login with username and password.

Request Body:

{
  "username": "admin",
  "password": "SecurePass123!"
}

Response (200 OK):

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIs...",
  "user": {
    "id": "usr_...",
    "username": "admin",
    "email": "admin@example.com"
  }
}

Error Responses: - 401 Unauthorized: Invalid credentials - 403 Forbidden: User account is inactive


POST /api/v1/auth/refresh

Refresh access token using refresh token.

Request Body:

{
  "refresh_token": "eyJhbGciOiJSUzI1NiIs..."
}

Response (200 OK):

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIs...",
  "user": { ... }
}

Error Responses: - 401 Unauthorized: Invalid or expired refresh token


POST /api/v1/auth/logout

Revoke a refresh token (logout from current device).

Headers: - Authorization: Bearer <access-token>

Request Body:

{
  "refresh_token": "eyJhbGciOiJSUzI1NiIs..."
}

Response (200 OK):

{
  "success": true
}


POST /api/v1/auth/logout-all

Revoke all refresh tokens for the user (logout from all devices).

Headers: - Authorization: Bearer <access-token>

Response (200 OK):

{
  "success": true
}


POST /api/v1/auth/change-password

Change user password.

Headers: - Authorization: Bearer <access-token>

Request Body:

{
  "current_password": "OldPass123!",
  "new_password": "NewPass456!"
}

Response (200 OK):

{
  "success": true
}

Error Responses: - 401 Unauthorized: Invalid current password - 400 Bad Request: Weak new password


POST /api/v1/auth/api-keys

Create a new API key.

Headers: - Authorization: Bearer <access-token>

Request Body:

{
  "name": "production-key",
  "organization_id": "org_...",
  "permissions": ["file:read", "file:write"],
  "expires_at": "2026-01-01T00:00:00Z"
}

Response (201 Created):

{
  "id": "key_...",
  "key": "ocm_live_...",
  "name": "production-key",
  "permissions": ["file:read", "file:write"],
  "created_at": "2025-10-09T12:00:00Z",
  "expires_at": "2026-01-01T00:00:00Z"
}

Note: The key value is only returned once. Store it securely.


GET /api/v1/auth/api-keys

List user's API keys.

Headers: - Authorization: Bearer <access-token>

Response (200 OK):

{
  "keys": [
    {
      "id": "key_...",
      "name": "production-key",
      "permissions": ["file:read", "file:write"],
      "created_at": "2025-10-09T12:00:00Z",
      "last_used_at": "2025-10-09T14:30:00Z",
      "expires_at": "2026-01-01T00:00:00Z"
    }
  ]
}


DELETE /api/v1/auth/api-keys/:id

Revoke an API key.

Headers: - Authorization: Bearer <access-token>

Response (200 OK):

{
  "success": true
}


File Operations

All file endpoints are prefixed with /api/v1/files.

POST /api/v1/files

Upload a new file.

Request (multipart/form-data):

file: [binary file data]
parent_id: [optional parent directory ID]

Example (curl):

curl -X POST http://localhost:8080/api/v1/files \
  -F "file=@document.pdf" \
  -F "parent_id=dir-123"

Response (201 Created):

{
  "id": "file-abc123",
  "name": "document.pdf",
  "path": "/documents/document.pdf",
  "parent_id": "dir-123",
  "type": "file",
  "size": 1048576,
  "mime_type": "application/pdf",
  "hash": "sha256:abc123...",
  "created_at": "2025-10-06T12:00:00Z",
  "updated_at": "2025-10-06T12:00:00Z",
  "accessed_at": "2025-10-06T12:00:00Z",
  "permissions": "",
  "tags": [],
  "is_deleted": false
}

Error Responses: - 400 Bad Request: Missing or invalid file - 413 Payload Too Large: File exceeds size limit - 500 Internal Server Error: Upload failed


GET /api/v1/files/:id

Get file metadata by ID.

Parameters: - id (path): File ID

Example:

curl http://localhost:8080/api/v1/files/file-abc123

Response (200 OK):

{
  "id": "file-abc123",
  "name": "document.pdf",
  "path": "/documents/document.pdf",
  "parent_id": "dir-123",
  "type": "file",
  "size": 1048576,
  "mime_type": "application/pdf",
  "hash": "sha256:abc123...",
  "created_at": "2025-10-06T12:00:00Z",
  "updated_at": "2025-10-06T12:00:00Z",
  "accessed_at": "2025-10-06T12:00:00Z",
  "permissions": "",
  "tags": [],
  "is_deleted": false
}

Error Responses: - 404 Not Found: File does not exist


GET /api/v1/files/:id/download

Download file content.

Parameters: - id (path): File ID

Example:

curl -O http://localhost:8080/api/v1/files/file-abc123/download

Response (200 OK): - Binary file content with appropriate Content-Type header - Content-Disposition: attachment; filename="document.pdf"

Error Responses: - 404 Not Found: File does not exist


GET /api/v1/files/:id/content

Get file content for preview (text files only).

Parameters: - id (path): File ID

Example:

curl http://localhost:8080/api/v1/files/file-abc123/content

Response (200 OK):

{
  "content": "File content as string...",
  "mime_type": "text/plain",
  "encoding": "utf-8"
}

Supported MIME Types: - text/plain - text/markdown - application/json - text/html - text/css - text/javascript - All text/* types

Error Responses: - 404 Not Found: File does not exist - 400 Bad Request: File is not a text file (binary files not supported) - 413 Payload Too Large: File exceeds preview size limit (10MB)

Use Case: This endpoint is designed for file preview in the frontend. It returns text content that can be rendered directly in the UI with syntax highlighting or Markdown rendering.


GET /api/v1/files/:id/stream

Stream file content for media playback (video/audio).

Parameters: - id (path): File ID

Headers: - Range (optional): Byte range for partial content (e.g., bytes=0-1023)

Example:

# Full file
curl http://localhost:8080/api/v1/files/file-abc123/stream

# Partial content (seeking)
curl -H "Range: bytes=1024-2047" http://localhost:8080/api/v1/files/file-abc123/stream

Response (200 OK or 206 Partial Content): - Binary media content with appropriate Content-Type header - Accept-Ranges: bytes header (indicates seeking support) - Content-Range header (for partial responses) - Content-Length header

Supported MIME Types: - Video: video/mp4, video/webm, video/ogg - Audio: audio/mpeg, audio/ogg, audio/wav, audio/mp4, audio/x-m4a

HTTP Range Requests (RFC 7233): This endpoint implements HTTP Range Requests, enabling video/audio seeking: - Client sends Range: bytes=start-end header - Server responds with 206 Partial Content status - Server includes Content-Range: bytes start-end/total header - Enables smooth seeking in media players (react-player, HTML5 video/audio)

Error Responses: - 404 Not Found: File does not exist - 416 Range Not Satisfiable: Invalid byte range - 400 Bad Request: File is not a media file

Use Case: This endpoint is designed for streaming video and audio files with seeking support. Frontend media players (react-player, react-h5-audio-player) automatically use Range requests for seeking, providing a smooth playback experience.


GET /api/v1/files

List files with optional filtering and pagination.

Query Parameters: - parent_id (optional): Filter by parent directory ID - limit (optional, default: 50): Number of results per page - offset (optional, default: 0): Number of results to skip

Example:

curl "http://localhost:8080/api/v1/files?parent_id=dir-123&limit=10&offset=0"

Response (200 OK):

{
  "files": [
    {
      "id": "file-1",
      "name": "file1.txt",
      "path": "/documents/file1.txt",
      "type": "file",
      ...
    },
    {
      "id": "file-2",
      "name": "file2.txt",
      "path": "/documents/file2.txt",
      "type": "file",
      ...
    }
  ],
  "total": 42
}

Error Responses: - 400 Bad Request: Invalid query parameters


DELETE /api/v1/files/:id

Delete a file (soft delete).

Parameters: - id (path): File ID

Example:

curl -X DELETE http://localhost:8080/api/v1/files/file-abc123

Response (200 OK):

{
  "success": true
}

Error Responses: - 404 Not Found: File does not exist


PUT /api/v1/files/:id/move

Move a file to a different parent directory.

Parameters: - id (path): File ID

Request Body:

{
  "new_parent_id": "dir-456"
}

Example:

curl -X PUT http://localhost:8080/api/v1/files/file-abc123/move \
  -H "Content-Type: application/json" \
  -d '{"new_parent_id":"dir-456"}'

Response (200 OK):

{
  "id": "file-abc123",
  "name": "document.pdf",
  "path": "/archive/document.pdf",
  "parent_id": "dir-456",
  ...
}

Error Responses: - 404 Not Found: File or parent directory does not exist - 400 Bad Request: Invalid parent ID


POST /api/v1/files/:id/copy

Copy a file to a new location.

Parameters: - id (path): File ID

Request Body:

{
  "destination_parent_id": "dir-789",
  "new_name": "document-copy.pdf"
}

Example:

curl -X POST http://localhost:8080/api/v1/files/file-abc123/copy \
  -H "Content-Type: application/json" \
  -d '{"destination_parent_id":"dir-789","new_name":"document-copy.pdf"}'

Response (201 Created):

{
  "id": "file-xyz789",
  "name": "document-copy.pdf",
  "path": "/backups/document-copy.pdf",
  "parent_id": "dir-789",
  ...
}

Error Responses: - 404 Not Found: File or destination directory does not exist - 400 Bad Request: Invalid parameters


PATCH /api/v1/files/:id

Update file metadata.

Parameters: - id (path): File ID

Request Body:

{
  "name": "new-name.pdf",
  "parent_id": "dir-new"
}

Example:

curl -X PATCH http://localhost:8080/api/v1/files/file-abc123 \
  -H "Content-Type: application/json" \
  -d '{"name":"renamed-document.pdf"}'

Response (200 OK):

{
  "id": "file-abc123",
  "name": "renamed-document.pdf",
  "path": "/documents/renamed-document.pdf",
  ...
}

Error Responses: - 404 Not Found: File does not exist - 400 Bad Request: Invalid filename or parent ID


Directory Operations

All directory endpoints are prefixed with /api/v1/directories.

POST /api/v1/directories

Create a new directory.

Request Body:

{
  "name": "my-folder",
  "parent_id": "dir-parent"
}

Example:

curl -X POST http://localhost:8080/api/v1/directories \
  -H "Content-Type: application/json" \
  -d '{"name":"documents","parent_id":"root"}'

Response (201 Created):

{
  "file": {
    "id": "dir-123",
    "name": "documents",
    "path": "/documents",
    "parent_id": "root",
    "type": "directory",
    "size": 0,
    "mime_type": "inode/directory",
    "created_at": "2025-10-06T12:00:00Z",
    ...
  },
  "child_count": 0
}

Error Responses: - 400 Bad Request: Missing or invalid name - 404 Not Found: Parent directory does not exist


GET /api/v1/directories/:id

Get directory metadata with child count.

Parameters: - id (path): Directory ID

Example:

curl http://localhost:8080/api/v1/directories/dir-123

Response (200 OK):

{
  "file": {
    "id": "dir-123",
    "name": "documents",
    "path": "/documents",
    "type": "directory",
    ...
  },
  "child_count": 42
}

Error Responses: - 404 Not Found: Directory does not exist


GET /api/v1/directories/:id/children

List immediate children of a directory.

Parameters: - id (path): Directory ID

Query Parameters: - limit (optional, default: 50): Number of results per page - offset (optional, default: 0): Number of results to skip

Example:

curl "http://localhost:8080/api/v1/directories/dir-123/children?limit=10&offset=0"

Response (200 OK):

{
  "files": [
    {
      "id": "file-1",
      "name": "file1.txt",
      "type": "file",
      ...
    },
    {
      "id": "dir-456",
      "name": "subfolder",
      "type": "directory",
      ...
    }
  ],
  "total": 15
}

Error Responses: - 404 Not Found: Directory does not exist - 400 Bad Request: Invalid query parameters


GET /api/v1/directories/:id/tree

Get directory tree with nested children.

Parameters: - id (path): Directory ID

Query Parameters: - depth (optional, default: 0 = unlimited): Maximum depth to traverse

Example:

curl "http://localhost:8080/api/v1/directories/dir-123/tree?depth=2"

Response (200 OK):

{
  "directory": {
    "file": {
      "id": "dir-123",
      "name": "documents",
      ...
    },
    "child_count": 2
  },
  "children": [
    {
      "directory": {
        "file": {
          "id": "dir-456",
          "name": "subfolder",
          ...
        },
        "child_count": 3
      },
      "children": [...]
    }
  ]
}

Error Responses: - 404 Not Found: Directory does not exist - 400 Bad Request: Invalid depth parameter


DELETE /api/v1/directories/:id

Delete a directory.

Parameters: - id (path): Directory ID

Query Parameters: - recursive (optional, default: false): Delete all contents

Example:

curl -X DELETE "http://localhost:8080/api/v1/directories/dir-123?recursive=true"

Response (200 OK):

{
  "success": true
}

Error Responses: - 404 Not Found: Directory does not exist - 400 Bad Request: Directory not empty and recursive=false


Filesystem Operations

GET /api/v1/filesystem/path-suggestions

Get filesystem path suggestions for autocomplete functionality.

Query Parameters: - prefix (required): Path prefix to search for (e.g., /Users/j) - limit (optional, default: 10): Maximum number of suggestions (1-50)

Example:

curl "http://localhost:8080/api/v1/filesystem/path-suggestions?prefix=/Users&limit=5" \
  -H "Authorization: Bearer <access-token>"

Response (200 OK):

{
  "suggestions": [
    "/Users/alice",
    "/Users/bob",
    "/Users/charlie",
    "/Users/dave",
    "/Users/eve"
  ]
}

Features: - Returns only directories (not files) - Filters by prefix matching - Sorted alphabetically - Security: Blocks access to system directories (/etc, /root, /sys, /proc, /dev) - Security: Prevents path traversal attacks (.. patterns rejected) - 5-second timeout per request - Requires org:read permission

Error Responses: - 400 Bad Request: Missing or invalid prefix parameter - 400 Bad Request: Invalid path (contains .. or invalid characters) - 400 Bad Request: Invalid limit (must be 1-50) - 403 Forbidden: Access to forbidden path (system directories) - 403 Forbidden: Missing required permission (org:read) - 404 Not Found: Prefix path does not exist - 500 Internal Server Error: Failed to read directory or operation timeout

Use Case: This endpoint is designed for path autocomplete in UI components (e.g., watched folder path input). It provides fast, secure directory suggestions as users type.


Search Operations

GET /api/v1/search

Search for files with filters and pagination.

Query Parameters: - q (optional): Search query (searches name and path) - path (optional): Filter by path prefix - type (optional): Filter by file type (file, directory, symlink) - tags (optional): Comma-separated list of tag IDs - min_size (optional): Minimum file size in bytes - max_size (optional): Maximum file size in bytes - start_date (optional): Filter files created after this date (ISO 8601) - end_date (optional): Filter files created before this date (ISO 8601) - limit (optional, default: 50): Number of results per page - offset (optional, default: 0): Number of results to skip - sort_by (optional, default: name): Sort field (name, size, created_at, updated_at) - sort_order (optional, default: asc): Sort order (asc, desc)

Example:

curl "http://localhost:8080/api/v1/search?q=document&type=file&min_size=1024&sort_by=size&sort_order=desc&limit=20"

Response (200 OK):

{
  "files": [
    {
      "id": "file-1",
      "name": "large-document.pdf",
      "size": 5242880,
      ...
    },
    {
      "id": "file-2",
      "name": "medium-document.pdf",
      "size": 2097152,
      ...
    }
  ],
  "total": 42,
  "page": 1,
  "page_size": 20,
  "total_pages": 3
}

Error Responses: - 400 Bad Request: Invalid query parameters


gRPC API

The gRPC API uses ConnectRPC for HTTP/2-based RPC with JSON support. All services are available at /ocmonica.v1.*.

Protocol

  • Transport: HTTP/2 (with h2c for development)
  • Encoding: Protocol Buffers (binary) or JSON
  • Base URL: Same as REST API

FileService

Service path: /ocmonica.v1.FileService/

UploadFile (Client Streaming)

Upload a file using streaming for large files.

Request (stream):

message UploadFileRequest {
  oneof data {
    FileMetadata metadata = 1;  // First message
    bytes chunk = 2;            // Subsequent messages
  }
}

Response:

message UploadFileResponse {
  File file = 1;
}

Example (ConnectRPC Web):

const client = createPromiseClient(FileService, transport);

const stream = client.uploadFile();
await stream.send({ metadata: { name: "file.pdf" } });
await stream.send({ chunk: buffer1 });
await stream.send({ chunk: buffer2 });
const response = await stream.complete();


DownloadFile (Server Streaming)

Download a file using streaming.

Request:

message DownloadFileRequest {
  string id = 1;
}

Response (stream):

message DownloadFileResponse {
  oneof data {
    File metadata = 1;    // First message
    bytes chunk = 2;      // Subsequent messages
  }
}


GetFile (Unary)

Get file metadata.

Request:

message GetFileRequest {
  string id = 1;
}

Response:

message GetFileResponse {
  File file = 1;
}


ListFiles (Unary)

List files with pagination.

Request:

message ListFilesRequest {
  optional string parent_id = 1;
  int32 limit = 2;
  int32 offset = 3;
}

Response:

message ListFilesResponse {
  repeated File files = 1;
  int64 total = 2;
}


DeleteFile (Unary)

Delete a file.

Request:

message DeleteFileRequest {
  string id = 1;
}

Response:

message DeleteFileResponse {
  bool success = 1;
}


MoveFile (Unary)

Move a file to a different parent.

Request:

message MoveFileRequest {
  string id = 1;
  string new_parent_id = 2;
}

Response:

message MoveFileResponse {
  File file = 1;
}


CopyFile (Unary)

Copy a file.

Request:

message CopyFileRequest {
  string id = 1;
  string destination_parent_id = 2;
  optional string new_name = 3;
}

Response:

message CopyFileResponse {
  File file = 1;
}


UpdateFile (Unary)

Update file metadata.

Request:

message UpdateFileRequest {
  string id = 1;
  optional string name = 2;
  optional string parent_id = 3;
}

Response:

message UpdateFileResponse {
  File file = 1;
}


DirectoryService

Service path: /ocmonica.v1.DirectoryService/

CreateDirectory (Unary)

Create a new directory.

Request:

message CreateDirectoryRequest {
  string name = 1;
  optional string parent_id = 2;
}

Response:

message CreateDirectoryResponse {
  Directory directory = 1;
}


GetDirectory (Unary)

Get directory metadata with child count.

Request:

message GetDirectoryRequest {
  string id = 1;
}

Response:

message GetDirectoryResponse {
  Directory directory = 1;
}


ListChildren (Unary)

List immediate children of a directory.

Request:

message ListChildrenRequest {
  string id = 1;
  int32 limit = 2;
  int32 offset = 3;
}

Response:

message ListChildrenResponse {
  repeated File files = 1;
  int64 total = 2;
}


GetTree (Unary)

Get directory tree.

Request:

message GetTreeRequest {
  string id = 1;
  int32 depth = 2;  // 0 = unlimited
}

Response:

message GetTreeResponse {
  DirectoryNode root = 1;
}


DeleteDirectory (Unary)

Delete a directory.

Request:

message DeleteDirectoryRequest {
  string id = 1;
  bool recursive = 2;
}

Response:

message DeleteDirectoryResponse {
  bool success = 1;
}


SearchService

Service path: /ocmonica.v1.SearchService/

Search (Unary)

Search for files with filters.

Request:

message SearchRequest {
  SearchQuery query = 1;
}

Response:

message SearchResponse {
  SearchResult result = 1;
}


SearchStream (Server Streaming)

Streaming search for large result sets.

Request:

message SearchStreamRequest {
  SearchQuery query = 1;
}

Response (stream):

message SearchStreamResponse {
  File file = 1;  // One file per message
}


PlaybackPositionService

Service path: /ocmonica.v1.PlaybackPositionService/

GetPlaybackPosition (Unary)

Get the saved playback position for a media file.

Request:

message GetPlaybackPositionRequest {
  string file_id = 1;
}

Response:

message GetPlaybackPositionResponse {
  string file_id = 1;
  double position_seconds = 2;      // Playback position in seconds
  double duration_seconds = 3;       // Total duration in seconds
  google.protobuf.Timestamp last_updated = 4;
}

Example (ConnectRPC Web):

import { createPromiseClient } from "@connectrpc/connect";
import { PlaybackPositionService } from "@/gen/ocmonica/v1/playback_position_service_connect";

const client = createPromiseClient(PlaybackPositionService, transport);

// Get playback position
const response = await client.getPlaybackPosition({
  fileId: "file-abc123",
});

console.log(`Resume from: ${response.positionSeconds}s / ${response.durationSeconds}s`);

Error Codes: - NOT_FOUND (5): No playback position saved for this file - INVALID_ARGUMENT (3): Missing or invalid file_id

Use Case: Retrieve the last saved playback position for a video or audio file, enabling "resume from where you left off" functionality for audiobooks and videos.


SavePlaybackPosition (Unary)

Save the current playback position for a media file.

Request:

message SavePlaybackPositionRequest {
  string file_id = 1;
  double position_seconds = 2;      // Current position in seconds
  double duration_seconds = 3;       // Total duration in seconds
}

Response:

message SavePlaybackPositionResponse {
  bool success = 1;
}

Example (ConnectRPC Web):

// Save position every 5 seconds while playing
setInterval(() => {
  const currentPosition = player.getCurrentTime();
  const duration = player.getDuration();

  await client.savePlaybackPosition({
    fileId: "file-abc123",
    positionSeconds: currentPosition,
    durationSeconds: duration,
  });
}, 5000);

Error Codes: - INVALID_ARGUMENT (3): Missing required fields or invalid values - INTERNAL (13): Failed to save position

Use Case: Automatically save playback progress as users watch videos or listen to audio files. Frontend media players use this endpoint to persist position every few seconds, enabling seamless resume across devices and sessions.


FilesystemService

Service path: /ocmonica.v1.FilesystemService/

GetPathSuggestions (Unary)

Get host filesystem directory paths for autocomplete functionality when selecting watched folder paths.

Request:

message GetPathSuggestionsRequest {
  string prefix = 1;        // Path prefix (e.g., "/Users/j")
  int32 limit = 2;          // Max suggestions (1-50, default 10)
}

Response:

message GetPathSuggestionsResponse {
  repeated string suggestions = 1;  // Array of absolute directory paths
}

Example (ConnectRPC Web):

import { createPromiseClient } from "@connectrpc/connect";
import { FilesystemService } from "@/gen/ocmonica/v1/filesystem_service_connect";
import { createConnectTransport } from "@connectrpc/connect-web";

const transport = createConnectTransport({
  baseUrl: "http://localhost:8080",
});

const client = createPromiseClient(FilesystemService, transport);

// Get path suggestions
const response = await client.getPathSuggestions({
  prefix: "/Users",
  limit: 5,
});

console.log("Directory suggestions:", response.suggestions);
// Output: ["/Users/alice", "/Users/bob", "/Users/charlie", ...]

Features: - Returns only directories (not files) - Filters by prefix matching - Sorted alphabetically - Security: Blocks access to system directories (/etc, /root, /sys, /proc, /dev) - Security: Prevents path traversal attacks (.. patterns rejected) - 5-second timeout per request - Requires org:read permission

Error Codes: - INVALID_ARGUMENT (3): Missing prefix, invalid path (contains ..), or invalid limit - PERMISSION_DENIED (7): Access to forbidden path or missing org:read permission - NOT_FOUND (5): Prefix path does not exist - INTERNAL (13): Failed to read directory or operation timeout

Use Case: This endpoint is designed for path autocomplete in UI components when users configure watched folder paths. It provides fast, secure directory suggestions as users type, preventing typos and ensuring valid paths are selected.


Data Models

File

{
  id: string;                    // Unique identifier
  name: string;                  // File name
  path: string;                  // Full path
  parent_id?: string;            // Parent directory ID (null for root)
  type: "file" | "directory" | "symlink";
  size: number;                  // Size in bytes
  mime_type: string;             // MIME type
  hash: string;                  // SHA-256 hash
  created_at: string;            // ISO 8601 timestamp
  updated_at: string;            // ISO 8601 timestamp
  accessed_at: string;           // ISO 8601 timestamp
  permissions: string;           // Future use
  tags: Tag[];                   // Associated tags
  is_deleted: boolean;           // Soft delete flag
  deleted_at?: string;           // Deletion timestamp
}

Directory

{
  file: File;                    // Directory file object
  child_count: number;           // Number of immediate children
}

DirectoryNode

{
  directory: Directory;          // Directory metadata
  children: DirectoryNode[];     // Nested children
}

Tag

{
  id: string;                    // Unique identifier
  name: string;                  // Tag name
  color: string;                 // Color code (hex)
  created_at: string;            // ISO 8601 timestamp
}

SearchQuery

{
  query?: string;                // Search text
  path?: string;                 // Path filter
  file_type?: "file" | "directory" | "symlink";
  tags?: string[];               // Tag IDs
  min_size?: number;             // Minimum size (bytes)
  max_size?: number;             // Maximum size (bytes)
  start_date?: string;           // ISO 8601 date
  end_date?: string;             // ISO 8601 date
  limit: number;                 // Results per page
  offset: number;                // Results to skip
  sort_by: string;               // Sort field
  sort_order: "asc" | "desc";    // Sort direction
}

SearchResult

{
  files: File[];                 // Matched files
  total: number;                 // Total matches
  page: number;                  // Current page
  page_size: number;             // Results per page
  total_pages: number;           // Total pages
}

Error Handling

REST API Errors

All errors follow a consistent JSON format:

{
  "error": {
    "code": "ERR_FILE_NOT_FOUND",
    "message": "File not found",
    "details": {
      "id": "file-abc123"
    }
  }
}

HTTP Status Codes

  • 200 OK: Success
  • 201 Created: Resource created
  • 400 Bad Request: Invalid input
  • 404 Not Found: Resource not found
  • 413 Payload Too Large: File too large
  • 500 Internal Server Error: Server error
  • 503 Service Unavailable: Server unhealthy

gRPC Errors

gRPC errors use standard gRPC status codes:

  • OK (0): Success
  • INVALID_ARGUMENT (3): Bad input
  • NOT_FOUND (5): Resource not found
  • ALREADY_EXISTS (6): Resource exists
  • RESOURCE_EXHAUSTED (8): Rate limit or quota
  • INTERNAL (13): Server error
  • UNAVAILABLE (14): Service unavailable

Example gRPC Error:

{
  "code": "not_found",
  "message": "File not found: file-abc123"
}


Examples

Upload and Download a File (REST)

# Upload
curl -X POST http://localhost:8080/api/v1/files \
  -F "file=@myfile.pdf" \
  -o upload-response.json

# Extract ID
FILE_ID=$(cat upload-response.json | jq -r '.id')

# Download
curl -O http://localhost:8080/api/v1/files/$FILE_ID/download

Create Directory Structure (REST)

# Create root directory
curl -X POST http://localhost:8080/api/v1/directories \
  -H "Content-Type: application/json" \
  -d '{"name":"documents"}' \
  | jq -r '.file.id' > root_id.txt

# Create subdirectory
curl -X POST http://localhost:8080/api/v1/directories \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"work\",\"parent_id\":\"$(cat root_id.txt)\"}"

Search and Filter (REST)

# Search for large PDF files
curl "http://localhost:8080/api/v1/search?q=report&type=file&min_size=1048576&sort_by=size&sort_order=desc" \
  | jq '.files[] | {name, size, created_at}'

Using gRPC with ConnectRPC (TypeScript)

import { createPromiseClient } from "@connectrpc/connect";
import { FileService } from "./gen/ocmonica/v1/file_service_connect";
import { createConnectTransport } from "@connectrpc/connect-web";

// Create transport
const transport = createConnectTransport({
  baseUrl: "http://localhost:8080",
});

// Create client
const client = createPromiseClient(FileService, transport);

// List files
const response = await client.listFiles({
  parentId: "dir-123",
  limit: 10,
  offset: 0,
});

console.log(`Found ${response.total} files`);
response.files.forEach(file => {
  console.log(`- ${file.name} (${file.size} bytes)`);
});

Error Handling (TypeScript)

import { ConnectError } from "@connectrpc/connect";

try {
  const response = await client.getFile({ id: "nonexistent" });
} catch (err) {
  if (err instanceof ConnectError) {
    if (err.code === "not_found") {
      console.log("File not found");
    } else {
      console.error("gRPC error:", err.message);
    }
  }
}

Observability Endpoints

GET /metrics

Prometheus metrics endpoint (no authentication required).

Response (text/plain):

# HELP ocmonica_file_uploads_total Total number of file uploads
# TYPE ocmonica_file_uploads_total counter
ocmonica_file_uploads_total{mime_type="application/pdf",status="success"} 42

# HELP ocmonica_auth_attempts_total Total number of authentication attempts
# TYPE ocmonica_auth_attempts_total counter
ocmonica_auth_attempts_total{method="password",status="success"} 156

# HELP ocmonica_db_query_duration_seconds Duration of database queries in seconds
# TYPE ocmonica_db_query_duration_seconds histogram
ocmonica_db_query_duration_seconds_bucket{operation="select",le="0.001"} 1024
...

Available Metrics: - ocmonica_file_uploads_total - File upload counter (by status, mime_type) - ocmonica_file_downloads_total - File download counter - ocmonica_file_deletions_total - File deletion counter - ocmonica_auth_attempts_total - Authentication attempts (by method, status) - ocmonica_api_key_usage_total - API key usage counter - ocmonica_db_query_duration_seconds - Database query latency histogram - ocmonica_search_queries_total - Search query counter - ocmonica_active_users - Active users gauge


Rate Limiting

Current Status: ✅ Implemented

Rate limits are enforced per IP address and per authenticated user:

Limits: - Per IP: 100 requests/minute - Per User (authenticated): 1000 requests/minute - File Uploads: No specific limit (controlled by general rate limit)

Rate Limit Headers: All responses include rate limit headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1696876800

Error Response (429 Too Many Requests):

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Try again in 60 seconds."
  }
}


Versioning

The API uses URL-based versioning (/api/v1). Breaking changes will increment the version number (/api/v2).

Backward Compatibility

Within a major version: - New fields may be added to responses - New optional parameters may be added - New endpoints may be added - Existing fields will not be removed or changed

Deprecation Policy

  • Deprecated features will be announced 6 months in advance
  • Deprecated endpoints will return a Deprecation header
  • Documentation will clearly mark deprecated features

Support

For API questions or issues: - GitHub Issues: https://github.com/j-pye/ocmonica/issues - Documentation: See DEVELOPMENT.md - Testing Guide: See TESTING.md

Resources