feat(file-sharing): Uses short term token for operations.

This commit is contained in:
damencho
2025-05-22 18:52:14 -05:00
committed by Дамян Минков
parent c7b6af1df3
commit 6afbff9b36
4 changed files with 129 additions and 105 deletions

View File

@@ -107,6 +107,7 @@ export interface IJitsiConference {
getParticipantCount: Function;
getParticipants: Function;
getRole: Function;
getShortTermCredentials: Function;
getSpeakerStats: () => ISpeakerStats;
getSsrcByTrack: Function;
getTranscriptionStatus: Function;

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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