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',
}

View File

@@ -0,0 +1,29 @@
import { Body, Controller, Post, UseGuards, ValidationPipe } from '@nestjs/common';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AuthService } from './auth.service';
import { LoginCredentialDto } from './dto/login-credential.dto';
import { SignUpDto } from './dto/sign-up.dto';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('/login')
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) {
return await this.authService.login(loginCredential);
}
@Post('/signUp')
async signUp(@Body(ValidationPipe) signUpCrendential: SignUpDto) {
return await this.authService.signUp(signUpCrendential);
}
@UseGuards(JwtAuthGuard)
@Post('/validateToken')
async validateToken(@GetAuthUser() authUser: AuthUserDto) {
return {
authStatus: true,
};
}
}

View File

@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '../user/entities/user.entity';
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
import { JwtModule } from '@nestjs/jwt';
import { jwtConfig } from '../../config/jwt.config';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)],
controllers: [AuthController],
providers: [AuthService, ImmichJwtService],
})
export class AuthModule {}

View File

@@ -0,0 +1,84 @@
import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserEntity } from '../user/entities/user.entity';
import { LoginCredentialDto } from './dto/login-credential.dto';
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
import { JwtPayloadDto } from './dto/jwt-payload.dto';
import { SignUpDto } from './dto/sign-up.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
private immichJwtService: ImmichJwtService,
) {}
private async validateUser(loginCredential: LoginCredentialDto): Promise<UserEntity> {
const user = await this.userRepository.findOne(
{ email: loginCredential.email },
{ select: ['id', 'email', 'password', 'salt'] },
);
const isAuthenticated = await this.validatePassword(user.password, loginCredential.password, user.salt);
if (user && isAuthenticated) {
return user;
}
return null;
}
public async login(loginCredential: LoginCredentialDto) {
const validatedUser = await this.validateUser(loginCredential);
if (!validatedUser) {
throw new BadRequestException('Incorrect email or password');
}
const payload = new JwtPayloadDto(validatedUser.id, validatedUser.email);
return {
accessToken: await this.immichJwtService.generateToken(payload),
userId: validatedUser.id,
userEmail: validatedUser.email,
};
}
public async signUp(signUpCrendential: SignUpDto) {
const registerUser = await this.userRepository.findOne({ email: signUpCrendential.email });
if (registerUser) {
throw new BadRequestException('User exist');
}
const newUser = new UserEntity();
newUser.email = signUpCrendential.email;
newUser.salt = await bcrypt.genSalt();
newUser.password = await this.hashPassword(signUpCrendential.password, newUser.salt);
try {
const savedUser = await this.userRepository.save(newUser);
return {
id: savedUser.id,
email: savedUser.email,
createdAt: savedUser.createdAt,
};
} catch (e) {
Logger.error('e', 'signUp');
throw new InternalServerErrorException('Failed to register new user');
}
}
private async hashPassword(password: string, salt: string): Promise<string> {
return bcrypt.hash(password, salt);
}
private async validatePassword(hasedPassword: string, inputPassword: string, salt: string): Promise<boolean> {
const hash = await bcrypt.hash(inputPassword, salt);
return hash === hasedPassword;
}
}

View File

@@ -0,0 +1,9 @@
export class JwtPayloadDto {
constructor(userId: string, email: string) {
this.userId = userId;
this.email = email;
}
userId: string;
email: string;
}

View File

@@ -0,0 +1,9 @@
import { IsNotEmpty } from 'class-validator';
export class LoginCredentialDto {
@IsNotEmpty()
email: string;
@IsNotEmpty()
password: string;
}

View File

@@ -0,0 +1,9 @@
import { IsNotEmpty } from 'class-validator';
export class SignUpDto {
@IsNotEmpty()
email: string;
@IsNotEmpty()
password: string;
}

View File

