feat(sync): WebDAV sur l'APK, et le QR du poste qui le porte
Jeton "utilisateur:mot-de-passe" sur une URL ni Gitea ni GitHub = WebDAV, authentification Basic. Fichier lu et ecrit tel quel ; l'ETag fait le sha (If-None-Match, If-Match, 412 -> conflit). Clair autorise vers l'IP fixe du poste seulement. Eprouve contre le WsgiDAV local : 304, PUT 204, puis synchro du poste. Docs : CLAUDE.md, android-client/README.md (page eveillee comprise). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,12 @@ type
|
||||
/// n'y ressemble pas est de toute facon inutilisable.
|
||||
function IsGitea: Boolean;
|
||||
|
||||
/// WEBDAV : toute autre URL dont le "jeton" s'ecrit utilisateur:mot-de-passe.
|
||||
/// Le fichier se lit et s'ecrit tel quel (le meme que publie le poste), et
|
||||
/// l'ETag HTTP joue le role du sha : If-None-Match a la lecture, If-Match a
|
||||
/// l'ecriture (412 = un autre appareil a publie entre-temps).
|
||||
function IsWebDav: Boolean;
|
||||
|
||||
/// UN DEPOT PUBLIC EST REFUSE (Gitea comme GitHub) : chaque synchro est un
|
||||
/// commit, et l'historique garde tous les instantanes, attaquables hors
|
||||
/// ligne par quiconque peut les lire. Message a afficher, ou '' si le depot
|
||||
@@ -116,6 +122,81 @@ begin
|
||||
FBaseUrl.ToLower.Contains('api.github.com/repos/');
|
||||
end;
|
||||
|
||||
function TSyncClient.IsWebDav: Boolean;
|
||||
begin
|
||||
Result := (not IsGitea) and FToken.Contains(':');
|
||||
end;
|
||||
|
||||
/// Basic sur WebDAV, Bearer pour l'endpoint PHP historique.
|
||||
function PlainAuthHeader(const AToken: string; AWebDav: Boolean): string;
|
||||
begin
|
||||
if AWebDav then
|
||||
Result := 'Basic ' + TNetEncoding.Base64.Encode(AToken).Replace(#13, '').Replace(#10, '')
|
||||
else
|
||||
Result := 'Bearer ' + AToken;
|
||||
end;
|
||||
|
||||
function PutWebDav(const AUrl, AToken, ABody, AETag: string;
|
||||
AConnectMs, ATimeoutMs: Integer): TSyncResult;
|
||||
var
|
||||
LHttp: THTTPClient;
|
||||
LResp: IHTTPResponse;
|
||||
LReq, LOut: TStringStream;
|
||||
begin
|
||||
Result := Default(TSyncResult);
|
||||
Result.Status := ssNetworkError;
|
||||
LHttp := THTTPClient.Create;
|
||||
LReq := TStringStream.Create(ABody, TEncoding.UTF8);
|
||||
LOut := TStringStream.Create('', TEncoding.UTF8);
|
||||
try
|
||||
LHttp.ConnectionTimeout := AConnectMs;
|
||||
LHttp.ResponseTimeout := ATimeoutMs;
|
||||
LHttp.HandleRedirects := False;
|
||||
LHttp.UserAgent := 'PMServer';
|
||||
LHttp.CustomHeaders['Connection'] := 'close';
|
||||
LHttp.CustomHeaders['Authorization'] := PlainAuthHeader(AToken, True);
|
||||
LHttp.CustomHeaders['Content-Type'] := 'application/json';
|
||||
// Sans ETag connu (premiere publication, serveur qui n'en rend pas) : pas
|
||||
// de condition, le dernier ecrit gagne - comme le poste dans ce cas.
|
||||
if AETag <> '' then
|
||||
LHttp.CustomHeaders['If-Match'] := AETag;
|
||||
LResp := nil;
|
||||
for var LTry := 1 to 3 do
|
||||
begin
|
||||
try
|
||||
LReq.Position := 0;
|
||||
LOut.Clear;
|
||||
LResp := LHttp.Put(AUrl, LReq, LOut);
|
||||
Break;
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
Result.Error := E.ClassName + ': ' + E.Message;
|
||||
if LTry = 3 then Exit;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
if LResp = nil then Exit;
|
||||
Result.HttpCode := LResp.StatusCode;
|
||||
case LResp.StatusCode of
|
||||
200, 201, 204:
|
||||
begin
|
||||
Result.Status := ssOK;
|
||||
Result.ETag := LResp.HeaderValue['ETag'];
|
||||
end;
|
||||
412: Result.Status := ssConflict;
|
||||
else
|
||||
Result.Status := ssHttpError;
|
||||
Result.Error := Format('HTTP %d %s - %s', [LResp.StatusCode,
|
||||
LResp.StatusText, Copy(LOut.DataString, 1, 300)]);
|
||||
end;
|
||||
finally
|
||||
LOut.Free;
|
||||
LReq.Free;
|
||||
LHttp.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
/// URL de l'API du depot (sans /contents/...), identique pour Gitea
|
||||
/// (https://hote/api/v1/repos/O/R) et GitHub (https://api.github.com/repos/O/R).
|
||||
function RepoApiUrl(const AContentsUrl: string): string;
|
||||
@@ -295,17 +376,19 @@ begin
|
||||
Result := Default(TSyncResult);
|
||||
Result.Status := ssNetworkError;
|
||||
|
||||
if not IsGitea then
|
||||
begin
|
||||
Result.Status := ssHttpError;
|
||||
Result.Error := 'Ecriture disponible seulement vers un depot Gitea ou GitHub';
|
||||
Exit;
|
||||
end;
|
||||
if (FBaseUrl = '') or (FToken = '') then
|
||||
begin
|
||||
Result.Error := 'URL ou jeton non configure';
|
||||
Exit;
|
||||
end;
|
||||
if IsWebDav then
|
||||
Exit(PutWebDav(FBaseUrl, FToken, AContainerJson, ASha, FConnectMs, FTimeoutMs));
|
||||
if not IsGitea then
|
||||
begin
|
||||
Result.Status := ssHttpError;
|
||||
Result.Error := 'Ecriture disponible seulement vers Gitea, GitHub ou WebDAV (jeton utilisateur:mot-de-passe)';
|
||||
Exit;
|
||||
end;
|
||||
Result.Error := PublicRepoWarning;
|
||||
if Result.Error <> '' then
|
||||
begin
|
||||
@@ -466,7 +549,7 @@ begin
|
||||
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken
|
||||
else
|
||||
begin
|
||||
LHttp.CustomHeaders['Authorization'] := 'Bearer ' + FToken;
|
||||
LHttp.CustomHeaders['Authorization'] := PlainAuthHeader(FToken, IsWebDav);
|
||||
// Pas sur Gitea : le sha n'est pas un ETag HTTP, et l'envoyer ferait
|
||||
// repondre 200 avec un corps complet de toute facon.
|
||||
if AKnownETag <> '' then
|
||||
|
||||
Reference in New Issue
Block a user