No description
  • Go 77.7%
  • JavaScript 17.2%
  • CSS 3.1%
  • HTML 1.5%
  • Shell 0.5%
Find a file
Jimmy Berglund 8c987f9243
Fix lightbox crash when opening previewable files
previewItems passed sortCompare directly as the array comparator, but
sortCompare expects the active sort as a third argument, so opening any
image/video/audio/pdf from the Files view threw "sort is undefined".
Pass state.sort instead; lightbox prev/next now also follows the table's
active column order.
2026-08-24 14:49:32 +02:00
cmd/trove Harden rate-limit IP extraction and username validation 2026-08-21 13:24:08 +02:00
docs Add WOPI host implementation plan 2026-08-21 16:36:07 +02:00
internal Fix lightbox crash when opening previewable files 2026-08-24 14:49:32 +02:00
scripts Fix WebDAV upload failures on locked and special-char resources 2026-08-14 12:38:33 +02:00
.env.example Harden rate-limit IP extraction and username validation 2026-08-21 13:24:08 +02:00
.gitignore Implement milestone 1: server skeleton 2026-08-07 14:21:25 +02:00
build-and-push.sh Add multi-arch build-and-push script 2026-08-13 21:19:17 +02:00
docker-compose.yml Run Collabora sidecar on host networking, pass through server_name 2026-08-21 20:10:01 +02:00
Dockerfile Add video thumbnails via ffmpeg 2026-08-19 22:40:51 +02:00
go.mod Add optional YAML config file support 2026-08-13 20:54:31 +02:00
go.sum Add optional YAML config file support 2026-08-13 20:54:31 +02:00
LICENSE Add MIT license 2026-08-13 21:26:53 +02:00
README.md Expand office editing docs into a Collabora setup guide 2026-08-21 20:10:01 +02:00
todo.md Removed todo item 2026-08-15 23:41:50 +02:00
trove.example.yaml Add WOPI host for external office editing 2026-08-21 17:38:02 +02:00

trove

A lightweight, self-hosted, encrypted cloud for a family or small group. One Go binary, SQLite metadata, Docker deployment. Files are encrypted at rest with a per-user key hierarchy — no server master secret.

Everything below is implemented: multi-user accounts with Argon2id auth, envelope encryption, WebDAV over encrypted storage, an embedded web UI (browse, upload/download, zip, gallery), a per-user trash, user-to-user sharing, public share links, a client-side encrypted vault, and a built-in PIM web UI (calendars, tasks, contacts) backed by the CalDAV/CardDAV-compatible data model, plus CalDAV/CardDAV protocol endpoints for external clients (see Status).

Features

  • Encryption at rest — ChaCha20-Poly1305, per-file keys, envelope-encrypted storage. Disk theft, stolen backups, or VPS snapshots reveal nothing.
  • Multi-user accounts — password verification doubles as the key-unwrap step (one Argon2id call per login). The first admin is created in the web UI on first boot, and admins invite new users with shareable links.
  • Recovery phrase — a per-user backup phrase shown once at setup; used to reset a forgotten password without losing any data.
  • WebDAV (/dav/) — mount your files from any WebDAV client (rclone, davfs2, Cyberduck, etc.).
  • Embedded web UI (/) — no build step, vanilla JS: login, browse, upload/download with progress, mkdir, rename, move, delete, zip download, a photo/video gallery view for the current folder plus a library-wide Photos view (in the navbar) grouped by month, and a Trash view (restore, delete forever, empty) in the navbar. Grid tiles and the Photos view are backed by server-side thumbnails and a server-side media index, so even thousands of images load as fast as a normal cloud gallery. The Photos view also groups by location (EXIF GPS, offline reverse-geocoded labels) and by user-created albums with multi-select add/remove. Albums can also contain whole folders: every photo placed in a folder (now or later, recursively) automatically joins the album. A Settings view (gear in the navbar) lets you change your display name and account password.
  • Encrypted vault — a client-side encrypted, server-blind vault: the passphrase is derived with PBKDF2 in the browser, a master key wraps per-file AES-GCM keys, and the server only ever stores ciphertext — it never sees the passphrase or any key. Change the vault passphrase from the Vault view; it is fully separate from your account and never touched by a password reset.
  • TrashDELETE moves items to a per-user trash instead of permanently deleting them. Restore (with automatic name (n) renaming on conflict), delete-forever, empty-trash, and a background purge honoring TROVE_TRASH_RETENTION (default 30d). Trash/restore are raw ciphertext renames — no re-encryption, keyless metadata.
  • Storage quotas — per-user quotas enforced on WebDAV uploads and trash restores (HTTP 507 when exceeded), charged to the folder owner for writes into folders shared with you. Admins set quotas in the Users view or via the API; TROVE_DEFAULT_QUOTA sets the quota for new users. Trash does not count toward usage.
  • Public share links — read-only links to a file or folder, served as a self-contained web page (/s/<token>) with browse, download, inline preview, and zip download. Optional password (bcrypt, IP rate-limited) and expiry, instant revocation, and read-only WebDAV (/s/<token>/dav) for mounting the link as a network drive. Capability tokens are never logged.
  • PIM web UI — calendars (month view + timed grid, recurring events), tasks (VTODO) and contacts (address books), all in the navbar. Backed by the CalDAV/CardDAV data model (byte-preserving encrypted .ics/.vcf per-user storage) and served to external clients by the CalDAV/CardDAV protocol layer below.
  • Office editing (WOPI) — optional integration with an external online office suite such as Collabora CODE: an Edit online button appears on documents (.odt, .docx, .xlsx, ...), opening them in the editor with real-time co-editing when several users have access. The server acts as a WOPI host for its encrypted files; see Office editing.

