mirror of https://github.com/nocodb/nocodb
Browse Source
* feat: thumbnail-processor * fix: use signedPath instead of path * fix: rebase * fix: minor fix * fix: url path * fix: use thumbnails for attachments carousel navpull/9075/head
Anbarasu
4 months ago
committed by
GitHub
32 changed files with 874 additions and 316 deletions
@ -1,21 +0,0 @@
|
||||
import { Test } from '@nestjs/testing'; |
||||
import { AttachmentsService } from '../services/attachments.service'; |
||||
import { AttachmentsController } from './attachments.controller'; |
||||
import type { TestingModule } from '@nestjs/testing'; |
||||
|
||||
describe('AttachmentsController', () => { |
||||
let controller: AttachmentsController; |
||||
|
||||
beforeEach(async () => { |
||||
const module: TestingModule = await Test.createTestingModule({ |
||||
controllers: [AttachmentsController], |
||||
providers: [AttachmentsService], |
||||
}).compile(); |
||||
|
||||
controller = module.get<AttachmentsController>(AttachmentsController); |
||||
}); |
||||
|
||||
it('should be defined', () => { |
||||
expect(controller).toBeDefined(); |
||||
}); |
||||
}); |
@ -0,0 +1,152 @@
|
||||
import path from 'path'; |
||||
import { Readable } from 'stream'; |
||||
import { Process, Processor } from '@nestjs/bull'; |
||||
import { Job } from 'bull'; |
||||
import { Logger } from '@nestjs/common'; |
||||
import sharp from 'sharp'; |
||||
import axios from 'axios'; |
||||
import type { AttachmentResType } from 'nocodb-sdk'; |
||||
import type { ThumbnailGeneratorJobData } from '~/interface/Jobs'; |
||||
import { JOBS_QUEUE, JobTypes } from '~/interface/Jobs'; |
||||
import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2'; |
||||
import { PresignedUrl } from '~/models'; |
||||
import { AttachmentsService } from '~/services/attachments.service'; |
||||
|
||||
const attachmentPreviews = ['image/']; |
||||
|
||||
@Processor(JOBS_QUEUE) |
||||
export class ThumbnailGeneratorProcessor { |
||||
constructor(private readonly attachmentsService: AttachmentsService) {} |
||||
|
||||
private logger = new Logger(ThumbnailGeneratorProcessor.name); |
||||
|
||||
@Process(JobTypes.ThumbnailGenerator) |
||||
async job(job: Job<ThumbnailGeneratorJobData>) { |
||||
try { |
||||
const { attachments } = job.data; |
||||
|
||||
const thumbnailPromises = attachments |
||||
.filter((attachment) => |
||||
attachmentPreviews.some((type) => |
||||
attachment.mimetype.startsWith(type), |
||||
), |
||||
) |
||||
.map(async (attachment) => { |
||||
const thumbnail = await this.generateThumbnail(attachment); |
||||
return { |
||||
path: attachment.path ?? attachment.url, |
||||
card_cover: thumbnail?.card_cover, |
||||
small: thumbnail?.small, |
||||
tiny: thumbnail?.tiny, |
||||
}; |
||||
}); |
||||
|
||||
return await Promise.all(thumbnailPromises); |
||||
} catch (error) { |
||||
this.logger.error('Failed to generate thumbnails', error); |
||||
} |
||||
} |
||||
|
||||
private async generateThumbnail( |
||||
attachment: AttachmentResType, |
||||
): Promise<{ [key: string]: string }> { |
||||
const { file, relativePath } = await this.getFileData(attachment); |
||||
|
||||
const thumbnailPaths = { |
||||
card_cover: path.join('nc', 'thumbnails', relativePath, 'card_cover.jpg'), |
||||
small: path.join('nc', 'thumbnails', relativePath, 'small.jpg'), |
||||
tiny: path.join('nc', 'thumbnails', relativePath, 'tiny.jpg'), |
||||
}; |
||||
|
||||
try { |
||||
const storageAdapter = await NcPluginMgrv2.storageAdapter(); |
||||
|
||||
await Promise.all( |
||||
Object.entries(thumbnailPaths).map(async ([size, thumbnailPath]) => { |
||||
let height; |
||||
switch (size) { |
||||
case 'card_cover': |
||||
height = 512; |
||||
break; |
||||
case 'small': |
||||
height = 128; |
||||
break; |
||||
case 'tiny': |
||||
height = 64; |
||||
break; |
||||
default: |
||||
height = 32; |
||||
break; |
||||
} |
||||
|
||||
const resizedImage = await sharp(file, { |
||||
limitInputPixels: false, |
||||
}) |
||||
.resize(undefined, height, { |
||||
fit: sharp.fit.cover, |
||||
kernel: 'lanczos3', |
||||
}) |
||||
.toBuffer(); |
||||
|
||||
await (storageAdapter as any).fileCreateByStream( |
||||
thumbnailPath, |
||||
Readable.from(resizedImage), |
||||
{ |
||||
mimetype: 'image/jpeg', |
||||
}, |
||||
); |
||||
}), |
||||
); |
||||
|
||||
return thumbnailPaths; |
||||
} catch (error) { |
||||
this.logger.error( |
||||
`Failed to generate thumbnails for ${attachment.path}`, |
||||
error, |
||||
); |
||||
} |
||||
} |
||||
|
||||
private async getFileData( |
||||
attachment: AttachmentResType, |
||||
): Promise<{ file: Buffer; relativePath: string }> { |
||||
let url, signedUrl, file; |
||||
let relativePath; |
||||
|
||||
if (attachment.path) { |
||||
relativePath = attachment.path.replace(/^download\//, ''); |
||||
url = await PresignedUrl.getSignedUrl({ |
||||
pathOrUrl: relativePath, |
||||
preview: false, |
||||
filename: attachment.title, |
||||
mimetype: attachment.mimetype, |
||||
}); |
||||
|
||||
const fullPath = await PresignedUrl.getPath(`${url}`); |
||||
const [fpath] = fullPath.split('?'); |
||||
const tempPath = await this.attachmentsService.getFile({ |
||||
path: path.join('nc', 'uploads', fpath), |
||||
}); |
||||
relativePath = fpath; |
||||
file = tempPath.path; |
||||
} else if (attachment.url) { |
||||
relativePath = decodeURI(new URL(attachment.url).pathname); |
||||
|
||||
signedUrl = await PresignedUrl.getSignedUrl({ |
||||
pathOrUrl: relativePath, |
||||
preview: false, |
||||
filename: attachment.title, |
||||
mimetype: attachment.mimetype, |
||||
}); |
||||
|
||||
file = (await axios({ url: signedUrl, responseType: 'arraybuffer' })) |
||||
.data as Buffer; |
||||
} |
||||
|
||||
if (relativePath.startsWith('/nc/uploads/')) { |
||||
relativePath = relativePath.replace('/nc/uploads/', ''); |
||||
} |
||||
|
||||
return { file, relativePath }; |
||||
} |
||||
} |
Loading…
Reference in new issue