use webdav for files
This commit is contained in:
+13
-10
@@ -2,21 +2,24 @@
|
||||
* MetaMesh Plugin: file-info
|
||||
*
|
||||
* ============================================================================
|
||||
* PLUGIN MOUNT ARCHITECTURE - DO NOT MODIFY WITHOUT AUTHORIZATION
|
||||
* PLUGIN FILE ACCESS ARCHITECTURE - WebDAV
|
||||
* ============================================================================
|
||||
*
|
||||
* Each plugin container has exactly 3 mounts (2 for plugins without output):
|
||||
* Plugins access files via WebDAV served by meta-sort nginx:
|
||||
*
|
||||
* 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)
|
||||
* WEBDAV_URL env var: http://meta-sort-dev/webdav
|
||||
*
|
||||
* 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
|
||||
* Endpoints:
|
||||
* GET /webdav/watch/... - Read media files
|
||||
* GET /webdav/plugin/... - Read plugin output
|
||||
* PUT /webdav/plugin/... - Write plugin output
|
||||
* HEAD /webdav/... - Get file stats (size, mtime)
|
||||
*
|
||||
* SECURITY: Plugins must NEVER write to /files directly.
|
||||
* All write operations go to /cache or /files/plugin/<id> only.
|
||||
* This plugin (file-info) uses:
|
||||
* - HEAD requests for file size
|
||||
* - Range GET requests for MIME type detection (magic bytes)
|
||||
*
|
||||
* FALLBACK: If WEBDAV_URL is not set, uses direct filesystem access.
|
||||
*
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
+90
-14
@@ -10,16 +10,63 @@
|
||||
* - sizeByte
|
||||
* - fileName
|
||||
* - extension
|
||||
*
|
||||
* FILE ACCESS:
|
||||
* - Uses WebDAV client when WEBDAV_URL is set (containerized mode)
|
||||
* - Falls back to direct filesystem access (local development)
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileTypeFromBuffer } from 'file-type';
|
||||
import { FileType } from '@metazla/filename-tools';
|
||||
import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js';
|
||||
import { MetaCoreClient } from './meta-core-client.js';
|
||||
import { createWebDAVClient, WebDAVClient } from './webdav-client.js';
|
||||
|
||||
const fileType = new FileType();
|
||||
|
||||
// Initialize WebDAV client if WEBDAV_URL is set
|
||||
const webdavClient = createWebDAVClient();
|
||||
if (webdavClient) {
|
||||
console.log('[file-info] Using WebDAV for file access');
|
||||
} else {
|
||||
console.log('[file-info] Using direct filesystem access');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MIME type from file via WebDAV (reads first 4KB for magic bytes)
|
||||
*/
|
||||
async function getMimeTypeViaWebDAV(client: WebDAVClient, filePath: string): Promise<string | undefined> {
|
||||
try {
|
||||
// Read first 4KB for magic byte detection
|
||||
const buffer = await client.readBytes(filePath, 0, 4095);
|
||||
const result = await fileTypeFromBuffer(buffer);
|
||||
return result?.mime;
|
||||
} catch (error) {
|
||||
console.warn(`[file-info] Failed to detect MIME type via WebDAV: ${error}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file type using MIME type and extension mappings
|
||||
*/
|
||||
async function getFileTypeViaWebDAV(client: WebDAVClient, filePath: string): Promise<string> {
|
||||
const mimeType = await getMimeTypeViaWebDAV(client, filePath);
|
||||
const extensionType = fileType.getFileTypeFromExtension(filePath);
|
||||
|
||||
if (mimeType) {
|
||||
// Use the FileType class's mime type mapping
|
||||
const mimeBasedType = (fileType as any).mimeTypeMappings?.[mimeType];
|
||||
if (mimeBasedType && mimeBasedType !== 'undefined') {
|
||||
return mimeBasedType;
|
||||
}
|
||||
}
|
||||
|
||||
return extensionType;
|
||||
}
|
||||
|
||||
export const manifest: PluginManifest = {
|
||||
id: 'file-info',
|
||||
name: 'File Information',
|
||||
@@ -71,30 +118,59 @@ export async function process(
|
||||
try {
|
||||
const { cid, filePath } = request;
|
||||
|
||||
// Get file type (video, audio, image, etc.) - matches old FileProcessor
|
||||
const typeResult = await fileType.getFileType(filePath);
|
||||
// Extract filename and extension (works for both filesystem and WebDAV)
|
||||
const fileName = path.basename(filePath);
|
||||
const extension = path.extname(filePath).slice(1).toLowerCase();
|
||||
|
||||
let typeResult: string | undefined;
|
||||
let mimeType: string | undefined;
|
||||
let fileSize: number;
|
||||
|
||||
if (webdavClient) {
|
||||
// ============================================================
|
||||
// WebDAV Mode: Access files via HTTP
|
||||
// ============================================================
|
||||
|
||||
// Get file stats (size) via HTTP HEAD
|
||||
const stats = await webdavClient.stat(filePath);
|
||||
fileSize = stats.size;
|
||||
|
||||
// Get MIME type via HTTP Range request (reads first 4KB for magic bytes)
|
||||
mimeType = await getMimeTypeViaWebDAV(webdavClient, filePath);
|
||||
|
||||
// Get file type from MIME type or extension
|
||||
typeResult = await getFileTypeViaWebDAV(webdavClient, filePath);
|
||||
|
||||
} else {
|
||||
// ============================================================
|
||||
// Filesystem Mode: Direct file access (local development)
|
||||
// ============================================================
|
||||
|
||||
// Get file type (video, audio, image, etc.)
|
||||
typeResult = await fileType.getFileType(filePath);
|
||||
|
||||
// Get MIME type
|
||||
mimeType = await fileType.getMimeTypeFromFile(filePath);
|
||||
|
||||
// Get file size
|
||||
const stats = await fs.stat(filePath);
|
||||
fileSize = stats.size;
|
||||
}
|
||||
|
||||
// Store metadata via meta-core API
|
||||
if (typeResult) {
|
||||
await metaCore.setProperty(cid, 'fileType', typeResult);
|
||||
}
|
||||
|
||||
// Get MIME type - matches old FileProcessor
|
||||
const mimeType = await fileType.getMimeTypeFromFile(filePath);
|
||||
if (mimeType) {
|
||||
await metaCore.setProperty(cid, 'mimeType', mimeType);
|
||||
}
|
||||
|
||||
// Get file size - matches old FileProcessor
|
||||
const stats = await fs.stat(filePath);
|
||||
await metaCore.setProperty(cid, 'sizeByte', String(stats.size));
|
||||
|
||||
// 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, 'sizeByte', String(fileSize));
|
||||
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`);
|
||||
const mode = webdavClient ? 'WebDAV' : 'filesystem';
|
||||
console.log(`[file-info] Processed ${fileName} in ${duration}ms (${mode})`);
|
||||
|
||||
await sendCallback({
|
||||
taskId: request.taskId,
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* WebDAV Client for Plugin File Access
|
||||
*
|
||||
* Provides HTTP-based file access to meta-sort's WebDAV endpoint.
|
||||
* Replaces direct filesystem access for containerized plugins.
|
||||
*/
|
||||
|
||||
export interface FileStats {
|
||||
size: number;
|
||||
mtime?: Date;
|
||||
}
|
||||
|
||||
export class WebDAVClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
// Remove trailing slash if present
|
||||
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert absolute file path to WebDAV URL
|
||||
* Strips /files prefix and appends to base URL
|
||||
*/
|
||||
private toWebDAVUrl(filePath: string): string {
|
||||
// filePath is like "/files/watch/movie.mkv"
|
||||
// We need to convert to "http://meta-sort-dev/webdav/watch/movie.mkv"
|
||||
let relativePath = filePath;
|
||||
|
||||
// Strip /files prefix if present
|
||||
if (relativePath.startsWith('/files/')) {
|
||||
relativePath = relativePath.substring(6); // Remove "/files"
|
||||
} else if (relativePath.startsWith('/files')) {
|
||||
relativePath = relativePath.substring(6);
|
||||
}
|
||||
|
||||
// Ensure path starts with /
|
||||
if (!relativePath.startsWith('/')) {
|
||||
relativePath = '/' + relativePath;
|
||||
}
|
||||
|
||||
return this.baseUrl + relativePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file stats (size, mtime) via HTTP HEAD request
|
||||
*/
|
||||
async stat(filePath: string): Promise<FileStats> {
|
||||
const url = this.toWebDAVUrl(filePath);
|
||||
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`WebDAV HEAD failed for ${filePath}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const lastModified = response.headers.get('last-modified');
|
||||
|
||||
return {
|
||||
size: contentLength ? parseInt(contentLength, 10) : 0,
|
||||
mtime: lastModified ? new Date(lastModified) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read first N bytes of a file (for magic byte detection)
|
||||
* Uses HTTP Range request
|
||||
*/
|
||||
async readBytes(filePath: string, start: number, end: number): Promise<Buffer> {
|
||||
const url = this.toWebDAVUrl(filePath);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Range': `bytes=${start}-${end}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok && response.status !== 206) {
|
||||
throw new Error(`WebDAV Range GET failed for ${filePath}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read entire file as buffer (for small files only)
|
||||
*/
|
||||
async readFile(filePath: string): Promise<Buffer> {
|
||||
const url = this.toWebDAVUrl(filePath);
|
||||
|
||||
const response = await fetch(url, { method: 'GET' });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`WebDAV GET failed for ${filePath}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file via HTTP PUT
|
||||
*/
|
||||
async writeFile(filePath: string, data: Buffer | string): Promise<void> {
|
||||
const url = this.toWebDAVUrl(filePath);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`WebDAV PUT failed for ${filePath}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create directory via HTTP MKCOL
|
||||
*/
|
||||
async mkdir(dirPath: string): Promise<void> {
|
||||
const url = this.toWebDAVUrl(dirPath);
|
||||
|
||||
const response = await fetch(url.endsWith('/') ? url : url + '/', {
|
||||
method: 'MKCOL',
|
||||
});
|
||||
|
||||
// 201 Created or 405 Method Not Allowed (already exists) are both OK
|
||||
if (!response.ok && response.status !== 405) {
|
||||
throw new Error(`WebDAV MKCOL failed for ${dirPath}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file exists via HTTP HEAD
|
||||
*/
|
||||
async exists(filePath: string): Promise<boolean> {
|
||||
const url = this.toWebDAVUrl(filePath);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List directory contents via HTTP GET (returns JSON from nginx autoindex)
|
||||
*/
|
||||
async readdir(dirPath: string): Promise<string[]> {
|
||||
const url = this.toWebDAVUrl(dirPath);
|
||||
const finalUrl = url.endsWith('/') ? url : url + '/';
|
||||
|
||||
const response = await fetch(finalUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`WebDAV GET (dir) failed for ${dirPath}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const entries = await response.json() as Array<{ name: string; type: string }>;
|
||||
return entries.map(e => e.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a WebDAV client from environment variables
|
||||
*/
|
||||
export function createWebDAVClient(): WebDAVClient | null {
|
||||
const webdavUrl = process.env.WEBDAV_URL;
|
||||
|
||||
if (!webdavUrl) {
|
||||
console.warn('[webdav-client] WEBDAV_URL not set, WebDAV client unavailable');
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WebDAVClient(webdavUrl);
|
||||
}
|
||||
Reference in New Issue
Block a user