You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

middleware.web.ts 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { v4 as uuidv4 } from 'uuid';
  2. import { getCurrentConference } from '../base/conference/functions';
  3. import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
  4. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  5. import { showErrorNotification } from '../notifications/actions';
  6. import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
  7. import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES } from './actionTypes';
  8. import { addFile, removeFile, updateFileProgress } from './actions';
  9. import { getFileExtension } from './functions.any';
  10. import logger from './logger';
  11. import { FILE_SHARING_PREFIX } from './constants';
  12. import { IFileMetadata } from './types';
  13. /**
  14. * Middleware that handles file sharing actions.
  15. *
  16. * @param {Store} store - The redux store.
  17. * @returns {Function}
  18. */
  19. MiddlewareRegistry.register(store => next => action => {
  20. switch (action.type) {
  21. case UPLOAD_FILES: {
  22. const state = store.getState();
  23. const conference = getCurrentConference(state);
  24. const sessionId = conference?.getMeetingUniqueId();
  25. const localParticipant = getLocalParticipant(state);
  26. const { fileSharing } = state['features/base/config'];
  27. const { connection } = state['features/base/connection'];
  28. const { jwt } = state['features/base/jwt'];
  29. const roomJid = conference?.room?.roomjid;
  30. for (const file of action.files) {
  31. const jid = connection!.getJid();
  32. const fileId = uuidv4();
  33. const fileMetadata: IFileMetadata = {
  34. authorParticipantId: localParticipant!.id,
  35. authorParticipantJid: jid,
  36. authorParticipantName: getParticipantDisplayName(state, localParticipant!.id),
  37. conferenceFullName: roomJid ?? '',
  38. fileId,
  39. fileName: file.name,
  40. fileSize: file.size,
  41. fileType: getFileExtension(file.name),
  42. timestamp: Date.now()
  43. };
  44. store.dispatch(addFile(fileMetadata));
  45. store.dispatch(updateFileProgress(fileId, 1));
  46. // Upload file.
  47. const formData = new FormData();
  48. formData.append('metadata', JSON.stringify(fileMetadata));
  49. // @ts-ignore
  50. formData.append('file', file as Blob, file.name);
  51. // Use XMLHttpRequest to track upload
  52. const xhr = new XMLHttpRequest();
  53. const handleError = () => {
  54. logger.warn('Could not upload file:', xhr.statusText);
  55. store.dispatch(removeFile(fileId));
  56. store.dispatch(showErrorNotification({
  57. titleKey: 'fileSharing.uploadFailedTitle',
  58. descriptionKey: 'fileSharing.uploadFailedDescription',
  59. appearance: NOTIFICATION_TYPE.ERROR
  60. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  61. };
  62. xhr.open('POST', `${fileSharing!.apiUrl!}/sessions/${sessionId}/files`);
  63. xhr.responseType = 'json';
  64. if (jwt) {
  65. xhr.setRequestHeader('Authorization', `Bearer ${jwt}`);
  66. }
  67. xhr.upload.onprogress = event => {
  68. if (event.lengthComputable) {
  69. // We use 99% as the max value to avoid showing 100% before the
  70. // upload is actually finished, that is, when the request is completed.
  71. const percent = Math.max((event.loaded / event.total) * 100, 99);
  72. store.dispatch(updateFileProgress(fileId, percent));
  73. }
  74. };
  75. xhr.onload = () => {
  76. if (xhr.status >= 200 && xhr.status < 300) {
  77. store.dispatch(updateFileProgress(fileId, 100));
  78. const metadataHandler = conference?.getMetadataHandler();
  79. metadataHandler?.setMetadata(`${FILE_SHARING_PREFIX}.${fileId}`, fileMetadata);
  80. } else {
  81. handleError();
  82. }
  83. };
  84. xhr.onerror = handleError;
  85. xhr.send(formData);
  86. }
  87. return next(action);
  88. }
  89. case REMOVE_FILE: {
  90. const state = store.getState();
  91. const conference = getCurrentConference(state);
  92. const { fileSharing } = state['features/base/config'];
  93. const { jwt } = state['features/base/jwt'];
  94. const sessionId = conference?.getMeetingUniqueId();
  95. let doDelete = false;
  96. // First remove the file metadata so others won't attempt to download it anymore.
  97. const metadataHandler = conference?.getMetadataHandler();
  98. if (metadataHandler) {
  99. const metadataId = `${FILE_SHARING_PREFIX}.${action.fileId}`;
  100. const existingMetadata = metadataHandler.getMetadata()[metadataId] ?? {};
  101. doDelete = (existingMetadata?.process ?? 100) === 100;
  102. metadataHandler.setMetadata(metadataId, {});
  103. }
  104. if (!doDelete) {
  105. return next(action);
  106. }
  107. // Now delete it from the server.
  108. fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${action.fileId}`, {
  109. method: 'DELETE',
  110. headers: {
  111. ...jwt && { 'Authorization': `Bearer ${jwt}` }
  112. }
  113. })
  114. .then(response => {
  115. if (!response.ok) {
  116. throw new Error(`Failed to delete file: ${response.statusText}`);
  117. }
  118. })
  119. .catch(error => {
  120. logger.warn('Could not delete file:', error);
  121. });
  122. return next(action);
  123. }
  124. case DOWNLOAD_FILE: {
  125. const state = store.getState();
  126. const { fileSharing } = state['features/base/config'];
  127. const conference = getCurrentConference(state);
  128. const sessionId = conference?.getMeetingUniqueId();
  129. fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/document`, {
  130. method: 'GET',
  131. headers: {
  132. 'X-File-Id': action.fileId,
  133. }
  134. })
  135. .then(response => response.json())
  136. .then(data => {
  137. const url = data.presignedUrl;
  138. if (!url) {
  139. throw new Error('No presigned URL found in the response.');
  140. }
  141. window.open(url, '_blank', 'noreferrer,noopener');
  142. })
  143. .catch(error => {
  144. logger.warn('Could not download file:', error);
  145. store.dispatch(showErrorNotification({
  146. titleKey: 'fileSharing.downloadFailedTitle',
  147. descriptionKey: 'fileSharing.downloadFailedDescription',
  148. appearance: NOTIFICATION_TYPE.ERROR
  149. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  150. });
  151. return next(action);
  152. }
  153. }
  154. return next(action);
  155. });