Commit Graph

1992 Commits

Author SHA1 Message Date
Edouard Vanbelle a847e0fd2f test(manifest-consistency): ensure hurl tests are ok 2026-08-23 23:19:11 +02:00
Edouard Vanbelle 8d696ccc60 test(ref_count): check ref_count accross copy_folder and folder deletion 2026-08-23 23:19:11 +02:00
Edouard Vanbelle 4b05eec8ad fix(storage): copy_folder_tree never incremented chunk_manifests.ref_count
Silent data loss on an ordinary UI folder copy.

The function bumped only storage.blobs:

    UPDATE storage.blobs b SET ref_count = ref_count + hc.cnt
      FROM (...) hc WHERE b.hash = hc.blob_hash;

but a CDC file's blob_hash names a MANIFEST, not a chunk. For any
multi-chunk file that predicate matches zero rows, so the copy took no
reference at all. Delete the original afterwards and remove_reference
walks the manifest to 0, dedup_gc reaps the manifest and every chunk
behind it, and the copy is unreadable.

Single-chunk files escaped by accident: their whole-file hash equals
their lone chunk's hash, so the UPDATE did match — bumping the wrong
counter, which surfaces as a manifest under-count plus a blob
over-count rather than as loss. That asymmetry is why the bug survived:
small files, which dominate most test corpora, look fine.

Reproduced through the UI on a 5 MiB / 18-chunk file:
chunk_manifests.ref_count stayed at 1 while two storage.files rows
referenced it, and manifests_consistency reported
manifest_refcount_mismatch with delta 1, reap_risk true.

The fix mirrors DedupService::add_reference — manifest first, blobs only
as fallback, with a NOT EXISTS guard so a single-chunk file is not
counted at both levels (which would turn the under-count into an
over-count). orphaned_at is cleared on the blobs branch, as
add_reference does when resurrecting a blob inside its GC grace window.

Only the reference-counting block changes; the rest of the function is
20260902000001 verbatim.

Existing drift is deliberately NOT repaired here — a schema migration
cannot know which counter is authoritative. manifests_consistency
reports it; repair belongs with the recovery framework.

NOT executed against a database: the test instance was down and the dev
instance is read-only by convention. A parse error would fail at boot,
before any data is touched. Verify by re-running the reproduction — a
folder copy of a >1 MiB file should now leave ref_count at 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:19:11 +02:00
Edouard Vanbelle 7e8029027e fix(dedup): log the manifest reap predicate at info, not debug
The reap statement is assembled from the registered reference sources,
so it cannot be grepped out of the source tree — and it DELETES
manifests. Hiding it behind a debug filter an operator has to know to
enable was the wrong default: if what GC considers "referenced" ever
changes, that has to be visible on the next boot without anyone going
looking for it.

Reported in testing: `RUST_LOG=info,oxicloud::dedup=debug` did not
surface it, while a global `RUST_LOG=debug` did — at the cost of an
unusably noisy boot. Rather than have operators carry a special filter
for a line describing a destructive statement, promote it.

The statement is whitespace-collapsed into a single `statement` field
so a multi-line query does not sprawl across the boot log, and the
registered `sources` are logged alongside it — that list is what
actually determines the predicate, so a change to it is the thing worth
noticing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:19:11 +02:00
Edouard Vanbelle 213e1c553a feat(consistency): reconcile chunk_manifests.ref_count
Step 3 (prerequisite 2) of docs/plan/derived-blobs.md, and the last one
before the thumbnail slice.

There are two reference counters and only one was ever verified.
add_reference bumps chunk_manifests.ref_count first and only falls back
to storage.blobs.ref_count, so a reference lands on whichever counter
its hash names: chunk references feed storage.blobs and are reconciled
by blobs_consistency::refcount_mismatch, while Blob references — every
CDC file, and every derived artifact once those exist — feed
chunk_manifests.ref_count, which nothing reconciled.

