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