Browse Source

feat: thumbnail generation migration

nc-feat/attachment-clean-up
mertmit 4 months ago
parent
commit
8ead328065
  1. 5
      packages/nocodb/src/db/BaseModelSqlv2.ts
  2. 4
      packages/nocodb/src/helpers/migrationJobs.ts
  3. 1
      packages/nocodb/src/interface/Jobs.ts
  4. 6
      packages/nocodb/src/modules/jobs/fallback/fallback-queue.service.ts
  5. 2
      packages/nocodb/src/modules/jobs/jobs.module.ts
  6. 6
      packages/nocodb/src/modules/jobs/jobs/thumbnail-generator/thumbnail-generator.processor.ts
  7. 7
      packages/nocodb/src/modules/jobs/migration-jobs/init-migration-jobs.ts
  8. 28
      packages/nocodb/src/modules/jobs/migration-jobs/nc_job_001_attachment.ts
  9. 201
      packages/nocodb/src/modules/jobs/migration-jobs/nc_job_002_thumbnail.ts

5
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -9572,7 +9572,10 @@ class BaseModelSqlv2 {
async getSource() {
// return this.source if defined or fetch and return
return this.source || (this.source = await Source.get(this.context, this.model.source_id));
return (
this.source ||
(this.source = await Source.get(this.context, this.model.source_id))
);
}
protected async clearFileReferences(args: {

4
packages/nocodb/src/helpers/migrationJobs.ts

@ -1,6 +1,6 @@
import { v4 as uuidv4 } from 'uuid';
import Noco from '~/Noco';
import { MetaTable, RootScopes } from '~/utils/globals';
import { v4 as uuidv4 } from 'uuid';
export const MIGRATION_JOBS_STORE_KEY = 'NC_MIGRATION_JOBS';
@ -73,7 +73,7 @@ export const updateMigrationJobsState = async (
) => {
const ncMeta = Noco.ncMeta;
const migrationJobsState = oldState || await getMigrationJobsState();
const migrationJobsState = oldState || (await getMigrationJobsState());
if (!migrationJobsState) {
const updatedState = {

1
packages/nocodb/src/interface/Jobs.ts

@ -5,6 +5,7 @@ export const JOBS_QUEUE = 'jobs';
export enum MigrationJobTypes {
InitMigrationJobs = 'init-migration-jobs',
Attachment = 'attachment',
Thumbnail = 'thumbnail',
}
export enum JobTypes {

6
packages/nocodb/src/modules/jobs/fallback/fallback-queue.service.ts

@ -14,6 +14,7 @@ import { ThumbnailGeneratorProcessor } from '~/modules/jobs/jobs/thumbnail-gener
import { AttachmentCleanUpProcessor } from '~/modules/jobs/jobs/attachment-clean-up/attachment-clean-up';
import { InitMigrationJobs } from '~/modules/jobs/migration-jobs/init-migration-jobs';
import { AttachmentMigrationProcessor } from '~/modules/jobs/migration-jobs/nc_job_001_attachment';
import { ThumbnailMigrationProcessor } from '~/modules/jobs/migration-jobs/nc_job_002_thumbnail';
export interface Job {
id: string;
@ -43,6 +44,7 @@ export class QueueService {
protected readonly attachmentCleanUpProcessor: AttachmentCleanUpProcessor,
protected readonly initMigrationJobs: InitMigrationJobs,
protected readonly attachmentMigrationProcessor: AttachmentMigrationProcessor,
protected readonly thumbnailMigrationProcessor: ThumbnailMigrationProcessor,
) {
this.emitter.on(JobStatus.ACTIVE, (data: { job: Job }) => {
const job = this.queueMemory.find((job) => job.id === data.job.id);
@ -124,6 +126,10 @@ export class QueueService {
this: this.attachmentMigrationProcessor,
fn: this.attachmentMigrationProcessor.job,
},
[MigrationJobTypes.Thumbnail]: {
this: this.thumbnailMigrationProcessor,
fn: this.thumbnailMigrationProcessor.job,
},
};
async jobWrapper(job: Job) {

2
packages/nocodb/src/modules/jobs/jobs.module.ts

@ -24,6 +24,7 @@ import { AttachmentCleanUpProcessor } from '~/modules/jobs/jobs/attachment-clean
// Migration Jobs
import { InitMigrationJobs } from '~/modules/jobs/migration-jobs/init-migration-jobs';
import { AttachmentMigrationProcessor } from '~/modules/jobs/migration-jobs/nc_job_001_attachment';
import { ThumbnailMigrationProcessor } from '~/modules/jobs/migration-jobs/nc_job_002_thumbnail';
// Jobs Module Related
import { JobsLogService } from '~/modules/jobs/jobs/jobs-log.service';
@ -90,6 +91,7 @@ export const JobsModuleMetadata = {
// Migration Jobs
InitMigrationJobs,
AttachmentMigrationProcessor,
ThumbnailMigrationProcessor,
],
exports: ['JobsService'],
};

6
packages/nocodb/src/modules/jobs/jobs/thumbnail-generator/thumbnail-generator.processor.ts

@ -129,11 +129,7 @@ export class ThumbnailGeneratorProcessor {
let relativePath;
if (attachment.path) {
relativePath = path.join(
'nc',
'uploads',
attachment.path.replace(/^download\//, ''),
);
relativePath = attachment.path.replace(/^download\//, '');
} else if (attachment.url) {
relativePath = decodeURI(new URL(attachment.url).pathname).replace(
/^\/+/,

7
packages/nocodb/src/modules/jobs/migration-jobs/init-migration-jobs.ts

@ -6,11 +6,14 @@ import { JOBS_QUEUE, MigrationJobTypes } from '~/interface/Jobs';
import { IJobsService } from '~/modules/jobs/jobs-service.interface';
import {
getMigrationJobsState,
updateMigrationJobsState,
instanceUuid,
updateMigrationJobsState,
} from '~/helpers/migrationJobs';
const migrationJobsList = [{ version: '1', job: MigrationJobTypes.Attachment }];
const migrationJobsList = [
{ version: '1', job: MigrationJobTypes.Attachment },
// { version: '2', job: MigrationJobTypes.Thumbnail },
];
@Processor(JOBS_QUEUE)
export class InitMigrationJobs {

28
packages/nocodb/src/modules/jobs/migration-jobs/nc_job_001_attachment.ts

@ -77,6 +77,8 @@ export class AttachmentMigrationProcessor {
(table) => {
table.increments('id').primary();
table.string('fk_model_id').notNullable();
table.integer('offset').defaultTo(0);
table.boolean('completed').defaultTo(false);
table.index('fk_model_id');
},
@ -162,7 +164,8 @@ export class AttachmentMigrationProcessor {
'fk_model_id',
ncMeta
.knexConnection(temp_processed_models_table)
.select('fk_model_id'),
.select('fk_model_id')
.where('completed', true),
)
.groupBy(selectFields)
.limit(modelLimit)
@ -229,9 +232,22 @@ export class AttachmentMigrationProcessor {
dbDriver,
});
const processedModel = await ncMeta
.knexConnection(temp_processed_models_table)
.where('fk_model_id', fk_model_id)
.first();
const dataLimit = 10;
let dataOffset = 0;
if (!processedModel) {
await ncMeta
.knexConnection(temp_processed_models_table)
.insert({ fk_model_id, offset: 0 });
} else {
dataOffset = processedModel.offset;
}
// eslint-disable-next-line no-constant-condition
while (true) {
const data = await baseModel.list(
@ -414,11 +430,19 @@ export class AttachmentMigrationProcessor {
);
}
}
// update offset
await ncMeta
.knexConnection(temp_processed_models_table)
.where('fk_model_id', fk_model_id)
.update({ offset: dataOffset });
}
// mark model as processed
await ncMeta
.knexConnection(temp_processed_models_table)
.insert({ fk_model_id });
.where('fk_model_id', fk_model_id)
.update({ completed: true });
}
}

201
packages/nocodb/src/modules/jobs/migration-jobs/nc_job_002_thumbnail.ts

@ -0,0 +1,201 @@
import path from 'path';
import debug from 'debug';
import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { forwardRef, Inject } from '@nestjs/common';
import { JOBS_QUEUE, MigrationJobTypes } from '~/interface/Jobs';
import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2';
import Noco from '~/Noco';
import { IJobsService } from '~/modules/jobs/jobs-service.interface';
import {
setMigrationJobsStallInterval,
updateMigrationJobsState,
} from '~/helpers/migrationJobs';
import mimetypes from '~/utils/mimeTypes';
import { RootScopes } from '~/utils/globals';
import { ThumbnailGeneratorProcessor } from '~/modules/jobs/jobs/thumbnail-generator/thumbnail-generator.processor';
const MIGRATION_JOB_VERSION = '2';
@Processor(JOBS_QUEUE)
export class ThumbnailMigrationProcessor {
private readonly debugLog = debug('nc:migration-jobs:attachment');
constructor(
@Inject(forwardRef(() => 'JobsService'))
private readonly jobsService: IJobsService,
private readonly thumbnailGeneratorProcessor: ThumbnailGeneratorProcessor,
) {}
log = (...msgs: string[]) => {
console.log('[nc_job_002_thumbnail]: ', ...msgs);
};
@Process(MigrationJobTypes.Attachment)
async job(job: Job) {
this.debugLog(`job started for ${job.id}`);
const interval = setMigrationJobsStallInterval();
try {
const ncMeta = Noco.ncMeta;
const temp_file_references_table = 'nc_temp_file_references';
const fileReferencesTableExists =
await ncMeta.knexConnection.schema.hasTable(temp_file_references_table);
if (!fileReferencesTableExists) {
// create temp file references table if not exists
await ncMeta.knexConnection.schema.createTable(
temp_file_references_table,
(table) => {
table.increments('id').primary();
table.string('file_path').notNullable();
table.string('mimetype');
table.boolean('referenced').defaultTo(false);
table.boolean('thumbnail_generated').defaultTo(false);
table.index('file_path');
},
);
// fallback scanning all files if temp table is not generated from previous migration
const storageAdapter = await NcPluginMgrv2.storageAdapter(ncMeta);
const fileScanStream = await storageAdapter.scanFiles('nc/uploads/**');
const fileReferenceBuffer = [];
fileScanStream.on('data', async (file) => {
const fileNameWithExt = path.basename(file);
const mimetype = mimetypes[path.extname(fileNameWithExt).slice(1)];
fileReferenceBuffer.push({
file_path: file,
mimetype,
referenced: true,
});
if (fileReferenceBuffer.length >= 100) {
fileScanStream.pause();
const processBuffer = fileReferenceBuffer.splice(0);
// skip or insert file references
const toSkip = await ncMeta
.knexConnection(temp_file_references_table)
.whereIn(
'file_path',
fileReferenceBuffer.map((f) => f.file_path),
);
const toSkipPaths = toSkip.map((f) => f.file_path);
const toInsert = processBuffer.filter(
(f) => !toSkipPaths.includes(f.file_path),
);
if (toInsert.length > 0) {
await ncMeta
.knexConnection(temp_file_references_table)
.insert(toInsert);
}
fileScanStream.resume();
}
});
await new Promise((resolve, reject) => {
fileScanStream.on('end', resolve);
fileScanStream.on('error', reject);
});
if (fileReferenceBuffer.length > 0) {
await ncMeta
.knexConnection(temp_file_references_table)
.insert(fileReferenceBuffer);
}
}
const limit = 10;
let offset = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const fileReferences = await ncMeta
.knexConnection(temp_file_references_table)
.where('thumbnail_generated', false)
.andWhere('referenced', true)
.andWhere('mimetype', 'like', 'image/%')
.limit(limit)
.offset(offset);
offset += limit;
if (fileReferences.length === 0) {
break;
}
try {
// manually call thumbnail generator job to control the concurrency
await this.thumbnailGeneratorProcessor.job({
data: {
context: {
base_id: RootScopes.ROOT,
workspace_id: RootScopes.ROOT,
},
attachments: fileReferences.map((f) => {
const isUrl = /^https?:\/\//i.test(f.file_path);
if (isUrl) {
return {
url: f.file_path,
mimetype: f.mimetype,
};
} else {
return {
path: path.join('download', f.file_path),
mimetype: f.mimetype,
};
}
}),
},
} as any);
// update the file references
await ncMeta
.knexConnection(temp_file_references_table)
.whereIn(
'file_path',
fileReferences.map((f) => f.file_path),
)
.update({
thumbnail_generated: true,
});
} catch (e) {
this.log(`error while generating thumbnail:`, e);
}
}
// bump the version
await updateMigrationJobsState({
version: MIGRATION_JOB_VERSION,
});
} catch (e) {
this.log(`error processing attachment migration job:`, e);
}
clearInterval(interval);
await updateMigrationJobsState({
locked: false,
stall_check: Date.now(),
});
// call init migration job again
await this.jobsService.add(MigrationJobTypes.InitMigrationJobs, {});
this.debugLog(`job completed for ${job.id}`);
}
}
Loading…
Cancel
Save