That gap was survivable only because dedup_gc's reap predicate carried a
second clause ("no storage.files row references this manifest") that
quietly compensated for drift on the bulk-delete paths where ref_count
is never decremented. Generalising that clause to the reference registry
in 1c8ead49 — so thumbnails stop being reaped — removed the
compensation, which is precisely why the counter now needs checking
directly. The two changes have to ship together.

Adds manifests_consistency, a recoverable job reporting
manifest_refcount_mismatch (severity inconsistent). The finding carries
reap_risk so an operator can triage: an under-count means GC reaps a
manifest whose content is still reachable, taking its chunks with it,
while an over-count merely pins storage.

A separate job rather than a second phase of blobs_consistency: one
subject per job, as the other five consistency tenants do, and it avoids
changing the cursor format of an existing recoverable job — which would
strand any run paused across the deploy.

The page query is assembled from the same registry dedup_gc reaps from
(via DedupService::reference_registry), built once at construction, and
pinned by a golden test. Two invariants the test guards: the files term
carries no NOT EXISTS guard — that guard keeps CDC rows out of the
*chunk* level and here would count nothing — and chunk_hashes appears
nowhere, since a manifest citing its own chunks is not a referrer of
itself.

fmt, clippy --all-features --all-targets, and 3 new unit tests clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:19:11 +02:00
Edouard Vanbelle f658e55751 refactor(storage): drive blobs_consistency refcount from the registry
Completes step 1 of docs/plan/derived-blobs.md. The chunk-level
`actual_ref_count` recompute was two correlated subqueries written
inline; it now sums the registered reference sources instead, so
`blobs_consistency` and `dedup_gc` answer "what references this hash"
from one place. If they ever diverged the sweep would bless counts the
collector disagrees with — and the collector wins, destructively.

No behaviour change: the generated expression is the same legacy-files
term (guarded by NOT EXISTS) plus the same manifests-citing-this-chunk
term, and a golden test pins the whole statement byte-for-byte.

Built once at construction, like the reap statement, so the sweep runs
a fixed query per page rather than assembling SQL inside the loop. The
builder refuses an empty registry rather than emitting a query where
every blob looks unreferenced and the entire table reports
refcount_mismatch; there is a test.

DI now constructs one registry and hands the same instance to both
consumers — `DedupService::reference_registry()` is what
`BlobsConsistencyCheck` receives, so agreement is structural rather
than a convention someone has to maintain.

The long comment explaining the single-chunk double-count trap moved
from the query site to the builder's doc comment, where the NOT EXISTS
guard it describes actually lives.

fmt, clippy --all-features --all-targets and the 17 affected unit tests
all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:19:11 +02:00
Edouard Vanbelle a68c938400 fix(storage): stop dedup_gc reaping manifests held only by new sources
Prerequisite 0 of docs/plan/derived-blobs.md. The zero-ref manifest
sweep read:

    WHERE m.ref_count <= 0
       OR NOT EXISTS (SELECT 1 FROM storage.files f
                       WHERE f.blob_hash = m.file_hash)

That OR hardcodes "storage.files is the only thing that can reference a
manifest". A thumbnail manifest held by storage.content_derived_blobs
has ref_count = 1, so the first clause is false — but no files row names
a thumbnail's Blob hash, so NOT EXISTS is true, the OR fires, and the
manifest is deleted, its chunks dereferenced and the bytes reaped on the
next sweep. Landing content_derived_blobs before this fix would destroy
the derived tier on the first GC run.

The second clause is not merely defensive: it is the ONLY reap path for
bulk deletes (user cascade, empty_trash), where the PG trigger touches
storage.blobs but never decrements the manifest and the per-file
cleanup_if_orphaned call is skipped. So the fix has to preserve that
role, not just add tables to the NOT EXISTS. It is now the union of
every registered manifest-level source.

