Skip to content

File Sync for Replicas

When using local storage (not S3), replicas need a way to sync uploaded files from the master server. VSKI provides automatic file synchronization as part of the replication system.

┌─────────────────────────────────────────────────────────────┐
│ Master │
│ ┌─────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ Database │ │ _file_journal │ │ Storage │ │
│ │ default.db │◄───│ id | op | path │◄───│ /files │ │
│ └──────┬──────┘ └────────┬────────┘ └──────┬──────┘ │
└─────────┼────────────────────┼────────────────────┼─────────┘
│ │ │
│ DB Sync │ Journal Sync │ File Download
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Replica │
│ ┌─────────────┐ ┌───────────┐ │
│ │ Database │ │ Storage │ │
│ │ default.db │ │ /files │ │
│ └─────────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────┘
ComponentDescription
_file_journalTable tracking file operations (add/delete)
File Journal SyncReplica fetches journal entries after DB sync
File DownloadReplica downloads new files from master
File DeletionReplica removes local files marked as deleted
AcknowledgmentReplica confirms sync, master cleans up journal

When a file is uploaded or deleted on the master:

-- On file upload
INSERT INTO _file_journal (operation, path, created)
VALUES ('add', 'collection_id/record_id/document.pdf', datetime('now'));
-- On file deletion
INSERT INTO _file_journal (operation, path, created)
VALUES ('delete', 'collection_id/record_id/document.pdf', datetime('now'));

The journal is stored in default.db and replicates automatically with the database.

Every sync cycle, the replica:

  1. Syncs database - Gets the latest _file_journal entries
  2. Fetches journal - Requests entries since last sync
  3. Downloads files - For each add operation, downloads file from master
  4. Deletes files - For each delete operation, removes local file
  5. Acknowledges - Tells master which entries were processed

The master uses a dual cleanup strategy:

StrategyTriggerPurpose
Ack-basedReplica confirms syncImmediate cleanup after sync
Time-basedEntries older than N daysSafety net for failed syncs

File journal is created automatically when:

  • Not in replica mode
  • Not using S3 storage
Terminal window
# Master configuration
JWT_SECRET=your-shared-secret
STORAGE_PATH=./data/files # Local storage path
FILE_SYNC_RETENTION_DAYS=7 # Time-based cleanup (default: 7)

File sync is enabled automatically when:

  • Running in replica mode
  • Not using S3 storage
Terminal window
# Replica configuration
REPLICA_MODE=replica
MASTER_URL=http://master:3001
JWT_SECRET=your-shared-secret # Must match master
STORAGE_PATH=./data/files # Local storage path
SYNC_INTERVAL=60 # Sync every 60 seconds
VariableDefaultDescription
FILE_SYNC_RETENTION_DAYS7Days to retain journal entries
STORAGE_PATH./data/filesPath for file storage
SYNC_INTERVAL60Sync interval in seconds

File sync is automatically skipped when using S3 storage:

Terminal window
# With S3, files are stored in the bucket
# No file journal is created
# Replicas access the same S3 bucket
S3_ENDPOINT=https://s3.amazonaws.com
S3_BUCKET=my-bucket
S3_ACCESS_KEY=xxx
S3_SECRET_KEY=xxx

Files are stored with the path structure:

{storage_path}/{collection_id}/{record_id}/{filename}

Examples:

  • files/abc123/def456/document.pdf
  • files/users/user789/avatar.png

When a record is deleted, all associated files are recorded in the journal:

# Deleting a record triggers file cleanup
await client.collection("posts").delete("record_id")
# The journal records all file deletions:
# the replica will delete all files for this record

Bulk deletes also trigger file journal entries:

# Bulk delete: files for all records are journaled
await client.collection("posts").bulk_delete(["id1", "id2", "id3"])

File sync happens after database sync in each cycle:

  1. Database sync completes
  2. File journal entries fetched
  3. Files downloaded/deleted
  4. Acknowledgment sent

