mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-27 12:08:02 -05:00
Merge pull request #534 from codimd/media-controller
This commit is contained in:
commit
e2696e647b
23 changed files with 698 additions and 32 deletions
|
@ -115,6 +115,16 @@ entity "Group" {
|
||||||
*canEdit : boolean
|
*canEdit : boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "MediaUpload" {
|
||||||
|
*id : text <<unique>>
|
||||||
|
--
|
||||||
|
*noteId : uuid <<FK Note>>
|
||||||
|
*userId : uuid <<FK User>>
|
||||||
|
*backendType: text
|
||||||
|
backendData: text
|
||||||
|
*createdAt : date
|
||||||
|
}
|
||||||
|
|
||||||
Note "1" - "1..*" Revision
|
Note "1" - "1..*" Revision
|
||||||
Revision "0..*" - "0..*" Authorship
|
Revision "0..*" - "0..*" Authorship
|
||||||
(Revision, Authorship) .. RevisionAuthorship
|
(Revision, Authorship) .. RevisionAuthorship
|
||||||
|
@ -129,4 +139,6 @@ authToken "1..*" -- "1" User
|
||||||
seesion "1..*" -- "1" User
|
seesion "1..*" -- "1" User
|
||||||
Note "0..*" -- "0..*" User : color
|
Note "0..*" -- "0..*" User : color
|
||||||
(Note, User) .. AuthorColors
|
(Note, User) .. AuthorColors
|
||||||
|
MediaUpload "0..*" -- "1" Note
|
||||||
|
MediaUpload "0..*" -- "1" User
|
||||||
@enduml
|
@enduml
|
||||||
|
|
|
@ -454,27 +454,112 @@ paths:
|
||||||
content:
|
content:
|
||||||
text/plain:
|
text/plain:
|
||||||
example: my-note
|
example: my-note
|
||||||
/media/upload:
|
/media:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- media
|
- media
|
||||||
summary: Uploads an image to the backend storage
|
summary: Uploads a media file to the backend storage
|
||||||
description: Uploads an image to be processed by the backend.
|
description: Uploads a file to be processed by the backend.
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
description: The binary image to upload.
|
description: The binary file to upload.
|
||||||
content:
|
content:
|
||||||
image/*:
|
application/pdf:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
|
image/apng:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/bmp:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/gif:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/heif:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/heic:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/heif-sequence:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/heic-sequence:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/jpeg:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/png:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/svg+xml:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/tiff:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
image/webp:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
parameters:
|
||||||
|
- in: header
|
||||||
|
name: HedgeDoc-Note
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: ID or alias of the parent note
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: The image was uploaded successfully.
|
description: The file was uploaded successfully.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
link:
|
||||||
|
type: string
|
||||||
'401':
|
'401':
|
||||||
"$ref": "#/components/responses/UnauthorizedError"
|
"$ref": "#/components/responses/UnauthorizedError"
|
||||||
'403':
|
'403':
|
||||||
"$ref": "#/components/responses/ForbiddenError"
|
"$ref": "#/components/responses/ForbiddenError"
|
||||||
|
/media/{filename}:
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- media
|
||||||
|
summary: Delete the specified file
|
||||||
|
operationId: deleteMedia
|
||||||
|
parameters:
|
||||||
|
- name: filename
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The name of the file to be deleted.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
example: e18d1b83e1821128615bad849ad0655a.jpg
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
"$ref": "#/components/responses/SuccessfullyDeleted"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/UnauthorizedError"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/ForbiddenError"
|
||||||
|
'404':
|
||||||
|
"$ref": "#/components/responses/NotFoundError"
|
||||||
/monitoring:
|
/monitoring:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -25,11 +25,13 @@
|
||||||
"@nestjs/common": "^7.0.0",
|
"@nestjs/common": "^7.0.0",
|
||||||
"@nestjs/core": "^7.0.0",
|
"@nestjs/core": "^7.0.0",
|
||||||
"@nestjs/platform-express": "^7.0.0",
|
"@nestjs/platform-express": "^7.0.0",
|
||||||
|
"@nestjs/serve-static": "^2.1.3",
|
||||||
"@nestjs/swagger": "^4.5.12",
|
"@nestjs/swagger": "^4.5.12",
|
||||||
"@nestjs/typeorm": "^7.1.0",
|
"@nestjs/typeorm": "^7.1.0",
|
||||||
"class-transformer": "^0.2.3",
|
"class-transformer": "^0.2.3",
|
||||||
"class-validator": "^0.12.2",
|
"class-validator": "^0.12.2",
|
||||||
"connect-typeorm": "^1.1.4",
|
"connect-typeorm": "^1.1.4",
|
||||||
|
"file-type": "^15.0.1",
|
||||||
"raw-body": "^2.4.1",
|
"raw-body": "^2.4.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|
|
@ -29,8 +29,10 @@ export class MeController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getMe(): UserInfoDto {
|
async getMe(): Promise<UserInfoDto> {
|
||||||
return this.usersService.getUserInfo();
|
return this.usersService.toUserDto(
|
||||||
|
await this.usersService.getUserByUsername('hardcoded'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('history')
|
@Get('history')
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
import { LoggerModule } from '../../../logger/logger.module';
|
import { LoggerModule } from '../../../logger/logger.module';
|
||||||
|
import { MediaUpload } from '../../../media/media-upload.entity';
|
||||||
|
import { MediaModule } from '../../../media/media.module';
|
||||||
|
import { AuthorColor } from '../../../notes/author-color.entity';
|
||||||
|
import { Note } from '../../../notes/note.entity';
|
||||||
|
import { NotesModule } from '../../../notes/notes.module';
|
||||||
|
import { Authorship } from '../../../revisions/authorship.entity';
|
||||||
|
import { Revision } from '../../../revisions/revision.entity';
|
||||||
|
import { AuthToken } from '../../../users/auth-token.entity';
|
||||||
|
import { Identity } from '../../../users/identity.entity';
|
||||||
|
import { User } from '../../../users/user.entity';
|
||||||
import { MediaController } from './media.controller';
|
import { MediaController } from './media.controller';
|
||||||
|
|
||||||
describe('Media Controller', () => {
|
describe('Media Controller', () => {
|
||||||
|
@ -8,8 +19,25 @@ describe('Media Controller', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [MediaController],
|
controllers: [MediaController],
|
||||||
imports: [LoggerModule],
|
imports: [LoggerModule, MediaModule, NotesModule],
|
||||||
}).compile();
|
})
|
||||||
|
.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({})
|
||||||
|
.compile();
|
||||||
|
|
||||||
controller = module.get<MediaController>(MediaController);
|
controller = module.get<MediaController>(MediaController);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,21 +1,75 @@
|
||||||
import {
|
import {
|
||||||
|
BadRequestException,
|
||||||
Controller,
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Headers,
|
||||||
|
NotFoundException,
|
||||||
|
Param,
|
||||||
Post,
|
Post,
|
||||||
|
UnauthorizedException,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import {
|
||||||
|
ClientError,
|
||||||
|
NotInDBError,
|
||||||
|
PermissionError,
|
||||||
|
} from '../../../errors/errors';
|
||||||
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
|
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')
|
@Controller('media')
|
||||||
export class MediaController {
|
export class MediaController {
|
||||||
constructor(private readonly logger: ConsoleLoggerService) {
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
private mediaService: MediaService,
|
||||||
|
private notesService: NotesService,
|
||||||
|
) {
|
||||||
this.logger.setContext(MediaController.name);
|
this.logger.setContext(MediaController.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('upload')
|
@Post()
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
uploadImage(@UploadedFile() file) {
|
async uploadMedia(
|
||||||
this.logger.debug('Recieved file: ' + file);
|
@UploadedFile() file: MulterFile,
|
||||||
|
@Headers('HedgeDoc-Note') noteId: string,
|
||||||
|
) {
|
||||||
|
//TODO: Get user from request
|
||||||
|
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, username, noteId);
|
||||||
|
return {
|
||||||
|
link: url,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ClientError || e instanceof NotInDBError) {
|
||||||
|
throw new BadRequestException(e.message);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':filename')
|
||||||
|
async deleteMedia(@Param('filename') filename: string) {
|
||||||
|
//TODO: Get user from request
|
||||||
|
const username = 'hardcoded';
|
||||||
|
try {
|
||||||
|
await this.mediaService.deleteFile(filename, username);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof PermissionError) {
|
||||||
|
throw new UnauthorizedException(e.message);
|
||||||
|
}
|
||||||
|
if (e instanceof NotInDBError) {
|
||||||
|
throw new NotFoundException(e.message);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { HistoryModule } from '../../history/history.module';
|
import { HistoryModule } from '../../history/history.module';
|
||||||
import { LoggerModule } from '../../logger/logger.module';
|
import { LoggerModule } from '../../logger/logger.module';
|
||||||
|
import { MediaModule } from '../../media/media.module';
|
||||||
import { MonitoringModule } from '../../monitoring/monitoring.module';
|
import { MonitoringModule } from '../../monitoring/monitoring.module';
|
||||||
import { NotesModule } from '../../notes/notes.module';
|
import { NotesModule } from '../../notes/notes.module';
|
||||||
import { RevisionsModule } from '../../revisions/revisions.module';
|
import { RevisionsModule } from '../../revisions/revisions.module';
|
||||||
|
@ -18,6 +19,7 @@ import { MonitoringController } from './monitoring/monitoring.controller';
|
||||||
RevisionsModule,
|
RevisionsModule,
|
||||||
MonitoringModule,
|
MonitoringModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
|
MediaModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
MeController,
|
MeController,
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { join } from 'path';
|
||||||
import { PublicApiModule } from './api/public/public-api.module';
|
import { PublicApiModule } from './api/public/public-api.module';
|
||||||
import { AuthorsModule } from './authors/authors.module';
|
import { AuthorsModule } from './authors/authors.module';
|
||||||
import { GroupsModule } from './groups/groups.module';
|
import { GroupsModule } from './groups/groups.module';
|
||||||
import { HistoryModule } from './history/history.module';
|
import { HistoryModule } from './history/history.module';
|
||||||
import { LoggerModule } from './logger/logger.module';
|
import { LoggerModule } from './logger/logger.module';
|
||||||
|
import { MediaModule } from './media/media.module';
|
||||||
import { MonitoringModule } from './monitoring/monitoring.module';
|
import { MonitoringModule } from './monitoring/monitoring.module';
|
||||||
import { NotesModule } from './notes/notes.module';
|
import { NotesModule } from './notes/notes.module';
|
||||||
import { PermissionsModule } from './permissions/permissions.module';
|
import { PermissionsModule } from './permissions/permissions.module';
|
||||||
|
@ -19,6 +22,11 @@ import { UsersModule } from './users/users.module';
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
}),
|
}),
|
||||||
|
ServeStaticModule.forRoot({
|
||||||
|
rootPath: join(__dirname, '..'),
|
||||||
|
// TODO: Get uploads directory from config
|
||||||
|
renderPath: 'uploads',
|
||||||
|
}),
|
||||||
NotesModule,
|
NotesModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
RevisionsModule,
|
RevisionsModule,
|
||||||
|
@ -29,6 +37,7 @@ import { UsersModule } from './users/users.module';
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
GroupsModule,
|
GroupsModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
|
MediaModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|
11
src/errors/errors.ts
Normal file
11
src/errors/errors.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export class NotInDBError extends Error {
|
||||||
|
name = 'NotInDBError';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClientError extends Error {
|
||||||
|
name = 'ClientError';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PermissionError extends Error {
|
||||||
|
name = 'PermissionError';
|
||||||
|
}
|
6
src/media/backends/backend-type.enum.ts
Normal file
6
src/media/backends/backend-type.enum.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export enum BackendType {
|
||||||
|
FILEYSTEM = 'filesystem',
|
||||||
|
S3 = 's3',
|
||||||
|
IMGUR = 'imgur',
|
||||||
|
AZURE = 'azure',
|
||||||
|
}
|
39
src/media/backends/filesystem-backend.ts
Normal file
39
src/media/backends/filesystem-backend.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||||
|
import { MediaBackend } from '../media-backend.interface';
|
||||||
|
import { BackendData } from '../media-upload.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FilesystemBackend implements MediaBackend {
|
||||||
|
constructor(private readonly logger: ConsoleLoggerService) {
|
||||||
|
this.logger.setContext(FilesystemBackend.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveFile(
|
||||||
|
buffer: Buffer,
|
||||||
|
fileName: string,
|
||||||
|
): Promise<[string, BackendData]> {
|
||||||
|
const filePath = FilesystemBackend.getFilePath(fileName);
|
||||||
|
this.logger.debug(`Writing file to: ${filePath}`, 'saveFile');
|
||||||
|
await fs.writeFile(filePath, buffer, null);
|
||||||
|
return ['/' + filePath, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(fileName: string, _: BackendData): Promise<void> {
|
||||||
|
return fs.unlink(FilesystemBackend.getFilePath(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileURL(fileName: string, _: BackendData): Promise<string> {
|
||||||
|
const filePath = FilesystemBackend.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);
|
||||||
|
}
|
||||||
|
}
|
25
src/media/media-backend.interface.ts
Normal file
25
src/media/media-backend.interface.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { BackendData } from './media-upload.entity';
|
||||||
|
|
||||||
|
export interface MediaBackend {
|
||||||
|
/**
|
||||||
|
* Saves a file according to backend internals.
|
||||||
|
* @param buffer File data
|
||||||
|
* @param fileName Name of the file to save. Can include a file extension.
|
||||||
|
* @return Tuple of file URL and internal backend data, which should be saved.
|
||||||
|
*/
|
||||||
|
saveFile(buffer: Buffer, fileName: string): Promise<[string, BackendData]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the URL of a previously saved file.
|
||||||
|
* @param fileName String to identify the file
|
||||||
|
* @param backendData Internal backend data
|
||||||
|
*/
|
||||||
|
getFileURL(fileName: string, backendData: BackendData): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file from the backend
|
||||||
|
* @param fileName String to identify the file
|
||||||
|
* @param backendData Internal backend data
|
||||||
|
*/
|
||||||
|
deleteFile(fileName: string, backendData: BackendData): Promise<void>;
|
||||||
|
}
|
62
src/media/media-upload.entity.ts
Normal file
62
src/media/media-upload.entity.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Note } from '../notes/note.entity';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { BackendType } from './backends/backend-type.enum';
|
||||||
|
|
||||||
|
export type BackendData = string | null;
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class MediaUpload {
|
||||||
|
@PrimaryColumn()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ManyToOne(_ => Note, { nullable: false })
|
||||||
|
note: Note;
|
||||||
|
|
||||||
|
@ManyToOne(_ => User, { nullable: false })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
backendType: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
backendData: BackendData;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
note: Note,
|
||||||
|
user: User,
|
||||||
|
extension: string,
|
||||||
|
backendType: BackendType,
|
||||||
|
backendData?: string,
|
||||||
|
): MediaUpload {
|
||||||
|
const upload = new MediaUpload();
|
||||||
|
const randomBytes = crypto.randomBytes(16);
|
||||||
|
upload.id = randomBytes.toString('hex') + '.' + extension;
|
||||||
|
upload.note = note;
|
||||||
|
upload.user = user;
|
||||||
|
upload.backendType = backendType;
|
||||||
|
if (backendData) {
|
||||||
|
upload.backendData = backendData;
|
||||||
|
} else {
|
||||||
|
upload.backendData = null;
|
||||||
|
}
|
||||||
|
return upload;
|
||||||
|
}
|
||||||
|
}
|
20
src/media/media.module.ts
Normal file
20
src/media/media.module.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
|
import { NotesModule } from '../notes/notes.module';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { FilesystemBackend } from './backends/filesystem-backend';
|
||||||
|
import { MediaUpload } from './media-upload.entity';
|
||||||
|
import { MediaService } from './media.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([MediaUpload]),
|
||||||
|
NotesModule,
|
||||||
|
UsersModule,
|
||||||
|
LoggerModule,
|
||||||
|
],
|
||||||
|
providers: [MediaService, FilesystemBackend],
|
||||||
|
exports: [MediaService],
|
||||||
|
})
|
||||||
|
export class MediaModule {}
|
54
src/media/media.service.spec.ts
Normal file
54
src/media/media.service.spec.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
|
import { AuthorColor } from '../notes/author-color.entity';
|
||||||
|
import { Note } from '../notes/note.entity';
|
||||||
|
import { NotesModule } from '../notes/notes.module';
|
||||||
|
import { Authorship } from '../revisions/authorship.entity';
|
||||||
|
import { Revision } from '../revisions/revision.entity';
|
||||||
|
import { AuthToken } from '../users/auth-token.entity';
|
||||||
|
import { Identity } from '../users/identity.entity';
|
||||||
|
import { User } from '../users/user.entity';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { MediaUpload } from './media-upload.entity';
|
||||||
|
import { MediaService } from './media.service';
|
||||||
|
|
||||||
|
describe('MediaService', () => {
|
||||||
|
let service: MediaService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
MediaService,
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(MediaUpload),
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imports: [LoggerModule, NotesModule, UsersModule],
|
||||||
|
})
|
||||||
|
.overrideProvider(getRepositoryToken(AuthorColor))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(MediaUpload))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Authorship))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(AuthToken))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Identity))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Note))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(Revision))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(User))
|
||||||
|
.useValue({})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
service = module.get<MediaService>(MediaService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
110
src/media/media.service.ts
Normal file
110
src/media/media.service.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import * as FileType from 'file-type';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ClientError, NotInDBError, PermissionError } from '../errors/errors';
|
||||||
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
|
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(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
@InjectRepository(MediaUpload)
|
||||||
|
private mediaUploadRepository: Repository<MediaUpload>,
|
||||||
|
private notesService: NotesService,
|
||||||
|
private usersService: UsersService,
|
||||||
|
private moduleRef: ModuleRef,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(MediaService.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isAllowedMimeType(mimeType: string): boolean {
|
||||||
|
const allowedTypes = [
|
||||||
|
'application/pdf',
|
||||||
|
'image/apng',
|
||||||
|
'image/bmp',
|
||||||
|
'image/gif',
|
||||||
|
'image/heif',
|
||||||
|
'image/heic',
|
||||||
|
'image/heif-sequence',
|
||||||
|
'image/heic-sequence',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/svg+xml',
|
||||||
|
'image/tiff',
|
||||||
|
'image/webp',
|
||||||
|
];
|
||||||
|
return allowedTypes.includes(mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async saveFile(file: MulterFile, username: string, noteId: string) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Saving '${file.originalname}' 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);
|
||||||
|
if (!fileTypeResult) {
|
||||||
|
throw new ClientError('Could not detect file type.');
|
||||||
|
}
|
||||||
|
if (!MediaService.isAllowedMimeType(fileTypeResult.mime)) {
|
||||||
|
throw new ClientError('MIME type not allowed.');
|
||||||
|
}
|
||||||
|
//TODO: Choose backend according to config
|
||||||
|
const mediaUpload = MediaUpload.create(
|
||||||
|
note,
|
||||||
|
user,
|
||||||
|
fileTypeResult.ext,
|
||||||
|
BackendType.FILEYSTEM,
|
||||||
|
);
|
||||||
|
this.logger.debug(`Generated filename: '${mediaUpload.id}'`, 'saveFile');
|
||||||
|
const backend = this.moduleRef.get(FilesystemBackend);
|
||||||
|
const [url, backendData] = await backend.saveFile(
|
||||||
|
file.buffer,
|
||||||
|
mediaUpload.id,
|
||||||
|
);
|
||||||
|
mediaUpload.backendData = backendData;
|
||||||
|
await this.mediaUploadRepository.save(mediaUpload);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteFile(filename: string, username: string) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Deleting '${filename}' for user '${username}'`,
|
||||||
|
'deleteFile',
|
||||||
|
);
|
||||||
|
const mediaUpload = await this.findUploadByFilename(filename);
|
||||||
|
if (mediaUpload.user.userName !== username) {
|
||||||
|
this.logger.warn(
|
||||||
|
`${username} tried to delete '${filename}', but is not the owner`,
|
||||||
|
'deleteFile',
|
||||||
|
);
|
||||||
|
throw new PermissionError(
|
||||||
|
`File '${filename}' is not owned by '${username}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const backend = this.moduleRef.get(FilesystemBackend);
|
||||||
|
await backend.deleteFile(filename, mediaUpload.backendData);
|
||||||
|
await this.mediaUploadRepository.remove(mediaUpload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findUploadByFilename(filename: string): Promise<MediaUpload> {
|
||||||
|
const mediaUpload = await this.mediaUploadRepository.findOne(filename, {
|
||||||
|
relations: ['user'],
|
||||||
|
});
|
||||||
|
if (mediaUpload === undefined) {
|
||||||
|
throw new NotInDBError(
|
||||||
|
`MediaUpload with filename '${filename}' not found`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return mediaUpload;
|
||||||
|
}
|
||||||
|
}
|
32
src/media/multer-file.interface.ts
Normal file
32
src/media/multer-file.interface.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
import { NotInDBError } from '../errors/errors';
|
||||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
import { Revision } from '../revisions/revision.entity';
|
import { Revision } from '../revisions/revision.entity';
|
||||||
import { RevisionsService } from '../revisions/revisions.service';
|
import { RevisionsService } from '../revisions/revisions.service';
|
||||||
|
@ -132,8 +133,19 @@ export class NotesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNoteByIdOrAlias(noteIdOrAlias: string): Promise<Note> {
|
async getNoteByIdOrAlias(noteIdOrAlias: string): Promise<Note> {
|
||||||
|
this.logger.debug(
|
||||||
|
`Trying to find note '${noteIdOrAlias}'`,
|
||||||
|
'getNoteByIdOrAlias',
|
||||||
|
);
|
||||||
const note = await this.noteRepository.findOne({
|
const note = await this.noteRepository.findOne({
|
||||||
where: [{ id: noteIdOrAlias }, { alias: noteIdOrAlias }],
|
where: [
|
||||||
|
{
|
||||||
|
id: noteIdOrAlias,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
alias: noteIdOrAlias,
|
||||||
|
},
|
||||||
|
],
|
||||||
relations: [
|
relations: [
|
||||||
'authorColors',
|
'authorColors',
|
||||||
'owner',
|
'owner',
|
||||||
|
@ -142,8 +154,9 @@ export class NotesService {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (note === undefined) {
|
if (note === undefined) {
|
||||||
//TODO: Improve error handling
|
throw new NotInDBError(
|
||||||
throw new Error('Note not found');
|
`Note with id/alias '${noteIdOrAlias}' not found.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,4 +48,20 @@ export class User {
|
||||||
identity => identity.user,
|
identity => identity.user,
|
||||||
)
|
)
|
||||||
identities: Identity[];
|
identities: Identity[];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
userName: string,
|
||||||
|
displayName: string,
|
||||||
|
): Pick<
|
||||||
|
User,
|
||||||
|
'userName' | 'displayName' | 'ownedNotes' | 'authToken' | 'identities'
|
||||||
|
> {
|
||||||
|
const newUser = new User();
|
||||||
|
newUser.userName = userName;
|
||||||
|
newUser.displayName = displayName;
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
|
import { User } from './user.entity';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
describe('UsersService', () => {
|
describe('UsersService', () => {
|
||||||
|
@ -7,9 +9,18 @@ describe('UsersService', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [UsersService],
|
providers: [
|
||||||
|
UsersService,
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(User),
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
imports: [LoggerModule],
|
imports: [LoggerModule],
|
||||||
}).compile();
|
})
|
||||||
|
.overrideProvider(getRepositoryToken(User))
|
||||||
|
.useValue({})
|
||||||
|
.compile();
|
||||||
|
|
||||||
service = module.get<UsersService>(UsersService);
|
service = module.get<UsersService>(UsersService);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,26 +1,44 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { NotInDBError } from '../errors/errors';
|
||||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||||
import { UserInfoDto } from './user-info.dto';
|
import { UserInfoDto } from './user-info.dto';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(private readonly logger: ConsoleLoggerService) {
|
constructor(
|
||||||
|
private readonly logger: ConsoleLoggerService,
|
||||||
|
@InjectRepository(User) private userRepository: Repository<User>,
|
||||||
|
) {
|
||||||
this.logger.setContext(UsersService.name);
|
this.logger.setContext(UsersService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserInfo(): UserInfoDto {
|
createUser(userName: string, displayName: string): Promise<User> {
|
||||||
//TODO: Use the database
|
const user = User.create(userName, displayName);
|
||||||
this.logger.warn('Using hardcoded data!');
|
return this.userRepository.save(user);
|
||||||
return {
|
|
||||||
displayName: 'foo',
|
|
||||||
userName: 'fooUser',
|
|
||||||
email: 'foo@example.com',
|
|
||||||
photo: '',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPhotoUrl(user: User) {
|
async deleteUser(userName: string) {
|
||||||
|
//TOOD: Handle owned notes and edits
|
||||||
|
const user = await this.userRepository.findOne({
|
||||||
|
where: { userName: userName },
|
||||||
|
});
|
||||||
|
await this.userRepository.delete(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserByUsername(userName: string): Promise<User> {
|
||||||
|
const user = await this.userRepository.findOne({
|
||||||
|
where: { userName: userName },
|
||||||
|
});
|
||||||
|
if (user === undefined) {
|
||||||
|
throw new NotInDBError(`User with username '${userName}' not found`);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPhotoUrl(user: User): string {
|
||||||
if (user.photo) {
|
if (user.photo) {
|
||||||
return user.photo;
|
return user.photo;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Test } from '@nestjs/testing';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import * as request from 'supertest';
|
import * as request from 'supertest';
|
||||||
import { PublicApiModule } from '../../src/api/public/public-api.module';
|
import { PublicApiModule } from '../../src/api/public/public-api.module';
|
||||||
|
import { NotInDBError } from '../../src/errors/errors';
|
||||||
import { GroupsModule } from '../../src/groups/groups.module';
|
import { GroupsModule } from '../../src/groups/groups.module';
|
||||||
import { LoggerModule } from '../../src/logger/logger.module';
|
import { LoggerModule } from '../../src/logger/logger.module';
|
||||||
import { NotesModule } from '../../src/notes/notes.module';
|
import { NotesModule } from '../../src/notes/notes.module';
|
||||||
|
@ -82,7 +83,7 @@ describe('Notes', () => {
|
||||||
.delete('/notes/test3')
|
.delete('/notes/test3')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
return expect(notesService.getNoteByIdOrAlias('test3')).rejects.toEqual(
|
return expect(notesService.getNoteByIdOrAlias('test3')).rejects.toEqual(
|
||||||
Error('Note not found'),
|
new NotInDBError("Note with id/alias 'test3' not found."),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
56
yarn.lock
56
yarn.lock
|
@ -614,6 +614,13 @@
|
||||||
"@angular-devkit/schematics" "9.1.7"
|
"@angular-devkit/schematics" "9.1.7"
|
||||||
fs-extra "9.0.0"
|
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":
|
"@nestjs/swagger@^4.5.12":
|
||||||
version "4.5.12"
|
version "4.5.12"
|
||||||
resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-4.5.12.tgz#e8aa65fbb0033007ece1d494b002f47ff472c20b"
|
resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-4.5.12.tgz#e8aa65fbb0033007ece1d494b002f47ff472c20b"
|
||||||
|
@ -669,6 +676,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sinonjs/commons" "^1.7.0"
|
"@sinonjs/commons" "^1.7.0"
|
||||||
|
|
||||||
|
"@tokenizer/token@^0.1.0", "@tokenizer/token@^0.1.1":
|
||||||
|
version "0.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3"
|
||||||
|
integrity sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w==
|
||||||
|
|
||||||
"@types/anymatch@*":
|
"@types/anymatch@*":
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
||||||
|
@ -737,6 +749,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.31.tgz#bac8d8aab6a823e91deb7f79083b2a35fa638f33"
|
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.31.tgz#bac8d8aab6a823e91deb7f79083b2a35fa638f33"
|
||||||
integrity sha512-LS1MCPaQKqspg7FvexuhmDbWUhE2yIJ+4AgVIyObfc06/UKZ8REgxGNjZc82wPLWmbeOm7S+gSsLgo75TanG4A==
|
integrity sha512-LS1MCPaQKqspg7FvexuhmDbWUhE2yIJ+4AgVIyObfc06/UKZ8REgxGNjZc82wPLWmbeOm7S+gSsLgo75TanG4A==
|
||||||
|
|
||||||
|
"@types/debug@^4.1.5":
|
||||||
|
version "4.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
|
||||||
|
integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
|
||||||
|
|
||||||
"@types/eslint-visitor-keys@^1.0.0":
|
"@types/eslint-visitor-keys@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
||||||
|
@ -3118,6 +3135,16 @@ file-entry-cache@^5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache "^2.0.1"
|
flat-cache "^2.0.1"
|
||||||
|
|
||||||
|
file-type@^15.0.1:
|
||||||
|
version "15.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/file-type/-/file-type-15.0.1.tgz#54175484953d48b970c095ba8737d4e0c3a9b407"
|
||||||
|
integrity sha512-0LieQlSA3bWUdErNrxzxfI4rhsvNAVPBO06R8pTc1hp9SE6nhqlVyvhcaXoMmtXkBTPnQenbMPLW9X76hH76oQ==
|
||||||
|
dependencies:
|
||||||
|
readable-web-to-node-stream "^2.0.0"
|
||||||
|
strtok3 "^6.0.3"
|
||||||
|
token-types "^2.0.0"
|
||||||
|
typedarray-to-buffer "^3.1.5"
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
file-uri-to-path@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||||
|
@ -3659,7 +3686,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer ">= 2.1.2 < 3"
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
ieee754@^1.1.4:
|
ieee754@^1.1.13, ieee754@^1.1.4:
|
||||||
version "1.1.13"
|
version "1.1.13"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
||||||
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
|
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
|
||||||
|
@ -5698,6 +5725,11 @@ pbkdf2@^3.0.3:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
|
peek-readable@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-3.1.0.tgz#250b08b7de09db8573d7fd8ea475215bbff14348"
|
||||||
|
integrity sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA==
|
||||||
|
|
||||||
performance-now@^2.1.0:
|
performance-now@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||||
|
@ -6028,6 +6060,11 @@ readable-stream@1.1.x:
|
||||||
isarray "0.0.1"
|
isarray "0.0.1"
|
||||||
string_decoder "~0.10.x"
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
|
readable-web-to-node-stream@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz#751e632f466552ac0d5c440cc01470352f93c4b7"
|
||||||
|
integrity sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA==
|
||||||
|
|
||||||
readdirp@^2.2.1:
|
readdirp@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
|
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
|
||||||
|
@ -6841,6 +6878,15 @@ strip-json-comments@~2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||||
|
|
||||||
|
strtok3@^6.0.3:
|
||||||
|
version "6.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.0.4.tgz#ede0d20fde5aa9fda56417c3558eaafccc724694"
|
||||||
|
integrity sha512-rqWMKwsbN9APU47bQTMEYTPcwdpKDtmf1jVhHzNW2cL1WqAxaM9iBb9t5P2fj+RV2YsErUWgQzHD5JwV0uCTEQ==
|
||||||
|
dependencies:
|
||||||
|
"@tokenizer/token" "^0.1.1"
|
||||||
|
"@types/debug" "^4.1.5"
|
||||||
|
peek-readable "^3.1.0"
|
||||||
|
|
||||||
superagent@^3.8.3:
|
superagent@^3.8.3:
|
||||||
version "3.8.3"
|
version "3.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128"
|
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128"
|
||||||
|
@ -7103,6 +7149,14 @@ toidentifier@1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||||
|
|
||||||
|
token-types@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/token-types/-/token-types-2.0.0.tgz#b23618af744818299c6fbf125e0fdad98bab7e85"
|
||||||
|
integrity sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw==
|
||||||
|
dependencies:
|
||||||
|
"@tokenizer/token" "^0.1.0"
|
||||||
|
ieee754 "^1.1.13"
|
||||||
|
|
||||||
tough-cookie@^2.3.3:
|
tough-cookie@^2.3.3:
|
||||||
version "2.4.3"
|
version "2.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
|
||||||
|
|
Loading…
Reference in a new issue