CAN Service: content-addressable storage with HTTP API, SQLite metadata, file-based blob storage, thumbnail generation, and integrity verification. can-sync v1: P2P sync sidecar using iroh-docs for encrypted peer-to-peer replication with library/filter-based selective sync. Fully builds but being superseded by v2 (simplified full-mirror approach). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 KiB
CAN Service API Reference
Base URL: http://localhost:3210
API Prefix: /api/v1/can/0
Content-Type: application/json (all responses are JSON)
The
0in the path is thecan_id. MVP supports only container0.
Quick Start
# Start the server
cargo run
# Store a file
curl -X POST http://localhost:3210/api/v1/can/0/ingest \
-F "file=@photo.jpg" \
-F "tags=vacation,beach" \
-F "description=Summer trip"
# Store data (no file needed)
curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \
-H "Content-Type: application/json" \
-d '{"data": {"sensor": "temp", "value": 22.5}, "tags": "iot,sensor"}'
# List everything
curl http://localhost:3210/api/v1/can/0/list
# Search by tag
curl "http://localhost:3210/api/v1/can/0/search?tags=vacation"
Response Envelope
All responses use a standard wrapper:
// Success
{
"status": "success",
"data": { ... }
}
// Error
{
"status": "error",
"error": "Human-readable error message"
}
HTTP Status Codes:
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad request (missing/invalid parameters) |
| 404 | Asset not found |
| 500 | Internal error / corrupted asset |
Endpoints
1. Ingest File (Multipart)
Upload a binary file with optional metadata.
POST /api/v1/can/0/ingest
Content-Type: multipart/form-data
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
file |
Binary | Yes | The file to store |
mime_type |
String | No | Override MIME type (auto-detected from filename if omitted) |
human_file_name |
String | No | Logical filename (e.g. report.pdf) |
human_readable_path |
String | No | Logical folder path (e.g. /docs/reports/) |
application |
String | No | Application that produced this file |
user |
String | No | User or agent identity |
tags |
String | No | Comma-separated tags (e.g. finance,Q4,report) |
description |
String | No | Human-readable description |
Response:
{
"status": "success",
"data": {
"timestamp": 1773014400123,
"hash": "a3b2c4d5e6f7...",
"filename": "1773014400123_a3b2c4d5e6f7_finance_Q4.pdf"
}
}
Example:
curl -X POST http://localhost:3210/api/v1/can/0/ingest \
-F "file=@quarterly_report.pdf" \
-F "application=WebUI" \
-F "user=jason" \
-F "tags=finance,Q4,report" \
-F "description=Q4 2025 financial report" \
-F "human_file_name=quarterly_report.pdf" \
-F "human_readable_path=/finance/reports/"
2. Ingest Data (JSON)
Store any JSON value directly -- no multipart needed. Designed for agents and programmatic use.
POST /api/v1/can/0/ingest/data
Content-Type: application/json
JSON Body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
data |
Any JSON | Yes | -- | The payload to store. Object, array, string, number, boolean, or null. |
mime_type |
String | No | application/json |
Override MIME type (e.g. text/plain to store as .txt) |
human_file_name |
String | No | Logical filename | |
human_readable_path |
String | No | Logical folder path | |
application |
String | No | Application/agent that produced this data | |
user |
String | No | User or agent identity | |
tags |
String | No | Comma-separated tags | |
description |
String | No | Human-readable description |
The data field is serialized to pretty-printed JSON and stored as a .json file (or .txt etc. if you override mime_type).
Response: Same as file ingest.
Examples:
# Minimal -- just dump an object
curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \
-H "Content-Type: application/json" \
-d '{"data": {"key": "value"}}'
# Agent saving structured output
curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \
-H "Content-Type: application/json" \
-d '{
"data": {
"agent_id": "planner-v2",
"session": "abc-123",
"steps": ["research", "outline", "draft"]
},
"application": "AgentOrchestrator",
"user": "planner_agent",
"tags": "agent,plan,session",
"description": "Planning output for session abc-123",
"human_file_name": "plan_output.json",
"human_readable_path": "/agents/planner/"
}'
# Store a plain string
curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \
-H "Content-Type: application/json" \
-d '{"data": "Log: task completed at 14:30", "tags": "log"}'
# Store an array
curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \
-H "Content-Type: application/json" \
-d '{"data": [1, 2, 3, "four"], "tags": "test"}'
# Store as plain text instead of JSON
curl -X POST http://localhost:3210/api/v1/can/0/ingest/data \
-H "Content-Type: application/json" \
-d '{"data": "plain text content", "mime_type": "text/plain"}'
3. Retrieve Asset
Download the physical file by its hash.
GET /api/v1/can/0/asset/{hash}
Path Parameters:
| Param | Type | Description |
|---|---|---|
hash |
String | The SHA-256 hash returned from ingest |
Response: Raw file bytes with headers:
Content-Typeset to the asset's MIME typeContent-Disposition: attachment; filename="<human_filename>"
Returns 500 if the asset is flagged as corrupted.
Example:
curl -o output.pdf http://localhost:3210/api/v1/can/0/asset/a3b2c4d5e6f7...
4. Get Asset Metadata
Retrieve all metadata for an asset without downloading the file.
GET /api/v1/can/0/asset/{hash}/meta
Response:
{
"status": "success",
"data": {
"hash": "a3b2c4d5e6f7...",
"mime_type": "application/pdf",
"application": "WebUI",
"user": "jason",
"tags": ["finance", "Q4", "report"],
"description": "Q4 2025 financial report",
"human_filename": "quarterly_report.pdf",
"human_path": "/finance/reports/",
"timestamp": 1773014400123,
"is_trashed": false,
"is_corrupted": false
}
}
5. Update Metadata
Modify tags and/or description for an existing asset. The physical file is unchanged.
PATCH /api/v1/can/0/asset/{hash}
Content-Type: application/json
JSON Body:
| Field | Type | Required | Description |
|---|---|---|---|
tags |
String[] | No | New tag list (replaces all existing tags) |
description |
String | No | New description |
Both fields are optional. Only provided fields are updated.
Example:
curl -X PATCH http://localhost:3210/api/v1/can/0/asset/a3b2c4d5e6f7... \
-H "Content-Type: application/json" \
-d '{
"tags": ["finance", "Q4", "report", "reviewed"],
"description": "Q4 2025 report - reviewed by CFO"
}'
Response:
{
"status": "success",
"data": "updated"
}
6. List Assets
Paginated listing of all assets with optional filtering.
GET /api/v1/can/0/list
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
limit |
Integer | 50 |
Page size |
offset |
Integer | 0 |
Starting position |
offset_time |
Integer | -- | Epoch ms cursor. Lists items strictly before/after this timestamp (based on order). Faster than offset for large datasets. |
order |
String | desc |
Sort direction: asc or desc (by timestamp) |
application |
String | -- | Filter to assets from this application |
include_trashed |
Boolean | false |
Include soft-deleted assets |
include_corrupted |
Boolean | false |
Include corrupted assets |
Response:
{
"status": "success",
"data": {
"items": [
{
"hash": "a3b2...",
"mime_type": "application/pdf",
"application": "WebUI",
"user": "jason",
"tags": ["finance"],
"description": "...",
"human_filename": "report.pdf",
"human_path": "/docs/",
"timestamp": 1773014400123,
"is_trashed": false,
"is_corrupted": false
}
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 142
}
}
}
Examples:
# First page, 10 items
curl "http://localhost:3210/api/v1/can/0/list?limit=10"
# Second page
curl "http://localhost:3210/api/v1/can/0/list?limit=10&offset=10"
# Only assets from a specific app, oldest first
curl "http://localhost:3210/api/v1/can/0/list?application=IoTAgent&order=asc"
# Include trashed assets
curl "http://localhost:3210/api/v1/can/0/list?include_trashed=true"
7. Search Assets
Search with multiple filters. All filters are AND-combined.
GET /api/v1/can/0/search
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
hash |
String | -- | Exact hash or prefix match (e.g. a3b2 matches a3b2c4d5...) |
start_time |
Integer | -- | Epoch ms lower bound (inclusive) |
end_time |
Integer | -- | Epoch ms upper bound (inclusive) |
tags |
String | -- | Comma-separated. AND logic: asset must have all specified tags. |
mime_type |
String | -- | Exact MIME type match (e.g. image/jpeg) |
user |
String | -- | Exact user/agent identity match |
application |
String | -- | Exact application match |
limit |
Integer | 50 |
Page size |
offset |
Integer | 0 |
Starting position |
order |
String | desc |
Sort direction: asc or desc |
include_trashed |
Boolean | false |
Include soft-deleted assets |
include_corrupted |
Boolean | false |
Include corrupted assets |
Response: Same structure as List (items + pagination).
Examples:
# Find by hash prefix
curl "http://localhost:3210/api/v1/can/0/search?hash=a3b2"
# All JPEG images from last 24 hours
curl "http://localhost:3210/api/v1/can/0/search?mime_type=image/jpeg&start_time=1773014400000"
# Assets tagged with BOTH "sensor" AND "temperature"
curl "http://localhost:3210/api/v1/can/0/search?tags=sensor,temperature"
# Everything from a specific agent
curl "http://localhost:3210/api/v1/can/0/search?application=PlannerAgent&user=agent_v2"
# Combine filters
curl "http://localhost:3210/api/v1/can/0/search?tags=report&application=WebUI&order=asc&limit=5"
8. Get Thumbnail
Generate a resized thumbnail for image assets. Non-image assets return a placeholder SVG.
GET /api/v1/can/0/asset/{hash}/thumb/{max_width}/{max_height}
Path Parameters:
| Param | Type | Description |
|---|---|---|
hash |
String | Asset hash |
max_width |
Integer | Maximum width in pixels |
max_height |
Integer | Maximum height in pixels |
Aspect ratio is always preserved. The image fits within the max_width x max_height box.
Response:
- Image assets: JPEG bytes (
Content-Type: image/jpeg). Cached in.thumbs/if enabled. - Non-image assets: SVG placeholder icon (
Content-Type: image/svg+xml).
Example:
# 200x200 thumbnail
curl -o thumb.jpg http://localhost:3210/api/v1/can/0/asset/a3b2.../thumb/200/200
Configuration
config.yaml at the project root (or pass a custom path as the first CLI argument):
storage_root: "/var/lib/can_data" # Where files are stored
admin_token: "super_secret_rebuild" # Bearer token for admin operations
enable_thumbnail_cache: true # Cache thumbnails in .thumbs/
rebuild_error_threshold: 50 # Error tolerance before hard rebuild
verify_interval_hours: 12 # Hours between integrity scrubs
Concepts
Hash
Every asset gets a unique SHA-256 hash computed as SHA256(timestamp_be_bytes + file_content). This hash is the primary identifier used in all API calls. Because the timestamp is mixed in, even identical file content produces different hashes at different times.
Physical Filename
Files are stored as: {timestamp}_{hash}_{tags}.{extension}
For example: 1773014400123_a3b2c4d5e6f7_finance_Q4.pdf
This naming allows offline integrity verification -- you can recompute the hash from the timestamp and file contents and compare it to the filename.
Tags
Tags are comma-separated strings. On ingest and in search, pass them as a single string: "finance,Q4,report". In metadata responses, they come back as an array: ["finance", "Q4", "report"].
When searching, tag matching uses AND logic: ?tags=finance,Q4 finds only assets that have both tags.
When patching, tags are replaced entirely (not merged).
Integrity Verification
A background verifier runs automatically:
- On startup: Scrubs all assets against their hashes.
- On file change: Watches the storage directory for modifications.
- Periodically: Every
verify_interval_hours.
Corrupted assets are flagged (is_corrupted: true) and excluded from standard list/search results unless include_corrupted=true is passed. Retrieving a corrupted asset via GET returns a 500 error.
OS File Attributes
Critical metadata is written to the host OS as file attributes for disaster recovery:
- Linux/macOS: Extended attributes (
xattr) under theuser.can.*namespace - Windows: NTFS Alternate Data Streams (
:can.*)
This means the SQLite database can be rebuilt from scratch by scanning the storage directory.