Merge pull request #1057 from hedgedoc/privateApi/media

This commit is contained in:
Yannick Bungers 2021-03-25 22:41:03 +01:00 committed by GitHub
commit 8387c9927e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 302 additions and 1 deletions

View file

@ -0,0 +1,86 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Test, TestingModule } from '@nestjs/testing';
import { MediaController } from './media.controller';
import { LoggerModule } from '../../../logger/logger.module';
import { ConfigModule } from '@nestjs/config';
import mediaConfigMock from '../../../config/mock/media.config.mock';
import appConfigMock from '../../../config/mock/app.config.mock';
import authConfigMock from '../../../config/mock/auth.config.mock';
import customizationConfigMock from '../../../config/mock/customization.config.mock';
import externalConfigMock from '../../../config/mock/external-services.config.mock';
import { MediaModule } from '../../../media/media.module';
import { NotesModule } from '../../../notes/notes.module';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AuthorColor } from '../../../notes/author-color.entity';
import { Authorship } from '../../../revisions/authorship.entity';
import { AuthToken } from '../../../auth/auth-token.entity';
import { Identity } from '../../../users/identity.entity';
import { MediaUpload } from '../../../media/media-upload.entity';
import { Note } from '../../../notes/note.entity';
import { Revision } from '../../../revisions/revision.entity';
import { User } from '../../../users/user.entity';
import { Tag } from '../../../notes/tag.entity';
import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
import { Group } from '../../../groups/group.entity';
describe('MediaController', () => {
let controller: MediaController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
MediaModule,
NotesModule,
LoggerModule,
ConfigModule.forRoot({
isGlobal: true,
load: [
appConfigMock,
mediaConfigMock,
authConfigMock,
customizationConfigMock,
externalConfigMock,
],
}),
],
controllers: [MediaController],
})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(AuthToken))
.useValue({})
.overrideProvider(getRepositoryToken(Identity))
.useValue({})
.overrideProvider(getRepositoryToken(MediaUpload))
.useValue({})
.overrideProvider(getRepositoryToken(Note))
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
.useValue({})
.overrideProvider(getRepositoryToken(User))
.useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.overrideProvider(getRepositoryToken(NoteGroupPermission))
.useValue({})
.overrideProvider(getRepositoryToken(NoteUserPermission))
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.compile();
controller = module.get<MediaController>(MediaController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
BadRequestException,
Controller,
Headers,
HttpCode,
InternalServerErrorException,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaService } from '../../../media/media.service';
import { MulterFile } from '../../../media/multer-file.interface';
import { MediaUploadUrlDto } from '../../../media/media-upload-url.dto';
import {
ClientError,
MediaBackendError,
NotInDBError,
} from '../../../errors/errors';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('media')
export class MediaController {
constructor(
private readonly logger: ConsoleLoggerService,
private mediaService: MediaService,
) {
this.logger.setContext(MediaController.name);
}
@Post()
@UseInterceptors(FileInterceptor('file'))
@HttpCode(201)
async uploadMedia(
@UploadedFile() file: MulterFile,
@Headers('HedgeDoc-Note') noteId: string,
): Promise<MediaUploadUrlDto> {
// ToDo: Get real userName
const username = 'hardcoded';
this.logger.debug(
`Recieved filename '${file.originalname}' for note '${noteId}' from user '${username}'`,
'uploadImage',
);
try {
const url = await this.mediaService.saveFile(
file.buffer,
username,
noteId,
);
return this.mediaService.toMediaUploadUrlDto(url);
} catch (e) {
if (e instanceof ClientError || e instanceof NotInDBError) {
throw new BadRequestException(e.message);
}
if (e instanceof MediaBackendError) {
throw new InternalServerErrorException(
'There was an error in the media backend',
);
}
throw e;
}
}
}

View file

