diff --git a/app/api/migrations/migrations/36-populate-mimetype-on-attachments/index.js b/app/api/migrations/migrations/36-populate-mimetype-on-attachments/index.js new file mode 100644 index 0000000000..db2e54dd8a --- /dev/null +++ b/app/api/migrations/migrations/36-populate-mimetype-on-attachments/index.js @@ -0,0 +1,38 @@ +import request from 'shared/JSONRequest'; +import { attachmentsPath } from 'api/files/filesystem'; +import mime from 'mime-types'; + +export default { + delta: 36, + + name: 'populate-mimetype-to-attachment', + + description: 'Populates mimetype of an attachment from a url', + + async up(db) { + process.stdout.write(`${this.name}...\r\n`); + const cursor = await db.collection('files').find({}); + + // eslint-disable-next-line no-await-in-loop + while (await cursor.hasNext()) { + // eslint-disable-next-line no-await-in-loop + const file = await cursor.next(); + if (file.url && !file.mimetype) { + // eslint-disable-next-line no-await-in-loop + const response = await request.head(file.url); + const mimetype = response.headers.get('content-type') || undefined; + // eslint-disable-next-line no-await-in-loop + await this.updateFile(db, file, mimetype); + } else if (file.filename && file.type === 'attachment' && !file.mimetype) { + const mimetype = mime.lookup(attachmentsPath(file.filename)) || undefined; + // eslint-disable-next-line no-await-in-loop + await this.updateFile(db, file, mimetype); + } + } + }, + async updateFile(db, file, mimetype) { + if (mimetype) { + await db.collection('files').updateOne({ _id: file._id }, { $set: { mimetype } }); + } + }, +}; diff --git a/app/api/migrations/migrations/36-populate-mimetype-on-attachments/specs/36-populate-mimetype-on-attachments.spec.js b/app/api/migrations/migrations/36-populate-mimetype-on-attachments/specs/36-populate-mimetype-on-attachments.spec.js new file mode 100644 index 0000000000..a9c9cb97ca --- /dev/null +++ b/app/api/migrations/migrations/36-populate-mimetype-on-attachments/specs/36-populate-mimetype-on-attachments.spec.js @@ -0,0 +1,165 @@ +import testingDB from 'api/utils/testing_db'; +import request from 'shared/JSONRequest'; +import * as attachmentMethods from 'api/files/filesystem'; +import mime from 'mime-types'; +import migration from '../index.js'; + +describe('migration populate-mimetype-on-attachments', () => { + let headRequestMock; + let attachmentPathMock; + let mimeMock; + + beforeEach(async () => { + spyOn(process.stdout, 'write'); + headRequestMock = spyOn(request, 'head'); + attachmentPathMock = spyOn(attachmentMethods, 'attachmentsPath'); + mimeMock = spyOn(mime, 'lookup'); + }); + + afterAll(async () => { + headRequestMock.mockRestore(); + attachmentPathMock.mockRestore(); + mimeMock.mockRestore(); + await testingDB.disconnect(); + }); + + it('should have a delta number', () => { + expect(migration.delta).toBe(36); + }); + + it('should populate mimetype with Content-Type', async () => { + const fixtures = { + files: [{ url: 'some/file/path.jpg' }, { url: 'some/other/path.jpg' }], + }; + await testingDB.clearAllAndLoad(fixtures); + const headers = { + get: jest + .fn() + .mockReturnValueOnce('application/pdf') + .mockReturnValueOnce('mimetype2'), + }; + headRequestMock.and.returnValue( + Promise.resolve({ + headers, + }) + ); + await migration.up(testingDB.mongodb); + expect(request.head).toHaveBeenCalledWith(fixtures.files[0].url); + expect(request.head).toHaveBeenCalledWith(fixtures.files[1].url); + expect(headers.get).toHaveBeenCalledWith('content-type'); + + const files = await testingDB.mongodb + .collection('files') + .find({}) + .toArray(); + + expect(files[0].mimetype).toEqual('application/pdf'); + expect(files[1].mimetype).toEqual('mimetype2'); + }); + + it('should not change the value of mimetype if it already exists in external attachments', async () => { + const fixturesWithMimetype = { + files: [ + { + url: 'some/url/item.jpg', + mimetype: 'application/pdf', + }, + ], + }; + await testingDB.clearAllAndLoad(fixturesWithMimetype); + + const file = await testingDB.mongodb.collection('files').findOne({}); + + expect(file.mimetype).toEqual(fixturesWithMimetype.files[0].mimetype); + }); + + it('should not change if value of mimetype already exists in internal attachments', async () => { + const fixturesWithFilenames = { + files: [ + { + filename: 'somename.pdf', + mimetype: 'application/pdf', + type: 'attachment', + }, + ], + }; + await testingDB.clearAllAndLoad(fixturesWithFilenames); + attachmentPathMock.and.returnValue('/some/path/to/file.pdf'); + mimeMock.and.returnValue('application/pdf'); + await migration.up(testingDB.mongodb); + + const file = await testingDB.mongodb.collection('files').findOne({}); + expect(file.mimetype).toEqual(fixturesWithFilenames.files[0].mimetype); + }); + + it('should update mimetype if filename exists in internal attachments', async () => { + const fixturesWithFilenames = { + files: [ + { + filename: 'somename.pdf', + type: 'attachment', + }, + ], + }; + await testingDB.clearAllAndLoad(fixturesWithFilenames); + mimeMock.and.returnValue('application/pdf'); + attachmentPathMock.and.returnValue('/some/path/to/file.pdf'); + await migration.up(testingDB.mongodb); + + const file = await testingDB.mongodb.collection('files').findOne({}); + expect(file.mimetype).toEqual('application/pdf'); + expect(attachmentMethods.attachmentsPath).toHaveBeenCalledWith( + fixturesWithFilenames.files[0].filename + ); + expect(mime.lookup).toHaveBeenCalledWith('/some/path/to/file.pdf'); + }); + it('should not update mimetype if type is not attachment in internal attachments', async () => { + const fixturesWithFilenames = { + files: [ + { + filename: 'somename.pdf', + type: 'document', + }, + ], + }; + await testingDB.clearAllAndLoad(fixturesWithFilenames); + mimeMock.and.returnValue('application/pdf'); + attachmentPathMock.and.returnValue('/some/path/to/file.pdf'); + await migration.up(testingDB.mongodb); + + expect(attachmentMethods.attachmentsPath).not.toHaveBeenCalledWith( + fixturesWithFilenames.files[0].filename + ); + expect(mime.lookup).not.toHaveBeenCalledWith('/some/path/to/file.pdf'); + }); + it('should not use local attachment if url field is present', async () => { + const mixedFixtures = { + files: [ + { + filename: 'somename.pdf', + type: 'attachment', + url: '/some/url/to/file.something', + }, + ], + }; + await testingDB.clearAllAndLoad(mixedFixtures); + const headers = { + get: jest + .fn() + .mockReturnValueOnce('application/pdf') + .mockReturnValueOnce('mimetype2'), + }; + headRequestMock.and.returnValue( + Promise.resolve({ + headers, + }) + ); + mimeMock.and.returnValue('application/pdf'); + attachmentPathMock.and.returnValue('/some/path/to/file.pdf'); + await migration.up(testingDB.mongodb); + + expect(attachmentMethods.attachmentsPath).not.toHaveBeenCalled(); + expect(mime.lookup).not.toHaveBeenCalled(); + expect(request.head).toHaveBeenCalledWith(mixedFixtures.files[0].url); + }); +}); diff --git a/package.json b/package.json index 2136c743f7..dfae563e7f 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "mark.js": "^8.11.1", "markdown-it": "11.0.0", "markdown-it-container": "3.0.0", + "mime-types": "^2.1.29", "moment": "2.27.0", "moment-timezone": "0.5.31", "mongodb": "^3.6.2", diff --git a/yarn.lock b/yarn.lock index 20b664de43..e466648f41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9428,6 +9428,11 @@ mime-db@1.45.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== +mime-db@1.46.0: + version "1.46.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee" + integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ== + mime-db@~1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" @@ -9439,6 +9444,13 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.45.0" +mime-types@^2.1.29: + version "2.1.29" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2" + integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ== + dependencies: + mime-db "1.46.0" + mime-types@~2.1.17, mime-types@~2.1.18: version "2.1.18" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"