/** * API Endpoints - Centralized URL constants */ import { normalizeAssetId, pickRootId } from "../utils/ids.js"; import { hasWindowsDrivePrefix, splitFilenameAndSubfolder, toPosixPath } from "../utils/path.js"; const DEFAULT_QUERY_LIMIT = 200; const DEFAULT_QUERY_OFFSET = 0; export interface AssetFilterParams { subfolder?: string | null; kind?: string | null; hasWorkflow?: boolean | null; minRating?: number | null; minSizeMB?: number | null; maxSizeMB?: number | null; minWidth?: number | null; minHeight?: number | null; maxWidth?: number | null; maxHeight?: number | null; workflowType?: string | null; workflowId?: string | null; workflowModel?: string | null; runsOn?: string | null; dateRange?: string | null; dateExact?: string | null; } export interface ListURLParams extends AssetFilterParams { q?: string; limit?: number; offset?: number; scope?: string; customRootId?: string | null; resolutionCompare?: string | null; sort?: string | null; cursor?: string | null; includeTotal?: boolean; groupStacks?: boolean; metadataMode?: string | null; } export const ENDPOINTS = { // Health & Config HEALTH: "/mjr/am/health", HEALTH_COUNTERS: "/mjr/am/health/counters", HEALTH_DB: "/mjr/am/health/db", STATUS: "/mjr/am/status", RUNTIME_EXECUTION: "/mjr/am/runtime/execution", CONFIG: "/mjr/am/config", VERSION: "/mjr/am/version", // Index & Search SCAN: "/mjr/am/scan", INDEX_FILES: "/mjr/am/index-files", INDEX_RESET: "/mjr/am/index/reset", DB_RESET: "/mjr/am/db/reset", SEARCH: "/mjr/am/search", LIST: "/mjr/am/list", ROOTS: "/mjr/am/roots", CUSTOM_ROOTS: "/mjr/am/custom-roots", CUSTOM_ROOTS_REMOVE: "/mjr/am/custom-roots/remove", BROWSER_FOLDER_OP: "/mjr/am/browser/folder-op", BROWSE_FOLDER: "/mjr/sys/browse-folder", FOLDER_INFO: "/mjr/am/folder-info", // Metadata METADATA: "/mjr/am/metadata", METADATA_CATALOG: "/mjr/am/metadata/catalog", METADATA_KEYS: "/mjr/am/metadata/keys", METADATA_PARSER_VERSION: "/mjr/am/metadata/parser-version", WORKFLOW_QUICK: "/mjr/am/workflow-quick", RETRY_SERVICES: "/mjr/am/retry-services", STAGE_TO_INPUT: "/mjr/am/stage-to-input", OPEN_IN_FOLDER: "/mjr/am/open-in-folder", COLLECT_FILES: "/mjr/am/collect-files", TOOLS_STATUS: "/mjr/am/tools/status", SETTINGS_OUTPUT_DIRECTORY: "/mjr/am/settings/output-directory", SETTINGS_INDEX_DIRECTORY: "/mjr/am/settings/index-directory", SETTINGS_WORKFLOW_ROOTS: "/mjr/am/settings/workflow-roots", SETTINGS_METADATA_FALLBACK: "/mjr/am/settings/metadata-fallback", SETTINGS_VECTOR_SEARCH: "/mjr/am/settings/vector-search", SETTINGS_VECTOR_SEARCH_UNLOAD: "/mjr/am/settings/vector-search/unload", SETTINGS_EXECUTION_GROUPING: "/mjr/am/settings/execution-grouping", SETTINGS_HUGGINGFACE: "/mjr/am/settings/huggingface", SETTINGS_AI_LOGGING: "/mjr/am/settings/ai-logging", SETTINGS_ROUTE_LOGGING: "/mjr/am/settings/route-logging", SETTINGS_STARTUP_LOGGING: "/mjr/am/settings/startup-logging", SETTINGS_LTXAV_RGB_FALLBACK: "/mjr/am/settings/ltxav-rgb-fallback", SETTINGS_JXL: "/mjr/am/settings/jxl", SETTINGS_BROWSER_SHOW_FOLDERS: "/mjr/am/settings/browser-show-folders", // View (ComfyUI native) VIEW: "/view", // Custom roots view (Majoor) CUSTOM_VIEW: "/mjr/am/custom-view", // Viewer helpers (Majoor) VIEWER_INFO: "/mjr/am/viewer/info", VIEWER_RESOURCE: "/mjr/am/viewer/resource", THUMBNAIL: "/mjr/am/thumbnail", // File upload UPLOAD_INPUT: "/mjr/am/upload_input", // File download DOWNLOAD: "/mjr/am/download", DOWNLOAD_CLEAN: "/mjr/am/download-clean", // Drag-out (OS) helpers BATCH_ZIP_CREATE: "/mjr/am/batch-zip", // Calendar helpers DATE_HISTOGRAM: "/mjr/am/date-histogram", // Asset operations ASSET_DELETE: "/mjr/am/asset/delete", ASSET_RENAME: "/mjr/am/asset/rename", // Watcher WATCHER_STATUS: "/mjr/am/watcher/status", WATCHER_TOGGLE: "/mjr/am/watcher/toggle", WATCHER_SCOPE: "/mjr/am/watcher/scope", WATCHER_SETTINGS: "/mjr/am/watcher/settings", // Collections COLLECTIONS: "/mjr/am/collections", // Stacks STACKS: "/mjr/am/stacks", STACKS_MERGE: "/mjr/am/stacks/merge", STACKS_DISSOLVE: "/mjr/am/stacks/dissolve", STACKS_AUTO_STACK: "/mjr/am/stacks/auto-stack", // Duplicate detection DUPLICATES_ALERTS: "/mjr/am/duplicates/alerts", DUPLICATES_ANALYZE: "/mjr/am/duplicates/analyze", DUPLICATES_MERGE_TAGS: "/mjr/am/duplicates/merge-tags", // DB backup/restore DB_BACKUPS: "/mjr/am/db/backups", DB_BACKUP_SAVE: "/mjr/am/db/backup-save", DB_BACKUP_RESTORE: "/mjr/am/db/backup-restore", // Vector / Semantic search VECTOR_SEARCH: "/mjr/am/vector/search", VECTOR_SIMILAR: "/mjr/am/vector/similar", VECTOR_ALIGNMENT: "/mjr/am/vector/alignment", VECTOR_AUTO_TAGS: "/mjr/am/vector/auto-tags", VECTOR_CAPTION: "/mjr/am/vector/caption", // Legacy alias kept for backward compatibility. VECTOR_ENHANCED_PROMPT: "/mjr/am/vector/enhanced-prompt", VECTOR_INDEX: "/mjr/am/vector/index", VECTOR_STATS: "/mjr/am/vector/stats", VECTOR_SUGGEST_COLLECTIONS: "/mjr/am/vector/suggest-collections", VECTOR_BACKFILL: "/mjr/am/db/backfill-missing-vectors", VECTOR_BACKFILL_STATUS: "/mjr/am/db/backfill-missing-vectors/status", // Hybrid search (FTS + semantic) HYBRID_SEARCH: "/mjr/am/search/hybrid", // Library audit AUDIT: "/mjr/am/audit", // Workflow library WORKFLOWS_CONTENT: "/mjr/am/workflows/content", WORKFLOWS_VALIDATE: "/mjr/am/workflows/validate", WORKFLOWS_VERSIONS: "/mjr/am/workflows/versions", WORKFLOWS_DIFF: "/mjr/am/workflows/diff", WORKFLOWS_SAVE: "/mjr/am/workflows/save", WORKFLOWS_DUPLICATE: "/mjr/am/workflows/duplicate", WORKFLOWS_MOVE: "/mjr/am/workflows/move", WORKFLOWS_DELETE: "/mjr/am/workflows/delete", WORKFLOWS_MARK_LOADED: "/mjr/am/workflows/mark-loaded", WORKFLOWS_FAVORITE: "/mjr/am/workflows/favorite", WORKFLOWS_INFO: "/mjr/am/workflows/info", WORKFLOWS_THUMBNAIL_CANDIDATES: "/mjr/am/workflows/thumbnail-candidates", WORKFLOWS_MODEL_FAMILIES: "/mjr/am/workflows/model-families", WORKFLOWS_TAGS: "/mjr/am/workflows/tags", WORKFLOWS_THUMBNAIL_SET: "/mjr/am/workflows/thumbnail/set", WORKFLOWS_OPEN_ROOT: "/mjr/am/workflows/open-root", }; export function appendAssetFilterQueryParams(url: string, filters: AssetFilterParams = {}): string { let nextUrl = String(url || ""); // Use correct separator based on whether URL already has query params const _sep = () => { const s = nextUrl.includes("?") ? "&" : "?"; return s; }; const _append = (param: any) => { nextUrl += _sep() + param; }; const { subfolder = null, kind = null, hasWorkflow = null, minRating = null, minSizeMB = null, maxSizeMB = null, minWidth = null, minHeight = null, maxWidth = null, maxHeight = null, workflowType = null, workflowId = null, workflowModel = null, runsOn = null, dateRange = null, dateExact = null, } = filters || {}; if (subfolder) { _append(`subfolder=${encodeURIComponent(String(subfolder))}`); } if (kind) { _append(`kind=${encodeURIComponent(kind)}`); } if (hasWorkflow !== null && hasWorkflow !== undefined) { _append(`has_workflow=${encodeURIComponent(hasWorkflow ? "true" : "false")}`); } if (minRating !== null && minRating !== undefined && Number(minRating) > 0) { _append(`min_rating=${encodeURIComponent(String(minRating))}`); } if (minSizeMB !== null && minSizeMB !== undefined && Number(minSizeMB) > 0) { _append(`min_size_mb=${encodeURIComponent(String(minSizeMB))}`); } if (maxSizeMB !== null && maxSizeMB !== undefined && Number(maxSizeMB) > 0) { _append(`max_size_mb=${encodeURIComponent(String(maxSizeMB))}`); } if (minWidth !== null && minWidth !== undefined && Number(minWidth) > 0) { _append(`min_width=${encodeURIComponent(String(minWidth))}`); } if (minHeight !== null && minHeight !== undefined && Number(minHeight) > 0) { _append(`min_height=${encodeURIComponent(String(minHeight))}`); } if (maxWidth !== null && maxWidth !== undefined && Number(maxWidth) > 0) { _append(`max_width=${encodeURIComponent(String(maxWidth))}`); } if (maxHeight !== null && maxHeight !== undefined && Number(maxHeight) > 0) { _append(`max_height=${encodeURIComponent(String(maxHeight))}`); } if (workflowType) { _append(`workflow_type=${encodeURIComponent(String(workflowType))}`); } if (workflowId) { _append(`workflow_id=${encodeURIComponent(String(workflowId))}`); } if (workflowModel) { _append(`workflow_model=${encodeURIComponent(String(workflowModel))}`); } if (runsOn) { _append(`runs_on=${encodeURIComponent(String(runsOn))}`); } if (dateRange) { _append(`date_range=${encodeURIComponent(String(dateRange))}`); } if (dateExact) { _append(`date_exact=${encodeURIComponent(String(dateExact))}`); } return nextUrl; } /** * Build view URL for asset */ export function buildViewURL(filename: any, subfolder: string | null = null, type = "output"): string { const fn = String(filename || "").trim(); if (!fn) return ""; let url = `${ENDPOINTS.VIEW}?filename=${encodeURIComponent(fn)}`; if (subfolder) { url += `&subfolder=${encodeURIComponent(subfolder)}`; } url += `&type=${encodeURIComponent(type)}`; return url; } export function buildThumbnailURL(asset: any, size = 384): string { const filepath = String(asset?.filepath || asset?.path || asset?.file_path || "").trim(); if (!filepath) return ""; const requestedSize = Math.max(64, Math.min(1024, Math.floor(Number(size) || 384))); return `${ENDPOINTS.THUMBNAIL}?filepath=${encodeURIComponent(filepath)}&size=${encodeURIComponent(String(requestedSize))}`; } /** * Build list URL with scope support. */ export function buildListURL(params: ListURLParams = {}): string { const { q = "*", limit = DEFAULT_QUERY_LIMIT, offset = DEFAULT_QUERY_OFFSET, scope = "output", subfolder = "", customRootId = null, kind = null, hasWorkflow = null, minRating = null, minSizeMB = null, maxSizeMB = null, resolutionCompare = null, minWidth = null, minHeight = null, maxWidth = null, maxHeight = null, workflowType = null, workflowId = null, workflowModel = null, runsOn = null, dateRange = null, dateExact = null, sort = null, cursor = null, includeTotal = true, groupStacks = false, metadataMode = null, } = params; let url = `${ENDPOINTS.LIST}?q=${encodeURIComponent(q)}&limit=${limit}&offset=${offset}&scope=${encodeURIComponent(scope)}`; if (subfolder) { url += `&subfolder=${encodeURIComponent(subfolder)}`; } if (customRootId) { url += `&custom_root_id=${encodeURIComponent(customRootId)}`; } else if (String(scope || "").toLowerCase() === "custom") { // Browser scope without selected root uses absolute filesystem paths. url += "&browser_mode=1"; } url = appendAssetFilterQueryParams(url, { kind, hasWorkflow, minRating, minSizeMB, maxSizeMB, minWidth, minHeight, maxWidth, maxHeight, workflowType, workflowId, workflowModel, runsOn, dateRange, dateExact, }); if (resolutionCompare) { url += `&resolution_compare=${encodeURIComponent(String(resolutionCompare))}`; } if (sort) { url += `&sort=${encodeURIComponent(String(sort))}`; } if (cursor) { url += `&cursor=${encodeURIComponent(String(cursor))}`; } if (includeTotal === false) { url += `&include_total=0`; } if (groupStacks) { url += "&group_stacks=1"; } if (metadataMode) { url += `&metadata_mode=${encodeURIComponent(String(metadataMode).toUpperCase())}`; } return url; } export function buildCustomViewURL(filename: any, subfolder = "", rootId = ""): string { const fn = String(filename || "").trim(); const rid = String(rootId || "").trim(); if (!fn || !rid) return ""; let url = `${ENDPOINTS.CUSTOM_VIEW}?root_id=${encodeURIComponent(rid)}&filename=${encodeURIComponent(fn)}`; if (subfolder) { url += `&subfolder=${encodeURIComponent(subfolder)}`; } return url; } export function buildBatchZipDownloadURL(token: any): string { return `${ENDPOINTS.BATCH_ZIP_CREATE}/${encodeURIComponent(String(token || ""))}`; } export function buildStacksURL({ limit = 50, offset = 0, includeTotal = true }: { limit?: number; offset?: number; includeTotal?: boolean } = {}): string { let url = `${ENDPOINTS.STACKS}?limit=${encodeURIComponent(String(limit))}&offset=${encodeURIComponent(String(offset))}`; if (includeTotal === false) url += "&include_total=0"; return url; } export function buildStackURL(stackId: any): string { return `${ENDPOINTS.STACKS}/${encodeURIComponent(String(stackId || ""))}`; } export function buildStackMembersURL(stackId: any, options: { limit?: number } = {}): string { let url = `${buildStackURL(stackId)}/members`; const limit = Number(options?.limit || 0); if (Number.isFinite(limit) && limit > 0) { url += `?limit=${encodeURIComponent(String(Math.floor(limit)))}`; } return url; } export function buildStackByJobURL(jobId: any): string { return `${ENDPOINTS.STACKS}/by-job/${encodeURIComponent(String(jobId || ""))}`; } export function buildNodeContextMembersURL( sourceNodeId: any, options: { jobId?: string; job_id?: string; latest?: boolean; limit?: number } = {}, ): string { const nodeId = String(sourceNodeId || "").trim(); let url = `${ENDPOINTS.STACKS}/by-node/${encodeURIComponent(nodeId)}/members`; const params = []; const jobId = String(options?.jobId || options?.job_id || "").trim(); if (jobId) params.push(`job_id=${encodeURIComponent(jobId)}`); if (options?.latest === false) params.push("latest=0"); const limit = Number(options?.limit || 0); if (Number.isFinite(limit) && limit > 0) { params.push(`limit=${encodeURIComponent(String(Math.trunc(limit)))}`); } if (params.length) url += `?${params.join("&")}`; return url; } export function buildStackCoverURL(stackId: any): string { return `${buildStackURL(stackId)}/cover`; } export function buildStackRenameURL(stackId: any): string { return `${buildStackURL(stackId)}/rename`; } export function buildViewerResourceURL(assetOrContext: any, relpath = ""): string { const ctx = (assetOrContext && typeof assetOrContext === "object" ? assetOrContext : {}) as Record; const rel = String(relpath || "").trim(); if (!rel) return ""; const assetId = normalizeAssetId(ctx?.asset_id ?? ctx?.id ?? null); let url = `${ENDPOINTS.VIEWER_RESOURCE}?relpath=${encodeURIComponent(rel)}`; if (assetId) { return `${url}&asset_id=${encodeURIComponent(assetId)}`; } const filepath = toPosixPath( String( ctx?.filepath || ctx?.path || ctx?.fullpath || ctx?.full_path || ctx?.file_info?.filepath || ctx?.file_info?.path || "", ).trim(), ); if (filepath) { return `${url}&filepath=${encodeURIComponent(filepath)}`; } const filename = String(ctx?.filename || ctx?.name || ctx?.file_info?.filename || "").trim(); if (!filename) return ""; url += `&filename=${encodeURIComponent(filename)}`; const subfolder = String(ctx?.subfolder || ctx?.file_info?.subfolder || "").trim(); if (subfolder) url += `&subfolder=${encodeURIComponent(subfolder)}`; const type = String(ctx?.type || ctx?.file_info?.type || "") .trim() .toLowerCase(); if (type && type !== "custom") { url += `&type=${encodeURIComponent(type)}`; } const rootId = String(pickRootId(ctx) || ctx?.root_id || ctx?.custom_root_id || "").trim(); if (rootId) { url += `&root_id=${encodeURIComponent(rootId)}`; } return url; } export function buildDateHistogramURL(params: AssetFilterParams & { scope?: string; customRootId?: string | null; month?: string } = {}): string { const { scope = "output", customRootId = null, month = "", kind = null, hasWorkflow = null, minRating = null, } = params; let url = `${ENDPOINTS.DATE_HISTOGRAM}?scope=${encodeURIComponent(scope)}&month=${encodeURIComponent(String(month || ""))}`; if (customRootId) { url += `&custom_root_id=${encodeURIComponent(customRootId)}`; } return appendAssetFilterQueryParams(url, { subfolder: params.subfolder ?? null, kind, hasWorkflow, minRating, minSizeMB: params.minSizeMB ?? null, maxSizeMB: params.maxSizeMB ?? null, minWidth: params.minWidth ?? null, minHeight: params.minHeight ?? null, maxWidth: params.maxWidth ?? null, maxHeight: params.maxHeight ?? null, workflowType: params.workflowType ?? null, workflowId: params.workflowId ?? null, workflowModel: params.workflowModel ?? null, runsOn: params.runsOn ?? null, dateRange: params.dateRange ?? null, dateExact: params.dateExact ?? null, }); } export function buildAssetViewURL(asset: Record | null | undefined): string { const mtime = asset?.mtime; const withMtime = (url: any) => { if (!url || !mtime) return url; return `${url}${url.includes("?") ? "&" : "?"}v=${encodeURIComponent(mtime)}`; }; const rawPath = toPosixPath( String( asset?.filepath || asset?.path || asset?.fullpath || asset?.full_path || asset?.file_info?.filepath || asset?.file_info?.path || "", ).trim(), ); const declaredType = String(asset?.type || asset?.file_info?.type || "") .toLowerCase() .trim(); const assetId = Number(asset?.id ?? asset?.asset_id); const isTemporaryAsset = declaredType === "temp" || rawPath.toLowerCase().includes("/temp/"); if (!isTemporaryAsset && Number.isSafeInteger(assetId) && assetId > 0) { return withMtime(`/mjr/am/viewer/asset/${encodeURIComponent(String(assetId))}`); } let filename = String( asset?.filename || asset?.name || asset?.file_info?.filename || "", ).trim(); let subfolder = String(asset?.subfolder || asset?.file_info?.subfolder || "").trim(); const pickFromPath = (p: any) => { const out = { type: "", subfolder: "", filename: "" }; const normalized = toPosixPath(p); if (!normalized) return out; const lower = normalized.toLowerCase(); const idxOut = lower.indexOf("/output/"); const idxIn = lower.indexOf("/input/"); const idxTemp = lower.indexOf("/temp/"); let baseIdx = -1; if (idxOut >= 0) { out.type = "output"; baseIdx = idxOut + "/output/".length; } else if (idxIn >= 0) { out.type = "input"; baseIdx = idxIn + "/input/".length; } else if (idxTemp >= 0) { out.type = "temp"; baseIdx = idxTemp + "/temp/".length; } if (baseIdx >= 0) { const rel = normalized.slice(baseIdx); const slash = rel.lastIndexOf("/"); if (slash >= 0) { out.subfolder = rel.slice(0, slash); out.filename = rel.slice(slash + 1); } else { out.filename = rel; } } else { const split = splitFilenameAndSubfolder(normalized); out.filename = split.filename; } return out; }; const fromPath = pickFromPath(rawPath); // If filename accidentally contains a relative path, split it for /view API. if (!subfolder && filename.includes("/")) { const idx = filename.lastIndexOf("/"); if (idx > 0) { subfolder = filename.slice(0, idx); filename = filename.slice(idx + 1); } } if (!filename && fromPath.filename) filename = fromPath.filename; if (!subfolder && fromPath.subfolder) subfolder = fromPath.subfolder; if (!filename) return ""; let type = String(asset?.type || asset?.file_info?.type || "") .toLowerCase() .trim(); if (type !== "input" && type !== "output" && type !== "temp" && type !== "custom") type = ""; if (!type && fromPath.type) type = fromPath.type; if (!type && rawPath) { if (rawPath.includes("/input/")) type = "input"; else if (rawPath.includes("/output/")) type = "output"; else if (rawPath.includes("/temp/")) type = "temp"; } if (!type) type = "output"; const hasNativeBucket = rawPath.includes("/output/") || rawPath.includes("/input/") || rawPath.includes("/temp/"); const subfolderLooksAbsolute = hasWindowsDrivePrefix(subfolder) || subfolder.startsWith("/"); // Fallback: non-native output/input roots (or broken absolute subfolder values) // cannot be served by ComfyUI `/view`, so use backend filepath streaming URL. if (rawPath && type !== "custom" && (subfolderLooksAbsolute || !hasNativeBucket)) { return withMtime(buildDownloadURL(rawPath, { inline: true })); } if (type === "custom") { const rid = String(pickRootId(asset) || "").trim(); if (rid) return withMtime(buildCustomViewURL(filename, subfolder, rid)); if (rawPath) { return withMtime( `${ENDPOINTS.CUSTOM_VIEW}?filepath=${encodeURIComponent(rawPath)}&browser_mode=1`, ); } // Fallback for malformed custom assets without root id. const fallbackType = fromPath.type || "output"; return withMtime(buildViewURL(filename, subfolder, fallbackType)); } // Prefer path-based type when explicit type conflicts with obvious filepath bucket. if (rawPath.includes("/output/")) type = "output"; if (rawPath.includes("/input/")) type = "input"; if (rawPath.includes("/temp/")) type = "temp"; return withMtime(buildViewURL(filename, subfolder, type)); } /** * Build download URL for asset */ export function buildDownloadURL(filepath: any, options: Record = {}) { if (!filepath) return ""; const inline = !!options?.inline; let url = `${ENDPOINTS.DOWNLOAD}?filepath=${encodeURIComponent(filepath)}`; if (inline) url += "&preview=1"; return url; } export function buildWorkflowContentURL(filepath: any): string { const fp = String(filepath || "").trim(); if (!fp) return ""; return `${ENDPOINTS.WORKFLOWS_CONTENT}?filepath=${encodeURIComponent(fp)}`; } export function buildWorkflowValidateURL(filepath: any): string { const fp = String(filepath || "").trim(); if (!fp) return ""; return `${ENDPOINTS.WORKFLOWS_VALIDATE}?filepath=${encodeURIComponent(fp)}`; } export function buildWorkflowVersionsURL(filepath: any): string { const fp = String(filepath || "").trim(); if (!fp) return ""; return `${ENDPOINTS.WORKFLOWS_VERSIONS}?filepath=${encodeURIComponent(fp)}`; } export function buildWorkflowDiffURL(filepath: any, version_filepath: any = ""): string { const fp = String(filepath || "").trim(); if (!fp) return ""; let url = `${ENDPOINTS.WORKFLOWS_DIFF}?filepath=${encodeURIComponent(fp)}`; const version = String(version_filepath || "").trim(); if (version) url += `&version_filepath=${encodeURIComponent(version)}`; return url; } /** * Build download URL for asset with ComfyUI metadata stripped. */ export function buildCleanDownloadURL(filepath: any) { if (!filepath) return ""; return `${ENDPOINTS.DOWNLOAD_CLEAN}?filepath=${encodeURIComponent(filepath)}`; }