feat(scripts): add list-webdav script for WebDAV directory listing
Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
@@ -0,0 +1,230 @@
|
|||||||
|
import { createClient } from "webdav";
|
||||||
|
|
||||||
|
function passContext(ctx) {
|
||||||
|
if (ctx?.context != null && typeof ctx.context === "object" && !Array.isArray(ctx.context)) {
|
||||||
|
return { ...ctx.context };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUrl(ctx) {
|
||||||
|
const fromConfig = ctx.config?.url;
|
||||||
|
if (typeof fromConfig === "string" && fromConfig.length > 0) {
|
||||||
|
return fromConfig;
|
||||||
|
}
|
||||||
|
const fromData = ctx.data?.url;
|
||||||
|
if (typeof fromData === "string" && fromData.length > 0) {
|
||||||
|
return fromData;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePath(ctx) {
|
||||||
|
const raw = ctx.config?.path ?? ctx.data?.path ?? "/";
|
||||||
|
if (typeof raw !== "string" || raw.length === 0) {
|
||||||
|
throw new Error("list-webdav: path must be a non-empty string");
|
||||||
|
}
|
||||||
|
return raw.startsWith("/") ? raw : `/${raw}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeaders(value, label) {
|
||||||
|
if (value == null) return {};
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
throw new Error(`list-webdav: ${label} must be an object`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
const headers = {};
|
||||||
|
for (const [key, val] of Object.entries(value)) {
|
||||||
|
if (val == null) continue;
|
||||||
|
if (typeof val === "string") {
|
||||||
|
headers[key] = val;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof val === "number" || typeof val === "boolean") {
|
||||||
|
headers[key] = String(val);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(`list-webdav: header "${key}" must be a string`);
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIsoDate(value) {
|
||||||
|
if (value == null || value === "") return null;
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return String(value);
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} item
|
||||||
|
*/
|
||||||
|
function normalizeEntry(item) {
|
||||||
|
const type = item.type === "directory" ? "directory" : "file";
|
||||||
|
return {
|
||||||
|
name: typeof item.basename === "string" ? item.basename : null,
|
||||||
|
path: typeof item.filename === "string" ? item.filename : null,
|
||||||
|
type,
|
||||||
|
size: type === "directory" ? null : typeof item.size === "number" ? item.size : null,
|
||||||
|
modified: toIsoDate(item.lastmod),
|
||||||
|
contentType: typeof item.mime === "string" ? item.mime : null,
|
||||||
|
etag: item.etag == null ? null : String(item.etag),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolvePassword(ctx) {
|
||||||
|
if (typeof ctx.config?.passwordSecret === "string" && ctx.config.passwordSecret.length > 0) {
|
||||||
|
return $secrets.reveal(await $secrets.get(ctx.config.passwordSecret));
|
||||||
|
}
|
||||||
|
if (typeof ctx.config?.password === "string") {
|
||||||
|
return ctx.config.password;
|
||||||
|
}
|
||||||
|
if (typeof ctx.data?.password === "string") {
|
||||||
|
return ctx.data.password;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listWebdav(ctx) {
|
||||||
|
const url = resolveUrl(ctx);
|
||||||
|
if (!url) {
|
||||||
|
throw new Error("list-webdav: url is required (ctx.config.url or ctx.data.url)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const remotePath = resolvePath(ctx);
|
||||||
|
const includeDirectories = ctx.config?.includeDirectories !== false;
|
||||||
|
const recursive = ctx.config?.recursive === true;
|
||||||
|
const glob = typeof ctx.config?.glob === "string" && ctx.config.glob.length > 0
|
||||||
|
? ctx.config.glob
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const username =
|
||||||
|
typeof ctx.config?.username === "string" && ctx.config.username.length > 0
|
||||||
|
? ctx.config.username
|
||||||
|
: typeof ctx.data?.username === "string" && ctx.data.username.length > 0
|
||||||
|
? ctx.data.username
|
||||||
|
: undefined;
|
||||||
|
const password = await resolvePassword(ctx);
|
||||||
|
const headers = {
|
||||||
|
...normalizeHeaders(ctx.config?.headers, "config.headers"),
|
||||||
|
...normalizeHeaders(ctx.data?.headers, "data.headers"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {Record<string, unknown>} */
|
||||||
|
const clientOptions = {};
|
||||||
|
if (username) clientOptions.username = username;
|
||||||
|
if (password) clientOptions.password = password;
|
||||||
|
if (Object.keys(headers).length > 0) clientOptions.headers = headers;
|
||||||
|
|
||||||
|
/** @type {Record<string, unknown>} */
|
||||||
|
const listOptions = { deep: recursive };
|
||||||
|
if (glob) listOptions.glob = glob;
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
{ url, path: remotePath, recursive, includeDirectories, glob: glob ?? null },
|
||||||
|
"list-webdav: listing directory",
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = createClient(url, clientOptions);
|
||||||
|
const rawItems = await client.getDirectoryContents(remotePath, listOptions);
|
||||||
|
const items = Array.isArray(rawItems) ? rawItems : rawItems.data;
|
||||||
|
|
||||||
|
let entries = items.map((item) => normalizeEntry(/** @type {Record<string, unknown>} */ (item)));
|
||||||
|
if (!includeDirectories) {
|
||||||
|
entries = entries.filter((entry) => entry.type !== "directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = { entries, count: entries.length };
|
||||||
|
log.info({ count: output.count, path: remotePath }, "list-webdav: listing complete");
|
||||||
|
|
||||||
|
return { output, context: { ...passContext(ctx), ...output } };
|
||||||
|
}
|
||||||
|
|
||||||
|
listWebdav.meta = {
|
||||||
|
description: "List files on a WebDAV server with names and file stats",
|
||||||
|
previewConfigKey: "url",
|
||||||
|
tags: ["WebDAV"],
|
||||||
|
config: {
|
||||||
|
url: {
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
description: "WebDAV base URL (or pass data.url)",
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: "string",
|
||||||
|
default: "/",
|
||||||
|
description: "Remote directory path to list",
|
||||||
|
},
|
||||||
|
includeDirectories: {
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
description: "Include directory entries in the result",
|
||||||
|
},
|
||||||
|
recursive: {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
description: "List subdirectories recursively (maps to webdav deep option)",
|
||||||
|
},
|
||||||
|
glob: {
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
description: "Optional glob filter (e.g. /**/*.pdf); implies deep listing on the client",
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
description: "Basic auth username",
|
||||||
|
},
|
||||||
|
passwordSecret: {
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
description: "Named secret holding the WebDAV password",
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
description: "Plain password (prefer passwordSecret in production)",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: "object",
|
||||||
|
required: false,
|
||||||
|
description: "Extra request headers",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
url: { type: "string", required: false, description: "Fallback WebDAV URL" },
|
||||||
|
path: { type: "string", required: false, description: "Fallback directory path" },
|
||||||
|
username: { type: "string", required: false },
|
||||||
|
password: { type: "string", required: false },
|
||||||
|
headers: { type: "object", required: false },
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entries: {
|
||||||
|
type: "array",
|
||||||
|
description: "Listed files and optionally directories with stats",
|
||||||
|
},
|
||||||
|
count: { type: "number", description: "Number of entries returned" },
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
entries: { type: "array", description: "Same as output.entries" },
|
||||||
|
count: { type: "number", description: "Same as output.count" },
|
||||||
|
},
|
||||||
|
example: {
|
||||||
|
data: {},
|
||||||
|
config: {
|
||||||
|
url: "https://cloud.example.com/remote.php/dav/files/me/",
|
||||||
|
path: "/Documents",
|
||||||
|
includeDirectories: true,
|
||||||
|
recursive: false,
|
||||||
|
username: "me",
|
||||||
|
passwordSecret: "cloud_password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default listWebdav;
|
||||||
Reference in New Issue
Block a user