Transfer repository from Gitlab

This commit is contained in:
Tran, Alex
2022-02-03 10:06:44 -06:00
parent af2efbdbbd
commit 568cc243f0
177 changed files with 13300 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
import {
Controller,
Post,
UseInterceptors,
UploadedFiles,
Body,
UseGuards,
Get,
Param,
ValidationPipe,
StreamableFile,
Response,
Query,
Logger,
UploadedFile,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AssetService } from './asset.service';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { multerOption } from '../../config/multer-option.config';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto';
import { createReadStream } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto';
import { ImageOptimizeService } from '../../modules/image-optimize/image-optimize.service';
import { AssetType } from './entities/asset.entity';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
@UseGuards(JwtAuthGuard)
@Controller('asset')
export class AssetController {
constructor(
private readonly assetService: AssetService,
private readonly imageOptimizeService: ImageOptimizeService,
) {}
@Post('upload')
@UseInterceptors(FilesInterceptor('files', 30, multerOption))
async uploadFile(
@GetAuthUser() authUser,
@UploadedFiles() files: Express.Multer.File[],
@Body(ValidationPipe) assetInfo: CreateAssetDto,
) {
files.forEach(async (file) => {
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
if (savedAsset && savedAsset.type == AssetType.IMAGE) {
await this.imageOptimizeService.resizeImage(savedAsset);
}
});
return 'ok';
}
@Get('/file')
async serveFile(
@GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res,
@Query(ValidationPipe) query: ServeFileDto,
): Promise<StreamableFile> {
let file = null;
const asset = await this.assetService.findOne(authUser, query.did, query.aid);
res.set({
'Content-Type': asset.mimeType,
});
if (query.isThumb === 'false' || !query.isThumb) {
file = createReadStream(asset.originalPath);
} else {
file = createReadStream(asset.resizePath);
}
return new StreamableFile(file);
}
@Get('/all')
async getAllAssets(@GetAuthUser() authUser: AuthUserDto, @Query(ValidationPipe) query: GetAllAssetQueryDto) {
return await this.assetService.getAllAssets(authUser, query);
}
@Get('/:deviceId')
async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) {
return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
}
}

View File

