mirror of
https://github.com/heroblackink/ultimate-todoist-sync-for-obsidian.git
synced 2026-07-22 07:40:27 +00:00
feat: add customizable storage directory with auto-migration
- Change default storage dir from .ultimate-todoist-sync to ultimate-todoist-sync (visible in file manager) - Add storageDirectory and lastStorageDirectory to settings - Implement migrateToNewDirectory() for automatic data migration when directory changes - Smart merge: keep newer files during migration (compare mtime) - Fix listFiles() to use adapter.list() instead of vault.getFiles() - Add Storage Directory setting in Backup & Recovery section - Update BackupOperation and TodoistToObsidian to use new dynamic paths
This commit is contained in:
parent
d1ac1bcb40
commit
0ea2074296
5 changed files with 202 additions and 99 deletions
|
|
@ -22,7 +22,7 @@ export class BackupOperation {
|
|||
}
|
||||
|
||||
getBackupFolder(): string {
|
||||
return this.plugin.storagePathManager?.getBackupsFilesPath() || StoragePathManager.BACKUPS_FILES_FILE;
|
||||
return this.plugin.storagePathManager?.getBackupsFilesPath() || 'ultimate-todoist-sync/backups/files';
|
||||
}
|
||||
|
||||
async backupFile(filePath: string): Promise<string | null> {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ export interface UltimateTodoistSyncSettings {
|
|||
taskId?: string;
|
||||
}>;
|
||||
maxBackupsPerFile: number;
|
||||
storageDirectory: string;
|
||||
lastStorageDirectory: string | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: UltimateTodoistSyncSettings = {
|
||||
|
|
@ -75,6 +77,8 @@ export const DEFAULT_SETTINGS: UltimateTodoistSyncSettings = {
|
|||
logRetentionDays: 365,
|
||||
todayLogs: [],
|
||||
maxBackupsPerFile: 100,
|
||||
storageDirectory: 'ultimate-todoist-sync',
|
||||
lastStorageDirectory: null,
|
||||
}
|
||||
|
||||
export class UltimateTodoistSyncSettingTab extends PluginSettingTab {
|
||||
|
|
@ -464,6 +468,46 @@ export class UltimateTodoistSyncSettingTab extends PluginSettingTab {
|
|||
// ============================================
|
||||
containerEl.createEl('h3', { text: 'Backup & Recovery' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Storage Directory')
|
||||
.setDesc('Directory for plugin data storage. Changing this will migrate existing data.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('ultimate-todoist-sync')
|
||||
.setValue(this.plugin.settings.storageDirectory)
|
||||
.onChange(async (value) => {
|
||||
const newDir = value.trim();
|
||||
const currentDir = this.plugin.settings.storageDirectory;
|
||||
|
||||
if (!newDir) {
|
||||
new Notice('Directory name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newDir === currentDir) return;
|
||||
|
||||
const confirmed = confirm(
|
||||
`Change storage directory from "${currentDir}" to "${newDir}"?\n\n` +
|
||||
'Existing data will be migrated to the new location.'
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (this.plugin.storagePathManager) {
|
||||
const success = await this.plugin.storagePathManager.migrateToNewDirectory(newDir);
|
||||
if (success) {
|
||||
this.plugin.settings.storageDirectory = newDir;
|
||||
await this.plugin.saveSettings();
|
||||
new Notice('Storage directory changed. Please reload the plugin.');
|
||||
} else {
|
||||
new Notice('Migration failed. Check console for details.');
|
||||
}
|
||||
} else {
|
||||
this.plugin.settings.storageDirectory = newDir;
|
||||
await this.plugin.saveSettings();
|
||||
new Notice('Storage directory updated. Please reload the plugin.');
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Backup Todoist Data')
|
||||
.setDesc('Backup all Todoist data to vault.')
|
||||
|
|
|
|||
|
|
@ -113,30 +113,16 @@ export class SettingsBackup {
|
|||
return null;
|
||||
}
|
||||
|
||||
const fileList = await adapter.list(backupDir);
|
||||
const jsonFiles = fileList.files.filter(f =>
|
||||
f.startsWith('settings-') && f.endsWith('.json')
|
||||
);
|
||||
const files = this.app.vault.getFiles()
|
||||
.filter(f => f.path.startsWith(backupDir + '/') && f.name.startsWith('settings-') && f.name.endsWith('.json'))
|
||||
.sort((a, b) => b.stat.mtime - a.stat.mtime);
|
||||
|
||||
if (jsonFiles.length === 0) {
|
||||
if (files.length === 0) {
|
||||
console.log('[SettingsBackup] No backup files found');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取每个文件的修改时间并排序
|
||||
const filesWithTime = await Promise.all(
|
||||
jsonFiles.map(async (fileName) => {
|
||||
const fullPath = `${backupDir}/${fileName}`;
|
||||
const stat = await adapter.stat(fullPath);
|
||||
return {
|
||||
path: fullPath,
|
||||
mtime: stat?.mtime || 0
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
filesWithTime.sort((a, b) => b.mtime - a.mtime);
|
||||
return filesWithTime[0].path;
|
||||
return files[0].path;
|
||||
} catch (error) {
|
||||
console.error('[SettingsBackup] Failed to get latest backup:', error);
|
||||
return null;
|
||||
|
|
@ -154,26 +140,12 @@ export class SettingsBackup {
|
|||
return [];
|
||||
}
|
||||
|
||||
const fileList = await adapter.list(backupDir);
|
||||
const jsonFiles = fileList.files.filter(f =>
|
||||
f.startsWith('settings-') && f.endsWith('.json')
|
||||
);
|
||||
const files = this.app.vault.getFiles()
|
||||
.filter(f => f.path.startsWith(backupDir + '/') && f.name.startsWith('settings-') && f.name.endsWith('.json'))
|
||||
.sort((a, b) => b.stat.mtime - a.stat.mtime);
|
||||
|
||||
// 获取每个文件的修改时间并排序
|
||||
const filesWithTime = await Promise.all(
|
||||
jsonFiles.map(async (fileName) => {
|
||||
const fullPath = `${backupDir}/${fileName}`;
|
||||
const stat = await adapter.stat(fullPath);
|
||||
return {
|
||||
path: fullPath,
|
||||
mtime: stat?.mtime || 0
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
filesWithTime.sort((a, b) => b.mtime - a.mtime);
|
||||
console.log('[SettingsBackup] Found backup files:', filesWithTime.map(f => f.path));
|
||||
return filesWithTime.map(f => f.path);
|
||||
console.log('[SettingsBackup] Found backup files:', files.map(f => f.path));
|
||||
return files.map(f => f.path);
|
||||
} catch (error) {
|
||||
console.error('[SettingsBackup] Failed to get backup list:', error);
|
||||
return [];
|
||||
|
|
@ -220,36 +192,17 @@ export class SettingsBackup {
|
|||
private async cleanOldBackups(): Promise<void> {
|
||||
try {
|
||||
const backupDir = this.getBackupDir();
|
||||
const adapter = this.app.vault.adapter;
|
||||
const files = this.app.vault.getFiles()
|
||||
.filter(f => f.path.startsWith(backupDir + '/') && f.name.startsWith('settings-'))
|
||||
.sort((a, b) => b.stat.mtime - a.stat.mtime);
|
||||
|
||||
const dirExists = await adapter.exists(backupDir);
|
||||
if (!dirExists) return;
|
||||
|
||||
const fileList = await adapter.list(backupDir);
|
||||
const jsonFiles = fileList.files.filter(f =>
|
||||
f.startsWith('settings-') && f.endsWith('.json')
|
||||
);
|
||||
|
||||
if (jsonFiles.length <= this.maxBackups) return;
|
||||
|
||||
// 获取每个文件的修改时间并排序
|
||||
const filesWithTime = await Promise.all(
|
||||
jsonFiles.map(async (fileName) => {
|
||||
const fullPath = `${backupDir}/${fileName}`;
|
||||
const stat = await adapter.stat(fullPath);
|
||||
return {
|
||||
path: fullPath,
|
||||
mtime: stat?.mtime || 0
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
filesWithTime.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
const filesToDelete = filesWithTime.slice(this.maxBackups);
|
||||
for (const file of filesToDelete) {
|
||||
await adapter.remove(file.path);
|
||||
console.log('[SettingsBackup] Deleted old backup:', file.path);
|
||||
if (files.length > this.maxBackups) {
|
||||
const filesToDelete = files.slice(this.maxBackups);
|
||||
const adapter = this.app.vault.adapter;
|
||||
for (const file of filesToDelete) {
|
||||
await adapter.remove(file.path);
|
||||
console.log('[SettingsBackup] Deleted old backup:', file.path);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SettingsBackup] Failed to clean old backups:', error);
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import UltimateTodoistSyncForObsidian from '../main';
|
|||
export class StoragePathManager {
|
||||
private app: App;
|
||||
private plugin: UltimateTodoistSyncForObsidian;
|
||||
private basePath = '.ultimate-todoist-sync';
|
||||
private appendLock = false;
|
||||
|
||||
static readonly SETTINGS_FILE = '.obsidian/plugins/ultimate-todoist-sync-for-obsidian/data.json';
|
||||
static readonly SETTINGS_TEMP_FILE = '.obsidian/plugins/ultimate-todoist-sync-for-obsidian/data.json.tmp';
|
||||
static readonly BACKUPS_TODOIST_FILE = '.ultimate-todoist-sync/backups/todoist';
|
||||
static readonly BACKUPS_FILES_FILE = '.ultimate-todoist-sync/backups/files';
|
||||
static readonly BACKUPS_SETTINGS_FILE = '.ultimate-todoist-sync/backups/settings';
|
||||
static readonly DEFAULT_BASE_PATH = 'ultimate-todoist-sync';
|
||||
static readonly LEGACY_BASE_PATH = '.ultimate-todoist-sync';
|
||||
|
||||
constructor(app: App, plugin: UltimateTodoistSyncForObsidian) {
|
||||
this.app = app;
|
||||
|
|
@ -19,27 +17,27 @@ export class StoragePathManager {
|
|||
}
|
||||
|
||||
getBasePath(): string {
|
||||
return this.basePath;
|
||||
return this.plugin.settings?.storageDirectory || StoragePathManager.DEFAULT_BASE_PATH;
|
||||
}
|
||||
|
||||
getLogsBasePath(): string {
|
||||
return `${this.basePath}/logs`;
|
||||
return `${this.getBasePath()}/logs`;
|
||||
}
|
||||
|
||||
getBackupsBasePath(): string {
|
||||
return `${this.basePath}/backups`;
|
||||
return `${this.getBasePath()}/backups`;
|
||||
}
|
||||
|
||||
getBackupsFilesPath(): string {
|
||||
return StoragePathManager.BACKUPS_FILES_FILE;
|
||||
return `${this.getBasePath()}/backups/files`;
|
||||
}
|
||||
|
||||
getBackupsTodoistPath(): string {
|
||||
return StoragePathManager.BACKUPS_TODOIST_FILE;
|
||||
return `${this.getBasePath()}/backups/todoist`;
|
||||
}
|
||||
|
||||
getBackupsSettingsPath(): string {
|
||||
return StoragePathManager.BACKUPS_SETTINGS_FILE;
|
||||
return `${this.getBasePath()}/backups/settings`;
|
||||
}
|
||||
|
||||
async getLogsPath(): Promise<string> {
|
||||
|
|
@ -100,12 +98,13 @@ export class StoragePathManager {
|
|||
}
|
||||
|
||||
async ensureAllDirs(): Promise<boolean> {
|
||||
const basePath = this.getBasePath();
|
||||
const dirs = [
|
||||
this.basePath,
|
||||
basePath,
|
||||
this.getLogsBasePath(),
|
||||
StoragePathManager.BACKUPS_TODOIST_FILE,
|
||||
StoragePathManager.BACKUPS_FILES_FILE,
|
||||
StoragePathManager.BACKUPS_SETTINGS_FILE
|
||||
`${basePath}/backups/todoist`,
|
||||
`${basePath}/backups/files`,
|
||||
`${basePath}/backups/settings`
|
||||
];
|
||||
|
||||
for (const dir of dirs) {
|
||||
|
|
@ -131,6 +130,105 @@ export class StoragePathManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
async migrateToNewDirectory(newDir: string): Promise<boolean> {
|
||||
const oldDir = this.plugin.settings?.lastStorageDirectory
|
||||
|| StoragePathManager.LEGACY_BASE_PATH;
|
||||
|
||||
if (oldDir === newDir) {
|
||||
console.log('[StoragePathManager] Directory unchanged, no migration needed');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`[StoragePathManager] Starting migration from "${oldDir}" to "${newDir}"`);
|
||||
|
||||
const adapter = this.app.vault.adapter;
|
||||
|
||||
try {
|
||||
const oldExists = await adapter.exists(oldDir);
|
||||
if (!oldExists) {
|
||||
console.log('[StoragePathManager] Old directory does not exist, no migration needed');
|
||||
this.plugin.settings.lastStorageDirectory = newDir;
|
||||
return true;
|
||||
}
|
||||
|
||||
const newExists = await adapter.exists(newDir);
|
||||
if (!newExists) {
|
||||
await adapter.mkdir(newDir);
|
||||
}
|
||||
|
||||
await this.migrateDirectoryContents(oldDir, newDir);
|
||||
|
||||
console.log('[StoragePathManager] Migration completed successfully');
|
||||
this.plugin.settings.lastStorageDirectory = newDir;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[StoragePathManager] Migration failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateDirectoryContents(srcDir: string, destDir: string): Promise<void> {
|
||||
const adapter = this.app.vault.adapter;
|
||||
|
||||
try {
|
||||
const result = await adapter.list(srcDir);
|
||||
|
||||
for (const folder of result.folders) {
|
||||
const newFolderPath = folder.replace(srcDir, destDir);
|
||||
const folderExists = await adapter.exists(newFolderPath);
|
||||
if (!folderExists) {
|
||||
await adapter.mkdir(newFolderPath);
|
||||
}
|
||||
await this.migrateDirectoryContents(folder, newFolderPath);
|
||||
}
|
||||
|
||||
for (const file of result.files) {
|
||||
const newFilePath = file.replace(srcDir, destDir);
|
||||
const content = await adapter.read(file);
|
||||
|
||||
const destExists = await adapter.exists(newFilePath);
|
||||
if (destExists) {
|
||||
const destContent = await adapter.read(newFilePath);
|
||||
const srcMtime = await this.getFileMtime(file);
|
||||
const destMtime = await this.getFileMtime(newFilePath);
|
||||
|
||||
if (srcMtime > destMtime) {
|
||||
await adapter.write(newFilePath, content);
|
||||
console.log(`[StoragePathManager] Updated file: ${newFilePath}`);
|
||||
} else {
|
||||
console.log(`[StoragePathManager] Kept newer file: ${newFilePath}`);
|
||||
}
|
||||
} else {
|
||||
await adapter.write(newFilePath, content);
|
||||
console.log(`[StoragePathManager] Migrated file: ${newFilePath}`);
|
||||
}
|
||||
|
||||
await adapter.remove(file);
|
||||
}
|
||||
|
||||
const remaining = await adapter.list(srcDir);
|
||||
if (remaining.folders.length === 0 && remaining.files.length === 0) {
|
||||
await adapter.rmdir(srcDir, true);
|
||||
console.log(`[StoragePathManager] Removed empty directory: ${srcDir}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[StoragePathManager] Error during migration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async getFileMtime(path: string): Promise<number> {
|
||||
try {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (file && 'stat' in file) {
|
||||
return file.stat.mtime;
|
||||
}
|
||||
return 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async readJsonFile<T>(path: string): Promise<T | null> {
|
||||
try {
|
||||
const adapter = this.app.vault.adapter;
|
||||
|
|
@ -226,22 +324,31 @@ export class StoragePathManager {
|
|||
if (!exists) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files: string[] = [];
|
||||
const allFiles = this.app.vault.getFiles();
|
||||
const prefix = dirPath + '/';
|
||||
for (const file of allFiles) {
|
||||
if (file.path.startsWith(prefix)) {
|
||||
files.push(file.path);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
|
||||
const result = await adapter.list(dirPath);
|
||||
return result.files;
|
||||
} catch (error) {
|
||||
console.error(`[StoragePathManager] Failed to list files in ${dirPath}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async listFolders(dirPath: string): Promise<string[]> {
|
||||
try {
|
||||
const adapter = this.app.vault.adapter;
|
||||
const exists = await adapter.exists(dirPath);
|
||||
if (!exists) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await adapter.list(dirPath);
|
||||
return result.folders;
|
||||
} catch (error) {
|
||||
console.error(`[StoragePathManager] Failed to list folders in ${dirPath}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(path: string): Promise<boolean> {
|
||||
try {
|
||||
const adapter = this.app.vault.adapter;
|
||||
|
|
@ -301,13 +408,12 @@ export class StoragePathManager {
|
|||
return;
|
||||
}
|
||||
|
||||
const fileObjects = files.map(path => {
|
||||
const file = this.app.vault.getAbstractFileByPath(path) as import('obsidian').TFile | null;
|
||||
return {
|
||||
const fileObjects = await Promise.all(
|
||||
files.map(async path => ({
|
||||
path,
|
||||
mtime: file?.stat.mtime || 0
|
||||
};
|
||||
});
|
||||
mtime: await this.getFileMtime(path)
|
||||
}))
|
||||
);
|
||||
|
||||
fileObjects.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export class TodoistToObsidianSync {
|
|||
const now: Date = new Date();
|
||||
const timeString = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
|
||||
|
||||
const backupFolder = this.plugin.storagePathManager?.getBackupsTodoistPath() || StoragePathManager.BACKUPS_TODOIST_FILE;
|
||||
const backupFolder = this.plugin.storagePathManager?.getBackupsTodoistPath() || 'ultimate-todoist-sync/backups/todoist';
|
||||
const tempFileName = `todoist-data-backup-${timeString}.tmp`;
|
||||
const fileName = `todoist-data-backup-${timeString}.json`;
|
||||
const tempPath = `${backupFolder}/${tempFileName}`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue