From 4cc3102758e96de41f3b6204d45e4094d0b3aaa6 Mon Sep 17 00:00:00 2001 From: hurxxxx Date: Mon, 24 Feb 2025 23:14:10 +0900 Subject: [PATCH] feat: onedrive file picker integration --- backend/open_webui/config.py | 12 + backend/open_webui/main.py | 8 + backend/open_webui/routers/retrieval.py | 9 + package-lock.json | 22 - package.json | 1 - src/lib/apis/retrieval/index.ts | 1 + .../admin/Settings/Documents.svelte | 15 + src/lib/components/chat/MessageInput.svelte | 11 +- .../chat/MessageInput/InputMenu.svelte | 47 +- src/lib/stores/index.ts | 1 + src/lib/utils/onedrive-auth.ts | 42 -- src/lib/utils/onedrive-file-picker.ts | 463 ++++++++++-------- 12 files changed, 337 insertions(+), 295 deletions(-) delete mode 100644 src/lib/utils/onedrive-auth.ts diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index b2f8dccca..91cc2e992 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1570,6 +1570,18 @@ GOOGLE_DRIVE_API_KEY = PersistentConfig( os.environ.get("GOOGLE_DRIVE_API_KEY", ""), ) +ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig( + "ENABLE_ONEDRIVE_INTEGRATION", + "onedrive.enable", + os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true", +) + +ONEDRIVE_CLIENT_ID = PersistentConfig( + "ONEDRIVE_CLIENT_ID", + "onedrive.client_id", + os.environ.get("ONEDRIVE_CLIENT_ID", ""), +) + # RAG Content Extraction CONTENT_EXTRACTION_ENGINE = PersistentConfig( "CONTENT_EXTRACTION_ENGINE", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 1371f7d15..62e53e34c 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -95,6 +95,7 @@ from open_webui.config import ( OLLAMA_API_CONFIGS, # OpenAI ENABLE_OPENAI_API, + ONEDRIVE_CLIENT_ID, OPENAI_API_BASE_URLS, OPENAI_API_KEYS, OPENAI_API_CONFIGS, @@ -217,11 +218,13 @@ from open_webui.config import ( GOOGLE_PSE_ENGINE_ID, GOOGLE_DRIVE_CLIENT_ID, GOOGLE_DRIVE_API_KEY, + ONEDRIVE_CLIENT_ID, ENABLE_RAG_HYBRID_SEARCH, ENABLE_RAG_LOCAL_WEB_FETCH, ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, ENABLE_RAG_WEB_SEARCH, ENABLE_GOOGLE_DRIVE_INTEGRATION, + ENABLE_ONEDRIVE_INTEGRATION, UPLOAD_DIR, # WebUI WEBUI_AUTH, @@ -568,6 +571,7 @@ app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION +app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID @@ -1150,6 +1154,7 @@ async def get_app_config(request: Request): "enable_admin_export": ENABLE_ADMIN_EXPORT, "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION, } if user is not None else {} @@ -1181,6 +1186,9 @@ async def get_app_config(request: Request): "client_id": GOOGLE_DRIVE_CLIENT_ID.value, "api_key": GOOGLE_DRIVE_API_KEY.value, }, + "onedrive": { + "client_id": ONEDRIVE_CLIENT_ID.value + } } if user is not None else {} diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index c2cb68c5d..51f77d6b1 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -353,6 +353,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES, "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT, "enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + "enable_onedrive_integration": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, "content_extraction": { "engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "tika_server_url": request.app.state.config.TIKA_SERVER_URL, @@ -381,6 +382,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "search": { "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH, "drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + "onedrive": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE, "searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL, "google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY, @@ -478,6 +480,7 @@ class ConfigUpdateForm(BaseModel): RAG_FULL_CONTEXT: Optional[bool] = None pdf_extract_images: Optional[bool] = None enable_google_drive_integration: Optional[bool] = None + enable_onedrive_integration: Optional[bool] = None file: Optional[FileConfig] = None content_extraction: Optional[ContentExtractionConfig] = None chunk: Optional[ChunkParamUpdateForm] = None @@ -507,6 +510,12 @@ async def update_rag_config( else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION ) + request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ( + form_data.enable_onedrive_integration + if form_data.enable_onedrive_integration is not None + else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION + ) + if form_data.file is not None: request.app.state.config.FILE_MAX_SIZE = form_data.file.max_size request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count diff --git a/package-lock.json b/package-lock.json index 066cf2be5..c65870772 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "open-webui", "version": "0.5.16", "dependencies": { - "@azure/msal-browser": "^4.4.0", "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", "@codemirror/language-data": "^6.5.1", @@ -135,27 +134,6 @@ "node": ">=6.0.0" } }, - "node_modules/@azure/msal-browser": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.4.0.tgz", - "integrity": "sha512-rU6juYXk67CKQmpgi6fDgZoPQ9InZ1760z1BSAH7RbeIc4lHZM/Tu+H0CyRk7cnrfvTkexyYE4pjYhMghpzheA==", - "license": "MIT", - "dependencies": { - "@azure/msal-common": "15.2.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-common": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz", - "integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@babel/runtime": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", diff --git a/package.json b/package.json index 0e8fe2bc6..86568869f 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ }, "type": "module", "dependencies": { - "@azure/msal-browser": "^4.4.0", "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", "@codemirror/language-data": "^6.5.1", diff --git a/src/lib/apis/retrieval/index.ts b/src/lib/apis/retrieval/index.ts index ed07ab5d0..31317fe0b 100644 --- a/src/lib/apis/retrieval/index.ts +++ b/src/lib/apis/retrieval/index.ts @@ -52,6 +52,7 @@ type YoutubeConfigForm = { type RAGConfigForm = { pdf_extract_images?: boolean; enable_google_drive_integration?: boolean; + enable_onedrive_integration?: boolean; chunk?: ChunkConfigForm; content_extraction?: ContentExtractConfigForm; web_loader_ssl_verification?: boolean; diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index b79086309..248f6e9f5 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -61,6 +61,7 @@ let RAG_FULL_CONTEXT = false; let enableGoogleDriveIntegration = false; + let enableOneDriveIntegration = false; let OpenAIUrl = ''; let OpenAIKey = ''; @@ -189,6 +190,7 @@ const res = await updateRAGConfig(localStorage.token, { pdf_extract_images: pdfExtractImages, enable_google_drive_integration: enableGoogleDriveIntegration, + enable_onedrive_integration: enableOneDriveIntegration, file: { max_size: fileMaxSize === '' ? null : fileMaxSize, max_count: fileMaxCount === '' ? null : fileMaxCount @@ -271,6 +273,7 @@ fileMaxCount = res?.file.max_count ?? ''; enableGoogleDriveIntegration = res.enable_google_drive_integration; + enableOneDriveIntegration = res.enable_onedrive_integration; } }); @@ -653,6 +656,18 @@ +
{$i18n.t('OneDrive')}
+ +
+
+
{$i18n.t('Enable OneDrive')}
+
+ +
+
+
+ +
diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index a81139d2f..bf2f5cddb 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -2,7 +2,7 @@ import { toast } from 'svelte-sonner'; import { v4 as uuidv4 } from 'uuid'; import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker'; - import { openOneDrivePicker } from '$lib/utils/onedrive-file-picker'; + import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker'; import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte'; const dispatch = createEventDispatcher(); @@ -1111,10 +1111,10 @@ }} uploadOneDriveHandler={async () => { try { - const fileData = await openOneDrivePicker(); + const fileData = await pickAndDownloadFile(); if (fileData) { const file = new File([fileData.blob], fileData.name, { - type: fileData.blob.type + type: fileData.blob.type || 'application/octet-stream' }); await uploadFileHandler(file); } else { @@ -1122,11 +1122,6 @@ } } catch (error) { console.error('OneDrive Error:', error); - toast.error( - $i18n.t('Error accessing OneDrive: {{error}}', { - error: error.message - }) - ); } }} onClose={async () => { diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte index 91f9cf81b..7f7660f19 100644 --- a/src/lib/components/chat/MessageInput/InputMenu.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -228,30 +228,41 @@ {/if} - {#if $config?.features?.enable_onedrive_integration || true} + {#if $config?.features?.enable_onedrive_integration} { uploadOneDriveHandler(); }} > - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{$i18n.t('OneDrive')}
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index f96670cb6..1f6b400e0 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -204,6 +204,7 @@ type Config = { enable_login_form: boolean; enable_web_search?: boolean; enable_google_drive_integration: boolean; + enable_onedrive_integration: boolean; enable_image_generation: boolean; enable_admin_export: boolean; enable_admin_chat_access: boolean; diff --git a/src/lib/utils/onedrive-auth.ts b/src/lib/utils/onedrive-auth.ts deleted file mode 100644 index be2de44a0..000000000 --- a/src/lib/utils/onedrive-auth.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PublicClientApplication } from '@azure/msal-browser'; - -const msalParams = { - auth: { - authority: 'https://login.microsoftonline.com/consumers', - clientId: '2ab80a1e-7300-4cb1-beac-c38c730e8b7f' - } -}; - -// MSAL 초기화 -const app = new PublicClientApplication(msalParams); - -export async function initializeMsal() { - try { - await app.initialize(); - console.log('MSAL initialized successfully'); - } catch (error) { - console.error('MSAL initialization error:', error); - } - } - - export async function getToken(): Promise { - const authParams = { scopes: ['OneDrive.ReadWrite'] }; - let accessToken = ''; - - try { - // Ensure initialization happens early - await initializeMsal(); - const resp = await app.acquireTokenSilent(authParams); - accessToken = resp.accessToken; - } catch (err) { - const resp = await app.loginPopup(authParams); - app.setActiveAccount(resp.account); - - if (resp.idToken) { - const resp2 = await app.acquireTokenSilent(authParams); - accessToken = resp2.accessToken; - } - } - - return accessToken; - } diff --git a/src/lib/utils/onedrive-file-picker.ts b/src/lib/utils/onedrive-file-picker.ts index d003e38ec..e3a80c912 100644 --- a/src/lib/utils/onedrive-file-picker.ts +++ b/src/lib/utils/onedrive-file-picker.ts @@ -1,211 +1,266 @@ -// src/lib/utils/onedrive-file-picker.ts -import { getToken } from './onedrive-auth'; +let CLIENT_ID = ''; +async function getCredentials() { + if (CLIENT_ID) return; + const response = await fetch('/api/config'); + if (!response.ok) { + throw new Error('Failed to fetch OneDrive credentials'); + } + const config = await response.json(); + CLIENT_ID = config.onedrive?.client_id; + if (!CLIENT_ID) { + throw new Error('OneDrive client ID not configured'); + } +} + +function loadMsalScript(): Promise { + return new Promise((resolve, reject) => { + const win = window; + if (win.msal) { + resolve(); + return; + } + const script = document.createElement('script'); + script.src = 'https://alcdn.msauth.net/browser/2.19.0/js/msal-browser.min.js'; + script.async = true; + script.onload = () => resolve(); + script.onerror = () => reject(new Error('Failed to load MSAL script')); + document.head.appendChild(script); + }); +} + +let msalInstance: any; + +// Initialize MSAL authentication +async function initializeMsal() { + if (!CLIENT_ID) { + await getCredentials(); + } + const msalParams = { + auth: { + authority: 'https://login.microsoftonline.com/consumers', + clientId: CLIENT_ID + } + }; + try { + await loadMsalScript(); + const win = window; + msalInstance = new win.msal.PublicClientApplication(msalParams); + if (msalInstance.initialize) { + await msalInstance.initialize(); + } + } catch (error) { + console.error('MSAL initialization error:', error); + } +} + +// Retrieve OneDrive access token +async function getToken(): Promise { + const authParams = { scopes: ['OneDrive.ReadWrite'] }; + let accessToken = ''; + try { + await initializeMsal(); + const resp = await msalInstance.acquireTokenSilent(authParams); + accessToken = resp.accessToken; + } catch (err) { + const resp = await msalInstance.loginPopup(authParams); + msalInstance.setActiveAccount(resp.account); + if (resp.idToken) { + const resp2 = await msalInstance.acquireTokenSilent(authParams); + accessToken = resp2.accessToken; + } + } + return accessToken; +} const baseUrl = "https://onedrive.live.com/picker"; const params = { - sdk: '8.0', - entry: { - oneDrive: { - files: {} - } - }, - authentication: {}, - messaging: { - origin: 'http://localhost:3000', // 현재 부모 페이지의 origin - channelId: '27' // 메시징 채널용 임의의 ID - }, - typesAndSources: { - mode: 'files', - pivots: { - oneDrive: true, - recent: true - } - } + sdk: '8.0', + entry: { + oneDrive: { + files: {} + } + }, + authentication: {}, + messaging: { + origin: window?.location?.origin, + channelId: crypto.randomUUID() + }, + typesAndSources: { + mode: 'files', + pivots: { + oneDrive: true, + recent: true + } + } }; -/** - * OneDrive 파일 피커 창을 열고, 사용자가 선택한 파일 메타데이터를 받아오는 함수 - */ -export async function openOneDrivePicker(): Promise { - // SSR 환경(SvelteKit)에서 window 객체가 없을 수 있으므로 가드 - if (typeof window === 'undefined') { - throw new Error('Not in browser environment'); - } - - return new Promise(async (resolve, reject) => { - let pickerWindow: Window | null = null; - let channelPort: MessagePort | null = null; - - try { - const authToken = await getToken(); - if (!authToken) { - return reject(new Error('Failed to acquire access token')); - } - - // 팝업 창 오픈 - pickerWindow = window.open('', 'OneDrivePicker', 'width=800,height=600'); - if (!pickerWindow) { - return reject(new Error('Failed to open OneDrive picker window')); - } - - // 쿼리스트링 구성 - const queryString = new URLSearchParams({ - filePicker: JSON.stringify(params) - }); - const url = `${baseUrl}?${queryString.toString()}`; - - // 새로 연 window에 form을 동적으로 추가하여 POST - const form = pickerWindow.document.createElement('form'); - form.setAttribute('action', url); - form.setAttribute('method', 'POST'); - - const input = pickerWindow.document.createElement('input'); - input.setAttribute('type', 'hidden'); - input.setAttribute('name', 'access_token'); - input.setAttribute('value', authToken); - - form.appendChild(input); - pickerWindow.document.body.appendChild(form); - form.submit(); - - // 부모 창에서 message 이벤트 수신 - const handleWindowMessage = (event: MessageEvent) => { - // pickerWindow가 아닌 다른 window에서 온 메시지는 무시 - if (event.source !== pickerWindow) return; - - const message = event.data; - - // 초기화 메시지 => SharedWorker(MessageChannel) 식으로 포트 받기 - if ( - message?.type === 'initialize' && - message?.channelId === params.messaging.channelId - ) { - channelPort = event.ports?.[0]; - if (!channelPort) return; - - channelPort.addEventListener('message', handlePortMessage); - channelPort.start(); - - // picker iframe에 'activate' 전달 - channelPort.postMessage({ - type: 'activate' - }); - } - }; - - // 포트 메시지 핸들러 - const handlePortMessage = async (portEvent: MessageEvent) => { - const portData = portEvent.data; - switch (portData.type) { - case 'notification': - console.log('notification:', portData); - break; - - case 'command': { - // picker에 응답 - channelPort?.postMessage({ - type: 'acknowledge', - id: portData.id - }); - - const command = portData.data; - - switch (command.command) { - case 'authenticate': { - // 재인증 - try { - const newToken = await getToken(); - if (newToken) { - channelPort?.postMessage({ - type: 'result', - id: portData.id, - data: { - result: 'token', - token: newToken - } - }); - } else { - throw new Error('Could not retrieve auth token'); - } - } catch (err) { - console.error(err); - channelPort?.postMessage({ - result: 'error', - error: { - code: 'tokenError', - message: 'Failed to get token' - }, - isExpected: true - }); - } - break; - } - - case 'close': { - // 사용자가 취소하거나 닫았을 경우 - cleanup(); - resolve(null); - break; - } - - case 'pick': { - // 사용자가 파일 선택 완료 - console.log('Picked:', command); - /** - * command 안에는 사용자가 선택한 파일들의 메타데이터 정보가 들어있습니다. - * 필요하다면 Microsoft Graph API 등을 통해 Blob(실제 파일 데이터)을 받아와야 할 수 있습니다. - */ - - // picker에 응답 - channelPort?.postMessage({ - type: 'result', - id: portData.id, - data: { - result: 'success' - } - }); - - // 선택한 파일들(메타정보)을 resolve - cleanup(); - resolve(command); - break; - } - - default: { - console.warn('Unsupported command:', command); - channelPort?.postMessage({ - result: 'error', - error: { - code: 'unsupportedCommand', - message: command.command - }, - isExpected: true - }); - break; - } - } - break; - } - } - }; - - function cleanup() { - window.removeEventListener('message', handleWindowMessage); - if (channelPort) { - channelPort.removeEventListener('message', handlePortMessage); - } - if (pickerWindow) { - pickerWindow.close(); - pickerWindow = null; - } - } - - // 메시지 이벤트 등록 - window.addEventListener('message', handleWindowMessage); - } catch (err) { - if (pickerWindow) pickerWindow.close(); - reject(err); - } - }); +// Download file from OneDrive +async function downloadOneDriveFile(fileInfo: any): Promise { + const accessToken = await getToken(); + if (!accessToken) { + throw new Error('Unable to retrieve OneDrive access token.'); + } + const fileInfoUrl = `${fileInfo["@sharePoint.endpoint"]}/drives/${fileInfo.parentReference.driveId}/items/${fileInfo.id}`; + const response = await fetch(fileInfoUrl, { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); + if (!response.ok) { + throw new Error('Failed to fetch file information.'); + } + const fileData = await response.json(); + const downloadUrl = fileData['@content.downloadUrl']; + const downloadResponse = await fetch(downloadUrl); + if (!downloadResponse.ok) { + throw new Error('Failed to download file.'); + } + return await downloadResponse.blob(); } + +// Open OneDrive file picker and return selected file metadata +export async function openOneDrivePicker(): Promise { + if (typeof window === 'undefined') { + throw new Error('Not in browser environment'); + } + return new Promise((resolve, reject) => { + let pickerWindow: Window | null = null; + let channelPort: MessagePort | null = null; + + const handleWindowMessage = (event: MessageEvent) => { + if (event.source !== pickerWindow) return; + const message = event.data; + if (message?.type === 'initialize' && message?.channelId === params.messaging.channelId) { + channelPort = event.ports?.[0]; + if (!channelPort) return; + channelPort.addEventListener('message', handlePortMessage); + channelPort.start(); + channelPort.postMessage({ type: 'activate' }); + } + }; + + const handlePortMessage = async (portEvent: MessageEvent) => { + const portData = portEvent.data; + switch (portData.type) { + case 'notification': + break; + case 'command': { + channelPort?.postMessage({ type: 'acknowledge', id: portData.id }); + const command = portData.data; + switch (command.command) { + case 'authenticate': { + try { + const newToken = await getToken(); + if (newToken) { + channelPort?.postMessage({ + type: 'result', + id: portData.id, + data: { result: 'token', token: newToken } + }); + } else { + throw new Error('Could not retrieve auth token'); + } + } catch (err) { + console.error(err); + channelPort?.postMessage({ + result: 'error', + error: { code: 'tokenError', message: 'Failed to get token' }, + isExpected: true + }); + } + break; + } + case 'close': { + cleanup(); + resolve(null); + break; + } + case 'pick': { + channelPort?.postMessage({ + type: 'result', + id: portData.id, + data: { result: 'success' } + }); + cleanup(); + resolve(command); + break; + } + default: { + console.warn('Unsupported command:', command); + channelPort?.postMessage({ + result: 'error', + error: { code: 'unsupportedCommand', message: command.command }, + isExpected: true + }); + break; + } + } + break; + } + } + }; + + function cleanup() { + window.removeEventListener('message', handleWindowMessage); + if (channelPort) { + channelPort.removeEventListener('message', handlePortMessage); + } + if (pickerWindow) { + pickerWindow.close(); + pickerWindow = null; + } + } + + const initializePicker = async () => { + try { + const authToken = await getToken(); + if (!authToken) { + return reject(new Error('Failed to acquire access token')); + } + pickerWindow = window.open('', 'OneDrivePicker', 'width=800,height=600'); + if (!pickerWindow) { + return reject(new Error('Failed to open OneDrive picker window')); + } + const queryString = new URLSearchParams({ + filePicker: JSON.stringify(params) + }); + const url = `${baseUrl}?${queryString.toString()}`; + const form = pickerWindow.document.createElement('form'); + form.setAttribute('action', url); + form.setAttribute('method', 'POST'); + const input = pickerWindow.document.createElement('input'); + input.setAttribute('type', 'hidden'); + input.setAttribute('name', 'access_token'); + input.setAttribute('value', authToken); + form.appendChild(input); + pickerWindow.document.body.appendChild(form); + form.submit(); + window.addEventListener('message', handleWindowMessage); + } catch (err) { + if (pickerWindow) pickerWindow.close(); + reject(err); + } + }; + + initializePicker(); + }); +} + +// Pick and download file from OneDrive +export async function pickAndDownloadFile(): Promise<{ blob: Blob; name: string } | null> { + try { + const pickerResult = await openOneDrivePicker(); + if (!pickerResult || !pickerResult.items || pickerResult.items.length === 0) { + return null; + } + const selectedFile = pickerResult.items[0]; + const blob = await downloadOneDriveFile(selectedFile); + return { blob, name: selectedFile.name }; + } catch (error) { + console.error('Error occurred during OneDrive file pick/download:', error); + throw error; + } +} + +export { downloadOneDriveFile };