Skip to content

Commit

Permalink
refactor: crop image flow (#1045)
Browse files Browse the repository at this point in the history
* perf: optimize thumbnail generation based on image height

* fix: e2e expect

* fix: e2e expect

* refactor: restructure image slicing process with database persistence

* feat: add repair table attachment thumbnail

* chore: remove useless code
  • Loading branch information
boris-w authored Nov 1, 2024
1 parent 77c889c commit 999bf3c
Show file tree
Hide file tree
Showing 27 changed files with 325 additions and 239 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export const createLocalQueueProvider = (queueName: string): Provider => ({
queueName,
});
},
addBulk: (jobs: JobsOptions[]) => {
jobs.forEach((job) => {
localQueueEventEmitter.emit('handle-listener', job);
});
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { EventJobModule } from '../../event-emitter/event-job/event-job.module';
import {
ATTACHMENTS_CROP_QUEUE,
AttachmentsCropQueueProcessor,
} from './attachments-crop.processor';
import { AttachmentsStorageModule } from './attachments-storage.module';

@Module({
providers: [AttachmentsCropQueueProcessor],
imports: [EventJobModule.registerQueue(ATTACHMENTS_CROP_QUEUE), AttachmentsStorageModule],
exports: [AttachmentsCropQueueProcessor],
})
export class AttachmentsCropModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { InjectQueue, Processor, WorkerHost } from '@nestjs/bullmq';
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '@teable/db-main-prisma';
import { Queue } from 'bullmq';
import type { Job } from 'bullmq';
import { AttachmentsStorageService } from '../attachments/attachments-storage.service';

interface IRecordImageJob {
bucket: string;
token: string;
path: string;
mimetype: string;
height?: number | null;
}

export const ATTACHMENTS_CROP_QUEUE = 'attachments-crop-queue';

@Injectable()
@Processor(ATTACHMENTS_CROP_QUEUE)
export class AttachmentsCropQueueProcessor extends WorkerHost {
private logger = new Logger(AttachmentsCropQueueProcessor.name);

constructor(
private readonly prismaService: PrismaService,
private readonly attachmentsStorageService: AttachmentsStorageService,
@InjectQueue(ATTACHMENTS_CROP_QUEUE) public readonly queue: Queue<IRecordImageJob>
) {
super();
}

public async process(job: Job<IRecordImageJob>) {
const { bucket, token, path, mimetype, height } = job.data;
if (mimetype.startsWith('image/') && height) {
const existingThumbnailPath = await this.prismaService.attachments.findUnique({
where: { token },
select: { thumbnailPath: true },
});
if (existingThumbnailPath?.thumbnailPath) {
this.logger.log(`path(${path}) image already has thumbnail`);
return;
}
const { lgThumbnailPath, smThumbnailPath } =
await this.attachmentsStorageService.cropTableImage(bucket, path, height);
await this.prismaService.attachments.update({
where: {
token,
},
data: {
thumbnailPath: JSON.stringify({
lg: lgThumbnailPath,
sm: smThumbnailPath,
}),
},
});
this.logger.log(`path(${path}) crop thumbnails success`);
return;
}
this.logger.log(`path(${path}) is not a image`);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { ATTACHMENT_LG_THUMBNAIL_HEIGHT, ATTACHMENT_SM_THUMBNAIL_HEIGHT } from '@teable/core';
import { PrismaService } from '@teable/db-main-prisma';
import { UploadType } from '@teable/openapi';
import { CacheService } from '../../cache/cache.service';
Expand All @@ -9,9 +8,9 @@ import { Events } from '../../event-emitter/events';
import {
generateTableThumbnailPath,
getTableThumbnailToken,
} from '../../utils/generate-table-thumbnail-path';
} from '../../utils/generate-thumbnail-path';
import { second } from '../../utils/second';
import { Timing } from '../../utils/timing';
import { ATTACHMENT_LG_THUMBNAIL_HEIGHT, ATTACHMENT_SM_THUMBNAIL_HEIGHT } from './constant';
import StorageAdapter from './plugins/adapter';
import { InjectStorageAdapter } from './plugins/storage';
import type { IRespHeaders } from './plugins/types';
Expand Down Expand Up @@ -81,10 +80,6 @@ export class AttachmentsStorageService {
let url = previewCache?.url;
if (!url) {
url = await this.storageAdapter.getPreviewUrl(bucket, path, expiresIn, respHeaders);
if (!url) {
this.logger.error(`Invalid token: ${token}`);
return '';
}
await this.cacheService.set(
`attachment:preview:${token}`,
{
Expand All @@ -97,61 +92,41 @@ export class AttachmentsStorageService {
return url;
}

private async getTableThumbnailUrl(
path: string,
token: string,
expiresIn: number = this.urlExpireIn
) {
const previewCache = await this.cacheService.get(`attachment:preview:${token}`);
if (previewCache?.url) {
return previewCache.url;
}
const url = await this.storageAdapter.getPreviewUrl(
async getTableThumbnailUrl(path: string, mimetype: string) {
return this.getPreviewUrlByPath(
StorageAdapter.getBucket(UploadType.Table),
path,
expiresIn
getTableThumbnailToken(path),
undefined,
{
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': mimetype,
}
);
if (url) {
await this.cacheService.set(
`attachment:preview:${token}`,
{
url,
expiresIn,
},
expiresIn
);
}
return url;
}

@Timing()
async getTableAttachmentThumbnailUrl(path: string, selected?: ('sm' | 'lg')[]) {
async cropTableImage(bucket: string, path: string, height: number) {
const { smThumbnailPath, lgThumbnailPath } = generateTableThumbnailPath(path);
const smThumbnailUrl = selected?.includes('sm')
? await this.getTableThumbnailUrl(smThumbnailPath, getTableThumbnailToken(smThumbnailPath))
: undefined;
const lgThumbnailUrl = selected?.includes('lg')
? await this.getTableThumbnailUrl(lgThumbnailPath, getTableThumbnailToken(lgThumbnailPath))
: undefined;
return { smThumbnailUrl, lgThumbnailUrl };
}

async cropTableImage(bucket: string, path: string) {
const { smThumbnailPath, lgThumbnailPath } = generateTableThumbnailPath(path);
const cutSmThumbnailPath = await this.storageAdapter.cropImage(
bucket,
path,
undefined,
ATTACHMENT_SM_THUMBNAIL_HEIGHT,
smThumbnailPath
);
const cutLgThumbnailPath = await this.storageAdapter.cropImage(
bucket,
path,
undefined,
ATTACHMENT_LG_THUMBNAIL_HEIGHT,
lgThumbnailPath
);
const cutSmThumbnailPath =
height > ATTACHMENT_SM_THUMBNAIL_HEIGHT
? await this.storageAdapter.cropImage(
bucket,
path,
undefined,
ATTACHMENT_SM_THUMBNAIL_HEIGHT,
smThumbnailPath
)
: undefined;
const cutLgThumbnailPath =
height > ATTACHMENT_LG_THUMBNAIL_HEIGHT
? await this.storageAdapter.cropImage(
bucket,
path,
undefined,
ATTACHMENT_LG_THUMBNAIL_HEIGHT,
lgThumbnailPath
)
: undefined;
this.eventEmitterService.emit(Events.CROP_IMAGE, {
bucket,
path,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { Module } from '@nestjs/common';
import { EventJobModule } from '../../event-emitter/event-job/event-job.module';
import { AttachmentsStorageModule } from './attachments-storage.module';
import {
ATTACHMENTS_TABLE_QUEUE,
AttachmentsTableQueueProcessor,
} from './attachments-table.processor';
import { AttachmentsTableService } from './attachments-table.service';

@Module({
providers: [AttachmentsTableService, AttachmentsTableQueueProcessor],
imports: [AttachmentsStorageModule, EventJobModule.registerQueue(ATTACHMENTS_TABLE_QUEUE)],
providers: [AttachmentsTableService],
imports: [AttachmentsStorageModule],
exports: [AttachmentsTableService],
})
export class AttachmentsTableModule {}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,11 @@ import { FieldType } from '@teable/core';
import type { IAttachmentCellValue, IRecord } from '@teable/core';
import { PrismaService } from '@teable/db-main-prisma';
import type { Prisma } from '@teable/db-main-prisma';
import { omit } from 'lodash';
import type { IChangeRecord } from '../../event-emitter/events';
import { AttachmentsTableQueueProcessor } from './attachments-table.processor';

@Injectable()
export class AttachmentsTableService {
constructor(
private readonly prismaService: PrismaService,
private readonly attachmentsTableQueueProcessor: AttachmentsTableQueueProcessor
) {}
constructor(private readonly prismaService: PrismaService) {}

private createUniqueKey(
tableId: string,
Expand All @@ -32,14 +27,7 @@ export class AttachmentsTableService {

async createRecords(userId: string, tableId: string, records: IRecord[]) {
const fieldRaws = await this.getAttachmentFields(tableId);
const newAttachments: (Prisma.AttachmentsTableCreateInput & {
attachment: {
path: string;
mimetype: string;
width?: number;
height?: number;
};
})[] = [];
const newAttachments: Prisma.AttachmentsTableCreateInput[] = [];
records.forEach((record) => {
const { id: recordId, fields } = record;
fieldRaws.forEach(({ id }) => {
Expand All @@ -53,43 +41,22 @@ export class AttachmentsTableService {
token: attachment.token,
attachmentId: attachment.id,
createdBy: userId,
attachment: {
path: attachment.path,
mimetype: attachment.mimetype,
width: attachment.width,
height: attachment.height,
},
});
});
});
});
await this.prismaService.$tx(async (prisma) => {
for (let i = 0; i < newAttachments.length; i++) {
await prisma.attachmentsTable.create({ data: omit(newAttachments[i], 'attachment') });
const { path, mimetype, width, height } = newAttachments[i].attachment;
if (mimetype.startsWith('image/') && width && height) {
await this.attachmentsTableQueueProcessor.queue.add(`crop_image_${tableId}`, {
tableId,
attachmentItem: {
path,
mimetype,
},
});
}
await prisma.attachmentsTable.create({
data: newAttachments[i],
});
}
});
}

async updateRecords(userId: string, tableId: string, records: IChangeRecord[]) {
const fieldRaws = await this.getAttachmentFields(tableId);
const newAttachments: (Prisma.AttachmentsTableCreateInput & {
attachment: {
path: string;
mimetype: string;
width?: number;
height?: number;
};
})[] = [];
const newAttachments: Prisma.AttachmentsTableCreateInput[] = [];
const needDelete: {
tableId: string;
fieldId: string;
Expand Down Expand Up @@ -141,12 +108,6 @@ export class AttachmentsTableService {
token: attachment.token,
attachmentId: attachment.id,
createdBy: userId,
attachment: {
path: attachment.path,
mimetype: attachment.mimetype,
width: attachment.width,
height: attachment.height,
},
});
}
});
Expand All @@ -156,17 +117,9 @@ export class AttachmentsTableService {
await this.prismaService.$tx(async (prisma) => {
needDelete.length && (await this.delete(needDelete));
for (let i = 0; i < newAttachments.length; i++) {
await prisma.attachmentsTable.create({ data: omit(newAttachments[i], 'attachment') });
const { path, mimetype, width, height } = newAttachments[i].attachment;
if (mimetype.startsWith('image/') && width && height) {
await this.attachmentsTableQueueProcessor.queue.add(`crop_image_${tableId}`, {
tableId,
attachmentItem: {
path,
mimetype,
},
});
}
await prisma.attachmentsTable.create({
data: newAttachments[i],
});
}
});
}
Expand Down
Loading

0 comments on commit 999bf3c

Please sign in to comment.