snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
229
apps/meteor/server/services/upload/service.ts
Normal file
229
apps/meteor/server/services/upload/service.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import fs from 'node:fs';
|
||||
import type Stream from 'node:stream';
|
||||
|
||||
import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails';
|
||||
import { api, ServiceClassInternal } from '@rocket.chat/core-services';
|
||||
import type { ISendFileLivechatMessageParams, ISendFileMessageParams, IUploadFileParams, IUploadService } from '@rocket.chat/core-services';
|
||||
import type { IUpload, IUser, FilesAndAttachments, IMessage, AtLeast } from '@rocket.chat/core-typings';
|
||||
import { isFileAttachment } from '@rocket.chat/core-typings';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { Uploads, Users } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { NOTIFICATION_ATTACHMENT_COLOR } from '../../../lib/constants';
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { canDeleteMessageAsync } from '../../lib/authorization/canDeleteMessage';
|
||||
import { i18n } from '../../lib/i18n';
|
||||
import { FileUpload } from '../../lib/media/file-upload';
|
||||
import { updateMessage } from '../../lib/messages/updateMessage';
|
||||
import { setUserAvatar } from '../../lib/users/setUserAvatar';
|
||||
import { parseFileIntoMessageAttachments, sendFileMessage } from '../../meteor-methods/messages/sendFileMessage';
|
||||
import { sendFileLivechatMessage } from '../../meteor-methods/omnichannel/sendFileLivechatMessage';
|
||||
import { UploadFS } from '../../ufs';
|
||||
|
||||
const logger = new Logger('UploadService');
|
||||
|
||||
export class UploadService extends ServiceClassInternal implements IUploadService {
|
||||
protected name = 'upload';
|
||||
|
||||
async uploadFile({ buffer, details, federation }: IUploadFileParams): Promise<IUpload> {
|
||||
const fileStore = FileUpload.getStore('Uploads');
|
||||
return fileStore.insert({ ...details, ...(federation && { federation }) }, buffer);
|
||||
}
|
||||
|
||||
async sendFileMessage({ roomId, file, userId, message }: ISendFileMessageParams): Promise<boolean | undefined> {
|
||||
return sendFileMessage(userId, { roomId, file, msgData: message });
|
||||
}
|
||||
|
||||
async sendFileLivechatMessage({ roomId, visitorToken, file, message }: ISendFileLivechatMessageParams): Promise<boolean> {
|
||||
return sendFileLivechatMessage({ roomId, visitorToken, file, msgData: message });
|
||||
}
|
||||
|
||||
async getFileBuffer({ file }: { file: IUpload }): Promise<Buffer> {
|
||||
const buffer = await FileUpload.getBuffer(file);
|
||||
|
||||
if (!(buffer instanceof Buffer)) {
|
||||
throw new Error('Unknown error');
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async extractMetadata(file: IUpload): Promise<{ height?: number; width?: number; format?: string }> {
|
||||
return FileUpload.extractMetadata(file);
|
||||
}
|
||||
|
||||
async parseFileIntoMessageAttachments(file: Partial<IUpload>, roomId: string, user: IUser): Promise<FilesAndAttachments> {
|
||||
return parseFileIntoMessageAttachments(file, roomId, user);
|
||||
}
|
||||
|
||||
async canDeleteFile(
|
||||
user: Pick<IUser, '_id' | 'username'>,
|
||||
file: Pick<IUpload, '_id' | 'userId' | 'rid' | 'expiresAt' | 'uploadedAt'>,
|
||||
msg: IMessage | null,
|
||||
): Promise<boolean> {
|
||||
if (msg) {
|
||||
return canDeleteMessageAsync(user, msg);
|
||||
}
|
||||
|
||||
if (!file.userId || !file.rid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If file is not confirmed and was sent by the same user
|
||||
if (file.expiresAt && file.userId === user._id) {
|
||||
return canAccessRoomIdAsync(file.rid, user._id);
|
||||
}
|
||||
|
||||
// It's a confirmed file but it has no message, so use data from the file to run message delete permission checks
|
||||
const msgForValidation = { u: { _id: file.userId }, ts: file.uploadedAt, rid: file.rid };
|
||||
return canDeleteMessageAsync(user, msgForValidation);
|
||||
}
|
||||
|
||||
async deleteFile(user: IUser, fileId: IUpload['_id'], msg: IMessage | null): Promise<{ deletedFiles: IUpload['_id'][] }> {
|
||||
// Find every file that is derived from the file that is being deleted (its thumbnails)
|
||||
const additionalFiles = await Uploads.findAllByOriginalFileId(fileId, { projection: { _id: 1 } })
|
||||
.map(({ _id }) => _id)
|
||||
.toArray();
|
||||
const allFiles = [fileId, ...additionalFiles];
|
||||
|
||||
if (msg) {
|
||||
await this.updateMessageRemovingFiles(msg, allFiles, user);
|
||||
}
|
||||
|
||||
return this.removeFileAndDerivates(fileId, additionalFiles);
|
||||
}
|
||||
|
||||
private async removeFileAndDerivates(
|
||||
fileId: IUpload['_id'],
|
||||
additionalFiles: IUpload['_id'][],
|
||||
): Promise<{ deletedFiles: IUpload['_id'][] }> {
|
||||
const store = FileUpload.getStore('Uploads');
|
||||
// Delete the main file first;
|
||||
await store.deleteById(fileId);
|
||||
|
||||
// The main file is already deleted; From here forward we'll return a success response even if some sub-process fails
|
||||
const deletedFiles: IUpload['_id'][] = [fileId];
|
||||
// Delete them one by one as the store may include requests to external services
|
||||
for await (const id of additionalFiles) {
|
||||
try {
|
||||
await store.deleteById(id);
|
||||
deletedFiles.push(id);
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Failed to delete derived file', fileId: id, originalFileId: fileId, err });
|
||||
}
|
||||
}
|
||||
|
||||
return { deletedFiles };
|
||||
}
|
||||
|
||||
private async updateMessageRemovingFiles(msg: IMessage, filesToRemove: IUpload['_id'][], user: IUser): Promise<void> {
|
||||
const text = `_${i18n.t('File_removed')}_`;
|
||||
const color = NOTIFICATION_ATTACHMENT_COLOR;
|
||||
|
||||
const newFiles = msg.files?.filter((file) => !filesToRemove.includes(file._id));
|
||||
const newAttachments = msg.attachments?.map((attachment) => {
|
||||
if (!isFileAttachment(attachment)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
// If the attachment doesn't have a `fileId`, we assume it's an old message with only one file, in which case checking the id is not needed
|
||||
if (attachment.fileId && !filesToRemove.includes(attachment.fileId)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
return { type: 'removed-file', color, text, ...(attachment.fileId && { fileId: attachment.fileId }) };
|
||||
});
|
||||
const newFile = msg.file?._id && !filesToRemove.includes(msg.file._id) ? msg.file : newFiles?.[0];
|
||||
|
||||
const editedMessage = {
|
||||
...msg,
|
||||
files: newFiles,
|
||||
attachments: newAttachments,
|
||||
file: newFile,
|
||||
};
|
||||
|
||||
await updateMessage(editedMessage, user, msg);
|
||||
}
|
||||
|
||||
async streamUploadedFile({
|
||||
file,
|
||||
imageResizeOpts,
|
||||
}: {
|
||||
file: IUpload;
|
||||
imageResizeOpts?: { width: number; height: number };
|
||||
}): Promise<Stream.Readable> {
|
||||
const stream = await FileUpload.getStore('Uploads')._store.getReadStream(file._id, file);
|
||||
if (!stream) {
|
||||
throw new Error('error-file-not-found');
|
||||
}
|
||||
|
||||
if (file?.type?.includes('image') && imageResizeOpts) {
|
||||
const { width, height } = imageResizeOpts;
|
||||
|
||||
const transformer = sharp().resize({ width, height, fit: 'contain' });
|
||||
|
||||
stream.on('error', (err) => transformer.destroy(err));
|
||||
|
||||
return stream.pipe(transformer);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
async uploadFileFromStream({
|
||||
streamParam,
|
||||
details,
|
||||
}: {
|
||||
streamParam: Stream.Readable;
|
||||
details: Omit<IUploadDetails, 'size'>;
|
||||
}): Promise<IUpload> {
|
||||
const resolver = Promise.withResolvers<IUpload>();
|
||||
|
||||
const tempFilePath = UploadFS.getTempFilePath(Random.id());
|
||||
|
||||
const writeStream = fs.createWriteStream(tempFilePath);
|
||||
streamParam.pipe(writeStream);
|
||||
|
||||
const cleanup = (err: unknown) => {
|
||||
fs.promises.unlink(tempFilePath).catch(() => undefined);
|
||||
resolver.reject(err);
|
||||
};
|
||||
|
||||
writeStream.on('finish', async () => {
|
||||
FileUpload.getStore('Uploads')
|
||||
.insert(
|
||||
{
|
||||
...details,
|
||||
size: writeStream.bytesWritten,
|
||||
},
|
||||
tempFilePath,
|
||||
)
|
||||
.then(resolver.resolve)
|
||||
.catch(cleanup);
|
||||
});
|
||||
|
||||
streamParam.on('error', async (err) => {
|
||||
writeStream.destroy();
|
||||
cleanup(err);
|
||||
});
|
||||
|
||||
writeStream.on('error', cleanup);
|
||||
|
||||
return resolver.promise;
|
||||
}
|
||||
|
||||
async setUserAvatar(user: Pick<IUser, '_id' | 'username'>, buffer: Buffer, contentType: string, service: 'rest'): Promise<void> {
|
||||
return setUserAvatar(user, buffer, contentType, service);
|
||||
}
|
||||
|
||||
async resetUserAvatar(user: AtLeast<IUser, '_id' | 'username'>): Promise<void> {
|
||||
if (!user.username) {
|
||||
throw new Error('Username is required to reset avatar');
|
||||
}
|
||||
|
||||
await FileUpload.getStore('Avatars').deleteByName(user.username);
|
||||
await Users.unsetAvatarData(user._id);
|
||||
void api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: undefined });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user