From 560efc71d8e1d2daa33da654d7ae253813aedf8a Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 24 Oct 2020 11:32:23 +0200 Subject: [PATCH 1/8] Use `useStaticAssets` instead of `@nestjs/serve-static` `serve-static` does not work with `createTestingModule` and is not recommended when "just" serving a few images. See https://github.com/nestjs/serve-static/issues/240 Signed-off-by: David Mehren --- package.json | 1 - src/app.module.ts | 5 ----- src/main.ts | 7 ++++++- yarn.lock | 7 ------- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 012173c8d..1bfd9eb58 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "@nestjs/common": "^7.0.0", "@nestjs/core": "^7.0.0", "@nestjs/platform-express": "^7.0.0", - "@nestjs/serve-static": "^2.1.3", "@nestjs/swagger": "^4.5.12", "@nestjs/typeorm": "^7.1.0", "class-transformer": "^0.2.3", diff --git a/src/app.module.ts b/src/app.module.ts index 57a842aa4..240333086 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,11 +22,6 @@ import { UsersModule } from './users/users.module'; autoLoadEntities: true, synchronize: true, }), - ServeStaticModule.forRoot({ - rootPath: join(__dirname, '..'), - // TODO: Get uploads directory from config - renderPath: 'uploads', - }), NotesModule, UsersModule, RevisionsModule, diff --git a/src/main.ts b/src/main.ts index e98b56051..f1cd28194 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,12 @@ import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; import { NestConsoleLoggerService } from './logger/nest-console-logger.service'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule); const logger = await app.resolve(NestConsoleLoggerService); logger.log('Switching logger', 'AppBootstrap'); app.useLogger(logger); @@ -24,6 +25,10 @@ async function bootstrap() { transform: true, }), ); + // TODO: Get uploads directory from config + app.useStaticAssets('uploads', { + prefix: '/uploads', + }); await app.listen(3000); logger.log('Listening on port 3000', 'AppBootstrap'); } diff --git a/yarn.lock b/yarn.lock index 2cdf87f00..b5e7affd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -614,13 +614,6 @@ "@angular-devkit/schematics" "9.1.7" fs-extra "9.0.0" -"@nestjs/serve-static@^2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@nestjs/serve-static/-/serve-static-2.1.3.tgz#bdcb6d3463d193153b334212facc24a9767046e9" - integrity sha512-9xyysggaOdfbABWqhty+hAkauDWv/Q8YKHm4OMXdQbQei5tquFuTjiSx8IFDOZeSOKlA9fjBq/2MXCJRSo23SQ== - dependencies: - path-to-regexp "0.1.7" - "@nestjs/swagger@^4.5.12": version "4.5.12" resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-4.5.12.tgz#e8aa65fbb0033007ece1d494b002f47ff472c20b" From 5f13c34a0705bf7ebcffb6e6c706ef1c5d521db7 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 24 Oct 2020 11:34:16 +0200 Subject: [PATCH 2/8] UsersService: Improve logging in `getNoteByIdOrAlias` Signed-off-by: David Mehren --- src/notes/notes.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index f6d5e4994..b31d5c05f 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -154,10 +154,15 @@ export class NotesService { ], }); if (note === undefined) { + this.logger.debug( + `Could not find note '${noteIdOrAlias}'`, + 'getNoteByIdOrAlias', + ); throw new NotInDBError( `Note with id/alias '${noteIdOrAlias}' not found.`, ); } + this.logger.debug(`Found note '${noteIdOrAlias}'`, 'getNoteByIdOrAlias'); return note; } From 9aa2a64a53b3414b82b6b1f6e6ec9049a7025edb Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 24 Oct 2020 11:34:49 +0200 Subject: [PATCH 3/8] UserEntity: Fix column types for create/update dates Signed-off-by: David Mehren --- src/users/user.entity.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/users/user.entity.ts b/src/users/user.entity.ts index 9b20e6444..206cde1b9 100644 --- a/src/users/user.entity.ts +++ b/src/users/user.entity.ts @@ -1,4 +1,9 @@ -import { Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; import { Column, OneToMany } from 'typeorm/index'; import { Note } from '../notes/note.entity'; import { AuthToken } from './auth-token.entity'; @@ -15,10 +20,10 @@ export class User { @Column() displayName: string; - @Column() + @CreateDateColumn() createdAt: Date; - @Column() + @UpdateDateColumn() updatedAt: Date; @Column({ From 0711dbb6ff72091d8ba0efb8b1f28a964511f214 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 24 Oct 2020 11:49:01 +0200 Subject: [PATCH 4/8] MediaService: Simplify `saveFile` signature As the `saveFile` method only really uses the files `Buffer`, this commit changes the signature so it directly gets a `Buffer` instead of a complicated `MulterFile` object. This also simplifies testing. Signed-off-by: David Mehren --- src/api/public/media/media.controller.ts | 8 +++++--- src/media/media.service.ts | 9 ++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/api/public/media/media.controller.ts b/src/api/public/media/media.controller.ts index 109a2d1c3..410340ece 100644 --- a/src/api/public/media/media.controller.ts +++ b/src/api/public/media/media.controller.ts @@ -19,14 +19,12 @@ import { import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { MediaService } from '../../../media/media.service'; import { MulterFile } from '../../../media/multer-file.interface'; -import { NotesService } from '../../../notes/notes.service'; @Controller('media') export class MediaController { constructor( private readonly logger: ConsoleLoggerService, private mediaService: MediaService, - private notesService: NotesService, ) { this.logger.setContext(MediaController.name); } @@ -44,7 +42,11 @@ export class MediaController { 'uploadImage', ); try { - const url = await this.mediaService.saveFile(file, username, noteId); + const url = await this.mediaService.saveFile( + file.buffer, + username, + noteId, + ); return { link: url, }; diff --git a/src/media/media.service.ts b/src/media/media.service.ts index 92c96ee49..7b59ede3b 100644 --- a/src/media/media.service.ts +++ b/src/media/media.service.ts @@ -10,7 +10,6 @@ import { UsersService } from '../users/users.service'; import { BackendType } from './backends/backend-type.enum'; import { FilesystemBackend } from './backends/filesystem-backend'; import { MediaUpload } from './media-upload.entity'; -import { MulterFile } from './multer-file.interface'; @Injectable() export class MediaService { @@ -44,14 +43,14 @@ export class MediaService { return allowedTypes.includes(mimeType); } - public async saveFile(file: MulterFile, username: string, noteId: string) { + public async saveFile(fileBuffer: Buffer, username: string, noteId: string) { this.logger.debug( - `Saving '${file.originalname}' for note '${noteId}' and user '${username}'`, + `Saving file for note '${noteId}' and user '${username}'`, 'saveFile', ); const note = await this.notesService.getNoteByIdOrAlias(noteId); const user = await this.usersService.getUserByUsername(username); - const fileTypeResult = await FileType.fromBuffer(file.buffer); + const fileTypeResult = await FileType.fromBuffer(fileBuffer); if (!fileTypeResult) { throw new ClientError('Could not detect file type.'); } @@ -68,7 +67,7 @@ export class MediaService { this.logger.debug(`Generated filename: '${mediaUpload.id}'`, 'saveFile'); const backend = this.moduleRef.get(FilesystemBackend); const [url, backendData] = await backend.saveFile( - file.buffer, + fileBuffer, mediaUpload.id, ); mediaUpload.backendData = backendData; From 5030a6d814eddc126cbefc8b83ecf76a4d00826e Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 24 Oct 2020 11:49:19 +0200 Subject: [PATCH 5/8] AppModule: Remove unused imports Signed-off-by: David Mehren --- src/app.module.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 240333086..9eb98ea4d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,7 +1,5 @@ import { Module } from '@nestjs/common'; -import { ServeStaticModule } from '@nestjs/serve-static'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { join } from 'path'; import { PublicApiModule } from './api/public/public-api.module'; import { AuthorsModule } from './authors/authors.module'; import { GroupsModule } from './groups/groups.module'; From 4f3ef414d4b6cc6751f73e2e78b22dc216729f87 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 24 Oct 2020 11:50:45 +0200 Subject: [PATCH 6/8] Add E2E tests for the `/media` route Signed-off-by: David Mehren --- test/public-api/fixtures/test.png | Bin 0 -> 7904 bytes test/public-api/media.e2e-spec.ts | 79 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 test/public-api/fixtures/test.png create mode 100644 test/public-api/media.e2e-spec.ts diff --git a/test/public-api/fixtures/test.png b/test/public-api/fixtures/test.png new file mode 100644 index 0000000000000000000000000000000000000000..5f04fc47282983bc528f8ea8d3c0e8b5c2c6be58 GIT binary patch literal 7904 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 wV2Hd7L { + let app: NestExpressApplication; + let mediaService: MediaService; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + PublicApiModule, + MediaModule, + TypeOrmModule.forRoot({ + type: 'sqlite', + database: './hedgedoc-e2e.sqlite', + autoLoadEntities: true, + dropSchema: true, + synchronize: true, + }), + NotesModule, + PermissionsModule, + GroupsModule, + LoggerModule, + ], + }).compile(); + app = moduleRef.createNestApplication(); + app.useStaticAssets('uploads', { + prefix: '/uploads', + }); + await app.init(); + const logger = await app.resolve(NestConsoleLoggerService); + logger.log('Switching logger', 'AppBootstrap'); + app.useLogger(logger); + const notesService: NotesService = moduleRef.get('NotesService'); + await notesService.createNote('test content', 'test_upload_media'); + const usersService: UsersService = moduleRef.get('UsersService'); + await usersService.createUser('hardcoded', 'Hard Coded'); + mediaService = moduleRef.get('MediaService'); + }); + + it('POST /media', async () => { + const uploadResponse = await request(app.getHttpServer()) + .post('/media') + .attach('file', 'test/public-api/fixtures/test.png') + .set('HedgeDoc-Note', 'test_upload_media') + .expect('Content-Type', /json/) + .expect(201); + const path = uploadResponse.body.link; + const testImage = await fs.readFile('test/public-api/fixtures/test.png'); + const downloadResponse = await request(app.getHttpServer()).get(path); + expect(downloadResponse.body).toEqual(testImage); + }); + + it('DELETE /media/{filename}', async () => { + const testImage = await fs.readFile('test/public-api/fixtures/test.png'); + const url = await mediaService.saveFile( + testImage, + 'hardcoded', + 'test_upload_media', + ); + const filename = url.split('/').pop(); + await request(app.getHttpServer()) + .delete('/media/' + filename) + .expect(200); + }); +}); From d42bc83e3844158c0aba3b6575cd5c5157db9af1 Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 24 Oct 2020 12:28:52 +0200 Subject: [PATCH 7/8] FilesystemBackend: Ensure uploads directory exists Signed-off-by: David Mehren --- src/media/backends/filesystem-backend.ts | 28 ++++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/media/backends/filesystem-backend.ts b/src/media/backends/filesystem-backend.ts index 5069fa969..663d50dfc 100644 --- a/src/media/backends/filesystem-backend.ts +++ b/src/media/backends/filesystem-backend.ts @@ -7,33 +7,43 @@ import { BackendData } from '../media-upload.entity'; @Injectable() export class FilesystemBackend implements MediaBackend { + // TODO: Get uploads directory from config + uploadDirectory = './uploads'; + constructor(private readonly logger: ConsoleLoggerService) { this.logger.setContext(FilesystemBackend.name); } + private getFilePath(fileName: string): string { + return join(this.uploadDirectory, fileName); + } + + private async ensureDirectory() { + try { + await fs.access(this.uploadDirectory); + } catch (e) { + await fs.mkdir(this.uploadDirectory); + } + } + async saveFile( buffer: Buffer, fileName: string, ): Promise<[string, BackendData]> { - const filePath = FilesystemBackend.getFilePath(fileName); + const filePath = this.getFilePath(fileName); this.logger.debug(`Writing file to: ${filePath}`, 'saveFile'); + await this.ensureDirectory(); await fs.writeFile(filePath, buffer, null); return ['/' + filePath, null]; } async deleteFile(fileName: string, _: BackendData): Promise { - return fs.unlink(FilesystemBackend.getFilePath(fileName)); + return fs.unlink(this.getFilePath(fileName)); } getFileURL(fileName: string, _: BackendData): Promise { - const filePath = FilesystemBackend.getFilePath(fileName); + const filePath = this.getFilePath(fileName); // TODO: Add server address to url return Promise.resolve('/' + filePath); } - - private static getFilePath(fileName: string): string { - // TODO: Get uploads directory from config - const uploadDirectory = './uploads'; - return join(uploadDirectory, fileName); - } } From 26554f7168182c38c0cc7586caab5fcba7a73fbf Mon Sep 17 00:00:00 2001 From: David Mehren Date: Sat, 24 Oct 2020 12:30:23 +0200 Subject: [PATCH 8/8] Use unique sqlite file for every E2E test Previously, this lead to locking errors, when multiple test runners accessed the same database and tried to clear it or tried to insert new test data. Signed-off-by: David Mehren --- test/public-api/media.e2e-spec.ts | 2 +- test/public-api/notes.e2e-spec.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/public-api/media.e2e-spec.ts b/test/public-api/media.e2e-spec.ts index a1f2c8357..533175874 100644 --- a/test/public-api/media.e2e-spec.ts +++ b/test/public-api/media.e2e-spec.ts @@ -25,7 +25,7 @@ describe('Notes', () => { MediaModule, TypeOrmModule.forRoot({ type: 'sqlite', - database: './hedgedoc-e2e.sqlite', + database: './hedgedoc-e2e-media.sqlite', autoLoadEntities: true, dropSchema: true, synchronize: true, diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index 09678dbb6..ce2190947 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -23,9 +23,10 @@ describe('Notes', () => { GroupsModule, TypeOrmModule.forRoot({ type: 'sqlite', - database: './hedgedoc-e2e.sqlite', + database: './hedgedoc-e2e-notes.sqlite', autoLoadEntities: true, synchronize: true, + dropSchema: true, }), LoggerModule, ], @@ -34,8 +35,6 @@ describe('Notes', () => { app = moduleRef.createNestApplication(); await app.init(); notesService = moduleRef.get(NotesService); - const noteRepository = moduleRef.get('NoteRepository'); - noteRepository.clear(); }); it(`POST /notes`, async () => {