Assembled once, not per sweep. An earlier cut of this change put a
format! inside the DELETE, which made the most dangerous statement in
the file unreadable, un-pasteable into psql, and injection-shaped even
though every input is &'static str. The statement is now built at
construction and stored on DedupService, so:

  * the reap loop runs a fixed statement with no string work,
  * the SQL string is stable, so prepared-statement cache keys are too,
  * a golden test pins it byte-for-byte — a reviewer reads the SQL in
    the test rather than mentally evaluating the registry,
  * initialize() logs it at debug with the contributing source names,
    recovering the "paste it into psql" property the literal had.

The registry is mandatory rather than Option. An empty registry makes
"nothing references it" vacuously true for every row, so the builder
panics instead of emitting a statement that would delete every manifest
in the database; DedupService::new always registers the two built-in
sources, so that panic is unreachable by construction. There is a test
for it.

Adds ref_exists_sql to the port, defaulting to (count) > 0 and
overridden by FilesReferenceSource with a real EXISTS. Without it the
reap predicate would have traded today's short-circuiting NOT EXISTS
for a COUNT(*) = 0 that scans every referrer — a regression precisely
on heavily-deduplicated blobs, which is what GC walks most.

fmt, clippy --all-features --all-targets, and the 11 affected unit
tests all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:19:11 +02:00
Edouard Vanbelle a708389d19 feat(storage): add BlobReferenceSource port + registry
Step 1 of docs/plan/derived-blobs.md. Makes "who references this blob
hash" an extension point instead of SQL hardcoded in two places
(dedup_gc's reap predicate and blobs_consistency's refcount recompute,
both naming storage.files and storage.chunk_manifests directly). Adding
a blob-owning table without teaching those two risks silent orphaning:
GC sees ref_count = 0 and reaps live content.

Behaviour is unchanged — this commit only introduces the port and the
two sources that reproduce today's SQL. Wiring follows.

Two levels, not one. add_reference bumps chunk_manifests.ref_count first
and only falls back to storage.blobs.ref_count, so a reference lands on
whichever counter its hash names and the two must be recomputed
separately. RefLevel is a parameter rather than a property of a source,
because storage.files legitimately contributes at both: a manifest-less
legacy row references a chunk, a CDC row references a Blob. The
NOT EXISTS guard on the chunk-level files term is load-bearing — for a
single-chunk file the whole-file hash equals its lone chunk's hash, so
without it the row is counted at both levels.

SQL fragments rather than a per-hash count. blobs_consistency recomputes
with one query per page, the expected count inlined as correlated
subqueries; asking each source for a count per hash would turn that into
sources x rows round-trips. So sources emit a fragment the registry sums
into the existing page query, and count_references exists only for the
on-demand path where the candidate set is already filtered to
ref_count = 0.

Fragments use their own aliases (cnt_f, cnt_m) rather than the sweeps'
outer-row aliases (b, m). A fragment reusing `m` would shadow the outer
alias in the manifest sweep and silently correlate against itself;
there is a test for it.

The SQL builders are free functions so the shape can be asserted without
constructing a pool — sqlx's connect_lazy still needs a Tokio context,
and the fragments are pure string assembly anyway.

9 unit tests. fmt, clippy --all-features --all-targets, build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:19:11 +02:00
Edouard Vanbelle 07bb38eacb docs(plan): consistency matrix + dedup_gc blocker, migration, schema trim; add hidden-system plan
derived-blobs.md — consolidates several review rounds.

BLOCKER found while building the coverage matrix: the zero-ref manifest
sweep in dedup_gc (dedup_service.rs:2574) deletes a manifest when
`ref_count <= 0 OR NOT EXISTS (SELECT 1 FROM storage.files ...)`. That
OR hardcodes "storage.files is the only thing that can reference a
manifest", so a thumbnail manifest held only by content_derived_blobs
is deleted on the next GC run, its chunks dereferenced and the bytes
reaped. Promoted to prerequisite 0 and delivery step 2. It is also the
missing half of the unreconciled chunk_manifests.ref_count: that OR is
the hack that made the drift survivable.

