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 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-15 15:05:08 +01:00
parent 7939d268f3
commit 7229028060
3 changed files with 71 additions and 7 deletions
+16 -7
View File
@@ -171,13 +171,22 @@ Par ordre de gravité, pas de taille.
Rangé par ce que ça apporte réellement sur un téléphone. 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 1. **Pièces jointes** — *listées*, pas encore enregistrables.
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 Une seule pièce jointe a fait passer le cache de **13 à 174 Ko** : le
fichier atterrit sur Android 10+ — le répertoire privé ne sert à rien à contenu voyage en base64 dans l'instantané, et il est déchiffré en mémoire
l'utilisateur, *Téléchargements* passe par `MediaStore` ou un partage par avec le reste. `TVault` lit donc nom, type et taille, et **jette
`Intent` — et en surveillant la mémoire, `content_b64` étant décodé en RAM `content_b64`** — le garder pour tout le coffre alors que rien ne l'affiche
avec le reste de l'instantané. 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, 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 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 de fond. **Pas de filtre d'étiquette** : la recherche texte couvre déjà les
+38
View File
@@ -23,6 +23,16 @@ type
Secret: Boolean; Secret: Boolean;
end; 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 TVaultEntry = record
Uuid, Title, Site, Username, Password, Folder, Tags, Kind, Totp: string; Uuid, Title, Site, Username, Password, Folder, Tags, Kind, Totp: string;
/// Sous-type (carte, cle SSH, serveur...). Vide pour un identifiant ou /// Sous-type (carte, cle SSH, serveur...). Vide pour un identifiant ou
@@ -34,6 +44,7 @@ type
/// fusion le jour ou le client ecrira. /// fusion le jour ou le client ecrira.
UpdatedAt: string; UpdatedAt: string;
Fields: TArray<TVaultField>; Fields: TArray<TVaultField>;
Attachments: TArray<TVaultAttachment>;
Deleted: Boolean; Deleted: Boolean;
/// Le corps d'une note EST son mot de passe cote stockage : il n'y a pas /// 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. /// de colonne separee. L'affichage doit donc l'intituler autrement.
@@ -160,6 +171,32 @@ begin
SetLength(Result, N); SetLength(Result, N);
end; end;
/// Pieces jointes : {filename, mime, size_bytes, content_b64}. On lit tout
/// SAUF content_b64 - voir TVaultAttachment.
function ParseAttachments(const AVal: TJSONValue): TArray<TVaultAttachment>;
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<string>('filename', '');
Result[N].Mime := LObj.GetValue<string>('mime', '');
Result[N].SizeBytes := LObj.GetValue<Int64>('size_bytes', 0);
if Result[N].FileName = '' then Continue;
Inc(N);
end;
SetLength(Result, N);
end;
procedure TVault.LoadFromJson(const APlainJson: string); procedure TVault.LoadFromJson(const APlainJson: string);
var var
LRoot, LObj: TJSONObject; LRoot, LObj: TJSONObject;
@@ -209,6 +246,7 @@ begin
FEntries[N].Totp := LObj.GetValue<string>('totp_secret', ''); FEntries[N].Totp := LObj.GetValue<string>('totp_secret', '');
FEntries[N].Tags := TagsToString(LObj.GetValue('tags')); FEntries[N].Tags := TagsToString(LObj.GetValue('tags'));
FEntries[N].Fields := ParseFields(LObj.GetValue('custom_fields')); FEntries[N].Fields := ParseFields(LObj.GetValue('custom_fields'));
FEntries[N].Attachments := ParseAttachments(LObj.GetValue('attachments'));
FEntries[N].Template := LObj.GetValue<string>('template', ''); FEntries[N].Template := LObj.GetValue<string>('template', '');
FEntries[N].IconB64 := LObj.GetValue<string>('icon_b64', ''); FEntries[N].IconB64 := LObj.GetValue<string>('icon_b64', '');
FEntries[N].UpdatedAt := LObj.GetValue<string>('updated_at', ''); FEntries[N].UpdatedAt := LObj.GetValue<string>('updated_at', '');
+17
View File
@@ -516,6 +516,16 @@ begin
OnSearchChanged(nil); OnSearchChanged(nil);
end; 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; procedure TForm8.ClearDetailRows;
var var
LRow: TLayout; LRow: TLayout;
@@ -573,6 +583,13 @@ begin
if LMeta <> '' then LMeta := LMeta + ' - '; if LMeta <> '' then LMeta := LMeta + ' - ';
LMeta := LMeta + FCurrent.Tags; LMeta := LMeta + FCurrent.Tags;
end; 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); Add('Dossier / etiquettes', LMeta, False);
// Le sous-type n'existe que pour les modeles (carte, cle SSH, serveur...) ; // Le sous-type n'existe que pour les modeles (carte, cle SSH, serveur...) ;
// Add ignore une valeur vide, donc rien a tester ici. // Add ignore une valeur vide, donc rien a tester ici.