@@ -0,0 +1,22 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe } from '@nestjs/common';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { DeviceInfoService } from './device-info.service';
import { CreateDeviceInfoDto } from './dto/create-device-info.dto';
import { UpdateDeviceInfoDto } from './dto/update-device-info.dto';
@UseGuards(JwtAuthGuard)
@Controller('device-info')
export class DeviceInfoController {
constructor(private readonly deviceInfoService: DeviceInfoService) {}
@Post()
async create(@Body(ValidationPipe) createDeviceInfoDto: CreateDeviceInfoDto, @GetAuthUser() authUser: AuthUserDto) {
return await this.deviceInfoService.create(createDeviceInfoDto, authUser);
}
@Patch()
async update(@Body(ValidationPipe) updateDeviceInfoDto: UpdateDeviceInfoDto, @GetAuthUser() authUser: AuthUserDto) {
return this.deviceInfoService.update(authUser.id, updateDeviceInfoDto);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { DeviceInfoService } from './device-info.service';
import { DeviceInfoController } from './device-info.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DeviceInfoEntity } from './entities/device-info.entity';
@Module({
imports: [TypeOrmModule.forFeature([DeviceInfoEntity])],
controllers: [DeviceInfoController],
providers: [DeviceInfoService],
})
export class DeviceInfoModule {}

View File

@@ -0,0 +1,63 @@
import { BadRequestException, HttpCode, Injectable, Logger, Res } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateDeviceInfoDto } from './dto/create-device-info.dto';
import { UpdateDeviceInfoDto } from './dto/update-device-info.dto';
import { DeviceInfoEntity } from './entities/device-info.entity';
@Injectable()
export class DeviceInfoService {
constructor(
@InjectRepository(DeviceInfoEntity)
private deviceRepository: Repository<DeviceInfoEntity>,
) {}
async create(createDeviceInfoDto: CreateDeviceInfoDto, authUser: AuthUserDto) {
const res = await this.deviceRepository.findOne({
deviceId: createDeviceInfoDto.deviceId,
userId: authUser.id,
});
if (res) {
Logger.log('Device Info Exist', 'createDeviceInfo');
return res;
}
const deviceInfo = new DeviceInfoEntity();
deviceInfo.deviceId = createDeviceInfoDto.deviceId;
deviceInfo.deviceType = createDeviceInfoDto.deviceType;
deviceInfo.userId = authUser.id;
try {
return await this.deviceRepository.save(deviceInfo);
} catch (e) {
Logger.error('Error creating new device info', 'createDeviceInfo');
}
}
async update(userId: string, updateDeviceInfoDto: UpdateDeviceInfoDto) {
const deviceInfo = await this.deviceRepository.findOne({
where: { deviceId: updateDeviceInfoDto.deviceId, userId: userId },
});
if (!deviceInfo) {
throw new BadRequestException('Device Not Found');
}
const res = await this.deviceRepository.update(
{
id: deviceInfo.id,
},
updateDeviceInfoDto,
);
if (res.affected == 1) {
return await this.deviceRepository.findOne({
where: { deviceId: updateDeviceInfoDto.deviceId, userId: userId },
});
} else {
throw new BadRequestException('Bad Request');
}
}
}

View File

@@ -0,0 +1,13 @@
import { IsNotEmpty, IsOptional } from 'class-validator';
import { DeviceType } from '../entities/device-info.entity';
export class CreateDeviceInfoDto {
@IsNotEmpty()
deviceId: string;
@IsNotEmpty()
deviceType: DeviceType;
@IsOptional()
isAutoBackup: boolean;
}

View File

@@ -0,0 +1,6 @@
import { PartialType } from '@nestjs/mapped-types';
import { IsOptional } from 'class-validator';
import { DeviceType } from '../entities/device-info.entity';
import { CreateDeviceInfoDto } from './create-device-info.dto';
export class UpdateDeviceInfoDto extends PartialType(CreateDeviceInfoDto) {}

View File

@@ -0,0 +1,32 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
@Entity('device_info')
@Unique(['userId', 'deviceId'])
export class DeviceInfoEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
userId: string;
@Column()
deviceId: string;
@Column()
deviceType: DeviceType;
@Column({ nullable: true })
notificationToken: string;
@CreateDateColumn()
createdAt: string;
@Column({ type: 'bool', default: false })
isAutoBackup: boolean;
}
export enum DeviceType {
IOS = 'IOS',
ANDROID = 'ANDROID',
WEB = 'WEB',
}

View File

@@ -0,0 +1,9 @@
export class ServerInfoDto {
diskSize: String;
diskUse: String;
diskAvailable: String;
diskSizeRaw: number;
diskUseRaw: number;
diskAvailableRaw: number;
diskUsagePercentage: number;
}

View File