Adds the 13-edge consistency coverage matrix (rows 1-6 and 13 covered,
7-11 not), and records that backend_consistency needs NO change — the
backend holds chunks, which neither new table references.

BlobReferenceSource correction: `ref_level()` was wrong because
FilesReferenceSource spans both levels (chunk for legacy manifest-less
rows, Blob for CDC rows). Replaced with
`ref_count_sql(level, alias) -> Option<String>`.

Also: migration of existing sidecar content; schema trim to the columns
nothing else owns (no size/format/codec/renderer; content_type kept as
non-key since it removes today's byte-sniffing); ext-{file_id}.jpg
corrected — the client generator ships and covers PDF, which has no
server-side rasteriser, so file_attached_blobs is required rather than
deferred; uploaded_by on the shares NOT NULL/no-FK convention; the
mermaid relation map; copy and version semantics with the
copy_file_satellites consolidation; the storage.files vs file_metadata
table-identity fix; DedupService -> BlobHandler recorded as decided.

NEW hidden-system.md — retires auth.users.image TEXT (inline base64
avatar, up to 512 KiB, already worked around with a narrow projection
after it was measured detoasting M avatars per group fan-out) in favour
of *_file_id pointers at ordinary storage.files rows in one shared
hidden system drive. Because storage.files is already a
BlobReferenceSource, a file pointer costs zero new reference sources
and zero new consistency edges. Records why the alternatives lose,
the drive's required properties (hidden at enumeration, trash off,
quota exempt, boot fail-fast), per-kind visibility in code, the
secrets exclusion rule, the avatar migration, and the future object
catalogue.

Docs only; no code or schema changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:19:11 +02:00
Edouard Vanbelle 8b8cec0ba1 docs(plan): revise derived-blobs design
Enrich OxiCloud to maximise the use of `dedup` Engine

2 cases will be covered:
- blobs issues from other blobs (thumbnail automatic generation from blob)
- by filename (ex: thumbnail uploaded from users)

A local cache will be added when blobs are remote (S3 or similar)
2026-08-23 23:19:11 +02:00
Edouard Vanbelle d57400f7d3 fix(i18n): fix too literal translation with jobs 2026-08-23 13:27:09 +02:00
Dionisio Pozo 34f04b216a Merge pull request #683 from EdouardVanbelle/refactor/userdto 2026-08-22 23:31:25 +02:00
Dionisio Pozo f231c36e76 Merge pull request #684 from yzxcj797/fix/floating-datetime-682 2026-08-22 23:31:03 +02:00
Dionisio Pozo 358ab670ea Merge pull request #680 from EdouardVanbelle/feat/active-sessions 2026-08-22 23:30:39 +02:00
Edouard Vanbelle 06e4df5318 fix(upload): fix race condition in front-end 2026-08-22 08:09:21 +02:00
yzxcj797 af9d9badb4 fix(caldav): accept floating-time DTSTART/DTEND values
parse_ical_datetime rejected any datetime without the trailing 'Z',
so events created without a timezone in calendar apps — which DAVx5
syncs as floating time per RFC 5545 3.3.5 form 2 — failed with
'Invalid DTSTART: Invalid datetime format: expected YYYYMMDDTHHMMSSZ'
and HTTP 400, breaking the whole event upload.

Accept the 15-char floating form and interpret the wall-clock time as
UTC. TZID-anchored forms remain unsupported until VTIMEZONE handling
lands.

