From 72290280606a3ac47b5d0e29316221c03a082998 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:05:08 +0100 Subject: [PATCH] feat(android): show attachments, without carrying their bytes One attachment took the cache from 13 to 174 KB: the content travels base64 in the snapshot and is decrypted in memory with the rest. TVault reads name, type and size and DROPS content_b64 - holding it for the whole vault while nothing displays it would be paying for nothing. The detail now says a document exists. Saving it is what remains, and what blocks that is not code but where a file lands on Android 10+. When that is settled, the content re-reads from the raw JSON. Co-Authored-By: Claude Opus 5 --- android-client/PLAN.md | 23 +++++++++++++++------- android-client/PMA.Vault.pas | 38 ++++++++++++++++++++++++++++++++++++ mainApk.pas | 17 ++++++++++++++++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/android-client/PLAN.md b/android-client/PLAN.md index b2ad7d7..d02f780 100644 --- a/android-client/PLAN.md +++ b/android-client/PLAN.md @@ -171,13 +171,22 @@ Par ordre de gravité, pas de taille. Rangé par ce que ça apporte réellement sur un téléphone. -1. ~~**Pièces jointes**~~ — **abandonné** (2026-08-15) : le coffre n'en contient - pas. Elles restent dans l'instantané et le client les ignore, ce qui ne - coûte rien. À reprendre le jour où il y en aura, en tranchant d'abord où un - fichier atterrit sur Android 10+ — le répertoire privé ne sert à rien à - l'utilisateur, *Téléchargements* passe par `MediaStore` ou un partage par - `Intent` — et en surveillant la mémoire, `content_b64` étant décodé en RAM - avec le reste de l'instantané. +1. **Pièces jointes** — *listées*, pas encore enregistrables. + + Une seule pièce jointe a fait passer le cache de **13 à 174 Ko** : le + contenu voyage en base64 dans l'instantané, et il est déchiffré en mémoire + avec le reste. `TVault` lit donc nom, type et taille, et **jette + `content_b64`** — le garder pour tout le coffre alors que rien ne l'affiche + serait payer sans rien acheter. + + Le détail affiche l'existence du document. Ce qui manque est + l'enregistrement, et ce qui le bloque n'est pas le code mais **où un fichier + atterrit sur Android 10+** : le répertoire privé ne sert à rien à + l'utilisateur, *Téléchargements* passe par `MediaStore`, le partage par un + `Intent`. Le jour venu, le contenu se relira depuis le JSON brut. + + À surveiller : quelques PDF suffiraient à peser plusieurs Mo, retéléchargés + entièrement à chaque instantané nouveau. 2. ~~**Filtre dossier**~~ — **fait**, liste déroulante à côté de la recherche, alimentée par le tableau `folders`, et le choix survit à un rafraîchissement de fond. **Pas de filtre d'étiquette** : la recherche texte couvre déjà les diff --git a/android-client/PMA.Vault.pas b/android-client/PMA.Vault.pas index 28cbe97..86c06b9 100644 --- a/android-client/PMA.Vault.pas +++ b/android-client/PMA.Vault.pas @@ -23,6 +23,16 @@ type Secret: Boolean; end; + /// Metadonnees d'une piece jointe. LE CONTENU N'EST PAS RETENU : il voyage + /// en base64 dans l'instantane et pese lourd - une seule piece jointe a fait + /// passer le cache de 13 a 174 Ko. Le garder en memoire pour tout le coffre + /// alors que rien ne l'affiche serait payer sans rien acheter. Le jour ou + /// l'enregistrement existera, il se relira depuis le JSON brut. + TVaultAttachment = record + FileName, Mime: string; + SizeBytes: Int64; + end; + TVaultEntry = record Uuid, Title, Site, Username, Password, Folder, Tags, Kind, Totp: string; /// Sous-type (carte, cle SSH, serveur...). Vide pour un identifiant ou @@ -34,6 +44,7 @@ type /// fusion le jour ou le client ecrira. UpdatedAt: string; Fields: TArray; + Attachments: TArray; Deleted: Boolean; /// Le corps d'une note EST son mot de passe cote stockage : il n'y a pas /// de colonne separee. L'affichage doit donc l'intituler autrement. @@ -160,6 +171,32 @@ begin SetLength(Result, N); end; +/// Pieces jointes : {filename, mime, size_bytes, content_b64}. On lit tout +/// SAUF content_b64 - voir TVaultAttachment. +function ParseAttachments(const AVal: TJSONValue): TArray; +var + LArr: TJSONArray; + LObj: TJSONObject; + I, N: Integer; +begin + Result := nil; + if not (AVal is TJSONArray) then Exit; + LArr := AVal as TJSONArray; + SetLength(Result, LArr.Count); + N := 0; + for I := 0 to LArr.Count - 1 do + begin + if not (LArr.Items[I] is TJSONObject) then Continue; + LObj := LArr.Items[I] as TJSONObject; + Result[N].FileName := LObj.GetValue('filename', ''); + Result[N].Mime := LObj.GetValue('mime', ''); + Result[N].SizeBytes := LObj.GetValue('size_bytes', 0); + if Result[N].FileName = '' then Continue; + Inc(N); + end; + SetLength(Result, N); +end; + procedure TVault.LoadFromJson(const APlainJson: string); var LRoot, LObj: TJSONObject; @@ -209,6 +246,7 @@ begin FEntries[N].Totp := LObj.GetValue('totp_secret', ''); FEntries[N].Tags := TagsToString(LObj.GetValue('tags')); FEntries[N].Fields := ParseFields(LObj.GetValue('custom_fields')); + FEntries[N].Attachments := ParseAttachments(LObj.GetValue('attachments')); FEntries[N].Template := LObj.GetValue('template', ''); FEntries[N].IconB64 := LObj.GetValue('icon_b64', ''); FEntries[N].UpdatedAt := LObj.GetValue('updated_at', ''); diff --git a/mainApk.pas b/mainApk.pas index 0cd83f2..322f8e7 100644 --- a/mainApk.pas +++ b/mainApk.pas @@ -516,6 +516,16 @@ begin OnSearchChanged(nil); end; +function HumanSize(ABytes: Int64): string; +begin + if ABytes >= 1024 * 1024 then + Result := Format('%.1f Mo', [ABytes / (1024 * 1024)]) + else if ABytes >= 1024 then + Result := Format('%.0f Ko', [ABytes / 1024]) + else + Result := Format('%d o', [ABytes]); +end; + procedure TForm8.ClearDetailRows; var LRow: TLayout; @@ -573,6 +583,13 @@ begin if LMeta <> '' then LMeta := LMeta + ' - '; LMeta := LMeta + FCurrent.Tags; end; + // Les pieces jointes ne sont pas encore enregistrables sur le telephone, + // mais taire leur existence serait pire : on saurait sur le poste qu'une + // entree porte un document, et pas ici. + for I := 0 to High(FCurrent.Attachments) do + Add('Piece jointe', FCurrent.Attachments[I].FileName + ' ' + + HumanSize(FCurrent.Attachments[I].SizeBytes), False); + Add('Dossier / etiquettes', LMeta, False); // Le sous-type n'existe que pour les modeles (carte, cle SSH, serveur...) ; // Add ignore une valeur vide, donc rien a tester ici.