Security model

Threat model: protect data at rest. This is not zero-knowledge — the server must decrypt to serve clients, so a plaintext key exists in memory only during a request.

Key hierarchy:

password       --Argon2id--> KEK  --wraps--> Identity private key (X25519, per user)
recovery phrase --Argon2id--> KEK2 --wraps--> Identity private key
Identity public key (stored) --ECIES--> FileKey (per file)      | content encrypted
                                                                  v
                                                      ChaCha20-Poly1305, 64KB chunks
  • Identity keypair (X25519, per user): the private key is sealed under the password/recovery KEKs; the public key is stored and used to seal FileKeys to the account via ECIES (ephemeral key + HKDF + ChaCha20-Poly1305).
  • FileKey (256-bit random, per file) encrypts content and is wrapped for the owner and for each share grant/link.
  • The single KDF output is split into a 16-byte verification tag and a 32-byte KEK — one Argon2id call both authenticates and unlocks the private key.
  • No cross-request caching by default: KEK and private key are zeroized after each request. Optional opt-in short-TTL key cache via TROVE_KEY_CACHE_TTL.
  • Directory listings read plaintext headers only — no key derivation on stat.
  • Brute-force protection: login, recovery, setup, invite, and share-link endpoints are rate-limited with exponential backoff keyed by username and by IP. The client IP is the TCP peer address; X-Forwarded-For is honored only for connections from proxies listed in TROVE_TRUSTED_PROXIES, so clients cannot spoof their way to fresh rate-limit buckets. Login limits are tunable via TROVE_LOGIN_MAX_ATTEMPTS and TROVE_LOGIN_LOCKOUT. All responses carry strict security headers (CSP script-src 'self', X-Frame-Options: DENY, Referrer-Policy, X-Content-Type-Options).
  • WOPI editor sessions: the access token is a random capability sealed under its own key (like share links); it carries only the file's keys, expires on a sliding idle TTL with a hard cap, is delivered by form POST (never in a URL), and dies with the file or account. The office suite itself sees plaintext over its own connection to trove — run both behind TLS.
Attacker has Can decrypt
Disk only Nothing
Disk + password That user's account (inherent to Level 1)

Getting started

Local build

Requires Go 1.26+.

go build -o trove ./cmd/trove

export TROVE_DATA_DIR=./data
export TROVE_PORT=8080

./trove serve

Alternatively, point it at a YAML config file (see trove.example.yaml):

./trove serve --config /etc/trove/trove.yaml

On first boot, open http://localhost:8080 and follow the Set up trove screen to create the first admin account. Save the one-time recovery phrase it shows — it is the only way to reset a forgotten password.

Docker

docker compose up --build -d

Data lives in the bind-mounted ./data directory. On first boot, open the web UI to create the admin account.

Configuration

Configuration can come from an optional YAML file (--config) and/or environment variables. Precedence is defaults < YAML file < environment variables, so TROVE_* always wins when both are set.

