Recipe: folder navigation (resolve a path, list folders/files, create+verify)

Recipe: folder navigation (resolve a path, list folders/files, create+verify)

Recipe: folder navigation (resolve a path, list folders/files, create+verify)

💡

Prompt example

List the folders in Content/Adam, list the.txt files in it, and create three named folders then confirm they exist.

Vibe prompts name folders by path ("the Content/Adam folder") but every component
takes a <folder-uuid>. These helpers bridge that gap with the list_children,
create_folder, and search components. All reads are Class A (safe anywhere); the create helper
is only for a confined parent (your run-root anchor).

Key facts

  • list_children (get_asset_child_nodes) is a folder-navigation tree: it returns
    only child folders - uploaded files never appear, even after they settle. Items
    expose name (not displayName), parentId, assetType (1 == folder), and
    hasChildren. Use it for folders only.
  • To list files, use search scoped by parentId Equals <folder-id> - it returns
    both files and folders under the parent (names live under item["identifiers"]). There
    is no server-side filename/extension filter, so match the name client-side.
  • Top-level bucket names repeat on a deployment (several Content/content). A path
    resolver must follow every matching branch and require a unique leaf, else it can
    silently pick the wrong tree. resolve_folder_path raises on ambiguity (fail-closed).

Python

# components: list_children, create_folder, search
ROOT_ID = "00000000-0000-0000-0000-000000000000"  # asset-tree root sentinel

def resolve_folder_path(sdk, path, root_id=ROOT_ID):
    """Resolve a '/'-separated folder path to one id; None if absent.

    Follows every branch whose folder name matches each segment and requires a
    unique leaf, because top-level bucket names can repeat. Raises on ambiguity.
    """
    frontier = [root_id]
    for seg in (s for s in path.strip("/").split("/") if s):
        nxt = []
        for cur in frontier:
            for c in list_children(sdk, cur):
                if c.get("assetType") == 1 and (c.get("name") or c.get("displayName")) == seg:
                    nxt.append(c["id"])
        frontier = nxt
    unique = list(dict.fromkeys(frontier))
    if len(unique) > 1:
        raise ValueError(f"Ambiguous folder path {path!r}: {len(unique)} matches")
    return unique[0] if unique else None

def list_folders(sdk, folder_id):
    """Immediate sub-folders of folder_id (assetType == 1)."""
    return [c for c in list_children(sdk, folder_id) if c.get("assetType") == 1]

def list_files_by_extension(sdk, folder_id, ext):
    """Immediate files whose name ends with ext (e.g. '.txt'); case-insensitive.

    Files are NOT in the navigation tree, so list them via search scoped to the
    folder (parentId Equals), then filter on the name client-side (no server-side
    extension filter). Freshly uploaded files appear only once indexed.
    """
    ext = ext.lower()
    flt = [{"fieldName": "parentId", "operator": "Equals", "values": folder_id}]
    items, offset, page = [], 0, 100
    while True:
        batch = (search(sdk, filters=flt, size=page, offset=offset) or {}).get("items", [])
        items.extend(batch)
        if len(batch) < page:
            break
        offset += 1  # offset is in PAGES, not records (see search-patterns.md)
    return [
        it for it in items
        if (it.get("identifiers") or {}).get("assetType") != 1
        and ((it.get("identifiers") or {}).get("displayName") or "").lower().endswith(ext)
    ]

def create_folders_and_verify(sdk, parent_id, names):
    """Create each name under parent_id, then confirm all exist. Returns {name: id}.

    Confinement: parent_id MUST be your run-root anchor (or a folder beneath it) -
    never an arbitrary shared folder. Raises if any expected folder is missing.
    """
    created = {name: create_folder(sdk, parent_id, name) for name in names}
    present = {(c.get("name") or c.get("displayName")) for c in list_folders(sdk, parent_id)}
    missing = [n for n in names if n not in present]
    if missing:
        raise AssertionError(f"folders not found after create: {missing}")
    return created

JavaScript

// components: listChildren, createFolder, search
const ROOT_ID = "00000000-0000-0000-0000-000000000000"; // asset-tree root sentinel

export async function resolveFolderPath(sdk, path, rootId = ROOT_ID) {
    let frontier = [rootId];
    for (const seg of path.split("/").filter(Boolean)) {
        const nxt = [];
        for (const cur of frontier) {
            for (const c of await listChildren(sdk, cur)) {
                if (c.assetType === 1 && (c.name ?? c.displayName) === seg) nxt.push(c.id);
            }
        }
        frontier = nxt;
    }
    const unique = [...new Set(frontier)];
    if (unique.length > 1) throw new Error(`Ambiguous folder path '${path}': ${unique.length} matches`);
    return unique.length ? unique[0] : null;
}

