mirror of
https://github.com/hesprs/obsidian-webdav-sync.git
synced 2026-07-22 14:30:27 +00:00
feat(settings): Add custom headers option (#158)
* feat(settings): Add custom headers option * add customHeaders to fs/webdav/api * eslint to oxlint
This commit is contained in:
parent
550d44f50c
commit
e2cdca9da0
14 changed files with 108 additions and 7 deletions
|
|
@ -27,6 +27,8 @@ export default class SelectRemoteBaseDirModal extends Modal {
|
|||
this.plugin.settings.serverUrl,
|
||||
token,
|
||||
target,
|
||||
false,
|
||||
this.plugin.settings.customHeaders,
|
||||
);
|
||||
return items.map((stat) => ({
|
||||
basename: remoteBasename(stat.path),
|
||||
|
|
|
|||
|
|
@ -136,11 +136,13 @@ const PROPFIND_BODY = `<?xml version="1.0" encoding="utf-8"?>
|
|||
</prop>
|
||||
</propfind>`;
|
||||
|
||||
// oxlint-disable-next-line max-params
|
||||
async function propfind(
|
||||
endpoint: string,
|
||||
token: string,
|
||||
url: string,
|
||||
depth: '0' | '1' | 'infinity',
|
||||
customHeaders: Record<string, string> = {},
|
||||
) {
|
||||
let retries = 0;
|
||||
while (true)
|
||||
|
|
@ -148,6 +150,7 @@ async function propfind(
|
|||
const response = await requestUrl({
|
||||
body: PROPFIND_BODY,
|
||||
headers: {
|
||||
...customHeaders,
|
||||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/xml',
|
||||
Depth: depth,
|
||||
|
|
@ -179,12 +182,18 @@ async function propfind(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getStat(endpoint: string, token: string, path: string): Promise<StatModel> {
|
||||
export async function getStat(
|
||||
endpoint: string,
|
||||
token: string,
|
||||
path: string,
|
||||
customHeaders: Record<string, string> = {},
|
||||
): Promise<StatModel> {
|
||||
const { items, stripPrefixes } = await propfind(
|
||||
endpoint,
|
||||
token,
|
||||
buildItemUrl(endpoint, path),
|
||||
'0',
|
||||
customHeaders,
|
||||
);
|
||||
const normalizedTargetPath = normalizeRemotePath(path);
|
||||
|
||||
|
|
@ -197,11 +206,13 @@ export async function getStat(endpoint: string, token: string, path: string): Pr
|
|||
throw new Error(`WebDAV stat not found for ${path}`);
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line max-params
|
||||
export async function getDirectoryContents(
|
||||
endpoint: string,
|
||||
token: string,
|
||||
path: string,
|
||||
infinity = false,
|
||||
customHeaders: Record<string, string> = {},
|
||||
): Promise<Array<StatModel>> {
|
||||
const contents: Array<StatModel> = [];
|
||||
let currentUrl = buildDirectoryUrl(endpoint, path);
|
||||
|
|
@ -214,6 +225,7 @@ export async function getDirectoryContents(
|
|||
token,
|
||||
currentUrl,
|
||||
infinity ? 'infinity' : '1',
|
||||
customHeaders,
|
||||
);
|
||||
|
||||
const parsedItems = items
|
||||
|
|
|
|||
|
|
@ -28,13 +28,25 @@ export default async function traverse({
|
|||
token,
|
||||
throwIfCancelled,
|
||||
}: TraverseWebDAVOptions) {
|
||||
const { filterRules, skipLargeFiles, serverUrl, remoteDir, exhaustiveRemoteTraversal } =
|
||||
await useSettings();
|
||||
const {
|
||||
filterRules,
|
||||
skipLargeFiles,
|
||||
serverUrl,
|
||||
remoteDir,
|
||||
exhaustiveRemoteTraversal,
|
||||
customHeaders,
|
||||
} = await useSettings();
|
||||
const encrypted = (await useSettings()).encryption.enabled;
|
||||
const result: StatsMap = new Map();
|
||||
|
||||
const getContentFunc = (path: string) =>
|
||||
apiLimiter.wrap(getDirectoryContents)(serverUrl, token, path, exhaustiveRemoteTraversal);
|
||||
apiLimiter.wrap(getDirectoryContents)(
|
||||
serverUrl,
|
||||
token,
|
||||
path,
|
||||
exhaustiveRemoteTraversal,
|
||||
customHeaders,
|
||||
);
|
||||
|
||||
const getContent = async (path: string) => {
|
||||
let retryCount = 0;
|
||||
|
|
|
|||
|
|
@ -6,9 +6,17 @@ import { getStat } from './api';
|
|||
|
||||
export async function statItem(path: string, statPath = path) {
|
||||
const plugin = await usePlugin();
|
||||
return Object.assign(await getStat(plugin.settings.serverUrl, plugin.getToken(), path), {
|
||||
statPath,
|
||||
});
|
||||
return Object.assign(
|
||||
await getStat(
|
||||
plugin.settings.serverUrl,
|
||||
plugin.getToken(),
|
||||
path,
|
||||
plugin.settings.customHeaders,
|
||||
),
|
||||
{
|
||||
statPath,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function getContent(webdav: WebDAVClient, path: string) {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ const en = {
|
|||
name: 'Credential',
|
||||
placeholder: 'Enter your credential',
|
||||
},
|
||||
customHeaders: {
|
||||
desc: 'Optional. One per line, as "Header-Name: value". Sent on every WebDAV request.',
|
||||
name: 'Custom headers',
|
||||
placeholder: 'X-Api-Key: abc123',
|
||||
},
|
||||
encryption: {
|
||||
desc: 'Encrypt files before upload and decrypt files when download. Encryption password will be stored in Obsidian keychain.',
|
||||
name: 'Encryption',
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ const translation: typeof en = {
|
|||
name: 'Пароль / Токен',
|
||||
placeholder: 'Введите пароль или токен',
|
||||
},
|
||||
customHeaders: {
|
||||
desc: 'Необязательный параметр. По одному на строку, в формате «Имя-заголовка: значение». Передается с каждым запросом WebDAV.',
|
||||
name: 'Пользовательские заголовки',
|
||||
placeholder: 'X-Api-Key: abc123',
|
||||
},
|
||||
encryption: {
|
||||
desc: 'Шифровать файлы перед загрузкой и расшифровывать при скачивании. Пароль шифрования будет сохранён в связке ключей Obsidian.',
|
||||
name: 'Шифрование',
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ const translation: typeof en = {
|
|||
name: '凭证',
|
||||
placeholder: '输入你的凭证',
|
||||
},
|
||||
customHeaders: {
|
||||
desc: '可选。每行一个,格式为“Header-Name: value”。随每个 WebDAV 请求发送。',
|
||||
name: '自定义标头',
|
||||
placeholder: 'X-Api-Key: abc123',
|
||||
},
|
||||
encryption: {
|
||||
desc: '在上传前加密文件,并在下载时解密文件。密码将存储在 Obsidian 的密钥链中。',
|
||||
name: '加密',
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ const zhHant: typeof en = {
|
|||
name: '憑證',
|
||||
placeholder: '請輸入憑證',
|
||||
},
|
||||
customHeaders: {
|
||||
desc: '可選。每行一個,格式為「Header-Name: value」。隨每個 WebDAV 請求發送。',
|
||||
name: '自訂標頭',
|
||||
placeholder: 'X-Api-Key: abc123',
|
||||
},
|
||||
encryption: {
|
||||
desc: '上傳前加密檔案,下載時解密檔案。加密密碼將儲存於 Obsidian 鑰匙圈中。',
|
||||
name: '加密',
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export default class WebDAVSyncPlugin extends Plugin {
|
|||
confirmBeforeDeleteInAutoSync: true,
|
||||
confirmBeforeSync: true,
|
||||
conflictStrategy: ConflictStrategy.DiffMatchPatch,
|
||||
customHeaders: {},
|
||||
encryption: {
|
||||
enabled: false,
|
||||
value: '',
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export class WebDAVService {
|
|||
|
||||
createWebDAVClient(): WebDAVClient {
|
||||
const client = createClient(this.plugin.settings.serverUrl, {
|
||||
headers: this.plugin.settings.customHeaders,
|
||||
password: getCredential(this.plugin),
|
||||
username: this.plugin.settings.account,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import SelectRemoteBaseDirModal from '~/components/SelectRemoteBaseDirModal';
|
|||
import t from '~/i18n';
|
||||
import { normalizeBaseDir } from '~/platform/path';
|
||||
import handleInput from '~/utils/handle-input';
|
||||
import parseHeaders from '~/utils/parse-headers';
|
||||
import BaseSettings from './settings.base';
|
||||
|
||||
export default class AccountSettings extends BaseSettings {
|
||||
|
|
@ -69,6 +70,28 @@ export default class AccountSettings extends BaseSettings {
|
|||
}),
|
||||
);
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName(t('settings.customHeaders.name'))
|
||||
.setDesc(t('settings.customHeaders.desc'))
|
||||
.addTextArea((textarea) => {
|
||||
const stringify = (h: Record<string, string>) =>
|
||||
Object.entries(h)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join('\n');
|
||||
textarea
|
||||
.setPlaceholder(t('settings.customHeaders.placeholder'))
|
||||
.setValue(stringify(this.plugin.settings.customHeaders));
|
||||
textarea.inputEl.addEventListener('blur', () => {
|
||||
const parsed = parseHeaders(textarea.getValue());
|
||||
if (parsed === false) new Notice(t('settings.invalidValue'));
|
||||
else {
|
||||
this.plugin.settings.customHeaders = parsed;
|
||||
void this.plugin.saveSettings();
|
||||
}
|
||||
textarea.setValue(stringify(this.plugin.settings.customHeaders));
|
||||
});
|
||||
});
|
||||
|
||||
this.displayCheckConnection();
|
||||
|
||||
new Setting(this.containerEl)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export type PluginSettings = {
|
|||
serverUrl: string;
|
||||
account: string;
|
||||
token: string;
|
||||
customHeaders: Record<string, string>;
|
||||
encryption: {
|
||||
enabled: boolean;
|
||||
value: string;
|
||||
|
|
|
|||
18
src/utils/parse-headers.ts
Normal file
18
src/utils/parse-headers.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
||||
|
||||
export default function parseHeaders(raw: string) {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
for (const line of raw.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === '') continue;
|
||||
const colon = trimmed.indexOf(':');
|
||||
if (colon === -1) return false;
|
||||
const name = trimmed.slice(0, colon).trim();
|
||||
const value = trimmed.slice(colon + 1).trim();
|
||||
if (!HEADER_NAME.test(name)) return false;
|
||||
headers[name] = value;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ test('uses remote-base-aware path when traversing child directories', async () =
|
|||
'token',
|
||||
'/test/',
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
expect(getDirectoryContentsMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
|
|
@ -31,6 +32,7 @@ test('uses remote-base-aware path when traversing child directories', async () =
|
|||
'token',
|
||||
'/test/webdav-sync/',
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -50,5 +52,6 @@ test('skips missing directories during traversal', async () => {
|
|||
'token',
|
||||
'/test/missing/',
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue