| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289 |
- import { Context } from '@kit.AbilityKit';
- import { fileIo } from '@kit.CoreFileKit';
- import { picker } from '@kit.CoreFileKit';
- import { http } from '@kit.NetworkKit';
- import { rcp } from '@kit.RemoteCommunicationKit';
- import { buffer, util } from '@kit.ArkTS';
- import { cryptoFramework } from '@kit.CryptoArchitectureKit';
- import { PreferencesUtil, ToastUtil, AppUtil } from '@pura/harmony-utils';
- import Logger from './Logger';
- import PlaylistTable from './PlaylistTable';
- import {
- PlaylistBackupData,
- ImportResult,
- BackupHistoryItem,
- BACKUP_FORMAT_VERSION,
- WebDavAccountBackupItem,
- SettingsBackupData,
- SettingBoolItem,
- SettingNumberItem,
- SettingStringItem
- } from '../../viewmodel/PlaylistBackup';
- import { RemoteDriveManager } from './RemoteDriveManager';
- import { WebDavAccount } from '../../viewmodel/WebDavAccount';
- import { RemoteDriveType } from '../enums/RemoteDriveType';
- const TAG = 'heanup PlaylistBackupManager';
- const BACKUP_HISTORY_KEY = 'playlist_backup_history';
- const AUTO_BACKUP_ENABLED_KEY = 'playlist_auto_backup_enabled';
- const MAX_HISTORY_ITEMS = 20;
- const AUTO_BACKUP_DELAY_MS = 5 * 60 * 1000; // 5分钟
- /**
- * 备份JSON校验结果
- */
- export interface ValidateResult {
- valid: boolean;
- errorMessage: string;
- data: PlaylistBackupData | null;
- }
- /**
- * 网盘账户导入统计
- */
- interface AccountImportResult {
- imported: number;
- skipped: number;
- }
- /**
- * 加密结果
- */
- interface EncryptionResult {
- cipherText: string; // base64
- salt: string; // base64
- iv: string; // base64
- tag: string; // base64
- }
- /**
- * 歌单备份管理器
- */
- export class PlaylistBackupManager {
- private static instance: PlaylistBackupManager | null = null;
- private context: Context | null = null;
- private autoBackupTimer: number = -1;
- private constructor() {
- }
- static getInstance(): PlaylistBackupManager {
- if (!PlaylistBackupManager.instance) {
- PlaylistBackupManager.instance = new PlaylistBackupManager();
- }
- return PlaylistBackupManager.instance;
- }
- setContext(context: Context): void {
- this.context = context;
- // 注册自动备份回调到 PlaylistTable(避免循环依赖)
- PlaylistTable.registerAutoBackupCallback(() => {
- this.scheduleAutoBackup();
- });
- }
- // ========== 序列化 ==========
- /**
- * 导出完整备份为 JSON 字符串(v2:歌单 + 网盘配置 + 设置项)
- * @param passphrase 可选加密密码,不为空时对密码字段加密
- */
- async exportToJson(passphrase: string = ''): Promise<string> {
- if (!this.context) {
- Logger.error(TAG, 'exportToJson: context 未设置');
- return '';
- }
- try {
- // 1. 导出歌单
- const playlistTable = new PlaylistTable(this.context);
- const playlistData = await playlistTable.exportAllPlaylists();
- // 2. 导出网盘账户
- const accounts = this.exportWebDavAccounts();
- // 3. 导出用户设置
- const settings = this.exportUserSettings();
- // 4. 构建 v2 备份数据
- const backupData: PlaylistBackupData = {
- version: BACKUP_FORMAT_VERSION,
- exportTime: new Date().toISOString(),
- appVersion: AppUtil.getVersionName(),
- playlists: playlistData.playlists,
- webDavAccounts: accounts,
- userSettings: settings
- };
- // 5. 加密敏感数据(密码)
- if (passphrase.length > 0 && accounts.length > 0) {
- const passwords: string[] = [];
- for (let i = 0; i < accounts.length; i++) {
- passwords.push(accounts[i].password);
- accounts[i].password = ''; // 清除明文密码
- }
- const encResult = await this.encryptData(JSON.stringify(passwords), passphrase);
- backupData.encrypted = true;
- backupData.encryptionSalt = encResult.salt;
- backupData.encryptionIv = encResult.iv;
- backupData.encryptionTag = encResult.tag;
- backupData.encryptedPasswords = encResult.cipherText;
- Logger.info(TAG, 'exportToJson: 密码已加密');
- }
- const jsonStr = JSON.stringify(backupData);
- Logger.info(TAG, `exportToJson: 导出成功, ${backupData.playlists.length} 个歌单, ${accounts.length} 个网盘账户, 加密=${backupData.encrypted === true}, ${jsonStr.length} 字节`);
- return jsonStr;
- } catch (error) {
- Logger.error(TAG, `exportToJson: 导出失败: ${(error as Error).message}`);
- return '';
- }
- }
- /**
- * 检查备份 JSON 是否已加密
- */
- isBackupEncrypted(jsonStr: string): boolean {
- try {
- const parsed = JSON.parse(jsonStr) as PlaylistBackupData;
- return parsed.encrypted === true;
- } catch (e) {
- return false;
- }
- }
- /**
- * 导出所有网盘账户
- */
- private exportWebDavAccounts(): WebDavAccountBackupItem[] {
- const backupItems: WebDavAccountBackupItem[] = [];
- try {
- const manager = RemoteDriveManager.getInstance();
- const allAccounts = manager.getAllWebDavAccounts();
- for (let i = 0; i < allAccounts.length; i++) {
- const acct = allAccounts[i];
- const item: WebDavAccountBackupItem = {
- name: acct.name,
- isActivate: acct.isActivate,
- sortOrder: acct.sortOrder,
- host: acct.host,
- localHost: acct.localHost,
- isUseLocalHost: acct.isUseLocalHost,
- port: acct.port,
- enableHttps: acct.enableHttps,
- filepath: acct.filepath,
- imageFilePath: acct.imageFilePath,
- lyricFilePath: acct.lyricFilePath,
- uploadFilePath: acct.uploadFilePath,
- coverPath: acct.coverPath || '',
- account: acct.account,
- password: acct.password,
- webType: acct.webType,
- smbShare: acct.smbShare,
- smbDomain: acct.smbDomain,
- navidromeBasePath: acct.navidromeBasePath,
- jellyfinBasePath: acct.jellyfinBasePath,
- embyBasePath: acct.embyBasePath,
- ftpEncoding: acct.ftpEncoding
- };
- backupItems.push(item);
- }
- Logger.info(TAG, `exportWebDavAccounts: 导出 ${backupItems.length} 个网盘账户`);
- } catch (error) {
- Logger.error(TAG, `exportWebDavAccounts: 失败: ${(error as Error).message}`);
- }
- return backupItems;
- }
- /**
- * 导出用户设置
- */
- private exportUserSettings(): SettingsBackupData {
- const boolKeys: string[] = [
- 'isBgPlayOpen', 'isAutoRatate', 'isMemoryPlay', 'isMusicMemoryPlay',
- 'isStartAutoPlay', 'isMemoryLastPlay', 'isSameTimePlay', 'isSavePlayMode',
- 'isMediacodec', 'preload_next_song', 'is_customize_bg', 'isMusicBGCover',
- 'is_grid_music', 'isCoverRectangle', 'isCircleBtn', 'isAutoScrollHide',
- 'IS_SHOW_ALLBAR', 'IS_SHOW_TITLTBAR', 'IS_SHOW_PLAYPAGE_BACK', 'IS_SHOW_SLLYRIC',
- 'IS_SHOW_HEADER', 'IS_COVER_TOP_BIG', 'IS_PLAYLIST_BG_GRASS', 'IS_AUTO_HIDE_PROGRESS',
- 'isSwipe', 'isShowSimi', 'isShowFAV', 'isShowHistory',
- 'isShowPrecious', 'isShowBackFast', 'openSkipSongAnimate', 'isShowFileName',
- 'isLongNameRoLL', 'showFindLocation', 'showZMIndex', 'autoHideTitle',
- 'autoParseMusicName', 'isNoJumpToHome', 'isCopyFileToDownLoad', 'isDeleteYuan',
- 'isDeletePicture', 'isDeleteLrc', 'webdavUploadAutoClear', 'webdavUploadAllowMobile',
- 'volumeSmall'
- ];
- const numberKeys: string[] = [
- 'longPressSpeed', 'twoFingerType', 'sonTwoFingerType', 'musicSortType',
- 'webDavSortType', 'navidromeSortType', 'customize_bg_blur', 'bg_brightness',
- 'lastColumns', 'defalut_home_type', 'themeMode', 'webdavUploadRetryCount'
- ];
- const stringKeys: string[] = [
- 'THEME_COLOR', 'is_customize_bg_path', 'fastForwardSeconds',
- 'webdavUploadDuplicateAction', 'LRC_API', 'COVER_API'
- ];
- const boolSettings: SettingBoolItem[] = [];
- for (let i = 0; i < boolKeys.length; i++) {
- const key = boolKeys[i];
- boolSettings.push({ key: key, value: PreferencesUtil.getBooleanSync(key, false) });
- }
- const numberSettings: SettingNumberItem[] = [];
- for (let i = 0; i < numberKeys.length; i++) {
- const key = numberKeys[i];
- numberSettings.push({ key: key, value: PreferencesUtil.getNumberSync(key, 0) });
- }
- const stringSettings: SettingStringItem[] = [];
- for (let i = 0; i < stringKeys.length; i++) {
- const key = stringKeys[i];
- const value = PreferencesUtil.getStringSync(key, '');
- // 跳过未设置的 string(空字符串),避免恢复时覆盖默认值
- if (value.length > 0) {
- stringSettings.push({ key: key, value: value });
- }
- }
- Logger.info(TAG, `exportUserSettings: 导出 ${boolSettings.length + numberSettings.length + stringSettings.length} 个设置项`);
- return {
- booleanSettings: boolSettings,
- numberSettings: numberSettings,
- stringSettings: stringSettings
- };
- }
- /**
- * 校验备份 JSON 字符串
- */
- validateBackupJson(jsonStr: string): ValidateResult {
- const result: ValidateResult = {
- valid: false,
- errorMessage: '',
- data: null
- };
- if (!jsonStr || jsonStr.length === 0) {
- result.errorMessage = '备份文件内容为空';
- return result;
- }
- try {
- const parsed = JSON.parse(jsonStr) as PlaylistBackupData;
- if (parsed.version === undefined || parsed.version === null) {
- result.errorMessage = '备份文件缺少版本号';
- return result;
- }
- if (parsed.version > BACKUP_FORMAT_VERSION) {
- result.errorMessage = `备份文件版本 (${parsed.version}) 不兼容,当前支持版本 ${BACKUP_FORMAT_VERSION}`;
- return result;
- }
- if (!parsed.playlists || !Array.isArray(parsed.playlists)) {
- result.errorMessage = '备份文件格式无效:缺少 playlists 字段';
- return result;
- }
- if (!parsed.exportTime) {
- result.errorMessage = '备份文件格式无效:缺少 exportTime 字段';
- return result;
- }
- // 校验每个歌单的必要字段
- for (let i = 0; i < parsed.playlists.length; i++) {
- const playlist = parsed.playlists[i];
- if (!playlist.name) {
- result.errorMessage = `备份文件格式无效:第 ${i + 1} 个歌单缺少名称`;
- return result;
- }
- if (!playlist.songs || !Array.isArray(playlist.songs)) {
- result.errorMessage = `备份文件格式无效:歌单 "${playlist.name}" 缺少 songs 字段`;
- return result;
- }
- }
- // v2 格式校验网盘账户(可选字段)
- if (parsed.version >= 2 && parsed.webDavAccounts !== undefined) {
- if (!Array.isArray(parsed.webDavAccounts)) {
- result.errorMessage = '备份文件格式无效:webDavAccounts 必须是数组';
- return result;
- }
- for (let i = 0; i < parsed.webDavAccounts.length; i++) {
- const acct = parsed.webDavAccounts[i];
- if (!acct.host || acct.port === undefined) {
- result.errorMessage = `备份文件格式无效:第 ${i + 1} 个网盘账户缺少必要字段`;
- return result;
- }
- }
- }
- // v2 格式校验用户设置(可选字段)
- if (parsed.version >= 2 && parsed.userSettings !== undefined) {
- const settings = parsed.userSettings;
- if (!settings.booleanSettings || !settings.numberSettings || !settings.stringSettings) {
- result.errorMessage = '备份文件格式无效:userSettings 缺少必要字段';
- return result;
- }
- }
- result.valid = true;
- result.data = parsed;
- return result;
- } catch (error) {
- result.errorMessage = `备份文件不是有效的 JSON 格式: ${(error as Error).message}`;
- return result;
- }
- }
- /**
- * 从 JSON 字符串导入完整备份(歌单 + 网盘配置 + 设置项)
- * @param passphrase 解密密码(若备份已加密则必填)
- */
- async importFromJson(
- jsonStr: string,
- conflictsToSkip: Set<string>,
- conflictsToOverwrite: Set<string>,
- conflictsToRename: Set<string>,
- passphrase: string = ''
- ): Promise<ImportResult | null> {
- if (!this.context) {
- Logger.error(TAG, 'importFromJson: context 未设置');
- return null;
- }
- const validateResult = this.validateBackupJson(jsonStr);
- if (!validateResult.valid || !validateResult.data) {
- ToastUtil.showToast(validateResult.errorMessage);
- return null;
- }
- const backupData = validateResult.data;
- // 解密密码字段
- if (backupData.encrypted === true && backupData.encryptedPasswords &&
- backupData.webDavAccounts && backupData.webDavAccounts.length > 0) {
- if (passphrase.length === 0) {
- ToastUtil.showToast('该备份已加密,请输入备份密码');
- return null;
- }
- try {
- const decryptedJson = await this.decryptData(
- backupData.encryptedPasswords,
- passphrase,
- backupData.encryptionSalt || '',
- backupData.encryptionIv || '',
- backupData.encryptionTag || ''
- );
- const passwords = JSON.parse(decryptedJson) as string[];
- for (let i = 0; i < backupData.webDavAccounts.length && i < passwords.length; i++) {
- backupData.webDavAccounts[i].password = passwords[i];
- }
- Logger.info(TAG, 'importFromJson: 密码解密成功');
- } catch (error) {
- Logger.error(TAG, `importFromJson: 密码解密失败: ${(error as Error).message}`);
- ToastUtil.showToast('备份密码错误,解密失败');
- return null;
- }
- }
- try {
- // 1. 导入歌单
- const playlistTable = new PlaylistTable(this.context);
- const playlistResult = await playlistTable.importPlaylists(
- backupData,
- conflictsToSkip,
- conflictsToOverwrite,
- conflictsToRename
- );
- const result: ImportResult = {
- totalPlaylists: playlistResult.totalPlaylists,
- importedPlaylists: playlistResult.importedPlaylists,
- skippedPlaylists: playlistResult.skippedPlaylists,
- overwrittenPlaylists: playlistResult.overwrittenPlaylists,
- renamedPlaylists: playlistResult.renamedPlaylists,
- totalSongs: playlistResult.totalSongs,
- importedSongs: playlistResult.importedSongs,
- importedAccounts: 0,
- skippedAccounts: 0,
- settingsImported: false
- };
- // 2. 导入网盘账户(v2)
- if (backupData.version >= 2 && backupData.webDavAccounts && backupData.webDavAccounts.length > 0) {
- const accountResult = await this.importWebDavAccounts(backupData.webDavAccounts);
- result.importedAccounts = accountResult.imported;
- result.skippedAccounts = accountResult.skipped;
- }
- // 3. 导入用户设置(v2)
- if (backupData.version >= 2 && backupData.userSettings) {
- result.settingsImported = this.importUserSettings(backupData.userSettings);
- }
- Logger.info(TAG, `importFromJson: 导入完成 - 歌单 ${result.importedPlaylists}, 网盘账户 ${result.importedAccounts}, 设置 ${result.settingsImported}`);
- return result;
- } catch (error) {
- Logger.error(TAG, `importFromJson: 导入失败: ${(error as Error).message}`);
- return null;
- }
- }
- /**
- * 导入网盘账户(按 host+port+account 去重)
- */
- private async importWebDavAccounts(accounts: WebDavAccountBackupItem[]): Promise<AccountImportResult> {
- const result: AccountImportResult = { imported: 0, skipped: 0 };
- try {
- const manager = RemoteDriveManager.getInstance();
- const existingAccounts = manager.getAllWebDavAccounts();
- for (let i = 0; i < accounts.length; i++) {
- const backupAcct = accounts[i];
- // 检查重复
- let isDuplicate = false;
- for (let j = 0; j < existingAccounts.length; j++) {
- const existing = existingAccounts[j];
- if (existing.host === backupAcct.host &&
- existing.port === backupAcct.port &&
- existing.account === backupAcct.account) {
- isDuplicate = true;
- break;
- }
- }
- if (isDuplicate) {
- result.skipped++;
- Logger.info(TAG, `importWebDavAccounts: 跳过重复账户 ${backupAcct.name} (${backupAcct.host}:${backupAcct.port})`);
- continue;
- }
- // 插入账户(注意参数顺序与 RemoteDriveManager.insertAccount 一致)
- await manager.insertAccount(
- backupAcct.name,
- backupAcct.host,
- backupAcct.localHost,
- backupAcct.isUseLocalHost,
- backupAcct.port,
- backupAcct.filepath,
- backupAcct.lyricFilePath,
- backupAcct.uploadFilePath,
- backupAcct.imageFilePath,
- backupAcct.account,
- backupAcct.password,
- backupAcct.enableHttps,
- backupAcct.coverPath,
- backupAcct.webType,
- backupAcct.smbShare,
- backupAcct.smbDomain,
- backupAcct.navidromeBasePath,
- backupAcct.jellyfinBasePath,
- backupAcct.embyBasePath,
- backupAcct.ftpEncoding,
- '', '', 0, // baiduAccessToken, baiduRefreshToken, baiduTokenExpiresAt(不恢复临时凭证)
- backupAcct.sortOrder
- );
- result.imported++;
- Logger.info(TAG, `importWebDavAccounts: 导入账户 ${backupAcct.name}`);
- }
- Logger.info(TAG, `importWebDavAccounts: 完成 - 导入 ${result.imported}, 跳过 ${result.skipped}`);
- } catch (error) {
- Logger.error(TAG, `importWebDavAccounts: 失败: ${(error as Error).message}`);
- }
- return result;
- }
- /**
- * 导入用户设置
- */
- private importUserSettings(settings: SettingsBackupData): boolean {
- try {
- let count = 0;
- // 导入 boolean 设置
- for (let i = 0; i < settings.booleanSettings.length; i++) {
- const item = settings.booleanSettings[i];
- PreferencesUtil.putSync(item.key, item.value);
- count++;
- }
- // 导入 number 设置
- for (let i = 0; i < settings.numberSettings.length; i++) {
- const item = settings.numberSettings[i];
- PreferencesUtil.putSync(item.key, item.value);
- count++;
- }
- // 导入 string 设置
- for (let i = 0; i < settings.stringSettings.length; i++) {
- const item = settings.stringSettings[i];
- // 跳过空字符串,避免覆盖默认值(如主题色)
- if (item.value.length === 0) {
- continue;
- }
- PreferencesUtil.putSync(item.key, item.value);
- // 特殊处理:THEME_COLOR 需要同步到 AppStorage 的 themeColor
- if (item.key === 'THEME_COLOR') {
- AppStorage.setOrCreate('themeColor', item.value);
- }
- count++;
- }
- Logger.info(TAG, `importUserSettings: 导入 ${count} 个设置项`);
- return true;
- } catch (error) {
- Logger.error(TAG, `importUserSettings: 失败: ${(error as Error).message}`);
- return false;
- }
- }
- /**
- * 检测冲突
- */
- async detectConflicts(jsonStr: string): Promise<string[]> {
- if (!this.context) {
- return [];
- }
- const validateResult = this.validateBackupJson(jsonStr);
- if (!validateResult.valid || !validateResult.data) {
- return [];
- }
- const playlistTable = new PlaylistTable(this.context);
- return playlistTable.detectConflicts(validateResult.data);
- }
- // ========== 本地文件操作 ==========
- /**
- * 导出备份到本地文件
- * @param passphrase 可选加密密码
- */
- async saveToLocal(passphrase: string = ''): Promise<boolean> {
- try {
- const jsonStr = await this.exportToJson(passphrase);
- if (!jsonStr || jsonStr.length === 0) {
- ToastUtil.showToast('导出备份数据失败');
- return false;
- }
- const now = new Date();
- const year = now.getFullYear();
- const month = String(now.getMonth() + 1).padStart(2, '0');
- const day = String(now.getDate()).padStart(2, '0');
- const hours = String(now.getHours()).padStart(2, '0');
- const minutes = String(now.getMinutes()).padStart(2, '0');
- const seconds = String(now.getSeconds()).padStart(2, '0');
- const fileName = `ttmusic_backup_${year}${month}${day}_${hours}${minutes}${seconds}.json`;
- const documentSaveOptions = new picker.DocumentSaveOptions();
- documentSaveOptions.newFileNames = [fileName];
- documentSaveOptions.fileSuffixChoices = ['.json'];
- const documentViewPicker = new picker.DocumentViewPicker();
- const saveResult = await documentViewPicker.save(documentSaveOptions);
- if (!saveResult || saveResult.length === 0) {
- Logger.info(TAG, 'saveToLocal: 用户取消了文件选择');
- return false;
- }
- const uri = saveResult[0];
- const file = fileIo.openSync(uri, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);
- fileIo.writeSync(file.fd, jsonStr);
- fileIo.closeSync(file.fd);
- // 记录历史
- const validateResult = this.validateBackupJson(jsonStr);
- const data = validateResult.data;
- const playlistCount = data ? data.playlists.length : 0;
- const accountCount = (data && data.webDavAccounts) ? data.webDavAccounts.length : 0;
- const hasSettings = !!(data && data.userSettings);
- this.addHistoryRecord('local', playlistCount, fileName, accountCount, hasSettings);
- ToastUtil.showToast(`备份成功: ${fileName}`);
- Logger.info(TAG, `saveToLocal: 文件保存成功: ${uri}`);
- return true;
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `saveToLocal: 保存失败: ${err.message}`);
- ToastUtil.showToast(`备份失败: ${err.message}`);
- return false;
- }
- }
- /**
- * 从本地文件加载备份
- */
- async loadFromLocal(): Promise<string> {
- try {
- const documentSelectOptions = new picker.DocumentSelectOptions();
- documentSelectOptions.fileSuffixFilters = ['.json'];
- documentSelectOptions.maxSelectNumber = 1;
- const documentViewPicker = new picker.DocumentViewPicker();
- const selectResult = await documentViewPicker.select(documentSelectOptions);
- if (!selectResult || selectResult.length === 0) {
- Logger.info(TAG, 'loadFromLocal: 用户取消了文件选择');
- return '';
- }
- const uri = selectResult[0];
- const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
- const stat = fileIo.statSync(file.fd);
- const buffer = new ArrayBuffer(stat.size);
- fileIo.readSync(file.fd, buffer);
- fileIo.closeSync(file.fd);
- const textDecoder = new util.TextDecoder('utf-8');
- const jsonStr = textDecoder.decodeWithStream(new Uint8Array(buffer));
- Logger.info(TAG, `loadFromLocal: 文件读取成功, ${jsonStr.length} 字节`);
- return jsonStr;
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `loadFromLocal: 读取失败: ${err.message}`);
- ToastUtil.showToast(`读取备份文件失败: ${err.message}`);
- return '';
- }
- }
- // ========== WebDAV 备份 ==========
- /**
- * 生成备份文件名
- */
- private generateBackupFileName(): string {
- const now = new Date();
- const year = now.getFullYear();
- const month = String(now.getMonth() + 1).padStart(2, '0');
- const day = String(now.getDate()).padStart(2, '0');
- const hours = String(now.getHours()).padStart(2, '0');
- const minutes = String(now.getMinutes()).padStart(2, '0');
- const seconds = String(now.getSeconds()).padStart(2, '0');
- return `ttmusic_backup_${year}${month}${day}_${hours}${minutes}${seconds}.json`;
- }
- /**
- * 获取 WebDAV 账户的基准路径
- */
- private getWebDavBasePath(account: WebDavAccount): string {
- const base = account.filepath || '/';
- if (base.length === 0) {
- return '/';
- }
- // 确保以 / 开头并以 / 结尾(用于拼接)
- let normalized = base;
- if (!normalized.startsWith('/')) {
- normalized = '/' + normalized;
- }
- return normalized.replace(/\/+$/, '');
- }
- /**
- * 确保 WebDAV 备份目录存在
- */
- private async ensureWebDavBackupDir(account: WebDavAccount): Promise<void> {
- const manager = RemoteDriveManager.getInstance();
- const basePath = this.getWebDavBasePath(account);
- try {
- await manager.createWebDavFolder(account, 'TTMusic', basePath);
- } catch (e) {
- // 目录可能已存在
- }
- try {
- await manager.createWebDavFolder(account, 'backups', basePath + '/TTMusic');
- } catch (e) {
- // 目录可能已存在
- }
- }
- /**
- * 上传备份到 WebDAV
- * @param passphrase 可选加密密码
- */
- async uploadToWebDav(account: WebDavAccount, passphrase: string = ''): Promise<boolean> {
- try {
- const jsonStr = await this.exportToJson(passphrase);
- if (!jsonStr || jsonStr.length === 0) {
- ToastUtil.showToast('导出备份数据失败');
- return false;
- }
- if (!this.context) {
- ToastUtil.showToast('上下文未初始化');
- return false;
- }
- const manager = RemoteDriveManager.getInstance();
- // 确保备份目录存在
- await this.ensureWebDavBackupDir(account);
- const fileName = this.generateBackupFileName();
- // 写入临时文件
- const tempPath = this.context.cacheDir + '/' + fileName;
- const tempFile = fileIo.openSync(tempPath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);
- fileIo.writeSync(tempFile.fd, jsonStr);
- fileIo.closeSync(tempFile.fd);
- // 通过 rcpSocket 上传
- const basePath = this.getWebDavBasePath(account);
- const remotePath = `${basePath}/TTMusic/backups/${fileName}`;
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- await manager.rcpSocket.uploadFile(
- tempPath,
- remotePath,
- host,
- account.port,
- account.account,
- account.password,
- account.enableHttps,
- () => {} // 备份文件很小,不需要进度回调
- );
- // 清理临时文件
- try {
- fileIo.unlinkSync(tempPath);
- } catch (e) {
- // 忽略
- }
- // 记录历史
- const validateResult = this.validateBackupJson(jsonStr);
- const data = validateResult.data;
- const playlistCount = data ? data.playlists.length : 0;
- const accountCount = (data && data.webDavAccounts) ? data.webDavAccounts.length : 0;
- const hasSettings = !!(data && data.userSettings);
- this.addHistoryRecord('webdav', playlistCount, fileName, accountCount, hasSettings);
- ToastUtil.showToast(`WebDAV备份成功: ${fileName}`);
- Logger.info(TAG, `uploadToWebDav: 上传成功: ${remotePath}`);
- return true;
- } catch (error) {
- const err = error as Error;
- Logger.error(TAG, `uploadToWebDav: 上传失败: ${err.message}`);
- ToastUtil.showToast(`WebDAV备份失败: ${err.message}`);
- return false;
- }
- }
- /**
- * 列出 WebDAV 上的备份文件
- */
- async listWebDavBackups(account: WebDavAccount): Promise<string[]> {
- try {
- const manager = RemoteDriveManager.getInstance();
- const basePath = this.getWebDavBasePath(account);
- const files = await manager.rcpSocket.getFileList(
- account.host,
- account.localHost,
- account.isUseLocalHost,
- account.port,
- `${basePath}/TTMusic/backups/`,
- account.account,
- account.password,
- account.enableHttps
- );
- const jsonFiles: string[] = [];
- for (let i = 0; i < files.length; i++) {
- const file = files[i];
- const name = file.fileName ? file.fileName : '';
- if (name.endsWith('.json')) {
- jsonFiles.push(name);
- }
- }
- // 按文件名倒序排列(最新的在前)
- jsonFiles.sort((a, b) => b.localeCompare(a));
- Logger.info(TAG, `listWebDavBackups: 找到 ${jsonFiles.length} 个备份文件`);
- return jsonFiles;
- } catch (error) {
- Logger.error(TAG, `listWebDavBackups: 列表失败: ${(error as Error).message}`);
- return [];
- }
- }
- /**
- * 从 WebDAV 下载备份文件(通过 rcp HTTP GET)
- */
- async downloadFromWebDav(account: WebDavAccount, fileName: string): Promise<string> {
- try {
- if (!this.context) {
- return '';
- }
- const basePath = this.getWebDavBasePath(account);
- const remotePath = `${basePath}/TTMusic/backups/${fileName}`;
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- const protocol = account.enableHttps ? 'https' : 'http';
- const url = `${protocol}://${host}:${account.port}${remotePath}`;
- const encodedCredentials = buffer.from(`${account.account}:${account.password}`).toString('base64');
- const secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' };
- const reqCfg: rcp.Configuration = {
- security: secCfg,
- transfer: {
- timeout: { connectMs: 15000 }
- }
- };
- const sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg };
- const rcpSession = rcp.createSession(sessionCfg);
- const headers: rcp.RequestHeaders = {
- 'Authorization': `Basic ${encodedCredentials}`,
- 'Accept': '*/*'
- };
- const req = new rcp.Request(url, 'GET', headers);
- const response = await rcpSession.fetch(req);
- rcpSession.close();
- if (response.statusCode !== 200) {
- throw new Error(`HTTP ${response.statusCode}`);
- }
- let jsonStr = '';
- if (response.body) {
- const textDecoder = new util.TextDecoder('utf-8');
- jsonStr = textDecoder.decodeWithStream(new Uint8Array(response.body as ArrayBuffer));
- }
- Logger.info(TAG, `downloadFromWebDav: 下载成功: ${fileName}, ${jsonStr.length} 字节`);
- return jsonStr;
- } catch (error) {
- Logger.error(TAG, `downloadFromWebDav: 下载失败: ${(error as Error).message}`);
- ToastUtil.showToast(`下载备份失败: ${(error as Error).message}`);
- return '';
- }
- }
- // ========== 服务器备份(VIP) ==========
- /**
- * 检查 VIP 状态
- */
- isVipUser(): boolean {
- return PreferencesUtil.getBooleanSync('hasActiveSubscription', false);
- }
- /**
- * 上传备份到服务器
- * @param passphrase 可选加密密码
- */
- async uploadToServer(passphrase: string = ''): Promise<boolean> {
- if (!this.isVipUser()) {
- ToastUtil.showToast('该功能仅限VIP用户使用');
- return false;
- }
- try {
- const jsonStr = await this.exportToJson(passphrase);
- if (!jsonStr || jsonStr.length === 0) {
- ToastUtil.showToast('导出备份数据失败');
- return false;
- }
- const token = PreferencesUtil.getStringSync('userToken', '');
- if (!token) {
- ToastUtil.showToast('请先登录');
- return false;
- }
- const httpRequest = http.createHttp();
- const options: http.HttpRequestOptions = {
- method: http.RequestMethod.POST,
- readTimeout: 10000,
- connectTimeout: 10000,
- header: {
- 'Content-Type': 'application/json'
- },
- extraData: JSON.stringify({
- token: token,
- backup_data: jsonStr
- })
- };
- const response = await httpRequest.request(
- 'https://pay.ss5.xyz/backup/playlist/upload',
- options
- );
- if (response.responseCode === 200) {
- const res = response.result as string;
- const json = JSON.parse(res) as ServerBackupResponse;
- if (json.code === 0) {
- const validateResult = this.validateBackupJson(jsonStr);
- const data = validateResult.data;
- const playlistCount = data ? data.playlists.length : 0;
- const accountCount = (data && data.webDavAccounts) ? data.webDavAccounts.length : 0;
- const hasSettings = !!(data && data.userSettings);
- this.addHistoryRecord('server', playlistCount, '', accountCount, hasSettings);
- ToastUtil.showToast('服务器备份成功');
- Logger.info(TAG, 'uploadToServer: 上传成功');
- return true;
- } else if (json.code === 40002 || json.code === 40003) {
- ToastUtil.showToast('登录已过期,请重新登录');
- this.pauseAutoBackup();
- return false;
- } else {
- ToastUtil.showToast(`服务器备份失败: ${json.msg || '未知错误'}`);
- return false;
- }
- } else {
- ToastUtil.showToast('服务器备份请求失败');
- return false;
- }
- } catch (error) {
- Logger.error(TAG, `uploadToServer: 上传失败: ${(error as Error).message}`);
- ToastUtil.showToast(`服务器备份失败: ${(error as Error).message}`);
- return false;
- }
- }
- /**
- * 从服务器下载备份
- */
- async downloadFromServer(backupId: string): Promise<string> {
- if (!this.isVipUser()) {
- ToastUtil.showToast('该功能仅限VIP用户使用');
- return '';
- }
- try {
- const token = PreferencesUtil.getStringSync('userToken', '');
- if (!token) {
- ToastUtil.showToast('请先登录');
- return '';
- }
- const httpRequest = http.createHttp();
- const url = `https://pay.ss5.xyz/backup/playlist/download?token=${encodeURIComponent(token)}&id=${encodeURIComponent(backupId)}`;
- const options: http.HttpRequestOptions = {
- method: http.RequestMethod.GET,
- readTimeout: 10000,
- connectTimeout: 10000,
- };
- const response = await httpRequest.request(url, options);
- if (response.responseCode === 200) {
- const res = response.result as string;
- const json = JSON.parse(res) as ServerBackupDownloadResponse;
- if (json.code === 0 && json.data && json.data.backup_data) {
- Logger.info(TAG, 'downloadFromServer: 下载成功');
- return json.data.backup_data;
- } else if (json.code === 40002 || json.code === 40003) {
- ToastUtil.showToast('登录已过期,请重新登录');
- return '';
- } else {
- ToastUtil.showToast(`下载失败: ${json.msg || '未知错误'}`);
- return '';
- }
- } else {
- ToastUtil.showToast('下载请求失败');
- return '';
- }
- } catch (error) {
- Logger.error(TAG, `downloadFromServer: 下载失败: ${(error as Error).message}`);
- ToastUtil.showToast(`下载失败: ${(error as Error).message}`);
- return '';
- }
- }
- /**
- * 获取服务器备份列表
- */
- async listServerBackups(): Promise<ServerBackupItem[]> {
- if (!this.isVipUser()) {
- return [];
- }
- try {
- const token = PreferencesUtil.getStringSync('userToken', '');
- if (!token) {
- return [];
- }
- const httpRequest = http.createHttp();
- const url = `https://pay.ss5.xyz/backup/playlist/lists?token=${encodeURIComponent(token)}`;
- const options: http.HttpRequestOptions = {
- method: http.RequestMethod.GET,
- readTimeout: 6000,
- connectTimeout: 6000,
- };
- const response = await httpRequest.request(url, options);
- if (response.responseCode === 200) {
- const res = response.result as string;
- const json = JSON.parse(res) as ServerBackupListResponse;
- if (json.code === 0 && json.data && json.data.backups) {
- Logger.info(TAG, `listServerBackups: 获取到 ${json.data.backups.length} 条备份`);
- return json.data.backups;
- }
- }
- return [];
- } catch (error) {
- Logger.error(TAG, `listServerBackups: 获取列表失败: ${(error as Error).message}`);
- return [];
- }
- }
- // ========== 自动备份防抖 ==========
- /**
- * 调度自动备份(防抖)
- */
- scheduleAutoBackup(): void {
- if (!this.isAutoBackupEnabled()) {
- return;
- }
- if (!this.isVipUser()) {
- this.setAutoBackupEnabled(false);
- Logger.info(TAG, 'scheduleAutoBackup: VIP已过期,关闭自动备份');
- return;
- }
- // 清除之前的定时器
- if (this.autoBackupTimer !== -1) {
- clearTimeout(this.autoBackupTimer);
- }
- // 设置新的 5 分钟定时器
- this.autoBackupTimer = setTimeout(() => {
- this.autoBackupTimer = -1;
- this.uploadToServer().then((success: boolean) => {
- if (success) {
- Logger.info(TAG, 'scheduleAutoBackup: 自动备份完成');
- }
- }).catch((error: Error) => {
- Logger.error(TAG, `scheduleAutoBackup: 自动备份失败: ${error.message}`);
- });
- }, AUTO_BACKUP_DELAY_MS);
- Logger.info(TAG, 'scheduleAutoBackup: 已设置 5 分钟后自动备份');
- }
- /**
- * 暂停自动备份
- */
- private pauseAutoBackup(): void {
- if (this.autoBackupTimer !== -1) {
- clearTimeout(this.autoBackupTimer);
- this.autoBackupTimer = -1;
- }
- }
- isAutoBackupEnabled(): boolean {
- return PreferencesUtil.getBooleanSync(AUTO_BACKUP_ENABLED_KEY, false);
- }
- setAutoBackupEnabled(enabled: boolean): void {
- PreferencesUtil.putSync(AUTO_BACKUP_ENABLED_KEY, enabled);
- }
- // ========== 备份历史 ==========
- /**
- * 添加备份历史记录
- */
- addHistoryRecord(type: string, playlistCount: number, filePath: string, accountCount: number = 0, hasSettings: boolean = false): void {
- try {
- const history = this.getBackupHistory();
- const record: BackupHistoryItem = {
- type: type,
- timestamp: new Date().toISOString(),
- playlistCount: playlistCount,
- filePath: filePath,
- accountCount: accountCount,
- hasSettings: hasSettings
- };
- history.unshift(record);
- // 保留最多 20 条
- while (history.length > MAX_HISTORY_ITEMS) {
- history.pop();
- }
- PreferencesUtil.putSync(BACKUP_HISTORY_KEY, JSON.stringify(history));
- } catch (error) {
- Logger.error(TAG, `addHistoryRecord: 保存历史失败: ${(error as Error).message}`);
- }
- }
- /**
- * 获取备份历史
- */
- getBackupHistory(): BackupHistoryItem[] {
- try {
- const jsonStr = PreferencesUtil.getStringSync(BACKUP_HISTORY_KEY, '[]');
- return JSON.parse(jsonStr) as BackupHistoryItem[];
- } catch (error) {
- return [];
- }
- }
- /**
- * 获取仅WebDAV类型的账户列表
- */
- getWebDavAccounts(): WebDavAccount[] {
- const manager = RemoteDriveManager.getInstance();
- const allAccounts = manager.getAllWebDavAccounts();
- const webdavAccounts: WebDavAccount[] = [];
- for (let i = 0; i < allAccounts.length; i++) {
- if (allAccounts[i].webType === RemoteDriveType.WebDav) {
- webdavAccounts.push(allAccounts[i]);
- }
- }
- return webdavAccounts;
- }
- // ========== 加密/解密(AES-256-GCM) ==========
- /**
- * 使用 AES-256-GCM 加密数据
- * 密钥由 passphrase + 随机 salt 通过 SHA-256 派生
- */
- private async encryptData(plaintext: string, passphrase: string): Promise<EncryptionResult> {
- // 生成随机 salt (16字节) 和 IV (12字节)
- const random = cryptoFramework.createRandom();
- const saltBlob = await random.generateRandom(16);
- const ivBlob = await random.generateRandom(12);
- // 派生密钥: SHA-256(passphrase + salt)
- const symKey = await this.deriveKey(passphrase, saltBlob);
- // AES-256-GCM 加密
- const cipher = cryptoFramework.createCipher('AES256|GCM|NoPadding');
- const gcmParams: cryptoFramework.GcmParamsSpec = {
- iv: { data: ivBlob.data },
- aad: { data: new Uint8Array(0) },
- authTag: { data: new Uint8Array(16) },
- algName: 'GcmParamsSpec'
- };
- await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, gcmParams);
- const encoder = new util.TextEncoder();
- const plaintextBytes = encoder.encodeInto(plaintext);
- // GCM doFinal 输出 = 密文 + authTag(末尾16字节)
- const cipherOutput = await cipher.doFinal({ data: plaintextBytes });
- const fullOutput = new Uint8Array(cipherOutput.data);
- const tagLen = 16;
- const cipherBytes = fullOutput.slice(0, fullOutput.byteLength - tagLen);
- const tagBytes = fullOutput.slice(fullOutput.byteLength - tagLen);
- // 使用 Base64Helper 编码
- const base64Helper = new util.Base64Helper();
- const result: EncryptionResult = {
- cipherText: base64Helper.encodeToStringSync(cipherBytes),
- salt: base64Helper.encodeToStringSync(saltBlob.data),
- iv: base64Helper.encodeToStringSync(ivBlob.data),
- tag: base64Helper.encodeToStringSync(tagBytes)
- };
- Logger.info(TAG, `encryptData: fullOutput=${fullOutput.byteLength}B, cipher=${cipherBytes.byteLength}B, tag=${tagBytes.byteLength}B, tagHead=[${tagBytes[0]},${tagBytes[1]},${tagBytes[2]},${tagBytes[3]}]`);
- return result;
- }
- /**
- * 使用 AES-256-GCM 解密数据
- */
- private async decryptData(
- cipherTextB64: string,
- passphrase: string,
- saltB64: string,
- ivB64: string,
- tagB64: string
- ): Promise<string> {
- // 使用 Base64Helper 解码,避免 buffer 内部池导致的数据错位
- const base64Helper = new util.Base64Helper();
- const saltData = base64Helper.decodeSync(saltB64);
- const ivData = base64Helper.decodeSync(ivB64);
- const tagData = base64Helper.decodeSync(tagB64);
- const cipherData = base64Helper.decodeSync(cipherTextB64);
- Logger.info(TAG, `decryptData: cipher=${cipherData.byteLength}B, salt=${saltData.byteLength}B, iv=${ivData.byteLength}B, tag=${tagData.byteLength}B`);
- // 同样方式派生密钥
- const symKey = await this.deriveKey(passphrase, { data: saltData });
- // AES-256-GCM 解密
- const cipher = cryptoFramework.createCipher('AES256|GCM|NoPadding');
- const gcmParams: cryptoFramework.GcmParamsSpec = {
- iv: { data: ivData },
- aad: { data: new Uint8Array(0) },
- authTag: { data: tagData },
- algName: 'GcmParamsSpec'
- };
- await cipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, symKey, gcmParams);
- // GCM 解密:密文(不含authTag)传入 doFinal,authTag 通过 gcmParams 传入
- const decryptOutput = await cipher.doFinal({ data: cipherData });
- const textDecoder = new util.TextDecoder('utf-8');
- return textDecoder.decodeWithStream(new Uint8Array(decryptOutput.data));
- }
- /**
- * 从 passphrase + salt 派生 AES-256 密钥(SHA-256)
- */
- private async deriveKey(passphrase: string, salt: cryptoFramework.DataBlob): Promise<cryptoFramework.SymKey> {
- const md = cryptoFramework.createMd('SHA256');
- const encoder = new util.TextEncoder();
- await md.update({ data: encoder.encodeInto(passphrase) });
- await md.update(salt);
- const hash = await md.digest();
- const keyGenerator = cryptoFramework.createSymKeyGenerator('AES256');
- return await keyGenerator.convertKey(hash);
- }
- }
- // ========== 服务器API响应接口 ==========
- interface ServerBackupResponse {
- code: number;
- msg: string;
- }
- export interface ServerBackupItem {
- id: string;
- timestamp: string;
- playlist_count: number;
- }
- interface ServerBackupDownloadResponse {
- code: number;
- msg: string;
- data: ServerBackupDownloadData;
- }
- interface ServerBackupDownloadData {
- backup_data: string;
- }
- interface ServerBackupListResponse {
- code: number;
- msg: string;
- data: ServerBackupListData;
- }
- interface ServerBackupListData {
- backups: ServerBackupItem[];
- }
|