@@ -0,0 +1,19 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { ServerInfoService } from './server-info.service';
@Controller('server-info')
export class ServerInfoController {
constructor(private readonly serverInfoService: ServerInfoService) {}
@Get()
async getServerInfo() {
return await this.serverInfoService.getServerInfo();
}
@Get('/ping')
async getServerPulse() {
return {
res: 'pong',
};
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ServerInfoService } from './server-info.service';
import { ServerInfoController } from './server-info.controller';
@Module({
controllers: [ServerInfoController],
providers: [ServerInfoService]
})
export class ServerInfoModule {}

View File

@@ -0,0 +1,54 @@
import { Injectable } from '@nestjs/common';
import systemInformation from 'systeminformation';
import { ServerInfoDto } from './dto/server-info.dto';
@Injectable()
export class ServerInfoService {
constructor() {}
async getServerInfo() {
const res = await systemInformation.fsSize();
const size = res[0].size;
const used = res[0].used;
const available = res[0].available;
const percentageUsage = res[0].use;
const serverInfo = new ServerInfoDto();
serverInfo.diskAvailable = this.getHumanReadableString(available);
serverInfo.diskSize = this.getHumanReadableString(size);
serverInfo.diskUse = this.getHumanReadableString(used);
serverInfo.diskAvailableRaw = available;
serverInfo.diskSizeRaw = size;
serverInfo.diskUseRaw = used;
serverInfo.diskUsagePercentage = percentageUsage;
return serverInfo;
}
private getHumanReadableString(sizeInByte: number) {
const pepibyte = 1.126 * Math.pow(10, 15);
const tebibyte = 1.1 * Math.pow(10, 12);
const gibibyte = 1.074 * Math.pow(10, 9);
const mebibyte = 1.049 * Math.pow(10, 6);
const kibibyte = 1024;
// Pebibyte
if (sizeInByte >= pepibyte) {
// Pe
return `${(sizeInByte / pepibyte).toFixed(1)}PB`;
} else if (tebibyte <= sizeInByte && sizeInByte < pepibyte) {
// Te
return `${(sizeInByte / tebibyte).toFixed(1)}TB`;
} else if (gibibyte <= sizeInByte && sizeInByte < tebibyte) {
// Gi
return `${(sizeInByte / gibibyte).toFixed(1)}GB`;
} else if (mebibyte <= sizeInByte && sizeInByte < gibibyte) {
// Mega
return `${(sizeInByte / mebibyte).toFixed(1)}MB`;
} else if (kibibyte <= sizeInByte && sizeInByte < mebibyte) {
// Kibi
return `${(sizeInByte / kibibyte).toFixed(1)}KB`;
} else {
return `${sizeInByte}B`;
}
}
}

View File

@@ -0,0 +1 @@
export class CreateUserDto {}

View File

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

View File

@@ -0,0 +1,19 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('users')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
email: string;
@Column({ select: false })
password: string;
@Column({ select: false })
salt: string;
@CreateDateColumn()
createdAt: string;
}

View File

@@ -0,0 +1,34 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
findAll() {
return this.userService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserEntity } from './entities/user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(UserEntity)
private userRepository: Repository<UserEntity>,
) {}
create(createUserDto: CreateUserDto) {
return 'This action adds a new user';
}
async findAll() {
try {
return 'welcome';
// return await this.userRepository.find();
// return await this.userRepository.query('select * from users');
} catch (e) {
console.log(e);
}
// return 'helloworld';
}
findOne(id: number) {
return `This action returns a #${id} user`;
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
}
remove(id: number) {
return `This action removes a #${id} user`;
}
}

49
server/src/app.module.ts Normal file
View File

@@ -0,0 +1,49 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { databaseConfig } from './config/database.config';
import { UserModule } from './api-v1/user/user.module';
import { AssetModule } from './api-v1/asset/asset.module';
import { AuthModule } from './api-v1/auth/auth.module';
import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module';
import { JwtModule } from '@nestjs/jwt';
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
import { AppLoggerMiddleware } from './middlewares/app-logger.middleware';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { immichAppConfig } from './config/app.config';
import { BullModule } from '@nestjs/bull';
import { ImageOptimizeModule } from './modules/image-optimize/image-optimize.module';
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
@Module({
imports: [
ConfigModule.forRoot(immichAppConfig),
TypeOrmModule.forRoot(databaseConfig),
UserModule,
AssetModule,
AuthModule,
ImmichJwtModule,
DeviceInfoModule,
BullModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
redis: {
host: configService.get('REDIS_HOST'),
port: configService.get('REDIS_PORT'),
password: configService.get('REDIS_PASSWORD'),
},
}),
inject: [ConfigService],
}),
ImageOptimizeModule,
ServerInfoModule,
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
// consumer.apply(AppLoggerMiddleware).forRoutes('*');
}
}

