Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
220 lines
5.9 KiB
TypeScript
220 lines
5.9 KiB
TypeScript
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
|
|
import { Emitter } from '@rocket.chat/emitter';
|
|
import { Random } from '@rocket.chat/random';
|
|
import fileSize from 'filesize';
|
|
|
|
import { getErrorMessage } from '../errorHandling';
|
|
import type { UploadsAPI, EncryptedFileUploadContent } from './ChatAPI';
|
|
import { isEncryptedUpload, type Upload } from './Upload';
|
|
import { i18n } from '../../../app/utils/lib/i18n';
|
|
import { sdk } from '../SDKClient';
|
|
import { USER_ACTIVITIES, UserAction } from '../UserAction';
|
|
import { settings } from '../settings';
|
|
import { fileUploadIsValidContentType } from '../utils/restrictions';
|
|
|
|
class UploadsStore extends Emitter<{ update: void; [x: `cancelling-${Upload['id']}`]: void }> implements UploadsAPI {
|
|
private rid: string;
|
|
|
|
private tmid?: string;
|
|
|
|
constructor({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }) {
|
|
super();
|
|
this.rid = rid;
|
|
this.tmid = tmid;
|
|
}
|
|
|
|
private uploads: readonly Upload[] = [];
|
|
|
|
private processingUploads: boolean = false;
|
|
|
|
set = (uploads: Upload[]): void => {
|
|
this.uploads = uploads;
|
|
this.emit('update');
|
|
};
|
|
|
|
get = (): readonly Upload[] => this.uploads;
|
|
|
|
subscribe = (callback: () => void): (() => void) => this.on('update', callback);
|
|
|
|
setProcessingUploads = (processing: boolean): void => {
|
|
this.processingUploads = processing;
|
|
this.emit('update');
|
|
};
|
|
|
|
getProcessingUploads = (): boolean => this.processingUploads;
|
|
|
|
cancel = (id: Upload['id']): void => {
|
|
this.emit(`cancelling-${id}`);
|
|
};
|
|
|
|
wipeFailedOnes = (): void => {
|
|
this.set(this.uploads.filter((upload) => !upload.error));
|
|
};
|
|
|
|
private updateUpload(id: Upload['id'], patch: Partial<Upload>): void {
|
|
this.set(this.uploads.map((upload) => (upload.id !== id ? upload : { ...upload, ...patch })));
|
|
}
|
|
|
|
removeUpload = (id: Upload['id']): void => {
|
|
this.set(this.uploads.filter((upload) => upload.id !== id));
|
|
|
|
if (this.uploads.length === 0) {
|
|
UserAction.stop(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid });
|
|
}
|
|
};
|
|
|
|
editUploadAltText = (uploadId: Upload['id'], altText: string) => {
|
|
this.set(
|
|
this.uploads.map((upload) => {
|
|
if (upload.id !== uploadId) {
|
|
return upload;
|
|
}
|
|
|
|
return {
|
|
...upload,
|
|
altText,
|
|
...(isEncryptedUpload(upload) && {
|
|
metadataForEncryption: { ...upload.metadataForEncryption, altText },
|
|
}),
|
|
};
|
|
}),
|
|
);
|
|
};
|
|
|
|
editUploadFileName = (uploadId: Upload['id'], fileName: Upload['file']['name']) => {
|
|
try {
|
|
this.set(
|
|
this.uploads.map((upload) => {
|
|
if (upload.id !== uploadId) {
|
|
return upload;
|
|
}
|
|
|
|
return {
|
|
...upload,
|
|
file: new File([upload.file], fileName, upload.file),
|
|
...(isEncryptedUpload(upload) && {
|
|
metadataForEncryption: { ...upload.metadataForEncryption, name: fileName },
|
|
}),
|
|
};
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
this.set(
|
|
this.uploads.map((upload) => {
|
|
if (upload.id !== uploadId) {
|
|
return upload;
|
|
}
|
|
|
|
return {
|
|
...upload,
|
|
percentage: 0,
|
|
error: new Error(i18n.t('FileUpload_Update_Failed')),
|
|
};
|
|
}),
|
|
);
|
|
}
|
|
};
|
|
|
|
clear = () => {
|
|
this.set([]);
|
|
UserAction.stop(this.rid, USER_ACTIVITIES.USER_UPLOADING, { tmid: this.tmid });
|
|
};
|
|
|
|
async send(file: File, encrypted?: EncryptedFileUploadContent): Promise<void> {
|
|
const maxFileSize = settings.peek('FileUpload_MaxFileSize');
|
|
const invalidContentType = !fileUploadIsValidContentType(encrypted ? encrypted.rawFile.type : file.type);
|
|
const id = Random.id();
|
|
|
|
this.set([
|
|
...this.uploads,
|
|
{
|
|
id,
|
|
file: encrypted ? encrypted.rawFile : file,
|
|
percentage: 0,
|
|
...(encrypted && {
|
|
encryptedFile: encrypted.encryptedFile,
|
|
metadataForEncryption: encrypted.fileContent.raw,
|
|
}),
|
|
},
|
|
]);
|
|
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
if (file.size === 0) {
|
|
return reject(new Error(i18n.t('FileUpload_File_Empty')));
|
|
}
|
|
|
|
// -1 maxFileSize means there is no limit
|
|
if (maxFileSize > -1 && (file.size || 0) > maxFileSize) {
|
|
return reject(new Error(i18n.t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) })));
|
|
}
|
|
|
|
if (invalidContentType) {
|
|
return reject(new Error(i18n.t('FileUpload_MediaType_NotAccepted__type__', { type: file.type })));
|
|
}
|
|
|
|
const xhr = sdk.rest.upload(
|
|
`/v1/rooms.media/${this.rid}`,
|
|
{
|
|
file,
|
|
...(encrypted && {
|
|
content: JSON.stringify(encrypted.fileContent.encrypted),
|
|
}),
|
|
},
|
|
{
|
|
load: (event) => {
|
|
resolve(event);
|
|
},
|
|
progress: (event) => {
|
|
if (!event.lengthComputable) {
|
|
return;
|
|
}
|
|
const progress = (event.loaded / event.total) * 100;
|
|
this.updateUpload(id, { percentage: Math.min(Math.round(progress), 99) || 0 });
|
|
},
|
|
error: (event) => {
|
|
this.updateUpload(id, { percentage: 0, error: new Error(xhr.responseText) });
|
|
reject(event);
|
|
},
|
|
},
|
|
);
|
|
|
|
xhr.onload = () => {
|
|
try {
|
|
if (xhr.readyState !== xhr.DONE) {
|
|
return;
|
|
}
|
|
|
|
if (xhr.status === 400) {
|
|
const error = JSON.parse(xhr.responseText);
|
|
this.updateUpload(id, { percentage: 0, error: new Error(error.error) });
|
|
return;
|
|
}
|
|
|
|
if (xhr.status === 200) {
|
|
const result = JSON.parse(xhr.responseText);
|
|
this.updateUpload(id, { id: result.file._id, url: result.file.url, percentage: 100 });
|
|
return;
|
|
}
|
|
|
|
this.updateUpload(id, { percentage: 0, error: new Error(i18n.t('FileUpload_Error')) });
|
|
} catch (error) {
|
|
this.updateUpload(id, { percentage: 0, error: new Error(getErrorMessage(error)) });
|
|
}
|
|
};
|
|
|
|
this.once(`cancelling-${id}`, () => {
|
|
xhr.abort();
|
|
this.removeUpload(id);
|
|
reject(new Error(i18n.t('FileUpload_Canceled')));
|
|
});
|
|
});
|
|
} catch (error: unknown) {
|
|
this.updateUpload(id, { percentage: 0, error: new Error(getErrorMessage(error)) });
|
|
}
|
|
}
|
|
}
|
|
|
|
export const createUploadsAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): UploadsAPI =>
|
|
new UploadsStore({ rid, tmid });
|