️ Put back preview service (#75)

* Put back the preview service
* Fix imports linter
This commit is contained in:
Romaric Mourgues
2023-06-12 11:09:36 +02:00
committed by GitHub
parent b7b9abbc97
commit d17972b2f1
16 changed files with 811 additions and 19 deletions
@@ -13,6 +13,12 @@ import {
ExecutionContext,
} from "../../../core/platform/framework/api/crud-service";
import gr from "../../global-resolver";
import {
PreviewClearMessageQueueRequest,
PreviewMessageQueueRequest,
} from "../../../services/previews/types";
import { PreviewFinishedProcessor } from "./preview";
import _ from "lodash";
export class FileServiceImpl {
version: "1";
@@ -23,6 +29,9 @@ export class FileServiceImpl {
async init(): Promise<this> {
try {
await Promise.all([(this.repository = await gr.database.getRepository<File>("files", File))]);
gr.platformServices.messageQueue.processor.addHandler(
new PreviewFinishedProcessor(this, this.repository),
);
} catch (err) {
logger.error("Error while initializing files service", err);
}
@@ -106,6 +115,60 @@ export class FileServiceImpl {
if (entity.upload_data.chunks === 1 && totalUploadedSize) {
entity.upload_data.size = totalUploadedSize;
await this.repository.save(entity, context);
/** Send preview generation task */
if (entity.upload_data.size < this.max_preview_file_size) {
const document: PreviewMessageQueueRequest["document"] = {
id: JSON.stringify(_.pick(entity, "id", "company_id")),
provider: gr.platformServices.storage.getConnectorType(),
path: getFilePath(entity),
encryption_algo: this.algorithm,
encryption_key: entity.encryption_key,
chunks: entity.upload_data.chunks,
filename: entity.metadata.name,
mime: entity.metadata.mime,
};
const output = {
provider: gr.platformServices.storage.getConnectorType(),
path: `${getFilePath(entity)}/thumbnails/`,
encryption_algo: this.algorithm,
encryption_key: entity.encryption_key,
pages: 10,
};
entity.metadata.thumbnails_status = "waiting";
await this.repository.save(entity, context);
if (!options?.ignoreThumbnails) {
try {
await gr.platformServices.messageQueue.publish<PreviewMessageQueueRequest>(
"services:preview",
{
data: { document, output },
},
);
if (options.waitForThumbnail) {
entity = await gr.services.files.getFile(
{
id: entity.id,
company_id: context.company.id,
},
context,
{ waitForThumbnail: true },
);
}
} catch (err) {
entity.metadata.thumbnails_status = "error";
await this.repository.save(entity, context);
logger.warn({ err }, "Previewing - Error while sending ");
}
}
}
/** End preview generation task generation */
}
}
@@ -227,6 +290,22 @@ export class FileServiceImpl {
totalChunks: fileToDelete.upload_data.chunks,
});
if (fileToDelete.thumbnails.length > 0) {
await gr.platformServices.messageQueue.publish<PreviewClearMessageQueueRequest>(
"services:preview:clear",
{
data: {
document: {
id: JSON.stringify(_.pick(fileToDelete, "id", "company_id")),
provider: gr.platformServices.storage.getConnectorType(),
path: `${path}/thumbnails/`,
thumbnails_number: fileToDelete.thumbnails.length,
},
},
},
);
}
return new DeleteResult("files", fileToDelete, true);
}
}
@@ -0,0 +1,67 @@
import { logger, TdriveContext } from "../../../core/platform/framework";
import { PreviewMessageQueueCallback } from "../../../../src/services/previews/types";
import Repository from "../../../../src/core/platform/services/database/services/orm/repository/repository";
import { File } from "../entities/file";
import { FileServiceImpl } from "./index";
import { MessageQueueHandler } from "../../../core/platform/services/message-queue/api";
import { ExecutionContext } from "../../../core/platform/framework/api/crud-service";
/**
* Update the file metadata and upload the thumbnails in storage
*/
export class PreviewFinishedProcessor
implements MessageQueueHandler<PreviewMessageQueueCallback, string>
{
constructor(readonly service: FileServiceImpl, private repository: Repository<File>) {}
async init(_context?: TdriveContext): Promise<this> {
return this;
}
readonly topics = {
in: "services:preview:callback",
};
readonly options = {
unique: true,
ack: true,
};
name = "FilePreviewProcessor";
validate(message: PreviewMessageQueueCallback): boolean {
return !!(message && message.document);
}
async process(message: PreviewMessageQueueCallback, context?: ExecutionContext): Promise<string> {
logger.info(
`${this.name} - Updating file metadata with preview generation ${message.thumbnails.length}`,
);
const pk: { company_id: string; id: string } = JSON.parse(message.document.id);
const entity = await this.repository.findOne(pk, {}, context);
if (!entity) {
logger.info(`This file ${message.document.id} does not exists anymore.`);
return;
}
entity.thumbnails = (message.thumbnails || []).map((thumb, index) => {
return {
index,
id: thumb.path.split("/").pop(),
size: thumb.size,
type: thumb.type,
width: thumb.width,
height: thumb.height,
url: `/internal/services/files/v1/companies/${entity.company_id}/files/${entity.id}/thumbnails/${index}`,
};
});
if (!entity.metadata) entity.metadata = {};
entity.metadata.thumbnails_status = "done";
await this.repository.save(entity, context);
return "done";
}
}
@@ -5,4 +5,5 @@ export type UploadOptions = {
totalChunks: number;
chunkNumber: number;
waitForThumbnail: boolean;
ignoreThumbnails: boolean;
};
@@ -28,6 +28,7 @@ export class FileController {
filename: q.resumableFilename || q.filename || file?.filename || undefined,
type: q.resumableType || q.type || file?.mimetype || undefined,
waitForThumbnail: q.thumbnail_sync,
ignoreThumbnails: q.ignore_thumbnails,
};
const id = request.params.id;
@@ -2,36 +2,39 @@ import { FastifyInstance } from "fastify";
import { IncomingMessage, Server, ServerResponse } from "http";
import { TdrivePlatform } from "../core/platform/platform";
import AuthServiceAPI from "../core/platform/services/auth/provider";
import { CounterAPI } from "../core/platform/services/counter/types";
import { CronAPI } from "../core/platform/services/cron/api";
import { DatabaseServiceAPI } from "../core/platform/services/database/api";
import EmailPusherAPI from "../core/platform/services/email-pusher/provider";
import { MessageQueueServiceAPI } from "../core/platform/services/message-queue/api";
import { PushServiceAPI } from "../core/platform/services/push/api";
import { RealtimeServiceAPI } from "../core/platform/services/realtime/api";
import WebServerAPI from "../core/platform/services/webserver/provider";
import { SearchServiceAPI } from "../core/platform/services/search/api";
import StorageAPI from "../core/platform/services/storage/provider";
import { MessageQueueServiceAPI } from "../core/platform/services/message-queue/api";
import { CounterAPI } from "../core/platform/services/counter/types";
import { DatabaseServiceAPI } from "../core/platform/services/database/api";
import AuthServiceAPI from "../core/platform/services/auth/provider";
import { PushServiceAPI } from "../core/platform/services/push/api";
import { CronAPI } from "../core/platform/services/cron/api";
import WebSocketAPI from "../core/platform/services/websocket/provider";
import TrackerAPI from "../core/platform/services/tracker/provider";
import EmailPusherAPI from "../core/platform/services/email-pusher/provider";
import WebServerAPI from "../core/platform/services/webserver/provider";
import WebSocketAPI from "../core/platform/services/websocket/provider";
import { logger } from "../core/platform/framework";
import assert from "assert";
import { CompanyServiceImpl } from "./user/services/companies";
import { WorkspaceServiceImpl } from "./workspaces/services/workspace";
import { UserExternalLinksServiceImpl } from "./user/services/external_links";
import { UserServiceImpl } from "./user/services/users/service";
import { CompanyApplicationServiceImpl } from "./applications/services/company-applications";
import { logger } from "../core/platform/framework";
import { ApplicationServiceImpl } from "./applications/services/applications";
import { FileServiceImpl } from "./files/services";
import { ConsoleServiceImpl } from "./console/service";
import { StatisticsServiceImpl } from "./statistics/service";
import { CompanyApplicationServiceImpl } from "./applications/services/company-applications";
import { ApplicationHooksService } from "./applications/services/hooks";
import OnlineServiceImpl from "./online/service";
import { ConsoleServiceImpl } from "./console/service";
import { DocumentsService } from "./documents/services";
import { DocumentsEngine } from "./documents/services/engine";
import { FileServiceImpl } from "./files/services";
import OnlineServiceImpl from "./online/service";
import { PreviewProcessService } from "./previews/services/files/processing/service";
import { StatisticsServiceImpl } from "./statistics/service";
import { TagsService } from "./tags/services/tags";
import { CompanyServiceImpl } from "./user/services/companies";
import { UserExternalLinksServiceImpl } from "./user/services/external_links";
import { UserServiceImpl } from "./user/services/users/service";
import { WorkspaceServiceImpl } from "./workspaces/services/workspace";
import { PreviewEngine } from "./previews/services/files/engine";
type PlatformServices = {
auth: AuthServiceAPI;
@@ -55,6 +58,9 @@ type TdriveServices = {
console: ConsoleServiceImpl;
statistics: StatisticsServiceImpl;
externalUser: UserExternalLinksServiceImpl;
preview: {
files: PreviewProcessService;
};
applications: {
marketplaceApps: ApplicationServiceImpl;
companyApps: CompanyApplicationServiceImpl;
@@ -106,6 +112,8 @@ class GlobalResolver {
assert(service, `Platform service ${key} was not initialized`);
});
await new PreviewEngine().init();
this.services = {
workspaces: await new WorkspaceServiceImpl().init(),
companies: await new CompanyServiceImpl().init(),
@@ -113,6 +121,9 @@ class GlobalResolver {
console: await new ConsoleServiceImpl().init(),
statistics: await new StatisticsServiceImpl().init(),
externalUser: await new UserExternalLinksServiceImpl().init(),
preview: {
files: await new PreviewProcessService().init(),
},
applications: {
marketplaceApps: await new ApplicationServiceImpl().init(),
companyApps: await new CompanyApplicationServiceImpl().init(),
@@ -0,0 +1,16 @@
import { Prefix, TdriveService } from "../../core/platform/framework";
@Prefix("/internal/services/previews/v1")
export default class PreviewsService extends TdriveService<undefined> {
version = "1";
name = "previews";
public async doInit(): Promise<this> {
return this;
}
// TODO: remove
api(): undefined {
return undefined;
}
}
@@ -0,0 +1,48 @@
import { logger, TwakeContext } from "../../../../../core/platform/framework";
import { PreviewClearMessageQueueRequest, PreviewMessageQueueCallback } from "../../../types";
import gr from "../../../../global-resolver";
import { MessageQueueHandler } from "../../../../../core/platform/services/message-queue/api";
/**
* Clear thumbnails when the delete task is called
*/
export class ClearProcessor
implements MessageQueueHandler<PreviewClearMessageQueueRequest, PreviewMessageQueueCallback>
{
readonly name = "ClearProcessor";
readonly topics = {
in: "services:preview:clear",
out: "services:preview:callback",
};
readonly options = {
unique: true,
ack: true,
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
init?(context?: TwakeContext): Promise<this> {
throw new Error("Method not implemented.");
}
validate(message: PreviewClearMessageQueueRequest): boolean {
return !!(message && message.document);
}
async process(message: PreviewClearMessageQueueRequest): Promise<PreviewMessageQueueCallback> {
logger.info(`${this.name} - Processing preview generation ${message.document.id}`);
if (!this.validate(message)) {
throw new Error("Missing required fields");
}
for (let i = 0; i < message.document.thumbnails_number; i++) {
await gr.platformServices.storage.remove(
`${message.document.path.replace(/\/$/, "")}/${i}.png`,
);
}
return { document: message.document, thumbnails: [] };
}
}
@@ -0,0 +1,15 @@
import { Initializable } from "../../../../../core/platform/framework";
import { ClearProcessor } from "./clear";
import { PreviewProcessor } from "./service";
import gr from "../../../../global-resolver";
/**
* The notification engine is in charge of processing data and delivering user notifications on the right place
*/
export class PreviewEngine implements Initializable {
async init(): Promise<this> {
gr.platformServices.messageQueue.processor.addHandler(new PreviewProcessor());
gr.platformServices.messageQueue.processor.addHandler(new ClearProcessor());
return this;
}
}
@@ -0,0 +1,118 @@
import fs, { promises as fsPromise } from "fs";
import { logger } from "../../../../../core/platform/framework";
import {
PreviewMessageQueueCallback,
PreviewMessageQueueRequest,
ThumbnailResult,
} from "../../../types";
import gr from "../../../../global-resolver";
import { MessageQueueHandler } from "../../../../../core/platform/services/message-queue/api";
import { getTmpFile } from "../../../../../utils/files";
const { unlink } = fsPromise;
/**
* Generate thumbnails when the upload is finished
*/
export class PreviewProcessor
implements MessageQueueHandler<PreviewMessageQueueRequest, PreviewMessageQueueCallback>
{
readonly name = "PreviewProcessor";
init?(): Promise<this> {
throw new Error("Method not implemented.");
}
readonly topics = {
in: "services:preview",
out: "services:preview:callback",
};
readonly options = {
unique: true,
ack: true,
};
validate(message: PreviewMessageQueueRequest): boolean {
return !!(message && message.document && message.output);
}
async process(message: PreviewMessageQueueRequest): Promise<PreviewMessageQueueCallback> {
logger.info(`${this.name} - Processing preview generation ${message.document.id}`);
let res: PreviewMessageQueueCallback = { document: message.document, thumbnails: [] };
try {
res = await this.generate(message);
} catch (err) {
logger.error(`${this.name} - Can't generate thumbnails ${err}`);
}
logger.info(
`${this.name} - Generated ${res.thumbnails.length} thumbnails from ${
message.document.filename || message.document.id
}`,
);
return res;
}
async generate(message: PreviewMessageQueueRequest): Promise<PreviewMessageQueueCallback> {
//Download original file
const readable = await gr.platformServices.storage.read(message.document.path, {
totalChunks: message.document.chunks,
encryptionAlgo: message.document.encryption_algo,
encryptionKey: message.document.encryption_key,
});
if (!readable) {
return { document: message.document, thumbnails: [] };
}
const inputPath = getTmpFile();
const writable = fs.createWriteStream(inputPath);
readable.pipe(writable);
await new Promise(r => {
writable.on("finish", r);
});
writable.end();
//Generate previews
let localThumbnails: ThumbnailResult[] = [];
try {
localThumbnails = await gr.services.preview.files.generateThumbnails(
{ path: inputPath, mime: message.document.mime, filename: message.document.filename },
message.output,
);
} catch (err) {
logger.error(`${this.name} - Can't generate thumbnails ${err}`);
localThumbnails = [];
throw Error("Can't generate thumbnails.");
}
const thumbnails: PreviewMessageQueueCallback["thumbnails"] = [];
for (let i = 0; i < localThumbnails.length; i++) {
const uploadThumbnailPath = `${message.output.path.replace(/\/$/, "")}/${i}.png`;
const uploadThumbnail = fs.createReadStream(localThumbnails[i].path);
await gr.platformServices.storage.write(uploadThumbnailPath, uploadThumbnail, {
encryptionAlgo: message.output.encryption_algo,
encryptionKey: message.output.encryption_key,
});
thumbnails.push({
path: uploadThumbnailPath,
size: localThumbnails[i].size,
type: localThumbnails[i].type,
width: localThumbnails[i].width,
height: localThumbnails[i].height,
});
await unlink(localThumbnails[i].path);
}
return { document: message.document, thumbnails };
}
}
@@ -0,0 +1,54 @@
import sharp from "sharp";
import { cleanFiles, getTmpFile } from "../../../../../utils/files";
import { PreviewMessageQueueRequest, ThumbnailResult } from "../../../types";
import { logger } from "../../../../../core/platform/framework/logger";
export async function generatePreview(
inputPaths: string[],
options: PreviewMessageQueueRequest["output"],
): Promise<{
output: ThumbnailResult[];
done: boolean;
error?: string;
}> {
const output: ThumbnailResult[] = [];
for (const inputPath of inputPaths) {
let result: sharp.OutputInfo;
const outputPath = getTmpFile();
try {
const inputMetadata = await sharp(inputPath).metadata();
const outputFormat = computeNewFormat(inputMetadata, options);
result = await sharp(inputPath).rotate().resize(outputFormat).toFile(outputPath);
output.push({
path: outputPath,
width: result.width,
height: result.height,
type: "image/png",
size: result.size,
});
} catch (error) {
logger.info(`sharp cant process ${error}`);
cleanFiles([outputPath]);
throw Error("Can't resize thumnail with Sharp");
}
}
return {
output,
done: true,
};
}
function computeNewFormat(
inputMetadata: sharp.Metadata,
options?: PreviewMessageQueueRequest["output"],
): { width: number; height: number } {
const maxOutputWidth = options?.width || 600;
const maxOutputHeight = options?.height || 400;
const inputWidth = inputMetadata.width;
const inputHeight = inputMetadata.height;
const scale = Math.max(inputWidth / maxOutputWidth, inputHeight / maxOutputHeight);
return { width: Math.round(inputWidth / scale), height: Math.round(inputHeight / scale) };
}
@@ -0,0 +1,29 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const unoconv = require("unoconv-promise");
import { cleanFiles } from "../../../../../utils/files";
import { logger } from "../../../../../core/platform/framework/logger";
export async function convertFromOffice(
path: string,
options: {
numberOfPages?: number;
},
): Promise<{ output: string; done: boolean }> {
if (options.numberOfPages >= 1) {
const outputPath = `${path}.pdf`;
try {
await unoconv.run({
file: path,
output: outputPath,
export: `PageRange=1-${options.numberOfPages}`,
});
} catch (err) {
logger.error(`unoconv cant process ${err}`);
cleanFiles([outputPath]);
throw Error("Can't convert file with unoconv");
}
return { output: outputPath, done: true };
} else {
logger.error("Unoconv can't processe, number of pages lower than 1");
}
}
@@ -0,0 +1,43 @@
import { fromPath } from "pdf2pic";
import { mkdirSync } from "fs";
import { cleanFiles, getTmpFile } from "../../../../../utils/files";
export async function convertFromPdf(
inputPath: string,
options: {
numberOfPages: number;
},
): Promise<{ output: string[]; done: boolean }> {
const pages: string[] = [];
try {
const pdfOptions = {
density: 100,
saveFilename: "output",
savePath: getTmpFile(),
format: "png",
};
mkdirSync(pdfOptions.savePath, { recursive: true });
const storeAsImage = fromPath(inputPath, pdfOptions);
try {
for (let i = 1; i <= options.numberOfPages; i++) {
const image = await storeAsImage(i);
pages.push(
`${pdfOptions.savePath}/${pdfOptions.saveFilename}.${image.page}.${pdfOptions.format}`,
);
}
} catch (err) {
if (!pages.length) {
throw err;
}
//Just no more page to convert
}
} catch (error) {
console.error(error);
for (const file of pages) {
cleanFiles([file]);
}
throw Error("Can't convert file with pdf-image.");
}
return { output: pages, done: true };
}
@@ -0,0 +1,71 @@
import { generatePreview as thumbnailsFromImages } from "./image";
import { convertFromOffice } from "./office";
import { convertFromPdf } from "./pdf";
import { isFileType } from "../../../utils";
import {
imageExtensions,
officeExtensions,
pdfExtensions,
videoExtensions,
} from "../../../../../utils/mime";
import { PreviewMessageQueueRequest, ThumbnailResult } from "../../../types";
import { generateVideoPreview } from "./video";
import { Initializable, TwakeServiceProvider } from "../../../../../core/platform/framework";
import { cleanFiles } from "../../../../../utils/files";
export class PreviewProcessService implements TwakeServiceProvider, Initializable {
name: "PreviewProcessService";
version: "1";
async init(): Promise<this> {
return this;
}
async generateThumbnails(
document: Pick<PreviewMessageQueueRequest["document"], "filename" | "mime" | "path">,
options: PreviewMessageQueueRequest["output"],
): Promise<ThumbnailResult[]> {
if (isFileType(document.mime, document.filename, officeExtensions)) {
const pdfPath = await convertFromOffice(document.path, {
numberOfPages: options.pages,
});
await cleanFiles([document.path]);
const thumbnailPath = await convertFromPdf(pdfPath.output, {
numberOfPages: options.pages,
});
await cleanFiles([pdfPath.output]);
const images = (await thumbnailsFromImages(thumbnailPath.output, options)).output;
await cleanFiles(thumbnailPath.output);
return images;
}
if (isFileType(document.mime, document.filename, pdfExtensions)) {
const thumbnailPath = await convertFromPdf(document.path, {
numberOfPages: options.pages,
});
await cleanFiles([document.path]);
const images = (await thumbnailsFromImages(thumbnailPath.output, options)).output;
await cleanFiles(thumbnailPath.output);
return images;
}
if (isFileType(document.mime, document.filename, imageExtensions)) {
const images = (await thumbnailsFromImages([document.path], options)).output;
await cleanFiles([document.path]);
return images;
}
if (isFileType(document.mime, document.filename, videoExtensions)) {
try {
const images = await generateVideoPreview([document.path]);
await cleanFiles([document.path]);
return images;
} catch (error) {
throw Error("failed to generate video preview");
}
}
throw "Can not proccess, file type can't be defined";
}
}
@@ -0,0 +1,167 @@
import ffmpeg from "fluent-ffmpeg";
import { temporaryThumbnailFile, ThumbnailResult } from "../../../types";
import fs from "fs";
import { cleanFiles, getTmpFile } from "../../../../../utils/files";
/**
* Generate thumbnails for given video files.
*
* @param {String[]} videoPaths - the input video paths
* @returns {Promise<ThumbnailResult[]>} - resolves when the thumbnails are generated
*/
export async function generateVideoPreview(videoPaths: string[]): Promise<ThumbnailResult[]> {
const output: ThumbnailResult[] = [];
for (const videoPath of videoPaths) {
const { fileName, folder, filePath } = getTemporaryThumbnailFile();
try {
const { width, height } = await takeVideoScreenshot(videoPath, folder, fileName);
output.push({
...getThumbnailInformation(filePath),
width,
height,
});
} catch (error) {
cleanFiles([filePath]);
throw Error(`failed to generate video preview: ${error}`);
}
}
return output;
}
/**
* Generate one thumbnail from a single video file.
* the thumbnail is the first frame from the video
*
* @param {String} inputPath - the input video Path
* @param {String} outputFolder - the output folder
* @param {String} outputFile - the output file name
* @returns {Primise} - resolves when the thumbnail is generated
*/
const takeVideoScreenshot = async (
inputPath: string,
outputFolder: string,
outputFile: string,
): Promise<{ width: number; height: number }> => {
return new Promise(async (resolve, reject) => {
try {
const { width, height } = await getVideoDimensions(inputPath);
const { width: outputWidth, height: outputHeight } = calculateThumbnailDimensions(
width,
height,
);
ffmpeg(inputPath)
.screenshot({
count: 1,
filename: outputFile,
folder: outputFolder,
timemarks: ["0"],
size: `${outputWidth}x${outputHeight}`,
})
.on("end", () => {
resolve({ width: outputWidth, height: outputHeight });
})
.on("error", error => {
reject(error);
});
} catch (error) {
reject(error);
}
});
};
/**
* Get the generated thumbnail information.
*
* @param {String} path - the path to the thumbnail
* @param {PreviewMessageQueueRequest["output"]} options - the options for the thumbnails
* @returns { { path: string, type: string, size: number } } - the thumbnail information
*/
const getThumbnailInformation = (
path: string,
): {
path: string;
type: string;
size: number;
} => {
const stats = fs.statSync(path);
return {
size: stats.size,
type: "image/jpg",
path,
};
};
/**
* generate a temporary thumbnail file.
*
* @returns {temporaryThumbnailFile} - the temporary thumbnail file information
*/
const getTemporaryThumbnailFile = (): temporaryThumbnailFile => {
const filePath = `${getTmpFile()}.jpg`;
const fileName = filePath.split("/").pop();
const folder = filePath.substring(0, filePath.lastIndexOf("/"));
return {
folder,
fileName,
filePath,
};
};
/**
* Detect video dimensions
*
* @param {String} videoPath - the video path
* @returns {Promise<{ width: number, height: number }>} - the video dimensions
*/
async function getVideoDimensions(videoPath: string): Promise<{ width: number; height: number }> {
return new Promise((resolve, reject) => {
ffmpeg.ffprobe(videoPath, (err, metadata) => {
if (err) {
reject(err);
}
const { width, height } = metadata.streams[0];
resolve({ width, height });
});
});
}
/**
* Calculate the thumbnail dimensions.
* maximum dimension is 1080p and the minimum dimension is 320x240.
* The aspect ratio is preserved.
*
* @param {Number} width - the video width
* @param {Number} height - the video height
* @returns { { width: number, height: number } } - the thumbnail dimensions
*/
function calculateThumbnailDimensions(
width: number,
height: number,
): {
width: number;
height: number;
} {
let newWidth = Math.min(width, 1920);
let newHeight = Math.min(height, 1080);
const ratio = width / height;
if (width > 1920 || height > 1080) {
newWidth = Math.min(1920, width);
newHeight = Math.min(1080, newWidth / ratio);
}
if (width < 320 || height < 240) {
newWidth = Math.max(320, width);
newHeight = Math.max(240, newWidth / ratio);
}
return { width: newWidth, height: newHeight };
}
@@ -0,0 +1,61 @@
export type PreviewMessageQueueRequest = {
document: {
id: string;
provider: string;
path: string;
encryption_algo?: string;
encryption_key?: string;
chunks?: number;
mime?: string;
filename?: string;
};
output: {
provider: string;
path: string;
encryption_algo?: string;
encryption_key?: string;
pages?: number; //Max number of pages for the document
width?: number; //Max width for the thumbnails
height?: number; //Max height for the thumbnails
};
};
export type PreviewClearMessageQueueRequest = {
document: {
id: string;
provider: string;
path: string;
thumbnails_number: number;
};
};
export type PreviewMessageQueueCallback = {
document: {
id: string;
path: string;
provider: string;
};
thumbnails: {
path: string;
size: number;
type: string;
width: number;
height: number;
provider?: string;
index?: number;
}[];
};
export type ThumbnailResult = {
path: string;
width: number;
height: number;
size: number;
type: string;
};
export type temporaryThumbnailFile = {
filePath: string;
fileName: string;
folder: string;
};
@@ -0,0 +1,11 @@
import mimes from "../../utils/mime";
export const TIMEOUT = 5 * 1000;
export const MAX_SIZE = 5 * 1024 * 1024;
export function isFileType(fileMime: string, fileName: string, requiredExtensions: string[]): any {
const extension = fileName.split(".").pop();
const secondaryExtensions = Object.keys(mimes).filter(k => mimes[k] === fileMime);
const fileExtensions = [extension, ...secondaryExtensions];
return fileExtensions.some(e => requiredExtensions.includes(e));
}