mirror of
https://github.com/open-webui/open-webui
synced 2025-06-04 03:37:35 +00:00
feat: add onedrive file picker
This commit is contained in:
parent
737b1723c7
commit
0335d479f9
22
package-lock.json
generated
22
package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"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",
|
||||||
@ -134,6 +135,27 @@
|
|||||||
"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,6 +51,7 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
|
@ -2,6 +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 { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
|
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@ -1108,6 +1109,26 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
uploadOneDriveHandler={async () => {
|
||||||
|
try {
|
||||||
|
const fileData = await openOneDrivePicker();
|
||||||
|
if (fileData) {
|
||||||
|
const file = new File([fileData.blob], fileData.name, {
|
||||||
|
type: fileData.blob.type
|
||||||
|
});
|
||||||
|
await uploadFileHandler(file);
|
||||||
|
} else {
|
||||||
|
console.log('No file was selected from OneDrive');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OneDrive Error:', error);
|
||||||
|
toast.error(
|
||||||
|
$i18n.t('Error accessing OneDrive: {{error}}', {
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onClose={async () => {
|
onClose={async () => {
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { config, user, tools as _tools, mobile } from '$lib/stores';
|
import { config, user, tools as _tools, mobile } from '$lib/stores';
|
||||||
import { createPicker } from '$lib/utils/google-drive-picker';
|
import { createPicker } from '$lib/utils/google-drive-picker';
|
||||||
|
|
||||||
import { getTools } from '$lib/apis/tools';
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
|
||||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
@ -24,6 +25,7 @@
|
|||||||
export let inputFilesHandler: Function;
|
export let inputFilesHandler: Function;
|
||||||
|
|
||||||
export let uploadGoogleDriveHandler: Function;
|
export let uploadGoogleDriveHandler: Function;
|
||||||
|
export let uploadOneDriveHandler: Function;
|
||||||
|
|
||||||
export let selectedToolIds: string[] = [];
|
export let selectedToolIds: string[] = [];
|
||||||
|
|
||||||
@ -225,6 +227,35 @@
|
|||||||
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
|
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_onedrive_integration || true}
|
||||||
|
<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"
|
||||||
|
on:click={() => {
|
||||||
|
uploadOneDriveHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-5 h-5">
|
||||||
|
<path
|
||||||
|
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"
|
||||||
|
fill="#0364B8"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7.5 13.5L2 10.5l5-3l5.5 3.16l-5 2.84z"
|
||||||
|
fill="#0078D4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16.19 10.75L12 7.94V4.5l5.5 3.16l-1.31 3.09z"
|
||||||
|
fill="#1490DF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 4.5l-5 3l-5-3l5-3l5 3z"
|
||||||
|
fill="#28A8EA"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="line-clamp-1">{$i18n.t('OneDrive')}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
42
src/lib/utils/onedrive-auth.ts
Normal file
42
src/lib/utils/onedrive-auth.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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;
|
||||||
|
}
|
211
src/lib/utils/onedrive-file-picker.ts
Normal file
211
src/lib/utils/onedrive-file-picker.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
// src/lib/utils/onedrive-file-picker.ts
|
||||||
|
import { getToken } from './onedrive-auth';
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OneDrive 파일 피커 창을 열고, 사용자가 선택한 파일 메타데이터를 받아오는 함수
|
||||||
|
*/
|
||||||
|
export async function openOneDrivePicker(): Promise<any> {
|
||||||
|
// SSR 환경(SvelteKit)에서 window 객체가 없을 수 있으므로 가드
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
throw new Error('Not in browser environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<any>(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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user