A fully commented template lives in trove.example.yaml; pass it to the server with ./trove serve --config /path/to/trove.yaml. Without --config (and no TROVE_CONFIG env var), only environment variables are used. The YAML keys below mirror each variable name (data_dir, port, ...), with the same value syntax: Go-style durations plus a Nd day suffix, and byte counts with optional suffixes (1GiB, 2MB).

Variable Default Description
TROVE_DATA_DIR data Directory for ciphertext files and the SQLite DB.
TROVE_PORT 8080 HTTP listen port.
TROVE_KEY_CACHE_TTL 0 (off) Optional short-TTL private-key cache for sync-heavy users.
TROVE_TRASH_RETENTION 30d How long trashed items are kept before the background purge deletes them. Set to 0 to keep items forever.
TROVE_DEFAULT_QUOTA 0 (unlimited) Storage quota granted to newly created users, in bytes. Suffixes are supported (1GiB, 2MB); 0 or unlimited means no limit. Admins can change any user's quota in the Users view.
TROVE_THUMB_SIZE 256 Longest edge (px) of gallery thumbnail tiles, in 16..4096.
TROVE_THUMB_MAX_PX 200000000 Pixel budget for images to be thumbnailed; larger images show a placeholder and stay downloadable (guards against memory exhaustion).
TROVE_THUMB_TTL 30d How long cached thumbnails are kept before the background sweeper reclaims them.
TROVE_ARGON2_MEMORY 19 Argon2id memory cost, in MiB.
TROVE_ARGON2_TIME 2 Argon2id time cost.
TROVE_ARGON2_THREADS 1 Argon2id parallelism.
TROVE_LOGIN_MAX_ATTEMPTS 5 Failed logins allowed before a username/IP is temporarily locked out.
TROVE_LOGIN_LOCKOUT 5m Base lockout duration for login throttling (per-attempt backoff scales it).
TROVE_TRUSTED_PROXIES (empty) Comma-separated proxy IPs/CIDRs (e.g. 127.0.0.1,::1,10.0.0.0/8) whose X-Forwarded-For header is trusted when deriving client IPs for rate limiting. Leave unset when not behind a reverse proxy; clients then cannot spoof the header.
TROVE_WOPI_ENABLED false Enable the WOPI host and the Edit online button (requires both URLs below).
TROVE_WOPI_CLIENT_URL (empty) Base URL of the WOPI client / office suite (e.g. http://code:9980). Discovery is fetched from <url>/hosting/discovery.
TROVE_WOPI_PUBLIC_URL (empty) The URL the office suite uses to reach trove's /wopi/ endpoints. Must be reachable from the editor container — localhost will not work.
TROVE_WOPI_TOKEN_TTL 2h How long an editor session token stays valid without editor activity; each request slides it forward.
TROVE_WOPI_TOKEN_MAX_TTL 24h Hard cap on a session token's total lifetime from issuance, however active the session.

Serve over HTTPS when exposing beyond localhost (e.g. a Caddy/Traefik reverse proxy); the app itself serves plain HTTP. WebDAV and CalDAV clients typically require TLS. When behind a reverse proxy, set TROVE_TRUSTED_PROXIES to the proxy's address so rate limiting keys on real client IPs.

Backups

Backing up trove is a matter of copying one self-contained directory: TROVE_DATA_DIR (default data). It holds the SQLite database (trove.db) with all accounts, keys, shares, and metadata, plus the encrypted file blobs in per-user blobs/ subdirectories.

  • Exclude .thumbs/ (a rebuildable thumbnail cache, regenerated on demand) and tmp/ (transient in-progress uploads) to save space.
  • Confidentiality: everything on disk is ciphertext, so a backup can be stored anywhere — offsite storage, another machine, or a public bucket leaks nothing.
  • Consistency: the database runs in SQLite WAL mode. Never hot-copy just trove.db (or its -wal/-shm companions non-atomically). Use one of:
    • a filesystem snapshot — btrfs/zfs/LVM snapshot of the data directory, or a snapshot-capable backup tool (restic, borg, kopia). The whole tree is captured atomically and SQLite replays the WAL on the next open; this works while the server is running; or
    • stop-then-copydocker compose stop (or SIGTERM; the server shuts down cleanly and closes the database), copy or tar the data directory, then start trove again.
  • Restore — extract the backup into a fresh directory and point TROVE_DATA_DIR at it (or restore it in place). Trash, quotas, shares, the PIM data, and the client-side vault are all covered automatically; missing thumbnails are regenerated.

Usage

Web UI

Log in with your account, then:

  • Browse files with the table view or the gallery grid.
  • Upload with drag-and-click (Upload), download files, and download folders as a zip (Zip).
  • Gallery shows the current folder's photos/videos as a grid; Photos shows every photo/video across your account, grouped by month. The Photos sidebar lists your albums (with live photo counts) and locations. Folders are added to albums from the file browser: the album button on a folder row lets you pick which albums it belongs to, and every photo inside it (including future uploads and subfolders) counts automatically. Membership is keyed on a file's stable id, so a member follows the file when it's moved and survives a trip through the trash (restored members come back); it is dropped only when the file is deleted forever.
  • Trash shows your deleted files and folders. Restore an item back to its original location (renamed with a (n) suffix if that path is now taken), delete it forever, or empty the whole trash. Items older than TROVE_TRASH_RETENTION are purged automatically.
  • Shared shows folders and files other people shared with you (each under Shared/<owner>/<name> in the file listing) plus the shares you've created; the Share button on a file or folder shares it with another user, and you can revoke a share from the Shared view at any time. Public share links are created and revoked in the same view.
  • Calendar — month view with a timed-event grid; create/edit/delete events (recurring events repeat via RRULE: daily/weekly/monthly/yearly, INTERVAL, COUNT, UNTIL, BYDAY with ordinals like 2TU/-1FR, BYMONTH, BYMONTHDAY, BYSETPOS, WKST, BYHOUR/BYMINUTE/BYSECOND). Tasks shows your todos with a checkbox and due date; completing a task marks the VTODO STATUS:COMPLETED/COMPLETED. Contacts — address books with name/email/phone/org, plus a raw-vCard editor.
  • Refresh re-reads the current view.
  • Users (admins only) — list users and delete accounts, create invite links, and revoke them. Anyone with a valid invite link can create an account at /invite/<token>. Each user row shows storage usage and lets you edit the quota (or set unlimited).
  • Settings — change your display name and your account password. Changing the password keeps your files intact (the private key is re-wrapped) but WebDAV/CalDAV/CardDAV clients must re-authenticate with the new password. The vault passphrase is separate and is changed from the Vault view; because the vault is encrypted in your browser, changing it requires the current passphrase and only ever touches the vault, never your account.
  • Uploading more than your quota returns 507 Insufficient Storage; if your trash contains items, a restore that would exceed your quota is rejected too.

WebDAV

The WebDAV root is /dav/. Example with rclone:

rclone config create trove webdav \
  url=http://localhost:8080/dav \
  vendor=other \
  user=alice \
  pass=<base64-encoded-password>
rclone mount trove: /mnt/trove

trove supports the protocol pieces a sync client needs:

  • Content-hash ETags — files uploaded via WebDAV carry a strong ETag that is the SHA-256 of the plaintext content, so a client can compare and detect changes without downloading. Pre-existing files (created before hashes were stored) get their hash backfilled on the first full download; until then they fall back to a stat-derived ETag (mtime-size). Because the hash is derived from the plaintext, it is recoverable only while the user's metadata key is unwrapped, and stays encrypted at rest.
  • X-OC-Mtime — send the local modification time (unix seconds) on PUT and trove preserves it, so clients can keep mtimes in sync.
  • Conditional PUTIf-Match and If-None-Match (including *) are honored on uploads and answered with 412 Precondition Failed, giving clients an atomic overwrite-if-unchanged primitive for conflict-free sync.

CalDAV / CardDAV

Calendars, tasks, and contacts are served to standard clients at /caldav/ and /carddav/ (RFC 4791 / RFC 6352), with sync-collection (RFC 6578), calendar-query, and addressbook-query/multiget. Point a CalDAV/CardDAV client (DAVx5, Thunderbird, Apple Calendar) at:

url:      https://your-host/caldav/
username: alice
password: <password>

Attendees on events are stored byte-exact (kept intact through CalDAV sync) and are viewable/editable in the web UI's event editor (name, email, partstat, role, RSVP). trove does not implement iTIP scheduling and never sends invitations itself: in Thunderbird, enable "Prefer client-side email scheduling" per calendar so invites go out via your email identity instead.

Office editing (WOPI)

With TROVE_WOPI_ENABLED=true and an office suite such as Collabora CODE reachable, documents get an Edit online button in the web UI. The server implements the WOPI host protocol (/wopi/): CheckFileInfo, GetFile/PutFile, and the WOPI lock model, so the editor can read and write the encrypted file like any other client. Saves are re-encrypted with a fresh per-file key; wraps for the owner, every user-to-user share covering the file, and every public link are preserved, so all existing access keeps working after an edit. Content-hash ETags are maintained, and quota is charged to the folder owner as usual.

# trove.yaml
wopi:
  enabled: true
  client_url: http://code:9980     # where trove fetches discovery from
  public_url: https://cloud.example.com  # where CODE reaches trove

Setting up Collabora CODE

The bundled compose file ships a Collabora sidecar under the office profile:

TROVE_WOPI_ENABLED=true \
TROVE_WOPI_CLIENT_URL=http://code:9980 \
TROVE_WOPI_PUBLIC_URL=https://cloud.example.com \
COLLABORA_HOSTS='https://cloud\.example\.com' \
CODE_EXTRA_PARAMS="--o:ssl.enable=false --o:ssl.termination=true" \
COLLABORA_SERVER_NAME=office.example.com \
docker compose --profile office up -d

Three parties must be able to reach each other, each governed by one setting:

Direction Carries Setting
browser → CODE the editor launch URL built from discovery TROVE_WOPI_CLIENT_URL
CODE → trove CheckFileInfo / GetFile / PutFile / lock calls against the WOPISrc TROVE_WOPI_PUBLIC_URL
trove → CODE the discovery document (/hosting/discovery) TROVE_WOPI_CLIENT_URL

localhost is almost never right for TROVE_WOPI_PUBLIC_URL: it is resolved inside the CODE container, where nothing listens on trove's port. Use the address at which trove is (or will be) published.

Alias groups — CODE refuses WOPI hosts it was not configured to trust via aliasgroup1, exposed as COLLABORA_HOSTS. Entries are regular expressions of complete origins including scheme and port (https://cloud\.example\.com, http://192\.168\.0\.106:8080); several aliases may be given space-separated in one group. The compose default https://.* trusts any https host but rejects plain-HTTP setups — override it when testing without TLS. An alias mismatch is the classic cause of an editor tab that fails immediately with a "session expired"-style error before a single request reaches trove.

Plain HTTP vs TLS — what CODE writes into its discovery document follows its own ssl settings, not the proxy in front of it:

  • direct HTTP access (browser reaches CODE itself): --o:ssl.enable=false --o:ssl.termination=false (the compose default);
  • behind a TLS-terminating proxy: --o:ssl.enable=false --o:ssl.termination=true plus COLLABORA_SERVER_NAME=<proxy's public host>, so launch URLs come out as https/wss.

Network topologies

Everything in one compose project behind a proxy (production). Both containers share the compose network: remove the sidecar's network_mode: host and restore a ports mapping ("9980:9980"), then point trove's client_url at http://code:9980. Terminate TLS once for both origins and set TROVE_WOPI_PUBLIC_URL=https://cloud.example.com, COLLABORA_HOSTS='https://cloud\.example\.com', COLLABORA_SERVER_NAME=office.example.com, and the termination flag above. Keep both origins on the same scheme or browsers will block the handoff as mixed content.

Native trove binary + containerized CODE on one machine. This is what the shipped compose file does out of the box: the sidecar runs with network_mode: host because rootless podman/Docker otherwise refuse traffic from the container to the host's LAN IP at the NAT/firewall boundary — even with the port open — which surfaces as "session expired". With host networking set TROVE_WOPI_CLIENT_URL=http://<LAN-IP-or-host>:9980 (this exact origin is what the browser opens) and TROVE_WOPI_PUBLIC_URL=http://<LAN-IP>:<port> (the origin CODE calls back on). For same-machine use plain localhost works in both places since host networking puts CODE outside the NAT.

Separate machines. Same rules, no shortcuts: client_url must be CODE's address as seen by browsers, public_url trove's address as seen by CODE, and that second origin must match a COLLABORA_HOSTS pattern.

Troubleshooting

Watch both sides: docker compose --profile office logs code, and trove's log, which records every callback that reaches /wopi/ as wopi request method=… path=… status=…. The CODE image ships no shell, so to probe connectivity run a throwaway curlimages/curl container attached to the same network rather than docker exec.

Symptom Likely cause
Editor tab fails immediately ("session expired"); no wopi request lines in trove's log CODE refused the WOPISrc: COLLABORA_HOSTS doesn't cover the TROVE_WOPI_PUBLIC_URL origin, or CODE cannot reach that URL at all
Document loads but saves fail ("Could not upload document"); trove logs only GETs, never POSTs The session was downgraded read-only because the pre-save lock was rejected — check CODE's log for the reason and re-open the file for a fresh token
Discovery/launch URLs say http:// though you browse over https:// CODE advertises its own ssl mode: set --o:ssl.termination=true and COLLABORA_SERVER_NAME
Browser console shows a CSP form-action violation during handoff TROVE_WOPI_CLIENT_URL differs from the origin actually browsed (scheme/host/port must match exactly)
Instant connection refused from CODE to a native trove on the same host Rootless container NAT/firewall boundary; give CODE network_mode: host or publish trove on all interfaces and open the port

Notes:

  • Co-editing — sessions are scoped to the document (not the user), so two users who open the same shared file join one live editing session.
  • Token delivery — access tokens are handed to the browser via form POST into a new tab: they never appear in URLs, history, or Referer headers, and they expire (sliding idle TTL with a hard cap) and can be revoked by deleting the file or the account.
  • Caveats — shares granted while a document is open only cover versions saved after the next session start; concurrent WebDAV and editor saves are last-write-wins (the WOPI lock protocol arbitrates editors only); a session left idle past TROVE_WOPI_TOKEN_TTL needs a page reload.

CLI

trove serve    Run the server
trove version  Print version
trove help     Show help

User management happens in the web UI (first-boot setup, invite links).

Development

go test ./...
go vet ./...

Layout:

  • cmd/trove — CLI entry point (serve, --config flag).
  • internal/config — env/YAML configuration, Argon2id tuning.
  • internal/db — SQLite schema and migrations.
  • internal/auth — user + key management, invite links, Argon2id verifier, Basic-auth middleware.
  • internal/crypto — envelope encryption, header format, chunked AEAD, recovery phrase.
  • internal/store — the encrypted flat-blob store and node index backing all files, directories, and the per-user trash.
  • internal/dav — WebDAV server over encrypted storage.
  • internal/share — user-to-user shares and public share links (grants, links, revocation, read-only link WebDAV).
  • internal/gallery — server-side thumbnails (encrypted ./.thumbs cache) and the account-wide media index behind the Photos view.
  • internal/pim — CalDAV/CardDAV data model over encrypted .ics/.vcf storage (calendars, events, recurrence, todos, address books, contacts) plus the protocol endpoints at /caldav/ and /carddav/.
  • internal/album — user photo albums (lightweight bookkeeping over media paths and folder members; no file moves).
  • internal/geo — offline reverse geocoding for Photos location labels (no third-party calls).
  • internal/limiter — brute-force rate limiting with exponential backoff.
  • internal/vault — server-blind index behind the client-side encrypted vault.
  • internal/quota — per-user quota checks and storage-usage accounting.
  • internal/wopi — WOPI host for external office suites: session tokens, editor discovery, CheckFileInfo/GetFile/PutFile, lock arbitration.
  • internal/server — HTTP routes, JSON API, embedded web UI, trash.

Status

Area Status
Server skeleton, config, Docker Done
Users & auth (Argon2id, web first-boot setup, invite links, recovery) Done
Encryption core (envelope, chunked AEAD, zeroization) Done
WebDAV over encryption Done
Web UI (browse, upload/download, zip, gallery) Done
Trash (delete→trash, restore, purge, retention) Done
Storage quotas (per-user, admin-managed, WebDAV enforcement) Done
User-to-user shares (shared Shared/ namespace, revoke, UI) Done
Public share links (browse/download/zip, password, expiry, revoke, WebDAV) Done
Gallery at scale (server-side thumbnails, encrypted cache, media index) Done
PIM web UI (calendars, tasks, contacts) Done
CalDAV / CardDAV protocol endpoints (external clients) Done
Client-side encrypted vault Done
Photos locations & albums Done
WebDAV sync protocol (hash ETags, X-OC-Mtime, conditional PUT) Done
Office editing (WOPI host, Collabora CODE, co-editing) Done

License

MIT — see LICENSE.