feat: sync ETag concurrency + fix chunk-transfer hang + sync overlay + auto-VACUUM
Sync optimistic concurrency (ETag/If-Match) - webdav GET captures the response ETag; PUT sends it back as If-Match so the server rejects (412) our write when another device changed the file between our pull and push. A 412 re-runs the whole pull→merge→push (bounded to 3) so the other device's changes are folded in instead of clobbered. Servers without ETags → empty etag → no If-Match → falls back to last-write-wins (no regression). onWebdavResult gained a 4th etag arg. Chunked webdav PUT (big vaults no longer black-screen on sync) - The whole encrypted snapshot base64'd into a single cmd://webdav/put URL blew past WebView2's cap → black screen once the vault grew (20MB of attachments). PUT bodies now stream through the file/chunk transport and commit via a new webdav/put-commit (reads the accumulated buffer). Chunk-transfer hang fix (root cause of the stuck "Preparing…" sync) - All chunked transfers (saveFile/writeFile/webdav PUT) share one reqId-keyed resolver. A resolved chunk's stale 30s timeout would later delete the CURRENT chunk's resolver and fire the wrong res(), leaving that chunk's await pending forever. Extracted a single _streamChunks() helper whose ack CLEARS the pending timeout, so resolvers stay strictly one-at-a-time. Also fixed _webdavCall referencing the Bridge-local cmd() from module scope (latent ReferenceError). Sync busy overlay - syncStatus() now drives the global busy overlay too, so a running sync blocks stray clicks (e.g. the auto-backup "Choose…" picker) and reads like the manual backup. The account-mismatch confirm hideBusy()s first so it's visible above the overlay. Auto-VACUUM (reclaim space after deleting large attachments) - SQLite never shrinks the file on DELETE, so deleting big attachments left vault.db bloated (35MB for 11 tiny entries). DB.CompactIfBloated VACUUMs when >20% of pages are free AND >~2MB is reclaimable — called on startup and after each attachment delete. A healthy small vault pays nothing. (Verified: 35MB → 695KB after the deletes.) Rebuild: BuildAssets + F9 (UMainForm + PM.Database + PM.Handler.Attachments). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1101,7 +1101,8 @@ begin
|
||||
// - PUT ok → status=200/201/204, body=''
|
||||
// - test → status=200..399 means reachable, body=''
|
||||
// Network errors → status=0, body=exception message.
|
||||
else if (ACmd = 'webdav/get') or (ACmd = 'webdav/put') or (ACmd = 'webdav/test') then
|
||||
else if (ACmd = 'webdav/get') or (ACmd = 'webdav/put') or (ACmd = 'webdav/test')
|
||||
or (ACmd = 'webdav/put-commit') then
|
||||
begin
|
||||
var LMethod := ACmd;
|
||||
var LReqId := GetParam('reqId');
|
||||
@@ -1109,6 +1110,26 @@ begin
|
||||
var LUser := GetParam('user');
|
||||
var LPwd := GetParam('pwd');
|
||||
var LData := GetParam('data');
|
||||
// put-commit: the (large) body arrived in chunks via file/chunk,
|
||||
// accumulated in FFileSaveChunks keyed by reqId. Pull it out and treat
|
||||
// the rest as a normal PUT.
|
||||
if ACmd = 'webdav/put-commit' then
|
||||
begin
|
||||
var LSB: TStringBuilder;
|
||||
if FFileSaveChunks.TryGetValue(LReqId, LSB) then
|
||||
begin
|
||||
LData := LSB.ToString;
|
||||
LSB.Free;
|
||||
FFileSaveChunks.Remove(LReqId);
|
||||
end
|
||||
else
|
||||
LData := '';
|
||||
LMethod := 'webdav/put';
|
||||
end;
|
||||
// Optimistic concurrency: JS passes the ETag it saw at pull time; we
|
||||
// send it as If-Match on the push so the server rejects (412) the
|
||||
// write when another device changed the file in between.
|
||||
var LIfMatch := GetParam('ifmatch');
|
||||
TThread.CreateAnonymousThread(
|
||||
procedure
|
||||
var
|
||||
@@ -1120,10 +1141,12 @@ begin
|
||||
LBodyB64: string;
|
||||
LStatus: Integer;
|
||||
LErr: string;
|
||||
LEtag: string;
|
||||
begin
|
||||
LStatus := 0;
|
||||
LBodyB64 := '';
|
||||
LErr := '';
|
||||
LEtag := '';
|
||||
try
|
||||
LHttp := System.Net.HttpClient.THTTPClient.Create;
|
||||
try
|
||||
@@ -1141,6 +1164,7 @@ begin
|
||||
try
|
||||
LResp := LHttp.Get(LUrl, LBodyStream);
|
||||
LStatus := LResp.StatusCode;
|
||||
LEtag := LResp.HeaderValue['ETag'];
|
||||
if (LStatus >= 200) and (LStatus < 300) and (LBodyStream.Size > 0) then
|
||||
begin
|
||||
SetLength(LBytes, LBodyStream.Size);
|
||||
@@ -1158,8 +1182,13 @@ begin
|
||||
LBytes := TNetEncoding.Base64.DecodeStringToBytes(LData);
|
||||
LReqStream := TBytesStream.Create(LBytes);
|
||||
try
|
||||
LResp := LHttp.Put(LUrl, LReqStream);
|
||||
if LIfMatch <> '' then
|
||||
LResp := LHttp.Put(LUrl, LReqStream, nil,
|
||||
[System.Net.URLClient.TNetHeader.Create('If-Match', LIfMatch)])
|
||||
else
|
||||
LResp := LHttp.Put(LUrl, LReqStream);
|
||||
LStatus := LResp.StatusCode;
|
||||
LEtag := LResp.HeaderValue['ETag'];
|
||||
finally
|
||||
LReqStream.Free;
|
||||
end;
|
||||
@@ -1182,7 +1211,7 @@ begin
|
||||
TThread.Queue(nil,
|
||||
procedure
|
||||
var
|
||||
LEscReq, LEscPayload: string;
|
||||
LEscReq, LEscPayload, LEscEtag: string;
|
||||
begin
|
||||
LEscReq := StringReplace(LReqId, '"', '\"', [rfReplaceAll]);
|
||||
// GET success path → ship body base64. Otherwise the field
|
||||
@@ -1196,12 +1225,16 @@ begin
|
||||
LEscPayload := StringReplace(LEscPayload, '"', '\"', [rfReplaceAll]);
|
||||
LEscPayload := StringReplace(LEscPayload, #13, '', [rfReplaceAll]);
|
||||
LEscPayload := StringReplace(LEscPayload, #10, '\n', [rfReplaceAll]);
|
||||
LEscEtag := StringReplace(LEtag, '\', '\\', [rfReplaceAll]);
|
||||
LEscEtag := StringReplace(LEscEtag, '"', '\"', [rfReplaceAll]);
|
||||
LEscEtag := StringReplace(LEscEtag, #13, '', [rfReplaceAll]);
|
||||
LEscEtag := StringReplace(LEscEtag, #10, '', [rfReplaceAll]);
|
||||
WebBrowser.ExecuteJavaScript(
|
||||
'if(window.Bridge&&Bridge.onWebdavResult)' +
|
||||
'Bridge.onWebdavResult("' + LEscReq + '",' +
|
||||
IntToStr(LStatus) + ',"' + LEscPayload + '")');
|
||||
LogLine(Format('%s %s → %d (%d bytes payload)',
|
||||
[LMethod, LUrl, LStatus, Length(LEscPayload)]));
|
||||
IntToStr(LStatus) + ',"' + LEscPayload + '","' + LEscEtag + '")');
|
||||
LogLine(Format('%s %s → %d (%d bytes payload, etag=%s)',
|
||||
[LMethod, LUrl, LStatus, Length(LEscPayload), LEtag]));
|
||||
end);
|
||||
end).Start;
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user