feat(desktop): sync to a private Gitea repo, chosen by the URL
Same discriminant as the Android client - "/api/v1/repos/" in the URL - so one address tells both ends which transport to speak, and nothing new appears in Settings beyond a placeholder and a warning. The bridge stays shared. Three things differ, and they are confined: the header is "token <PAT>" rather than Basic (Gitea answers a silent 401 to the other), a PUT carries a JSON envelope with the file base64-encoded inside it, and the connection test uses GET because the contents API does not answer HEAD. The sha does the ETag's whole job. It comes back from the pull, rides in the PUT body instead of an If-Match, and a stale one earns a 409 - handled exactly like WebDAV's 412, by re-running pull->merge->push so the other device's changes are folded in rather than clobbered. On a first push `sha` is OMITTED rather than sent empty: Gitea reads its presence as "replace that exact version". `branch` is omitted too, so the repo's default is used instead of a name we would be guessing. A failing PUT now reads its response body: Gitea explains a 409 or a 422 there, and without it the error was just a number. The test button checks the SHAPE of what came back, not only the status. A URL one path segment off answers 200 with valid JSON that is not a file - which would otherwise surface much later as "wrong sync password" and send you looking on the wrong side. Four tests cover that parsing, including the line-wrapped base64 Gitea sends and the array an accidental directory URL returns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1419,6 +1419,11 @@ begin
|
||||
// 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');
|
||||
// GITEA, reconnu a l'URL. Meme transport, trois differences : le jeton
|
||||
// remplace l'authentification Basic, le corps d'un PUT est du JSON que JS
|
||||
// a deja mis en forme (le sha y voyage, pas dans un If-Match), et le test
|
||||
// de connexion se fait par GET - l'API des contenus ne repond pas a HEAD.
|
||||
var LGitea := LUrl.ToLower.Contains('/api/v1/repos/');
|
||||
TThread.CreateAnonymousThread(
|
||||
procedure
|
||||
var
|
||||
@@ -1441,13 +1446,17 @@ begin
|
||||
try
|
||||
LHttp.ConnectionTimeout := 10000;
|
||||
LHttp.ResponseTimeout := 30000;
|
||||
if (LUser <> '') then
|
||||
// "token <PAT>" et non "Bearer" : c'est la forme que Gitea attend
|
||||
// d'un jeton d'acces personnel, et l'autre donne un 401 muet.
|
||||
if LGitea then
|
||||
LHttp.CustomHeaders['Authorization'] := 'token ' + LPwd
|
||||
else if (LUser <> '') then
|
||||
begin
|
||||
LHttp.CredentialsStorage.AddCredential(
|
||||
System.Net.URLClient.TCredentialsStorage.TCredential.Create(
|
||||
System.Net.URLClient.TAuthTargetType.Server, '', '', LUser, LPwd));
|
||||
end;
|
||||
if LMethod = 'webdav/get' then
|
||||
if (LMethod = 'webdav/get') or ((LMethod = 'webdav/test') and LGitea) then
|
||||
begin
|
||||
LBodyStream := TBytesStream.Create;
|
||||
try
|
||||
@@ -1470,15 +1479,31 @@ begin
|
||||
begin
|
||||
LBytes := TNetEncoding.Base64.DecodeStringToBytes(LData);
|
||||
LReqStream := TBytesStream.Create(LBytes);
|
||||
// La reponse est LUE meme en echec : Gitea explique un 409 ou un
|
||||
// 422 dans son corps, et sans cela le message d'erreur se
|
||||
// reduisait au code, ce qui ne dit pas quoi corriger.
|
||||
LBodyStream := TBytesStream.Create;
|
||||
try
|
||||
if LIfMatch <> '' then
|
||||
LResp := LHttp.Put(LUrl, LReqStream, nil,
|
||||
if LGitea then
|
||||
LResp := LHttp.Put(LUrl, LReqStream, LBodyStream,
|
||||
[System.Net.URLClient.TNetHeader.Create('Content-Type', 'application/json')])
|
||||
else if LIfMatch <> '' then
|
||||
LResp := LHttp.Put(LUrl, LReqStream, LBodyStream,
|
||||
[System.Net.URLClient.TNetHeader.Create('If-Match', LIfMatch)])
|
||||
else
|
||||
LResp := LHttp.Put(LUrl, LReqStream);
|
||||
LResp := LHttp.Put(LUrl, LReqStream, LBodyStream);
|
||||
LStatus := LResp.StatusCode;
|
||||
LEtag := LResp.HeaderValue['ETag'];
|
||||
if ((LStatus < 200) or (LStatus >= 300)) and (LBodyStream.Size > 0) then
|
||||
begin
|
||||
SetLength(LBytes, LBodyStream.Size);
|
||||
Move(LBodyStream.Bytes[0], LBytes[0], LBodyStream.Size);
|
||||
// Tronque : le corps d'une erreur tient en une phrase utile,
|
||||
// et le reste n'a rien a faire dans un appel JavaScript.
|
||||
LErr := Copy(TEncoding.UTF8.GetString(LBytes), 1, 300);
|
||||
end;
|
||||
finally
|
||||
LBodyStream.Free;
|
||||
LReqStream.Free;
|
||||
end;
|
||||
end
|
||||
@@ -1506,7 +1531,8 @@ begin
|
||||
// GET success path → ship body base64. Otherwise the field
|
||||
// carries either the empty string or the exception message
|
||||
// (for status=0 network errors).
|
||||
if (LMethod = 'webdav/get') and (LStatus >= 200) and (LStatus < 300) then
|
||||
if ((LMethod = 'webdav/get') or ((LMethod = 'webdav/test') and LGitea))
|
||||
and (LStatus >= 200) and (LStatus < 300) then
|
||||
LEscPayload := LBodyB64
|
||||
else
|
||||
LEscPayload := LErr;
|
||||
|
||||
Reference in New Issue
Block a user