aka. Dyson Network File System
Go implementation of the Dyson Network file service.
master: HTTP API, gRPC, uploads, file serving, health checkworker: post-upload media processing, derived file generation, cleanupstorage: optional local storage node for filesystem-backed deployments
Use the first positional argument as the command.
go run ./cmd master
go run ./cmd migrate-legacy --config ./config.toml --legacy-dsn "$LEGACY_DATABASE_DSN"
go run ./cmd reanalyze-missing --config ./config.toml
go run ./cmd validate-storage --config ./config.tomlZEROLOG_PRETTY=trueenables console-style pretty logsLOG_LEVEL=debug|info|warn|errorsets the log level
go run ./cmd masterUse the one-shot migrator to import data from the old C# database into the new schema:
go run ./cmd migrate-legacy --config ./config.toml --legacy-dsn "$LEGACY_DATABASE_DSN"Flags:
--dry-runto simulate without writing--skip-derivedto skip thumbnail/compression child reconstruction--batch-sizeto tune import batch size--continue-on-errorto keep going after row-level failures
Repair missing image/video metadata from stored source files:
go run ./cmd reanalyze-missing --config ./config.tomlIt shows a preview first, then asks for confirmation before changing anything.
Flags:
--reanalyze-limitto cap the preview/repair batch size--file-idto target one or more file IDs, comma-separated--preview-countto control how many candidates are shown first--yesto skip the confirmation prompt
Both direct upload and chunked upload creation accept the same metadata payload:
{
"hash": "...",
"file_name": "clip.mov",
"file_size": 12345,
"content_type": "video/quicktime",
"pool_id": "...",
"expired_at": "2026-05-17T12:34:56Z",
"chunk_size": 5242880,
"parent_id": "...",
"usage": "...",
"application_type": "..."
}directupload uses multipart form data with the same field names, plusfileparent_idis optional and can still be resolved server-side when omittedhashis stored on the created file/task when provided- upload quota is checked before task creation or direct upload processing
- quota refusal returns
403 Forbidden - if
pool_idis omitted, the quota check uses the resolved default pool
Quota values are reported in MB.
Base quota is the sum of:
- leveling quota
- perk quota
Leveling quota uses account.profile.level, clamped to 0..120, with these milestones:
Lv0:512MBLv10:1GBLv60:5GBLv120:10GB
Between those milestones, the quota is interpolated piecewise.
Perk quota is added on top of leveling quota:
- perk
1:10GB - perk
2:25GB - perk
3:50GB
Extra quota comes from active quota_records and is added after the base quota.
GET /api/billing/quota returns:
{
"based_quota": 15360,
"extra_quota": 25,
"total_quota": 15385
}GET /api/billing/usage returns:
{
"used_quota": 300,
"total_quota": 15385,
"total_file_count": 2,
"total_usage_bytes": 209715200
}Usage accounting rules:
used_quotais billable usage in MB, not raw bytes- raw file bytes are returned separately as
total_usage_bytes - pool billing
cost_multiplieraffects billable usage and quota checks - the multiplier is applied per file based on the file's pool
Folders are created with POST /folders.
Request body:
{
"name": "Projects",
"parent_id": "..."
}- A folder is stored as a
cloud_filesrow withis_folder = trueandindexed = true parent_idis optional- The current implementation does not yet validate that the parent exists or is a folder
- Root folders automatically get a private read permission record
Files expose read/write/manage permissions through GET /files/:id/permissions and PUT /files/:id/permissions.
- No file permission rows means the file is public
- A
privatepermission row withreadmakes a file private by default - Permission checks inherit from ancestor folders
PUT /files/:id/permissionsreplaces the full permission set in one batch
Request body:
{
"items": [
{
"id": "...",
"file_id": "...",
"subject_type": "account",
"subject_id": "...",
"permission": "read"
},
{
"id": "...",
"file_id": "...",
"subject_type": "scope",
"subject_id": "files.manage",
"permission": "manage"
}
]
}subject_typecan bepublic,private,account, orscopepermissionis typicallyread,write, ormanage- Send the full desired list; omitted rows are removed
List responses include extra metadata for navigation and access UI:
children_countfor immediate child countpermission_statusfor current access state
Performance notes:
- Child counts and inherited permission status are resolved in batches for list responses to avoid per-file query fan-out.
- Postgres should have a composite index on
cloud_files(parent_id, deleted_at)sochildren_countlookups do not fall back to full table scans. - Postgres should have a composite index on
file_permissions(file_id, permission, deleted_at)so inherited permission-source lookups stay index-backed. - These indexes are declared in the GORM models and are created by
AutoMigrate, but existing deployments need a restart or migration run before the new indexes appear. - If file-list or permission logs still show slow SQL after rollout, verify the indexes exist with
pg_indexesand inspect the hot queries withEXPLAIN (ANALYZE, BUFFERS).
Example:
"permission_status": {
"readable": true,
"writable": false,
"manageable": false,
"visibility": "private",
"inherited_from": "..."
}Validate file_objects.storage_key against remote S3 objects and clean up orphans:
go run ./cmd validate-storage --config ./config.toml --yesIt snapshots remote keys first, then compares the snapshot against the database in batches.
Flags:
--validate-snapshotto choose the snapshot file path--validate-prefixto limit the remote listing prefix--validate-batchto control DB batch size--yesto skip the confirmation prompt
Use --config or CONFIG_PATH for a TOML config file.
Key settings:
app.namedatabase.dsnhttp.portgrpc.portgrpc.useTLSgrpc.certFilegrpc.keyFilestorage.tempDirstorage.localDirauth.targetauth.useTLSpassport.targetpassport.useTLSquota.leveling.level1quota.leveling.level10quota.leveling.level60quota.leveling.level120nats.urlmode.mastermode.workermode.storage
Pool storage is configured with [[pools]] and seeded into the database at startup.
Example:
[[pools]]
id = "500e5ed8-bd44-4359-bc0a-ec85e2adf447"
name = "Default"
default = true
hidden = false
[pools.storage]
endpoint = "http://minio:9000"
bucket = "dyson-files"
enableSigned = true
enableSsl = false
secretId = "minio"
secretKey = "minio123"- Public read is the default.
- Explicit ACL rows restrict access when present.
masterresolves storage from pool config stored in the database.workerlistens for file upload events and builds thumbnails, blurhash, and other derived artifacts.mastercan use S3 directly; local storage is still supported.- The Docker image expects
ffmpegandlibvipsruntime packages. - The E2EE file route has been removed.