Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
7d79f4f
fix: pruning does not get rid of physical files
cardoso Apr 29, 2025
3cbc520
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso Apr 29, 2025
4bfed4f
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso Apr 30, 2025
f84bee3
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 2, 2025
33e7d5f
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 2, 2025
bcaf170
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 2, 2025
b0bf299
test: add e2e tests for message pruning functionality
cardoso May 2, 2025
342d75b
test: update prune messages e2e test to ensure proper message deletion
cardoso May 2, 2025
2e9d10e
test: update prune messages test to use role-based selector for menu …
cardoso May 2, 2025
c1def42
test: remove unnecessary media type blacklist setting from prune mess…
cardoso May 2, 2025
15b0bd5
refactor: enhance file deletion logic and improve prune messages tests
cardoso May 5, 2025
f9d96f0
test: check file download and channel deletion
cardoso May 5, 2025
254ecb6
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 5, 2025
9755866
refactor: improve test descriptions for prune messages with attached …
cardoso May 5, 2025
2ee3ea2
refactor: enhance test cases for pruning messages with attached files
cardoso May 6, 2025
0eabc6e
refactor: speed up prune-messages test
cardoso May 6, 2025
82c06ef
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 6, 2025
097a228
refactor: update logging levels in cleanRoomHistory and enhance prune…
cardoso May 6, 2025
745103a
test: enhance download validation in prune-messages tests
cardoso May 6, 2025
b7b8761
refactor: improve condition check for failed file deletions in cleanR…
cardoso May 6, 2025
4e45d05
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 6, 2025
4ee25d5
refactor: enhance HomeChannel and prune-messages tests with improved …
cardoso May 7, 2025
5a12109
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 7, 2025
9a1f27c
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 7, 2025
6a0e86f
test(fixme): delete channel with dangling files
cardoso May 7, 2025
13cbd21
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 7, 2025
f53fd29
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 8, 2025
304de7e
test(fixme): message deletion with dangling
cardoso May 8, 2025
b0c39ec
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 8, 2025
6afcc07
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 9, 2025
b285b03
fix: improves file deletion and prune messages
cardoso May 9, 2025
f105e5b
chore: simplifies file deletion logic
cardoso May 12, 2025
1f07439
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 12, 2025
4a1befe
fix: improves file deletion handling in prune history
cardoso May 12, 2025
6f95786
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
cardoso May 14, 2025
895d84b
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 15, 2025
2025e87
revert: room history cleaning API endpoint
cardoso May 15, 2025
9a1d8ed
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
cardoso May 16, 2025
622a93c
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
cardoso May 16, 2025
8213241
chore: apply changes from develop
cardoso May 16, 2025
99da0b1
test: add e2e test for clean history
cardoso May 16, 2025
4eb619c
test: simplify clean-history spec
cardoso May 16, 2025
326c828
fix: file deletion issues in prune messages
cardoso May 16, 2025
e8158b3
refactor: move ENOENT handling to ufs-local
cardoso May 17, 2025
acf5d31
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
cardoso May 17, 2025
0797ca4
Create young-ants-applaud.md
cardoso May 17, 2025
da275f0
test: ufs-local
cardoso May 19, 2025
bdd8b72
test: use unmocked config
cardoso May 19, 2025
41b1d9c
Merge branch 'develop' into SUP-745-fix-pruning-ufs-local
kodiakhq[bot] May 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/young-ants-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes file deletion being called twice and ignoring errors with FileSystem storage type.
1 change: 1 addition & 0 deletions apps/meteor/.mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
'lib/callbacks.spec.ts',
'server/lib/ldap/*.spec.ts',
'server/lib/ldap/**/*.spec.ts',
'server/ufs/*.spec.ts',
'ee/server/lib/ldap/*.spec.ts',
'ee/tests/**/*.tests.ts',
'ee/tests/**/*.spec.ts',
Expand Down
115 changes: 115 additions & 0 deletions apps/meteor/server/ufs/ufs-local.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import fs from 'fs';

import { expect } from 'chai';
import { before, beforeEach, afterEach, describe, it } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import type { SinonStub } from 'sinon';

// import { Config } from './ufs-config';
// import { UploadFS } from './ufs';
import type { LocalStore as LocalStoreClass } from './ufs-local';

const fakeCollection = {
removeById: sinon.stub(),
findOne: sinon.stub(),
};

// Proxyquire chain to mock all Meteor dependencies
const ufsFilterMock = proxyquire.noCallThru().load('./ufs-filter', {
'meteor/meteor': {},
'meteor/check': {},
'meteor/mongo': {},
'meteor/npm-mongo': {},
});

