Get thumbnail from app (#68)

* Renamed multipart filed name 'files' to 'assetData'. 
* Added an additional field name of 'thumbnailData' to multipart form.
* Implemented upload mechanism for thumbnail directly from the mobile client.
* Removed dead code
* Implemented a version checking mechanism.
This commit is contained in:
Alex
2022-03-22 01:22:04 -05:00
committed by GitHub
parent be72df70fe
commit e407a4fa13
29 changed files with 480 additions and 244 deletions

View File

@@ -16,7 +16,7 @@ import {
} from '@nestjs/common';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AssetService } from './asset.service';
import { FilesInterceptor } from '@nestjs/platform-express';
import { FileFieldsInterceptor, 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';
@@ -29,34 +29,43 @@ import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto';
import { CommunicationGateway } from '../communication/communication.gateway';
@UseGuards(JwtAuthGuard)
@Controller('asset')
export class AssetController {
constructor(
private wsCommunicateionGateway: CommunicationGateway,
private assetService: AssetService,
private assetOptimizeService: AssetOptimizeService,
private backgroundTaskService: BackgroundTaskService,
) {}
@Post('upload')
@UseInterceptors(FilesInterceptor('files', 30, multerOption))
@UseInterceptors(
FileFieldsInterceptor(
[
{ name: 'assetData', maxCount: 1 },
{ name: 'thumbnailData', maxCount: 1 },
],
multerOption,
),
)
async uploadFile(
@GetAuthUser() authUser,
@UploadedFiles() files: Express.Multer.File[],
@UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
@Body(ValidationPipe) assetInfo: CreateAssetDto,
) {
files.forEach(async (file) => {
uploadFiles.assetData.forEach(async (file) => {
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
if (savedAsset && savedAsset.type == AssetType.IMAGE) {
await this.assetOptimizeService.resizeImage(savedAsset);
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
if (uploadFiles.thumbnailData != null) {
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
}
if (savedAsset && savedAsset.type == AssetType.VIDEO) {
await this.assetOptimizeService.getVideoThumbnail(savedAsset, file.originalname);
}
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
});
return 'ok';

View File

@@ -8,9 +8,12 @@ import { AssetOptimizeService } from '../../modules/image-optimize/image-optimiz
import { BullModule } from '@nestjs/bull';
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
import { CommunicationModule } from '../communication/communication.module';
@Module({
imports: [
CommunicationModule,
BullModule.registerQueue({
name: 'optimize',
defaultJobOptions: {

View File

@@ -24,6 +24,12 @@ export class AssetService {
private assetRepository: Repository<AssetEntity>,
) {}
public async updateThumbnailInfo(assetId: string, path: string) {
return await this.assetRepository.update(assetId, {
resizePath: path,
});
}
public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
const asset = new AssetEntity();
asset.deviceAssetId = assetInfo.deviceAssetId;

View File

@@ -5,6 +5,7 @@ import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { ServerInfoService } from './server-info.service';
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
import { serverVersion } from '../../constants/server_version.constant';
@Controller('server-info')
export class ServerInfoController {
@@ -30,4 +31,9 @@ export class ServerInfoController {
mapboxSecret: this.configService.get('MAPBOX_KEY'),
};
}
@Get('/version')
async getServerVersion() {
return serverVersion;
}
}

View File

@@ -23,17 +23,33 @@ export const multerOption: MulterOptions = {
destination: (req: Request, file: Express.Multer.File, cb: any) => {
const uploadPath = multerConfig.dest;
const userPath = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
if (file.fieldname == 'assetData') {
const originalUploadFolder = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
if (!existsSync(userPath)) {
mkdirSync(userPath, { recursive: true });
if (!existsSync(originalUploadFolder)) {
mkdirSync(originalUploadFolder, { recursive: true });
}
cb(null, originalUploadFolder);
} else if (file.fieldname == 'thumbnailData') {
const thumbnailUploadFolder = `${uploadPath}/${req.user['id']}/thumb/${req.body['deviceId']}`;
if (!existsSync(thumbnailUploadFolder)) {
mkdirSync(thumbnailUploadFolder, { recursive: true });
}
cb(null, thumbnailUploadFolder);
}
cb(null, userPath);
},
filename: (req: Request, file: Express.Multer.File, cb: any) => {
cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
// console.log(req, file);
if (file.fieldname == 'assetData') {
cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
} else if (file.fieldname == 'thumbnailData') {
cb(null, `${file.originalname.split('.')[0]}.jpeg`);
}
},
}),
};

View File

@@ -0,0 +1,9 @@
// major.minor.patch+build
// check mobile/pubspec.yml for current release version
export const serverVersion = {
major: 1,
minor: 3,
patch: 0,
build: 0,
};

View File

@@ -41,7 +41,6 @@ export class BackgroundTaskProcessor {
async extractExif(job: Job) {
const { savedAsset, fileName, fileSize }: { savedAsset: AssetEntity; fileName: string; fileSize: number } =
job.data;
const fileBuffer = await readFile(savedAsset.originalPath);
const exifData = await exifr.parse(fileBuffer);

View File

@@ -22,122 +22,4 @@ export class ImageOptimizeProcessor {
private backgroundTaskService: BackgroundTaskService,
) {}
@Process('resize-image')
async resizeUploadedImage(job: Job) {
const { savedAsset }: { savedAsset: AssetEntity } = job.data;
const basePath = APP_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 });
}
readFile(savedAsset.originalPath, async (err, data) => {
if (err) {
console.error('Error Reading File');
}
// Special Assets Type - ios
if (
savedAsset.mimeType == 'image/heic' ||
savedAsset.mimeType == 'image/heif' ||
savedAsset.mimeType == 'image/dng'
) {
let desitnation = '';
if (savedAsset.mimeType == 'image/heic') {
desitnation = resizePath.replace('.HEIC', '.jpeg');
} else if (savedAsset.mimeType == 'image/heif') {
desitnation = resizePath.replace('.HEIF', '.jpeg');
} else if (savedAsset.mimeType == 'image/dng') {
desitnation = resizePath.replace('.DNG', '.jpeg');
}
sharp(data)
.toFormat('jpeg')
.resize(512, 512, { fit: 'outside' })
.toFile(desitnation, async (err, info) => {
if (err) {
console.error('Error resizing file ', err);
return;
}
const res = await this.assetRepository.update(savedAsset, { resizePath: desitnation });
if (res.affected) {
this.wsCommunicateionGateway.server
.to(savedAsset.userId)
.emit('on_upload_success', JSON.stringify(savedAsset));
}
// Tag Image
this.backgroundTaskService.tagImage(desitnation, savedAsset);
});
} else {
sharp(data)
.resize(512, 512, { fit: 'outside' })
.toFile(resizePath, async (err, info) => {
if (err) {
console.error('Error resizing file ', err);
return;
}
const res = await this.assetRepository.update(savedAsset, { resizePath: resizePath });
if (res.affected) {
this.wsCommunicateionGateway.server
.to(savedAsset.userId)
.emit('on_upload_success', JSON.stringify(savedAsset));
}
// Tag Image
this.backgroundTaskService.tagImage(resizePath, savedAsset);
});
}
});
return 'ok';
}
@Process('get-video-thumbnail')
async resizeUploadedVideo(job: Job) {
const { savedAsset, filename }: { savedAsset: AssetEntity; filename: String } = job.data;
const basePath = APP_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 });
}
ffmpeg(savedAsset.originalPath)
.thumbnail({
count: 1,
timestamps: [1],
folder: resizeDir,
filename: `${filename}.png`,
})
.on('end', async (a) => {
const thumbnailPath = `${resizeDir}/${filename}.png`;
const res = await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
if (res.affected) {
this.wsCommunicateionGateway.server
.to(savedAsset.userId)
.emit('on_upload_success', JSON.stringify(savedAsset));
}
// Tag Image
this.backgroundTaskService.tagImage(thumbnailPath, savedAsset);
});
return 'ok';
}
}

View File

@@ -7,33 +7,4 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
@Injectable()
export class AssetOptimizeService {
constructor(@InjectQueue('optimize') private optimizeQueue: Queue) {}
public async resizeImage(savedAsset: AssetEntity) {
const job = await this.optimizeQueue.add(
'resize-image',
{
savedAsset,
},
{ jobId: randomUUID() },
);
return {
jobId: job.id,
};
}
public async getVideoThumbnail(savedAsset: AssetEntity, filename: string) {
const job = await this.optimizeQueue.add(
'get-video-thumbnail',
{
savedAsset,
filename,
},
{ jobId: randomUUID() },
);
return {
jobId: job.id,
};
}
}