DragonWind/lib/ampp-folder-placer.js

207 lines
8.4 KiB
JavaScript

"use strict";
// ================================================================
// AMPP Folder Placer — lib/ampp-folder-placer.js
//
// placeAsset() — original v3.5 port (filename -- parsing)
// placeAssetInFolderById() — direct placement by known folder ID
// getOrCreateFolderByPath() — hierarchy lookup + create, returns folder ID
// listAmppFolders() — fetch AMPP virtual folder tree (probes endpoints)
// amppRequest() — shared low-level REST helper
// ================================================================
const PREFIX_DELIM = "--";
// ── Low-level REST helper ─────────────────────────────────────────────────────
async function amppRequest(method, baseUrl, token, apiPath, body = null) {
const url = `${baseUrl.replace(/\/$/, "")}/${apiPath}`;
const opts = {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(15000),
};
if (body !== null) {
opts.body = typeof body === "string" ? body : JSON.stringify(body);
}
const r = await fetch(url, opts);
if (!r.ok) {
const text = await r.text().catch(() => "");
throw new Error(
`AMPP API ${method} ${apiPath} → HTTP ${r.status}: ${text.slice(0, 200)}`
);
}
return r.json();
}
// ── Get or create a folder by slash-separated path ───────────────────────────
// e.g. "NEWS/PACKAGES" → looks up hierarchy, creates missing levels, returns ID.
async function getOrCreateFolderByPath(baseUrl, folderPath, token) {
const parts = folderPath.split("/").map(s => s.trim()).filter(Boolean);
if (!parts.length) throw new Error("folderPath is empty");
let parentId = null;
let currentPath = "";
let expectedDepth = 0;
for (const fname of parts) {
currentPath = currentPath ? `${currentPath}/${fname}` : fname;
expectedDepth++;
let folderId = null;
// Try hierarchy lookup first
try {
const encoded = currentPath.split("/").map(encodeURIComponent).join("/");
const resp = await amppRequest("GET", baseUrl, token,
`api/v1/store/folder/folders/hierarchy?path=${encoded}`);
const hlist = resp?.["hierarchy:list"];
if (Array.isArray(hlist) && hlist.length === expectedDepth) {
const last = hlist[hlist.length - 1];
const candidateId = (last?.["folder:id"] ?? "").toString().trim();
if (candidateId) folderId = candidateId;
}
} catch (err) {
console.warn(`[AMPP] Hierarchy lookup for '${currentPath}': ${err.message}`);
}
// Create the folder if not found
if (!folderId) {
const createBody = { "name:text": fname };
if (parentId) createBody["parentFolders:tags"] = [parentId];
const fresp = await amppRequest("POST", baseUrl, token,
"api/v1/store/folder/folders", createBody);
const createdId = (fresp?.["folder:id"] ?? "").toString().trim();
if (!createdId) throw new Error(`Folder create response missing folder:id for '${fname}'`);
folderId = createdId;
}
parentId = folderId;
}
return parentId; // ID of the deepest folder
}
// ── List AMPP virtual folders ────────────────────────────────────────────────
// Probes multiple candidate endpoints in order; uses the first that responds.
// Returns: { folders: [{id, name, parentId, path}], endpoint }
async function listAmppFolders(baseUrl, token) {
let lastError = null;
// Helper: normalise raw array of folder objects
function normaliseFolders(raw) {
if (!Array.isArray(raw)) return null;
const folders = raw.map((f) => ({
id: (f["folder:id"] ?? f.id ?? "").toString().trim(),
name: (f["name:text"] ?? f.name ?? f["folder:name"] ?? "").toString().trim(),
parentId: (f["parentFolder:id"] ?? f.parentId ?? f["parent:id"] ?? "").toString().trim(),
path: (f["folder:path"] ?? f.path ?? "").toString().trim(),
})).filter(f => f.id && f.name);
return folders;
}
// 1. Try POST querypage (same pattern as the jobs API)
const queryPagePaths = [
"api/v1/store/folder/folders/querypage?skip=0&limit=500&sort=name:text&asc=true",
"api/v1/store/folder/folders/querypage",
];
for (const candidate of queryPagePaths) {
try {
const resp = await amppRequest("POST", baseUrl, token, candidate, "{}");
const raw = Array.isArray(resp) ? resp
: (resp?.["folder:list"] ?? resp?.items ?? resp?.results ?? resp?.folders ?? []);
const folders = normaliseFolders(raw);
if (folders && folders.length > 0) {
console.log(`[AMPP folders] ${folders.length} folders via POST ${candidate}`);
return { folders, endpoint: `POST ${candidate}` };
}
} catch (err) {
lastError = err;
console.warn(`[AMPP folders] POST ${candidate} failed: ${err.message}`);
}
}
// 2. Try GET candidates
const getCandidates = [
"api/v1/store/folder/folders?limit=500",
"api/v1/store/folder/folders",
"api/v1/store/folder/folder/list",
"api/v1/store/folder/folders/list",
];
for (const candidate of getCandidates) {
try {
const resp = await amppRequest("GET", baseUrl, token, candidate);
const raw = Array.isArray(resp) ? resp
: (resp?.["folder:list"] ?? resp?.folders ?? resp?.items ?? resp?.results ?? []);
const folders = normaliseFolders(raw);
if (folders && folders.length > 0) {
console.log(`[AMPP folders] ${folders.length} folders via GET ${candidate}`);
return { folders, endpoint: candidate };
}
} catch (err) {
lastError = err;
console.warn(`[AMPP folders] GET ${candidate} failed: ${err.message}`);
}
}
// 3. Hierarchy walk: call hierarchy with empty path to probe root-level folders
try {
const resp = await amppRequest("GET", baseUrl, token, "api/v1/store/folder/folders/hierarchy?path=");
const hlist = resp?.["hierarchy:list"] ?? resp?.["folder:list"];
if (Array.isArray(hlist) && hlist.length > 0) {
const folders = normaliseFolders(hlist);
if (folders && folders.length > 0) {
console.log(`[AMPP folders] ${folders.length} root folders via hierarchy walk`);
return { folders, endpoint: "hierarchy-root" };
}
}
} catch (err) {
lastError = err;
console.warn(`[AMPP folders] hierarchy root probe failed: ${err.message}`);
}
// All endpoints failed — caller should fall back to manual path entry
throw new Error(
`Could not list AMPP folders — no list endpoint available. ${lastError?.message ?? ""}`
);
}
// ── Direct placement by known folder ID ──────────────────────────────────────
async function placeAssetInFolderById(baseUrl, assetId, folderId, token) {
if (!assetId) throw new Error("assetId is required");
if (!folderId) throw new Error("folderId is required");
const linkBody = JSON.stringify(
{ "folder:id": folderId, "asset:id": assetId },
null, 0
);
await amppRequest("POST", baseUrl, token, "api/v1/store/folder/references", linkBody);
return { placed: true, folderId, assetId };
}
// ── Original v3.5 placement (filename -- parsing) ────────────────────────────
async function placeAsset(baseUrl, assetName, assetId, token) {
if (!assetName.includes(PREFIX_DELIM)) {
return { placed: false, folderId: null, path: "", reason: `no "${PREFIX_DELIM}" delimiter in filename` };
}
const parts = assetName.split(PREFIX_DELIM);
if (parts.length < 2) {
return { placed: false, folderId: null, path: "", reason: "fewer than 2 parts after split" };
}
const folderNames = parts.slice(0, -1);
const folderPath = folderNames.map(s => s.trim()).filter(Boolean).join("/");
const folderId = await getOrCreateFolderByPath(baseUrl, folderPath, token);
if (folderId) {
const linkBody = JSON.stringify({ "folder:id": folderId, "asset:id": assetId }, null, 0);
await amppRequest("POST", baseUrl, token, "api/v1/store/folder/references", linkBody);
}
return { placed: true, folderId, path: folderPath };
}
module.exports = { placeAsset, placeAssetInFolderById, getOrCreateFolderByPath, listAmppFolders, amppRequest };