refactor: migrated file operations

pull/243/head
Josh Moore 1 year ago
parent 1c0965c510
commit f4d05bdb33

@ -0,0 +1,103 @@
import fs from 'fs-extra';
import sharp from 'sharp';
import crypto from 'crypto';
import Vibrant from 'node-vibrant';
import ffmpeg from 'ffmpeg-static';
import toArray from 'stream-to-array';
import { exec } from 'child_process';
import { removeLocation } from '@xoi/gps-metadata-remover';
import { isProd } from '@tycrek/joint';
//@ts-ignore
import shell from 'any-shell-escape';
type SrcDest = { src: string, dest: string };
/**
* Strips GPS EXIF data from a file
*/
export const removeGPS = (file: string): Promise<boolean> => {
return new Promise((resolve, reject) =>
fs.open(file, 'r+')
.then((fd) => removeLocation(file,
// Read function
(size: number, offset: number): Promise<Buffer> =>
fs.read(fd, Buffer.alloc(size), 0, size, offset)
.then(({ buffer }) => Promise.resolve(buffer)),
// Write function
(val: string, offset: number, enc: BufferEncoding): Promise<void> =>
fs.write(fd, Buffer.alloc(val.length, val, enc), 0, val.length, offset)
.then(() => Promise.resolve())))
.then(resolve)
.catch(reject));
}
/**
* SHA256 file hasher
*/
export const sha256 = (file: string): Promise<string> => new Promise((resolve, reject) =>
toArray(fs.createReadStream(file))
.then((parts: any[]) => Buffer.concat(parts.map((part: any) => Buffer.isBuffer(part) ? part : Buffer.from(part))))
.then((buf: Buffer) => crypto.createHash('sha256').update(buf).digest('hex'))
.then((hash: string) => resolve(hash))
.catch(reject));
const VIBRANT = { COLOURS: 256, QUALITY: 3 };
export const vibrant = (file: string, mimetype: string): Promise<string> => new Promise((resolve, reject) =>
// todo: random hex colour
mimetype.includes('video') || mimetype.includes('webp') ? `#335599`
: sharp(file).png().toBuffer()
.then((data) => Vibrant.from(data)
.maxColorCount(VIBRANT.COLOURS)
.quality(VIBRANT.QUALITY)
.getPalette())
.then((palettes) => resolve(palettes[Object.keys(palettes).sort((a, b) => palettes[b]!.population - palettes[a]!.population)[0]]!.hex))
.catch((err) => reject(err))
)
/**
* Thumbnail operations
*/
export class Thumbnail {
private static readonly THUMBNAIL = {
QUALITY: 75,
WIDTH: 200 * 2,
HEIGHT: 140 * 2,
}
private static getImageThumbnail({ src, dest }: SrcDest) {
return new Promise((resolve, reject) =>
sharp(src)
.resize(this.THUMBNAIL.WIDTH, this.THUMBNAIL.HEIGHT, { kernel: 'cubic' })
.jpeg({ quality: this.THUMBNAIL.QUALITY })
.toFile(dest)
.then(resolve)
.catch(reject));
}
private static getVideoThumbnail({ src, dest }: SrcDest) {
exec(this.getCommand({ src, dest }))
}
private static getCommand({ src, dest }: SrcDest) {
return shell([
ffmpeg, '-y',
'-v', (isProd() ? 'error' : 'debug'), // Log level
'-i', src, // Input file
'-ss', '00:00:01.000', // Timestamp of frame to grab
'-vf', `scale=${this.THUMBNAIL.WIDTH}:${this.THUMBNAIL.HEIGHT}:force_original_aspect_ratio=increase,crop=${this.THUMBNAIL.WIDTH}:${this.THUMBNAIL.HEIGHT}`, // Dimensions of output file
'-frames:v', '1', // Number of frames to grab
dest // Output file
]);
}
// old default
/*
export default (file: FileData): Promise<string> =>
new Promise((resolve, reject) =>
(file.is.video ? getVideoThumbnail : (file.is.image && !file.mimetype.includes('webp')) ? getImageThumbnail : () => Promise.resolve())(file)
.then(() => resolve((file.is.video || file.is.image) ? getNewName(file.randomId) : file.is.audio ? 'views/ass-audio-icon.png' : 'views/ass-file-icon.png'))
.catch(reject));
*/
}

@ -1,16 +0,0 @@
import { FileData } from './types/definitions';
import fs from 'fs-extra';
import crypto from 'crypto';
import toArray from 'stream-to-array';
import { log } from './utils';
/**
* Generates a SHA1 hash for the provided file
*/
export default (file: FileData): Promise<string> =>
new Promise((resolve, reject) =>
toArray((fs.createReadStream(file.path)))
.then((parts: any[]) => Buffer.concat(parts.map((part: any) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))))
.then((buf: Buffer) => crypto.createHash('sha1').update(buf).digest('hex')) // skipcq: JS-D003
.then((hash: string) => log.debug(`Hash for ${file.originalname}`, hash, 'SHA1, hex').callback(() => resolve(hash)))
.catch(reject));

