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.
Files changed (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. 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
+ }