mirror of
https://github.com/open-webui/open-webui
synced 2025-05-17 03:54:02 +00:00
feat: onedrive file picker integration
This commit is contained in:
parent
0335d479f9
commit
4cc3102758
@ -1570,6 +1570,18 @@ GOOGLE_DRIVE_API_KEY = PersistentConfig(
|
|||||||
os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
|
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
|
# RAG Content Extraction
|
||||||
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
||||||
"CONTENT_EXTRACTION_ENGINE",
|
"CONTENT_EXTRACTION_ENGINE",
|
||||||
|
@ -95,6 +95,7 @@ from open_webui.config import (
|
|||||||
OLLAMA_API_CONFIGS,
|
OLLAMA_API_CONFIGS,
|
||||||
# OpenAI
|
# OpenAI
|
||||||
ENABLE_OPENAI_API,
|
ENABLE_OPENAI_API,
|
||||||
|
ONEDRIVE_CLIENT_ID,
|
||||||
OPENAI_API_BASE_URLS,
|
OPENAI_API_BASE_URLS,
|
||||||
OPENAI_API_KEYS,
|
OPENAI_API_KEYS,
|
||||||
OPENAI_API_CONFIGS,
|
OPENAI_API_CONFIGS,
|
||||||
@ -217,11 +218,13 @@ from open_webui.config import (
|
|||||||
GOOGLE_PSE_ENGINE_ID,
|
GOOGLE_PSE_ENGINE_ID,
|
||||||
GOOGLE_DRIVE_CLIENT_ID,
|
GOOGLE_DRIVE_CLIENT_ID,
|
||||||
GOOGLE_DRIVE_API_KEY,
|
GOOGLE_DRIVE_API_KEY,
|
||||||
|
ONEDRIVE_CLIENT_ID,
|
||||||
ENABLE_RAG_HYBRID_SEARCH,
|
ENABLE_RAG_HYBRID_SEARCH,
|
||||||
ENABLE_RAG_LOCAL_WEB_FETCH,
|
ENABLE_RAG_LOCAL_WEB_FETCH,
|
||||||
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||||
ENABLE_RAG_WEB_SEARCH,
|
ENABLE_RAG_WEB_SEARCH,
|
||||||
ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
|
ENABLE_ONEDRIVE_INTEGRATION,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
# WebUI
|
# WebUI
|
||||||
WEBUI_AUTH,
|
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.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_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.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
|
||||||
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
|
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
|
||||||
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
|
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_export": ENABLE_ADMIN_EXPORT,
|
||||||
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
||||||
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
"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
|
if user is not None
|
||||||
else {}
|
else {}
|
||||||
@ -1181,6 +1186,9 @@ async def get_app_config(request: Request):
|
|||||||
"client_id": GOOGLE_DRIVE_CLIENT_ID.value,
|
"client_id": GOOGLE_DRIVE_CLIENT_ID.value,
|
||||||
"api_key": GOOGLE_DRIVE_API_KEY.value,
|
"api_key": GOOGLE_DRIVE_API_KEY.value,
|
||||||
},
|
},
|
||||||
|
"onedrive": {
|
||||||
|
"client_id": ONEDRIVE_CLIENT_ID.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if user is not None
|
if user is not None
|
||||||
else {}
|
else {}
|
||||||
|
@ -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,
|
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
|
||||||
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
|
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
|
||||||
"enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
"enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
|
"enable_onedrive_integration": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||||
"content_extraction": {
|
"content_extraction": {
|
||||||
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||||
"tika_server_url": request.app.state.config.TIKA_SERVER_URL,
|
"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": {
|
"search": {
|
||||||
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
|
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||||
"drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
"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,
|
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
|
||||||
"searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
|
"searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
|
||||||
"google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY,
|
"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
|
RAG_FULL_CONTEXT: Optional[bool] = None
|
||||||
pdf_extract_images: Optional[bool] = None
|
pdf_extract_images: Optional[bool] = None
|
||||||
enable_google_drive_integration: Optional[bool] = None
|
enable_google_drive_integration: Optional[bool] = None
|
||||||
|
enable_onedrive_integration: Optional[bool] = None
|
||||||
file: Optional[FileConfig] = None
|
file: Optional[FileConfig] = None
|
||||||
content_extraction: Optional[ContentExtractionConfig] = None
|
content_extraction: Optional[ContentExtractionConfig] = None
|
||||||
chunk: Optional[ChunkParamUpdateForm] = None
|
chunk: Optional[ChunkParamUpdateForm] = None
|
||||||
@ -507,6 +510,12 @@ async def update_rag_config(
|
|||||||
else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION
|
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:
|
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_SIZE = form_data.file.max_size
|
||||||
request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count
|
request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count
|
||||||
|
22
package-lock.json
generated
22
package-lock.json
generated
@ -8,7 +8,6 @@
|
|||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.5.16",
|
"version": "0.5.16",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^4.4.0",
|
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/language-data": "^6.5.1",
|
"@codemirror/language-data": "^6.5.1",
|
||||||
@ -135,27 +134,6 @@
|
|||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.24.1",
|
"version": "7.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
|
||||||
|
@ -51,7 +51,6 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^4.4.0",
|
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/language-data": "^6.5.1",
|
"@codemirror/language-data": "^6.5.1",
|
||||||
|
@ -52,6 +52,7 @@ type YoutubeConfigForm = {
|
|||||||
type RAGConfigForm = {
|
type RAGConfigForm = {
|
||||||
pdf_extract_images?: boolean;
|
pdf_extract_images?: boolean;
|
||||||
enable_google_drive_integration?: boolean;
|
enable_google_drive_integration?: boolean;
|
||||||
|
enable_onedrive_integration?: boolean;
|
||||||
chunk?: ChunkConfigForm;
|
chunk?: ChunkConfigForm;
|
||||||
content_extraction?: ContentExtractConfigForm;
|
content_extraction?: ContentExtractConfigForm;
|
||||||
web_loader_ssl_verification?: boolean;
|
web_loader_ssl_verification?: boolean;
|
||||||
|
@ -61,6 +61,7 @@
|
|||||||
let RAG_FULL_CONTEXT = false;
|
let RAG_FULL_CONTEXT = false;
|
||||||
|
|
||||||
let enableGoogleDriveIntegration = false;
|
let enableGoogleDriveIntegration = false;
|
||||||
|
let enableOneDriveIntegration = false;
|
||||||
|
|
||||||
let OpenAIUrl = '';
|
let OpenAIUrl = '';
|
||||||
let OpenAIKey = '';
|
let OpenAIKey = '';
|
||||||
@ -189,6 +190,7 @@
|
|||||||
const res = await updateRAGConfig(localStorage.token, {
|
const res = await updateRAGConfig(localStorage.token, {
|
||||||
pdf_extract_images: pdfExtractImages,
|
pdf_extract_images: pdfExtractImages,
|
||||||
enable_google_drive_integration: enableGoogleDriveIntegration,
|
enable_google_drive_integration: enableGoogleDriveIntegration,
|
||||||
|
enable_onedrive_integration: enableOneDriveIntegration,
|
||||||
file: {
|
file: {
|
||||||
max_size: fileMaxSize === '' ? null : fileMaxSize,
|
max_size: fileMaxSize === '' ? null : fileMaxSize,
|
||||||
max_count: fileMaxCount === '' ? null : fileMaxCount
|
max_count: fileMaxCount === '' ? null : fileMaxCount
|
||||||
@ -271,6 +273,7 @@
|
|||||||
fileMaxCount = res?.file.max_count ?? '';
|
fileMaxCount = res?.file.max_count ?? '';
|
||||||
|
|
||||||
enableGoogleDriveIntegration = res.enable_google_drive_integration;
|
enableGoogleDriveIntegration = res.enable_google_drive_integration;
|
||||||
|
enableOneDriveIntegration = res.enable_onedrive_integration;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -653,6 +656,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm font-medium mb-1">{$i18n.t('OneDrive')}</div>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<div class="text-xs font-medium">{$i18n.t('Enable OneDrive')}</div>
|
||||||
|
<div>
|
||||||
|
<Switch bind:state={enableOneDriveIntegration} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||||
|
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
|
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';
|
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@ -1111,10 +1111,10 @@
|
|||||||
}}
|
}}
|
||||||
uploadOneDriveHandler={async () => {
|
uploadOneDriveHandler={async () => {
|
||||||
try {
|
try {
|
||||||
const fileData = await openOneDrivePicker();
|
const fileData = await pickAndDownloadFile();
|
||||||
if (fileData) {
|
if (fileData) {
|
||||||
const file = new File([fileData.blob], fileData.name, {
|
const file = new File([fileData.blob], fileData.name, {
|
||||||
type: fileData.blob.type
|
type: fileData.blob.type || 'application/octet-stream'
|
||||||
});
|
});
|
||||||
await uploadFileHandler(file);
|
await uploadFileHandler(file);
|
||||||
} else {
|
} else {
|
||||||
@ -1122,11 +1122,6 @@
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('OneDrive Error:', error);
|
console.error('OneDrive Error:', error);
|
||||||
toast.error(
|
|
||||||
$i18n.t('Error accessing OneDrive: {{error}}', {
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClose={async () => {
|
onClose={async () => {
|
||||||
|
@ -228,30 +228,41 @@
|
|||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $config?.features?.enable_onedrive_integration || true}
|
{#if $config?.features?.enable_onedrive_integration}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
uploadOneDriveHandler();
|
uploadOneDriveHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-5 h-5">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
|
||||||
<path
|
<mask id="mask0_87_7796" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="6" width="32" height="20">
|
||||||
d="M21.69 13.91l-5.5-3.16l-4.08 3.45l-1.87-1.08l-4.86 4.47l.86.5a2.998 2.998 0 0 0 4.09-1.11a3 3 0 0 0 4.09-1.11a3.06 3.06 0 0 0 1.27-.13a3 3 0 0 0 4.09-1.11a2.81 2.81 0 0 0 1.91-.72z"
|
<path d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z" fill="#C4C4C4"/>
|
||||||
fill="#0364B8"
|
</mask>
|
||||||
/>
|
<g mask="url(#mask0_87_7796)">
|
||||||
<path
|
<path d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z" fill="url(#paint0_linear_87_7796)"/>
|
||||||
d="M7.5 13.5L2 10.5l5-3l5.5 3.16l-5 2.84z"
|
<path d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z" fill="url(#paint1_linear_87_7796)"/>
|
||||||
fill="#0078D4"
|
<path d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z" fill="url(#paint2_linear_87_7796)"/>
|
||||||
/>
|
<path d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z" fill="url(#paint3_linear_87_7796)"/>
|
||||||
<path
|
</g>
|
||||||
d="M16.19 10.75L12 7.94V4.5l5.5 3.16l-1.31 3.09z"
|
<defs>
|
||||||
fill="#1490DF"
|
<linearGradient id="paint0_linear_87_7796" x1="4.42591" y1="24.6668" x2="27.2309" y2="23.2764" gradientUnits="userSpaceOnUse">
|
||||||
/>
|
<stop stop-color="#2086B8"/>
|
||||||
<path
|
<stop offset="1" stop-color="#46D3F6"/>
|
||||||
d="M12 4.5l-5 3l-5-3l5-3l5 3z"
|
</linearGradient>
|
||||||
fill="#28A8EA"
|
<linearGradient id="paint1_linear_87_7796" x1="23.8302" y1="19.6668" x2="30.2108" y2="15.2082" gradientUnits="userSpaceOnUse">
|
||||||
/>
|
<stop stop-color="#1694DB"/>
|
||||||
|
<stop offset="1" stop-color="#62C3FE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear_87_7796" x1="8.51037" y1="7.33333" x2="23.3335" y2="15.9348" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#0D3D78"/>
|
||||||
|
<stop offset="1" stop-color="#063B83"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint3_linear_87_7796" x1="-0.340429" y1="19.9998" x2="14.5634" y2="14.4649" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#16589B"/>
|
||||||
|
<stop offset="1" stop-color="#1464B7"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="line-clamp-1">{$i18n.t('OneDrive')}</div>
|
<div class="line-clamp-1">{$i18n.t('OneDrive')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
@ -204,6 +204,7 @@ type Config = {
|
|||||||
enable_login_form: boolean;
|
enable_login_form: boolean;
|
||||||
enable_web_search?: boolean;
|
enable_web_search?: boolean;
|
||||||
enable_google_drive_integration: boolean;
|
enable_google_drive_integration: boolean;
|
||||||
|
enable_onedrive_integration: boolean;
|
||||||
enable_image_generation: boolean;
|
enable_image_generation: boolean;
|
||||||
enable_admin_export: boolean;
|
enable_admin_export: boolean;
|
||||||
enable_admin_chat_access: boolean;
|
enable_admin_chat_access: boolean;
|
||||||
|
@ -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<string> {
|
|
||||||
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;
|
|
||||||
}
|
|
@ -1,211 +1,266 @@
|
|||||||
// src/lib/utils/onedrive-file-picker.ts
|
let CLIENT_ID = '';
|
||||||
import { getToken } from './onedrive-auth';
|
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
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<string> {
|
||||||
|
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 baseUrl = "https://onedrive.live.com/picker";
|
||||||
const params = {
|
const params = {
|
||||||
sdk: '8.0',
|
sdk: '8.0',
|
||||||
entry: {
|
entry: {
|
||||||
oneDrive: {
|
oneDrive: {
|
||||||
files: {}
|
files: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
authentication: {},
|
authentication: {},
|
||||||
messaging: {
|
messaging: {
|
||||||
origin: 'http://localhost:3000', // 현재 부모 페이지의 origin
|
origin: window?.location?.origin,
|
||||||
channelId: '27' // 메시징 채널용 임의의 ID
|
channelId: crypto.randomUUID()
|
||||||
},
|
},
|
||||||
typesAndSources: {
|
typesAndSources: {
|
||||||
mode: 'files',
|
mode: 'files',
|
||||||
pivots: {
|
pivots: {
|
||||||
oneDrive: true,
|
oneDrive: true,
|
||||||
recent: true
|
recent: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Download file from OneDrive
|
||||||
* OneDrive 파일 피커 창을 열고, 사용자가 선택한 파일 메타데이터를 받아오는 함수
|
async function downloadOneDriveFile(fileInfo: any): Promise<Blob> {
|
||||||
*/
|
const accessToken = await getToken();
|
||||||
export async function openOneDrivePicker(): Promise<any> {
|
if (!accessToken) {
|
||||||
// SSR 환경(SvelteKit)에서 window 객체가 없을 수 있으므로 가드
|
throw new Error('Unable to retrieve OneDrive access token.');
|
||||||
if (typeof window === 'undefined') {
|
}
|
||||||
throw new Error('Not in browser environment');
|
const fileInfoUrl = `${fileInfo["@sharePoint.endpoint"]}/drives/${fileInfo.parentReference.driveId}/items/${fileInfo.id}`;
|
||||||
}
|
const response = await fetch(fileInfoUrl, {
|
||||||
|
headers: {
|
||||||
return new Promise<any>(async (resolve, reject) => {
|
'Authorization': `Bearer ${accessToken}`
|
||||||
let pickerWindow: Window | null = null;
|
}
|
||||||
let channelPort: MessagePort | null = null;
|
});
|
||||||
|
if (!response.ok) {
|
||||||
try {
|
throw new Error('Failed to fetch file information.');
|
||||||
const authToken = await getToken();
|
}
|
||||||
if (!authToken) {
|
const fileData = await response.json();
|
||||||
return reject(new Error('Failed to acquire access token'));
|
const downloadUrl = fileData['@content.downloadUrl'];
|
||||||
}
|
const downloadResponse = await fetch(downloadUrl);
|
||||||
|
if (!downloadResponse.ok) {
|
||||||
// 팝업 창 오픈
|
throw new Error('Failed to download file.');
|
||||||
pickerWindow = window.open('', 'OneDrivePicker', 'width=800,height=600');
|
}
|
||||||
if (!pickerWindow) {
|
return await downloadResponse.blob();
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open OneDrive file picker and return selected file metadata
|
||||||
|
export async function openOneDrivePicker(): Promise<any | null> {
|
||||||
|
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 };
|
||||||
|
Loading…
Reference in New Issue
Block a user