Fixes #682
2026-08-22 07:28:21 +08:00
Edouard Vanbelle 537e7f15ef fix(users): /api/admin/users always returns a FullUserDto[] 2026-08-22 00:14:37 +02:00
Edouard Vanbelle a8fa281a02 refactor(user): apply chanoges to hurl tests 2026-08-21 23:56:25 +02:00
Edouard Vanbelle c583b26355 refactor(user): apply change on update entries 2026-08-21 23:18:23 +02:00
Edouard Vanbelle 6a11036d96 feat(admin): show active session/users on dashboard 2026-08-21 23:00:49 +02:00
Edouard Vanbelle 117815ef4d feat(user): show if user is online 2026-08-21 19:34:11 +02:00
Edouard Vanbelle ec9b5087f3 refactor(User): apply changes on frontend 2026-08-21 17:10:02 +02:00
Edouard Vanbelle a11ae679cf refactor(User): move UserDto to PublicUserDto 2026-08-21 16:10:51 +02:00
Edouard Vanbelle d17b3b6bd3 refactor(User): wire /api/auth/me to SelfUserDto and /api/admin/users to FullUserDto 2026-08-21 15:43:43 +02:00
Edouard Vanbelle ec70b21c6e refactor(User): clear separation PublicUserDto, FullUserDto, SelfUserDto 2026-08-21 14:29:36 +02:00
Edouard Vanbelle cc3be1ec38 refactor: apply clippy recos for rustc 1.98.0 2026-08-21 14:27:42 +02:00
Edouard Vanbelle 8cd25d7e0f security(RUSTSEC-2026-0258): update h2 crate
and ignore alert for aws dependency, risk of DoS is null with AWS/S3)
2026-08-21 13:49:49 +02:00
Edouard Vanbelle 543a1a88eb feat(admin > users): UI: correct oidc badge 2026-08-20 10:42:09 +02:00
Edouard Vanbelle 2049535516 feat(session): admin UI showing online sessions 2026-08-20 10:42:09 +02:00
Edouard Vanbelle 20e6e05bb4 feat(sessions): identify online sessions (connected users)
identify online session by writing the `last_seen_at`
information is stored in a map and flush each 30s to prevent performance impact on pgsql
2026-08-20 10:40:27 +02:00
Dionisio Pozo 9d69d9c7e7 Merge pull request #678 from Dessalines39394/fix/star-history-chart
fix(docs): restore the broken Star History chart
2026-08-16 22:18:14 +02:00
Dessalines39394 00e831ea83 fix(docs): restore Star History chart with a working provider
The Star History chart was broken because GitHub stargazer API restrictions disabled the previous service. Point the chart at a working alternative so the README graph renders again.
2026-08-16 11:12:08 +00:00
Dionisio Pozo 65e6026789 Merge pull request #676 from EdouardVanbelle/feat/explicit-login-failure-reason
feat(oidc): explicit login failure reason
2026-08-15 00:58:47 +02:00
Edouard Vanbelle 1a8306f3db feat(login): prevent login form flash on OIDC callback 2026-08-14 13:38:24 +02:00
Edouard Vanbelle 0d5a726ef4 feat(oidc): explicit rejection reason
Show explicitly login rejection (for example when a user does not have a valid
email reported from OIDC but email verification is set)
2026-08-14 13:38:24 +02:00
Dionisio Pozo 5d44b0ea75 Merge pull request #674 from EdouardVanbelle/feat/ui-log-namespaced
Feat/UI log namespaced
2026-08-14 03:58:44 +02:00
Dionisio Pozo 4e2b2e072d Merge pull request #673 from EdouardVanbelle/fix/user-preferences
fix(user-pref): permit edition of user prefs for OIDC account
2026-08-14 03:58:29 +02:00
Edouard Vanbelle 4154019603 feat(ui:delta): add heartbeat on worker 2026-08-14 01:52:53 +02:00
Edouard Vanbelle 55a3890ffa doc: add UI diagnostics help 2026-08-14 01:52:53 +02:00
Edouard Vanbelle 24da7443de feat(ui delta upload): user can change oxi.UPLOAD_BATCH_BYTES
```
oxi.UPLOAD_BATCH_BYTES // get current value
oxi.UPLOAD_BATCH_BYTES=1024*1024 // change values
```

