use webdav for files

This commit is contained in:
Pierre
2026-01-17 14:51:42 +01:00
parent 544b44a590
commit 24b1c2fcea
2 changed files with 219 additions and 5 deletions
+31 -5
View File
@@ -16,6 +16,15 @@ import ffmpeg from 'fluent-ffmpeg';
import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js'; import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js';
import { MetaCoreClient } from './meta-core-client.js'; import { MetaCoreClient } from './meta-core-client.js';
import { readJson, writeJson } from './cache.js'; import { readJson, writeJson } from './cache.js';
import { createWebDAVClient, WebDAVClient } from './webdav-client.js';
// Initialize WebDAV client if WEBDAV_URL is set
const webdavClient = createWebDAVClient();
if (webdavClient) {
console.log('[ffmpeg] Using WebDAV for file access');
} else {
console.log('[ffmpeg] Using direct filesystem access');
}
export const manifest: PluginManifest = { export const manifest: PluginManifest = {
id: 'ffmpeg', id: 'ffmpeg',
@@ -92,15 +101,30 @@ interface FileInfoCache {
}; };
} }
function runFFprobe(filePath: string): Promise<FFprobeData> { /**
* Run FFprobe on a file path or URL
* FFprobe natively supports HTTP URLs for input
*/
function runFFprobe(inputPath: string): Promise<FFprobeData> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ffmpeg(filePath).ffprobe((err, data) => { ffmpeg(inputPath).ffprobe((err, data) => {
if (err) reject(err); if (err) reject(err);
else resolve(data as FFprobeData); else resolve(data as FFprobeData);
}); });
}); });
} }
/**
* Get the input path for FFprobe - WebDAV URL or local file path
*/
function getFFprobeInput(filePath: string): string {
if (webdavClient) {
// FFprobe can read from HTTP URLs directly
return webdavClient.toWebDAVUrl(filePath);
}
return filePath;
}
export async function process( export async function process(
request: ProcessRequest, request: ProcessRequest,
sendCallback: (payload: CallbackPayload) => Promise<void> sendCallback: (payload: CallbackPayload) => Promise<void>
@@ -150,8 +174,9 @@ export async function process(
} }
} }
// Run FFprobe // Run FFprobe (uses WebDAV URL if available, otherwise local path)
const data = await runFFprobe(filePath); const ffprobeInput = getFFprobeInput(filePath);
const data = await runFFprobe(ffprobeInput);
const metadata: Record<string, string> = {}; const metadata: Record<string, string> = {};
const fileinfo: FileInfoCache = { streamdetails: {} }; const fileinfo: FileInfoCache = { streamdetails: {} };
@@ -287,7 +312,8 @@ export async function process(
} }
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
console.log(`[ffmpeg] Processed in ${duration}ms`); const mode = webdavClient ? 'WebDAV' : 'filesystem';
console.log(`[ffmpeg] Processed in ${duration}ms (${mode})`);
await sendCallback({ await sendCallback({
taskId: request.taskId, taskId: request.taskId,
+188
View File
@@ -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
*/
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);
}