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 exampleList 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
exposename(notdisplayName),parentId,assetType(1== folder), and
hasChildren. Use it for folders only.- To list files, use
searchscoped byparentId Equals <folder-id>- it returns
both files and folders under the parent (names live underitem["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_pathraises 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 createdJavaScript
// 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_foldersreads the navigation tree (folders appear instantly,
no indexing lag);list_files_by_extensionusessearch, so freshly uploaded files
surface only once indexed - poll if you just uploaded. resolve_folder_pathislist_childrenper 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/rootIdto skip the
ambiguous top level entirely. - Pair this with
recipes/search-patterns.mdonce you hold the folder id (e.g. sort or
page large folders viasearchwith aparentId Equalsfilter). - Folder URLs always end with
/. Include the trailing slash when comparing or constructing folder URL strings -Content/Photo/Events/2024/, notContent/Photo/Events/2024. create_folder/createFolderis 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/deleteAsseton 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 Equalssearch may miss them for several seconds. To confirm a folder exists immediately after creation, use a directurl-filter search or fetch by id - those bypass the index lag. moveAsset/move_assetis 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, pollurl Equals <new-path>(oruuidSearchField Equals <id>and inspect the URL) until the asset appears at the destination.
Updated 27 days ago
