documentation-hub 5.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- package/vite.config.ts +152 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackupService - Main Process
|
|
3
|
+
* Handles document backup and restoration with automatic cleanup
|
|
4
|
+
*
|
|
5
|
+
* This service runs in the Electron main process and provides secure
|
|
6
|
+
* file system operations via IPC. It implements the backup strategy
|
|
7
|
+
* for document processing operations.
|
|
8
|
+
*
|
|
9
|
+
* @architecture Main Process Service
|
|
10
|
+
* @security Context isolation compliant - no renderer access
|
|
11
|
+
* @performance IPC overhead: ~1-5ms per operation
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const backupService = new BackupService();
|
|
16
|
+
* const backupPath = await backupService.createBackup('/path/to/document.docx');
|
|
17
|
+
* // Later, restore if needed:
|
|
18
|
+
* await backupService.restoreBackup(backupPath, '/path/to/document.docx');
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createHash } from 'crypto';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import * as fs from 'fs/promises';
|
|
25
|
+
import * as os from 'os';
|
|
26
|
+
import { app } from 'electron';
|
|
27
|
+
import { logger } from '../../src/utils/logger';
|
|
28
|
+
|
|
29
|
+
// Create namespaced logger for backup operations
|
|
30
|
+
const log = logger.namespace('BackupService');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Service for managing document backups in the main process
|
|
34
|
+
*/
|
|
35
|
+
export class BackupService {
|
|
36
|
+
private backupDir: string;
|
|
37
|
+
private maxBackupAge: number = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
38
|
+
private maxBackupsPerDocument: number = 5;
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
// Use app data directory for backups
|
|
42
|
+
// In main process, we can access os and path directly
|
|
43
|
+
this.backupDir = path.join(os.homedir(), '.dochub', 'backups');
|
|
44
|
+
this.ensureBackupDirectory();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a backup of the document
|
|
49
|
+
*
|
|
50
|
+
* @param documentPath Absolute path to document to backup
|
|
51
|
+
* @returns Promise resolving to backup file path
|
|
52
|
+
* @throws Error if backup creation fails
|
|
53
|
+
*/
|
|
54
|
+
async createBackup(documentPath: string): Promise<string> {
|
|
55
|
+
try {
|
|
56
|
+
// Validate input path
|
|
57
|
+
if (!documentPath || typeof documentPath !== 'string') {
|
|
58
|
+
throw new Error('Invalid document path');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read original document
|
|
62
|
+
const documentData = await fs.readFile(documentPath);
|
|
63
|
+
|
|
64
|
+
// Generate backup filename with timestamp
|
|
65
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
66
|
+
const originalName = path.basename(documentPath, path.extname(documentPath));
|
|
67
|
+
const extension = path.extname(documentPath);
|
|
68
|
+
const hash = this.generateHash(documentData).substring(0, 8);
|
|
69
|
+
const backupName = `${originalName}_${timestamp}_${hash}${extension}`;
|
|
70
|
+
const backupPath = path.join(this.backupDir, backupName);
|
|
71
|
+
|
|
72
|
+
// Create backup
|
|
73
|
+
await fs.writeFile(backupPath, documentData);
|
|
74
|
+
|
|
75
|
+
// Store backup metadata
|
|
76
|
+
await this.saveBackupMetadata(documentPath, backupPath, {
|
|
77
|
+
originalPath: documentPath,
|
|
78
|
+
backupPath,
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
size: documentData.length,
|
|
81
|
+
checksum: this.generateHash(documentData),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Clean up old backups
|
|
85
|
+
await this.cleanupOldBackups(documentPath);
|
|
86
|
+
|
|
87
|
+
log.info('Created backup', { backupPath, size: documentData.length, checksum: hash });
|
|
88
|
+
return backupPath;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const message = `Failed to create backup: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
91
|
+
log.error('Create backup failed', { error: message, documentPath });
|
|
92
|
+
throw new Error(message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Restore document from backup
|
|
98
|
+
*
|
|
99
|
+
* @param backupPath Path to backup file
|
|
100
|
+
* @param targetPath Path where document should be restored
|
|
101
|
+
* @throws Error if restoration fails or integrity check fails
|
|
102
|
+
*/
|
|
103
|
+
async restoreBackup(backupPath: string, targetPath: string): Promise<void> {
|
|
104
|
+
try {
|
|
105
|
+
// Validate inputs
|
|
106
|
+
if (!backupPath || !targetPath) {
|
|
107
|
+
throw new Error('Invalid backup or target path');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Verify backup exists
|
|
111
|
+
await fs.access(backupPath);
|
|
112
|
+
|
|
113
|
+
// Read backup data
|
|
114
|
+
const backupData = await fs.readFile(backupPath);
|
|
115
|
+
|
|
116
|
+
// Verify integrity
|
|
117
|
+
const metadata = await this.getBackupMetadata(backupPath);
|
|
118
|
+
if (metadata) {
|
|
119
|
+
const currentChecksum = this.generateHash(backupData);
|
|
120
|
+
if (currentChecksum !== metadata.checksum) {
|
|
121
|
+
throw new Error('Backup integrity check failed - file may be corrupted');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Restore to target path
|
|
126
|
+
await fs.writeFile(targetPath, backupData);
|
|
127
|
+
|
|
128
|
+
log.info('Restored backup', { backupPath, targetPath });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const message = `Failed to restore backup: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
131
|
+
log.error('Restore backup failed', { error: message, backupPath, targetPath });
|
|
132
|
+
throw new Error(message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* List all backups for a document
|
|
138
|
+
*
|
|
139
|
+
* @param documentPath Path to document
|
|
140
|
+
* @returns Promise resolving to array of backup info, sorted by creation date (newest first)
|
|
141
|
+
*/
|
|
142
|
+
async listBackups(documentPath: string): Promise<BackupInfo[]> {
|
|
143
|
+
try {
|
|
144
|
+
const files = await fs.readdir(this.backupDir);
|
|
145
|
+
const documentName = path.basename(documentPath, path.extname(documentPath));
|
|
146
|
+
const backups: BackupInfo[] = [];
|
|
147
|
+
|
|
148
|
+
for (const file of files) {
|
|
149
|
+
if (file.startsWith(documentName) && !file.endsWith('.meta')) {
|
|
150
|
+
const filePath = path.join(this.backupDir, file);
|
|
151
|
+
const stats = await fs.stat(filePath);
|
|
152
|
+
const metadata = await this.getBackupMetadata(filePath);
|
|
153
|
+
|
|
154
|
+
backups.push({
|
|
155
|
+
path: filePath,
|
|
156
|
+
filename: file,
|
|
157
|
+
size: stats.size,
|
|
158
|
+
created: stats.birthtime,
|
|
159
|
+
modified: stats.mtime,
|
|
160
|
+
originalPath: metadata?.originalPath || documentPath,
|
|
161
|
+
checksum: metadata?.checksum,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort by creation date (newest first)
|
|
167
|
+
return backups.sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
168
|
+
} catch (error) {
|
|
169
|
+
log.error('Failed to list backups', { error, documentPath });
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Delete a specific backup
|
|
176
|
+
*
|
|
177
|
+
* @param backupPath Path to backup file to delete
|
|
178
|
+
* @throws Error if deletion fails
|
|
179
|
+
*/
|
|
180
|
+
async deleteBackup(backupPath: string): Promise<void> {
|
|
181
|
+
try {
|
|
182
|
+
await fs.unlink(backupPath);
|
|
183
|
+
|
|
184
|
+
// Delete metadata file
|
|
185
|
+
const metadataPath = `${backupPath}.meta`;
|
|
186
|
+
try {
|
|
187
|
+
await fs.unlink(metadataPath);
|
|
188
|
+
} catch {
|
|
189
|
+
// Metadata file might not exist - ignore
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
log.info('Deleted backup', { backupPath });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const message = `Failed to delete backup: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
195
|
+
log.error('Delete backup failed', { error: message, backupPath });
|
|
196
|
+
throw new Error(message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Clean up old backups for a document
|
|
202
|
+
*
|
|
203
|
+
* Removes backups that are:
|
|
204
|
+
* - Older than maxBackupAge (default 7 days)
|
|
205
|
+
* - Exceed maxBackupsPerDocument count (default 5)
|
|
206
|
+
*
|
|
207
|
+
* @param documentPath Path to document
|
|
208
|
+
* @returns Promise resolving to number of backups deleted
|
|
209
|
+
*/
|
|
210
|
+
async cleanupOldBackups(documentPath: string): Promise<number> {
|
|
211
|
+
const backups = await this.listBackups(documentPath);
|
|
212
|
+
let deletedCount = 0;
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
|
|
215
|
+
// Sort by creation date (newest first)
|
|
216
|
+
const sortedBackups = [...backups].sort((a, b) => b.created.getTime() - a.created.getTime());
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < sortedBackups.length; i++) {
|
|
219
|
+
const backup = sortedBackups[i];
|
|
220
|
+
const age = now - backup.created.getTime();
|
|
221
|
+
|
|
222
|
+
// Delete if too old or exceeds max count
|
|
223
|
+
if (age > this.maxBackupAge || i >= this.maxBackupsPerDocument) {
|
|
224
|
+
await this.deleteBackup(backup.path);
|
|
225
|
+
deletedCount++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (deletedCount > 0) {
|
|
230
|
+
log.info('Cleaned up old backups', { deletedCount, documentPath });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return deletedCount;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Clean up all old backups across all documents
|
|
238
|
+
*
|
|
239
|
+
* @returns Promise resolving to number of backups deleted
|
|
240
|
+
*/
|
|
241
|
+
async cleanupAllOldBackups(): Promise<number> {
|
|
242
|
+
try {
|
|
243
|
+
const files = await fs.readdir(this.backupDir);
|
|
244
|
+
let deletedCount = 0;
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
|
|
247
|
+
for (const file of files) {
|
|
248
|
+
if (!file.endsWith('.meta')) {
|
|
249
|
+
const filePath = path.join(this.backupDir, file);
|
|
250
|
+
const stats = await fs.stat(filePath);
|
|
251
|
+
const age = now - stats.birthtime.getTime();
|
|
252
|
+
|
|
253
|
+
if (age > this.maxBackupAge) {
|
|
254
|
+
await this.deleteBackup(filePath);
|
|
255
|
+
deletedCount++;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
log.info('Cleaned up all old backups', { deletedCount });
|
|
261
|
+
return deletedCount;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
log.error('Failed to cleanup all old backups', { error });
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Verify backup integrity
|
|
270
|
+
*
|
|
271
|
+
* @param backupPath Path to backup file
|
|
272
|
+
* @returns Promise resolving to true if backup is valid, false otherwise
|
|
273
|
+
*/
|
|
274
|
+
async verifyBackup(backupPath: string): Promise<boolean> {
|
|
275
|
+
try {
|
|
276
|
+
const data = await fs.readFile(backupPath);
|
|
277
|
+
const metadata = await this.getBackupMetadata(backupPath);
|
|
278
|
+
|
|
279
|
+
if (!metadata) {
|
|
280
|
+
// No metadata, but file exists - consider valid
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const currentChecksum = this.generateHash(data);
|
|
285
|
+
return currentChecksum === metadata.checksum;
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get backup storage information
|
|
293
|
+
*
|
|
294
|
+
* @returns Promise resolving to storage statistics
|
|
295
|
+
*/
|
|
296
|
+
async getBackupStorageInfo(): Promise<StorageInfo> {
|
|
297
|
+
try {
|
|
298
|
+
const files = await fs.readdir(this.backupDir);
|
|
299
|
+
let totalSize = 0;
|
|
300
|
+
let fileCount = 0;
|
|
301
|
+
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
if (!file.endsWith('.meta')) {
|
|
304
|
+
const filePath = path.join(this.backupDir, file);
|
|
305
|
+
const stats = await fs.stat(filePath);
|
|
306
|
+
totalSize += stats.size;
|
|
307
|
+
fileCount++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
totalSize,
|
|
313
|
+
fileCount,
|
|
314
|
+
averageSize: fileCount > 0 ? totalSize / fileCount : 0,
|
|
315
|
+
path: this.backupDir,
|
|
316
|
+
};
|
|
317
|
+
} catch {
|
|
318
|
+
return {
|
|
319
|
+
totalSize: 0,
|
|
320
|
+
fileCount: 0,
|
|
321
|
+
averageSize: 0,
|
|
322
|
+
path: this.backupDir,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Create backup with automatic cleanup (convenience method)
|
|
329
|
+
*
|
|
330
|
+
* @param documentPath Path to document
|
|
331
|
+
* @returns Promise resolving to backup file path
|
|
332
|
+
*/
|
|
333
|
+
async createBackupWithCleanup(documentPath: string): Promise<string> {
|
|
334
|
+
const backupPath = await this.createBackup(documentPath);
|
|
335
|
+
await this.cleanupOldBackups(documentPath);
|
|
336
|
+
return backupPath;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Update backup service configuration
|
|
341
|
+
*
|
|
342
|
+
* @param config Configuration options
|
|
343
|
+
*/
|
|
344
|
+
setConfig(config: Partial<BackupConfig>): void {
|
|
345
|
+
if (config.backupDir) {
|
|
346
|
+
this.backupDir = config.backupDir;
|
|
347
|
+
this.ensureBackupDirectory();
|
|
348
|
+
}
|
|
349
|
+
if (config.maxBackupAgeDays !== undefined) {
|
|
350
|
+
this.maxBackupAge = config.maxBackupAgeDays * 24 * 60 * 60 * 1000;
|
|
351
|
+
}
|
|
352
|
+
if (config.maxBackupsPerDocument !== undefined) {
|
|
353
|
+
this.maxBackupsPerDocument = config.maxBackupsPerDocument;
|
|
354
|
+
}
|
|
355
|
+
log.info('Configuration updated', { config });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Private helper methods
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Ensure backup directory exists
|
|
362
|
+
*/
|
|
363
|
+
private async ensureBackupDirectory(): Promise<void> {
|
|
364
|
+
try {
|
|
365
|
+
await fs.mkdir(this.backupDir, { recursive: true });
|
|
366
|
+
log.debug('Backup directory ensured', { backupDir: this.backupDir });
|
|
367
|
+
} catch (error) {
|
|
368
|
+
log.error('Failed to create backup directory', { error, backupDir: this.backupDir });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Generate SHA-256 hash for data
|
|
374
|
+
*
|
|
375
|
+
* @param data Buffer to hash
|
|
376
|
+
* @returns Hex string of hash
|
|
377
|
+
*/
|
|
378
|
+
private generateHash(data: Buffer): string {
|
|
379
|
+
return createHash('sha256').update(data).digest('hex');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Save backup metadata to .meta file
|
|
384
|
+
*
|
|
385
|
+
* @param _originalPath Original document path (unused, kept for signature compatibility)
|
|
386
|
+
* @param backupPath Backup file path
|
|
387
|
+
* @param metadata Metadata object to save
|
|
388
|
+
*/
|
|
389
|
+
private async saveBackupMetadata(
|
|
390
|
+
_originalPath: string,
|
|
391
|
+
backupPath: string,
|
|
392
|
+
metadata: BackupMetadata
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
const metadataPath = `${backupPath}.meta`;
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
log.error('Failed to save backup metadata', { error, metadataPath });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get backup metadata from .meta file
|
|
405
|
+
*
|
|
406
|
+
* @param backupPath Backup file path
|
|
407
|
+
* @returns Promise resolving to metadata object or null if not found
|
|
408
|
+
*/
|
|
409
|
+
private async getBackupMetadata(backupPath: string): Promise<BackupMetadata | null> {
|
|
410
|
+
const metadataPath = `${backupPath}.meta`;
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const data = await fs.readFile(metadataPath, 'utf-8');
|
|
414
|
+
return JSON.parse(data);
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Type definitions
|
|
422
|
+
|
|
423
|
+
export interface BackupInfo {
|
|
424
|
+
path: string;
|
|
425
|
+
filename: string;
|
|
426
|
+
size: number;
|
|
427
|
+
created: Date;
|
|
428
|
+
modified: Date;
|
|
429
|
+
originalPath: string;
|
|
430
|
+
checksum?: string;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface BackupMetadata {
|
|
434
|
+
originalPath: string;
|
|
435
|
+
backupPath: string;
|
|
436
|
+
timestamp: string;
|
|
437
|
+
size: number;
|
|
438
|
+
checksum: string;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export interface StorageInfo {
|
|
442
|
+
totalSize: number;
|
|
443
|
+
fileCount: number;
|
|
444
|
+
averageSize: number;
|
|
445
|
+
path: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface BackupConfig {
|
|
449
|
+
backupDir?: string;
|
|
450
|
+
maxBackupAgeDays?: number;
|
|
451
|
+
maxBackupsPerDocument?: number;
|
|
452
|
+
}
|