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/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/app.module.ts b/src/app.module.ts index 57a842aa4..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'; @@ -22,11 +20,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/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); - } } 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; 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; } 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({ diff --git a/test/public-api/fixtures/test.png b/test/public-api/fixtures/test.png new file mode 100644 index 000000000..5f04fc472 Binary files /dev/null and b/test/public-api/fixtures/test.png differ diff --git a/test/public-api/media.e2e-spec.ts b/test/public-api/media.e2e-spec.ts new file mode 100644 index 000000000..533175874 --- /dev/null +++ b/test/public-api/media.e2e-spec.ts @@ -0,0 +1,79 @@ +import { NestExpressApplication } from '@nestjs/platform-express'; +import { Test } from '@nestjs/testing'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { promises as fs } from 'fs'; +import * as request from 'supertest'; +import { PublicApiModule } from '../../src/api/public/public-api.module'; +import { GroupsModule } from '../../src/groups/groups.module'; +import { LoggerModule } from '../../src/logger/logger.module'; +import { NestConsoleLoggerService } from '../../src/logger/nest-console-logger.service'; +import { MediaModule } from '../../src/media/media.module'; +import { MediaService } from '../../src/media/media.service'; +import { NotesModule } from '../../src/notes/notes.module'; +import { NotesService } from '../../src/notes/notes.service'; +import { PermissionsModule } from '../../src/permissions/permissions.module'; +import { UsersService } from '../../src/users/users.service'; + +describe('Notes', () => { + let app: NestExpressApplication; + let mediaService: MediaService; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + PublicApiModule, + MediaModule, + TypeOrmModule.forRoot({ + type: 'sqlite', + database: './hedgedoc-e2e-media.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); + }); +}); 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 () => { 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"