@ -14,9 +14,10 @@ import { FrontendConfigModule } from '../../frontend-config/frontend-config.modu
import { HistoryController } from './me/history/history.controller'; import { HistoryController } from './me/history/history.controller';
import { HistoryModule } from '../../history/history.module'; import { HistoryModule } from '../../history/history.module';
import { NotesModule } from '../../notes/notes.module'; import { NotesModule } from '../../notes/notes.module';
import { MediaModule } from '../../media/media.module';
import { MediaController } from './media/media.controller';
import { NotesController } from './notes/notes.controller'; import { NotesController } from './notes/notes.controller';
import { PermissionsModule } from '../../permissions/permissions.module'; import { PermissionsModule } from '../../permissions/permissions.module';
import { MediaModule } from '../../media/media.module';
import { RevisionsModule } from '../../revisions/revisions.module'; import { RevisionsModule } from '../../revisions/revisions.module';
@Module({ @Module({
@ -34,6 +35,7 @@ import { RevisionsModule } from '../../revisions/revisions.module';
controllers: [ controllers: [
TokensController, TokensController,
ConfigController, ConfigController,
MediaController,
HistoryController, HistoryController,
NotesController, NotesController,
], ],

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

Binary file not shown.

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,138 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access
*/
import { ConfigModule, ConfigService } from '@nestjs/config';
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 mediaConfigMock from '../../src/config/mock/media.config.mock';
import appConfigMock from '../../src/config/mock/app.config.mock';
import authConfigMock from '../../src/config/mock/auth.config.mock';
import customizationConfigMock from '../../src/config/mock/customization.config.mock';
import externalConfigMock from '../../src/config/mock/external-services.config.mock';
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 { NotesModule } from '../../src/notes/notes.module';
import { NotesService } from '../../src/notes/notes.service';
import { PermissionsModule } from '../../src/permissions/permissions.module';
import { AuthModule } from '../../src/auth/auth.module';
import { join } from 'path';
import { PrivateApiModule } from '../../src/api/private/private-api.module';
import { UsersService } from '../../src/users/users.service';
describe('Media', () => {
let app: NestExpressApplication;
let uploadPath: string;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
mediaConfigMock,
appConfigMock,
authConfigMock,
customizationConfigMock,
externalConfigMock,
],
}),
PrivateApiModule,
MediaModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: './hedgedoc-e2e-private-media.sqlite',
autoLoadEntities: true,
dropSchema: true,
synchronize: true,
}),
NotesModule,
PermissionsModule,
GroupsModule,
LoggerModule,
AuthModule,
],
}).compile();
const config = moduleRef.get<ConfigService>(ConfigService);
uploadPath = config.get('mediaConfig').backend.filesystem.uploadPath;
app = moduleRef.createNestApplication<NestExpressApplication>();
app.useStaticAssets(uploadPath, {
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 userService: UsersService = moduleRef.get('UsersService');
await userService.createUser('hardcoded', 'Testy');
});
describe('POST /media', () => {
it('works', async () => {
const uploadResponse = await request(app.getHttpServer())
.post('/media')
.attach('file', 'test/private-api/fixtures/test.png')
.set('HedgeDoc-Note', 'test_upload_media')
.expect('Content-Type', /json/)
.expect(201);
const path: string = uploadResponse.body.link;
const testImage = await fs.readFile('test/private-api/fixtures/test.png');
const downloadResponse = await request(app.getHttpServer()).get(path);
expect(downloadResponse.body).toEqual(testImage);
// Remove /upload/ from path as we just need the filename.
const fileName = path.replace('/uploads/', '');
// delete the file afterwards
await fs.unlink(join(uploadPath, fileName));
});
describe('fails:', () => {
it('MIME type not supported', async () => {
await request(app.getHttpServer())
.post('/media')
.attach('file', 'test/private-api/fixtures/test.zip')
.set('HedgeDoc-Note', 'test_upload_media')
.expect(400);
expect(await fs.access(uploadPath)).toBeFalsy();
});
it('note does not exist', async () => {
await request(app.getHttpServer())
.post('/media')
.attach('file', 'test/private-api/fixtures/test.zip')
.set('HedgeDoc-Note', 'i_dont_exist')
.expect(400);
expect(await fs.access(uploadPath)).toBeFalsy();
});
it('mediaBackend error', async () => {
await fs.rmdir(uploadPath);
await fs.mkdir(uploadPath, {
mode: '444',
});
await request(app.getHttpServer())
.post('/media')
.attach('file', 'test/private-api/fixtures/test.png')
.set('HedgeDoc-Note', 'test_upload_media')
.expect('Content-Type', /json/)
.expect(500);
await fs.rmdir(uploadPath);
});
});
});
afterAll(async () => {
// Delete the upload folder
await fs.rmdir(uploadPath);
});
});