export async function listFolders(sdk, folderId) {
    return (await listChildren(sdk, folderId)).filter((c) => c.assetType === 1);
}

export async function listFilesByExtension(sdk, folderId, ext) {
    // Files are NOT in the navigation tree - list them via search (parentId Equals),
    // then filter on the name client-side (no server-side extension filter).
    const e = ext.toLowerCase();
    const flt = [{ fieldName: "parentId", operator: "Equals", values: folderId }];
    const items = [];
    for (let offset = 0; ; offset += 1) {
        const res = await search(sdk, null, flt, null, 100, offset);
        const batch = res ? res.items : []; // JS returns false when empty
        items.push(...batch);
        if (batch.length < 100) break;
    }
    return items.filter(
        (it) => it.identifiers?.assetType !== 1 &&
            (it.identifiers?.displayName ?? "").toLowerCase().endsWith(e),
    );
}

export async function createFoldersAndVerify(sdk, parentId, names) {
    // Confinement: parentId MUST be your run-root anchor - never a shared folder.
    const created = {};
    for (const name of names) created[name] = await createFolder(sdk, parentId, name);
    const present = new Set((await listFolders(sdk, parentId)).map((c) => c.name ?? c.displayName));
    const missing = names.filter((n) => !present.has(n));
    if (missing.length) throw new Error(`folders not found after create: ${missing}`);
    return created;
}

Initializing the SDK (Python, admin API)

When writing a standalone script that calls the SDK directly (outside the recipe framework):

import sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')  # required on Windows; customer folder names contain Unicode

import boto3, json
from nomad_media_pip.src.nomad_sdk import Nomad_SDK

# Credentials from Secrets Manager (nomad/<prefix>/adminUserCredentials)
sm = boto3.client("secretsmanager", region_name="<region>")
creds = json.loads(sm.get_secret_value(SecretId="nomad/<prefix>/adminUserCredentials")["SecretString"])

sdk = Nomad_SDK({
    "serviceApiUrl": "<admin-cloudfront-url>",  # "Nomad Admin API Gateway" distribution
    "apiType": "admin",   # required; admin-only methods raise InvalidAPITypeException otherwise
    "username": creds["username"],
    "password": creds["password"],
    "disableLogging": True,
})

get_asset_child_nodes direct call - both asset_id and folder_id take the same UUID:

# page 0, page_size 200
raw = sdk.get_asset_child_nodes(folder_id, folder_id, "name", False, 0, 200)
# response is either a list or {"items": [...]} - normalize:
children = raw if isinstance(raw, list) else (raw or {}).get("items", [])

For most enumeration tasks, prefer sdk.search with parentId Equals - it returns both
files and folders and works through the standard search index. Use get_asset_child_nodes
only when you need pure folder-tree navigation (no OpenSearch indexing lag, folders only).

Notes

  • Folders vs files: list_folders reads the navigation tree (folders appear instantly,
    no indexing lag); list_files_by_extension uses search, so freshly uploaded files
    surface only once indexed - poll if you just uploaded.
  • resolve_folder_path is list_children per path segment - fine for shallow paths; for
    deep trees prefer caching the resolved id and reusing it.
  • To resolve under a known bucket, pass its id as root_id/rootId to skip the
    ambiguous top level entirely.
  • Pair this with recipes/search-patterns.md once you hold the folder id (e.g. sort or
    page large folders via search with a parentId Equals filter).
  • Folder URLs always end with /. Include the trailing slash when comparing or constructing folder URL strings - Content/Photo/Events/2024/, not Content/Photo/Events/2024.
  • create_folder / createFolder is synchronous. The returned id is immediately usable - no polling required. You can create a parent and then create children under it in the same run without waiting.
  • Create large trees top-down, depth by depth. Sort all folders by path depth (shallowest first) and create each depth level before proceeding to the next. Adding a short pause (~1 second) between levels gives the system time to register each parent. Creating a child before its parent results in an error.
  • Deleting a folder cascade-deletes all descendants. delete_asset / deleteAsset on a folder permanently removes every subfolder and file beneath it - there is no confirmation and no recovery. Before deleting, verify the folder's URL is not a prefix of any folder you intend to keep.
  • Verifying creation via search has a lag. After creating folders, a parentId Equals search may miss them for several seconds. To confirm a folder exists immediately after creation, use a direct url-filter search or fetch by id - those bypass the index lag.
  • moveAsset / move_asset is fire-and-forget. The API call returns immediately - the move is queued and executed asynchronously in the background. A successful return does not mean the asset has moved yet. Do not search for the asset at its new location right after calling move; it will still appear at the old URL until the operation completes. To verify a move finished, poll url Equals <new-path> (or uuidSearchField Equals <id> and inspect the URL) until the asset appears at the destination.

Did this page help you?