@ -1,26 +0,0 @@
/**
* This strips GPS EXIF data from files
*/
import { removeLocation } from '@xoi/gps-metadata-remover';
import fs from 'fs-extra';
/**
* This strips GPS EXIF data from files using the @xoi/gps-metadata-remover package
* @returns A Promise that resolves to `true` if GPS data was removed, `false` if not
*/
export const removeGPS = (file: string): Promise<boolean> => {
return new Promise((resolve, reject) =>
fs.open(file, 'r+')
.then((fd) => removeLocation(file,
// Read function
(size: number, offset: number): Promise<Buffer> =>
fs.read(fd, Buffer.alloc(size), 0, size, offset)
.then(({ buffer }) => Promise.resolve(buffer)),
// Write function
(val: string, offset: number, enc: BufferEncoding): Promise<void> =>
fs.write(fd, Buffer.alloc(val.length, val, enc), 0, val.length, offset)
.then(() => Promise.resolve())))
.then(resolve)
.catch(reject));
}

@ -1,80 +0,0 @@
import { FileData } from './types/definitions';
import { Config } from 'ass-json';
import fs from 'fs-extra';
import ffmpeg from 'ffmpeg-static';
import sharp from 'sharp';
// @ts-ignore
import shell from 'any-shell-escape';
import { exec } from 'child_process';
import { isProd, path } from './utils';
const { diskFilePath }: Config = fs.readJsonSync(path('config.json'));
// Thumbnail parameters
const THUMBNAIL = {
QUALITY: 75,
WIDTH: 200 * 2,
HEIGHT: 140 * 2,
}
/**
* Builds a safe escaped ffmpeg command
*/
function getCommand(src: String, dest: String) {
return shell([
ffmpeg, '-y',
'-v', (isProd ? 'error' : 'debug'), // Log level
'-i', src, // Input file
'-ss', '00:00:01.000', // Timestamp of frame to grab
'-vf', `scale=${THUMBNAIL.WIDTH}:${THUMBNAIL.HEIGHT}:force_original_aspect_ratio=increase,crop=${THUMBNAIL.WIDTH}:${THUMBNAIL.HEIGHT}`, // Dimensions of output file
'-frames:v', '1', // Number of frames to grab
dest // Output file
]);
}
/**
* Builds a thumbnail filename
*/
function getNewName(oldName: String) {
return oldName.concat('.thumbnail.jpg');
}
/**
* Builds a path to the thumbnails
*/
function getNewNamePath(oldName: String) {
return path(diskFilePath, 'thumbnails/', getNewName(oldName));
}
/**
* Extracts an image from a video file to use as a thumbnail, using ffmpeg
*/
function getVideoThumbnail(file: FileData) {
return new Promise((resolve: Function, reject: Function) => exec(
getCommand(file.path, getNewNamePath(file.randomId)),
// @ts-ignore
(err: Error) => (err ? reject(err) : resolve())
));
}
/**
* Generates a thumbnail for the provided image
*/
function getImageThumbnail(file: FileData) {
return new Promise((resolve, reject) =>
sharp(file.path)
.resize(THUMBNAIL.WIDTH, THUMBNAIL.HEIGHT, { kernel: 'cubic' })
.jpeg({ quality: THUMBNAIL.QUALITY })
.toFile(getNewNamePath(file.randomId))
.then(resolve)
.catch(reject));
}
/**
* Generates a thumbnail
*/
export default (file: FileData): Promise<string> =>
new Promise((resolve, reject) =>
(file.is.video ? getVideoThumbnail : (file.is.image && !file.mimetype.includes('webp')) ? getImageThumbnail : () => Promise.resolve())(file)
.then(() => resolve((file.is.video || file.is.image) ? getNewName(file.randomId) : file.is.audio ? 'views/ass-audio-icon.png' : 'views/ass-file-icon.png'))
.catch(reject));

@ -1,26 +0,0 @@
import { FileData } from './types/definitions';
import Vibrant from 'node-vibrant';
import sharp from 'sharp';
import { randomHexColour } from './utils';
// Vibrant parameters
const COLOR_COUNT = 256;
const QUALITY = 3;
/**
* Extracts a prominent colour from the provided image file
*/
function getVibrant(file: FileData, resolve: Function, reject: Function) {
sharp(file.path).png().toBuffer()
.then((data) => Vibrant.from(data)
.maxColorCount(COLOR_COUNT)
.quality(QUALITY)
.getPalette())
.then((palettes) => resolve(palettes[Object.keys(palettes).sort((a, b) => palettes[b]!.population - palettes[a]!.population)[0]]!.hex))
.catch((err) => reject(err));
}
/**
* Extracts a colour from an image file. Returns a random Hex value if provided file is a video
*/
export default (file: FileData): Promise<string> => new Promise((resolve, reject) => (!file.is.image || file.mimetype.includes('webp')) ? resolve(randomHexColour()) : getVibrant(file, resolve, reject)); // skipcq: JS-0229
Loading…
Cancel
Save