diff --git a/asab/library/item.py b/asab/library/item.py index 15bc3ea9..18d00a2b 100644 --- a/asab/library/item.py +++ b/asab/library/item.py @@ -3,21 +3,22 @@ @dataclasses.dataclass class LibraryItem: - """ - The data class that contains the info about a specific item in the library. + """ + The data class that contains the info about a specific item in the library. - Attributes: - name (str): The absolute path of the Item. It can be directly fed into `LibraryService.read(...)`. - type (str): Can be either `dir` if the Item is a directory or `item` if Item is of any other type. - layer (int): The number of highest layer in which this Item is found. The higher the number, the lower the layer is. - providers (list): List of `LibraryProvider` objects containing this Item. - disabled (bool): `True` if the Item is disabled, `False` otherwise. If the Item is disabled, `LibraryService.read(...)` will return `None`. - override (int): If `True`, this item is marked as an override for the providers with the same Item name. - """ - - name: str - type: str - layer: int - providers: list - disabled: bool = False - override: int = 0 # Default value for override is False + Attributes: + name (str): The absolute path of the Item. It can be directly fed into `LibraryService.read(...)`. + type (str): Can be either `dir` if the Item is a directory or `item` if Item is of any other type. + layer (int): The number of highest layer in which this Item is found. The higher the number, the lower the layer is. + providers (list): List of `LibraryProvider` objects containing this Item. + disabled (bool): `True` if the Item is disabled, `False` otherwise. If the Item is disabled, `LibraryService.read(...)` will return `None`. + override (int): If `True`, this item is marked as an override for the providers with the same Item name. + target (str): Specifies the target context, e.g., "tenant" or "global". Defaults to "global". + """ + name: str + type: str + layer: int + providers: list + disabled: bool = False + override: int = 0 + target: str = "global" # Default to "global" if not tenant-specific diff --git a/asab/library/providers/zookeeper.py b/asab/library/providers/zookeeper.py index 05fec80f..406f1160 100644 --- a/asab/library/providers/zookeeper.py +++ b/asab/library/providers/zookeeper.py @@ -286,7 +286,7 @@ async def list(self, path: str) -> list: tenant_node_path = self.build_path(path, tenant_specific=True) if tenant_node_path != global_node_path: tenant_nodes = await self.Zookeeper.get_children(tenant_node_path) or [] - tenant_items = await self.process_nodes(tenant_nodes, path) + tenant_items = await self.process_nodes(tenant_nodes, path, target="tenant") else: tenant_items = [] # Combine items, with tenant items taking precedence over global ones @@ -295,20 +295,27 @@ async def list(self, path: str) -> list: return list(combined_items.values()) - async def process_nodes(self, nodes, base_path): + async def process_nodes(self, nodes, base_path, target="global"): + """ + Processes a list of nodes and creates corresponding LibraryItem objects. + + Args: + nodes (list): List of node names to process. + base_path (str): The base path for the nodes. + target (str): Specifies the target context, e.g., "tenant" or "global". + + Returns: + list: A list of LibraryItem objects. + """ items = [] for node in nodes: # Remove any component that starts with '.' startswithdot = functools.reduce(lambda x, y: x or y.startswith('.'), node.split(os.path.sep), False) if startswithdot: continue - # Extract the last 5 characters of the node name - last_five_chars = node[-5:] - # Check if there is a period in the last five characters, - # We detect files in Zookeeper by the presence of a dot in the filename, - # but exclude filenames ending with '.io' or '.d' (e.g., 'logman.io', server_https.d) - # from being considered as files. + # Determine if this is a file or directory + last_five_chars = node[-5:] if '.' in last_five_chars and not node.endswith(('.io', '.d')): fname = base_path + node ftype = "item" @@ -316,11 +323,13 @@ async def process_nodes(self, nodes, base_path): fname = base_path + node + '/' ftype = "dir" + # Add the item with the specified target items.append(LibraryItem( name=fname, type=ftype, layer=self.Layer, providers=[self], + target=target )) return items diff --git a/asab/library/service.py b/asab/library/service.py index e0acb798..26242a0e 100644 --- a/asab/library/service.py +++ b/asab/library/service.py @@ -489,6 +489,59 @@ def check_disabled(self, path: str) -> bool: return False + async def get_item_metadata(self, path: str) -> typing.Optional[dict]: + """ + Retrieve metadata for a specific file in the library, including its `target`. + + Args: + path (str): The absolute path of the file to retrieve metadata for. + Must start with '/' and include a filename with an extension. + + Returns: + dict: Metadata for the specified file, including `target`, or None if not found. + """ + # Validate the path format + _validate_path_item(path) + + # Split into directory and filename + directory, filename = os.path.split(path) + + if not directory or not filename: + L.warning("Invalid path '{}': missing directory or filename.".format(path)) + return None + # Ensure directory ends with '/' + if not directory.endswith('/'): + directory += '/' + + try: + # Fetch all items in the directory + items = await self.list(directory) + except Exception as e: + L.warning("Failed to list items in directory '{}': {}".format(directory, e)) + return None + + # Use dictionary for faster lookup + items_dict = {item.name: item for item in items} + + # Retrieve the item by path + item = items_dict.get(path) + if item and item.type == "item": + # Match found; return metadata including `target` + return { + "name": item.name, + "type": item.type, + "layer": item.layer, + "providers": item.providers, + "disabled": item.disabled, + "override": item.override, + "target": item.target, # Include the target in the metadata + } + + # Item not found + L.info("Item '{}' not found in directory '{}'.".format(filename, directory)) + return None + + async def export(self, path: str = "/", remove_path: bool = False) -> typing.IO: """ Return a file-like stream containing a gzipped tar archive of the library contents of the path.