Fix(assets): attached files land in assets/, and uploads are no longer corrupted - #41
Merged
Merged
Conversation
…r corrupted Two bugs in the same button, one per vault kind. On a LOCAL vault, attaching a file wrote it to the vault ROOT, next to the notes, and linked it with a hand-built `'../'.repeat(depth)` path, so a note in a folder got ``. Paste already wrote to `assets/`, and so did the cloud vault, which is why it looked like paste and attach were different features (Discord, reported by xenin, whose `shape_image 18.jpg` was the seventeenth collision in their vault root). Desktop had the same bug and fixed it in ZenNotes#377; the port here was missed. Attach now writes to `assets/` and links by vault-relative path, an image as `![[assets/pic.jpg]]`, matching paste and the cloud vault. On a SELF-HOSTED vault, every uploaded attachment arrived corrupt (#40). The upload hand-built its multipart body as one string and marked the file part `Content-Transfer-Encoding: base64`. That header is a MIME construct: RFC 7578 dropped it from multipart/form-data and Go's mime/multipart ignores it, so the server wrote the base64 TEXT to disk as the file's bytes. The file arrived in `assets/` with the right name, a third larger than the original, and unreadable by anything. The bytes must stay base64 to cross the native bridge at all, since a CapacitorHttp string body is written out as UTF-8 and mangles anything above 0x7F. `dataType: 'formData'` is the supported way through: both native layers decode a `base64File` entry and write the raw bytes into the part. The sibling direct-object upload already used the same mechanism with `dataType: 'file'`. Verified against the real Go server by posting both body shapes: the old one stored 638352 bytes beginning `iVBORw0K`, the base64 text of the PNG, and the new one stored 478762 bytes with the same sha256 as the original. The pure rules move into `imported-assets.ts` so they can be unit-tested; `vault-core` cannot be loaded by `node --test` because it reaches `@shared/*` through a Vite alias, and it re-exports them so every existing importer is untouched. Claude-Session: https://claude.ai/code/session_01AYTRixg5TJmxn2j6FCqfUD
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two bugs in the same Add image / attach button, one per vault kind. Closes #40.
Local vault: the file went to the vault root
importDroppedFileasked for a unique name in'', the vault root, wrote the file there, and hand-built the link as'../'.repeat(depth) + filename, so a note in a folder got.Paste already wrote to
assets/, and so did the cloud vault, which is why it looked like paste and attach were different features. Reported on Discord by xenin, whoseshape_image 18.jpgwas the seventeenth name collision in their vault root.Desktop had this exact bug and fixed it in ZenNotes#377; the port here was missed.
Now:
assets/, linked by vault-relative path, an image as![[assets/pic.jpg]], matching paste and the cloud vault.Self-hosted vault: every upload arrived corrupt (#40)
The upload hand-built its multipart body as one string and marked the file part
Content-Transfer-Encoding: base64. That header is a MIME construct: RFC 7578 dropped it frommultipart/form-data, and Go'smime/multipartignores it. The server wrote the base64 text to disk as the file's bytes, so the attachment landed inassets/with the right name, about a third larger than the original, and unreadable by anything, which matches the report exactly, including that it opened as nothing straight from the Assets folder.The bytes must stay base64 to cross the native bridge, since a CapacitorHttp string body is written out as UTF-8 and mangles anything above 0x7F.
dataType: 'formData'is the supported route: both native layers decode abase64Fileentry and write the raw bytes into the part (AndroidCapacitorHttpUrlConnection.writeFormDataRequestBody, iOSCapacitorUrlRequest.getRequestDataFromFormData). The sibling direct-object upload already used the same mechanism withdataType: 'file'.Proven against the real Go server
Both body shapes posted to a running server with the same PNG:
iVBORw0K(base64 text)\x89PNGThe old file is exactly 4/3 the original, the signature of base64 inflation.
Verification
npm test: 28 passednpm run typecheck: passednpm run buildandnpm run sync: passedtestDebugUnitTest,lintDebug, andassembleDebugpassed (601 Gradle tasks)Files already sitting in a vault root do not move themselves, and attachments already uploaded corrupt stay corrupt and need re-adding.