mirror of
https://github.com/immich-app/immich.git
synced 2024-12-26 10:50:29 +02:00
refactor(server): upload config (#3252)
This commit is contained in:
parent
382341f550
commit
1064128fde
@ -1,18 +1,21 @@
|
|||||||
import { AssetType } from '@app/infra/entities';
|
import { AssetType } from '@app/infra/entities';
|
||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
assetEntityStub,
|
assetEntityStub,
|
||||||
authStub,
|
authStub,
|
||||||
IAccessRepositoryMock,
|
IAccessRepositoryMock,
|
||||||
newAccessRepositoryMock,
|
newAccessRepositoryMock,
|
||||||
newAssetRepositoryMock,
|
newAssetRepositoryMock,
|
||||||
|
newCryptoRepositoryMock,
|
||||||
newStorageRepositoryMock,
|
newStorageRepositoryMock,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
import { when } from 'jest-when';
|
import { when } from 'jest-when';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
import { ICryptoRepository } from '../crypto';
|
||||||
|
import { mimeTypes } from '../domain.constant';
|
||||||
import { IStorageRepository } from '../storage';
|
import { IStorageRepository } from '../storage';
|
||||||
import { AssetStats, IAssetRepository } from './asset.repository';
|
import { AssetStats, IAssetRepository } from './asset.repository';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService, UploadFieldName } from './asset.service';
|
||||||
import { AssetStatsResponseDto, DownloadResponseDto } from './dto';
|
import { AssetStatsResponseDto, DownloadResponseDto } from './dto';
|
||||||
import { mapAsset } from './response-dto';
|
import { mapAsset } from './response-dto';
|
||||||
|
|
||||||
@ -39,10 +42,62 @@ const statResponse: AssetStatsResponseDto = {
|
|||||||
total: 33,
|
total: 33,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uploadFile = {
|
||||||
|
nullAuth: {
|
||||||
|
authUser: null,
|
||||||
|
fieldName: UploadFieldName.ASSET_DATA,
|
||||||
|
file: {
|
||||||
|
checksum: Buffer.from('checksum', 'utf8'),
|
||||||
|
originalPath: 'upload/admin/image.jpeg',
|
||||||
|
originalName: 'image.jpeg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filename: (fieldName: UploadFieldName, filename: string) => {
|
||||||
|
return {
|
||||||
|
authUser: authStub.admin,
|
||||||
|
fieldName,
|
||||||
|
file: {
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
checksum: Buffer.from('checksum', 'utf8'),
|
||||||
|
originalPath: `upload/admin/${filename}`,
|
||||||
|
originalName: filename,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadTests = [
|
||||||
|
{
|
||||||
|
label: 'asset',
|
||||||
|
fieldName: UploadFieldName.ASSET_DATA,
|
||||||
|
filetypes: Object.keys({ ...mimeTypes.image, ...mimeTypes.video }),
|
||||||
|
invalid: ['.xml', '.html'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'live photo',
|
||||||
|
fieldName: UploadFieldName.LIVE_PHOTO_DATA,
|
||||||
|
filetypes: Object.keys(mimeTypes.video),
|
||||||
|
invalid: ['.xml', '.html', '.jpg', '.jpeg'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'sidecar',
|
||||||
|
fieldName: UploadFieldName.SIDECAR_DATA,
|
||||||
|
filetypes: Object.keys(mimeTypes.sidecar),
|
||||||
|
invalid: ['.xml', '.html', '.jpg', '.jpeg', '.mov', '.mp4'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'profile',
|
||||||
|
fieldName: UploadFieldName.PROFILE_DATA,
|
||||||
|
filetypes: Object.keys(mimeTypes.profile),
|
||||||
|
invalid: ['.xml', '.html', '.cr2', '.arf', '.mov', '.mp4'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe(AssetService.name, () => {
|
describe(AssetService.name, () => {
|
||||||
let sut: AssetService;
|
let sut: AssetService;
|
||||||
let accessMock: IAccessRepositoryMock;
|
let accessMock: IAccessRepositoryMock;
|
||||||
let assetMock: jest.Mocked<IAssetRepository>;
|
let assetMock: jest.Mocked<IAssetRepository>;
|
||||||
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||||
let storageMock: jest.Mocked<IStorageRepository>;
|
let storageMock: jest.Mocked<IStorageRepository>;
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@ -52,8 +107,83 @@ describe(AssetService.name, () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
accessMock = newAccessRepositoryMock();
|
accessMock = newAccessRepositoryMock();
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
sut = new AssetService(accessMock, assetMock, storageMock);
|
sut = new AssetService(accessMock, assetMock, cryptoMock, storageMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canUpload', () => {
|
||||||
|
it('should require an authenticated user', () => {
|
||||||
|
expect(() => sut.canUploadFile(uploadFile.nullAuth)).toThrowError(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { fieldName, filetypes, invalid } of uploadTests) {
|
||||||
|
describe(`${fieldName}`, () => {
|
||||||
|
for (const filetype of filetypes) {
|
||||||
|
it(`should accept ${filetype}`, () => {
|
||||||
|
expect(sut.canUploadFile(uploadFile.filename(fieldName, `asset${filetype}`))).toEqual(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const filetype of invalid) {
|
||||||
|
it(`should reject ${filetype}`, () => {
|
||||||
|
expect(() => sut.canUploadFile(uploadFile.filename(fieldName, `asset${filetype}`))).toThrowError(
|
||||||
|
BadRequestException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUploadFilename', () => {
|
||||||
|
it('should require authentication', () => {
|
||||||
|
expect(() => sut.getUploadFilename(uploadFile.nullAuth)).toThrowError(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be the original extension for asset upload', () => {
|
||||||
|
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.ASSET_DATA, 'image.jpg'))).toEqual(
|
||||||
|
'random-uuid.jpg',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be the mov extension for live photo upload', () => {
|
||||||
|
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.LIVE_PHOTO_DATA, 'image.mp4'))).toEqual(
|
||||||
|
'random-uuid.mov',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be the xmp extension for sidecar upload', () => {
|
||||||
|
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.SIDECAR_DATA, 'image.html'))).toEqual(
|
||||||
|
'random-uuid.xmp',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be the original extension for profile upload', () => {
|
||||||
|
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.PROFILE_DATA, 'image.jpg'))).toEqual(
|
||||||
|
'random-uuid.jpg',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUploadFolder', () => {
|
||||||
|
it('should require authentication', () => {
|
||||||
|
expect(() => sut.getUploadFolder(uploadFile.nullAuth)).toThrowError(UnauthorizedException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return profile for profile uploads', () => {
|
||||||
|
expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.PROFILE_DATA, 'image.jpg'))).toEqual(
|
||||||
|
'upload/profile/admin_id',
|
||||||
|
);
|
||||||
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/profile/admin_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return upload for everything else', () => {
|
||||||
|
expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.ASSET_DATA, 'image.jpg'))).toEqual(
|
||||||
|
'upload/upload/admin_id',
|
||||||
|
);
|
||||||
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/upload/admin_id');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getMapMarkers', () => {
|
describe('getMapMarkers', () => {
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { AssetEntity } from '@app/infra/entities';
|
import { AssetEntity } from '@app/infra/entities';
|
||||||
import { BadRequestException, Inject } from '@nestjs/common';
|
import { BadRequestException, Inject, Logger } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
|
import sanitize from 'sanitize-filename';
|
||||||
import { AccessCore, IAccessRepository, Permission } from '../access';
|
import { AccessCore, IAccessRepository, Permission } from '../access';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
|
import { ICryptoRepository } from '../crypto';
|
||||||
import { mimeTypes } from '../domain.constant';
|
import { mimeTypes } from '../domain.constant';
|
||||||
import { HumanReadableSize, usePagination } from '../domain.util';
|
import { HumanReadableSize, usePagination } from '../domain.util';
|
||||||
import { ImmichReadStream, IStorageRepository } from '../storage';
|
import { ImmichReadStream, IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
||||||
import { IAssetRepository } from './asset.repository';
|
import { IAssetRepository } from './asset.repository';
|
||||||
import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
|
import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
|
||||||
import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto';
|
import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto';
|
||||||
@ -21,6 +23,12 @@ export enum UploadFieldName {
|
|||||||
PROFILE_DATA = 'file',
|
PROFILE_DATA = 'file',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UploadRequest {
|
||||||
|
authUser: AuthUserDto | null;
|
||||||
|
fieldName: UploadFieldName;
|
||||||
|
file: UploadFile;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UploadFile {
|
export interface UploadFile {
|
||||||
checksum: Buffer;
|
checksum: Buffer;
|
||||||
originalPath: string;
|
originalPath: string;
|
||||||
@ -28,16 +36,82 @@ export interface UploadFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AssetService {
|
export class AssetService {
|
||||||
|
private logger = new Logger(AssetService.name);
|
||||||
private access: AccessCore;
|
private access: AccessCore;
|
||||||
|
private storageCore = new StorageCore();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
) {
|
) {
|
||||||
this.access = new AccessCore(accessRepository);
|
this.access = new AccessCore(accessRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
|
||||||
|
this.access.requireUploadAccess(authUser);
|
||||||
|
|
||||||
|
const filename = file.originalName;
|
||||||
|
|
||||||
|
switch (fieldName) {
|
||||||
|
case UploadFieldName.ASSET_DATA:
|
||||||
|
if (mimeTypes.isAsset(filename)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UploadFieldName.LIVE_PHOTO_DATA:
|
||||||
|
if (mimeTypes.isVideo(filename)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UploadFieldName.SIDECAR_DATA:
|
||||||
|
if (mimeTypes.isSidecar(filename)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UploadFieldName.PROFILE_DATA:
|
||||||
|
if (mimeTypes.isProfile(filename)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`Unsupported file type ${filename}`);
|
||||||
|
throw new BadRequestException(`Unsupported file type ${filename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUploadFilename({ authUser, fieldName, file }: UploadRequest): string {
|
||||||
|
this.access.requireUploadAccess(authUser);
|
||||||
|
|
||||||
|
const originalExt = extname(file.originalName);
|
||||||
|
|
||||||
|
const lookup = {
|
||||||
|
[UploadFieldName.ASSET_DATA]: originalExt,
|
||||||
|
[UploadFieldName.LIVE_PHOTO_DATA]: '.mov',
|
||||||
|
[UploadFieldName.SIDECAR_DATA]: '.xmp',
|
||||||
|
[UploadFieldName.PROFILE_DATA]: originalExt,
|
||||||
|
};
|
||||||
|
|
||||||
|
return sanitize(`${this.cryptoRepository.randomUUID()}${lookup[fieldName]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUploadFolder({ authUser, fieldName }: UploadRequest): string {
|
||||||
|
authUser = this.access.requireUploadAccess(authUser);
|
||||||
|
|
||||||
|
let folder = this.storageCore.getFolderLocation(StorageFolder.UPLOAD, authUser.id);
|
||||||
|
if (fieldName === UploadFieldName.PROFILE_DATA) {
|
||||||
|
folder = this.storageCore.getFolderLocation(StorageFolder.PROFILE, authUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storageRepository.mkdirSync(folder);
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||||
return this.assetRepository.getMapMarkers(authUser.id, options);
|
return this.assetRepository.getMapMarkers(authUser.id, options);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
import {
|
import { ICryptoRepository, IJobRepository, IStorageRepository, JobName, mimeTypes } from '@app/domain';
|
||||||
ICryptoRepository,
|
|
||||||
IJobRepository,
|
|
||||||
IStorageRepository,
|
|
||||||
JobName,
|
|
||||||
mimeTypes,
|
|
||||||
UploadFieldName,
|
|
||||||
} from '@app/domain';
|
|
||||||
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
||||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
assetEntityStub,
|
assetEntityStub,
|
||||||
authStub,
|
authStub,
|
||||||
@ -102,57 +95,6 @@ const _getAssetCountByTimeBucket = (): AssetCountByTimeBucket[] => {
|
|||||||
return [result1, result2];
|
return [result1, result2];
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFile = {
|
|
||||||
nullAuth: {
|
|
||||||
authUser: null,
|
|
||||||
fieldName: UploadFieldName.ASSET_DATA,
|
|
||||||
file: {
|
|
||||||
checksum: Buffer.from('checksum', 'utf8'),
|
|
||||||
originalPath: 'upload/admin/image.jpeg',
|
|
||||||
originalName: 'image.jpeg',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
filename: (fieldName: UploadFieldName, filename: string) => {
|
|
||||||
return {
|
|
||||||
authUser: authStub.admin,
|
|
||||||
fieldName,
|
|
||||||
file: {
|
|
||||||
mimeType: 'image/jpeg',
|
|
||||||
checksum: Buffer.from('checksum', 'utf8'),
|
|
||||||
originalPath: `upload/admin/${filename}`,
|
|
||||||
originalName: filename,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadTests = [
|
|
||||||
{
|
|
||||||
label: 'asset',
|
|
||||||
fieldName: UploadFieldName.ASSET_DATA,
|
|
||||||
filetypes: Object.keys({ ...mimeTypes.image, ...mimeTypes.video }),
|
|
||||||
invalid: ['.xml', '.html'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'live photo',
|
|
||||||
fieldName: UploadFieldName.LIVE_PHOTO_DATA,
|
|
||||||
filetypes: Object.keys(mimeTypes.video),
|
|
||||||
invalid: ['.xml', '.html', '.jpg', '.jpeg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'sidecar',
|
|
||||||
fieldName: UploadFieldName.SIDECAR_DATA,
|
|
||||||
filetypes: Object.keys(mimeTypes.sidecar),
|
|
||||||
invalid: ['.xml', '.html', '.jpg', '.jpeg', '.mov', '.mp4'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'profile',
|
|
||||||
fieldName: UploadFieldName.PROFILE_DATA,
|
|
||||||
filetypes: Object.keys(mimeTypes.profile),
|
|
||||||
invalid: ['.xml', '.html', '.cr2', '.arf', '.mov', '.mp4'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('AssetService', () => {
|
describe('AssetService', () => {
|
||||||
let sut: AssetService;
|
let sut: AssetService;
|
||||||
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
|
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
|
||||||
@ -275,80 +217,6 @@ describe('AssetService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canUpload', () => {
|
|
||||||
it('should require an authenticated user', () => {
|
|
||||||
expect(() => sut.canUploadFile(uploadFile.nullAuth)).toThrowError(UnauthorizedException);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const { fieldName, filetypes, invalid } of uploadTests) {
|
|
||||||
describe(`${fieldName}`, () => {
|
|
||||||
for (const filetype of filetypes) {
|
|
||||||
it(`should accept ${filetype}`, () => {
|
|
||||||
expect(sut.canUploadFile(uploadFile.filename(fieldName, `asset${filetype}`))).toEqual(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const filetype of invalid) {
|
|
||||||
it(`should reject ${filetype}`, () => {
|
|
||||||
expect(() => sut.canUploadFile(uploadFile.filename(fieldName, `asset${filetype}`))).toThrowError(
|
|
||||||
BadRequestException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getUploadFilename', () => {
|
|
||||||
it('should require authentication', () => {
|
|
||||||
expect(() => sut.getUploadFilename(uploadFile.nullAuth)).toThrowError(UnauthorizedException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be the original extension for asset upload', () => {
|
|
||||||
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.ASSET_DATA, 'image.jpg'))).toEqual(
|
|
||||||
'random-uuid.jpg',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be the mov extension for live photo upload', () => {
|
|
||||||
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.LIVE_PHOTO_DATA, 'image.mp4'))).toEqual(
|
|
||||||
'random-uuid.mov',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be the xmp extension for sidecar upload', () => {
|
|
||||||
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.SIDECAR_DATA, 'image.html'))).toEqual(
|
|
||||||
'random-uuid.xmp',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be the original extension for profile upload', () => {
|
|
||||||
expect(sut.getUploadFilename(uploadFile.filename(UploadFieldName.PROFILE_DATA, 'image.jpg'))).toEqual(
|
|
||||||
'random-uuid.jpg',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getUploadFolder', () => {
|
|
||||||
it('should require authentication', () => {
|
|
||||||
expect(() => sut.getUploadFolder(uploadFile.nullAuth)).toThrowError(UnauthorizedException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return profile for profile uploads', () => {
|
|
||||||
expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.PROFILE_DATA, 'image.jpg'))).toEqual(
|
|
||||||
'upload/profile/admin_id',
|
|
||||||
);
|
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/profile/admin_id');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return upload for everything else', () => {
|
|
||||||
expect(sut.getUploadFolder(uploadFile.filename(UploadFieldName.ASSET_DATA, 'image.jpg'))).toEqual(
|
|
||||||
'upload/upload/admin_id',
|
|
||||||
);
|
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/upload/admin_id');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('uploadFile', () => {
|
describe('uploadFile', () => {
|
||||||
it('should handle a file upload', async () => {
|
it('should handle a file upload', async () => {
|
||||||
const assetEntity = _getAsset_1();
|
const assetEntity = _getAsset_1();
|
||||||
|
@ -12,9 +12,6 @@ import {
|
|||||||
mapAssetWithoutExif,
|
mapAssetWithoutExif,
|
||||||
mimeTypes,
|
mimeTypes,
|
||||||
Permission,
|
Permission,
|
||||||
StorageCore,
|
|
||||||
StorageFolder,
|
|
||||||
UploadFieldName,
|
|
||||||
UploadFile,
|
UploadFile,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { AssetEntity, AssetType } from '@app/infra/entities';
|
import { AssetEntity, AssetType } from '@app/infra/entities';
|
||||||
@ -30,10 +27,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Response as Res } from 'express';
|
import { Response as Res } from 'express';
|
||||||
import { constants } from 'fs';
|
import { constants } from 'fs';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path, { extname } from 'path';
|
import path from 'path';
|
||||||
import sanitize from 'sanitize-filename';
|
|
||||||
import { QueryFailedError, Repository } from 'typeorm';
|
import { QueryFailedError, Repository } from 'typeorm';
|
||||||
import { UploadRequest } from '../../app.interceptor';
|
|
||||||
import { IAssetRepository } from './asset-repository';
|
import { IAssetRepository } from './asset-repository';
|
||||||
import { AssetCore } from './asset.core';
|
import { AssetCore } from './asset.core';
|
||||||
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
||||||
@ -70,7 +65,6 @@ export class AssetService {
|
|||||||
readonly logger = new Logger(AssetService.name);
|
readonly logger = new Logger(AssetService.name);
|
||||||
private assetCore: AssetCore;
|
private assetCore: AssetCore;
|
||||||
private access: AccessCore;
|
private access: AccessCore;
|
||||||
private storageCore = new StorageCore();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||||
@ -84,69 +78,6 @@ export class AssetService {
|
|||||||
this.access = new AccessCore(accessRepository);
|
this.access = new AccessCore(accessRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
|
|
||||||
this.access.requireUploadAccess(authUser);
|
|
||||||
|
|
||||||
const filename = file.originalName;
|
|
||||||
|
|
||||||
switch (fieldName) {
|
|
||||||
case UploadFieldName.ASSET_DATA:
|
|
||||||
if (mimeTypes.isAsset(filename)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case UploadFieldName.LIVE_PHOTO_DATA:
|
|
||||||
if (mimeTypes.isVideo(filename)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case UploadFieldName.SIDECAR_DATA:
|
|
||||||
if (mimeTypes.isSidecar(filename)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case UploadFieldName.PROFILE_DATA:
|
|
||||||
if (mimeTypes.isProfile(filename)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.error(`Unsupported file type ${filename}`);
|
|
||||||
throw new BadRequestException(`Unsupported file type ${filename}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUploadFilename({ authUser, fieldName, file }: UploadRequest): string {
|
|
||||||
this.access.requireUploadAccess(authUser);
|
|
||||||
|
|
||||||
const originalExt = extname(file.originalName);
|
|
||||||
|
|
||||||
const lookup = {
|
|
||||||
[UploadFieldName.ASSET_DATA]: originalExt,
|
|
||||||
[UploadFieldName.LIVE_PHOTO_DATA]: '.mov',
|
|
||||||
[UploadFieldName.SIDECAR_DATA]: '.xmp',
|
|
||||||
[UploadFieldName.PROFILE_DATA]: originalExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
return sanitize(`${this.cryptoRepository.randomUUID()}${lookup[fieldName]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUploadFolder({ authUser, fieldName }: UploadRequest): string {
|
|
||||||
authUser = this.access.requireUploadAccess(authUser);
|
|
||||||
|
|
||||||
let folder = this.storageCore.getFolderLocation(StorageFolder.UPLOAD, authUser.id);
|
|
||||||
if (fieldName === UploadFieldName.PROFILE_DATA) {
|
|
||||||
folder = this.storageCore.getFolderLocation(StorageFolder.PROFILE, authUser.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.storageRepository.mkdirSync(folder);
|
|
||||||
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async uploadFile(
|
public async uploadFile(
|
||||||
authUser: AuthUserDto,
|
authUser: AuthUserDto,
|
||||||
dto: CreateAssetDto,
|
dto: CreateAssetDto,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AuthUserDto, UploadFieldName, UploadFile } from '@app/domain';
|
import { AssetService, UploadFieldName, UploadFile } from '@app/domain';
|
||||||
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
||||||
import { PATH_METADATA } from '@nestjs/common/constants';
|
import { PATH_METADATA } from '@nestjs/common/constants';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
@ -7,7 +7,6 @@ import { createHash } from 'crypto';
|
|||||||
import { NextFunction, RequestHandler } from 'express';
|
import { NextFunction, RequestHandler } from 'express';
|
||||||
import multer, { diskStorage, StorageEngine } from 'multer';
|
import multer, { diskStorage, StorageEngine } from 'multer';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AssetService } from './api-v1/asset/asset.service';
|
|
||||||
import { AuthRequest } from './app.guard';
|
import { AuthRequest } from './app.guard';
|
||||||
|
|
||||||
export enum Route {
|
export enum Route {
|
||||||
@ -43,12 +42,6 @@ const callbackify = async <T>(fn: (...args: any[]) => T, callback: Callback<T>)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface UploadRequest {
|
|
||||||
authUser: AuthUserDto | null;
|
|
||||||
fieldName: UploadFieldName;
|
|
||||||
file: UploadFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
const asRequest = (req: AuthRequest, file: Express.Multer.File) => {
|
const asRequest = (req: AuthRequest, file: Express.Multer.File) => {
|
||||||
return {
|
return {
|
||||||
authUser: req.user || null,
|
authUser: req.user || null,
|
||||||
|
Loading…
Reference in New Issue
Block a user