feat(sync): support self-managed GitLab snippets

This commit is contained in:
ccq authored and GitHub committed 2026-09-20 17:30:23 +08:00
1 parent f909f85075
commit b9cd24f718
18 files changed
+734 -98

No files matched your search

@@ -3845,9 +3845,13 @@ const webdavError = ref(false);
const syncMethodTab = ref<"webdav" | "snippet">("webdav");
const snippetProvider = ref<SnippetProvider>((localStorage.getItem("dbx-snippet-provider") as SnippetProvider) || "github");
const snippetInstanceUrl = ref(localStorage.getItem("dbx-gitlab-instance-url") || "https://gitlab.com");
const activeSnippetInstanceUrl = ref(snippetInstanceUrl.value);
const snippetInstanceError = ref("");
const snippetPreferenceKey = () => `dbx-snippet-remember-token-${snippetProvider.value}${snippetProvider.value === "gitlab" ? `-${activeSnippetInstanceUrl.value}` : ""}`;
const snippetId = ref("");
const snippetToken = ref("");
const snippetRememberToken = ref(localStorage.getItem(`dbx-snippet-remember-token-${snippetProvider.value}`) === "true");
const snippetRememberToken = ref(localStorage.getItem(snippetPreferenceKey()) === "true");
const snippetHasSavedToken = ref(false);
const snippetPassphrase = ref("");
const snippetSecretsPassphrase = ref("");
@@ -3861,7 +3865,7 @@ const pendingLegacyCleanupId = ref("");
const snippetSyncSettingsLoading = ref(true);
const webdavReady = computed(() => !!webdavEndpoint.value.trim() && !webdavBusy.value && (!webdavSyncSecrets.value || !!webdavSecretsPassphrase.value.trim() || webdavHasSavedSecretsPassphrase.value));
const snippetReady = computed(() => !snippetSyncSettingsLoading.value && !snippetBusy.value && (!!snippetToken.value.trim() || snippetHasSavedToken.value));
const snippetReady = computed(() => !snippetSyncSettingsLoading.value && !snippetBusy.value && (snippetProvider.value !== "gitlab" || (!snippetInstanceError.value && snippetInstanceUrl.value === activeSnippetInstanceUrl.value)) && (!!snippetToken.value.trim() || snippetHasSavedToken.value));
const snippetUploadReady = computed(() => snippetReady.value && !!snippetPassphrase.value.trim() && (!snippetIncludeSecrets.value || !!snippetSecretsPassphrase.value.trim()));
// Legacy plaintext snippets have no outer encryption password. Let the
// backend require one only after it detects an encrypted envelope so those
@@ -3871,6 +3875,7 @@ const snippetDownloadReady = computed(() => snippetReady.value && (!snippetResto
function currentSnippetConfig(replaceLegacySnippet = false): SnippetSyncConfig {
return {
provider: snippetProvider.value,
instanceUrl: snippetProvider.value === "gitlab" ? activeSnippetInstanceUrl.value : undefined,
token: snippetToken.value.trim() || undefined,
snippetId: snippetId.value.trim() || undefined,
replaceLegacySnippet: replaceLegacySnippet || undefined,
@@ -3882,43 +3887,70 @@ function currentSnippetAccountConfig(): SnippetSyncConfig {
}
async function refreshSnippetTokenStatus() {
const account = `${snippetProvider.value}:${activeSnippetInstanceUrl.value}`;
try {
const status = await snippetTokenStatus(currentSnippetAccountConfig());
if (account !== `${snippetProvider.value}:${activeSnippetInstanceUrl.value}`) return;
snippetHasSavedToken.value = status.hasSavedToken;
if (status.hasSavedToken) snippetRememberToken.value = true;
} catch {
snippetHasSavedToken.value = false;
if (account === `${snippetProvider.value}:${activeSnippetInstanceUrl.value}`) snippetHasSavedToken.value = false;
}
}
async function refreshSnippetSyncSettings(provider = snippetProvider.value) {
async function refreshSnippetSyncSettings(provider = snippetProvider.value, instanceUrl = activeSnippetInstanceUrl.value) {
const isCurrent = () => provider === snippetProvider.value && instanceUrl === activeSnippetInstanceUrl.value;
try {
const settings = await snippetSyncSettings(provider);
if (provider !== snippetProvider.value) return;
const settings = await snippetSyncSettings(provider, provider === "gitlab" ? instanceUrl : undefined);
if (!isCurrent()) return;
pendingLegacyCleanupId.value = settings.legacyCleanupRequiredId || "";
if (settings.snippetId) {
snippetId.value = settings.snippetId;
return;
}
const legacyId = localStorage.getItem(`dbx-snippet-id-${provider}`)?.trim();
const legacyId = provider === "gitlab" ? undefined : localStorage.getItem(`dbx-snippet-id-${provider}`)?.trim();
if (legacyId) {
await saveSnippetSyncId(provider, legacyId);
localStorage.removeItem(`dbx-snippet-id-${provider}`);
}
if (provider !== snippetProvider.value) return;
if (!isCurrent()) return;
snippetId.value = legacyId || "";
} catch {
if (provider === snippetProvider.value) {
if (isCurrent()) {
snippetId.value = "";
pendingLegacyCleanupId.value = "";
}
} finally {
if (provider === snippetProvider.value) snippetSyncSettingsLoading.value = false;
if (isCurrent()) snippetSyncSettingsLoading.value = false;
}
}
async function persistSnippetSyncId() {
await saveSnippetSyncId(snippetProvider.value, snippetId.value.trim() || undefined);
await saveSnippetSyncId(snippetProvider.value, snippetId.value.trim() || undefined, snippetProvider.value === "gitlab" ? activeSnippetInstanceUrl.value : undefined);
}
function commitSnippetInstance() {
try {
const url = new URL(snippetInstanceUrl.value.trim());
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error();
snippetInstanceUrl.value = url.href.replace(/\/$/, "");
snippetInstanceError.value = "";
} catch {
snippetInstanceError.value = t("settings.syncGitLabInstanceInvalid");
return;
}
if (snippetInstanceUrl.value === activeSnippetInstanceUrl.value) return;
activeSnippetInstanceUrl.value = snippetInstanceUrl.value;
localStorage.setItem("dbx-gitlab-instance-url", activeSnippetInstanceUrl.value);
snippetRememberToken.value = localStorage.getItem(snippetPreferenceKey()) === "true";
snippetToken.value = "";
snippetHasSavedToken.value = false;
snippetId.value = "";
legacySnippetId.value = "";
pendingLegacyCleanupId.value = "";
snippetSyncSettingsLoading.value = true;
void refreshSnippetTokenStatus();
void refreshSnippetSyncSettings();
}
async function applySnippetTokenPreference() {
@@ -3940,7 +3972,7 @@ async function runSnippetAction(kind: "test" | "upload" | "download" | "migrate"
snippetError.value = false;
try {
localStorage.setItem("dbx-snippet-provider", snippetProvider.value);
localStorage.setItem(`dbx-snippet-remember-token-${snippetProvider.value}`, String(snippetRememberToken.value));
localStorage.setItem(snippetPreferenceKey(), String(snippetRememberToken.value));
if (persistCurrentSnippetId) await persistSnippetSyncId();
await applySnippetTokenPreference();
snippetMessage.value = await action();
@@ -4325,8 +4357,9 @@ watch([webdavAutoUploadEnabled, webdavAutoUploadIntervalMinutes], () => {
watch(snippetProvider, (provider) => {
localStorage.setItem("dbx-snippet-provider", provider);
snippetId.value = "";
snippetRememberToken.value = localStorage.getItem(`dbx-snippet-remember-token-${provider}`) === "true";
snippetRememberToken.value = localStorage.getItem(snippetPreferenceKey()) === "true";
snippetToken.value = "";
snippetHasSavedToken.value = false;
legacySnippetId.value = "";
pendingLegacyCleanupId.value = "";
snippetSyncSettingsLoading.value = true;
@@ -8456,7 +8489,7 @@ LIMIT 100;</pre
<Tabs v-model="syncMethodTab" class="w-full">
<TabsList v-if="!isWeb" class="grid w-full grid-cols-2">
<TabsTrigger value="webdav">WebDAV</TabsTrigger>
<TabsTrigger value="snippet">GitHub / Gitee</TabsTrigger>
<TabsTrigger value="snippet">{{ t("settings.syncSnippetTitle") }}</TabsTrigger>
</TabsList>
<TabsContent value="webdav" data-settings-search-id="sync-webdav" :class="['mt-5 space-y-5', settingsSearchTargetClass('sync-webdav')]">
@@ -8583,9 +8616,16 @@ LIMIT 100;</pre
<SelectContent>
<SelectItem value="github">GitHub Gist</SelectItem>
<SelectItem value="gitee">{{ t("settings.syncSnippetProviderGitee") }}</SelectItem>
<SelectItem value="gitlab">GitLab Snippets</SelectItem>
</SelectContent>
</Select>
</div>
<div v-if="snippetProvider === 'gitlab'" class="space-y-2 md:col-span-2">
<Label for="gitlab-instance-url">{{ t("settings.syncGitLabInstance") }}</Label>
<Input id="gitlab-instance-url" v-model="snippetInstanceUrl" type="url" autocomplete="url" :disabled="!!snippetBusy" placeholder="https://gitlab.example.com" @blur="commitSnippetInstance" @keydown.enter="commitSnippetInstance" />
<p v-if="snippetInstanceError" class="text-xs text-destructive">{{ snippetInstanceError }}</p>
<p v-if="activeSnippetInstanceUrl.startsWith('http://')" class="text-xs text-destructive">{{ t("settings.syncGitLabHttpWarning") }}</p>
</div>
<div class="space-y-2">
<Label for="snippet-sync-id">{{ t("settings.syncSnippetId") }}</Label>
<Input id="snippet-sync-id" v-model="snippetId" autocomplete="off" :disabled="snippetSyncSettingsLoading || !!snippetBusy" :placeholder="t('settings.syncSnippetIdPlaceholder')" @blur="persistSnippetSyncId" />