mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
feat(file-sharing): Uses short term token for operations.
This commit is contained in:
@@ -107,6 +107,7 @@ export interface IJitsiConference {
|
||||
getParticipantCount: Function;
|
||||
getParticipants: Function;
|
||||
getRole: Function;
|
||||
getShortTermCredentials: Function;
|
||||
getSpeakerStats: () => ISpeakerStats;
|
||||
getSsrcByTrack: Function;
|
||||
getTranscriptionStatus: Function;
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
*/
|
||||
export const FILE_SHARING_PREFIX = 'files';
|
||||
|
||||
|
||||
/**
|
||||
* Used for requesting file sharing service short-term credentials.
|
||||
*/
|
||||
export const FILE_SHARING_SERVICE = 'file-sharing';
|
||||
|
||||
/**
|
||||
* The max file size we accept for upload.
|
||||
*/
|
||||
export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES } from './actionTypes';
|
||||
import { addFile, removeFile, updateFileProgress } from './actions';
|
||||
import { getFileExtension } from './functions.any';
|
||||
import logger from './logger';
|
||||
import { FILE_SHARING_PREFIX } from './constants';
|
||||
import { FILE_SHARING_PREFIX, FILE_SHARING_SERVICE } from './constants';
|
||||
import { IFileMetadata } from './types';
|
||||
import { IStore } from '../app/types';
|
||||
|
||||
/**
|
||||
* Middleware that handles file sharing actions.
|
||||
@@ -24,86 +25,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case UPLOAD_FILES: {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const sessionId = conference?.getMeetingUniqueId();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { fileSharing } = state['features/base/config'];
|
||||
const { connection } = state['features/base/connection'];
|
||||
const { jwt } = state['features/base/jwt'];
|
||||
const roomJid = conference?.room?.roomjid;
|
||||
|
||||
for (const file of action.files) {
|
||||
const jid = connection!.getJid();
|
||||
const fileId = uuidv4();
|
||||
const fileMetadata: IFileMetadata = {
|
||||
authorParticipantId: localParticipant!.id,
|
||||
authorParticipantJid: jid,
|
||||
authorParticipantName: getParticipantDisplayName(state, localParticipant!.id),
|
||||
conferenceFullName: roomJid ?? '',
|
||||
fileId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: getFileExtension(file.name),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
store.dispatch(addFile(fileMetadata));
|
||||
store.dispatch(updateFileProgress(fileId, 1));
|
||||
|
||||
// Upload file.
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('metadata', JSON.stringify(fileMetadata));
|
||||
|
||||
// @ts-ignore
|
||||
formData.append('file', file as Blob, file.name);
|
||||
|
||||
// Use XMLHttpRequest to track upload
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const handleError = () => {
|
||||
logger.warn('Could not upload file:', xhr.statusText);
|
||||
|
||||
store.dispatch(removeFile(fileId));
|
||||
store.dispatch(showErrorNotification({
|
||||
titleKey: 'fileSharing.uploadFailedTitle',
|
||||
descriptionKey: 'fileSharing.uploadFailedDescription',
|
||||
appearance: NOTIFICATION_TYPE.ERROR
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
};
|
||||
|
||||
xhr.open('POST', `${fileSharing!.apiUrl!}/sessions/${sessionId}/files`);
|
||||
xhr.responseType = 'json';
|
||||
|
||||
if (jwt) {
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${jwt}`);
|
||||
conference?.getShortTermCredentials(FILE_SHARING_SERVICE).then((token: string) => {
|
||||
for (const file of action.files) {
|
||||
uploadFile(file, store, token);
|
||||
}
|
||||
|
||||
xhr.upload.onprogress = event => {
|
||||
if (event.lengthComputable) {
|
||||
// We use 99% as the max value to avoid showing 100% before the
|
||||
// upload is actually finished, that is, when the request is completed.
|
||||
const percent = Math.max((event.loaded / event.total) * 100, 99);
|
||||
|
||||
store.dispatch(updateFileProgress(fileId, percent));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
store.dispatch(updateFileProgress(fileId, 100));
|
||||
|
||||
const metadataHandler = conference?.getMetadataHandler();
|
||||
|
||||
metadataHandler?.setMetadata(`${FILE_SHARING_PREFIX}.${fileId}`, fileMetadata);
|
||||
} else {
|
||||
handleError();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = handleError;
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
});
|
||||
|
||||
return next(action);
|
||||
}
|
||||
@@ -112,7 +39,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const { fileSharing } = state['features/base/config'];
|
||||
const { jwt } = state['features/base/jwt'];
|
||||
const sessionId = conference?.getMeetingUniqueId();
|
||||
let doDelete = false;
|
||||
|
||||
@@ -133,18 +59,19 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
|
||||
// Now delete it from the server.
|
||||
fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${action.fileId}`, {
|
||||
conference?.getShortTermCredentials(FILE_SHARING_SERVICE)
|
||||
.then((token: string) => fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${action.fileId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
...jwt && { 'Authorization': `Bearer ${jwt}` }
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
}))
|
||||
.then((response: { ok: any; statusText: any; }) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete file: ${response.statusText}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error: any) => {
|
||||
logger.warn('Could not delete file:', error);
|
||||
});
|
||||
|
||||
@@ -157,14 +84,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const conference = getCurrentConference(state);
|
||||
const sessionId = conference?.getMeetingUniqueId();
|
||||
|
||||
fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/document`, {
|
||||
conference?.getShortTermCredentials(FILE_SHARING_SERVICE)
|
||||
.then((token: string) => fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${action.fileId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-File-Id': action.fileId,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
}))
|
||||
.then((response: any) => response.json())
|
||||
.then((data: { presignedUrl: any; }) => {
|
||||
const url = data.presignedUrl;
|
||||
|
||||
if (!url) {
|
||||
@@ -173,7 +101,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
window.open(url, '_blank', 'noreferrer,noopener');
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error: any) => {
|
||||
logger.warn('Could not download file:', error);
|
||||
|
||||
store.dispatch(showErrorNotification({
|
||||
@@ -189,3 +117,93 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Uploads a file to the server.
|
||||
*
|
||||
* @param {File} file - The file to upload.
|
||||
* @param {IStore} store - The redux store.
|
||||
* @param {string} token - The token to use for requests.
|
||||
* @returns {void}
|
||||
*/
|
||||
function uploadFile(file: File, store: IStore, token: string): void {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const sessionId = conference?.getMeetingUniqueId();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { fileSharing } = state['features/base/config'];
|
||||
const { connection } = state['features/base/connection'];
|
||||
const roomJid = conference?.room?.roomjid;
|
||||
|
||||
const jid = connection!.getJid();
|
||||
const fileId = uuidv4();
|
||||
const fileMetadata: IFileMetadata = {
|
||||
authorParticipantId: localParticipant!.id,
|
||||
authorParticipantJid: jid,
|
||||
authorParticipantName: getParticipantDisplayName(state, localParticipant!.id),
|
||||
conferenceFullName: roomJid ?? '',
|
||||
fileId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: getFileExtension(file.name),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
store.dispatch(addFile(fileMetadata));
|
||||
store.dispatch(updateFileProgress(fileId, 1));
|
||||
|
||||
// Upload file.
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('metadata', JSON.stringify(fileMetadata));
|
||||
|
||||
// @ts-ignore
|
||||
formData.append('file', file as Blob, file.name);
|
||||
|
||||
// Use XMLHttpRequest to track upload
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const handleError = () => {
|
||||
logger.warn('Could not upload file:', xhr.statusText);
|
||||
|
||||
store.dispatch(removeFile(fileId));
|
||||
store.dispatch(showErrorNotification({
|
||||
titleKey: 'fileSharing.uploadFailedTitle',
|
||||
descriptionKey: 'fileSharing.uploadFailedDescription',
|
||||
appearance: NOTIFICATION_TYPE.ERROR
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
};
|
||||
|
||||
xhr.open('POST', `${fileSharing!.apiUrl!}/sessions/${sessionId}/files`);
|
||||
xhr.responseType = 'json';
|
||||
|
||||
if (token) {
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
xhr.upload.onprogress = event => {
|
||||
if (event.lengthComputable) {
|
||||
// We use 99% as the max value to avoid showing 100% before the
|
||||
// upload is actually finished, that is, when the request is completed.
|
||||
const percent = Math.max((event.loaded / event.total) * 100, 99);
|
||||
|
||||
store.dispatch(updateFileProgress(fileId, percent));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
store.dispatch(updateFileProgress(fileId, 100));
|
||||
|
||||
const metadataHandler = conference?.getMetadataHandler();
|
||||
|
||||
metadataHandler?.setMetadata(`${FILE_SHARING_PREFIX}.${fileId}`, fileMetadata);
|
||||
} else {
|
||||
handleError();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = handleError;
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
@@ -10,13 +10,7 @@ security:
|
||||
- HttpBearerKey: []
|
||||
tags:
|
||||
- name: Content sharing history
|
||||
description: crud operation for screenshots
|
||||
- name: Webhook controller
|
||||
description: listens for room destroy events and generates screenshots webhooks and return jaas sign links
|
||||
- name: Document sharing history
|
||||
description: crud operation for shared documents
|
||||
- name: JaaS controller
|
||||
description: Management of jaas screenshots
|
||||
paths:
|
||||
/v1/documents/sessions/{sessionId}/files:
|
||||
get:
|
||||
@@ -58,11 +52,12 @@ paths:
|
||||
tags:
|
||||
- Document sharing history
|
||||
summary: Save an document and metadata
|
||||
description: Add a document to a meeting - allowed only to moderators if any
|
||||
description: Add a document to a meeting - allowed only to those with file-sharing feature if any
|
||||
operationId: addDocumentInMeeting
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
description: The session ID of the meeting.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@@ -77,9 +72,11 @@ paths:
|
||||
properties:
|
||||
metadata:
|
||||
type: string
|
||||
example: '{"conferenceFullName":"myroomname@conference.tenant.jitsi-meet.example.com","timestamp":1741017572040,"fileSize":1042157,"fileId":"e393a7e5-e790-4f43-836e-d27238201904","authorParticipantJid":"9b9a25dc-7e1a-46b8-bae1-ad8be604871c@jitsi-meet.example.com/SVBypbHvcgRT"}'
|
||||
description: The metadata of the document in JSON format.
|
||||
example: '{"conferenceFullName":"myroomname@conference.tenant.jitsi-meet.example.com","timestamp":1741017572040,"fileSize":1042157,"fileId":"e393a7e5-e790-4f43-836e-d27238201904"}'
|
||||
file:
|
||||
type: string
|
||||
description: The file to be uploaded.
|
||||
format: binary
|
||||
responses:
|
||||
'200':
|
||||
@@ -114,21 +111,22 @@ paths:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/v1/documents/sessions/{sessionId}/document:
|
||||
/v1/documents/sessions/{sessionId}/files/{fileId}:
|
||||
get:
|
||||
tags:
|
||||
- Document sharing history
|
||||
summary: Get file pre-signed URL plus document info
|
||||
description: Used by UI to get the presign url for the file before serving it to the user who needs it
|
||||
operationId: getFileInfo
|
||||
operationId: getDocumentInfoDuringMeeting
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: X-File-Id
|
||||
in: header
|
||||
- name: fileId
|
||||
in: path
|
||||
description: The file ID to be deleted.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@@ -139,7 +137,6 @@ paths:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: '#/components/schemas/DocumentMetadataResponse'
|
||||
/v1/documents/sessions/{sessionId}/files/{fileId}:
|
||||
delete:
|
||||
tags:
|
||||
- Document sharing history
|
||||
@@ -149,11 +146,14 @@ paths:
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
description: The session ID of the meeting.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: 86bf35e2-62a5-497e-9cae-efd35139f81f
|
||||
- name: fileId
|
||||
in: path
|
||||
description: The file ID to be deleted.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
||||
Reference in New Issue
Block a user