Files
upstream-rocketchat/apps/meteor/client/lib/chats/uploads.ts
upstream-mirror 30810eecd7
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
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

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