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,656 @@
|
|
|
1
|
+
import { autoUpdater, UpdateInfo } from 'electron-updater';
|
|
2
|
+
import { app, shell } from 'electron';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { PublicClientApplication, Configuration, InteractiveRequest, AccountInfo } from '@azure/msal-node';
|
|
6
|
+
import { logger } from '../src/utils/logger';
|
|
7
|
+
import * as yaml from 'js-yaml';
|
|
8
|
+
|
|
9
|
+
const log = logger.namespace('CustomUpdater');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SharePoint update provider configuration
|
|
13
|
+
*/
|
|
14
|
+
interface UpdateProviderConfig {
|
|
15
|
+
type: 'github' | 'sharepoint';
|
|
16
|
+
sharePointUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Custom updater with delta update support, silent install, and SharePoint source
|
|
21
|
+
*/
|
|
22
|
+
export class CustomUpdater {
|
|
23
|
+
private mainWindow: Electron.BrowserWindow | null = null;
|
|
24
|
+
private updateInfo: UpdateInfo | null = null;
|
|
25
|
+
private isDev: boolean;
|
|
26
|
+
private forceUpdatesInDev: boolean;
|
|
27
|
+
|
|
28
|
+
// SharePoint update source state
|
|
29
|
+
private msalApp: PublicClientApplication | null = null;
|
|
30
|
+
private sharePointUrl: string | null = null;
|
|
31
|
+
private accessToken: string | null = null;
|
|
32
|
+
private currentProvider: 'github' | 'sharepoint' = 'github';
|
|
33
|
+
private accountInfo: AccountInfo | null = null;
|
|
34
|
+
|
|
35
|
+
constructor(mainWindow: Electron.BrowserWindow | null) {
|
|
36
|
+
this.mainWindow = mainWindow;
|
|
37
|
+
this.isDev = !app.isPackaged;
|
|
38
|
+
this.forceUpdatesInDev = process.env.FORCE_DEV_UPDATE_CONFIG === 'true';
|
|
39
|
+
this.configureDevUpdateServer();
|
|
40
|
+
this.setupAutoUpdater();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Returns true if updates should be skipped (dev mode without force flag) */
|
|
44
|
+
private shouldSkipUpdates(): boolean {
|
|
45
|
+
return this.isDev && !this.forceUpdatesInDev;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Configure dev update server for local testing
|
|
50
|
+
* Set FORCE_DEV_UPDATE_CONFIG=true env var to test against local server
|
|
51
|
+
*/
|
|
52
|
+
private configureDevUpdateServer(): void {
|
|
53
|
+
const forceDevConfig = process.env.FORCE_DEV_UPDATE_CONFIG === 'true';
|
|
54
|
+
|
|
55
|
+
if (forceDevConfig || (this.isDev && process.env.TEST_UPDATES === 'true')) {
|
|
56
|
+
// When running from dist/electron/, go up two levels to project root
|
|
57
|
+
const devConfigPath = path.join(__dirname, '..', '..', 'dev-app-update.yml');
|
|
58
|
+
|
|
59
|
+
if (fs.existsSync(devConfigPath)) {
|
|
60
|
+
log.info('Using dev update config for local testing:', devConfigPath);
|
|
61
|
+
autoUpdater.forceDevUpdateConfig = true;
|
|
62
|
+
autoUpdater.updateConfigPath = devConfigPath;
|
|
63
|
+
} else {
|
|
64
|
+
log.warn('Dev update config not found at:', devConfigPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private setupAutoUpdater(): void {
|
|
70
|
+
// Configure auto-updater
|
|
71
|
+
autoUpdater.autoDownload = false;
|
|
72
|
+
autoUpdater.autoInstallOnAppQuit = true;
|
|
73
|
+
autoUpdater.autoRunAppAfterInstall = true; // Auto-restart after silent install
|
|
74
|
+
autoUpdater.disableDifferentialDownload = false; // Enable delta/blockmap updates
|
|
75
|
+
|
|
76
|
+
// Set up event handlers
|
|
77
|
+
autoUpdater.on('checking-for-update', () => {
|
|
78
|
+
this.sendToWindow('update-checking');
|
|
79
|
+
log.info('Checking for updates...');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
autoUpdater.on('update-available', (info) => {
|
|
83
|
+
this.updateInfo = info;
|
|
84
|
+
this.sendToWindow('update-available', {
|
|
85
|
+
version: info.version,
|
|
86
|
+
releaseDate: info.releaseDate,
|
|
87
|
+
releaseNotes: info.releaseNotes,
|
|
88
|
+
});
|
|
89
|
+
log.info(`Update available: ${info.version}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
autoUpdater.on('update-not-available', (info) => {
|
|
93
|
+
this.sendToWindow('update-not-available', {
|
|
94
|
+
version: info.version,
|
|
95
|
+
});
|
|
96
|
+
log.info('No updates available');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
autoUpdater.on('error', (error) => {
|
|
100
|
+
this.sendToWindow('update-error', {
|
|
101
|
+
message: error.message,
|
|
102
|
+
});
|
|
103
|
+
log.error('Update error:', error);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
autoUpdater.on('download-progress', (progressObj) => {
|
|
107
|
+
this.sendToWindow('update-download-progress', {
|
|
108
|
+
bytesPerSecond: progressObj.bytesPerSecond,
|
|
109
|
+
percent: progressObj.percent,
|
|
110
|
+
transferred: progressObj.transferred,
|
|
111
|
+
total: progressObj.total,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
autoUpdater.on('update-downloaded', (info) => {
|
|
116
|
+
this.sendToWindow('update-downloaded', {
|
|
117
|
+
version: info.version,
|
|
118
|
+
releaseNotes: info.releaseNotes,
|
|
119
|
+
});
|
|
120
|
+
log.info(`Update downloaded: ${info.version}`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Send status to renderer process
|
|
126
|
+
*/
|
|
127
|
+
private sendToWindow(channel: string, data?: any): void {
|
|
128
|
+
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
129
|
+
this.mainWindow.webContents.send(channel, data);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check for updates (supports GitHub and SharePoint sources)
|
|
135
|
+
*/
|
|
136
|
+
public async checkForUpdates(): Promise<any> {
|
|
137
|
+
if (this.shouldSkipUpdates()) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
message: 'Updates are not available in development mode',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// SharePoint: Only use if enabled AND URL provided AND authenticated
|
|
145
|
+
if (this.currentProvider === 'sharepoint' && this.sharePointUrl && this.accessToken) {
|
|
146
|
+
try {
|
|
147
|
+
const result = await this.checkSharePointUpdates();
|
|
148
|
+
if (result.success) return result;
|
|
149
|
+
// Fall through to GitHub on failure
|
|
150
|
+
log.warn('SharePoint update check failed, falling back to GitHub');
|
|
151
|
+
this.sendToWindow('update-status', { message: 'SharePoint unavailable, checking GitHub...' });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
log.error('SharePoint update check error', { error });
|
|
154
|
+
this.sendToWindow('update-status', { message: 'SharePoint error, falling back to GitHub...' });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Default: GitHub
|
|
159
|
+
try {
|
|
160
|
+
const result = await autoUpdater.checkForUpdates();
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
updateInfo: result?.updateInfo,
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
log.error('Failed to check for updates:', error);
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
message: error instanceof Error ? error.message : 'Failed to check for updates',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Download update
|
|
176
|
+
*/
|
|
177
|
+
public async downloadUpdate(): Promise<any> {
|
|
178
|
+
try {
|
|
179
|
+
await autoUpdater.downloadUpdate();
|
|
180
|
+
return {
|
|
181
|
+
success: true,
|
|
182
|
+
message: 'Download started',
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
log.error('Failed to download update:', error);
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
message: error instanceof Error ? error.message : 'Failed to download update',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Install update and restart
|
|
195
|
+
* Shows installer UI for reliable MSI installation on Windows
|
|
196
|
+
*/
|
|
197
|
+
public quitAndInstall(): void {
|
|
198
|
+
log.info('Installing update and restarting...');
|
|
199
|
+
|
|
200
|
+
// Remove listeners that might prevent quit
|
|
201
|
+
app.removeAllListeners('window-all-closed');
|
|
202
|
+
|
|
203
|
+
// quitAndInstall(isSilent, isForceRunAfter)
|
|
204
|
+
// isSilent=false: Show installer UI (required for MSI on Windows)
|
|
205
|
+
// isForceRunAfter=false: Don't auto-launch after install (avoids MSI error 2753)
|
|
206
|
+
// Note: User will need to manually start the app after update completes
|
|
207
|
+
// This is more reliable than auto-launch which can fail with path issues
|
|
208
|
+
autoUpdater.quitAndInstall(false, false);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check for updates on startup
|
|
213
|
+
*/
|
|
214
|
+
public async checkOnStartup(): Promise<void> {
|
|
215
|
+
if (this.shouldSkipUpdates()) {
|
|
216
|
+
log.info('Skipping update check in development mode');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Wait a bit for the window to load
|
|
221
|
+
setTimeout(async () => {
|
|
222
|
+
try {
|
|
223
|
+
log.info('Checking for updates on startup...');
|
|
224
|
+
await this.checkForUpdates();
|
|
225
|
+
} catch (error) {
|
|
226
|
+
log.error('Startup update check failed:', error);
|
|
227
|
+
}
|
|
228
|
+
}, 3000);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Start scheduled periodic update checks
|
|
233
|
+
* @param intervalMs Interval between checks in milliseconds (default: 4 hours)
|
|
234
|
+
*/
|
|
235
|
+
public startScheduledChecks(intervalMs: number = 4 * 60 * 60 * 1000): void {
|
|
236
|
+
if (this.shouldSkipUpdates()) {
|
|
237
|
+
log.info('Skipping scheduled update checks in development mode');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
log.info(`Starting scheduled update checks every ${intervalMs / (60 * 60 * 1000)} hours`);
|
|
242
|
+
|
|
243
|
+
setInterval(async () => {
|
|
244
|
+
try {
|
|
245
|
+
log.info('Running scheduled update check...');
|
|
246
|
+
await this.checkForUpdates();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
log.error('Scheduled update check failed:', error);
|
|
249
|
+
}
|
|
250
|
+
}, intervalMs);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// SharePoint Update Source Methods
|
|
255
|
+
// ============================================================================
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Set the update provider (GitHub or SharePoint)
|
|
259
|
+
*/
|
|
260
|
+
public async setProvider(config: UpdateProviderConfig): Promise<{ success: boolean; error?: string }> {
|
|
261
|
+
try {
|
|
262
|
+
this.currentProvider = config.type;
|
|
263
|
+
|
|
264
|
+
if (config.type === 'sharepoint' && config.sharePointUrl) {
|
|
265
|
+
this.sharePointUrl = config.sharePointUrl;
|
|
266
|
+
log.info('Update provider set to SharePoint', { url: config.sharePointUrl });
|
|
267
|
+
} else {
|
|
268
|
+
this.sharePointUrl = null;
|
|
269
|
+
this.accessToken = null;
|
|
270
|
+
this.currentProvider = 'github';
|
|
271
|
+
log.info('Update provider set to GitHub (default)');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { success: true };
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const message = error instanceof Error ? error.message : 'Failed to set provider';
|
|
277
|
+
log.error('Failed to set update provider', { error: message });
|
|
278
|
+
return { success: false, error: message };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Interactive Microsoft login for SharePoint access
|
|
284
|
+
*/
|
|
285
|
+
public async sharePointLogin(): Promise<{ success: boolean; error?: string }> {
|
|
286
|
+
try {
|
|
287
|
+
if (!this.msalApp) {
|
|
288
|
+
this.initializeMsal();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!this.msalApp) {
|
|
292
|
+
return { success: false, error: 'Failed to initialize authentication' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Try to acquire token silently first if we have cached account
|
|
296
|
+
if (this.accountInfo) {
|
|
297
|
+
try {
|
|
298
|
+
const silentResult = await this.msalApp.acquireTokenSilent({
|
|
299
|
+
account: this.accountInfo,
|
|
300
|
+
scopes: ['https://graph.microsoft.com/Files.Read.All'],
|
|
301
|
+
});
|
|
302
|
+
this.accessToken = silentResult.accessToken;
|
|
303
|
+
log.info('SharePoint token acquired silently');
|
|
304
|
+
return { success: true };
|
|
305
|
+
} catch {
|
|
306
|
+
// Silent acquisition failed, proceed with interactive
|
|
307
|
+
log.info('Silent token acquisition failed, proceeding with interactive login');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Interactive login - open browser for user to sign in
|
|
312
|
+
const interactiveRequest: InteractiveRequest = {
|
|
313
|
+
scopes: ['https://graph.microsoft.com/Files.Read.All'],
|
|
314
|
+
openBrowser: async (url: string) => {
|
|
315
|
+
await shell.openExternal(url);
|
|
316
|
+
},
|
|
317
|
+
successTemplate: '<h1>Authentication Successful</h1><p>You can close this window and return to Documentation Hub.</p>',
|
|
318
|
+
errorTemplate: '<h1>Authentication Failed</h1><p>{{error}}</p>',
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const authResult = await this.msalApp.acquireTokenInteractive(interactiveRequest);
|
|
322
|
+
this.accessToken = authResult.accessToken;
|
|
323
|
+
this.accountInfo = authResult.account;
|
|
324
|
+
|
|
325
|
+
log.info('SharePoint login successful');
|
|
326
|
+
return { success: true };
|
|
327
|
+
} catch (error) {
|
|
328
|
+
const message = error instanceof Error ? error.message : 'Login failed';
|
|
329
|
+
log.error('SharePoint login failed', { error: message });
|
|
330
|
+
return { success: false, error: message };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Logout from SharePoint
|
|
336
|
+
*/
|
|
337
|
+
public async sharePointLogout(): Promise<void> {
|
|
338
|
+
this.accessToken = null;
|
|
339
|
+
this.accountInfo = null;
|
|
340
|
+
|
|
341
|
+
if (this.msalApp) {
|
|
342
|
+
try {
|
|
343
|
+
const tokenCache = this.msalApp.getTokenCache();
|
|
344
|
+
const accounts = await tokenCache.getAllAccounts();
|
|
345
|
+
for (const account of accounts) {
|
|
346
|
+
await tokenCache.removeAccount(account);
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
log.warn('Error clearing token cache:', error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
log.info('SharePoint logout complete');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Test SharePoint connection
|
|
358
|
+
*/
|
|
359
|
+
public async testSharePointConnection(url: string): Promise<{ success: boolean; message: string; authenticated?: boolean }> {
|
|
360
|
+
// Validate URL format
|
|
361
|
+
if (!this.isValidSharePointUrl(url)) {
|
|
362
|
+
return { success: false, message: 'Invalid SharePoint URL. Must be https://*.sharepoint.com/sites/...' };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check if authenticated
|
|
366
|
+
if (!this.accessToken) {
|
|
367
|
+
return { success: false, message: 'Not authenticated. Please sign in to Microsoft first.', authenticated: false };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
// Try to access latest.yml
|
|
372
|
+
const latestYmlUrl = this.buildGraphApiUrl(url, 'latest.yml');
|
|
373
|
+
log.info('Testing SharePoint connection', { url: latestYmlUrl });
|
|
374
|
+
|
|
375
|
+
const response = await this.fetchWithAuth(latestYmlUrl);
|
|
376
|
+
|
|
377
|
+
if (response.ok) {
|
|
378
|
+
return { success: true, message: 'Connected! Update manifest (latest.yml) found.', authenticated: true };
|
|
379
|
+
} else if (response.status === 404) {
|
|
380
|
+
return { success: true, message: 'Connected to SharePoint, but latest.yml not found. Please upload the update files.', authenticated: true };
|
|
381
|
+
} else if (response.status === 401 || response.status === 403) {
|
|
382
|
+
return { success: false, message: `Access denied (${response.status}). Please check folder permissions.`, authenticated: true };
|
|
383
|
+
} else {
|
|
384
|
+
return { success: false, message: `SharePoint returned status ${response.status}: ${response.statusText}`, authenticated: true };
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const message = error instanceof Error ? error.message : 'Connection failed';
|
|
388
|
+
log.error('SharePoint connection test failed', { error: message });
|
|
389
|
+
return { success: false, message: `Connection failed: ${message}`, authenticated: true };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Check for updates from SharePoint
|
|
395
|
+
*/
|
|
396
|
+
private async checkSharePointUpdates(): Promise<any> {
|
|
397
|
+
if (!this.sharePointUrl || !this.accessToken) {
|
|
398
|
+
throw new Error('SharePoint not configured or not authenticated');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Fetch latest.yml from SharePoint
|
|
402
|
+
const latestYmlUrl = this.buildGraphApiUrl(this.sharePointUrl, 'latest.yml');
|
|
403
|
+
log.info('Checking for updates from SharePoint', { url: latestYmlUrl });
|
|
404
|
+
|
|
405
|
+
const response = await this.fetchWithAuth(latestYmlUrl);
|
|
406
|
+
|
|
407
|
+
if (!response.ok) {
|
|
408
|
+
throw new Error(`Failed to fetch latest.yml: ${response.status} ${response.statusText}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const yamlContent = await response.text();
|
|
412
|
+
const updateManifest = yaml.load(yamlContent) as any;
|
|
413
|
+
|
|
414
|
+
if (!updateManifest || !updateManifest.version) {
|
|
415
|
+
throw new Error('Invalid latest.yml format - missing version');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const currentVersion = app.getVersion();
|
|
419
|
+
const availableVersion = updateManifest.version;
|
|
420
|
+
|
|
421
|
+
log.info('SharePoint update check', { currentVersion, availableVersion });
|
|
422
|
+
|
|
423
|
+
// Compare versions
|
|
424
|
+
if (this.isNewerVersion(availableVersion, currentVersion)) {
|
|
425
|
+
// Update available
|
|
426
|
+
this.updateInfo = {
|
|
427
|
+
version: availableVersion,
|
|
428
|
+
releaseDate: updateManifest.releaseDate || new Date().toISOString(),
|
|
429
|
+
releaseNotes: updateManifest.releaseNotes || '',
|
|
430
|
+
files: updateManifest.files || [],
|
|
431
|
+
path: updateManifest.path || '',
|
|
432
|
+
sha512: updateManifest.sha512 || '',
|
|
433
|
+
} as UpdateInfo;
|
|
434
|
+
|
|
435
|
+
this.sendToWindow('update-available', {
|
|
436
|
+
version: availableVersion,
|
|
437
|
+
releaseDate: updateManifest.releaseDate || '',
|
|
438
|
+
releaseNotes: updateManifest.releaseNotes || '',
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
log.info(`SharePoint update available: ${availableVersion}`);
|
|
442
|
+
return { success: true, updateInfo: this.updateInfo };
|
|
443
|
+
} else {
|
|
444
|
+
// No update available
|
|
445
|
+
this.sendToWindow('update-not-available', {
|
|
446
|
+
version: currentVersion,
|
|
447
|
+
});
|
|
448
|
+
log.info('No SharePoint updates available');
|
|
449
|
+
return { success: true, updateInfo: null };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Validate SharePoint URL format
|
|
455
|
+
*/
|
|
456
|
+
private isValidSharePointUrl(url: string): boolean {
|
|
457
|
+
try {
|
|
458
|
+
const parsed = new URL(url);
|
|
459
|
+
// Must be HTTPS and a sharepoint.com domain
|
|
460
|
+
if (parsed.protocol !== 'https:') return false;
|
|
461
|
+
if (!parsed.hostname.endsWith('.sharepoint.com')) return false;
|
|
462
|
+
// Should contain /sites/ in the path
|
|
463
|
+
if (!parsed.pathname.includes('/sites/')) return false;
|
|
464
|
+
return true;
|
|
465
|
+
} catch {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Initialize MSAL for interactive login
|
|
472
|
+
* Uses the public Microsoft Office client ID which allows delegated user permissions
|
|
473
|
+
*/
|
|
474
|
+
private initializeMsal(): void {
|
|
475
|
+
const config: Configuration = {
|
|
476
|
+
auth: {
|
|
477
|
+
// Microsoft Office public client ID - works for delegated user auth
|
|
478
|
+
clientId: 'd3590ed6-52b3-4102-aeff-aad2292ab01c',
|
|
479
|
+
authority: 'https://login.microsoftonline.com/common',
|
|
480
|
+
},
|
|
481
|
+
cache: {
|
|
482
|
+
// Use in-memory cache (not persisted between sessions)
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
this.msalApp = new PublicClientApplication(config);
|
|
487
|
+
log.info('MSAL client initialized for SharePoint updates');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Build Microsoft Graph API URL for SharePoint file access
|
|
492
|
+
*
|
|
493
|
+
* Converts a SharePoint URL like:
|
|
494
|
+
* https://company.sharepoint.com/sites/IT/Shared Documents/Updates
|
|
495
|
+
*
|
|
496
|
+
* To a Graph API URL like:
|
|
497
|
+
* https://graph.microsoft.com/v1.0/sites/company.sharepoint.com:/sites/IT:/drive/root:/Shared Documents/Updates/filename:/content
|
|
498
|
+
*/
|
|
499
|
+
private buildGraphApiUrl(folderUrl: string, fileName: string): string {
|
|
500
|
+
const url = new URL(folderUrl);
|
|
501
|
+
const hostname = url.hostname;
|
|
502
|
+
const pathParts = url.pathname.split('/').filter(p => p);
|
|
503
|
+
|
|
504
|
+
// Find the site path (e.g., /sites/IT)
|
|
505
|
+
const siteIndex = pathParts.indexOf('sites');
|
|
506
|
+
if (siteIndex === -1) {
|
|
507
|
+
throw new Error('Invalid SharePoint URL - must contain /sites/');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const siteName = pathParts[siteIndex + 1];
|
|
511
|
+
if (!siteName) {
|
|
512
|
+
throw new Error('Invalid SharePoint URL - missing site name');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Everything after the site name is the folder path
|
|
516
|
+
const folderPath = pathParts.slice(siteIndex + 2).join('/');
|
|
517
|
+
|
|
518
|
+
// Build the Graph API URL
|
|
519
|
+
// Format: /sites/{hostname}:/sites/{siteName}:/drive/root:/{folderPath}/{fileName}:/content
|
|
520
|
+
const graphUrl = `https://graph.microsoft.com/v1.0/sites/${hostname}:/sites/${siteName}:/drive/root:/${encodeURIComponent(folderPath)}/${encodeURIComponent(fileName)}:/content`;
|
|
521
|
+
|
|
522
|
+
return graphUrl;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Fetch with Bearer token authentication
|
|
527
|
+
*/
|
|
528
|
+
private async fetchWithAuth(url: string): Promise<Response> {
|
|
529
|
+
if (!this.accessToken) {
|
|
530
|
+
throw new Error('No access token available');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return fetch(url, {
|
|
534
|
+
headers: {
|
|
535
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
536
|
+
'User-Agent': `DocumentationHub/${app.getVersion()} (${process.platform})`,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Compare versions to determine if target is newer than current
|
|
543
|
+
*/
|
|
544
|
+
private isNewerVersion(target: string, current: string): boolean {
|
|
545
|
+
const targetParts = target.split('.').map(Number);
|
|
546
|
+
const currentParts = current.split('.').map(Number);
|
|
547
|
+
|
|
548
|
+
for (let i = 0; i < Math.max(targetParts.length, currentParts.length); i++) {
|
|
549
|
+
const t = targetParts[i] || 0;
|
|
550
|
+
const c = currentParts[i] || 0;
|
|
551
|
+
if (t > c) return true;
|
|
552
|
+
if (t < c) return false;
|
|
553
|
+
}
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Download a specific file from SharePoint using interactive authentication
|
|
559
|
+
*
|
|
560
|
+
* This method is used for downloading dictionary files. It requires the user
|
|
561
|
+
* to be authenticated via sharePointLogin() first.
|
|
562
|
+
*
|
|
563
|
+
* @param fileUrl - Direct SharePoint URL to the file (e.g., https://company.sharepoint.com/sites/IT/Shared Documents/Dictionary.xlsx)
|
|
564
|
+
* @returns Buffer containing the file content, or error
|
|
565
|
+
*/
|
|
566
|
+
public async downloadSharePointFile(fileUrl: string): Promise<{ success: boolean; data?: Buffer; error?: string }> {
|
|
567
|
+
try {
|
|
568
|
+
// Validate URL format
|
|
569
|
+
if (!this.isValidSharePointUrl(fileUrl)) {
|
|
570
|
+
return { success: false, error: 'Invalid SharePoint URL. Must be https://*.sharepoint.com/sites/...' };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Check authentication
|
|
574
|
+
if (!this.accessToken) {
|
|
575
|
+
// Try to acquire token silently first
|
|
576
|
+
const loginResult = await this.sharePointLogin();
|
|
577
|
+
if (!loginResult.success) {
|
|
578
|
+
return { success: false, error: loginResult.error || 'Authentication required. Please sign in first.' };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Build Graph API URL for the specific file
|
|
583
|
+
const graphApiUrl = this.buildGraphApiUrlForFile(fileUrl);
|
|
584
|
+
log.info('Downloading SharePoint file via Graph API:', graphApiUrl);
|
|
585
|
+
|
|
586
|
+
// Download the file
|
|
587
|
+
const response = await this.fetchWithAuth(graphApiUrl);
|
|
588
|
+
|
|
589
|
+
if (!response.ok) {
|
|
590
|
+
const errorText = await response.text();
|
|
591
|
+
log.error('SharePoint file download failed:', {
|
|
592
|
+
status: response.status,
|
|
593
|
+
statusText: response.statusText,
|
|
594
|
+
error: errorText,
|
|
595
|
+
});
|
|
596
|
+
return {
|
|
597
|
+
success: false,
|
|
598
|
+
error: `Download failed: ${response.status} ${response.statusText}`,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Convert response to Buffer
|
|
603
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
604
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
605
|
+
|
|
606
|
+
log.info('SharePoint file downloaded successfully', { size: buffer.length });
|
|
607
|
+
return { success: true, data: buffer };
|
|
608
|
+
} catch (error) {
|
|
609
|
+
const message = error instanceof Error ? error.message : 'Download failed';
|
|
610
|
+
log.error('SharePoint file download error:', { error: message });
|
|
611
|
+
return { success: false, error: message };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Build Microsoft Graph API URL for a specific SharePoint file
|
|
617
|
+
*
|
|
618
|
+
* Converts a SharePoint file URL like:
|
|
619
|
+
* https://company.sharepoint.com/sites/IT/Shared Documents/Folder/Dictionary.xlsx
|
|
620
|
+
*
|
|
621
|
+
* To a Graph API URL like:
|
|
622
|
+
* https://graph.microsoft.com/v1.0/sites/company.sharepoint.com:/sites/IT:/drive/root:/Shared Documents/Folder/Dictionary.xlsx:/content
|
|
623
|
+
*/
|
|
624
|
+
private buildGraphApiUrlForFile(fileUrl: string): string {
|
|
625
|
+
const url = new URL(fileUrl);
|
|
626
|
+
const hostname = url.hostname;
|
|
627
|
+
const pathParts = url.pathname.split('/').filter(p => p);
|
|
628
|
+
|
|
629
|
+
// Find the site path (e.g., /sites/IT)
|
|
630
|
+
const siteIndex = pathParts.indexOf('sites');
|
|
631
|
+
if (siteIndex === -1) {
|
|
632
|
+
throw new Error('Invalid SharePoint URL - must contain /sites/');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const siteName = pathParts[siteIndex + 1];
|
|
636
|
+
if (!siteName) {
|
|
637
|
+
throw new Error('Invalid SharePoint URL - missing site name');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Everything after the site name is the file path (including filename)
|
|
641
|
+
const filePath = pathParts.slice(siteIndex + 2).join('/');
|
|
642
|
+
|
|
643
|
+
// Build the Graph API URL for the specific file
|
|
644
|
+
// Format: /sites/{hostname}:/sites/{siteName}:/drive/root:/{filePath}:/content
|
|
645
|
+
const graphUrl = `https://graph.microsoft.com/v1.0/sites/${hostname}:/sites/${siteName}:/drive/root:/${filePath}:/content`;
|
|
646
|
+
|
|
647
|
+
return graphUrl;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Check if authenticated for SharePoint access
|
|
652
|
+
*/
|
|
653
|
+
public isSharePointAuthenticated(): boolean {
|
|
654
|
+
return this.accessToken !== null;
|
|
655
|
+
}
|
|
656
|
+
}
|