Skip to content

feat: uploads sync#30

Open
MoustaphaCamara wants to merge 57 commits into
masterfrom
mouss/uploads
Open

feat: uploads sync#30
MoustaphaCamara wants to merge 57 commits into
masterfrom
mouss/uploads

Conversation

@MoustaphaCamara

@MoustaphaCamara MoustaphaCamara commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

fix #27
Add uploads
Handle them inside desktop app
Push to eLabFTW
patch entries
move back button
fix modal

ALSO:
have a button to "see in eLabFTW" if the entry already has a correspondance
e.g., See experiment #79 in eLabFTW so we know it's an exp to update

Summary by CodeRabbit

  • New Features
    • Added file uploads to entries, including file selection, encrypted local storage, upload listing, and a 100 MB limit.
    • Added links to view entries in eLabFTW.
    • Entries can now synchronize associated uploads with eLabFTW.
    • Upload synchronization warnings are displayed without blocking other uploads.
  • Bug Fixes
    • Improved profile database reliability during rapid updates.
    • Improved error handling for encryption, uploads, and remote synchronization.
  • Style
    • Refined upload cards, panels, profile controls, and application layout styling.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Upload Storage, Sync, and Frontend Integration

Layer / File(s) Summary
Encryption, storage, and database schema
crypto.go, storage.go, sqlite.go
Raw XChaCha20-Poly1305 operations, XDG data paths, encrypted upload paths, SQLite connection limits, and schema migrations for uploads and remote mappings are added.
Entry-associated upload lifecycle
uploads.go
Uploads are imported, encrypted, stored with entry metadata, listed by entry, and selected through a Wails file dialog.
Remote link listing and upload push
elabftw_client.go, sync_links.go, sync_push.go, sync_uploads.go
Remote entry links are listed, decrypted uploads are posted as multipart requests, mappings are persisted, and upload failures are returned as warnings.
Frontend upload and remote-link controls
frontend/src/components/MainApp.svelte, frontend/src/components/Uploads/UploadsPanel.svelte, frontend/wailsjs/go/main/*, frontend/src/css/*, frontend/src/components/Instances/InstancesView.svelte
Wails bindings, models, remote-link buttons, upload importing, warning alerts, and supporting styles are added.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: nicolascarpi

Poem

Files beam aboard through encrypted night,
SQLite charts their metadata right.
Remote links shimmer, uploads align,
Warnings report through the alert design.
Engage the panel—mission complete.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers XDG_DATA_HOME storage, encryption, and most metadata, but it does not show the required long_name field for #27. Persist long_name in the upload model, schema, and APIs so the stored metadata fully matches #27.
Out of Scope Changes check ⚠️ Warning The PR adds remote-link sync, push logic, UI panels, and styling tweaks that go beyond the file-storage/encryption scope of #27. Split the remote-sync, UI, and cosmetic changes into separate PRs or justify them in the issue scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly describes the main change: upload sync support.

Comment @coderabbitai help to get the list of available commands.

@MoustaphaCamara
MoustaphaCamara changed the base branch from master to mouss/alertFix June 10, 2026 10:24
@NicolasCARPi

Copy link
Copy Markdown
Contributor

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (9)
uploads.go (2)

116-125: 💤 Low value

CreatedAt and ModifiedAt timestamps not populated in returned struct.

Number One notes that while the database auto-generates created_at and modified_at via the schema defaults, the returned StoredUpload struct does not include these values. If the frontend relies on these fields immediately after import, they will be empty strings. Consider querying the inserted row to retrieve the actual timestamps, or generating them in Go before insertion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uploads.go` around lines 116 - 125, The returned StoredUpload struct is
missing CreatedAt and ModifiedAt values; update the insertion flow that
constructs and returns &StoredUpload{...} to populate CreatedAt and ModifiedAt
(either by generating timestamps in Go before inserting or by querying the
inserted row from the DB to read the auto-generated created_at/modified_at);
specifically set the CreatedAt and ModifiedAt fields on the returned
StoredUpload (alongside ID, RealName, LongName, StorageName, Hash,
HashAlgorithm, Filesize, State) using the chosen approach so the frontend
receives real timestamps immediately after import.

54-57: 💤 Low value

Captain, this approach reads the entire file into memory before encryption.

Number One reporting a potential memory concern, Captain. For files of significant magnitude, loading the complete contents via os.ReadFile could strain our ship's resources considerably. I recommend we consider a streaming approach for production use—processing the file in chunks would allow us to compute the hash incrementally while encrypting without holding the entire payload in memory.

For the current mission parameters, if file sizes are expected to remain modest (a few megabytes), this approach is acceptable. However, I advise logging this in the ship's manifest for future enhancement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uploads.go` around lines 54 - 57, The current code reads the entire file into
memory via os.ReadFile(sourcePath) (the block creating content, err), which can
OOM for large files; replace it with a streaming approach: open the file with
os.Open(sourcePath), defer Close, create a hash.Hash (e.g., sha256.New()) and an
encrypting writer/stream (your existing encryption routine or a
cipher.StreamWriter), then use io.Copy or io.TeeReader to copy from the file
into the encrypting writer while updating the hash incrementally so you never
buffer the whole file in memory; update any uses of the in-memory content
variable to work with the streamed output (e.g., writing encrypted bytes to disk
or returning a reader).
sync_push.go (1)

432-441: ⚡ Quick win

Recommend consolidating entity type mapping logic.

Number One observes that elabftwUploadEntityType duplicates entity type mapping logic already present in elabftwEntityPath (lines 32-41). Both functions map "experiment" → "experiments" and "resource" → "items", differing only in the leading slash.

Consider refactoring to eliminate duplication and ensure consistency. One approach:

♻️ Proposed consolidation
+func elabftwEntityName(entityType string) (string, error) {
+	switch entityType {
+	case "experiment":
+		return "experiments", nil
+	case "resource":
+		return "items", nil
+	default:
+		return "", fmt.Errorf("Invalid eLabFTW entity type")
+	}
+}
+
 func elabftwEntityPath(entityType string) (string, error) {
-	switch entityType {
-	case "experiment":
-		return "/experiments", nil
-	case "resource":
-		return "/items", nil
-	default:
-		return "", fmt.Errorf("Invalid eLabFTW entity type")
+	name, err := elabftwEntityName(entityType)
+	if err != nil {
+		return "", err
 	}
+	return "/" + name, nil
 }

-// handle uploads
 func elabftwUploadEntityType(entityType string) (string, error) {
-	switch entityType {
-	case "experiment":
-		return "experiments", nil
-	case "resource":
-		return "items", nil
-	default:
-		return "", fmt.Errorf("Invalid eLabFTW entity type")
-	}
+	return elabftwEntityName(entityType)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sync_push.go` around lines 432 - 441, elabftwUploadEntityType duplicates the
"experiment"→"experiments" and "resource"→"items" mapping already implemented in
elabftwEntityPath; consolidate by centralizing the mapping in a single helper
used by both functions (for example, create/reuse a single map or helper
function like elabftwEntityBase that returns "experiments" or "items") and
update elabftwEntityPath to prefix the slash as needed and
elabftwUploadEntityType to call that helper (preserve same error behavior and
error message when unknown types are passed).
sync_links.go (1)

78-81: 💤 Low value

Recommendation: Wrap the URL generation error with context.

Number One observes that when elabftwOnlineURL fails during iteration, the error lacks context about which remote link caused the failure. This could complicate debugging when multiple remote links exist for an entry. Consider enriching the error message with the failing link's identifiers.

🔧 Proposed enhancement
 link.URL, err = elabftwOnlineURL(link.SiteURL, link.Type, link.RemoteID)
 if err != nil {
-	return nil, err
+	return nil, fmt.Errorf("generate URL for remote link (type=%s, remoteID=%d): %w", link.Type, link.RemoteID, err)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sync_links.go` around lines 78 - 81, The call to elabftwOnlineURL that
assigns link.URL lacks contextual info on error; modify the error handling
around elabftwOnlineURL(link.SiteURL, link.Type, link.RemoteID) so that when it
returns an error you wrap or annotate it with the link identifiers (e.g.,
link.SiteURL, link.Type, link.RemoteID or link.RemoteID) and a short message
indicating which remote link failed—use fmt.Errorf or errors.Wrap to return a
new error that includes the original error plus those identifiers where link.URL
is being set.
sync_uploads.go (1)

115-119: ⚖️ Poor tradeoff

Operational consideration: Partial upload push can leave inconsistent state.

Number One notes that if pushing multiple uploads and one fails mid-iteration, previously pushed uploads remain on the remote server while the function returns an error. This creates a partially-synchronized state that may confuse users or require manual cleanup.

Consider one of the following approaches:

  1. Document this behavior clearly for callers
  2. Track which uploads were successfully pushed in a local2remote-like join table
  3. Implement idempotency checks before pushing each upload

This is primarily an operational concern for the calling code to handle, but warrants explicit documentation or enhanced state tracking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sync_uploads.go` around lines 115 - 119, The loop over uploads calling
a.pushUploadToRemoteEntity can leave a partially-synced state if one push fails;
modify the code so each successful push is recorded in a local-to-remote join
(or tracking) table and make pushUploadToRemoteEntity idempotent: before
pushing, check the join table for an existing mapping for upload.ID and
remoteEntityID to skip duplicates, after a successful push insert the mapping
(e.g., record upload.ID -> remoteUploadID), and ensure failures return without
mutating the join table; alternatively, if you prefer not to persist state here,
add clear documentation on this function's partial-sync behavior and expected
caller compensation.
dialogs.go (1)

10-10: 💤 Low value

Captain's log: Documentation inconsistency detected.

The comment states "Handle uploads interactions," but this file's sole responsibility is file selection via dialog. The actual upload handling occurs in uploads.go. Recommend updating to: "Handle file selection dialog for uploads."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dialogs.go` at line 10, Update the top-of-file comment in dialogs.go to
accurately describe its responsibility: replace "Handle uploads interactions"
with "Handle file selection dialog for uploads" so it doesn't imply upload
processing (which happens in uploads.go); ensure any nearby comments or
package-level docstrings that reference uploads interactions are similarly
corrected to mention file selection/dialog only.
frontend/src/components/MainApp.svelte (1)

345-351: 💤 Low value

Orders: Remove debug artifacts before mission completion.

The comment markers <!-- FILES TEST --> (line 345) and <!-- EOFT--> (line 351) appear to be temporary development annotations. These should be removed or replaced with descriptive section comments before the pull request exits draft status.

🧹 Proposed cleanup
-        <!-- FILES TEST -->
         <UploadsPanel
           {profileUuid}
           entryId={currentEntryId}
           onAlert={(nextAlert) => alert = nextAlert}
         />
-        <!-- EOFT-->
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/MainApp.svelte` around lines 345 - 351, Remove the
temporary debug HTML comments around the UploadsPanel component: delete the
markers "<!-- FILES TEST -->" and "<!-- EOFT-->" (or replace them with a clear
descriptive comment) so the JSX/Svelte markup is clean; ensure the UploadsPanel
usage with props profileUuid, entryId={currentEntryId}, and onAlert={(nextAlert)
=> alert = nextAlert} remains unchanged and only the debug annotations are
removed or replaced with a concise, descriptive comment.
frontend/src/components/Uploads/UploadsPanel.svelte (2)

72-76: 💤 Low value

Note: File size formatting has GB/TB ceiling.

The formatFileSize helper handles bytes, KB, and MB ranges but will display all files ≥1GB as MB with potentially large decimal values (e.g., "1024.0 MB" instead of "1.0 GB"). For encrypted research data or media files, this edge case may surface.

🔧 Proposed enhancement
 function formatFileSize(bytes: number): string {
   if (bytes < 1024) return `${bytes} B`;
   if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
-  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
+  return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/Uploads/UploadsPanel.svelte` around lines 72 - 76,
The formatFileSize function currently only formats bytes, KB, and MB and will
show >=1GB values as large MB numbers; update formatFileSize in
UploadsPanel.svelte to handle GB and TB thresholds as well (e.g., bytes < 1024
-> B, < 1024^2 -> KB, < 1024^3 -> MB, < 1024^4 -> GB, else TB) and format each
branch with a suitable toFixed precision (e.g., one decimal) so values like
1024^3 bytes display as "1.0 GB" instead of "1024.0 MB".

41-59: ⚖️ Poor tradeoff

Recommendation: Consider transaction-like error handling in the import workflow.

The current implementation calls ImportUpload (which encrypts and stores the file) followed by AttachUploadToEntry (which creates the entry association). If the attach operation fails after a successful import, the encrypted file remains in storage but orphaned from the entry. While this doesn't corrupt data or prevent functionality, it leaves an unreferenced encrypted file on disk.

For improved consistency, consider either:

  1. Catching the attach failure and displaying guidance to retry attachment, or
  2. Documenting this behavior as acceptable (orphaned uploads can be cleaned up by a future maintenance operation)

The current behavior is functional and the user experience remains intact through error alerts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/Uploads/UploadsPanel.svelte` around lines 41 - 59,
The import workflow in importUpload calls ImportUpload then AttachUploadToEntry
which can leave an orphaned encrypted file if the attach step fails; update
importUpload to perform transaction-like cleanup: after a successful
ImportUpload, wrap the AttachUploadToEntry call in its own try/catch and on
attach failure call the upload-delete/cleanup API (e.g., DeleteUpload or
RemoveUpload with profileUuid and upload.id) or at minimum present a clear
retry/cleanup alert and surface the upload id so the user or a background job
can remove it; ensure you still call refreshUploads and preserve existing
onAlert error/success messages (referencing importUpload, ImportUpload,
AttachUploadToEntry, DeleteUpload/RemoveUpload, refreshUploads, onAlert).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@elabftw_client.go`:
- Line 216: elabftwRequestWithContentType builds the request URL by
concatenating elabftwAPIBaseURL(cfg.SiteURL) and path without normalizing, which
is inconsistent with elabftwRequest (which forces apiPath = "/" +
strings.TrimLeft(apiPath, "/")); fix by normalizing the path at the start of
elabftwRequestWithContentType (e.g., set path = "/" + strings.TrimLeft(path,
"/") or reuse elabftwRequest normalization logic) before calling http.NewRequest
so all callers produce well-formed URLs; update the code around the
http.NewRequest(...) invocation in elabftwRequestWithContentType accordingly.

In `@sync_push.go`:
- Around line 209-211: The current flows in patchExistingRemoteEntry and
postNewRemoteEntry call pushEntryUploadsToRemoteEntity after committing the
local DB changes (e.g., updating local2remote.modified_at or inserting
local2remote), which leaves the DB claiming success when uploads fail; fix by
moving upload synchronization earlier or decoupling it: either (A) call
pushEntryUploadsToRemoteEntity before you update/insert local2remote and only
commit the modified_at / insert if the push succeeds (wrap both push + DB update
in the same transaction/rollback path), or (B) add a separate tracking table
(e.g., entry_uploads_synced) and make
postNewRemoteEntry/patchExistingRemoteEntry insert/update the entry row first
but mark uploads as pending, then call pushEntryUploadsToRemoteEntity and
atomically update the entry_uploads_synced row on success; ensure
pushEntryUploadsToRemoteEntity is idempotent and that error returns cause no DB
state that falsely indicates uploads were synced.

In `@sync_uploads.go`:
- Around line 43-51: The code reads the entire encrypted file into memory via
os.ReadFile(encryptedPath) then calls decryptRawBytes(a.activeKey,
encryptedContent) without validating size; add a defensive size check before
reading by stat-ing encryptedPath (os.Stat) and rejecting files over a safe
threshold (e.g., configurable maxUploadDecryptSize) with a clear error, or
switch to streaming (open file, io.Reader) and feed a bounded reader to the
decryption routine; update the code paths that use encryptedPath, os.ReadFile
and decryptRawBytes to enforce this limit (or to accept an io.Reader) so large
or malicious files are rejected or processed in a streaming manner.

In `@uploads.go`:
- Around line 95-109: The INSERT into the uploads table (the Exec call inserting
realName, longName, storageName, hash, hashAlgorithm, filesize with state
"local") will violate the UNIQUE(hash, hash_algorithm) constraint for duplicate
files; change the logic to perform a conflict-safe insert (e.g., "INSERT OR
IGNORE" or "INSERT ... ON CONFLICT DO NOTHING") and then SELECT the existing row
when the insert was ignored so the function returns the existing upload record
instead of erroring; update the block that executes the INSERT (the Exec call
using realName, longName, storageName, hash, hashAlgorithm, filesize) to handle
conflict and fetch the existing row by hash/hash_algorithm when insert did not
create a new row.
- Around line 215-220: DetachUploadFromEntry is missing validation that entryID
and uploadID are > 0; add the same checks used in AttachUploadToEntry so the
function rejects non-positive IDs. Specifically, at the start of
DetachUploadFromEntry (before proceeding with requireUnlockedProfile or early
returned after it) validate entryID > 0 and uploadID > 0 and return the same
validation error/response used by AttachUploadToEntry when those checks fail to
keep behavior consistent.

---

Nitpick comments:
In `@dialogs.go`:
- Line 10: Update the top-of-file comment in dialogs.go to accurately describe
its responsibility: replace "Handle uploads interactions" with "Handle file
selection dialog for uploads" so it doesn't imply upload processing (which
happens in uploads.go); ensure any nearby comments or package-level docstrings
that reference uploads interactions are similarly corrected to mention file
selection/dialog only.

In `@frontend/src/components/MainApp.svelte`:
- Around line 345-351: Remove the temporary debug HTML comments around the
UploadsPanel component: delete the markers "<!-- FILES TEST -->" and "<!--
EOFT-->" (or replace them with a clear descriptive comment) so the JSX/Svelte
markup is clean; ensure the UploadsPanel usage with props profileUuid,
entryId={currentEntryId}, and onAlert={(nextAlert) => alert = nextAlert} remains
unchanged and only the debug annotations are removed or replaced with a concise,
descriptive comment.

In `@frontend/src/components/Uploads/UploadsPanel.svelte`:
- Around line 72-76: The formatFileSize function currently only formats bytes,
KB, and MB and will show >=1GB values as large MB numbers; update formatFileSize
in UploadsPanel.svelte to handle GB and TB thresholds as well (e.g., bytes <
1024 -> B, < 1024^2 -> KB, < 1024^3 -> MB, < 1024^4 -> GB, else TB) and format
each branch with a suitable toFixed precision (e.g., one decimal) so values like
1024^3 bytes display as "1.0 GB" instead of "1024.0 MB".
- Around line 41-59: The import workflow in importUpload calls ImportUpload then
AttachUploadToEntry which can leave an orphaned encrypted file if the attach
step fails; update importUpload to perform transaction-like cleanup: after a
successful ImportUpload, wrap the AttachUploadToEntry call in its own try/catch
and on attach failure call the upload-delete/cleanup API (e.g., DeleteUpload or
RemoveUpload with profileUuid and upload.id) or at minimum present a clear
retry/cleanup alert and surface the upload id so the user or a background job
can remove it; ensure you still call refreshUploads and preserve existing
onAlert error/success messages (referencing importUpload, ImportUpload,
AttachUploadToEntry, DeleteUpload/RemoveUpload, refreshUploads, onAlert).

In `@sync_links.go`:
- Around line 78-81: The call to elabftwOnlineURL that assigns link.URL lacks
contextual info on error; modify the error handling around
elabftwOnlineURL(link.SiteURL, link.Type, link.RemoteID) so that when it returns
an error you wrap or annotate it with the link identifiers (e.g., link.SiteURL,
link.Type, link.RemoteID or link.RemoteID) and a short message indicating which
remote link failed—use fmt.Errorf or errors.Wrap to return a new error that
includes the original error plus those identifiers where link.URL is being set.

In `@sync_push.go`:
- Around line 432-441: elabftwUploadEntityType duplicates the
"experiment"→"experiments" and "resource"→"items" mapping already implemented in
elabftwEntityPath; consolidate by centralizing the mapping in a single helper
used by both functions (for example, create/reuse a single map or helper
function like elabftwEntityBase that returns "experiments" or "items") and
update elabftwEntityPath to prefix the slash as needed and
elabftwUploadEntityType to call that helper (preserve same error behavior and
error message when unknown types are passed).

In `@sync_uploads.go`:
- Around line 115-119: The loop over uploads calling a.pushUploadToRemoteEntity
can leave a partially-synced state if one push fails; modify the code so each
successful push is recorded in a local-to-remote join (or tracking) table and
make pushUploadToRemoteEntity idempotent: before pushing, check the join table
for an existing mapping for upload.ID and remoteEntityID to skip duplicates,
after a successful push insert the mapping (e.g., record upload.ID ->
remoteUploadID), and ensure failures return without mutating the join table;
alternatively, if you prefer not to persist state here, add clear documentation
on this function's partial-sync behavior and expected caller compensation.

In `@uploads.go`:
- Around line 116-125: The returned StoredUpload struct is missing CreatedAt and
ModifiedAt values; update the insertion flow that constructs and returns
&StoredUpload{...} to populate CreatedAt and ModifiedAt (either by generating
timestamps in Go before inserting or by querying the inserted row from the DB to
read the auto-generated created_at/modified_at); specifically set the CreatedAt
and ModifiedAt fields on the returned StoredUpload (alongside ID, RealName,
LongName, StorageName, Hash, HashAlgorithm, Filesize, State) using the chosen
approach so the frontend receives real timestamps immediately after import.
- Around line 54-57: The current code reads the entire file into memory via
os.ReadFile(sourcePath) (the block creating content, err), which can OOM for
large files; replace it with a streaming approach: open the file with
os.Open(sourcePath), defer Close, create a hash.Hash (e.g., sha256.New()) and an
encrypting writer/stream (your existing encryption routine or a
cipher.StreamWriter), then use io.Copy or io.TeeReader to copy from the file
into the encrypting writer while updating the hash incrementally so you never
buffer the whole file in memory; update any uses of the in-memory content
variable to work with the streamed output (e.g., writing encrypted bytes to disk
or returning a reader).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 93bb6c4f-cf63-46d5-9f8f-9550cddc14d8

📥 Commits

Reviewing files that changed from the base of the PR and between 76c95f9 and 0e3e311.

📒 Files selected for processing (14)
  • crypto.go
  • dialogs.go
  • elabftw_client.go
  • frontend/src/components/MainApp.svelte
  • frontend/src/components/Uploads/UploadsPanel.svelte
  • frontend/wailsjs/go/main/App.d.ts
  • frontend/wailsjs/go/main/App.js
  • frontend/wailsjs/go/models.ts
  • sqlite.go
  • storage.go
  • sync_links.go
  • sync_push.go
  • sync_uploads.go
  • uploads.go

Comment thread elabftw_client.go Outdated
Comment thread sync_push.go Outdated
Comment thread sync_uploads.go
Comment thread uploads.go
Comment thread uploads.go Outdated
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/elabftw/desktop/issues/comments/4669105127","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/elabftw/desktop/pull/30?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: skip review by coderabbit.ai -->\n\n> [!IMPORTANT]\n> ## Review skipped\n> \n> Draft detected.\n> \n> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Organization UI\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro\n> \n> **Run ID**: `7ef1d419-c71b-4585-ab24-9e7fed372c90`\n> \n> </details>\n> \n> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.\n> \n> Use the checkbox below for a quick retry:\n> - [ ] <!-- {\"checkboxId\": \"e9bb8d72-00e8-4f67-9cb2-caf3b22574fe\"} --> 🔍 Trigger review\n\n<!-- end of auto-generated comment: skip review by coderabbit.ai -->\n\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nThis pull request implements encrypted file upload management for entries: users can import files from disk, attach/detach uploads to entries, and list associated uploads. Files are stored encrypted under XDG data directories with SHA-256 hash-based bucketing, and pushed to remote eLabFTW instances during entry synchronization.\n\n## Changes\n\n**Entry File Uploads with Encryption and Remote Sync**\n\n| Layer / File(s) | Summary |\n|---|---|\n| **Encryption, Storage, and Database Schema** <br> `crypto.go`, `storage.go`, `sqlite.go` | Encryption helpers now encapsulate XChaCha20-Poly1305 nonce/ciphertext construction. Storage paths use XDG data semantics with hash-bucket subdirectories for uploads. SQLite schema adds `uploads` table (hash, filesize, state, timestamps) and `entry_uploads` join table (composite key, `ON DELETE CASCADE`) with indexes. |\n| **Upload Management Core** <br> `uploads.go` | `StoredUpload` struct captures file metadata. `ImportUpload` reads source file, computes SHA-256, encrypts content, and inserts into profile database. `AttachUploadToEntry` and `DetachUploadFromEntry` manage join-table associations. `ListEntryUploads` queries metadata for an entry. All include validation and contextual error wrapping. |\n| **Remote Link Listing and Upload Push** <br> `sync_links.go`, `sync_uploads.go`, `sync_push.go`, `elabftw_client.go` | `EntryRemoteLink` struct and `ListEntryRemoteLinks` query remote links from local2remote join. `pushUploadToRemoteEntity` decrypts upload from disk and POSTs as multipart/form-data to remote. `pushEntryUploadsToRemoteEntity` batches uploads for an entry. `sync_push.go` hooks upload push into POST/PATCH entry operations. HTTP helper supports custom Content-Type for multipart requests. |\n| **Frontend Components and UI Integration** <br> `dialogs.go`, `frontend/src/components/MainApp.svelte`, `frontend/src/components/Uploads/UploadsPanel.svelte` | `SelectFile()` opens file dialog. `UploadsPanel` component renders upload list, import button (using file dialog), and per-upload remove button. `MainApp` tracks per-entry `remoteLinks` state, displays remote-link buttons in editor header, and embeds uploads panel in editor form with alert forwarding. |\n| **TypeScript/JavaScript API Bridge** <br> `frontend/wailsjs/go/main/App.d.ts`, `frontend/wailsjs/go/main/App.js`, `frontend/wailsjs/go/models.ts` | Auto-generated bindings export TypeScript function signatures and JavaScript wrapper functions for upload operations (Attach/Detach/Import/List/SelectFile). `EntryRemoteLink` and `StoredUpload` model classes deserialize from JSON with typed properties and `createFrom()` helpers. |\n\n---\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~60 minutes\n\n---\n\n## Possibly related PRs\n\n- [elabftw/desktop#7](https://github.com/elabftw/desktop/pull/7): Both PRs enhance the XChaCha20-Poly1305 encryption layer—this PR refactors the raw byte encrypt/decrypt helpers while the retrieved PR adds passphrase-derived encryption for entry metadata.\n\n- [elabftw/desktop#18](https://github.com/elabftw/desktop/pull/18): Both PRs extend the entry synchronization flow in `sync_push.go`—this PR adds upload push after POST/PATCH operations, building directly on the push/patch infrastructure from the retrieved PR.\n\n---\n\n## Suggested reviewers\n\n- NicolasCARPi\n\n---\n\n## Poem\n\n> 🚀 **Uploads Set Sail** 🚀  \n> Files now encrypted in our XDG bay,  \n> Join them to entries, then push far away,  \n> Hash-bucketed paths keep our secrets safe,  \n> Remote sync completes the feature's grace.  \n> ⚓️ *Engage the upload drive, Commander.* ⚓️\n\n</details>\n\n<!-- walkthrough_end -->\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 2 | ❌ 3</summary>\n\n### ❌ Failed checks (1 warning, 2 inconclusive)\n\n|         Check name         | Status         | Explanation                                                                                                                                                                                                                                                                                                                      | Resolution                                                                                                                                                     |\n| :------------------------: | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|     Docstring Coverage     | ⚠️ Warning     | Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%.                                                                                                                                                                                                                                             | Write docstrings for the functions missing them to satisfy the coverage threshold.                                                                             |\n|         Title check        | ❓ Inconclusive | The title '(wip) feat: uploads' is vague and generic, using the WIP prefix and 'uploads' without specifying what upload functionality is being implemented.                                                                                                                                                                      | Consider using a more descriptive title like 'Add file upload storage and encryption support' to clearly communicate the primary changes to reviewers.         |\n| Out of Scope Changes check | ❓ Inconclusive | The PR includes additional features beyond issue `#27` requirements: remote link viewing functionality [sync_links.go], upload syncing to eLabFTW [sync_uploads.go, sync_push.go], and UI components [UploadsPanel.svelte, MainApp.svelte]. While these complement the core upload feature, they extend beyond the explicit scope. | Clarify whether the remote link viewing and eLabFTW synchronization features are part of the intended scope or if they should be addressed in separate issues. |\n\n<details>\n<summary>✅ Passed checks (2 passed)</summary>\n\n|      Check name     | Status   | Explanation                                                                                                                                                                                                                                                                                                               |\n| :-----------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n|  Description Check  | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                                                                                                                                                               |\n| Linked Issues check | ✅ Passed | The pull request successfully implements all coding requirements from issue `#27`: files are stored in XDG_DATA_HOME [storage.go], encrypted using XChaCha20-Poly1305 [crypto.go], and metadata including hash, hash_algorithm, filesize, created_at, modified_at, state, real_name, and long_name is tracked [uploads.go]. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n<!-- tips_start -->\n\n---\n\n\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1,"signal":{}}},"response":{"url":"https://api.github.com/repos/elabftw/desktop/issues/comments/4669105127","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Wed, 10 Jun 2026 15:46:34 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"1441:CB219:4C5F146:12006605:6A2986D9","x-xss-protection":"0"},"data":{"message":"Requires authentication","documentation_url":"https://docs.github.com/rest","status":"401"}}}

@MoustaphaCamara MoustaphaCamara changed the title (wip) feat: uploads feat: uploads sync Jul 17, 2026
@MoustaphaCamara
MoustaphaCamara marked this pull request as ready for review July 17, 2026 14:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
uploads.go (2)

87-115: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Roll back metadata when encrypted-file persistence fails, Captain.

The row is committed before path creation and os.WriteFile. Either failure leaves a listed upload whose file does not exist. Use a transaction with rollback and remove the file if commit subsequently fails.

Suggested lifecycle
-	res, err := db.Exec(`
+	tx, err := db.Begin()
+	if err != nil {
+		return nil, fmt.Errorf("begin upload import: %w", err)
+	}
+	defer tx.Rollback()
+
+	res, err := tx.Exec(`
 		INSERT INTO uploads (
 			entry_id,
 			real_name,
 			long_name,
 			hash,
 			hash_algorithm,
 			filesize,
 			state
 		)
 		VALUES (?, ?, ?, ?, ?, ?, ?)
 	`, entryID, realName, longName, hash, hashAlgorithm, filesize, "local")
@@
 	if err := os.WriteFile(destPath, encryptedContent, 0o600); err != nil {
 		return nil, fmt.Errorf("write encrypted file: %w", err)
 	}
+
+	if err := tx.Commit(); err != nil {
+		_ = os.Remove(destPath)
+		return nil, fmt.Errorf("commit upload import: %w", err)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uploads.go` around lines 87 - 115, The upload metadata flow around the INSERT
and encrypted-file persistence must use a transaction: begin before inserting,
execute the INSERT through the transaction, and roll back on path creation or
os.WriteFile failure. Commit only after the file is successfully written, and
remove the created file if the final commit fails, preserving the existing error
context.

60-74: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Set the upload cap before os.ReadFile in uploads.go:60-71, Captain. A large import can still consume excessive memory, and the plaintext size needs headroom for encryption overhead; otherwise files that fit locally can become unsyncable once stored and hit maxUploadSize on push.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uploads.go` around lines 60 - 74, Update the upload flow before os.ReadFile
in the source-file handling path to validate the file size against maxUploadSize
with sufficient allowance for encryption overhead, rejecting oversized inputs
before reading them into memory. Preserve the existing hashing, metadata, and
encryption behavior for files within the adjusted limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sqlite.go`:
- Line 198: Update the error message returned by the schema migration block to
report version 4 instead of version 5, keeping the existing error wrapping and
migration behavior unchanged.
- Around line 35-39: Update the profile database lifecycle around the SQLite
handle initialization so all operations for the same profile reuse one
profile-scoped *sql.DB or otherwise serialize writes across handles, rather than
relying on SetMaxOpenConns(1) and SetMaxIdleConns(1) per handle. Preserve the
existing single-connection settings and add a bounded SQLite busy timeout only
if needed to handle remaining lock contention.

In `@sync_uploads.go`:
- Around line 65-85: Update the upload construction flow around decryptRawBytes
and multipart.NewWriter to avoid retaining encrypted content, plaintext, and a
complete multipart body simultaneously. Stream the multipart form through an
io.Pipe (or equivalent) while reading and decrypting incrementally, preserving
the existing upload field name and upload.RealName, and pass the streaming
reader to the HTTP request.
- Around line 188-205: Update the upload flow around rememberRemoteUpload so a
successful remote creation and its local mapping are persisted or reconciled
atomically enough for retries; when mapping persistence fails after the POST,
retain the remote upload identity and recover it on the next sync instead of
issuing another remote creation. Preserve the existing warning behavior only
after reconciliation cannot establish the mapping.
- Around line 51-63: Update ImportUpload in uploads.go to enforce the shared
plaintext user-file limit before reading or encrypting the source, while
retaining a separate encrypted-storage bound in sync_uploads.go:51-63 that
allows for AEAD overhead. Update sync_uploads.go:51-63 accordingly without
applying the plaintext limit to encrypted output. In
frontend/src/components/Uploads/UploadsPanel.svelte:76, advertise the shared
limit only after ImportUpload enforces it.

---

Outside diff comments:
In `@uploads.go`:
- Around line 87-115: The upload metadata flow around the INSERT and
encrypted-file persistence must use a transaction: begin before inserting,
execute the INSERT through the transaction, and roll back on path creation or
os.WriteFile failure. Commit only after the file is successfully written, and
remove the created file if the final commit fails, preserving the existing error
context.
- Around line 60-74: Update the upload flow before os.ReadFile in the
source-file handling path to validate the file size against maxUploadSize with
sufficient allowance for encryption overhead, rejecting oversized inputs before
reading them into memory. Preserve the existing hashing, metadata, and
encryption behavior for files within the adjusted limit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce7e117e-b3b3-46bb-9432-02335ee40b89

📥 Commits

Reviewing files that changed from the base of the PR and between 0e3e311 and c8c70c3.

📒 Files selected for processing (17)
  • crypto.go
  • elabftw_client.go
  • frontend/src/components/Instances/InstancesView.svelte
  • frontend/src/components/MainApp.svelte
  • frontend/src/components/Uploads/UploadsPanel.svelte
  • frontend/src/css/buttons.css
  • frontend/src/css/main.css
  • frontend/src/css/uploads.css
  • frontend/wailsjs/go/main/App.d.ts
  • frontend/wailsjs/go/main/App.js
  • frontend/wailsjs/go/models.ts
  • sqlite.go
  • storage.go
  • sync_links.go
  • sync_push.go
  • sync_uploads.go
  • uploads.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/components/MainApp.svelte
  • sync_links.go

Comment thread sqlite.go
Comment thread sqlite.go Outdated
Comment thread sync_uploads.go Outdated
Comment thread sync_uploads.go Outdated
Comment thread sync_uploads.go
previously, upload sync built the entire multipart/form-data request in a
 bytes.Buffer before sending it to eLabFTW.

For large uploads this meant keeping three copies of the data in memory
at the same time
- encrypted upload read from disk;
- decrypted plaintext;
- and the complete multipart request body.

Replace the in-memory multipart buffer with an io.Pipe so the multipart
body is generated and sent as the HTTP request is being transmitted.
This removes the additional multipart copy and significantly reduces
peak memory usage during upload synchronization.

The encrypted upload is still read and decrypted as a single payload,
since the current encrypted file format stores each upload as one
authenticated XChaCha20-Poly1305 message. True end-to-end streaming
would require a different on-disk encryption format and is outside the
scope of this change.

Also release the encrypted buffer after decryption and clear the
plaintext before returning from the function.
@MoustaphaCamara
MoustaphaCamara removed the request for review from NicolasCARPi July 24, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

add files

2 participants