1# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2# SPDX-License-Identifier: Apache-2.0
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import builtins
17import logging
18import os
19import re
20import threading
21from collections.abc import Callable, Iterator, Sequence
22from typing import IO, Any
23from urllib.parse import urlparse
24
25from .client import StorageClient
26from .config import RESERVED_POSIX_PROFILE_NAME, SUPPORTED_IMPLICIT_PROFILE_PROTOCOLS, PathMapping, StorageClientConfig
27from .file import ObjectFile, PosixFile
28from .telemetry import Telemetry
29from .types import MSC_PROTOCOL, ExecutionMode, ObjectMetadata, PatternList, SignerType, SymlinkHandling, SyncResult
30from .utils import build_full_path
31
32_TELEMETRY_PROVIDER: Callable[[], Telemetry] | None = None
33_TELEMETRY_PROVIDER_LOCK = threading.Lock()
34_STORAGE_CLIENT_CACHE: dict[str, StorageClient] = {}
35_STORAGE_CLIENT_CACHE_LOCK = threading.Lock()
36_PATH_MAPPING_CACHE: dict[str | None, PathMapping] = {}
37_PATH_MAPPING_CACHE_LOCK = threading.Lock()
38_PROCESS_ID = os.getpid()
39
40logger = logging.getLogger(__name__)
41
42
43def _reinitialize_after_fork() -> None:
44 """
45 Reinitialize module state after fork to ensure fork-safety.
46
47 This function is called automatically after a fork to:
48 1. Clear the storage client cache (cached clients may have invalid state)
49 2. Reinitialize locks (parent's lock state must not be inherited)
50 3. Update process ID tracking
51
52 Note: The telemetry provider is intentionally inherited by child processes,
53 only its lock is reinitialized.
54 """
55 global _STORAGE_CLIENT_CACHE, _STORAGE_CLIENT_CACHE_LOCK # noqa: PLW0602
56 global _PATH_MAPPING_CACHE, _PATH_MAPPING_CACHE_LOCK # noqa: PLW0602
57 global _TELEMETRY_PROVIDER_LOCK
58 global _PROCESS_ID
59
60 _STORAGE_CLIENT_CACHE.clear()
61 _STORAGE_CLIENT_CACHE_LOCK = threading.Lock()
62 _PATH_MAPPING_CACHE.clear()
63 _PATH_MAPPING_CACHE_LOCK = threading.Lock()
64 # we don't need to reset telemetry provider as it is supposed to be a top-level Python function
65 _TELEMETRY_PROVIDER_LOCK = threading.Lock()
66 _PROCESS_ID = os.getpid()
67
68
69def _check_and_reinitialize_if_forked() -> None:
70 """
71 Check if the current process is a fork and reinitialize if needed.
72
73 This provides fork-safety for systems where os.register_at_fork is not available
74 or as a fallback mechanism.
75 """
76 global _PROCESS_ID # noqa: PLW0602
77
78 current_pid = os.getpid()
79 if current_pid != _PROCESS_ID:
80 _reinitialize_after_fork()
81
82
83if hasattr(os, "register_at_fork"):
84 os.register_at_fork(after_in_child=_reinitialize_after_fork)
85
86
[docs]
87def get_telemetry_provider() -> Callable[[], Telemetry] | None:
88 """
89 Get the function used to create :py:class:`Telemetry` instances for storage clients created by shortcuts.
90
91 :return: A function that provides a telemetry instance.
92 """
93 global _TELEMETRY_PROVIDER # noqa: PLW0602
94
95 return _TELEMETRY_PROVIDER
96
97
[docs]
98def set_telemetry_provider(telemetry_provider: Callable[[], Telemetry] | None) -> None:
99 """
100 Set the function used to create :py:class:`Telemetry` instances for storage clients created by shortcuts.
101
102 :param telemetry_provider: A function that provides a telemetry instance. The function must be defined at the top level of a module to work with pickling.
103 """
104 global _TELEMETRY_PROVIDER
105 global _TELEMETRY_PROVIDER_LOCK # noqa: PLW0602
106
107 with _TELEMETRY_PROVIDER_LOCK:
108 _TELEMETRY_PROVIDER = telemetry_provider
109
110
111def _resolve_msc_url(url: str) -> tuple[str, str]:
112 """
113 Resolve an MSC URL to a profile name and path.
114
115 :param url: The MSC URL to resolve (msc://profile/path)
116 :return: A tuple of (profile_name, path)
117 """
118 pr = urlparse(url)
119 profile = pr.netloc
120 # Normalize only the object path so the msc:// scheme separator and profile stay intact.
121 pr = pr._replace(path=re.sub(r"/+", "/", pr.path))
122 path = build_full_path(url, pr)
123 path = path.removeprefix("/")
124 return profile, path
125
126
127def _read_cached_path_mapping() -> PathMapping:
128 """
129 Read path mapping once per ``MSC_CONFIG`` value for shortcut URL resolution.
130
131 Path mapping checks happen on every non-MSC shortcut call, including POSIX paths. Caching here keeps that hot path
132 from repeatedly loading and validating the full MSC config while preserving ``StorageClientConfig.read_path_mapping``
133 behavior for direct callers.
134 """
135 cache_key = os.getenv("MSC_CONFIG", None)
136 if cache_key in _PATH_MAPPING_CACHE:
137 return _PATH_MAPPING_CACHE[cache_key]
138
139 with _PATH_MAPPING_CACHE_LOCK:
140 if cache_key in _PATH_MAPPING_CACHE:
141 return _PATH_MAPPING_CACHE[cache_key]
142
143 path_mapping = StorageClientConfig.read_path_mapping()
144 _PATH_MAPPING_CACHE[cache_key] = path_mapping
145 return path_mapping
146
147
148def _resolve_non_msc_url(url: str) -> tuple[str, str]:
149 """
150 Resolve a non-MSC URL to a profile name and path.
151
152 Resolution process:
153 1. First check if MSC config exists
154 2. If config exists, check for possible path mapping
155 3. If no mapping is found, fall back to the reserved POSIX profile (``__filesystem__``) for file paths or create an implicit profile based on URL
156
157 :param url: The non-MSC URL to resolve
158 :return: A tuple of (profile_name, path)
159 """
160 # Check if we have a valid path mapping, if so check if there is a matching mapping
161 path_mapping = _read_cached_path_mapping()
162 if path_mapping:
163 # Look for a matching mapping
164 possible_mapping = path_mapping.find_mapping(url)
165 if possible_mapping:
166 return possible_mapping # return the profile name and path
167
168 # For file paths, use the default POSIX profile
169 if url.startswith("file://"):
170 pr = urlparse(url)
171 return RESERVED_POSIX_PROFILE_NAME, build_full_path(url, pr)
172 elif url.startswith("/"):
173 url = os.path.normpath(url)
174 return RESERVED_POSIX_PROFILE_NAME, url
175
176 # For other URL protocol, create an implicit profile name
177 pr = urlparse(url)
178 protocol = pr.scheme.lower()
179
180 # Translate relative paths to absolute paths
181 if not protocol:
182 return RESERVED_POSIX_PROFILE_NAME, os.path.realpath(url)
183
184 # Validate the protocol is supported
185 if protocol not in SUPPORTED_IMPLICIT_PROFILE_PROTOCOLS:
186 supported_protocols = ", ".join([f"{p}://" for p in SUPPORTED_IMPLICIT_PROFILE_PROTOCOLS])
187 raise ValueError(
188 f'Unknown URL "{url}", expecting "{MSC_PROTOCOL}" or a supported protocol ({supported_protocols}) or a POSIX path'
189 )
190
191 # Build the implicit profile name using the format _protocol-bucket
192 bucket = pr.netloc
193 if not bucket:
194 raise ValueError(f'Invalid URL "{url}", bucket name is required for {protocol}:// URLs')
195
196 profile_name = f"_{protocol}-{bucket}"
197
198 # Return normalized path with leading slash removed
199 path = build_full_path(url, pr)
200 path = path.removeprefix("/")
201
202 return profile_name, path
203
204
[docs]
205def resolve_storage_client(url: str) -> tuple[StorageClient, str]:
206 """
207 Build and return a :py:class:`multistorageclient.StorageClient` instance based on the provided URL or path.
208
209 This function parses the given URL or path and determines the appropriate storage profile and path.
210 It supports URLs with the protocol ``msc://``, as well as POSIX paths or ``file://`` URLs for local file
211 system access. If the profile has already been instantiated, it returns the cached client. Otherwise,
212 it creates a new :py:class:`StorageClient` and caches it.
213
214 The function also supports implicit profiles for non-MSC URLs. When a non-MSC URL is provided (like s3://,
215 gs://, ais://, file://), MSC will infer the storage provider based on the URL protocol and create an implicit
216 profile with the naming convention "_protocol-bucket" (e.g., "_s3-bucket1", "_gs-bucket1").
217
218 Path mapping defined in the MSC configuration are also applied before creating implicit profiles.
219 This allows for explicit mappings between source paths and destination MSC profiles.
220
221 This function is fork-safe: after a fork, the cache is automatically cleared and new client instances
222 are created in the child process to avoid sharing stale connections or file descriptors.
223
224 :param url: The storage location, which can be:
225 - A URL in the format ``msc://profile/path`` for object storage.
226 - A local file system path (absolute POSIX path) or a ``file://`` URL.
227 - A non-MSC URL with a supported protocol (s3://, gs://, ais://).
228
229 :return: A tuple containing the :py:class:`multistorageclient.StorageClient` instance and the parsed path.
230
231 :raises ValueError: If the URL's protocol is neither ``msc`` nor a valid local file system path
232 or a supported non-MSC protocol.
233 """
234 global _STORAGE_CLIENT_CACHE # noqa: PLW0602
235 global _STORAGE_CLIENT_CACHE_LOCK # noqa: PLW0602
236
237 _check_and_reinitialize_if_forked()
238
239 # Normalize the path for msc:/ prefix due to pathlib.Path('msc://')
240 if url.startswith("msc:/") and not url.startswith("msc://"):
241 url = url.replace("msc:/", "msc://")
242
243 # Resolve the URL to a profile name and path
244 profile, path = _resolve_msc_url(url) if url.startswith(MSC_PROTOCOL) else _resolve_non_msc_url(url)
245
246 # Check if the profile has already been instantiated
247 if profile in _STORAGE_CLIENT_CACHE:
248 return _STORAGE_CLIENT_CACHE[profile], path
249
250 # Create a new StorageClient instance and cache it
251 with _STORAGE_CLIENT_CACHE_LOCK:
252 if profile in _STORAGE_CLIENT_CACHE:
253 return _STORAGE_CLIENT_CACHE[profile], path
254 else:
255 client = StorageClient(
256 config=StorageClientConfig.from_file(profile=profile, telemetry_provider=get_telemetry_provider())
257 )
258 _STORAGE_CLIENT_CACHE[profile] = client
259
260 return client, path
261
262
[docs]
263def open(url: str, mode: str = "rb", **kwargs: Any) -> PosixFile | ObjectFile:
264 """
265 Open a file at the given URL using the specified mode.
266
267 The function utilizes the :py:class:`multistorageclient.StorageClient` to open a file at the provided path.
268 The URL is parsed, and the corresponding :py:class:`multistorageclient.StorageClient` is retrieved or built.
269
270 :param url: The URL of the file to open. (example: ``msc://profile/prefix/dataset.tar``)
271 :param mode: The file mode to open the file in.
272
273 :return: A file-like object that allows interaction with the file.
274
275 :raises ValueError: If the URL's protocol does not match the expected protocol ``msc``.
276 """
277 client, path = resolve_storage_client(url)
278 return client.open(path, mode, **kwargs)
279
280
[docs]
281def glob(pattern: str, attribute_filter_expression: str | None = None) -> builtins.list[str]:
282 """
283 Return a list of files matching a pattern.
284
285 This function supports glob-style patterns for matching multiple files within a storage system. The pattern is
286 parsed, and the associated :py:class:`multistorageclient.StorageClient` is used to retrieve the
287 list of matching files.
288
289 :param pattern: The glob-style pattern to match files. (example: ``msc://profile/prefix/**/*.tar``)
290 :param attribute_filter_expression: The attribute filter expression to apply to the result.
291
292 :return: A list of file paths matching the pattern.
293
294 :raises ValueError: If the URL's protocol does not match the expected protocol ``msc``.
295 """
296 client, path = resolve_storage_client(pattern)
297 if not pattern.startswith(MSC_PROTOCOL) and client.profile == RESERVED_POSIX_PROFILE_NAME:
298 return client.glob(path, include_url_prefix=False, attribute_filter_expression=attribute_filter_expression)
299 else:
300 return client.glob(path, include_url_prefix=True, attribute_filter_expression=attribute_filter_expression)
301
302
[docs]
303def upload_file(url: str, local_path: str | IO, attributes: dict[str, Any] | None = None) -> None:
304 """
305 Upload a file to the given URL from a local path.
306
307 The function utilizes the :py:class:`multistorageclient.StorageClient` to upload a file (object) to the
308 provided path. The URL is parsed, and the corresponding :py:class:`multistorageclient.StorageClient`
309 is retrieved or built.
310
311 :param url: The URL of the file. (example: ``msc://profile/prefix/dataset.tar``)
312 :param local_path: The local path or file-like object to read from.
313
314 :raises ValueError: If the URL's protocol does not match the expected protocol ``msc``.
315 """
316 client, path = resolve_storage_client(url)
317 return client.upload_file(remote_path=path, local_path=local_path, attributes=attributes)
318
319
[docs]
320def download_file(url: str, local_path: str | IO) -> None:
321 """
322 Download a file in a given remote_path to a local path
323
324 The function utilizes the :py:class:`multistorageclient.StorageClient` to download a file (object) at the
325 provided path. The URL is parsed, and the corresponding :py:class:`multistorageclient.StorageClient`
326 is retrieved or built.
327
328 :param url: The URL of the file to download. (example: ``msc://profile/prefix/dataset.tar``)
329 :param local_path: The local path or file-like object to write to.
330
331 :raises ValueError: If the URL's protocol does not match the expected protocol ``msc``.
332 """
333 client, path = resolve_storage_client(url)
334 return client.download_file(remote_path=path, local_path=local_path)
335
336
[docs]
337def is_empty(url: str) -> bool:
338 """
339 Checks whether the specified URL contains any objects.
340
341 :param url: The URL to check, typically pointing to a storage location.
342 :return: ``True`` if there are no objects/files under this URL, ``False`` otherwise.
343
344 :raises ValueError: If the URL's protocol does not match the expected protocol ``msc``.
345 """
346 client, path = resolve_storage_client(url)
347 return client.is_empty(path)
348
349
[docs]
350def is_file(url: str) -> bool:
351 """
352 Checks whether the specified url points to a file (rather than a directory or folder).
353
354 The function utilizes the :py:class:`multistorageclient.StorageClient` to check if a file (object) exists
355 at the provided path. The URL is parsed, and the corresponding :py:class:`multistorageclient.StorageClient`
356 is retrieved or built.
357
358 :param url: The URL to check the existence of a file. (example: ``msc://profile/prefix/dataset.tar``)
359 """
360 client, path = resolve_storage_client(url)
361 return client.is_file(path=path)
362
363
[docs]
364def sync(
365 source_url: str,
366 target_url: str,
367 delete_unmatched_files: bool = False,
368 execution_mode: ExecutionMode = ExecutionMode.LOCAL,
369 patterns: PatternList | None = None,
370 preserve_source_attributes: bool = False,
371 ignore_hidden: bool = True,
372 dryrun: bool = False,
373 dryrun_output_path: str | None = None,
374 symlink_handling: SymlinkHandling = SymlinkHandling.FOLLOW,
375) -> SyncResult:
376 """
377 Syncs files from the source storage to the target storage.
378
379 :param source_url: The URL for the source storage.
380 :param target_url: The URL for the target storage.
381 :param delete_unmatched_files: Whether to delete files at the target that are not present at the source.
382 :param execution_mode: The execution mode to use. Currently supports "local" and "ray".
383 :param patterns: PatternList for include/exclude filtering. If None, all files are included.
384 :param preserve_source_attributes: Whether to preserve source file metadata attributes during synchronization.
385 When False (default), only file content is copied. When True, custom metadata attributes are also preserved.
386
387 .. warning::
388 **Performance Impact**: When enabled without a ``metadata_provider`` configured, this will make a HEAD
389 request for each object to retrieve attributes, which can significantly impact performance on large-scale
390 sync operations. For production use at scale, configure a ``metadata_provider`` in your storage profile.
391 :param ignore_hidden: Whether to ignore hidden files and directories (starting with dot). Default is True.
392 :param dryrun: If True, only enumerate and compare objects without performing any copy/delete operations.
393 The returned :py:class:`SyncResult` will include a :py:class:`DryrunResult` with paths to JSONL files.
394 :param dryrun_output_path: Directory to write dryrun JSONL files into. If None (default), a temporary
395 directory is created automatically. Ignored when dryrun is False.
396 :param symlink_handling: How to handle symbolic links during sync.
397 :py:attr:`SymlinkHandling.FOLLOW` (default) dereferences symlinks and copies the target's bytes.
398 :py:attr:`SymlinkHandling.SKIP` excludes symlinks from the sync.
399 :py:attr:`SymlinkHandling.PRESERVE` recreates symlinks on the target via
400 :py:meth:`AbstractStorageClient.make_symlink` instead of copying bytes (required for
401 round-trip preservation of symlinks).
402 """
403 source_client, source_path = resolve_storage_client(source_url)
404 target_client, target_path = resolve_storage_client(target_url)
405 return target_client.sync_from(
406 source_client,
407 source_path,
408 target_path,
409 delete_unmatched_files,
410 execution_mode=execution_mode,
411 patterns=patterns,
412 preserve_source_attributes=preserve_source_attributes,
413 ignore_hidden=ignore_hidden,
414 dryrun=dryrun,
415 dryrun_output_path=dryrun_output_path,
416 symlink_handling=symlink_handling,
417 )
418
419
[docs]
420def sync_replicas(
421 source_url: str,
422 replica_indices: Sequence[int] | None = None,
423 delete_unmatched_files: bool = False,
424 execution_mode: ExecutionMode = ExecutionMode.LOCAL,
425 patterns: PatternList | None = None,
426 ignore_hidden: bool = True,
427 symlink_handling: SymlinkHandling = SymlinkHandling.FOLLOW,
428) -> None:
429 """
430 Syncs files from the source storage to all the replicas.
431
432 :param source_url: The URL for the source storage.
433 :param replica_indices: Specify the indices of the replicas to sync to. If not provided, all replicas will be synced. Index starts from 0.
434 :param delete_unmatched_files: Whether to delete files at the replicas that are not present at the source.
435 :param execution_mode: The execution mode to use. Currently supports "local" and "ray".
436 :param patterns: PatternList for include/exclude filtering. If None, all files are included.
437 :param ignore_hidden: Whether to ignore hidden files and directories (starting with dot). Default is True.
438 :param symlink_handling: How to handle symbolic links during sync.
439 :py:attr:`SymlinkHandling.FOLLOW` (default) dereferences symlinks and copies the target's bytes.
440 :py:attr:`SymlinkHandling.SKIP` excludes symlinks from the sync.
441 :py:attr:`SymlinkHandling.PRESERVE` recreates symlinks on each replica via
442 :py:meth:`AbstractStorageClient.make_symlink` instead of copying bytes.
443 """
444 source_client, source_path = resolve_storage_client(source_url)
445 source_client.sync_replicas(
446 source_path,
447 replica_indices=replica_indices,
448 delete_unmatched_files=delete_unmatched_files,
449 execution_mode=execution_mode,
450 patterns=patterns,
451 ignore_hidden=ignore_hidden,
452 symlink_handling=symlink_handling,
453 )
454
455
[docs]
456def list(
457 url: str,
458 start_after: str | None = None,
459 end_at: str | None = None,
460 include_directories: bool = False,
461 attribute_filter_expression: str | None = None,
462 show_attributes: bool = False,
463 patterns: PatternList | None = None,
464 symlink_handling: SymlinkHandling = SymlinkHandling.FOLLOW,
465) -> Iterator[ObjectMetadata]:
466 """
467 Lists the contents of the specified URL prefix.
468
469 This function retrieves the corresponding :py:class:`multistorageclient.StorageClient`
470 for the given URL and returns an iterator of objects (files or directories) stored under the provided prefix.
471
472 :param url: The prefix to list objects under.
473 :param start_after: The key to start after (i.e. exclusive). An object with this key doesn't have to exist.
474 :param end_at: The key to end at (i.e. inclusive). An object with this key doesn't have to exist.
475 :param include_directories: Whether to include directories in the result. When True, directories are returned alongside objects.
476 :param attribute_filter_expression: The attribute filter expression to apply to the result.
477 :param show_attributes: Whether to return attributes in the result.
478 :param patterns: PatternList for include/exclude filtering. If None, all files are included.
479 :param symlink_handling: How to handle symbolic links. Only applicable for POSIX file storage.
480 :return: An iterator of :py:class:`ObjectMetadata` objects representing the files (and optionally directories)
481 accessible under the specified URL prefix. The returned keys will always be prefixed with msc://.
482 """
483 client, path = resolve_storage_client(url)
484 return client.list(
485 path=path,
486 start_after=start_after,
487 end_at=end_at,
488 include_directories=include_directories,
489 include_url_prefix=True,
490 attribute_filter_expression=attribute_filter_expression,
491 show_attributes=show_attributes,
492 patterns=patterns,
493 symlink_handling=symlink_handling,
494 )
495
496
[docs]
497def list_recursive(
498 url: str,
499 start_after: str | None = None,
500 end_at: str | None = None,
501 max_workers: int = 32,
502 look_ahead: int = 2,
503 patterns: PatternList | None = None,
504 symlink_handling: SymlinkHandling = SymlinkHandling.FOLLOW,
505) -> Iterator[ObjectMetadata]:
506 """
507 Lists files recursively under the specified URL.
508
509 This function retrieves the corresponding :py:class:`multistorageclient.StorageClient`
510 for the given URL and returns an iterator of files under the provided path.
511
512 :param url: The path to list objects under.
513 :param start_after: The key to start after (i.e. exclusive). An object with this key doesn't have to exist.
514 :param end_at: The key to end at (i.e. inclusive). An object with this key doesn't have to exist.
515 :param max_workers: Maximum concurrent workers for provider-level recursive listing.
516 :param look_ahead: Prefixes to buffer per worker for provider-level recursive listing.
517 :param patterns: PatternList for include/exclude filtering. If None, all files are included.
518 :param symlink_handling: How to handle symbolic links during listing.
519 :return: An iterator of :py:class:`ObjectMetadata` objects representing files accessible under the specified URL path.
520 The returned keys use the same URL-prefix behavior as :py:meth:`multistorageclient.list`.
521 """
522 client, path = resolve_storage_client(url)
523 return client.list_recursive(
524 path=path,
525 start_after=start_after,
526 end_at=end_at,
527 max_workers=max_workers,
528 look_ahead=look_ahead,
529 include_url_prefix=True,
530 patterns=patterns,
531 symlink_handling=symlink_handling,
532 )
533
534
[docs]
535def write(url: str, body: bytes, attributes: dict[str, Any] | None = None) -> None:
536 """
537 Writes an object to the storage provider at the specified path.
538
539 :param url: The path where the object should be written.
540 :param body: The content to write to the object.
541 """
542 client, path = resolve_storage_client(url)
543 client.write(path=path, body=body, attributes=attributes)
544
545
[docs]
546def make_symlink(url: str, target_url: str) -> None:
547 """
548 Creates a symbolic link at ``url`` pointing to ``target_url``.
549
550 Both URLs must resolve to the same storage profile.
551
552 :param url: The URL where the symlink will be created.
553 :param target_url: The URL of the target that the symlink points to.
554 :raises ValueError: If the two URLs resolve to different storage profiles.
555 """
556 client, path = resolve_storage_client(url)
557 target_client, target_path = resolve_storage_client(target_url)
558 if client is not target_client:
559 raise ValueError("Cannot create cross-profile symlink: url and target_url must belong to the same profile.")
560 client.make_symlink(path=path, target=target_path)
561
562
[docs]
563def delete(url: str, recursive: bool = False) -> None:
564 """
565 Deletes the specified object(s) from the storage provider.
566
567 This function retrieves the corresponding :py:class:`multistorageclient.StorageClient`
568 for the given URL and deletes the object(s) at the specified path.
569
570 :param url: The URL of the object to delete. (example: ``msc://profile/prefix/file.txt``)
571 :param recursive: Whether to delete objects in the path recursively.
572 """
573 client, path = resolve_storage_client(url)
574 client.delete(path, recursive=recursive)
575
576
[docs]
577def info(url: str) -> ObjectMetadata:
578 """
579 Retrieves metadata or information about an object stored at the specified path.
580
581 :param url: The URL of the object to retrieve information about. (example: ``msc://profile/prefix/file.txt``)
582
583 :return: An :py:class:`ObjectMetadata` object representing the object's metadata.
584 """
585 client, path = resolve_storage_client(url)
586 return client.info(path)
587
588
597
598
[docs]
599def generate_presigned_url(
600 url: str,
601 *,
602 method: str = "GET",
603 signer_type: SignerType | None = None,
604 signer_options: dict[str, Any] | None = None,
605) -> str:
606 """
607 Generate a pre-signed URL granting temporary access to the object at *url*.
608
609 :param url: The storage URL. (example: ``msc://profile/prefix/file.bin``)
610 :param method: The HTTP method the URL should authorise (e.g. ``"GET"``, ``"PUT"``).
611 :param signer_type: The signing backend to use. ``None`` means the provider's native signer.
612 :param signer_options: Backend-specific options forwarded to the signer.
613 :return: A pre-signed URL string.
614 """
615 client, path = resolve_storage_client(url)
616 return client.generate_presigned_url(path, method=method, signer_type=signer_type, signer_options=signer_options)