View File

@@ -0,0 +1,19 @@
import { ConfigModuleOptions } from '@nestjs/config';
import Joi from 'joi';
export const immichAppConfig: ConfigModuleOptions = {
envFilePath: '.env',
isGlobal: true,
validationSchema: Joi.object({
NODE_ENV: Joi.string().required().valid('development', 'production', 'staging').default('development'),
DB_HOST: Joi.string().required(),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_DATABASE: Joi.string().required(),
UPLOAD_LOCATION: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
REDIS_HOST: Joi.string().required(),
REDIS_PORT: Joi.string().required(),
REDIS_PASSWORD: Joi.string().required(),
}),
};

View File

@@ -0,0 +1,27 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import dotenv from 'dotenv';
const result = dotenv.config();
if (result.error) {
console.log(result.error);
}
export const databaseConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: process.env.DB_HOST,
port: 5432,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: [__dirname + '/../**/*.entity.{js,ts}'],
synchronize: true,
// logging: true,
// logger: 'advanced-console',
// ssl: process.env.NODE_ENV == 'production',
// extra: {
// ssl: {
// rejectUnauthorized: false,
// },
// },
};

View File

@@ -0,0 +1,7 @@
import { JwtModuleOptions } from '@nestjs/jwt';
import { jwtSecret } from '../constants/jwt.constant';
export const jwtConfig: JwtModuleOptions = {
secret: jwtSecret,
signOptions: { expiresIn: '36500d' },
};

View File

@@ -0,0 +1,38 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { existsSync, mkdirSync } from 'fs';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { Request } from 'express';
export const multerConfig = {
dest: process.env.UPLOAD_LOCATION,
};
export const multerOption: MulterOptions = {
fileFilter: (req: Request, file: any, cb: any) => {
if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime)$/)) {
cb(null, true);
} else {
cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
}
},
storage: diskStorage({
destination: (req: Request, file: Express.Multer.File, cb: any) => {
const uploadPath = multerConfig.dest;
const userPath = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
if (!existsSync(userPath)) {
mkdirSync(userPath, { recursive: true });
}
cb(null, userPath);
},
filename: (req: Request, file: Express.Multer.File, cb: any) => {
cb(null, `${file.originalname}${req.body['fileExtension']}`);
},
}),
};

View File

@@ -0,0 +1 @@
export const jwtSecret = process.env.JWT_SECRET;

View File

@@ -0,0 +1,21 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { UserEntity } from '../api-v1/user/entities/user.entity';
// import { AuthUserDto } from './dto/auth-user.dto';
export class AuthUserDto {
id: string;
email: string;
}
export const GetAuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => {
const req = ctx.switchToHttp().getRequest();
const { id, email } = req.user as UserEntity;
const authUser: any = {
id: id.toString(),
email,
};
return authUser;
});

11
server/src/main.ts Normal file
View File

@@ -0,0 +1,11 @@
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.set('trust proxy');
await app.listen(3000);
}
bootstrap();

View File

@@ -0,0 +1,22 @@
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class AppLoggerMiddleware implements NestMiddleware {
private logger = new Logger('HTTP');
use(request: Request, response: Response, next: NextFunction): void {
const { ip, method, path: url, baseUrl } = request;
const userAgent = request.get('user-agent') || '';
response.on('close', () => {
const { statusCode } = response;
const contentLength = response.get('content-length');
this.logger.log(`${method} ${baseUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`);
});
next();
}
}

View File

@@ -0,0 +1,36 @@
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';
import { AssetModule } from '../../api-v1/asset/asset.module';
import { AssetService } from '../../api-v1/asset/asset.service';
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import { ImageOptimizeProcessor } from './image-optimize.processor';
import { ImageOptimizeService } from './image-optimize.service';
import { MachineLearningProcessor } from './machine-learning.processor';
@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]),
],
providers: [ImageOptimizeService, ImageOptimizeProcessor, MachineLearningProcessor],
exports: [ImageOptimizeService],
})
export class ImageOptimizeModule {}

View File

