This commit is contained in:
Pierre
2026-01-10 19:14:55 +01:00
parent 48492023ea
commit 4c88566b01
5 changed files with 113 additions and 86 deletions
+10
View File
@@ -3,10 +3,15 @@
FROM node:20-slim AS builder
# Install git for GitHub dependencies
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json tsconfig.json ./
RUN npm install
# Build the git dependency (filename-tools)
RUN cd node_modules/@metazla/filename-tools && npm install && npm run build
COPY src/ ./src/
RUN npm run build
@@ -16,8 +21,13 @@ FROM node:20-slim
WORKDIR /app
# Install git for GitHub dependencies
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
COPY package.json ./
RUN npm install --omit=dev
# Build the git dependency in production too
RUN cd node_modules/@metazla/filename-tools && npm install && npm run build
COPY --from=builder /app/dist ./dist
+2 -6
View File
@@ -6,18 +6,14 @@
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts"
"start": "node dist/index.js"
},
"dependencies": {
"fastify": "^5.0.0",
"file-type": "^19.0.0",
"mime-types": "^2.1.35"
"@metazla/filename-tools": "github:worph/filename-tool"
},
"devDependencies": {
"@types/mime-types": "^2.1.4",
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.3.0"
},
"engines": {
+54
View File
@@ -0,0 +1,54 @@
/**
* File-based cache utility for plugin data
* Matches the old ctx.cache API: readJson, writeJson
*/
import { readFile, writeFile, mkdir } from 'fs/promises';
import { existsSync } from 'fs';
import path from 'path';
const CACHE_DIR = process.env.CACHE_DIR || '/cache';
/**
* Ensure cache directory exists
*/
async function ensureCacheDir(): Promise<void> {
if (!existsSync(CACHE_DIR)) {
await mkdir(CACHE_DIR, { recursive: true });
}
}
/**
* Read JSON from cache
* @param filename - Cache filename (e.g., "abc123.json")
* @returns Parsed JSON object or null if not found
*/
export async function readJson<T = unknown>(filename: string): Promise<T | null> {
try {
const filePath = path.join(CACHE_DIR, filename);
if (!existsSync(filePath)) {
return null;
}
const content = await readFile(filePath, 'utf-8');
return JSON.parse(content) as T;
} catch (error) {
console.debug(`[cache] Failed to read ${filename}:`, error);
return null;
}
}
/**
* Write JSON to cache
* @param filename - Cache filename (e.g., "abc123.json")
* @param data - Data to cache
*/
export async function writeJson(filename: string, data: unknown): Promise<void> {
try {
await ensureCacheDir();
const filePath = path.join(CACHE_DIR, filename);
await writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
console.debug(`[cache] Wrote ${filename}`);
} catch (error) {
console.warn(`[cache] Failed to write ${filename}:`, error);
}
}
+19 -1
View File
@@ -1,6 +1,24 @@
/**
* MetaMesh Plugin: file-info
* Entry point
*
* ============================================================================
* PLUGIN MOUNT ARCHITECTURE - DO NOT MODIFY WITHOUT AUTHORIZATION
* ============================================================================
*
* Each plugin container has exactly 3 mounts (2 for plugins without output):
*
* 1. /files (READ-ONLY) - Shared media files, read access only
* 2. /cache (READ-WRITE) - Plugin-specific cache folder
* 3. /files/plugin/<id> (READ-WRITE) - Plugin output folder (if needed)
*
* This plugin (file-info) only requires mounts 1 and 2:
* - /files (RO) - to read files for type/size detection
* - /cache (RW) - to cache file info results
*
* SECURITY: Plugins must NEVER write to /files directly.
* All write operations go to /cache or /files/plugin/<id> only.
*
* ============================================================================
*/
import Fastify from 'fastify';
+28 -79
View File
@@ -3,15 +3,23 @@
*
* Extracts basic file information: type, MIME type, and size.
* This is the first plugin in the processing chain.
*
* Matches old FileProcessor output:
* - fileType
* - mimeType
* - sizeByte
* - fileName
* - extension
*/
import { stat } from 'fs/promises';
import { basename, extname } from 'path';
import { fileTypeFromFile } from 'file-type';
import { lookup as mimeTypeLookup } from 'mime-types';
import { promises as fs } from 'fs';
import * as path from 'path';
import { FileType } from '@metazla/filename-tools';
import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js';
import { MetaCoreClient } from './meta-core-client.js';
const fileType = new FileType();
export const manifest: PluginManifest = {
id: 'file-info',
name: 'File Information',
@@ -24,12 +32,6 @@ export const manifest: PluginManifest = {
defaultQueue: 'fast',
timeout: 30000,
schema: {
privateFilePath: {
label: 'Original File Path',
type: 'string',
readonly: true,
hint: 'Physical path on disk (used by Stremio addon and FUSE)',
},
fileType: {
label: 'File Type',
type: 'string',
@@ -59,41 +61,6 @@ export const manifest: PluginManifest = {
config: {},
};
// File type detection based on extension (matching @metazla/filename-tools)
// Note: Images are classified as 'document' not 'image' per original design
const EXTENSION_MAPPINGS: Record<string, string> = {
// Video
'mp4': 'video', 'mkv': 'video', 'webm': 'video', 'avi': 'video',
'mov': 'video', 'wmv': 'video', 'flv': 'video', 'm4v': 'video',
'mpg': 'video', 'mpeg': 'video', '3gp': 'video', '3g2': 'video',
'f4v': 'video', 'm2ts': 'video', 'mts': 'video', 'ts': 'video',
'vob': 'video', 'ogm': 'video', 'divx': 'video', 'xvid': 'video',
// Audio
'mp3': 'audio', 'wav': 'audio', 'flac': 'audio', 'aac': 'audio',
'ogg': 'audio', 'm4a': 'audio',
// Document (includes images per original design)
'pdf': 'document', 'doc': 'document', 'docx': 'document',
'ppt': 'document', 'pptx': 'document', 'xls': 'document',
'xlsx': 'document', 'txt': 'document', 'rtf': 'document',
// Images are classified as 'document' per original library
'jpg': 'document', 'jpeg': 'document', 'png': 'document',
'gif': 'document', 'bmp': 'document', 'tif': 'document',
'tiff': 'document', 'svg': 'document', 'webp': 'document',
// Archive
'zip': 'archive', 'rar': 'archive', '7z': 'archive',
'tar': 'archive', 'gz': 'archive', 'bz2': 'archive', 'xz': 'archive',
// Subtitles
'srt': 'subtitle', 'sub': 'subtitle', 'sbv': 'subtitle', 'vtt': 'subtitle',
// Torrent
'torrent': 'torrent',
};
function getFileTypeFromExtension(ext: string): string {
if (!ext) return 'undefined';
const lowerExt = ext.toLowerCase();
return EXTENSION_MAPPINGS[lowerExt] || 'other';
}
export async function process(
request: ProcessRequest,
sendCallback: (payload: CallbackPayload) => Promise<void>
@@ -104,44 +71,27 @@ export async function process(
try {
const { cid, filePath } = request;
// Get file stats
const stats = await stat(filePath);
// Get file name and extension
const fileName = basename(filePath);
const extension = extname(filePath).slice(1).toLowerCase();
// Determine file type
let fileType = getFileTypeFromExtension(extension);
// Try to get MIME type from file magic bytes
let mimeType: string | undefined;
try {
const detected = await fileTypeFromFile(filePath);
if (detected) {
mimeType = detected.mime;
}
} catch {
// Fallback to extension-based lookup
// Get file type (video, audio, image, etc.) - matches old FileProcessor
const typeResult = await fileType.getFileType(filePath);
if (typeResult) {
await metaCore.setProperty(cid, 'fileType', typeResult);
}
// Fallback MIME type lookup by extension
if (!mimeType) {
mimeType = mimeTypeLookup(extension) || 'application/octet-stream';
// Get MIME type - matches old FileProcessor
const mimeType = await fileType.getMimeTypeFromFile(filePath);
if (mimeType) {
await metaCore.setProperty(cid, 'mimeType', mimeType);
}
// Prepare metadata
const metadata = {
fileType,
mimeType,
sizeByte: String(stats.size),
fileName,
extension,
filePath, // Store the file path for other plugins/services
};
// Get file size - matches old FileProcessor
const stats = await fs.stat(filePath);
await metaCore.setProperty(cid, 'sizeByte', String(stats.size));
// Write metadata to meta-core
await metaCore.mergeMetadata(cid, metadata);
// Also store filename and extension for convenience - matches old FileProcessor
const fileName = path.basename(filePath);
const extension = path.extname(filePath).slice(1).toLowerCase();
await metaCore.setProperty(cid, 'fileName', fileName);
await metaCore.setProperty(cid, 'extension', extension);
const duration = Date.now() - startTime;
console.log(`[file-info] Processed ${fileName} in ${duration}ms`);
@@ -150,7 +100,6 @@ export async function process(
taskId: request.taskId,
status: 'completed',
duration,
metadata, // Include metadata in callback as fallback
});
} catch (error) {
const duration = Date.now() - startTime;