Merge pull request #3 from rishighan/mimetype-check

 MIMEtype check for comic book archives
This commit was merged in pull request #3.
This commit is contained in:
2023-03-23 23:59:30 -04:00
committed by GitHub
10 changed files with 4201 additions and 16348 deletions

View File

@@ -7,8 +7,10 @@ module.exports = {
ignorePatterns: [ "test/*"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "esnext",
project: "tsconfig.json",
sourceType: "module"
sourceType: "module",
ecmaVersion: "latest",
},
plugins: ["prefer-arrow", "import", "@typescript-eslint"],
rules: {

View File

@@ -1,13 +1,14 @@
const path = require("path");
const mkdir = require("mkdirp").sync;
const DbService = require("moleculer-db");
const MongoAdapter = require("moleculer-db-adapter-mongoose");
export const DbMixin = (collection, model) => {
if(process.env.MONGO_URI) {
if (process.env.MONGO_URI) {
const MongooseAdapter = require("moleculer-db-adapter-mongoose");
return {
mixins: [DbService],
adapter: new MongoAdapter(process.env.MONGO_URI, {
adapter: new MongooseAdapter(process.env.MONGO_URI, {
user: process.env.MONGO_INITDB_ROOT_USERNAME,
pass: process.env.MONGO_INITDB_ROOT_PASSWORD,
keepAlive: true,
@@ -15,7 +16,6 @@ export const DbMixin = (collection, model) => {
family: 4,
}),
model,
collection,
};
}
mkdir(path.resolve("./data"));

View File

@@ -25,15 +25,13 @@ const RawFileDetailsSchema = mongoose.Schema({
filePath: String,
fileSize: Number,
extension: String,
mimeType: String,
containedIn: String,
pageCount: Number,
cover: {
filePath: String,
stats: Object,
},
calibreMetadata: {
coverWriteResult: String,
},
});
const LOCGSchema = mongoose.Schema({

20372
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "threetwo-core-service",
"version": "0.0.1",
"description": "",
"description": "Endpoints for common operations in ThreeTwo",
"scripts": {
"build": "tsc --build tsconfig.json",
"dev": "ts-node ./node_modules/moleculer/bin/moleculer-runner.js --hot --repl --config moleculer.config.ts services/**/*.service.ts",
@@ -18,26 +18,30 @@
"microservices",
"moleculer"
],
"author": "",
"author": "Rishi Ghan",
"devDependencies": {
"@elastic/elasticsearch": "^8.6.0",
"@types/lodash": "^4.14.168",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"eslint": "^8.36.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-prefer-arrow": "^1.2.2",
"install": "^0.13.0",
"jest": "^27.5.1",
"jest-cli": "^27.5.1",
"jest": "^29.5.0",
"jest-cli": "^29.5.0",
"moleculer-repl": "^0.7.0",
"node-calibre": "^2.1.1",
"npm": "^8.4.1",
"ts-jest": "^27.1.4",
"ts-node": "^8.8.1",
"typescript": "^4.6.4"
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1",
"typescript": "^5.0.2"
},
"dependencies": {
"@npcz/magic": "^1.3.14",
"redis": "^4.6.5",
"@socket.io/redis-adapter": "^8.1.0",
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git",
"@elastic/elasticsearch": "^8.6.0",
"@jorgeferrero/stream-to-buffer": "^2.0.6",
"@root/walk": "^1.1.0",
"@types/jest": "^27.4.1",
@@ -61,22 +65,20 @@
"leven": "^3.1.0",
"lodash": "^4.17.21",
"mkdirp": "^0.5.5",
"moleculer": "^0.14.28",
"moleculer": "^0.14.29",
"moleculer-bull": "github:rishighan/moleculer-bull#1.0.0",
"moleculer-db": "^0.8.23",
"moleculer-db-adapter-mongo": "^0.4.17",
"moleculer-db-adapter-mongoose": "^0.9.2",
"moleculer-io": "^2.2.0",
"moleculer-web": "^0.10.5",
"mongoosastic-ts": "^6.0.3",
"mongoose": "^6.10.4",
"mongoose-paginate-v2": "^1.3.18",
"nats": "^1.3.2",
"node-calibre": "^2.1.1",
"opds-extra": "^3.0.9",
"p7zip-threetwo": "^1.0.4",
"sanitize-filename-ts": "^1.0.2",
"sharp": "^0.30.4",
"socket.io-redis": "^6.1.1",
"threetwo-ui-typings": "^1.0.14",
"through2": "^4.0.2",
"unrar": "^0.2.0",

View File

@@ -80,6 +80,7 @@ export default class QueueService extends Service {
filePath,
fileSize,
extension,
mimeType,
cover,
containedIn,
comicInfoJSON,
@@ -113,6 +114,7 @@ export default class QueueService extends Service {
filePath,
fileSize,
extension,
mimeType,
containedIn,
cover,
},

View File

@@ -75,6 +75,7 @@ export default class ImportService extends Service {
return await walkFolder(ctx.params.basePathToWalk, [
".cbz",
".cbr",
".cb7",
]);
},
},

View File

@@ -1,10 +1,16 @@
"use strict";
import { Service, ServiceBroker, ServiceSchema } from "moleculer";
import { createClient } from "redis";
import { createAdapter } from "@socket.io/redis-adapter";
const SocketIOService = require("moleculer-io");
const redisAdapter = require("socket.io-redis");
const redisURL = new URL(process.env.REDIS_URI);
console.log(redisURL.hostname);
// console.log(redisURL.hostname);
const pubClient = createClient({ url: `redis://${redisURL.hostname}:6379` });
(async () => {
await pubClient.connect();
})();
const subClient = pubClient.duplicate();
export default class SocketService extends Service {
// @ts-ignore
public constructor(
@@ -66,10 +72,7 @@ export default class SocketService extends Service {
},
},
options: {
adapter: redisAdapter({
host: redisURL.hostname,
port: 6379,
}),
adapter: createAdapter(pubClient, subClient),
},
},
},

View File

@@ -3,6 +3,7 @@ const fse = require("fs-extra");
import path from "path";
import fs from "fs";
import { FileMagic, MagicFlags } from "@npcz/magic";
const { readdir, stat } = require("fs/promises");
import {
IExplodedPathResponse,
@@ -17,6 +18,17 @@ import { sanitize } from "sanitize-filename-ts";
const ALLOWED_IMAGE_FILE_FORMATS = [".jpg", ".jpeg", ".png"];
// Tell FileMagic where to find the magic.mgc file
FileMagic.magicFile = require.resolve("@npcz/magic/dist/magic.mgc");
// We can onlu use MAGIC_PRESERVE_ATIME on operating suystems that support
// it and that includes OS X for example. It's a good practice as we don't
// want to change the last access time because we are just checking the file
// contents type
if (process.platform === "darwin" || process.platform === "linux") {
FileMagic.defaulFlags = MagicFlags.MAGIC_PRESERVE_ATIME;
}
export const walkFolder = async (
folder: string,
formats: string[]
@@ -73,11 +85,11 @@ export const explodePath = (filePath: string): IExplodedPathResponse => {
// returns a promise which resolves true if file exists:
export const checkFileExists = (filepath) => {
return new Promise((resolve, reject) => {
fs.access(filepath, fs.constants.F_OK, error => {
fs.access(filepath, fs.constants.F_OK, (error) => {
resolve(!error);
});
});
}
};
export const getSizeOfDirectory = async (
directoryPath: string,
@@ -109,6 +121,12 @@ export const constructPaths = (
walkedFolder.extension,
});
/**
* Method that gets file metadata from a filepath.
* Extracts the file extension, file name with and without the extension
* @param {string} filePath
* @returns {Object} object
*/
export const getFileConstituents = (filePath: string) => {
const extension = path.extname(filePath);
const fileNameWithExtension = path.basename(filePath);
@@ -123,14 +141,33 @@ export const getFileConstituents = (filePath: string) => {
};
};
/**
* Method that infers MIME type from a filepath
* @param {string} filePath
* @returns {Promise} string
*/
export const getMimeType = async (filePath: string) => {
return await FileMagic.getInstance().then((magic: FileMagic) => {
return magic.detect(
path.resolve(filePath),
magic.flags | MagicFlags.MAGIC_MIME
);
});
};
export const createDirectory = async (options: any, directoryPath: string) => {
try {
await fse.ensureDir(directoryPath, options);
console.info(`Directory [ %s ] was created.`, directoryPath);
} catch (error) {
throw new Errors.MoleculerError("Failed to create directory", 500, "FileOpsError", error);
throw new Errors.MoleculerError(
"Failed to create directory",
500,
"FileOpsError",
error
);
}
}
};
const filterOutDotFiles = (entities) =>
entities.filter((ent) => !ent.name.startsWith("."));

View File

@@ -44,6 +44,7 @@ import {
getFileConstituents,
createDirectory,
walkFolder,
getMimeType,
} from "../utils/file.utils";
import { convertXMLToJSON } from "./xml.utils";
const fse = require("fs-extra");
@@ -62,7 +63,8 @@ interface RarFile {
}
const UNRAR_BIN_PATH = process.env.UNRAR_BIN_PATH || "/usr/local/bin/unrar";
// errors array
const errors = [];
/**
* Method that extracts comicInfo.xml file from a .rar archive, if one exists.
* Also extracts the first image in the listing, which is assumed to be the cover.
@@ -70,7 +72,8 @@ const UNRAR_BIN_PATH = process.env.UNRAR_BIN_PATH || "/usr/local/bin/unrar";
* @returns {any}
*/
export const extractComicInfoXMLFromRar = async (
filePath: string
filePath: string,
mimeType: string,
): Promise<any> => {
try {
// Create the target directory
@@ -87,13 +90,13 @@ export const extractComicInfoXMLFromRar = async (
const archive = new Unrar({
path: path.resolve(filePath),
bin: `${UNRAR_BIN_PATH}`, // this will change depending on Docker base OS
arguments: ["-v"]
arguments: ["-v"],
});
const filesInArchive: [RarFile] = await new Promise(
(resolve, reject) => {
return archive.list((err, entries) => {
if (err) {
console.log(`DEBUG: ${JSON.stringify(err, null, 2)}` )
console.log(`DEBUG: ${JSON.stringify(err, null, 2)}`);
reject(err);
}
resolve(entries);
@@ -151,7 +154,9 @@ export const extractComicInfoXMLFromRar = async (
const comicInfoJSON = await convertXMLToJSON(
comicinfostring.toString()
);
console.log(`comicInfo.xml successfully written: ${comicInfoJSON.comicinfo}`)
console.log(
`comicInfo.xml successfully written: ${comicInfoJSON.comicinfo}`
);
resolve({ comicInfoJSON: comicInfoJSON.comicinfo });
}
});
@@ -182,6 +187,7 @@ export const extractComicInfoXMLFromRar = async (
extension,
containedIn: targetDirectory,
fileSize: fse.statSync(filePath).size,
mimeType,
cover: {
filePath: path.relative(
process.cwd(),
@@ -202,7 +208,8 @@ export const extractComicInfoXMLFromRar = async (
};
export const extractComicInfoXMLFromZip = async (
filePath: string
filePath: string,
mimeType: string,
): Promise<any> => {
try {
// Create the target directory
@@ -247,7 +254,7 @@ export const extractComicInfoXMLFromZip = async (
// Push the first file (cover) to our extraction target
extractionTargets.push(files[0].name);
filesToWriteToDisk.coverFile = path.basename(files[0].name);
console.log(`sanitized or not, here I am: ${filesToWriteToDisk.coverFile}`);
if (!isEmpty(comicInfoXMLFileObject)) {
filesToWriteToDisk.comicInfoXML = comicInfoXMLFileObject[0].name;
extractionTargets.push(filesToWriteToDisk.comicInfoXML);
@@ -318,6 +325,7 @@ export const extractComicInfoXMLFromZip = async (
filePath,
name: fileNameWithoutExtension,
extension,
mimeType,
containedIn: targetDirectory,
fileSize: fse.statSync(filePath).size,
cover: {
@@ -342,18 +350,17 @@ export const extractComicInfoXMLFromZip = async (
export const extractFromArchive = async (filePath: string) => {
console.info(`Unrar is located at: ${UNRAR_BIN_PATH}`);
console.info(`p7zip is located at: ${process.env.SEVENZ_BINARY_PATH}`);
const { extension } = getFileConstituents(filePath);
console.log(
`Detected file type is ${extension}, looking for comicinfo.xml...`
);
switch (extension) {
case ".cbz":
case ".cb7":
const cbzResult = await extractComicInfoXMLFromZip(filePath);
const mimeType = await getMimeType(filePath);
console.log(`File has the following mime-type: ${mimeType}`);
switch (mimeType) {
case "application/x-7z-compressed; charset=binary":
case "application/zip; charset=binary":
const cbzResult = await extractComicInfoXMLFromZip(filePath, mimeType);
return Object.assign({}, ...cbzResult);
case ".cbr":
const cbrResult = await extractComicInfoXMLFromRar(filePath);
case "application/x-rar; charset=binary":
const cbrResult = await extractComicInfoXMLFromRar(filePath, mimeType);
return Object.assign({}, ...cbrResult);
default:
@@ -374,13 +381,20 @@ export const uncompressEntireArchive = async (
filePath: string,
options: any
) => {
const { extension } = getFileConstituents(filePath);
switch (extension) {
case ".cbz":
case ".cb7":
return await uncompressZipArchive(filePath, options);
case ".cbr":
return await uncompressRarArchive(filePath, options);
const mimeType = await getMimeType(filePath);
console.log(`File has the following mime-type: ${mimeType}`);
switch (mimeType) {
case "application/x-7z-compressed; charset=binary":
case "application/zip; charset=binary":
return await uncompressZipArchive(filePath, {
...options,
mimeType,
});
case "application/x-rar; charset=binary":
return await uncompressRarArchive(filePath, {
...options,
mimeType,
});
}
};