const { UploadFS } = proxyquire.noCallThru().load('./ufs', {
'meteor/meteor': {},
'meteor/check': {},
'meteor/mongo': {},
'meteor/npm-mongo': {},
'./ufs-filter': ufsFilterMock,
'./ufs-store': {
Store: Object,
},
});

// Create the UploadFS mock object as a callable function with properties
// function UploadFS() {
// return UploadFS;
// }
// Object.assign(UploadFS, {
// store: {},
// config: new Config(),
// addStore: sinon.stub(),
// getStore: sinon.stub().returns(undefined),
// getTempFilePath: sinon.stub().returns('/tmp/mockfile'),
// });
// The module must export { UploadFS: ... }
const ufsMock = { UploadFS };
const ufsMockModule = { UploadFS };
const ufsIndexMock = { UploadFS };

const ufsStoreMockRaw = proxyquire.noCallThru().load('./ufs-store', {
'meteor/meteor': {},
'meteor/check': {},
'meteor/mongo': {},
'meteor/npm-mongo': {},
'./ufs': ufsMockModule,
'./ufs-filter': ufsFilterMock,
'./index': ufsIndexMock,
});
// Patch ufsStoreMock to export all possible ways (default, named, etc)
const ufsStoreMock = Object.assign({}, ufsStoreMockRaw, { default: ufsStoreMockRaw, ufsStoreMock: ufsStoreMockRaw });
const localStoreProxy = proxyquire.noCallThru().load('./ufs-local', {
// 'mkdirp': sinon.stub(),
'meteor/meteor': {},
'meteor/check': {},
'meteor/mongo': {},
'meteor/npm-mongo': {},
'./ufs': ufsMock,
'./ufs-store': ufsStoreMock,
'./index': ufsIndexMock,
});

const { LocalStore } = localStoreProxy as {
LocalStore: typeof LocalStoreClass;
};

describe('LocalStore', () => {
let store: LocalStoreClass;
let unlinkStub: SinonStub;

before(() => {
fakeCollection.removeById.resolves();
fakeCollection.findOne.resolves({ _id: 'test', name: 'file.txt' });
store = new LocalStore({ name: 'test', collection: fakeCollection as any, path: '/tmp/ufs-local' });
});

beforeEach(() => {
unlinkStub = sinon.stub(fs.promises, 'unlink').resolves();
fakeCollection.removeById.resetHistory();
fakeCollection.findOne.resetHistory();
});

afterEach(() => {
unlinkStub.restore();
});

it('should not throw if file does not exist (ENOENT)', async () => {
unlinkStub.rejects(Object.assign(new Error('not found'), { code: 'ENOENT' }));
await expect(store.delete('test')).to.be.fulfilled;
// Should only call unlink once
expect(unlinkStub.calledOnce).to.be.true;
});

it('should throw if unlink fails with non-ENOENT error', async () => {
unlinkStub.rejects(Object.assign(new Error('fail'), { code: 'EACCES' }));
await expect(store.delete('test')).to.be.rejectedWith('fail');
});

it('should call findOne to get file info', async () => {
await store.delete('test');
expect(fakeCollection.findOne.calledWith('test')).to.be.true;
});
});
17 changes: 7 additions & 10 deletions apps/meteor/server/ufs/ufs-local.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'fs';
import { stat, unlink } from 'fs/promises';
import { unlink } from 'fs/promises';
import { isNativeError } from 'util/types';

import type { IUpload } from '@rocket.chat/core-typings';
import mkdirp from 'mkdirp';
Expand Down Expand Up @@ -68,18 +69,14 @@ export class LocalStore extends Store {

this.delete = async (fileId, options) => {
const path = await this.getFilePath(fileId);

try {
if (!(await stat(path)).isFile()) {
return;
await unlink(path);
} catch (error) {
if (!isNativeError(error) || !('code' in error) || !(error.code === 'ENOENT')) {
throw error;
}
} catch (_e) {
// FIXME(user) don't ignore, rather this block shouldn't run twice like it does now
return;
}

await unlink(path);
await this.removeById(fileId, { session: options?.session });
await this.removeById(fileId, { session: options?.session }, true);
};

this.getReadStream = async (fileId: string, file: IUpload, options?: { start?: number; end?: number }) => {
Expand Down
6 changes: 4 additions & 2 deletions apps/meteor/server/ufs/ufs-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,11 @@ export class Store {
};
}

async removeById(fileId: string, options?: { session?: ClientSession }) {
async removeById(fileId: string, options?: { session?: ClientSession }, isDeleted = false): Promise<void> {
// Delete the physical file in the store
await this.delete(fileId);
if (!isDeleted) {
await this.delete(fileId);
}

const tmpFile = UploadFS.getTempFilePath(fileId);

Expand Down
Loading