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
|
||||
|
||||
@@ -416,6 +416,32 @@ gestionnaire ne s'exécutait pas (dont « Tap again to delete »). Une vraie
|
||||
détail reste dans le journal local.
|
||||
- **Nom par défaut `data.json`** au lieu de `vault-sync.json` — voir CLAUDE.md.
|
||||
|
||||
## WebDAV — 2026-10-02
|
||||
|
||||
Troisième transport, à côté de Gitea et GitHub, éprouvé contre le **WsgiDAV
|
||||
local du poste** (`E:\webdav.json`, `0.0.0.0:8080`) : lecture, 304 à
|
||||
l'ouverture, écriture 204, puis synchro du poste qui reprend la modification.
|
||||
|
||||
- **Reconnu au jeton, pas à l'URL** : une URL ni Gitea ni GitHub dont le jeton
|
||||
s'écrit `utilisateur:mot-de-passe` → `IsWebDav`, authentification Basic.
|
||||
Pas de champ « utilisateur » de plus dans l'écran : le jeton en tient lieu
|
||||
(sans `:`, c'est toujours l'ancien endpoint PHP, en Bearer).
|
||||
- **Le fichier tel quel**, le même que publie le poste : GET / PUT sans
|
||||
enveloppe base64. **L'ETag HTTP fait le sha** : `If-None-Match` à la lecture
|
||||
(304 = rien à faire), `If-Match` à l'écriture, **412 → `ssConflict`**, donc la
|
||||
même reprise relire-fusionner-republier que le 409/422 de Gitea. Sans ETag
|
||||
connu, pas de condition : le dernier écrit gagne, comme le poste.
|
||||
- **Une URL qui finit en `.json` n'est plus réécrite** par `normalizeRepoUrl`
|
||||
(elle en faisait une URL d'API Gitea) — sauf les formes longues d'un dépôt,
|
||||
`/src/branch/…` et `/blob/…`.
|
||||
- **Clair autorisé vers `192.168.1.28` seulement** (IP fixe du poste), dans
|
||||
`network_security_config.xml` : le mot de passe WebDAV passe en clair sur le
|
||||
Wi-Fi, le coffre reste chiffré. À retirer quand le serveur parlera HTTPS.
|
||||
Android ne sait pas autoriser « le réseau local » : chaque adresse se nomme.
|
||||
- **Côté poste** : pare-feu Windows (règle `WebDAV 8080`, `LocalSubnet`) **et**
|
||||
Portmaster (entrant autorisé pour Python) — sans le second, le port restait
|
||||
fermé malgré la règle.
|
||||
|
||||
## Saisie : ce qui a résisté
|
||||
|
||||
- **`user-select: none` du `body`** était hérité par les champs : le curseur ne
|
||||
|
||||
@@ -22,5 +22,9 @@
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<!-- ponytail: WebDAV local du poste (IP fixe), en clair sur le Wi-Fi.
|
||||
Le coffre reste chiffre ; le mot de passe WebDAV, lui, passe en
|
||||
clair. A retirer quand le serveur parlera HTTPS. -->
|
||||
<domain includeSubdomains="false">192.168.1.28</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
||||
@@ -528,7 +528,7 @@
|
||||
champ. Une adresse d'API Gitea en fait un jeton d'acces personnel
|
||||
envoye en "token" ; l'endpoint PHP en fait un "Bearer". Le
|
||||
nommer d'apres un seul des deux le rend faux pour l'autre. -->
|
||||
<input class="field-in" id="tok" placeholder="access token"
|
||||
<input class="field-in" id="tok" placeholder="access token (WebDAV: user:password)"
|
||||
type="password" autocomplete="off">
|
||||
<div class="in-x" data-clear="tok">×</div>
|
||||
</div>
|
||||
@@ -615,13 +615,13 @@
|
||||
entier. Un input d'une ligne ne defilait pas sous le doigt dans la
|
||||
WebView de l'app, et la fin d'une URL d'API restait inatteignable. -->
|
||||
<textarea class="field-in multi" id="sUrl" rows="1"
|
||||
placeholder="repository URL - Gitea or github.com/owner/repo (private)"
|
||||
placeholder="repository URL - Gitea, github.com/owner/repo (private), or WebDAV file URL"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="none"
|
||||
spellcheck="false" inputmode="url"></textarea>
|
||||
<div class="in-x" data-clear="sUrl">×</div>
|
||||
</div>
|
||||
<div class="in-wrap">
|
||||
<input class="field-in" id="sTok" placeholder="access token"
|
||||
<input class="field-in" id="sTok" placeholder="access token (WebDAV: user:password)"
|
||||
type="password" autocomplete="off">
|
||||
<div class="in-x" data-clear="sTok">×</div>
|
||||
</div>
|
||||
@@ -1903,6 +1903,9 @@ function normalizeRepoUrl(u) {
|
||||
u = u.replace(/\s+/g, '');
|
||||
if (!u || /\/api\/v1\/repos\//i.test(u) || /api\.github\.com\/repos\//i.test(u) ||
|
||||
/\.php(\?|$)/i.test(u)) return u;
|
||||
// WEBDAV : l'URL du fichier lui-meme, a garder telle quelle. Seules les
|
||||
// formes longues d'un depot (/src/branch/, /blob/) finissent aussi en .json.
|
||||
if (/\.json$/i.test(u) && !/\/(src\/branch|blob)\//i.test(u)) return u;
|
||||
// GITHUB : l'adresse du depot est sur github.com, l'API sur api.github.com.
|
||||
var g = u.match(/^https?:\/\/(www\.)?github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?(?:\/blob\/[^\/]+\/(.+?))?\/?$/i);
|
||||
if (g) return 'https://api.github.com/repos/' + g[2] + '/' + g[3] +
|
||||
|
||||
Reference in New Issue
Block a user