diff --git a/package.json b/package.json index 4b6dd5c77..1bfd9eb58 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "class-transformer": "^0.2.3", "class-validator": "^0.12.2", "connect-typeorm": "^1.1.4", + "file-type": "^15.0.1", "raw-body": "^2.4.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", diff --git a/src/media/media.module.ts b/src/media/media.module.ts index eebd3501a..f791218e4 100644 --- a/src/media/media.module.ts +++ b/src/media/media.module.ts @@ -1,8 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotesModule } from '../notes/notes.module'; +import { UsersModule } from '../users/users.module'; import { MediaUpload } from './media-upload.entity'; +import { MediaService } from './media.service'; @Module({ - imports: [TypeOrmModule.forFeature([MediaUpload])], + imports: [TypeOrmModule.forFeature([MediaUpload]), NotesModule, UsersModule], + providers: [MediaService], + exports: [MediaService], }) export class MediaModule {} diff --git a/src/media/media.service.spec.ts b/src/media/media.service.spec.ts new file mode 100644 index 000000000..5009e4a57 --- /dev/null +++ b/src/media/media.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MediaService } from './media.service'; + +describe('MediaService', () => { + let service: MediaService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MediaService], + }).compile(); + + service = module.get(MediaService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/media/media.service.ts b/src/media/media.service.ts new file mode 100644 index 000000000..c8dfa2c3e --- /dev/null +++ b/src/media/media.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import * as FileType from 'file-type'; +import { Repository } from 'typeorm'; +import { NotesService } from '../notes/notes.service'; +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 { + constructor( + @InjectRepository(MediaUpload) + private mediaUploadRepository: Repository, + private notesService: NotesService, + private usersService: UsersService, + ) {} + + public async saveFile(file: MulterFile, username: string, noteId: string) { + const note = await this.notesService.getNoteByIdOrAlias(noteId); + const user = await this.usersService.getUserByUsername(username); + const fileTypeResult = await FileType.fromBuffer(file.buffer); + if (!fileTypeResult) { + throw new Error('Could not detect file type.'); + } + if (!MediaService.isAllowedMimeType(fileTypeResult.mime)) { + throw new Error('MIME type not allowed'); + } + //TODO: Choose backend according to config + const mediaUpload = MediaUpload.create( + note, + user, + fileTypeResult.ext, + BackendType.FILEYSTEM, + ); + const backend = new FilesystemBackend(); + const [url, backendData] = await backend.saveFile( + file.buffer, + mediaUpload.id, + ); + mediaUpload.backendData = backendData; + await this.mediaUploadRepository.save(mediaUpload); + return url; + } + + private static isAllowedMimeType(mimeType: string): boolean { + //TODO: Which mimetypes are allowed? + return true; + } +} diff --git a/src/media/multer-file.interface.ts b/src/media/multer-file.interface.ts new file mode 100644 index 000000000..a602a5008 --- /dev/null +++ b/src/media/multer-file.interface.ts @@ -0,0 +1,32 @@ +import { Readable } from 'stream'; + +// Type from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/multer/index.d.ts +export interface MulterFile { + /** Name of the form field associated with this file. */ + fieldname: string; + /** Name of the file on the uploader's computer. */ + originalname: string; + /** + * Value of the `Content-Transfer-Encoding` header for this file. + * @deprecated since July 2015 + * @see RFC 7578, Section 4.7 + */ + encoding: string; + /** Value of the `Content-Type` header for this file. */ + mimetype: string; + /** Size of the file in bytes. */ + size: number; + /** + * A readable stream of this file. Only available to the `_handleFile` + * callback for custom `StorageEngine`s. + */ + stream: Readable; + /** `DiskStorage` only: Directory to which this file has been uploaded. */ + destination: string; + /** `DiskStorage` only: Name of this file within `destination`. */ + filename: string; + /** `DiskStorage` only: Full path to the uploaded file. */ + path: string; + /** `MemoryStorage` only: A Buffer containing the entire file. */ + buffer: Buffer; +}