values are stored in localstorage, default is 8*1024*1024
2026-08-14 01:52:14 +02:00
Edouard Vanbelle ef465b99de feat(ui:delta upload): warn user if refresh page during upload 2026-08-14 01:52:14 +02:00
Edouard Vanbelle e357caf43b feat(ui): add a logger to change log level and permit better diagnostics
in this case logs are added in the delta upload worker

you can increase verbosity from console via:

```javascript
// Usage:
oxi.setLogLevel('oxi:upload', 'debug')    // deep dive
oxi.setLogLevel('oxi:upload', 'warn')     // quiet
oxi.log.setLevel('debug')                  // everything to debug
```

values are stored in localstorage

example:

```
oxi.setLogLevel('oxi:upload', 'debug');
'oxi:upload → debug'
deltaUpload.ts:120 [f32ce1] delta start {file: 'Revue de presseg.odp', size: 23658927}
deltaUpload.ts:185 [f32ce1] worker: worker start {file: 'Revue de presseg.odp', size: 23658927}
deltaUpload.ts:186 [f32ce1] worker: wasm loaded
deltaUpload.ts:186 [f32ce1] worker: hashed — blake3=150edbc35f3475a0bdeecd15fe5278dda305ce3b27e53e9c40515a69f0b883f3 (76 chunks)
deltaUpload.ts:186 [f32ce1] worker: negotiate: 76 hashes → 76 missing, 0 dedup'd
deltaUpload.ts:186 [f32ce1] worker: chunk PUT: 28 chunks, 8825338 bytes
deltaUpload.ts:186 [f32ce1] worker: chunk PUT: 29 chunks, 8950230 bytes
deltaUpload.ts:186 [f32ce1] worker: chunk PUT: 19 chunks, 5883663 bytes
deltaUpload.ts:186 [f32ce1] worker: ✅ committed — uploaded 23 658 927 B (no dedup, blake3=150edbc35f3475a0bdeecd15fe5278dda305ce3b27e53e9c40515a69f0b883f3)
deltaUpload.ts:185 [f32ce1] worker: commit HTTP 201 {blake3: '150edbc35f3475a0bdeecd15fe5278dda305ce3b27e53e9c40515a69f0b883f3', uploadedBytes: 23658927, reusedBytes: 0, totalBytes: 23658927, attempt: 0}
deltaUpload.ts:213 [f32ce1] delta done {file: 'Revue de presseg.odp', blake3: '150edbc35f3475a0bdeecd15fe5278dda305ce3b27e53e9c40515a69f0b883f3', savedBytes: 0, uploadedBytes: 23658927}
```
2026-08-14 01:52:03 +02:00
Edouard Vanbelle 1104a4cb06 fix(user-pref): permit edition of user prefs for OIDC account 2026-08-13 20:39:55 +02:00
Dionisio Pozo b1493e2d0e Merge pull request #671 from EdouardVanbelle/i18n/context-translation 2026-08-13 16:58:42 +02:00
Dionisio Pozo aa0f12cd26 Merge pull request #670 from EdouardVanbelle/chore/dot-env-example 2026-08-13 16:58:34 +02:00
Edouard Vanbelle 27ab392a35 i18n: fix uncontexted strings
For example jobs where translated into "employment" in other language
Remove english text in other languages
2026-08-13 00:30:55 +02:00
Edouard Vanbelle c61bc818b1 doc: listen to 0.0.0.0 as example for Docker
consider that many user will use docker to test Oxicloud
2026-08-12 20:53:16 +02:00
Dionisio Pozo 06aff99fa3 Merge pull request #635 from swissiety/rfc-6868-param-encoding
fix(vcard): add RFC 6868 parameter value encoding
2026-08-12 10:32:26 +02:00
Edouard Vanbelle 555bf82a16 doc(dpop): update dpop status 2026-08-12 00:11:17 +02:00
Edouard Vanbelle 5a82aa1511 feat(session): UI: improve admin > sessions view 2026-08-11 23:50:43 +02:00