feat(sync): GitHub en plus de Gitea, et refus d'un depot public
GitHub parle la meme API des contenus (base64, sha, corps du PUT) : meme chemin, reconnu a api.github.com/repos/. User-Agent pose partout (GitHub refuse sans). Un depot public est refuse avant toute lecture ou ecriture, Gitea comme GitHub : l'historique garde tous les instantanes chiffres. Telephone : github.com/O/R se convertit en URL d'API et inversement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,7 @@ type
|
|||||||
FBaseUrl: string;
|
FBaseUrl: string;
|
||||||
FToken: string;
|
FToken: string;
|
||||||
FTimeoutMs: Integer;
|
FTimeoutMs: Integer;
|
||||||
|
FPrivacyChecked: Boolean;
|
||||||
public
|
public
|
||||||
constructor Create(const ABaseUrl, AToken: string);
|
constructor Create(const ABaseUrl, AToken: string);
|
||||||
|
|
||||||
@@ -73,6 +74,12 @@ type
|
|||||||
/// n'y ressemble pas est de toute facon inutilisable.
|
/// n'y ressemble pas est de toute facon inutilisable.
|
||||||
function IsGitea: Boolean;
|
function IsGitea: 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
|
||||||
|
/// est prive ou si on n'a pas pu le savoir. Verifie une fois par instance.
|
||||||
|
function PublicRepoWarning: string;
|
||||||
|
|
||||||
/// POURQUOI UN 404 OU UN 401, en clair pour l'utilisateur. Gitea repond 404
|
/// POURQUOI UN 404 OU UN 401, en clair pour l'utilisateur. Gitea repond 404
|
||||||
/// aussi bien pour un fichier absent que pour un depot ou un proprietaire
|
/// aussi bien pour un fichier absent que pour un depot ou un proprietaire
|
||||||
/// inexistant : deux requetes de plus tranchent - le depot, puis
|
/// inexistant : deux requetes de plus tranchent - le depot, puis
|
||||||
@@ -102,7 +109,55 @@ uses
|
|||||||
|
|
||||||
function TSyncClient.IsGitea: Boolean;
|
function TSyncClient.IsGitea: Boolean;
|
||||||
begin
|
begin
|
||||||
Result := FBaseUrl.ToLower.Contains('/api/v1/repos/');
|
// GitHub parle la meme API des contenus : meme chemin pour les deux.
|
||||||
|
Result := FBaseUrl.ToLower.Contains('/api/v1/repos/') or
|
||||||
|
FBaseUrl.ToLower.Contains('api.github.com/repos/');
|
||||||
|
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;
|
||||||
|
var
|
||||||
|
LP: Integer;
|
||||||
|
begin
|
||||||
|
LP := Pos('/contents/', AContentsUrl);
|
||||||
|
if LP > 0 then Result := Copy(AContentsUrl, 1, LP - 1) else Result := '';
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TSyncClient.PublicRepoWarning: string;
|
||||||
|
var
|
||||||
|
LHttp: THTTPClient;
|
||||||
|
LResp: IHTTPResponse;
|
||||||
|
LDoc: TJSONObject;
|
||||||
|
LPriv: Boolean;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
if FPrivacyChecked or not IsGitea or (RepoApiUrl(FBaseUrl) = '') then Exit;
|
||||||
|
FPrivacyChecked := True;
|
||||||
|
LHttp := THTTPClient.Create;
|
||||||
|
try
|
||||||
|
LHttp.ConnectionTimeout := FTimeoutMs;
|
||||||
|
LHttp.ResponseTimeout := FTimeoutMs;
|
||||||
|
LHttp.HandleRedirects := False;
|
||||||
|
LHttp.UserAgent := 'PMServer';
|
||||||
|
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
|
||||||
|
try
|
||||||
|
LResp := LHttp.Get(RepoApiUrl(FBaseUrl));
|
||||||
|
except
|
||||||
|
Exit; // doute reseau : pas de verdict, la vraie requete le dira
|
||||||
|
end;
|
||||||
|
if LResp.StatusCode <> 200 then Exit;
|
||||||
|
LDoc := TJSONObject.ParseJSONValue(LResp.ContentAsString(TEncoding.UTF8)) as TJSONObject;
|
||||||
|
if LDoc = nil then Exit;
|
||||||
|
try
|
||||||
|
if LDoc.TryGetValue<Boolean>('private', LPriv) and not LPriv then
|
||||||
|
Result := 'This repository is PUBLIC - make it private before syncing: anyone could download your encrypted vault and all its history.';
|
||||||
|
finally
|
||||||
|
LDoc.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
LHttp.Free;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TSyncClient.Diagnose(AHttpCode: Integer): string;
|
function TSyncClient.Diagnose(AHttpCode: Integer): string;
|
||||||
@@ -126,10 +181,12 @@ begin
|
|||||||
if AHttpCode = 401 then
|
if AHttpCode = 401 then
|
||||||
Exit('Token refused - expired, revoked or mistyped');
|
Exit('Token refused - expired, revoked or mistyped');
|
||||||
if (AHttpCode <> 404) or not IsGitea then Exit;
|
if (AHttpCode <> 404) or not IsGitea then Exit;
|
||||||
// https://hote/api/v1/repos/OWNER/REPO/contents/FICHIER
|
// https://hote/api/v1/repos/O/R/contents/F ou https://api.github.com/repos/O/R/...
|
||||||
LP := Pos('/api/v1/repos/', LowerCase(FBaseUrl));
|
// Tout ce qui precede "repos/" est la racine de l'API, pour les deux.
|
||||||
LApi := Copy(FBaseUrl, 1, LP + Length('/api/v1/') - 1);
|
LP := Pos('/repos/', LowerCase(FBaseUrl));
|
||||||
LParts := Copy(FBaseUrl, LP + Length('/api/v1/repos/'), MaxInt).Split(['/']);
|
if LP = 0 then Exit;
|
||||||
|
LApi := Copy(FBaseUrl, 1, LP);
|
||||||
|
LParts := Copy(FBaseUrl, LP + Length('/repos/'), MaxInt).Split(['/']);
|
||||||
if Length(LParts) < 2 then Exit;
|
if Length(LParts) < 2 then Exit;
|
||||||
LOwner := LParts[0];
|
LOwner := LParts[0];
|
||||||
LRepo := LParts[1];
|
LRepo := LParts[1];
|
||||||
@@ -138,6 +195,7 @@ begin
|
|||||||
LHttp.ConnectionTimeout := FTimeoutMs;
|
LHttp.ConnectionTimeout := FTimeoutMs;
|
||||||
LHttp.ResponseTimeout := FTimeoutMs;
|
LHttp.ResponseTimeout := FTimeoutMs;
|
||||||
LHttp.HandleRedirects := False;
|
LHttp.HandleRedirects := False;
|
||||||
|
LHttp.UserAgent := 'PMServer';
|
||||||
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
|
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
|
||||||
case CodeOf(LApi + 'repos/' + LOwner + '/' + LRepo) of
|
case CodeOf(LApi + 'repos/' + LOwner + '/' + LRepo) of
|
||||||
200: Result := 'No vault in this repository yet - sync from the PC first';
|
200: Result := 'No vault in this repository yet - sync from the PC first';
|
||||||
@@ -237,7 +295,7 @@ begin
|
|||||||
if not IsGitea then
|
if not IsGitea then
|
||||||
begin
|
begin
|
||||||
Result.Status := ssHttpError;
|
Result.Status := ssHttpError;
|
||||||
Result.Error := 'Ecriture disponible seulement vers un depot Gitea';
|
Result.Error := 'Ecriture disponible seulement vers un depot Gitea ou GitHub';
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
if (FBaseUrl = '') or (FToken = '') then
|
if (FBaseUrl = '') or (FToken = '') then
|
||||||
@@ -245,6 +303,12 @@ begin
|
|||||||
Result.Error := 'URL ou jeton non configure';
|
Result.Error := 'URL ou jeton non configure';
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
Result.Error := PublicRepoWarning;
|
||||||
|
if Result.Error <> '' then
|
||||||
|
begin
|
||||||
|
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
LEnv := TJSONObject.Create;
|
LEnv := TJSONObject.Create;
|
||||||
LB64 := TBase64Encoding.Create(0); // pas de coupure : voir PMA.Crypto
|
LB64 := TBase64Encoding.Create(0); // pas de coupure : voir PMA.Crypto
|
||||||
@@ -266,6 +330,7 @@ begin
|
|||||||
LHttp.ConnectionTimeout := FTimeoutMs;
|
LHttp.ConnectionTimeout := FTimeoutMs;
|
||||||
LHttp.ResponseTimeout := FTimeoutMs;
|
LHttp.ResponseTimeout := FTimeoutMs;
|
||||||
LHttp.HandleRedirects := False;
|
LHttp.HandleRedirects := False;
|
||||||
|
LHttp.UserAgent := 'PMServer'; // GitHub refuse toute requete sans
|
||||||
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
|
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
|
||||||
LHttp.CustomHeaders['Content-Type'] := 'application/json';
|
LHttp.CustomHeaders['Content-Type'] := 'application/json';
|
||||||
try
|
try
|
||||||
@@ -346,11 +411,18 @@ begin
|
|||||||
Result.Error := 'URL ou jeton non configure';
|
Result.Error := 'URL ou jeton non configure';
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
Result.Error := PublicRepoWarning;
|
||||||
|
if Result.Error <> '' then
|
||||||
|
begin
|
||||||
|
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
LHttp := THTTPClient.Create;
|
LHttp := THTTPClient.Create;
|
||||||
try
|
try
|
||||||
LHttp.ConnectionTimeout := FTimeoutMs;
|
LHttp.ConnectionTimeout := FTimeoutMs;
|
||||||
LHttp.ResponseTimeout := FTimeoutMs;
|
LHttp.ResponseTimeout := FTimeoutMs;
|
||||||
|
LHttp.UserAgent := 'PMServer'; // GitHub refuse toute requete sans
|
||||||
// Pas de redirection suivie : une redirection vers un autre hote nous
|
// Pas de redirection suivie : une redirection vers un autre hote nous
|
||||||
// ferait envoyer le jeton ailleurs.
|
// ferait envoyer le jeton ailleurs.
|
||||||
LHttp.HandleRedirects := False;
|
LHttp.HandleRedirects := False;
|
||||||
|
|||||||
@@ -574,7 +574,7 @@
|
|||||||
entier. Un input d'une ligne ne defilait pas sous le doigt dans la
|
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. -->
|
WebView de l'app, et la fin d'une URL d'API restait inatteignable. -->
|
||||||
<textarea class="field-in multi" id="sUrl" rows="1"
|
<textarea class="field-in multi" id="sUrl" rows="1"
|
||||||
placeholder="repository URL, e.g. https://git.example.com/owner/repo"
|
placeholder="repository URL - Gitea or github.com/owner/repo (private)"
|
||||||
autocomplete="off" autocorrect="off" autocapitalize="none"
|
autocomplete="off" autocorrect="off" autocapitalize="none"
|
||||||
spellcheck="false" inputmode="url"></textarea>
|
spellcheck="false" inputmode="url"></textarea>
|
||||||
<div class="in-x" data-clear="sUrl">×</div>
|
<div class="in-x" data-clear="sUrl">×</div>
|
||||||
@@ -1696,6 +1696,9 @@ document.addEventListener('input', function (ev) {
|
|||||||
|
|
||||||
// L'inverse, pour l'affichage : l'URL d'API se relit comme l'adresse du depot.
|
// L'inverse, pour l'affichage : l'URL d'API se relit comme l'adresse du depot.
|
||||||
function shortRepoUrl(u) {
|
function shortRepoUrl(u) {
|
||||||
|
var g = /^https:\/\/api\.github\.com\/repos\/([^\/]+)\/([^\/]+)\/contents\/(.+)$/i.exec(u || '');
|
||||||
|
if (g) return 'https://github.com/' + g[1] + '/' + g[2] +
|
||||||
|
(g[3] === 'vault-sync.json' ? '' : '/blob/main/' + g[3]);
|
||||||
var m = /^(https?:\/\/[^\/]+)\/api\/v1\/repos\/([^\/]+)\/([^\/]+)\/contents\/(.+)$/i.exec(u || '');
|
var m = /^(https?:\/\/[^\/]+)\/api\/v1\/repos\/([^\/]+)\/([^\/]+)\/contents\/(.+)$/i.exec(u || '');
|
||||||
if (!m) return u;
|
if (!m) return u;
|
||||||
return m[1] + '/' + m[2] + '/' + m[3] +
|
return m[1] + '/' + m[2] + '/' + m[3] +
|
||||||
@@ -1703,7 +1706,12 @@ function shortRepoUrl(u) {
|
|||||||
}
|
}
|
||||||
function normalizeRepoUrl(u) {
|
function normalizeRepoUrl(u) {
|
||||||
u = u.replace(/\s+/g, '');
|
u = u.replace(/\s+/g, '');
|
||||||
if (!u || /\/api\/v1\/repos\//i.test(u) || /\.php(\?|$)/i.test(u)) return u;
|
if (!u || /\/api\/v1\/repos\//i.test(u) || /api\.github\.com\/repos\//i.test(u) ||
|
||||||
|
/\.php(\?|$)/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] +
|
||||||
|
'/contents/' + (g[4] || 'vault-sync.json');
|
||||||
var m = u.match(/^(https?:\/\/[^\/]+)\/([^\/]+)\/([^\/]+?)(?:\.git)?(?:\/src\/branch\/[^\/]+\/(.+?))?\/?$/i);
|
var m = u.match(/^(https?:\/\/[^\/]+)\/([^\/]+)\/([^\/]+?)(?:\.git)?(?:\/src\/branch\/[^\/]+\/(.+?))?\/?$/i);
|
||||||
if (!m) return u;
|
if (!m) return u;
|
||||||
return m[1] + '/api/v1/repos/' + m[2] + '/' + m[3] + '/contents/' + (m[4] || 'vault-sync.json');
|
return m[1] + '/api/v1/repos/' + m[2] + '/' + m[3] + '/contents/' + (m[4] || 'vault-sync.json');
|
||||||
|
|||||||
@@ -1396,7 +1396,14 @@ begin
|
|||||||
// changeait chaque ":" et "/" en trois caracteres.
|
// changeait chaque ":" et "/" en trois caracteres.
|
||||||
var LShort := LQrUrl;
|
var LShort := LQrUrl;
|
||||||
var LApi := Pos('/api/v1/repos/', LShort);
|
var LApi := Pos('/api/v1/repos/', LShort);
|
||||||
if (LApi > 0) and EndsText('/contents/vault-sync.json', LShort) then
|
// GitHub : l'adresse du depot est sur github.com, l'API sur api.github.com.
|
||||||
|
if StartsText('https://api.github.com/repos/', LShort) and
|
||||||
|
EndsText('/contents/vault-sync.json', LShort) then
|
||||||
|
LShort := 'https://github.com/' +
|
||||||
|
Copy(LShort, Length('https://api.github.com/repos/') + 1,
|
||||||
|
Length(LShort) - Length('https://api.github.com/repos/') -
|
||||||
|
Length('/contents/vault-sync.json'))
|
||||||
|
else if (LApi > 0) and EndsText('/contents/vault-sync.json', LShort) then
|
||||||
LShort := Copy(LShort, 1, LApi) +
|
LShort := Copy(LShort, 1, LApi) +
|
||||||
Copy(LShort, LApi + Length('/api/v1/repos/'),
|
Copy(LShort, LApi + Length('/api/v1/repos/'),
|
||||||
Length(LShort) - LApi - Length('/api/v1/repos/') - Length('/contents/vault-sync.json') + 1);
|
Length(LShort) - LApi - Length('/api/v1/repos/') - Length('/contents/vault-sync.json') + 1);
|
||||||
@@ -1608,7 +1615,9 @@ begin
|
|||||||
// remplace l'authentification Basic, le corps d'un PUT est du JSON que JS
|
// 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
|
// 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.
|
// de connexion se fait par GET - l'API des contenus ne repond pas a HEAD.
|
||||||
var LGitea := LUrl.ToLower.Contains('/api/v1/repos/');
|
// GitHub parle la meme API des contenus : meme traitement.
|
||||||
|
var LGitea := LUrl.ToLower.Contains('/api/v1/repos/') or
|
||||||
|
LUrl.ToLower.Contains('api.github.com/repos/');
|
||||||
TThread.CreateAnonymousThread(
|
TThread.CreateAnonymousThread(
|
||||||
procedure
|
procedure
|
||||||
var
|
var
|
||||||
@@ -1631,6 +1640,8 @@ begin
|
|||||||
try
|
try
|
||||||
LHttp.ConnectionTimeout := 10000;
|
LHttp.ConnectionTimeout := 10000;
|
||||||
LHttp.ResponseTimeout := 30000;
|
LHttp.ResponseTimeout := 30000;
|
||||||
|
// GitHub REFUSE toute requete sans User-Agent (403) ; Gitea l'ignore.
|
||||||
|
LHttp.UserAgent := 'PMServer';
|
||||||
// "token <PAT>" et non "Bearer" : c'est la forme que Gitea attend
|
// "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.
|
// d'un jeton d'acces personnel, et l'autre donne un 401 muet.
|
||||||
if LGitea then
|
if LGitea then
|
||||||
|
|||||||
+28
-3
@@ -65,8 +65,8 @@ Bridge.onSyncQr = function (size, bits) {
|
|||||||
'background:#fff;display:block;border-radius:6px">' +
|
'background:#fff;display:block;border-radius:6px">' +
|
||||||
'<path d="' + d + '" fill="#000"/></svg>' +
|
'<path d="' + d + '" fill="#000"/></svg>' +
|
||||||
'<p style="font-size:11px;color:var(--text-faint);margin:6px 0 0">' +
|
'<p style="font-size:11px;color:var(--text-faint);margin:6px 0 0">' +
|
||||||
'Scan it with the phone camera and open it with Coffre: the URL and the ' +
|
'In the phone app: Settings → Scan the QR shown on the PC. The URL and ' +
|
||||||
'token fill in, then type the sync password (never in the QR) and Save. ' +
|
'the token fill in; type the sync password (never in the QR), then Save. ' +
|
||||||
'Hidden again in 60 s.</p>';
|
'Hidden again in 60 s.</p>';
|
||||||
box.hidden = false;
|
box.hidden = false;
|
||||||
clearTimeout(syncQrTimer);
|
clearTimeout(syncQrTimer);
|
||||||
@@ -83,8 +83,27 @@ Bridge.onWebdavResult = function(reqId, status, payload, etag) {
|
|||||||
// discriminant the Android client uses, so one URL tells both ends which
|
// discriminant the Android client uses, so one URL tells both ends which
|
||||||
// transport to speak. Everything below that differs is confined to the two
|
// transport to speak. Everything below that differs is confined to the two
|
||||||
// call sites in runSyncNow(); the bridge itself is shared.
|
// call sites in runSyncNow(); the bridge itself is shared.
|
||||||
|
// GitHub parle la MEME API des contenus (base64, sha, meme corps de PUT) : un
|
||||||
|
// seul chemin pour les deux, reconnu a l'URL.
|
||||||
function syncIsGitea(url) {
|
function syncIsGitea(url) {
|
||||||
return (url || '').toLowerCase().includes('/api/v1/repos/');
|
const u = (url || '').toLowerCase();
|
||||||
|
return u.includes('/api/v1/repos/') || u.includes('api.github.com/repos/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// UN DEPOT PUBLIC EST REFUSE. Chaque synchro est un commit : l'historique garde
|
||||||
|
// tous les instantanes, chacun attaquable hors ligne par qui peut les lire.
|
||||||
|
// '' = prive ou impossible a savoir (on ne bloque pas sur un doute reseau).
|
||||||
|
async function syncRepoPublicWarning(cfg) {
|
||||||
|
if (!syncIsGitea(cfg.url)) return '';
|
||||||
|
const repoUrl = cfg.url.split('/contents/')[0];
|
||||||
|
const r = await _webdavCall('get', repoUrl, cfg.user, cfg.pwd);
|
||||||
|
if (r.status < 200 || r.status >= 300 || !r.payload) return '';
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(new TextDecoder().decode(base64ToBytes(r.payload)));
|
||||||
|
if (o && o.private === false)
|
||||||
|
return 'This repository is PUBLIC - make it private before syncing: anyone could download your encrypted vault and all its history.';
|
||||||
|
} catch (e) { /* forme inattendue : pas de verdict */ }
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwraps a Gitea contents response into { container, sha }. The API answers
|
// Unwraps a Gitea contents response into { container, sha }. The API answers
|
||||||
@@ -278,6 +297,8 @@ async function syncTestConnection() {
|
|||||||
try {
|
try {
|
||||||
un = giteaUnwrap(new TextDecoder().decode(base64ToBytes(r.payload || '')));
|
un = giteaUnwrap(new TextDecoder().decode(base64ToBytes(r.payload || '')));
|
||||||
} catch (e) { /* traite comme une forme inattendue */ }
|
} catch (e) { /* traite comme une forme inattendue */ }
|
||||||
|
const pub = await syncRepoPublicWarning(cfg);
|
||||||
|
if (pub) return toast(pub, 'error');
|
||||||
return un
|
return un
|
||||||
? toast('Connection OK · file found in the repository')
|
? toast('Connection OK · file found in the repository')
|
||||||
: toast('Reached Gitea, but this URL does not return a file — ' +
|
: toast('Reached Gitea, but this URL does not return a file — ' +
|
||||||
@@ -634,6 +655,10 @@ async function runSyncNow(_attempt) {
|
|||||||
// and buildSyncSnapshot are expensive on large vaults — no point
|
// and buildSyncSnapshot are expensive on large vaults — no point
|
||||||
// running them if we can't reach the server).
|
// running them if we can't reach the server).
|
||||||
syncStatus('Connecting…');
|
syncStatus('Connecting…');
|
||||||
|
if (_attempt === 0) {
|
||||||
|
const pub = await syncRepoPublicWarning(cfg);
|
||||||
|
if (pub) { syncStatus(''); return toast(pub, 'error'); }
|
||||||
|
}
|
||||||
let pullResp;
|
let pullResp;
|
||||||
try {
|
try {
|
||||||
pullResp = await _webdavCall('get', cfg.url, cfg.user, cfg.pwd);
|
pullResp = await _webdavCall('get', cfg.url, cfg.user, cfg.pwd);
|
||||||
|
|||||||
+7
-1
@@ -2769,7 +2769,13 @@ begin
|
|||||||
// est affiche, hors ligne est un etat normal, et une alerte ici
|
// est affiche, hors ligne est un etat normal, et une alerte ici
|
||||||
// apprendrait a l'utilisateur a ignorer les alertes.
|
// apprendrait a l'utilisateur a ignorer les alertes.
|
||||||
Log(' failed: ' + LRes.Error);
|
Log(' failed: ' + LRes.Error);
|
||||||
if (not ASilent) and (LRes.HttpCode = 401) then
|
// Depot public : refus LOCAL (HttpCode 0), le message dit quoi faire.
|
||||||
|
if (not ASilent) and (LRes.Status = ssRefused) and (LRes.HttpCode = 0) then
|
||||||
|
begin
|
||||||
|
var LPub := LRes.Error;
|
||||||
|
TThread.Queue(nil, procedure begin ShowSettingsError(LPub); end);
|
||||||
|
end
|
||||||
|
else if (not ASilent) and (LRes.HttpCode = 401) then
|
||||||
begin
|
begin
|
||||||
var LWhy401 := LClient.Diagnose(401);
|
var LWhy401 := LClient.Diagnose(401);
|
||||||
TThread.Queue(nil, procedure begin ShowSettingsError(LWhy401); end);
|
TThread.Queue(nil, procedure begin ShowSettingsError(LWhy401); end);
|
||||||
|
|||||||
Reference in New Issue
Block a user