mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: add backup recommendations and update backup strategy UI
- Introduced new backup recommendations feature with interfaces for recommended storage providers. - Updated i18n translations for backup strategy to reflect new terminology and improved descriptions. - Enhanced types with optional private and public keys in user profiles. - Redesigned backup-related styles for better layout and responsiveness. - Updated TypeScript configuration to include shared modules. - Configured Vite to resolve shared modules and allow filesystem access. - Added cron triggers for periodic tasks in Wrangler configuration.
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
import {
|
||||
type BackupDestinationRecord,
|
||||
type BackupDestinationType,
|
||||
type BackupRuntimeState,
|
||||
type BackupSettings,
|
||||
createBackupDestinationRecord,
|
||||
createDefaultBackupSettings,
|
||||
} from '@shared/backup';
|
||||
import type { RemoteBackupBrowserResponse, RemoteBackupItem } from './api';
|
||||
import { t } from './i18n';
|
||||
|
||||
export interface PersistedRemoteBrowserState {
|
||||
cache: Record<string, RemoteBackupBrowserResponse>;
|
||||
pathByDestination: Record<string, string>;
|
||||
pageByKey: Record<string, number>;
|
||||
selectedDestinationId: string | null;
|
||||
}
|
||||
|
||||
export const REMOTE_BROWSER_STORAGE_KEY = 'nodewarden.backup.remote-browser.v1';
|
||||
export const REMOTE_BROWSER_ITEMS_PER_PAGE = 10;
|
||||
|
||||
export const COMMON_TIME_ZONES = [
|
||||
'UTC',
|
||||
'Asia/Shanghai',
|
||||
'Asia/Tokyo',
|
||||
'Asia/Singapore',
|
||||
'Europe/London',
|
||||
'Europe/Berlin',
|
||||
'America/New_York',
|
||||
'America/Chicago',
|
||||
'America/Denver',
|
||||
'America/Los_Angeles',
|
||||
];
|
||||
|
||||
export const WEEKDAY_OPTIONS = [
|
||||
{ value: 1, label: 'txt_backup_weekday_monday' },
|
||||
{ value: 2, label: 'txt_backup_weekday_tuesday' },
|
||||
{ value: 3, label: 'txt_backup_weekday_wednesday' },
|
||||
{ value: 4, label: 'txt_backup_weekday_thursday' },
|
||||
{ value: 5, label: 'txt_backup_weekday_friday' },
|
||||
{ value: 6, label: 'txt_backup_weekday_saturday' },
|
||||
{ value: 0, label: 'txt_backup_weekday_sunday' },
|
||||
] as const;
|
||||
|
||||
export function detectBrowserTimeZone(): string {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||
} catch {
|
||||
return 'UTC';
|
||||
}
|
||||
}
|
||||
|
||||
function createLocalizedDestinationName(type: BackupDestinationType, index: number): string {
|
||||
if (type === 'e3') return t('txt_backup_destination_name_default_e3', { index: String(index) });
|
||||
if (type === 'placeholder') return `${t('txt_backup_destination_reserved')} ${index}`;
|
||||
return t('txt_backup_destination_name_default_webdav', { index: String(index) });
|
||||
}
|
||||
|
||||
export function createDraftDestinationRecord(type: BackupDestinationType, index: number): BackupDestinationRecord {
|
||||
return createBackupDestinationRecord(type, index, {
|
||||
timezone: detectBrowserTimeZone(),
|
||||
name: createLocalizedDestinationName(type, index),
|
||||
});
|
||||
}
|
||||
|
||||
export function createDraftBackupSettings(): BackupSettings {
|
||||
return createDefaultBackupSettings(detectBrowserTimeZone(), {
|
||||
destinationName: createLocalizedDestinationName('webdav', 1),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatDateTime(value: string | null | undefined): string {
|
||||
if (!value) return t('txt_backup_never');
|
||||
const parsed = new Date(value);
|
||||
if (!Number.isFinite(parsed.getTime())) return value;
|
||||
return parsed.toLocaleString();
|
||||
}
|
||||
|
||||
export function formatBytes(value: number | null | undefined): string {
|
||||
const n = Number(value || 0);
|
||||
if (!Number.isFinite(n) || n <= 0) return t('txt_backup_unknown_size');
|
||||
if (n < 1024) return `${n} B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||||
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
export function isReplaceRequiredError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? String(error.message || '') : '';
|
||||
return message.toLowerCase().includes('fresh instance');
|
||||
}
|
||||
|
||||
export function isZipCandidate(item: RemoteBackupItem): boolean {
|
||||
return !item.isDirectory && /\.zip$/i.test(item.name || '');
|
||||
}
|
||||
|
||||
function getRemoteItemSortTime(item: RemoteBackupItem): number {
|
||||
if (!item.modifiedAt) return 0;
|
||||
const parsed = new Date(item.modifiedAt);
|
||||
return Number.isFinite(parsed.getTime()) ? parsed.getTime() : 0;
|
||||
}
|
||||
|
||||
export function compareRemoteItems(a: RemoteBackupItem, b: RemoteBackupItem): number {
|
||||
const timeDiff = getRemoteItemSortTime(b) - getRemoteItemSortTime(a);
|
||||
if (timeDiff !== 0) return timeDiff;
|
||||
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
||||
return b.name.localeCompare(a.name, 'en');
|
||||
}
|
||||
|
||||
export function getRemoteBrowserCacheKey(destinationId: string, path: string = ''): string {
|
||||
return `${destinationId}:${path}`;
|
||||
}
|
||||
|
||||
function getRemoteBrowserStorage(): Storage | null {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
return window.localStorage;
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage access failures.
|
||||
}
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.sessionStorage) {
|
||||
return window.sessionStorage;
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage access failures.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function loadPersistedRemoteBrowserState(): PersistedRemoteBrowserState {
|
||||
try {
|
||||
const storage = getRemoteBrowserStorage();
|
||||
const raw = storage?.getItem(REMOTE_BROWSER_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return {
|
||||
cache: {},
|
||||
pathByDestination: {},
|
||||
pageByKey: {},
|
||||
selectedDestinationId: null,
|
||||
};
|
||||
}
|
||||
const parsed = JSON.parse(raw) as Partial<PersistedRemoteBrowserState>;
|
||||
return {
|
||||
cache: parsed.cache && typeof parsed.cache === 'object' ? parsed.cache : {},
|
||||
pathByDestination: parsed.pathByDestination && typeof parsed.pathByDestination === 'object' ? parsed.pathByDestination : {},
|
||||
pageByKey: parsed.pageByKey && typeof parsed.pageByKey === 'object' ? parsed.pageByKey : {},
|
||||
selectedDestinationId: typeof parsed.selectedDestinationId === 'string' ? parsed.selectedDestinationId : null,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
cache: {},
|
||||
pathByDestination: {},
|
||||
pageByKey: {},
|
||||
selectedDestinationId: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function persistRemoteBrowserState(state: PersistedRemoteBrowserState): void {
|
||||
try {
|
||||
const storage = getRemoteBrowserStorage();
|
||||
storage?.setItem(REMOTE_BROWSER_STORAGE_KEY, JSON.stringify(state));
|
||||
} catch {
|
||||
// Ignore cache persistence failures.
|
||||
}
|
||||
}
|
||||
|
||||
export function invalidateRemoteBrowserCacheForDestination(
|
||||
destinationId: string,
|
||||
cache: Record<string, RemoteBackupBrowserResponse>,
|
||||
pathByDestination: Record<string, string>,
|
||||
pageByKey: Record<string, number>
|
||||
): PersistedRemoteBrowserState {
|
||||
return {
|
||||
cache: Object.fromEntries(Object.entries(cache).filter(([key]) => !key.startsWith(`${destinationId}:`))),
|
||||
pathByDestination: Object.fromEntries(Object.entries(pathByDestination).filter(([key]) => key !== destinationId)),
|
||||
pageByKey: Object.fromEntries(Object.entries(pageByKey).filter(([key]) => !key.startsWith(`${destinationId}:`))),
|
||||
selectedDestinationId: destinationId,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDestinationById(
|
||||
settings: BackupSettings | null,
|
||||
destinationId: string | null | undefined
|
||||
): BackupDestinationRecord | null {
|
||||
if (!settings || !destinationId) return null;
|
||||
return settings.destinations.find((destination) => destination.id === destinationId) || null;
|
||||
}
|
||||
|
||||
export function getVisibleDestinations(settings: BackupSettings | null | undefined): BackupDestinationRecord[] {
|
||||
return (settings?.destinations || []).filter((destination) => destination.type !== 'placeholder');
|
||||
}
|
||||
|
||||
export function getFirstVisibleDestinationId(settings: BackupSettings | null | undefined): string | null {
|
||||
return getVisibleDestinations(settings)[0]?.id || null;
|
||||
}
|
||||
|
||||
export function getDestinationTypeLabel(type: BackupDestinationType): string {
|
||||
if (type === 'e3') return t('txt_backup_protocol_e3');
|
||||
if (type === 'placeholder') return t('txt_backup_destination_reserved');
|
||||
return t('txt_backup_protocol_webdav');
|
||||
}
|
||||
Reference in New Issue
Block a user