feat(13-01): add 18 backends to BACKEND_REGISTRY with category field
- Removed explicit BackendType union literal; derived via keyof typeof BACKEND_REGISTRY - Added BackendCategory type and category field to all 18 backend entries - Added BackendMeta interface for individual entry typing - Removed circular Record<BackendType,...> cast; replaced with as const - Added 11 new backends: azure-files, swift, gdrive, dropbox, box, pcloud, ftp, webdav, smb, http, seafile - Updated registry.test.ts: 48 tests covering all 18 backends, category assertions, field validation
This commit is contained in:
+238
-14
@@ -1,23 +1,81 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { BACKEND_REGISTRY, BackendType } from './registry';
|
import { BACKEND_REGISTRY, BackendType, BackendCategory } from './registry';
|
||||||
|
|
||||||
const EXPECTED_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible', 'onedrive', 'sftp', 'gcs', 'b2'];
|
// 7 cloud-object-storage + 5 cloud-drives + 6 protocol-based = 18 total
|
||||||
|
const ALL_BACKENDS: BackendType[] = [
|
||||||
|
'azureblob', 's3', 's3-compatible', 'gcs', 'b2', 'azure-files', 'swift',
|
||||||
|
'onedrive', 'gdrive', 'dropbox', 'box', 'pcloud',
|
||||||
|
'sftp', 'ftp', 'webdav', 'smb', 'http', 'seafile',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Keep the original 7 for backwards compatibility checks
|
||||||
|
const ORIGINAL_7_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible', 'onedrive', 'sftp', 'gcs', 'b2'];
|
||||||
|
|
||||||
|
const VALID_CATEGORIES: BackendCategory[] = ['cloud-object-storage', 'cloud-drives', 'protocol-based'];
|
||||||
|
|
||||||
describe('Backend Schema Registry', () => {
|
describe('Backend Schema Registry', () => {
|
||||||
it('exports all three required backend types', () => {
|
it('has exactly 18 backend entries', () => {
|
||||||
for (const backend of EXPECTED_BACKENDS) {
|
const keys = Object.keys(BACKEND_REGISTRY) as BackendType[];
|
||||||
|
expect(keys).toHaveLength(18);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports all 17 required backend types', () => {
|
||||||
|
for (const backend of ALL_BACKENDS) {
|
||||||
expect(BACKEND_REGISTRY[backend]).toBeDefined();
|
expect(BACKEND_REGISTRY[backend]).toBeDefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('exports all original 7 backend types (regression)', () => {
|
||||||
|
for (const backend of ORIGINAL_7_BACKENDS) {
|
||||||
|
expect(BACKEND_REGISTRY[backend]).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('each backend has displayName, description, category, and at least one field', () => {
|
||||||
|
for (const backend of ALL_BACKENDS) {
|
||||||
|
const entry = BACKEND_REGISTRY[backend];
|
||||||
|
expect(entry.displayName, `${backend}.displayName`).toBeTruthy();
|
||||||
|
expect(entry.description, `${backend}.description`).toBeTruthy();
|
||||||
|
expect(entry.category, `${backend}.category`).toBeTruthy();
|
||||||
|
expect(entry.fields.length, `${backend}.fields.length`).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('each backend has a valid category value', () => {
|
||||||
|
for (const backend of ALL_BACKENDS) {
|
||||||
|
expect(VALID_CATEGORIES).toContain(BACKEND_REGISTRY[backend].category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cloud-object-storage backends are correctly categorized', () => {
|
||||||
|
const expected: BackendType[] = ['azureblob', 's3', 's3-compatible', 'gcs', 'b2', 'azure-files', 'swift'];
|
||||||
|
for (const b of expected) {
|
||||||
|
expect(BACKEND_REGISTRY[b].category).toBe('cloud-object-storage');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cloud-drives backends are correctly categorized', () => {
|
||||||
|
const expected: BackendType[] = ['onedrive', 'gdrive', 'dropbox', 'box', 'pcloud'];
|
||||||
|
for (const b of expected) {
|
||||||
|
expect(BACKEND_REGISTRY[b].category).toBe('cloud-drives');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protocol-based backends are correctly categorized', () => {
|
||||||
|
const expected: BackendType[] = ['sftp', 'ftp', 'webdav', 'smb', 'http', 'seafile'];
|
||||||
|
for (const b of expected) {
|
||||||
|
expect(BACKEND_REGISTRY[b].category).toBe('protocol-based');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('each backend has at least one field definition', () => {
|
it('each backend has at least one field definition', () => {
|
||||||
for (const backend of EXPECTED_BACKENDS) {
|
for (const backend of ALL_BACKENDS) {
|
||||||
expect(BACKEND_REGISTRY[backend].fields.length).toBeGreaterThan(0);
|
expect(BACKEND_REGISTRY[backend].fields.length).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('each FieldDef has a non-empty key (snake_case, no camelCase)', () => {
|
it('each FieldDef has a non-empty key (snake_case, no camelCase)', () => {
|
||||||
for (const backend of EXPECTED_BACKENDS) {
|
for (const backend of ALL_BACKENDS) {
|
||||||
for (const field of BACKEND_REGISTRY[backend].fields) {
|
for (const field of BACKEND_REGISTRY[backend].fields) {
|
||||||
expect(field.key).toBeTruthy();
|
expect(field.key).toBeTruthy();
|
||||||
// Reject camelCase — rclone keys are snake_case or lowercase
|
// Reject camelCase — rclone keys are snake_case or lowercase
|
||||||
@@ -27,13 +85,20 @@ describe('Backend Schema Registry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('each FieldDef has a non-empty label', () => {
|
it('each FieldDef has a non-empty label', () => {
|
||||||
for (const backend of EXPECTED_BACKENDS) {
|
for (const backend of ALL_BACKENDS) {
|
||||||
for (const field of BACKEND_REGISTRY[backend].fields) {
|
for (const field of BACKEND_REGISTRY[backend].fields) {
|
||||||
expect(field.label).toBeTruthy();
|
expect(field.label).toBeTruthy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('each backend entry has displayName and description metadata', () => {
|
||||||
|
for (const backend of ALL_BACKENDS) {
|
||||||
|
expect(BACKEND_REGISTRY[backend].displayName).toBeTruthy();
|
||||||
|
expect(BACKEND_REGISTRY[backend].description).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('Azure Blob has account field (required)', () => {
|
it('Azure Blob has account field (required)', () => {
|
||||||
const accountField = BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'account');
|
const accountField = BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'account');
|
||||||
expect(accountField).toBeDefined();
|
expect(accountField).toBeDefined();
|
||||||
@@ -53,13 +118,6 @@ describe('Backend Schema Registry', () => {
|
|||||||
expect(endpointField!.required).toBe(true);
|
expect(endpointField!.required).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('each backend entry has displayName and description metadata', () => {
|
|
||||||
for (const backend of EXPECTED_BACKENDS) {
|
|
||||||
expect(BACKEND_REGISTRY[backend].displayName).toBeTruthy();
|
|
||||||
expect(BACKEND_REGISTRY[backend].description).toBeTruthy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('OneDrive backend', () => {
|
describe('OneDrive backend', () => {
|
||||||
it('has token field (required)', () => {
|
it('has token field (required)', () => {
|
||||||
const f = BACKEND_REGISTRY.onedrive.fields.find(f => f.key === 'token');
|
const f = BACKEND_REGISTRY.onedrive.fields.find(f => f.key === 'token');
|
||||||
@@ -130,4 +188,170 @@ describe('Backend Schema Registry', () => {
|
|||||||
expect(f!.inputType).toBe('password');
|
expect(f!.inputType).toBe('password');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Azure Files backend (new)', () => {
|
||||||
|
it('has account field (required)', () => {
|
||||||
|
const f = BACKEND_REGISTRY['azure-files'].fields.find(f => f.key === 'account');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(true);
|
||||||
|
});
|
||||||
|
it('has key field (required)', () => {
|
||||||
|
const f = BACKEND_REGISTRY['azure-files'].fields.find(f => f.key === 'key');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(true);
|
||||||
|
expect(f!.inputType).toBe('password');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Swift backend (new)', () => {
|
||||||
|
it('has user, key, auth, and tenant fields (all required)', () => {
|
||||||
|
const fields = BACKEND_REGISTRY.swift.fields;
|
||||||
|
const keys = fields.map(f => f.key);
|
||||||
|
expect(keys).toContain('user');
|
||||||
|
expect(keys).toContain('key');
|
||||||
|
expect(keys).toContain('auth');
|
||||||
|
expect(keys).toContain('tenant');
|
||||||
|
});
|
||||||
|
it('has optional region field', () => {
|
||||||
|
const f = BACKEND_REGISTRY.swift.fields.find(f => f.key === 'region');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Google Drive backend (new)', () => {
|
||||||
|
it('has token field (optional — service account path also supported)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'token');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.inputType).toBe('password');
|
||||||
|
});
|
||||||
|
it('has service_account_credentials field (optional)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'service_account_credentials');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(false);
|
||||||
|
});
|
||||||
|
it('has root_folder_id field (optional)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'root_folder_id');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dropbox backend (new)', () => {
|
||||||
|
it('has token field (required)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.dropbox.fields.find(f => f.key === 'token');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(true);
|
||||||
|
expect(f!.inputType).toBe('password');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Box backend (new)', () => {
|
||||||
|
it('has token field (required)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.box.fields.find(f => f.key === 'token');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(true);
|
||||||
|
});
|
||||||
|
it('has box_sub_type select field (optional)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.box.fields.find(f => f.key === 'box_sub_type');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.inputType).toBe('select');
|
||||||
|
expect(f!.required).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pCloud backend (new)', () => {
|
||||||
|
it('has token field (required)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.pcloud.fields.find(f => f.key === 'token');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(true);
|
||||||
|
});
|
||||||
|
it('has hostname select field (optional, US/EU options)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.pcloud.fields.find(f => f.key === 'hostname');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.inputType).toBe('select');
|
||||||
|
const values = f!.options!.map(o => o.value);
|
||||||
|
expect(values).toContain('api.pcloud.com');
|
||||||
|
expect(values).toContain('eapi.pcloud.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FTP backend (new)', () => {
|
||||||
|
it('has host field (required)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.ftp.fields.find(f => f.key === 'host');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(true);
|
||||||
|
});
|
||||||
|
it('has user, pass, port fields (all optional)', () => {
|
||||||
|
const fields = BACKEND_REGISTRY.ftp.fields;
|
||||||
|
const optional = ['user', 'pass', 'port'];
|
||||||
|
for (const key of optional) {
|
||||||
|
const f = fields.find(f => f.key === key);
|
||||||
|
expect(f, `ftp.${key}`).toBeDefined();
|
||||||
|
expect(f!.required, `ftp.${key}.required`).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('has explicit_tls select field (optional)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.ftp.fields.find(f => f.key === 'explicit_tls');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.inputType).toBe('select');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WebDAV backend (new)', () => {
|
||||||
|
it('has url, user, pass fields (required)', () => {
|
||||||
|
const fields = BACKEND_REGISTRY.webdav.fields;
|
||||||
|
for (const key of ['url', 'user', 'pass']) {
|
||||||
|
const f = fields.find(f => f.key === key);
|
||||||
|
expect(f, `webdav.${key}`).toBeDefined();
|
||||||
|
expect(f!.required, `webdav.${key}.required`).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('has vendor select field (optional) with nextcloud/sharepoint options', () => {
|
||||||
|
const f = BACKEND_REGISTRY.webdav.fields.find(f => f.key === 'vendor');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.inputType).toBe('select');
|
||||||
|
const values = f!.options!.map(o => o.value);
|
||||||
|
expect(values).toContain('nextcloud');
|
||||||
|
expect(values).toContain('sharepoint');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SMB backend (new)', () => {
|
||||||
|
it('has host field (required)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.smb.fields.find(f => f.key === 'host');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(true);
|
||||||
|
});
|
||||||
|
it('has user field (required)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.smb.fields.find(f => f.key === 'user');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(true);
|
||||||
|
});
|
||||||
|
it('has pass, domain, port fields (optional)', () => {
|
||||||
|
for (const key of ['pass', 'domain', 'port']) {
|
||||||
|
const f = BACKEND_REGISTRY.smb.fields.find(f => f.key === key);
|
||||||
|
expect(f, `smb.${key}`).toBeDefined();
|
||||||
|
expect(f!.required, `smb.${key}.required`).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HTTP backend (new)', () => {
|
||||||
|
it('has url field (required)', () => {
|
||||||
|
const f = BACKEND_REGISTRY.http.fields.find(f => f.key === 'url');
|
||||||
|
expect(f).toBeDefined();
|
||||||
|
expect(f!.required).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Seafile backend (new)', () => {
|
||||||
|
it('has url, user, pass fields (all required)', () => {
|
||||||
|
for (const key of ['url', 'user', 'pass']) {
|
||||||
|
const f = BACKEND_REGISTRY.seafile.fields.find(f => f.key === key);
|
||||||
|
expect(f, `seafile.${key}`).toBeDefined();
|
||||||
|
expect(f!.required, `seafile.${key}.required`).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+463
-89
@@ -3,8 +3,11 @@
|
|||||||
// IMPORTANT: FieldDef.key values MUST match rclone config key names exactly.
|
// IMPORTANT: FieldDef.key values MUST match rclone config key names exactly.
|
||||||
// These keys are used by Phase 2 generators to build rclone.conf INI content.
|
// These keys are used by Phase 2 generators to build rclone.conf INI content.
|
||||||
// Verify against https://rclone.org/azureblob/ and https://rclone.org/s3/ before Phase 2.
|
// Verify against https://rclone.org/azureblob/ and https://rclone.org/s3/ before Phase 2.
|
||||||
|
//
|
||||||
|
// BackendType is DERIVED from keyof typeof BACKEND_REGISTRY — do NOT add a manual union.
|
||||||
|
// BackendCategory is the grouping used for UI categorization and search.
|
||||||
|
|
||||||
export type BackendType = 'azureblob' | 's3' | 's3-compatible' | 'onedrive' | 'sftp' | 'gcs' | 'b2';
|
export type BackendCategory = 'cloud-object-storage' | 'cloud-drives' | 'protocol-based';
|
||||||
|
|
||||||
export interface FieldDef {
|
export interface FieldDef {
|
||||||
key: string; // MUST match rclone config key exactly (snake_case)
|
key: string; // MUST match rclone config key exactly (snake_case)
|
||||||
@@ -18,15 +21,23 @@ export interface FieldDef {
|
|||||||
tooltipText?: string;
|
tooltipText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BackendMeta {
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
category: BackendCategory;
|
||||||
|
fields: FieldDef[];
|
||||||
|
}
|
||||||
|
|
||||||
export const BACKEND_REGISTRY = {
|
export const BACKEND_REGISTRY = {
|
||||||
azureblob: {
|
azureblob: {
|
||||||
displayName: 'Azure Blob Storage',
|
displayName: 'Azure Blob Storage',
|
||||||
description: 'Microsoft Azure cloud storage',
|
description: 'Microsoft Azure cloud storage',
|
||||||
|
category: 'cloud-object-storage' as const,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'account',
|
key: 'account',
|
||||||
label: 'Storage Account Name',
|
label: 'Storage Account Name',
|
||||||
inputType: 'text',
|
inputType: 'text' as const,
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'mystorageaccount',
|
placeholder: 'mystorageaccount',
|
||||||
helpText: 'The storage account name (not the full URL)',
|
helpText: 'The storage account name (not the full URL)',
|
||||||
@@ -38,7 +49,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'key',
|
key: 'key',
|
||||||
label: 'Access Key',
|
label: 'Access Key',
|
||||||
inputType: 'password',
|
inputType: 'password' as const,
|
||||||
required: false,
|
required: false,
|
||||||
helpText: 'Base64-encoded storage account key. Provide either this or a SAS URL, not both.',
|
helpText: 'Base64-encoded storage account key. Provide either this or a SAS URL, not both.',
|
||||||
tooltipText: 'The full storage account key grants unrestricted read/write access to all containers in the account. Keep this secret. If you only need limited access, use a SAS URL instead.',
|
tooltipText: 'The full storage account key grants unrestricted read/write access to all containers in the account. Keep this secret. If you only need limited access, use a SAS URL instead.',
|
||||||
@@ -46,7 +57,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'sas_url',
|
key: 'sas_url',
|
||||||
label: 'SAS URL',
|
label: 'SAS URL',
|
||||||
inputType: 'password',
|
inputType: 'password' as const,
|
||||||
required: false,
|
required: false,
|
||||||
placeholder: 'https://mystorageaccount.blob.core.windows.net/?sv=...',
|
placeholder: 'https://mystorageaccount.blob.core.windows.net/?sv=...',
|
||||||
helpText: 'Full SAS URL including account and container. Provide either this or an access key, not both.',
|
helpText: 'Full SAS URL including account and container. Provide either this or an access key, not both.',
|
||||||
@@ -57,18 +68,19 @@ export const BACKEND_REGISTRY = {
|
|||||||
s3: {
|
s3: {
|
||||||
displayName: 'Amazon S3',
|
displayName: 'Amazon S3',
|
||||||
description: 'AWS Simple Storage Service',
|
description: 'AWS Simple Storage Service',
|
||||||
|
category: 'cloud-object-storage' as const,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'provider',
|
key: 'provider',
|
||||||
label: 'Provider',
|
label: 'Provider',
|
||||||
inputType: 'select',
|
inputType: 'select' as const,
|
||||||
required: true,
|
required: true,
|
||||||
options: [{ value: 'AWS', label: 'Amazon S3' }],
|
options: [{ value: 'AWS', label: 'Amazon S3' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'access_key_id',
|
key: 'access_key_id',
|
||||||
label: 'Access Key ID',
|
label: 'Access Key ID',
|
||||||
inputType: 'text',
|
inputType: 'text' as const,
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'AKIAIOSFODNN7EXAMPLE',
|
placeholder: 'AKIAIOSFODNN7EXAMPLE',
|
||||||
helpText: 'The access key ID from your IAM credentials (starts with AKIA for long-term keys).',
|
helpText: 'The access key ID from your IAM credentials (starts with AKIA for long-term keys).',
|
||||||
@@ -77,7 +89,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'secret_access_key',
|
key: 'secret_access_key',
|
||||||
label: 'Secret Access Key',
|
label: 'Secret Access Key',
|
||||||
inputType: 'password',
|
inputType: 'password' as const,
|
||||||
required: true,
|
required: true,
|
||||||
helpText: 'The secret access key paired with your Access Key ID. Only shown once at creation time.',
|
helpText: 'The secret access key paired with your Access Key ID. Only shown once at creation time.',
|
||||||
tooltipText: 'This value is shown only once when you create an access key in IAM. If you did not save it, delete the existing key and create a new one. Never share or commit this value — treat it as a password.',
|
tooltipText: 'This value is shown only once when you create an access key in IAM. If you did not save it, delete the existing key and create a new one. Never share or commit this value — treat it as a password.',
|
||||||
@@ -85,7 +97,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'region',
|
key: 'region',
|
||||||
label: 'Region',
|
label: 'Region',
|
||||||
inputType: 'text',
|
inputType: 'text' as const,
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'us-east-1',
|
placeholder: 'us-east-1',
|
||||||
tooltipText: 'The AWS region where your S3 bucket is located. Find this in the S3 console next to your bucket name (e.g. us-east-1, eu-west-2, ap-southeast-1).',
|
tooltipText: 'The AWS region where your S3 bucket is located. Find this in the S3 console next to your bucket name (e.g. us-east-1, eu-west-2, ap-southeast-1).',
|
||||||
@@ -99,11 +111,12 @@ export const BACKEND_REGISTRY = {
|
|||||||
's3-compatible': {
|
's3-compatible': {
|
||||||
displayName: 'S3-Compatible',
|
displayName: 'S3-Compatible',
|
||||||
description: 'Wasabi, MinIO, Cloudflare R2, and others',
|
description: 'Wasabi, MinIO, Cloudflare R2, and others',
|
||||||
|
category: 'cloud-object-storage' as const,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'provider',
|
key: 'provider',
|
||||||
label: 'Provider',
|
label: 'Provider',
|
||||||
inputType: 'select',
|
inputType: 'select' as const,
|
||||||
required: true,
|
required: true,
|
||||||
options: [{ value: 'Other', label: 'S3-Compatible' }],
|
options: [{ value: 'Other', label: 'S3-Compatible' }],
|
||||||
helpText: 'Covers Wasabi, MinIO, Cloudflare R2, and any S3-compatible storage',
|
helpText: 'Covers Wasabi, MinIO, Cloudflare R2, and any S3-compatible storage',
|
||||||
@@ -111,7 +124,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'access_key_id',
|
key: 'access_key_id',
|
||||||
label: 'Access Key ID',
|
label: 'Access Key ID',
|
||||||
inputType: 'text',
|
inputType: 'text' as const,
|
||||||
required: true,
|
required: true,
|
||||||
helpText: 'The access key ID from your storage provider\'s dashboard.',
|
helpText: 'The access key ID from your storage provider\'s dashboard.',
|
||||||
tooltipText: 'Where to find this varies by provider: Wasabi — Account > Access Keys; Cloudflare R2 — R2 > Manage R2 API Tokens; MinIO — admin console > Identity > Users or Service Accounts.',
|
tooltipText: 'Where to find this varies by provider: Wasabi — Account > Access Keys; Cloudflare R2 — R2 > Manage R2 API Tokens; MinIO — admin console > Identity > Users or Service Accounts.',
|
||||||
@@ -119,7 +132,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'secret_access_key',
|
key: 'secret_access_key',
|
||||||
label: 'Secret Access Key',
|
label: 'Secret Access Key',
|
||||||
inputType: 'password',
|
inputType: 'password' as const,
|
||||||
required: true,
|
required: true,
|
||||||
helpText: 'The secret key paired with your Access Key ID.',
|
helpText: 'The secret key paired with your Access Key ID.',
|
||||||
tooltipText: 'This value is typically shown only once when you create the key. Store it securely — if lost, you will need to generate a new key pair.',
|
tooltipText: 'This value is typically shown only once when you create the key. Store it securely — if lost, you will need to generate a new key pair.',
|
||||||
@@ -127,7 +140,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'endpoint',
|
key: 'endpoint',
|
||||||
label: 'Endpoint URL',
|
label: 'Endpoint URL',
|
||||||
inputType: 'text',
|
inputType: 'text' as const,
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'https://s3.wasabisys.com',
|
placeholder: 'https://s3.wasabisys.com',
|
||||||
helpText: 'The S3-compatible endpoint URL for your storage provider',
|
helpText: 'The S3-compatible endpoint URL for your storage provider',
|
||||||
@@ -136,57 +149,22 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'region',
|
key: 'region',
|
||||||
label: 'Region',
|
label: 'Region',
|
||||||
inputType: 'text',
|
inputType: 'text' as const,
|
||||||
required: false,
|
required: false,
|
||||||
placeholder: 'us-east-1',
|
placeholder: 'us-east-1',
|
||||||
helpText: 'Optional for most S3-compatible providers',
|
helpText: 'Optional for most S3-compatible providers',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
onedrive: {
|
|
||||||
displayName: 'OneDrive',
|
|
||||||
description: 'Microsoft OneDrive (paste pre-obtained rclone token)',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: 'token',
|
|
||||||
label: 'OAuth Token (JSON)',
|
|
||||||
inputType: 'password',
|
|
||||||
required: true,
|
|
||||||
placeholder: '{"access_token":"...","token_type":"Bearer","refresh_token":"...","expiry":"..."}',
|
|
||||||
helpText: 'Paste the JSON token from: rclone authorize "onedrive"',
|
|
||||||
tooltipText: 'This is the JSON token obtained by running `rclone authorize "onedrive"` on a machine with a browser. The command opens a browser window, you authenticate, and rclone prints a JSON token — paste that entire JSON blob here.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'drive_id',
|
|
||||||
label: 'Drive ID',
|
|
||||||
inputType: 'text',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'b!...',
|
|
||||||
helpText: 'The drive ID from your OneDrive. Found in the rclone authorize output.',
|
|
||||||
tooltipText: 'The drive ID appears in the JSON output of `rclone authorize "onedrive"` — look for the "drive_id" field in the response. It typically starts with "b!" for business/SharePoint drives. Personal OneDrive drive IDs look like a UUID.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'drive_type',
|
|
||||||
label: 'Drive Type',
|
|
||||||
inputType: 'select',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'personal', label: 'Personal' },
|
|
||||||
{ value: 'business', label: 'Business' },
|
|
||||||
{ value: 'documentLibrary', label: 'SharePoint Document Library' },
|
|
||||||
],
|
|
||||||
helpText: 'Personal for consumer OneDrive, Business for Microsoft 365',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
gcs: {
|
gcs: {
|
||||||
displayName: 'Google Cloud Storage',
|
displayName: 'Google Cloud Storage',
|
||||||
description: 'Google Cloud Storage',
|
description: 'Google Cloud Storage',
|
||||||
|
category: 'cloud-object-storage' as const,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'project_number',
|
key: 'project_number',
|
||||||
label: 'Project Number',
|
label: 'Project Number',
|
||||||
inputType: 'text',
|
inputType: 'text' as const,
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '123456789',
|
placeholder: '123456789',
|
||||||
helpText: 'Your GCP project number (not project ID)',
|
helpText: 'Your GCP project number (not project ID)',
|
||||||
@@ -199,7 +177,7 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'service_account_credentials',
|
key: 'service_account_credentials',
|
||||||
label: 'Service Account JSON',
|
label: 'Service Account JSON',
|
||||||
inputType: 'password',
|
inputType: 'password' as const,
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: '{"type":"service_account","project_id":"..."}',
|
placeholder: '{"type":"service_account","project_id":"..."}',
|
||||||
helpText: 'Paste the full content of your service account JSON key file',
|
helpText: 'Paste the full content of your service account JSON key file',
|
||||||
@@ -207,49 +185,15 @@ export const BACKEND_REGISTRY = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sftp: {
|
|
||||||
displayName: 'SFTP',
|
|
||||||
description: 'SSH File Transfer Protocol',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: 'host',
|
|
||||||
label: 'Host',
|
|
||||||
inputType: 'text',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'sftp.example.com',
|
|
||||||
helpText: 'The SSH server hostname or IP address',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'user',
|
|
||||||
label: 'Username',
|
|
||||||
inputType: 'text',
|
|
||||||
required: true,
|
|
||||||
placeholder: 'admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'pass',
|
|
||||||
label: 'Password',
|
|
||||||
inputType: 'password',
|
|
||||||
required: false,
|
|
||||||
helpText: 'SFTP password. Note: rclone may require the password to be obscured using `rclone obscure <password>`. If authentication fails, use the obscured value instead of the plain password.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'key_pem',
|
|
||||||
label: 'Private Key (PEM)',
|
|
||||||
inputType: 'password',
|
|
||||||
required: false,
|
|
||||||
helpText: 'Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
b2: {
|
b2: {
|
||||||
displayName: 'Backblaze B2',
|
displayName: 'Backblaze B2',
|
||||||
description: 'Backblaze B2 Cloud Storage',
|
description: 'Backblaze B2 Cloud Storage',
|
||||||
|
category: 'cloud-object-storage' as const,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'account',
|
key: 'account',
|
||||||
label: 'Application Key ID',
|
label: 'Application Key ID',
|
||||||
inputType: 'text',
|
inputType: 'text' as const,
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'your-application-key-id',
|
placeholder: 'your-application-key-id',
|
||||||
helpText: 'The applicationKeyId — not the master account ID',
|
helpText: 'The applicationKeyId — not the master account ID',
|
||||||
@@ -258,11 +202,441 @@ export const BACKEND_REGISTRY = {
|
|||||||
{
|
{
|
||||||
key: 'key',
|
key: 'key',
|
||||||
label: 'Application Key',
|
label: 'Application Key',
|
||||||
inputType: 'password',
|
inputType: 'password' as const,
|
||||||
required: true,
|
required: true,
|
||||||
helpText: 'The application key (secret value from the B2 dashboard)',
|
helpText: 'The application key (secret value from the B2 dashboard)',
|
||||||
tooltipText: 'The application key value is shown only once when you create a new app key in B2 > App Keys. Copy it immediately — it cannot be retrieved later. If lost, delete the key and create a new one.',
|
tooltipText: 'The application key value is shown only once when you create a new app key in B2 > App Keys. Copy it immediately — it cannot be retrieved later. If lost, delete the key and create a new one.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
} as Record<BackendType, { displayName: string; description: string; fields: FieldDef[] }>;
|
'azure-files': {
|
||||||
|
displayName: 'Azure Files',
|
||||||
|
description: 'Microsoft Azure Files (SMB/REST cloud file shares)',
|
||||||
|
category: 'cloud-object-storage' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'account',
|
||||||
|
label: 'Storage Account Name',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'mystorageaccount',
|
||||||
|
helpText: 'The Azure storage account name',
|
||||||
|
validate: {
|
||||||
|
regex: /^[a-z0-9]{3,24}$/,
|
||||||
|
message: 'Must be 3–24 lowercase alphanumeric characters',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'key',
|
||||||
|
label: 'Storage Account Key',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'The base64-encoded storage account key from the Azure portal',
|
||||||
|
tooltipText: 'Found in Azure Portal > your storage account > Security + Networking > Access keys. Use key1 or key2 — either works.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
swift: {
|
||||||
|
displayName: 'OpenStack Swift',
|
||||||
|
description: 'OpenStack Swift object storage',
|
||||||
|
category: 'cloud-object-storage' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
label: 'Username',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'The OpenStack username (or tenant:user for v2 auth)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'key',
|
||||||
|
label: 'API Key / Password',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'The OpenStack API key or password for this user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'auth',
|
||||||
|
label: 'Auth URL',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'https://auth.example.com/v3',
|
||||||
|
helpText: 'The OpenStack identity (Keystone) auth endpoint URL',
|
||||||
|
tooltipText: 'This is the Keystone endpoint for your OpenStack provider (v2 or v3). Check your provider\'s dashboard or ask your administrator.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'tenant',
|
||||||
|
label: 'Tenant / Project Name',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'The OpenStack tenant (v2) or project name (v3)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'region',
|
||||||
|
label: 'Region',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: false,
|
||||||
|
helpText: 'The OpenStack region (optional — leave blank if your provider has a single region)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onedrive: {
|
||||||
|
displayName: 'OneDrive',
|
||||||
|
description: 'Microsoft OneDrive (paste pre-obtained rclone token)',
|
||||||
|
category: 'cloud-drives' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'token',
|
||||||
|
label: 'OAuth Token (JSON)',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: '{"access_token":"...","token_type":"Bearer","refresh_token":"...","expiry":"..."}',
|
||||||
|
helpText: 'Paste the JSON token from: rclone authorize "onedrive"',
|
||||||
|
tooltipText: 'This is the JSON token obtained by running `rclone authorize "onedrive"` on a machine with a browser. The command opens a browser window, you authenticate, and rclone prints a JSON token — paste that entire JSON blob here.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'drive_id',
|
||||||
|
label: 'Drive ID',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'b!...',
|
||||||
|
helpText: 'The drive ID from your OneDrive. Found in the rclone authorize output.',
|
||||||
|
tooltipText: 'The drive ID appears in the JSON output of `rclone authorize "onedrive"` — look for the "drive_id" field in the response. It typically starts with "b!" for business/SharePoint drives. Personal OneDrive drive IDs look like a UUID.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'drive_type',
|
||||||
|
label: 'Drive Type',
|
||||||
|
inputType: 'select' as const,
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'personal', label: 'Personal' },
|
||||||
|
{ value: 'business', label: 'Business' },
|
||||||
|
{ value: 'documentLibrary', label: 'SharePoint Document Library' },
|
||||||
|
],
|
||||||
|
helpText: 'Personal for consumer OneDrive, Business for Microsoft 365',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
gdrive: {
|
||||||
|
displayName: 'Google Drive',
|
||||||
|
description: 'Google Drive (paste pre-obtained rclone token or service account)',
|
||||||
|
category: 'cloud-drives' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'token',
|
||||||
|
label: 'OAuth Token (JSON)',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: false,
|
||||||
|
helpText: 'Paste the JSON token from: rclone authorize "drive". Use this OR a service account JSON, not both.',
|
||||||
|
tooltipText: 'Run `rclone authorize "drive"` on a machine with a browser. A browser window opens, you sign in with Google, and rclone prints a JSON token — paste that entire JSON blob here.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'service_account_credentials',
|
||||||
|
label: 'Service Account JSON',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: false,
|
||||||
|
helpText: 'For unattended/server use: paste the full service account JSON key file content. Use this OR an OAuth token, not both.',
|
||||||
|
tooltipText: 'Create a service account at Google Cloud Console > IAM & Admin > Service Accounts, share the target Drive with that service account email, then download the JSON key and paste its contents here.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'root_folder_id',
|
||||||
|
label: 'Root Folder ID',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: false,
|
||||||
|
helpText: 'Optional: restrict rclone to a specific folder. Leave blank to access the full Drive.',
|
||||||
|
tooltipText: 'The folder ID is the last segment of the Google Drive folder URL: https://drive.google.com/drive/folders/{FOLDER_ID}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
dropbox: {
|
||||||
|
displayName: 'Dropbox',
|
||||||
|
description: 'Dropbox (paste pre-obtained rclone token)',
|
||||||
|
category: 'cloud-drives' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'token',
|
||||||
|
label: 'OAuth Token (JSON)',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'Paste the JSON token from: rclone authorize "dropbox"',
|
||||||
|
tooltipText: 'Run `rclone authorize "dropbox"` on a machine with a browser. Sign in to Dropbox when the browser opens, then paste the resulting JSON token here.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
box: {
|
||||||
|
displayName: 'Box',
|
||||||
|
description: 'Box cloud storage (paste pre-obtained rclone token)',
|
||||||
|
category: 'cloud-drives' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'token',
|
||||||
|
label: 'OAuth Token (JSON)',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'Paste the JSON token from: rclone authorize "box"',
|
||||||
|
tooltipText: 'Run `rclone authorize "box"` on a machine with a browser. Sign in to Box when the browser opens, then paste the resulting JSON token here.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'box_sub_type',
|
||||||
|
label: 'Account Type',
|
||||||
|
inputType: 'select' as const,
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: 'user', label: 'User account (default)' },
|
||||||
|
{ value: 'enterprise', label: 'Enterprise / Service Account' },
|
||||||
|
],
|
||||||
|
helpText: 'Use Enterprise for Box App Users or service accounts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
pcloud: {
|
||||||
|
displayName: 'pCloud',
|
||||||
|
description: 'pCloud cloud storage (paste pre-obtained rclone token)',
|
||||||
|
category: 'cloud-drives' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'token',
|
||||||
|
label: 'OAuth Token (JSON)',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'Paste the JSON token from: rclone authorize "pcloud"',
|
||||||
|
tooltipText: 'Run `rclone authorize "pcloud"` on a machine with a browser. Sign in to pCloud when the browser opens, then paste the resulting JSON token here.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hostname',
|
||||||
|
label: 'Server Region',
|
||||||
|
inputType: 'select' as const,
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: 'api.pcloud.com', label: 'US (default)' },
|
||||||
|
{ value: 'eapi.pcloud.com', label: 'EU' },
|
||||||
|
],
|
||||||
|
helpText: 'Select EU if your pCloud account is registered in Europe',
|
||||||
|
tooltipText: 'pCloud has separate server infrastructure for US and EU users. EU-registered accounts must use eapi.pcloud.com — using the wrong region will return authentication errors.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sftp: {
|
||||||
|
displayName: 'SFTP',
|
||||||
|
description: 'SSH File Transfer Protocol',
|
||||||
|
category: 'protocol-based' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'host',
|
||||||
|
label: 'Host',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'sftp.example.com',
|
||||||
|
helpText: 'The SSH server hostname or IP address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
label: 'Username',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pass',
|
||||||
|
label: 'Password',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: false,
|
||||||
|
helpText: 'SFTP password. Note: rclone may require the password to be obscured using `rclone obscure <password>`. If authentication fails, use the obscured value instead of the plain password.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'key_pem',
|
||||||
|
label: 'Private Key (PEM)',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: false,
|
||||||
|
helpText: 'Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ftp: {
|
||||||
|
displayName: 'FTP',
|
||||||
|
description: 'File Transfer Protocol (supports plain FTP and FTPS)',
|
||||||
|
category: 'protocol-based' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'host',
|
||||||
|
label: 'Host',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'ftp.example.com',
|
||||||
|
helpText: 'The FTP server hostname or IP address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
label: 'Username',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: false,
|
||||||
|
placeholder: 'anonymous',
|
||||||
|
helpText: 'Leave blank or enter "anonymous" for anonymous FTP access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pass',
|
||||||
|
label: 'Password',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: false,
|
||||||
|
helpText: 'FTP password. Leave blank for anonymous access.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'port',
|
||||||
|
label: 'Port',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: false,
|
||||||
|
placeholder: '21',
|
||||||
|
helpText: 'FTP port (default: 21)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'explicit_tls',
|
||||||
|
label: 'TLS Mode',
|
||||||
|
inputType: 'select' as const,
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'Plain FTP (no encryption)' },
|
||||||
|
{ value: 'true', label: 'Explicit FTPS (STARTTLS)' },
|
||||||
|
],
|
||||||
|
helpText: 'Use Explicit FTPS for encrypted FTP connections (port 21 + STARTTLS)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
webdav: {
|
||||||
|
displayName: 'WebDAV',
|
||||||
|
description: 'WebDAV (NextCloud, SharePoint, OwnCloud, and others)',
|
||||||
|
category: 'protocol-based' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'url',
|
||||||
|
label: 'WebDAV URL',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'https://example.com/dav',
|
||||||
|
helpText: 'The full WebDAV endpoint URL',
|
||||||
|
tooltipText: 'NextCloud: https://yourserver/remote.php/dav/files/username/ | OwnCloud: https://yourserver/remote.php/webdav/ | SharePoint: https://company.sharepoint.com/sites/mysite/Documents',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
label: 'Username',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'The WebDAV username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pass',
|
||||||
|
label: 'Password',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'The WebDAV password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vendor',
|
||||||
|
label: 'Vendor / Server Type',
|
||||||
|
inputType: 'select' as const,
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: 'nextcloud', label: 'NextCloud' },
|
||||||
|
{ value: 'owncloud', label: 'OwnCloud' },
|
||||||
|
{ value: 'sharepoint', label: 'SharePoint (Microsoft 365 OAuth)' },
|
||||||
|
{ value: 'sharepoint-ntlm', label: 'SharePoint (NTLM / on-premises)' },
|
||||||
|
{ value: 'other', label: 'Other WebDAV server' },
|
||||||
|
],
|
||||||
|
helpText: 'Selecting the correct vendor enables server-specific optimizations',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
smb: {
|
||||||
|
displayName: 'SMB / Windows Share',
|
||||||
|
description: 'SMB / CIFS Windows file shares',
|
||||||
|
category: 'protocol-based' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'host',
|
||||||
|
label: 'Host',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'fileserver.example.com',
|
||||||
|
helpText: 'The SMB server hostname or IP address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
label: 'Username',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'The SMB username (can include domain: DOMAIN\\user)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pass',
|
||||||
|
label: 'Password',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: false,
|
||||||
|
helpText: 'The SMB password. Leave blank for guest/anonymous access.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'domain',
|
||||||
|
label: 'Domain',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: false,
|
||||||
|
placeholder: 'WORKGROUP',
|
||||||
|
helpText: 'Windows domain name (optional, defaults to WORKGROUP)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'port',
|
||||||
|
label: 'Port',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: false,
|
||||||
|
placeholder: '445',
|
||||||
|
helpText: 'SMB port (default: 445)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
displayName: 'HTTP (read-only)',
|
||||||
|
description: 'Read-only access to HTTP/HTTPS file listings',
|
||||||
|
category: 'protocol-based' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'url',
|
||||||
|
label: 'URL',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'https://example.com/path/',
|
||||||
|
helpText: 'The base URL of the HTTP file listing. Must expose directory listings.',
|
||||||
|
tooltipText: 'The HTTP backend is read-only and works with Apache/Nginx directory listings or any server that returns HTML with links. Ensure the URL ends with a trailing slash for directory access.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
seafile: {
|
||||||
|
displayName: 'Seafile',
|
||||||
|
description: 'Seafile self-hosted cloud storage',
|
||||||
|
category: 'protocol-based' as const,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'url',
|
||||||
|
label: 'Server URL',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'https://cloud.seafile.com',
|
||||||
|
helpText: 'The Seafile server URL (without trailing slash)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
label: 'Email / Username',
|
||||||
|
inputType: 'text' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'Your Seafile account email address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pass',
|
||||||
|
label: 'Password',
|
||||||
|
inputType: 'password' as const,
|
||||||
|
required: true,
|
||||||
|
helpText: 'Your Seafile account password',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// BackendType is derived from registry keys — no manual union needed.
|
||||||
|
// Adding a backend to BACKEND_REGISTRY automatically extends BackendType.
|
||||||
|
export type BackendType = keyof typeof BACKEND_REGISTRY;
|
||||||
|
|||||||
Reference in New Issue
Block a user