mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 06:50:10 +00:00
Compare commits
4
Commits
bf51309fbb
...
ff85698edb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff85698edb | ||
|
|
c3dc53bac1 | ||
|
|
1acc31eda0 | ||
|
|
c694f1bfce |
@@ -9,7 +9,8 @@
|
|||||||
export const BACKUP_DEFAULT_TIMEZONE = 'UTC';
|
export const BACKUP_DEFAULT_TIMEZONE = 'UTC';
|
||||||
export const BACKUP_DEFAULT_RETENTION_COUNT = 30;
|
export const BACKUP_DEFAULT_RETENTION_COUNT = 30;
|
||||||
export const BACKUP_DEFAULT_S3_REGION = 'auto';
|
export const BACKUP_DEFAULT_S3_REGION = 'auto';
|
||||||
export const BACKUP_DEFAULT_REMOTE_PATH = 'nodewarden';
|
export const BACKUP_DEFAULT_S3_ROOT_PATH = '';
|
||||||
|
export const BACKUP_DEFAULT_WEBDAV_REMOTE_PATH = 'nodewarden';
|
||||||
export const BACKUP_DEFAULT_INTERVAL_HOURS = 24;
|
export const BACKUP_DEFAULT_INTERVAL_HOURS = 24;
|
||||||
export const BACKUP_DEFAULT_START_TIME = '03:00';
|
export const BACKUP_DEFAULT_START_TIME = '03:00';
|
||||||
|
|
||||||
@@ -109,14 +110,14 @@ export function createDefaultBackupDestinationConfig(type: BackupDestinationType
|
|||||||
region: BACKUP_DEFAULT_S3_REGION,
|
region: BACKUP_DEFAULT_S3_REGION,
|
||||||
accessKeyId: '',
|
accessKeyId: '',
|
||||||
secretAccessKey: '',
|
secretAccessKey: '',
|
||||||
rootPath: BACKUP_DEFAULT_REMOTE_PATH,
|
rootPath: BACKUP_DEFAULT_S3_ROOT_PATH,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
remotePath: BACKUP_DEFAULT_REMOTE_PATH,
|
remotePath: BACKUP_DEFAULT_WEBDAV_REMOTE_PATH,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -275,11 +275,6 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
|||||||
onCreateAccountPasskey={props.onCreateAccountPasskey}
|
onCreateAccountPasskey={props.onCreateAccountPasskey}
|
||||||
onEnableAccountPasskeyDirectUnlock={props.onEnableAccountPasskeyDirectUnlock}
|
onEnableAccountPasskeyDirectUnlock={props.onEnableAccountPasskeyDirectUnlock}
|
||||||
onDeleteAccountPasskey={props.onDeleteAccountPasskey}
|
onDeleteAccountPasskey={props.onDeleteAccountPasskey}
|
||||||
pendingAuthRequests={props.pendingAuthRequests}
|
|
||||||
pendingAuthRequestsLoading={props.pendingAuthRequestsLoading}
|
|
||||||
onRefreshPendingAuthRequests={props.onRefreshPendingAuthRequests}
|
|
||||||
onApproveAuthRequest={props.onApproveAuthRequest}
|
|
||||||
onDenyAuthRequest={props.onDenyAuthRequest}
|
|
||||||
onLockTimeoutChange={props.onLockTimeoutChange}
|
onLockTimeoutChange={props.onLockTimeoutChange}
|
||||||
onSessionTimeoutActionChange={props.onSessionTimeoutActionChange}
|
onSessionTimeoutActionChange={props.onSessionTimeoutActionChange}
|
||||||
onNotify={props.onNotify}
|
onNotify={props.onNotify}
|
||||||
|
|||||||
@@ -89,42 +89,38 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="section-head">
|
<div className="section-head">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="flush-title">{t('txt_device_management')}</h3>
|
<h3 className="flush-title">{t('txt_authorized_devices')}</h3>
|
||||||
<div className="muted-inline section-note">
|
<div className="muted-inline section-note">
|
||||||
{t('txt_manage_device_sessions_and_30_day_totp_trusted_sessions')}
|
{t('txt_manage_device_sessions_and_30_day_totp_trusted_sessions')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<button type="button" className="btn btn-secondary small" disabled={props.loading} onClick={props.onRefresh}>
|
||||||
|
<RefreshCw size={14} className="btn-icon" />
|
||||||
|
{t('txt_refresh')}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-danger small" onClick={props.onRevokeAll}>
|
||||||
|
<ShieldOff size={14} className="btn-icon" />
|
||||||
|
{t('txt_revoke_all_trusted')}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-danger small" onClick={props.onRemoveAll}>
|
||||||
|
<Trash2 size={14} className="btn-icon" />
|
||||||
|
{t('txt_remove_all_devices')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="actions">
|
{!!props.error && (
|
||||||
<button type="button" className="btn btn-secondary small" disabled={props.loading} onClick={props.onRefresh}>
|
<div className="local-error">
|
||||||
<RefreshCw size={14} className="btn-icon" />
|
<span>{props.error}</span>
|
||||||
{t('txt_refresh')}
|
<button type="button" className="btn btn-secondary small" disabled={props.loading} onClick={props.onRefresh}>
|
||||||
</button>
|
<RefreshCw size={14} className="btn-icon" />
|
||||||
<button type="button" className="btn btn-danger small" onClick={props.onRevokeAll}>
|
{t('txt_refresh')}
|
||||||
<ShieldOff size={14} className="btn-icon" />
|
</button>
|
||||||
{t('txt_revoke_all_trusted')}
|
</div>
|
||||||
</button>
|
)}
|
||||||
<button type="button" className="btn btn-danger small" onClick={props.onRemoveAll}>
|
<table className="table authorized-devices-table">
|
||||||
<Trash2 size={14} className="btn-icon" />
|
|
||||||
{t('txt_remove_all_devices')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="card">
|
|
||||||
<h3 className="section-title-flush">{t('txt_authorized_devices')}</h3>
|
|
||||||
{!!props.error && (
|
|
||||||
<div className="local-error">
|
|
||||||
<span>{props.error}</span>
|
|
||||||
<button type="button" className="btn btn-secondary small" disabled={props.loading} onClick={props.onRefresh}>
|
|
||||||
<RefreshCw size={14} className="btn-icon" />
|
|
||||||
{t('txt_refresh')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<table className="table authorized-devices-table">
|
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col className="authorized-devices-col-device" />
|
<col className="authorized-devices-col-device" />
|
||||||
<col className="authorized-devices-col-type" />
|
<col className="authorized-devices-col-type" />
|
||||||
@@ -233,7 +229,7 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
|
|||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import { useEffect, useMemo, useState } from 'preact/hooks';
|
|||||||
import { Clipboard, KeyRound, RefreshCw, ShieldCheck, ShieldOff, Trash2 } from 'lucide-preact';
|
import { Clipboard, KeyRound, RefreshCw, ShieldCheck, ShieldOff, Trash2 } from 'lucide-preact';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
import qrcode from 'qrcode-generator';
|
import qrcode from 'qrcode-generator';
|
||||||
import type { AccountPasskeyCredential, AuthRequest, Profile } from '@/lib/types';
|
import type { AccountPasskeyCredential, Profile } from '@/lib/types';
|
||||||
import { AVAILABLE_LOCALES, getLocale, setLocale, t, type Locale } from '@/lib/i18n';
|
import { AVAILABLE_LOCALES, getLocale, setLocale, t, type Locale } from '@/lib/i18n';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog';
|
import ConfirmDialog from '@/components/ConfirmDialog';
|
||||||
import PendingAuthRequestsPanel from '@/components/PendingAuthRequestsPanel';
|
|
||||||
|
|
||||||
interface SettingsPageProps {
|
interface SettingsPageProps {
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
@@ -23,11 +22,6 @@ interface SettingsPageProps {
|
|||||||
onCreateAccountPasskey: (name: string, masterPassword: string, directUnlock: boolean) => Promise<AccountPasskeyCredential | null>;
|
onCreateAccountPasskey: (name: string, masterPassword: string, directUnlock: boolean) => Promise<AccountPasskeyCredential | null>;
|
||||||
onEnableAccountPasskeyDirectUnlock: (id: string, masterPassword: string) => Promise<void>;
|
onEnableAccountPasskeyDirectUnlock: (id: string, masterPassword: string) => Promise<void>;
|
||||||
onDeleteAccountPasskey: (id: string, masterPassword: string) => Promise<void>;
|
onDeleteAccountPasskey: (id: string, masterPassword: string) => Promise<void>;
|
||||||
pendingAuthRequests: AuthRequest[];
|
|
||||||
pendingAuthRequestsLoading: boolean;
|
|
||||||
onRefreshPendingAuthRequests: () => Promise<void>;
|
|
||||||
onApproveAuthRequest: (request: AuthRequest) => Promise<void>;
|
|
||||||
onDenyAuthRequest: (request: AuthRequest) => Promise<void>;
|
|
||||||
onLockTimeoutChange: (minutes: 0 | 1 | 5 | 15 | 30) => void;
|
onLockTimeoutChange: (minutes: 0 | 1 | 5 | 15 | 30) => void;
|
||||||
onSessionTimeoutActionChange: (action: 'lock' | 'logout') => void;
|
onSessionTimeoutActionChange: (action: 'lock' | 'logout') => void;
|
||||||
onNotify?: (type: 'success' | 'error' | 'warning', text: string) => void;
|
onNotify?: (type: 'success' | 'error' | 'warning', text: string) => void;
|
||||||
@@ -515,15 +509,6 @@ export default function SettingsPage(props: SettingsPageProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<PendingAuthRequestsPanel
|
|
||||||
pendingAuthRequests={props.pendingAuthRequests}
|
|
||||||
pendingAuthRequestsLoading={props.pendingAuthRequestsLoading}
|
|
||||||
onRefreshPendingAuthRequests={props.onRefreshPendingAuthRequests}
|
|
||||||
onApproveAuthRequest={props.onApproveAuthRequest}
|
|
||||||
onDenyAuthRequest={props.onDenyAuthRequest}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<section className="settings-module sensitive-actions-module">
|
<section className="settings-module sensitive-actions-module">
|
||||||
<div className="sensitive-actions-grid">
|
<div className="sensitive-actions-grid">
|
||||||
<div className="sensitive-action">
|
<div className="sensitive-action">
|
||||||
|
|||||||
@@ -54,21 +54,18 @@ function renderRecommendedProviderDetails(provider: RecommendedProvider) {
|
|||||||
<>
|
<>
|
||||||
<div className="backup-recommendation-steps">
|
<div className="backup-recommendation-steps">
|
||||||
<div className="backup-recommendation-step">
|
<div className="backup-recommendation-step">
|
||||||
<strong>1.</strong> {t('txt_backup_recommend_koofr_step_1')}
|
<strong>1.</strong> {t('txt_backup_recommend_koofr_step_2_prefix')}{' '}
|
||||||
</div>
|
|
||||||
<div className="backup-recommendation-step">
|
|
||||||
<strong>2.</strong> {t('txt_backup_recommend_koofr_step_2_prefix')}{' '}
|
|
||||||
<a href={provider.passwordUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_koofr_password_link')}</a>
|
<a href={provider.passwordUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_koofr_password_link')}</a>
|
||||||
{t('txt_backup_recommend_koofr_step_2_suffix')}
|
{t('txt_backup_recommend_koofr_step_2_suffix')}
|
||||||
</div>
|
</div>
|
||||||
<div className="backup-recommendation-step">
|
<div className="backup-recommendation-step">
|
||||||
<strong>3.</strong> {t('txt_backup_recommend_koofr_step_3')}
|
<strong>2.</strong> {t('txt_backup_recommend_koofr_step_3')}
|
||||||
</div>
|
</div>
|
||||||
<div className="backup-recommendation-step">
|
<div className="backup-recommendation-step">
|
||||||
<strong>4.</strong> {t('txt_backup_recommend_koofr_step_4')}
|
<strong>3.</strong> {t('txt_backup_recommend_koofr_step_4')}
|
||||||
</div>
|
</div>
|
||||||
<div className="backup-recommendation-step">
|
<div className="backup-recommendation-step">
|
||||||
<strong>5.</strong> {t('txt_backup_recommend_koofr_step_5_prefix')}{' '}
|
<strong>4.</strong> {t('txt_backup_recommend_koofr_step_5_prefix')}{' '}
|
||||||
<a href={provider.storageUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_koofr_storage_link')}</a>
|
<a href={provider.storageUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_koofr_storage_link')}</a>
|
||||||
{t('txt_backup_recommend_koofr_step_5_suffix')}
|
{t('txt_backup_recommend_koofr_step_5_suffix')}
|
||||||
</div>
|
</div>
|
||||||
@@ -98,13 +95,10 @@ function renderRecommendedProviderDetails(provider: RecommendedProvider) {
|
|||||||
return (
|
return (
|
||||||
<div className="backup-recommendation-steps">
|
<div className="backup-recommendation-steps">
|
||||||
<div className="backup-recommendation-step">
|
<div className="backup-recommendation-step">
|
||||||
<strong>1.</strong> {t('txt_backup_recommend_pcloud_step_1')}
|
<strong>1.</strong> {t('txt_backup_recommend_pcloud_step_2')}
|
||||||
</div>
|
</div>
|
||||||
<div className="backup-recommendation-step">
|
<div className="backup-recommendation-step">
|
||||||
<strong>2.</strong> {t('txt_backup_recommend_pcloud_step_2')}
|
<strong>2.</strong> {t('txt_backup_recommend_pcloud_step_3')}
|
||||||
</div>
|
|
||||||
<div className="backup-recommendation-step">
|
|
||||||
<strong>3.</strong> {t('txt_backup_recommend_pcloud_step_3')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -112,18 +106,87 @@ function renderRecommendedProviderDetails(provider: RecommendedProvider) {
|
|||||||
return (
|
return (
|
||||||
<div className="backup-recommendation-steps">
|
<div className="backup-recommendation-steps">
|
||||||
<div className="backup-recommendation-step">
|
<div className="backup-recommendation-step">
|
||||||
<strong>1.</strong> {t('txt_backup_recommend_infinicloud_step_1')}
|
<strong>1.</strong> {t('txt_backup_recommend_infinicloud_step_2_prefix')}{' '}
|
||||||
</div>
|
|
||||||
<div className="backup-recommendation-step">
|
|
||||||
<strong>2.</strong> {t('txt_backup_recommend_infinicloud_step_2_prefix')}{' '}
|
|
||||||
<a href="https://infini-cloud.net/en/modules/mypage/usage/" target="_blank" rel="noreferrer">My Page</a>
|
<a href="https://infini-cloud.net/en/modules/mypage/usage/" target="_blank" rel="noreferrer">My Page</a>
|
||||||
{t('txt_backup_recommend_infinicloud_step_2_suffix')}
|
{t('txt_backup_recommend_infinicloud_step_2_suffix')}
|
||||||
</div>
|
</div>
|
||||||
<div className="backup-recommendation-step">
|
<div className="backup-recommendation-step">
|
||||||
<strong>3.</strong> {t('txt_backup_recommend_infinicloud_step_3')}
|
<strong>2.</strong> {t('txt_backup_recommend_infinicloud_step_3')}
|
||||||
</div>
|
</div>
|
||||||
<div className="backup-recommendation-step">
|
<div className="backup-recommendation-step">
|
||||||
<strong>4.</strong> {t('txt_backup_recommend_infinicloud_step_4')}
|
<strong>3.</strong> {t('txt_backup_recommend_infinicloud_step_4')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'backblaze-b2':
|
||||||
|
return (
|
||||||
|
<div className="backup-recommendation-steps">
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>1.</strong> {t('txt_backup_recommend_backblaze_step_2_prefix')}{' '}
|
||||||
|
<a href={provider.bucketsUrl} target="_blank" rel="noreferrer">Buckets</a>
|
||||||
|
{t('txt_backup_recommend_backblaze_step_2_suffix')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>2.</strong> {t('txt_backup_recommend_backblaze_step_3')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>3.</strong> {t('txt_backup_recommend_backblaze_step_4_prefix')}{' '}
|
||||||
|
<a href={provider.applicationKeysUrl} target="_blank" rel="noreferrer">Application Keys</a>
|
||||||
|
{t('txt_backup_recommend_backblaze_step_4_suffix')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>4.</strong> {t('txt_backup_recommend_backblaze_step_5')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>5.</strong> {t('txt_backup_recommend_s3_path_prefix_step')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'cloudflare-r2':
|
||||||
|
return (
|
||||||
|
<div className="backup-recommendation-steps">
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>1.</strong> {t('txt_backup_recommend_cloudflare_r2_step_1_prefix')}{' '}
|
||||||
|
<a href={provider.bucketUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_cloudflare_r2_bucket_link')}</a>
|
||||||
|
{t('txt_backup_recommend_cloudflare_r2_step_1_suffix')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>2.</strong> {t('txt_backup_recommend_cloudflare_r2_step_2_prefix')}{' '}
|
||||||
|
<a href={provider.apiTokenUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_cloudflare_r2_api_link')}</a>
|
||||||
|
{t('txt_backup_recommend_cloudflare_r2_step_2_suffix')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>3.</strong> {t('txt_backup_recommend_cloudflare_r2_step_3')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>4.</strong> {t('txt_backup_recommend_cloudflare_r2_step_4')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>5.</strong> {t('txt_backup_recommend_cloudflare_r2_step_5')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'tigris':
|
||||||
|
return (
|
||||||
|
<div className="backup-recommendation-steps">
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>1.</strong> {t('txt_backup_recommend_tigris_step_2_prefix')}{' '}
|
||||||
|
<a href={provider.bucketUrl} target="_blank" rel="noreferrer">Create Bucket</a>
|
||||||
|
{t('txt_backup_recommend_tigris_step_2_suffix')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>2.</strong> {t('txt_backup_recommend_tigris_step_3_prefix')}{' '}
|
||||||
|
<a href={provider.accessKeyUrl} target="_blank" rel="noreferrer">{t('txt_backup_recommend_tigris_access_key_link')}</a>
|
||||||
|
{t('txt_backup_recommend_tigris_step_3_suffix')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>3.</strong> {t('txt_backup_recommend_tigris_step_4')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>4.</strong> {t('txt_backup_recommend_tigris_step_5')}
|
||||||
|
</div>
|
||||||
|
<div className="backup-recommendation-step">
|
||||||
|
<strong>5.</strong> {t('txt_backup_recommend_s3_path_prefix_step')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -147,6 +210,9 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
|
|||||||
<div className="backup-inline-note">
|
<div className="backup-inline-note">
|
||||||
{props.selectedRecommendedProvider.id === 'infinicloud' ? t('txt_backup_recommend_infinicloud_summary')
|
{props.selectedRecommendedProvider.id === 'infinicloud' ? t('txt_backup_recommend_infinicloud_summary')
|
||||||
: props.selectedRecommendedProvider.id === 'koofr' ? t('txt_backup_recommend_koofr_summary')
|
: props.selectedRecommendedProvider.id === 'koofr' ? t('txt_backup_recommend_koofr_summary')
|
||||||
|
: props.selectedRecommendedProvider.id === 'backblaze-b2' ? t('txt_backup_recommend_backblaze_summary')
|
||||||
|
: props.selectedRecommendedProvider.id === 'cloudflare-r2' ? t('txt_backup_recommend_cloudflare_r2_summary')
|
||||||
|
: props.selectedRecommendedProvider.id === 'tigris' ? t('txt_backup_recommend_tigris_summary')
|
||||||
: t('txt_backup_recommend_pcloud_summary')}
|
: t('txt_backup_recommend_pcloud_summary')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,7 +453,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
|
|||||||
className="input"
|
className="input"
|
||||||
value={(props.selectedDestination.destination as WebDavBackupDestination).remotePath}
|
value={(props.selectedDestination.destination as WebDavBackupDestination).remotePath}
|
||||||
disabled={props.loadingSettings || props.disableWhileBusy}
|
disabled={props.loadingSettings || props.disableWhileBusy}
|
||||||
placeholder="nodewarden/backups"
|
placeholder="nodewarden"
|
||||||
onInput={(event) => props.onUpdateDestination((destination) => ({
|
onInput={(event) => props.onUpdateDestination((destination) => ({
|
||||||
...destination,
|
...destination,
|
||||||
destination: {
|
destination: {
|
||||||
@@ -504,7 +570,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
|
|||||||
className="input"
|
className="input"
|
||||||
value={(props.selectedDestination.destination as S3BackupDestination).rootPath}
|
value={(props.selectedDestination.destination as S3BackupDestination).rootPath}
|
||||||
disabled={props.loadingSettings || props.disableWhileBusy}
|
disabled={props.loadingSettings || props.disableWhileBusy}
|
||||||
placeholder="nodewarden/backups"
|
placeholder=""
|
||||||
onInput={(event) => props.onUpdateDestination((destination) => ({
|
onInput={(event) => props.onUpdateDestination((destination) => ({
|
||||||
...destination,
|
...destination,
|
||||||
destination: {
|
destination: {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Download, FileUp } from 'lucide-preact';
|
import { Download, FileUp } from 'lucide-preact';
|
||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
import type { RecommendedProvider } from '@/lib/backup-recommendations';
|
import type { RecommendedProvider } from '@/lib/backup-recommendations';
|
||||||
import { hasLinkedStorages } from '@/lib/backup-recommendations';
|
import { hasLinkedStorages } from '@/lib/backup-recommendations';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import { BackupIncludeAttachmentsField } from './BackupIncludeAttachmentsField';
|
import { BackupIncludeAttachmentsField } from './BackupIncludeAttachmentsField';
|
||||||
|
|
||||||
|
const MOBILE_RECOMMENDATIONS_QUERY = '(max-width: 760px)';
|
||||||
|
|
||||||
interface BackupOperationsSidebarProps {
|
interface BackupOperationsSidebarProps {
|
||||||
disableWhileBusy: boolean;
|
disableWhileBusy: boolean;
|
||||||
exporting: boolean;
|
exporting: boolean;
|
||||||
@@ -18,7 +21,30 @@ interface BackupOperationsSidebarProps {
|
|||||||
onSelectProvider: (providerId: string) => void;
|
onSelectProvider: (providerId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultRecommendationsOpen() {
|
||||||
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !window.matchMedia(MOBILE_RECOMMENDATIONS_QUERY).matches;
|
||||||
|
}
|
||||||
|
|
||||||
export function BackupOperationsSidebar(props: BackupOperationsSidebarProps) {
|
export function BackupOperationsSidebar(props: BackupOperationsSidebarProps) {
|
||||||
|
const [recommendationsOpen, setRecommendationsOpen] = useState(getDefaultRecommendationsOpen);
|
||||||
|
const [recommendationsTouched, setRecommendationsTouched] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function' || recommendationsTouched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const media = window.matchMedia(MOBILE_RECOMMENDATIONS_QUERY);
|
||||||
|
const syncOpenState = () => setRecommendationsOpen(!media.matches);
|
||||||
|
|
||||||
|
syncOpenState();
|
||||||
|
media.addEventListener('change', syncOpenState);
|
||||||
|
return () => media.removeEventListener('change', syncOpenState);
|
||||||
|
}, [recommendationsTouched]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="backup-operations-sidebar">
|
<aside className="backup-operations-sidebar">
|
||||||
<div className="section-head">
|
<div className="section-head">
|
||||||
@@ -41,7 +67,14 @@ export function BackupOperationsSidebar(props: BackupOperationsSidebarProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details className="backup-recommendations-disclosure">
|
<details
|
||||||
|
className="backup-recommendations-disclosure"
|
||||||
|
open={recommendationsOpen}
|
||||||
|
onToggle={(event) => {
|
||||||
|
setRecommendationsTouched(true);
|
||||||
|
setRecommendationsOpen((event.currentTarget as HTMLDetailsElement).open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<summary className="backup-recommendations-summary">
|
<summary className="backup-recommendations-summary">
|
||||||
<span>
|
<span>
|
||||||
<strong>{t('txt_backup_recommend_title')}</strong>
|
<strong>{t('txt_backup_recommend_title')}</strong>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export interface RecommendedStorageLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RecommendedProviderBase {
|
export interface RecommendedProviderBase {
|
||||||
id: 'infinicloud' | 'koofr' | 'pcloud';
|
id: 'infinicloud' | 'koofr' | 'pcloud' | 'backblaze-b2' | 'cloudflare-r2' | 'tigris';
|
||||||
name: string;
|
name: string;
|
||||||
capacity: string;
|
capacity: string;
|
||||||
protocol: 'webdav' | 's3';
|
protocol: 'webdav' | 's3';
|
||||||
@@ -28,7 +28,25 @@ export interface PcloudProvider extends RecommendedProviderBase {
|
|||||||
id: 'pcloud';
|
id: 'pcloud';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RecommendedProvider = InfinicloudProvider | KoofrProvider | PcloudProvider;
|
export interface BackblazeB2Provider extends RecommendedProviderBase {
|
||||||
|
id: 'backblaze-b2';
|
||||||
|
bucketsUrl: string;
|
||||||
|
applicationKeysUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CloudflareR2Provider extends RecommendedProviderBase {
|
||||||
|
id: 'cloudflare-r2';
|
||||||
|
bucketUrl: string;
|
||||||
|
apiTokenUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TigrisProvider extends RecommendedProviderBase {
|
||||||
|
id: 'tigris';
|
||||||
|
bucketUrl: string;
|
||||||
|
accessKeyUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RecommendedProvider = InfinicloudProvider | KoofrProvider | PcloudProvider | BackblazeB2Provider | CloudflareR2Provider | TigrisProvider;
|
||||||
|
|
||||||
export const RECOMMENDED_PROVIDERS: RecommendedProvider[] = [
|
export const RECOMMENDED_PROVIDERS: RecommendedProvider[] = [
|
||||||
{
|
{
|
||||||
@@ -61,6 +79,33 @@ export const RECOMMENDED_PROVIDERS: RecommendedProvider[] = [
|
|||||||
signupUrl: 'https://u.pcloud.com/#/register?invite=GITx7ZvEU1N7',
|
signupUrl: 'https://u.pcloud.com/#/register?invite=GITx7ZvEU1N7',
|
||||||
hasAffiliateLink: true,
|
hasAffiliateLink: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'backblaze-b2',
|
||||||
|
name: 'Backblaze B2',
|
||||||
|
capacity: '10G',
|
||||||
|
protocol: 's3',
|
||||||
|
signupUrl: 'https://secure.backblaze.com/user_signin.htm',
|
||||||
|
bucketsUrl: 'https://secure.backblaze.com/b2_buckets.htm',
|
||||||
|
applicationKeysUrl: 'https://secure.backblaze.com/app_keys.htm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cloudflare-r2',
|
||||||
|
name: 'Cloudflare R2',
|
||||||
|
capacity: '10G',
|
||||||
|
protocol: 's3',
|
||||||
|
signupUrl: 'https://dash.cloudflare.com/?to=/:account/r2/new',
|
||||||
|
bucketUrl: 'https://dash.cloudflare.com/?to=/:account/r2/new',
|
||||||
|
apiTokenUrl: 'https://dash.cloudflare.com/?to=/:account/r2/api-tokens/create?type=user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tigris',
|
||||||
|
name: 'Tigris',
|
||||||
|
capacity: '5G',
|
||||||
|
protocol: 's3',
|
||||||
|
signupUrl: 'https://console.storage.dev/signup',
|
||||||
|
bucketUrl: 'https://console.storage.dev/createbucket',
|
||||||
|
accessKeyUrl: 'https://console.storage.dev/createaccesskey',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function hasLinkedStorages(provider: RecommendedProvider): provider is KoofrProvider {
|
export function hasLinkedStorages(provider: RecommendedProvider): provider is KoofrProvider {
|
||||||
|
|||||||
@@ -85,6 +85,37 @@ const en: Record<string, string> = {
|
|||||||
"txt_backup_recommend_pcloud_step_1": "Register a pCloud account with just your email address.",
|
"txt_backup_recommend_pcloud_step_1": "Register a pCloud account with just your email address.",
|
||||||
"txt_backup_recommend_pcloud_step_2": "Use https://webdav.pcloud.com/ as the WebDAV server URL.",
|
"txt_backup_recommend_pcloud_step_2": "Use https://webdav.pcloud.com/ as the WebDAV server URL.",
|
||||||
"txt_backup_recommend_pcloud_step_3": "Use your registration email as the WebDAV username and your account password as the WebDAV password.",
|
"txt_backup_recommend_pcloud_step_3": "Use your registration email as the WebDAV username and your account password as the WebDAV password.",
|
||||||
|
"txt_backup_recommend_backblaze_summary": "S3-compatible object storage with 10 GB free and no credit card required.",
|
||||||
|
"txt_backup_recommend_backblaze_step_1": "Register or sign in to a Backblaze account.",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_prefix": "Open",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_suffix": ", click Create a Bucket, enter only the bucket name, leave the other settings unchanged, and create it.",
|
||||||
|
"txt_backup_recommend_backblaze_step_3": "After creation, put the displayed Endpoint into S3 Endpoint URL, use the bucket name for Bucket Name, and use the middle segment of the endpoint, such as us-west-004, for Region.",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_prefix": "Open",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_suffix": ", click Add a New Application Key, enter any Name of Key, leave the other settings unchanged, and create it.",
|
||||||
|
"txt_backup_recommend_backblaze_step_5": "Use keyID as the access key and applicationKey as the secret key.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_summary": "S3-compatible object storage with 10 GB free, but it requires credit card verification.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_bucket_link": "Create bucket page",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_api_link": "API token page",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "Open the",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ", enter only the bucket name, and create it directly.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "Open the",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ", select Object Read & Write for permissions, and create it directly.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_3": "Ignore the token value after creation. Fill Access Key ID into Access ID, and Secret Access Key into Access Password.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_4": "Copy the address shown below into S3 Endpoint URL, fill Bucket Name exactly as shown, and leave Region as auto.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_5": "Set Path Prefix as needed, for example nodewarden, or leave it empty if you do not want a folder prefix.",
|
||||||
|
"txt_backup_recommend_s3_path_prefix_step": "Set Path Prefix as needed, for example nodewarden, or leave it empty if you do not want a folder prefix.",
|
||||||
|
"txt_backup_recommend_tigris_summary": "S3-compatible object storage with 5 GB free and no credit card required.",
|
||||||
|
"txt_backup_recommend_tigris_signup_link": "signup page",
|
||||||
|
"txt_backup_recommend_tigris_bucket_link": "Create Bucket page",
|
||||||
|
"txt_backup_recommend_tigris_access_key_link": "Create Access Key page",
|
||||||
|
"txt_backup_recommend_tigris_step_1_prefix": "Open the",
|
||||||
|
"txt_backup_recommend_tigris_step_1_suffix": ", sign up, and log in to Tigris.",
|
||||||
|
"txt_backup_recommend_tigris_step_2_prefix": "Open",
|
||||||
|
"txt_backup_recommend_tigris_step_2_suffix": ", enter only the bucket name, leave everything else unchanged, and create it.",
|
||||||
|
"txt_backup_recommend_tigris_step_3_prefix": "Then open the",
|
||||||
|
"txt_backup_recommend_tigris_step_3_suffix": ", use any name you like, and create it.",
|
||||||
|
"txt_backup_recommend_tigris_step_4": "Ignore Endpoint URL IAM after creation. Fill the other displayed values into the backup page using the matching field names.",
|
||||||
|
"txt_backup_recommend_tigris_step_5": "Finally, click Manage Key Permissions and turn on Admin Access, otherwise writing backups will fail.",
|
||||||
"txt_backup_add_destination": "Add Destination",
|
"txt_backup_add_destination": "Add Destination",
|
||||||
"txt_backup_schedule_panel_title": "Automatic Schedule",
|
"txt_backup_schedule_panel_title": "Automatic Schedule",
|
||||||
"txt_backup_schedule_panel_note": "Each destination can keep its own daily backup schedule.",
|
"txt_backup_schedule_panel_note": "Each destination can keep its own daily backup schedule.",
|
||||||
|
|||||||
@@ -85,6 +85,37 @@ const es: Record<string, string> = {
|
|||||||
"txt_backup_recommend_pcloud_step_1": "Registre una cuenta pCloud solo con su dirección de correo.",
|
"txt_backup_recommend_pcloud_step_1": "Registre una cuenta pCloud solo con su dirección de correo.",
|
||||||
"txt_backup_recommend_pcloud_step_2": "Use https://webdav.pcloud.com/ como URL del servidor WebDAV.",
|
"txt_backup_recommend_pcloud_step_2": "Use https://webdav.pcloud.com/ como URL del servidor WebDAV.",
|
||||||
"txt_backup_recommend_pcloud_step_3": "Use su correo de registro como nombre de usuario WebDAV y su contraseña de cuenta como contraseña WebDAV.",
|
"txt_backup_recommend_pcloud_step_3": "Use su correo de registro como nombre de usuario WebDAV y su contraseña de cuenta como contraseña WebDAV.",
|
||||||
|
"txt_backup_recommend_backblaze_summary": "Almacenamiento de objetos compatible con S3 con 10 GB gratis y sin tarjeta de crédito.",
|
||||||
|
"txt_backup_recommend_backblaze_step_1": "Registre o inicie sesión en una cuenta de Backblaze.",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_prefix": "Abra",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_suffix": ", haga clic en Create a Bucket, introduzca solo el nombre del bucket, deje lo demás sin cambios y créelo.",
|
||||||
|
"txt_backup_recommend_backblaze_step_3": "Después de crearlo, ponga el Endpoint mostrado en S3 Endpoint URL, use el nombre del bucket en Bucket Name y la parte central del endpoint, como us-west-004, en Region.",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_prefix": "Abra",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_suffix": ", haga clic en Add a New Application Key, introduzca cualquier Name of Key, deje lo demás sin cambios y créelo.",
|
||||||
|
"txt_backup_recommend_backblaze_step_5": "Use keyID como clave de acceso y applicationKey como clave secreta.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_summary": "Almacenamiento de objetos compatible con S3 con 10 GB gratis, pero requiere verificación con tarjeta de crédito.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_bucket_link": "página para crear bucket",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_api_link": "página de token API",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "Abra la",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ", introduzca solo el nombre del bucket y créelo directamente.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "Abra la",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ", seleccione Object Read & Write en permisos y créelo directamente.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_3": "Ignore el valor del token después de crearlo. Use Access Key ID como ID de acceso y Secret Access Key como contraseña de acceso.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_4": "Copie la dirección mostrada abajo en S3 Endpoint URL, rellene Bucket Name tal como aparece y deje Region en auto.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_5": "Configure Path Prefix si lo necesita, por ejemplo nodewarden, o déjelo vacío si no quiere un prefijo de carpeta.",
|
||||||
|
"txt_backup_recommend_s3_path_prefix_step": "Configure Path Prefix si lo necesita, por ejemplo nodewarden, o déjelo vacío si no quiere un prefijo de carpeta.",
|
||||||
|
"txt_backup_recommend_tigris_summary": "Almacenamiento de objetos compatible con S3 con 5 GB gratis y sin tarjeta de crédito.",
|
||||||
|
"txt_backup_recommend_tigris_signup_link": "página de registro",
|
||||||
|
"txt_backup_recommend_tigris_bucket_link": "página Create Bucket",
|
||||||
|
"txt_backup_recommend_tigris_access_key_link": "página Create Access Key",
|
||||||
|
"txt_backup_recommend_tigris_step_1_prefix": "Abra la",
|
||||||
|
"txt_backup_recommend_tigris_step_1_suffix": ", regístrese e inicie sesión en Tigris.",
|
||||||
|
"txt_backup_recommend_tigris_step_2_prefix": "Abra",
|
||||||
|
"txt_backup_recommend_tigris_step_2_suffix": ", introduzca solo el nombre del bucket, deje todo lo demás sin cambios y créelo.",
|
||||||
|
"txt_backup_recommend_tigris_step_3_prefix": "Luego abra la",
|
||||||
|
"txt_backup_recommend_tigris_step_3_suffix": ", use cualquier nombre y créela.",
|
||||||
|
"txt_backup_recommend_tigris_step_4": "Ignore Endpoint URL IAM después de crearla. Rellene los demás valores mostrados en la página de copia de seguridad usando los nombres correspondientes.",
|
||||||
|
"txt_backup_recommend_tigris_step_5": "Por último, haga clic en Manage Key Permissions y active Admin Access; de lo contrario, no podrá escribir copias de seguridad.",
|
||||||
"txt_backup_add_destination": "Añadir destino",
|
"txt_backup_add_destination": "Añadir destino",
|
||||||
"txt_backup_schedule_panel_title": "Programación automática",
|
"txt_backup_schedule_panel_title": "Programación automática",
|
||||||
"txt_backup_schedule_panel_note": "Cada destino puede mantener su propia programación de copia de seguridad diaria.",
|
"txt_backup_schedule_panel_note": "Cada destino puede mantener su propia programación de copia de seguridad diaria.",
|
||||||
|
|||||||
@@ -86,6 +86,37 @@ const ru: Record<string, string> = {
|
|||||||
"txt_backup_recommend_pcloud_step_1": "Зарегистрируйте учетную запись pCloud, используя только свой адрес электронной почты.",
|
"txt_backup_recommend_pcloud_step_1": "Зарегистрируйте учетную запись pCloud, используя только свой адрес электронной почты.",
|
||||||
"txt_backup_recommend_pcloud_step_2": "Используйте https://webdav.ploud.com/ в качестве URL-адреса сервера WebDAV.",
|
"txt_backup_recommend_pcloud_step_2": "Используйте https://webdav.ploud.com/ в качестве URL-адреса сервера WebDAV.",
|
||||||
"txt_backup_recommend_pcloud_step_3": "Используйте свой регистрационный адрес электронной почты в качестве имени пользователя WebDAV и пароль своей учетной записи в качестве пароля WebDAV.",
|
"txt_backup_recommend_pcloud_step_3": "Используйте свой регистрационный адрес электронной почты в качестве имени пользователя WebDAV и пароль своей учетной записи в качестве пароля WebDAV.",
|
||||||
|
"txt_backup_recommend_backblaze_summary": "S3-совместимое объектное хранилище с бесплатными 10 ГБ и без кредитной карты.",
|
||||||
|
"txt_backup_recommend_backblaze_step_1": "Зарегистрируйте учетную запись Backblaze или войдите в нее.",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_prefix": "Откройте",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_suffix": ", нажмите Create a Bucket, введите только имя bucket, оставьте остальные настройки без изменений и создайте его.",
|
||||||
|
"txt_backup_recommend_backblaze_step_3": "После создания вставьте показанный Endpoint в S3 Endpoint URL, имя bucket укажите в Bucket Name, а среднюю часть endpoint, например us-west-004, используйте как Region.",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_prefix": "Откройте",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_suffix": ", нажмите Add a New Application Key, введите любое Name of Key, оставьте остальные настройки без изменений и создайте ключ.",
|
||||||
|
"txt_backup_recommend_backblaze_step_5": "Используйте keyID как ключ доступа, а applicationKey как секретный ключ.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_summary": "S3-совместимое объектное хранилище с бесплатными 10 ГБ, но с обязательной проверкой кредитной карты.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_bucket_link": "страницу создания bucket",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_api_link": "страницу API token",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "Откройте",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ", введите только имя bucket и сразу создайте его.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "Откройте",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ", выберите Object Read & Write в разрешениях и сразу создайте токен.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_3": "После создания игнорируйте token value. Введите Access Key ID как ID доступа, а Secret Access Key как пароль доступа.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_4": "Вставьте показанный ниже адрес в S3 Endpoint URL, заполните Bucket Name как показано и оставьте Region в значении auto.",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_5": "Укажите Path Prefix при необходимости, например nodewarden, или оставьте пустым, если префикс папки не нужен.",
|
||||||
|
"txt_backup_recommend_s3_path_prefix_step": "Укажите Path Prefix при необходимости, например nodewarden, или оставьте пустым, если префикс папки не нужен.",
|
||||||
|
"txt_backup_recommend_tigris_summary": "S3-совместимое объектное хранилище с бесплатными 5 ГБ и без кредитной карты.",
|
||||||
|
"txt_backup_recommend_tigris_signup_link": "страницу регистрации",
|
||||||
|
"txt_backup_recommend_tigris_bucket_link": "страницу Create Bucket",
|
||||||
|
"txt_backup_recommend_tigris_access_key_link": "страницу Create Access Key",
|
||||||
|
"txt_backup_recommend_tigris_step_1_prefix": "Откройте",
|
||||||
|
"txt_backup_recommend_tigris_step_1_suffix": ", зарегистрируйтесь и войдите в Tigris.",
|
||||||
|
"txt_backup_recommend_tigris_step_2_prefix": "Откройте",
|
||||||
|
"txt_backup_recommend_tigris_step_2_suffix": ", введите только имя bucket, ничего больше не меняйте и создайте его.",
|
||||||
|
"txt_backup_recommend_tigris_step_3_prefix": "Затем откройте",
|
||||||
|
"txt_backup_recommend_tigris_step_3_suffix": ", введите любое имя и создайте ключ.",
|
||||||
|
"txt_backup_recommend_tigris_step_4": "После создания игнорируйте Endpoint URL IAM. Остальные показанные значения заполните на странице резервного копирования по совпадающим названиям полей.",
|
||||||
|
"txt_backup_recommend_tigris_step_5": "В конце нажмите Manage Key Permissions и включите Admin Access, иначе запись резервных копий не будет работать.",
|
||||||
"txt_backup_add_destination": "Добавить пункт назначения",
|
"txt_backup_add_destination": "Добавить пункт назначения",
|
||||||
"txt_backup_schedule_panel_title": "Автоматическое расписание",
|
"txt_backup_schedule_panel_title": "Автоматическое расписание",
|
||||||
"txt_backup_schedule_panel_note": "Каждый пункт назначения может иметь собственный ежедневный график резервного копирования.",
|
"txt_backup_schedule_panel_note": "Каждый пункт назначения может иметь собственный ежедневный график резервного копирования.",
|
||||||
|
|||||||
@@ -85,6 +85,37 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_backup_recommend_pcloud_step_1": "先用邮箱注册一个 pCloud 账号。",
|
"txt_backup_recommend_pcloud_step_1": "先用邮箱注册一个 pCloud 账号。",
|
||||||
"txt_backup_recommend_pcloud_step_2": "WebDAV 地址填写 https://webdav.pcloud.com/ 。",
|
"txt_backup_recommend_pcloud_step_2": "WebDAV 地址填写 https://webdav.pcloud.com/ 。",
|
||||||
"txt_backup_recommend_pcloud_step_3": "注册邮箱用作 WebDAV 用户名,注册密码用作 WebDAV 密码。",
|
"txt_backup_recommend_pcloud_step_3": "注册邮箱用作 WebDAV 用户名,注册密码用作 WebDAV 密码。",
|
||||||
|
"txt_backup_recommend_backblaze_summary": "兼容 S3 的对象存储,免费容量 10 GB,无需信用卡。",
|
||||||
|
"txt_backup_recommend_backblaze_step_1": "先注册或登录 Backblaze 账号。",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_prefix": "打开",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_suffix": ",点击创建一个桶,只输入桶名字,其他地方不修改,然后创建。",
|
||||||
|
"txt_backup_recommend_backblaze_step_3": "创建后显示的 Endpoint 填到 S3 端点 URL;桶名字填到存储桶名称;区域填 Endpoint 中间那段,例如 us-west-004。",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_prefix": "打开",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_suffix": ",点击 Add a New Application Key,随便输入 Name of Key,其他地方不动,然后创建。",
|
||||||
|
"txt_backup_recommend_backblaze_step_5": "生成结果里的 keyID 填到 访问 ID,applicationKey 填到 访问密码。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_summary": "兼容 S3 的对象存储,免费容量 10 GB,需要信用卡认证。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_bucket_link": "创建储存桶页面",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_api_link": "API 创建页面",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "打开",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ",只输入存储桶名称,直接创建。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "打开",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ",权限全选“对象读和写”,直接创建。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_3": "创建后令牌值不用管;Access Key ID 填到 访问 ID,Secret Access Key 填到 访问密码。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_4": "把下面显示的地址填到 S3 端点 URL;存储桶名称如实填写;区域保持 auto 不改。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_5": "路径前缀按需要填写,例如 nodewarden;不想分目录可以留空。",
|
||||||
|
"txt_backup_recommend_s3_path_prefix_step": "路径前缀按需要填写,例如 nodewarden;不想分目录可以留空。",
|
||||||
|
"txt_backup_recommend_tigris_summary": "兼容 S3 的对象存储。免费容量 5 GB,无需信用卡。",
|
||||||
|
"txt_backup_recommend_tigris_signup_link": "注册页面",
|
||||||
|
"txt_backup_recommend_tigris_bucket_link": "Create Bucket 页面",
|
||||||
|
"txt_backup_recommend_tigris_access_key_link": "Create Access Key 页面",
|
||||||
|
"txt_backup_recommend_tigris_step_1_prefix": "打开",
|
||||||
|
"txt_backup_recommend_tigris_step_1_suffix": ",注册并登录 Tigris。",
|
||||||
|
"txt_backup_recommend_tigris_step_2_prefix": "打开",
|
||||||
|
"txt_backup_recommend_tigris_step_2_suffix": ",只输入桶的名字,其他地方不动,直接创建。",
|
||||||
|
"txt_backup_recommend_tigris_step_3_prefix": "然后打开",
|
||||||
|
"txt_backup_recommend_tigris_step_3_suffix": ",名字随意,直接创建。",
|
||||||
|
"txt_backup_recommend_tigris_step_4": "创建后显示的 Endpoint URL IAM 不用管;其余显示出来的内容按名称填写到备份页面里。",
|
||||||
|
"txt_backup_recommend_tigris_step_5": "最后点击 Manage Key Permissions,把 Admin Access 打开,否则无法写入。",
|
||||||
"txt_backup_add_destination": "新增地点",
|
"txt_backup_add_destination": "新增地点",
|
||||||
"txt_backup_schedule_panel_title": "自动备份计划",
|
"txt_backup_schedule_panel_title": "自动备份计划",
|
||||||
"txt_backup_schedule_panel_note": "每个备份地点都可以单独配置自己的每日自动备份计划。",
|
"txt_backup_schedule_panel_note": "每个备份地点都可以单独配置自己的每日自动备份计划。",
|
||||||
@@ -266,15 +297,15 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_backup_webdav_username": "WebDAV 用户名",
|
"txt_backup_webdav_username": "WebDAV 用户名",
|
||||||
"txt_backup_webdav_password": "WebDAV 密码",
|
"txt_backup_webdav_password": "WebDAV 密码",
|
||||||
"txt_backup_webdav_path": "远程目录",
|
"txt_backup_webdav_path": "远程目录",
|
||||||
"txt_backup_s3_endpoint": "S3 端点",
|
"txt_backup_s3_endpoint": "S3 端点 URL",
|
||||||
"txt_backup_s3_addressing_style": "S3 寻址方式",
|
"txt_backup_s3_addressing_style": "寻址方式",
|
||||||
"txt_backup_s3_addressing_path_style": "path-style(默认)",
|
"txt_backup_s3_addressing_path_style": "path-style(默认)",
|
||||||
"txt_backup_s3_addressing_virtual_hosted_style": "virtual-hosted-style",
|
"txt_backup_s3_addressing_virtual_hosted_style": "virtual-hosted-style",
|
||||||
"txt_backup_s3_bucket": "存储桶",
|
"txt_backup_s3_bucket": "存储桶名称",
|
||||||
"txt_backup_s3_region": "区域",
|
"txt_backup_s3_region": "区域",
|
||||||
"txt_backup_s3_access_key": "访问密钥",
|
"txt_backup_s3_access_key": "访问 ID",
|
||||||
"txt_backup_s3_secret_key": "秘密密钥",
|
"txt_backup_s3_secret_key": "访问密码",
|
||||||
"txt_backup_s3_path": "远程路径",
|
"txt_backup_s3_path": "路径前缀",
|
||||||
"txt_backup_reserved_name": "预留类型名称",
|
"txt_backup_reserved_name": "预留类型名称",
|
||||||
"txt_backup_reserved_notes": "预留备注",
|
"txt_backup_reserved_notes": "预留备注",
|
||||||
"txt_backup_reserved_notes_placeholder": "给下一个备份地点先留个说明",
|
"txt_backup_reserved_notes_placeholder": "给下一个备份地点先留个说明",
|
||||||
|
|||||||
@@ -85,6 +85,37 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_backup_recommend_pcloud_step_1": "先用郵箱註冊一個 pCloud 賬號。",
|
"txt_backup_recommend_pcloud_step_1": "先用郵箱註冊一個 pCloud 賬號。",
|
||||||
"txt_backup_recommend_pcloud_step_2": "WebDAV 地址填寫 https://webdav.pcloud.com/ 。",
|
"txt_backup_recommend_pcloud_step_2": "WebDAV 地址填寫 https://webdav.pcloud.com/ 。",
|
||||||
"txt_backup_recommend_pcloud_step_3": "註冊郵箱用作 WebDAV 用戶名,註冊密碼用作 WebDAV 密碼。",
|
"txt_backup_recommend_pcloud_step_3": "註冊郵箱用作 WebDAV 用戶名,註冊密碼用作 WebDAV 密碼。",
|
||||||
|
"txt_backup_recommend_backblaze_summary": "兼容 S3 的對象儲存,免費容量 10 GB,無需信用卡。",
|
||||||
|
"txt_backup_recommend_backblaze_step_1": "先註冊或登入 Backblaze 賬號。",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_prefix": "打開",
|
||||||
|
"txt_backup_recommend_backblaze_step_2_suffix": ",點擊創建一個桶,只輸入桶名字,其他地方不修改,然後創建。",
|
||||||
|
"txt_backup_recommend_backblaze_step_3": "創建後顯示的 Endpoint 填到 S3 端點 URL;桶名字填到儲存桶名稱;區域填 Endpoint 中間那段,例如 us-west-004。",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_prefix": "打開",
|
||||||
|
"txt_backup_recommend_backblaze_step_4_suffix": ",點擊 Add a New Application Key,隨便輸入 Name of Key,其他地方不動,然後創建。",
|
||||||
|
"txt_backup_recommend_backblaze_step_5": "生成結果裡的 keyID 填存取金鑰,applicationKey 填秘密金鑰。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_summary": "兼容 S3 的對象儲存,免費容量 10 GB,需要信用卡驗證。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_bucket_link": "創建儲存桶頁面",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_api_link": "API 創建頁面",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_prefix": "打開",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_1_suffix": ",只輸入儲存桶名稱,直接創建。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_prefix": "打開",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_2_suffix": ",權限全選「對象讀和寫」,直接創建。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_3": "創建後令牌值不用管;Access Key ID 填到存取 ID,Secret Access Key 填到存取密碼。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_4": "把下面顯示的地址填到 S3 端點 URL;儲存桶名稱如實填寫;區域保持 auto 不改。",
|
||||||
|
"txt_backup_recommend_cloudflare_r2_step_5": "路徑前綴按需要填寫,例如 nodewarden;不想分目錄可以留空。",
|
||||||
|
"txt_backup_recommend_s3_path_prefix_step": "路徑前綴按需要填寫,例如 nodewarden;不想分目錄可以留空。",
|
||||||
|
"txt_backup_recommend_tigris_summary": "兼容 S3 的對象儲存。免費容量 5 GB,無需信用卡。",
|
||||||
|
"txt_backup_recommend_tigris_signup_link": "註冊頁面",
|
||||||
|
"txt_backup_recommend_tigris_bucket_link": "Create Bucket 頁面",
|
||||||
|
"txt_backup_recommend_tigris_access_key_link": "Create Access Key 頁面",
|
||||||
|
"txt_backup_recommend_tigris_step_1_prefix": "打開",
|
||||||
|
"txt_backup_recommend_tigris_step_1_suffix": ",註冊並登入 Tigris。",
|
||||||
|
"txt_backup_recommend_tigris_step_2_prefix": "打開",
|
||||||
|
"txt_backup_recommend_tigris_step_2_suffix": ",只輸入桶的名字,其他地方不動,直接創建。",
|
||||||
|
"txt_backup_recommend_tigris_step_3_prefix": "然後打開",
|
||||||
|
"txt_backup_recommend_tigris_step_3_suffix": ",名字隨意,直接創建。",
|
||||||
|
"txt_backup_recommend_tigris_step_4": "創建後顯示的 Endpoint URL IAM 不用管;其餘顯示出來的內容按名稱填寫到備份頁面裡。",
|
||||||
|
"txt_backup_recommend_tigris_step_5": "最後點擊 Manage Key Permissions,把 Admin Access 打開,否則無法寫入。",
|
||||||
"txt_backup_add_destination": "新增地點",
|
"txt_backup_add_destination": "新增地點",
|
||||||
"txt_backup_schedule_panel_title": "自動備份計劃",
|
"txt_backup_schedule_panel_title": "自動備份計劃",
|
||||||
"txt_backup_schedule_panel_note": "每個備份地點都可以單獨配置自己的每日自動備份計劃。",
|
"txt_backup_schedule_panel_note": "每個備份地點都可以單獨配置自己的每日自動備份計劃。",
|
||||||
|
|||||||
@@ -351,6 +351,15 @@
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] .backup-recommendation-step a {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] .backup-recommendation-step a:hover,
|
||||||
|
:root[data-theme='dark'] .backup-recommendation-step a:focus-visible {
|
||||||
|
color: var(--primary-strong);
|
||||||
|
}
|
||||||
|
|
||||||
:root[data-theme='dark'] .restore-progress-overlay {
|
:root[data-theme='dark'] .restore-progress-overlay {
|
||||||
background: var(--overlay-strong);
|
background: var(--overlay-strong);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
|
|||||||
@@ -193,6 +193,18 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backup-recommendation-step a {
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-recommendation-step a:hover,
|
||||||
|
.backup-recommendation-step a:focus-visible {
|
||||||
|
color: #1742b0;
|
||||||
|
}
|
||||||
|
|
||||||
.backup-recommendation-inline-note {
|
.backup-recommendation-inline-note {
|
||||||
color: #475467;
|
color: #475467;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|||||||
Reference in New Issue
Block a user