If file sync fails, the journal entries remain for the next cycle.

If file download fails:

  • Journal entry is NOT acknowledged
  • File will be retried on next sync
  • Other files continue syncing
  • Error is logged but doesn’t stop sync

Large files are downloaded with:

  • 5-minute HTTP timeout
  • SHA256 checksum verification
  • Atomic file writes (temp file → final location)

The replica’s storage matches the master’s structure:

Master Storage Replica Storage
./data/files/ ./data/files/
├── col1/ ├── col1/
│ └── rec1/ │ └── rec1/
│ └── file.pdf │ └── file.pdf
└── col2/ └── col2/
└── rec2/ └── rec2/
└── image.png └── image.png
Terminal window
# On master, query the journal
sqlite3 data/db/default.db "SELECT COUNT(*) FROM _file_journal"
# View recent entries
sqlite3 data/db/default.db \
"SELECT * FROM _file_journal ORDER BY id DESC LIMIT 10"

Watch for file sync activity:

# Replica logs
INFO starting replication sync
INFO replication sync completed
INFO syncing files count=5
DEBUG downloaded file path=col1/rec1/doc.pdf
DEBUG deleted local file path=col2/rec2/old.png
  1. Check S3 configuration - File sync is skipped with S3
  2. Verify STORAGE_PATH - Ensure replica has write access
  3. Check sync interval - Files sync after DB sync
  4. Review journal - Check for entries in _file_journal
  1. Check replica connectivity - Ack-based cleanup depends on replicas
  2. Verify retention setting - FILE_SYNC_RETENTION_DAYS
  3. Manual cleanup - Run CleanupOlderThan manually
-- Manual cleanup (entries older than 7 days)
DELETE FROM _file_journal
WHERE created < datetime('now', '-7 days');

If file checksum fails:

  1. File is NOT saved to replica
  2. Journal entry remains unacknowledged
  3. Retry happens on next sync
  4. Check master file integrity

Files deleted on master will be deleted on replica during sync. If replica has extra files:

  1. They were created before replication was set up
  2. They were created directly on replica (shouldn’t happen - read-only)
  3. Manual cleanup may be needed
Terminal window
# For frequent sync (every minute)
FILE_SYNC_RETENTION_DAYS=1 # 1 day is sufficient
# For infrequent sync (hourly or more)
FILE_SYNC_RETENTION_DAYS=7 # More buffer for issues

Both master and replica need space for:

  • Database files
  • Uploaded files
  • Temporary download files
Terminal window
# Same path structure on both servers
# Master
STORAGE_PATH=/var/lib/vski/files
# Replica
STORAGE_PATH=/var/lib/vski/files
Terminal window
# Upload a file on master
curl -X POST http://master:3001/api/collections/docs/records \
-H "Authorization: Bearer $TOKEN" \
-F "file=@test.pdf"
# Wait for sync interval
sleep 60
# Verify on replica
curl http://replica:3002/files/docs/record_id/test.pdf

For systems with large files:

  • Increase SYNC_INTERVAL to reduce bandwidth
  • Monitor sync duration
  • Consider S3 for better scalability

The file sync uses these internal replica endpoints:

EndpointDescription
GET /api/replica/files?since=Get journal entries since ID
GET /api/replica/file/*pathDownload file from master
POST /api/replica/files/ackAcknowledge synced entries

These are internal endpoints used by the sync process and require replica authentication.

  1. Atomic with DB sync - Journal replicates with the database
  2. No separate state - Single source of truth
  3. Automatic consistency - DB transaction guarantees
  4. Simple recovery - Restore DB, get journal state
  1. Ack-based - Immediate cleanup when replicas confirm
  2. Time-based - Safety net for:
    • Offline replicas
    • Failed acknowledgments
    • New replicas (don’t need old entries)
  1. Batching - More efficient for multiple files
  2. Consistency - Files sync after DB is consistent
  3. Simplicity - No separate file sync connection
  4. Reliability - Retry on next cycle if failed