@@ -0,0 +1,35 @@
import { Module } from '@nestjs/common';
import { AssetService } from './asset.service';
import { AssetController } from './asset.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from './entities/asset.entity';
import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module';
import { ImageOptimizeService } from '../../modules/image-optimize/image-optimize.service';
import { BullModule } from '@nestjs/bull';
@Module({
imports: [
BullModule.registerQueue({
name: 'image',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
BullModule.registerQueue({
name: 'machine-learning',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
TypeOrmModule.forFeature([AssetEntity]),
ImageOptimizeModule,
],
controllers: [AssetController],
providers: [AssetService, ImageOptimizeService],
exports: [],
})
export class AssetModule {}

View File

@@ -0,0 +1,105 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { AssetEntity, AssetType } from './entities/asset.entity';
import _ from 'lodash';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
@Injectable()
export class AssetService {
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
) {}
public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
const asset = new AssetEntity();
asset.deviceAssetId = assetInfo.deviceAssetId;
asset.userId = authUser.id;
asset.deviceId = assetInfo.deviceId;
asset.type = assetInfo.assetType || AssetType.OTHER;
asset.originalPath = path;
asset.createdAt = assetInfo.createdAt;
asset.modifiedAt = assetInfo.modifiedAt;
asset.isFavorite = assetInfo.isFavorite;
asset.lat = assetInfo.lat;
asset.lon = assetInfo.lon;
asset.mimeType = mimeType;
try {
const res = await this.assetRepository.save(asset);
return res;
} catch (e) {
Logger.error(`Error Create New Asset ${e}`, 'createUserAsset');
}
}
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
const rows = await this.assetRepository.find({
where: {
userId: authUser.id,
deviceId: deviceId,
},
select: ['deviceAssetId'],
});
const res = [];
rows.forEach((v) => res.push(v.deviceAssetId));
return res;
}
public async getAllAssets(authUser: AuthUserDto, query: GetAllAssetQueryDto): Promise<GetAllAssetReponseDto> {
// Each page will take 100 images.
try {
const assets = await this.assetRepository
.createQueryBuilder('a')
.where('a."userId" = :userId', { userId: authUser.id })
.andWhere('a."createdAt" < :lastQueryCreatedAt', {
lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(),
})
.orderBy('a."createdAt"::date', 'DESC')
.take(200)
.getMany();
if (assets.length > 0) {
const data = _.groupBy(assets, (a) => new Date(a.createdAt).toISOString().slice(0, 10));
const formattedData = [];
Object.keys(data).forEach((v) => formattedData.push({ date: v, assets: data[v] }));
const response = new GetAllAssetReponseDto();
response.count = assets.length;
response.data = formattedData;
response.nextPageKey = assets[assets.length - 1].createdAt;
return response;
} else {
const response = new GetAllAssetReponseDto();
response.count = 0;
response.data = [];
response.nextPageKey = 'null';
return response;
}
} catch (e) {
Logger.error(e, 'getAllAssets');
}
}
public async findOne(authUser: AuthUserDto, deviceId: string, assetId: string): Promise<AssetEntity> {
const rows = await this.assetRepository.query(
'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."userId" = $2 AND a."deviceId" = $3',
[assetId, authUser.id, deviceId],
);
if (rows.lengh == 0) {
throw new BadRequestException('Not Found');
}
return rows[0] as AssetEntity;
}
}

View File

@@ -0,0 +1,31 @@
import { IsNotEmpty, IsOptional } from 'class-validator';
import { AssetType } from '../entities/asset.entity';
export class CreateAssetDto {
@IsNotEmpty()
deviceAssetId: string;
@IsNotEmpty()
deviceId: string;
@IsNotEmpty()
assetType: AssetType;
@IsNotEmpty()
createdAt: string;
@IsNotEmpty()
modifiedAt: string;
@IsNotEmpty()
isFavorite: boolean;
@IsNotEmpty()
fileExtension: string;
@IsOptional()
lat: string;
@IsOptional()
lon: string;
}

View File

@@ -0,0 +1,6 @@
import { IsNotEmpty, IsOptional } from 'class-validator';
export class GetAllAssetQueryDto {
@IsOptional()
nextPageKey: string;
}

View File

@@ -0,0 +1,7 @@
import { AssetEntity } from '../entities/asset.entity';
export class GetAllAssetReponseDto {
data: Array<{ date: string; assets: Array<AssetEntity> }>;
count: number;
nextPageKey: string;
}

View File

@@ -0,0 +1,6 @@
import { IsNotEmpty } from 'class-validator';
class GetAssetDto {
@IsNotEmpty()
deviceId: string;
}

View File

@@ -0,0 +1,16 @@
import { Transform } from 'class-transformer';
import { IsBoolean, IsBooleanString, IsNotEmpty, IsOptional } from 'class-validator';
export class ServeFileDto {
//assetId
@IsNotEmpty()
aid: string;
//deviceId
@IsNotEmpty()
did: string;
@IsOptional()
@IsBooleanString()
isThumb: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateAssetDto } from './create-asset.dto';
export class UpdateAssetDto extends PartialType(CreateAssetDto) {}

View File

@@ -0,0 +1,54 @@
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn, Unique } from 'typeorm';
@Entity('assets')
@Unique(['deviceAssetId', 'userId', 'deviceId'])
export class AssetEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
deviceAssetId: string;
@Column()
userId: string;
@Column()
deviceId: string;
@Column()
type: AssetType;
@Column()
originalPath: string;
@Column({ nullable: true })
resizePath: string;
@Column()
createdAt: string;
@Column()
modifiedAt: string;
@Column({ type: 'boolean', default: false })
isFavorite: boolean;
@Column({ nullable: true })
description: string;
@Column({ nullable: true })
lat: string;
@Column({ nullable: true })
lon: string;
@Column({ nullable: true })
mimeType: string;
}
export enum AssetType {
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
AUDIO = 'AUDIO',
OTHER = 'OTHER',
}