@@ -0,0 +1,60 @@
import { InjectQueue, Process, Processor } from '@nestjs/bull';
import { InjectRepository } from '@nestjs/typeorm';
import { Job, Queue } from 'bull';
import { Repository } from 'typeorm';
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import sharp from 'sharp';
import fs, { existsSync, mkdirSync } from 'fs';
import { ConfigService } from '@nestjs/config';
import { randomUUID } from 'crypto';
@Processor('image')
export class ImageOptimizeProcessor {
constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectQueue('machine-learning') private machineLearningQueue: Queue,
private configService: ConfigService,
) {}
@Process('optimize')
async handleOptimization(job: Job) {
const { savedAsset }: { savedAsset: AssetEntity } = job.data;
const basePath = this.configService.get('UPLOAD_LOCATION');
const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
// Create folder for thumb image if not exist
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
if (!existsSync(resizeDir)) {
mkdirSync(resizeDir, { recursive: true });
}
fs.readFile(savedAsset.originalPath, (err, data) => {
if (err) {
console.error('Error Reading File');
}
sharp(data)
.resize(512, 512, { fit: 'outside' })
.toFile(resizePath, async (err, info) => {
if (err) {
console.error('Error resizing file ', err);
}
await this.assetRepository.update(savedAsset, { resizePath: resizePath });
const jobb = await this.machineLearningQueue.add(
'object-detection',
{
resizePath,
},
{ jobId: randomUUID() },
);
});
});
return 'ok';
}
}

View File

@@ -0,0 +1,29 @@
import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import { randomUUID } from 'crypto';
import { join } from 'path';
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
@Injectable()
export class ImageOptimizeService {
constructor(
@InjectQueue('image') private imageQueue: Queue,
@InjectQueue('machine-learning') private machineLearningQueue: Queue,
) {}
public async resizeImage(savedAsset: AssetEntity) {
const job = await this.imageQueue.add(
'optimize',
{
savedAsset,
},
{ jobId: randomUUID() },
);
return {
jobId: job.id,
};
}
}

View File

@@ -0,0 +1,39 @@
import { Process, Processor } from '@nestjs/bull';
import { InjectRepository } from '@nestjs/typeorm';
import { Job } from 'bull';
import { Repository } from 'typeorm';
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import sharp from 'sharp';
import fs, { existsSync, mkdirSync } from 'fs';
import { ConfigService } from '@nestjs/config';
import * as tfnode from '@tensorflow/tfjs-node';
import * as cocoSsd from '@tensorflow-models/coco-ssd';
@Processor('machine-learning')
export class MachineLearningProcessor {
constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
private configService: ConfigService,
) {}
@Process('object-detection')
async handleOptimization(job: Job) {
try {
const { resizePath }: { resizePath: string } = job.data;
const image = fs.readFileSync(resizePath);
const decodedImage = tfnode.node.decodeImage(image, 3) as tfnode.Tensor3D;
const model = await cocoSsd.load();
const predictions = await model.detect(decodedImage);
console.log('start predictions ------------------ ');
for (var result of predictions) {
console.log(`Found ${result.class} with score ${result.score}`);
}
console.log('end predictions ------------------ ');
return 'ok';
} catch (e) {
console.log('Error object detection ', e);
}
}
}

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { ImmichJwtService } from './immich-jwt.service';
import { JwtModule } from '@nestjs/jwt';
import { jwtConfig } from '../../config/jwt.config';
import { JwtStrategy } from './strategies/jwt.strategy';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '../../api-v1/user/entities/user.entity';
@Module({
imports: [JwtModule.register(jwtConfig), TypeOrmModule.forFeature([UserEntity])],
providers: [ImmichJwtService, JwtStrategy],
exports: [ImmichJwtService],
})
export class ImmichJwtModule {}

View File

@@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto';
@Injectable()
export class ImmichJwtService {
constructor(private jwtService: JwtService) {}
public async generateToken(payload: JwtPayloadDto) {
return this.jwtService.sign({
...payload,
});
}
}

View File

@@ -0,0 +1,33 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Repository } from 'typeorm';
import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto';
import { UserEntity } from '../../../api-v1/user/entities/user.entity';
import { jwtSecret } from '../../../constants/jwt.constant';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
@InjectRepository(UserEntity)
private usersRepository: Repository<UserEntity>,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtSecret,
});
}
async validate(payload: JwtPayloadDto) {
const { userId } = payload;
const user = await this.usersRepository.findOne({ id: userId });
if (!user) {
throw new UnauthorizedException('Failure to validate JWT payload');
}
return user;
}
}