From d1097daced99a5a2b11dc4890d7f984722280dcd Mon Sep 17 00:00:00 2001 From: Enam Mijbah Noor Date: Mon, 29 Jan 2024 14:46:01 +0600 Subject: [PATCH 1/4] Implement B2URI in `ls` & `rm` of `_b2v4` --- b2/_internal/_cli/b2args.py | 25 +++++++++- b2/_internal/_utils/uri.py | 16 +++--- b2/_internal/b2v3/registry.py | 11 +++++ b2/_internal/console_tool.py | 49 +++++++++++++++---- .../+b2uri-special-characters.fixed.md | 1 + test/unit/_utils/test_uri.py | 3 ++ 6 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 changelog.d/+b2uri-special-characters.fixed.md diff --git a/b2/_internal/_cli/b2args.py b/b2/_internal/_cli/b2args.py index 178d81d8d..5880c7234 100644 --- a/b2/_internal/_cli/b2args.py +++ b/b2/_internal/_cli/b2args.py @@ -29,10 +29,33 @@ def b2_file_uri(value: str) -> B2URIBase: return b2_uri -B2_URI_ARG_TYPE = wrap_with_argument_type_error(parse_b2_uri) +def b2_uri(value: str) -> B2URI: + uri = parse_b2_uri(value) + if not isinstance(uri, B2URI): + raise ValueError( + f"B2 URI of the form b2://bucket/path/ is required, but {value} was provided" + ) + return uri + + +B2_URI_ARG_TYPE = wrap_with_argument_type_error(b2_uri) B2_URI_FILE_ARG_TYPE = wrap_with_argument_type_error(b2_file_uri) +def add_b2_uri_argument(parser: argparse.ArgumentParser, name="B2_URI"): + """ + Add a B2 URI pointing to a file as an argument to the parser. + """ + parser.add_argument( + name, + type=B2_URI_ARG_TYPE, + help= + "B2 URI pointing to a bucket, directory or a pattern, " + "e.g. b2://yourBucket, b2://yourBucket/file.txt, b2://yourBucket/folder, " + "b2://yourBucket/*.txt or b2id://fileId", + ) + + def add_b2_file_argument(parser: argparse.ArgumentParser, name="B2_URI"): """ Add a B2 URI pointing to a file as an argument to the parser. diff --git a/b2/_internal/_utils/uri.py b/b2/_internal/_utils/uri.py index 91da61df4..ab4dcaa5c 100644 --- a/b2/_internal/_utils/uri.py +++ b/b2/_internal/_utils/uri.py @@ -11,7 +11,7 @@ import dataclasses import pathlib -import urllib +import urllib.parse from pathlib import Path from b2sdk.v2 import ( @@ -80,18 +80,18 @@ def __str__(self) -> str: def parse_uri(uri: str) -> Path | B2URI | B2FileIdURI: - parsed = urllib.parse.urlparse(uri) + parsed = urllib.parse.urlsplit(uri) if parsed.scheme == "": return pathlib.Path(uri) return _parse_b2_uri(uri, parsed) def parse_b2_uri(uri: str) -> B2URI | B2FileIdURI: - parsed = urllib.parse.urlparse(uri) + parsed = urllib.parse.urlsplit(uri) return _parse_b2_uri(uri, parsed) -def _parse_b2_uri(uri, parsed: urllib.parse.ParseResult) -> B2URI | B2FileIdURI: +def _parse_b2_uri(uri, parsed: urllib.parse.SplitResult) -> B2URI | B2FileIdURI: if parsed.scheme in ("b2", "b2id"): if not parsed.netloc: raise ValueError(f"Invalid B2 URI: {uri!r}") @@ -101,12 +101,10 @@ def _parse_b2_uri(uri, parsed: urllib.parse.ParseResult) -> B2URI | B2FileIdURI: ) if parsed.scheme == "b2": - return B2URI(bucket_name=parsed.netloc, path=parsed.path) + path = urllib.parse.urlunsplit(parsed._replace(scheme="", netloc="")) + return B2URI(bucket_name=parsed.netloc, path=path) elif parsed.scheme == "b2id": - file_id = parsed.netloc - if not file_id: - raise ValueError(f"File id was not provided in B2 URI: {uri!r}") - return B2FileIdURI(file_id=file_id) + return B2FileIdURI(file_id=parsed.netloc) else: raise ValueError(f"Unsupported URI scheme: {parsed.scheme!r}") diff --git a/b2/_internal/b2v3/registry.py b/b2/_internal/b2v3/registry.py index 7bf0091ab..1288a27b8 100644 --- a/b2/_internal/b2v3/registry.py +++ b/b2/_internal/b2v3/registry.py @@ -11,6 +11,17 @@ # ruff: noqa: F405 from b2._internal._b2v4.registry import * # noqa + +class Ls(B2URIBucketNFolderNameArgMixin, BaseLs): + __doc__ = BaseLs.__doc__ + # TODO: fix doc + + +class Rm(B2URIBucketNFolderNameArgMixin, BaseRm): + __doc__ = BaseRm.__doc__ + # TODO: fix doc + + B2.register_subcommand(AuthorizeAccount) B2.register_subcommand(CancelAllUnfinishedLargeFiles) B2.register_subcommand(CancelLargeFile) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 6e2a39a5f..74d22a420 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -126,7 +126,7 @@ autocomplete_install, ) from b2._internal._cli.b2api import _get_b2api_for_profile -from b2._internal._cli.b2args import add_b2_file_argument +from b2._internal._cli.b2args import add_b2_file_argument, add_b2_uri_argument from b2._internal._cli.const import ( B2_APPLICATION_KEY_ENV_VAR, B2_APPLICATION_KEY_ID_ENV_VAR, @@ -632,6 +632,27 @@ def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URIBase: return B2URI(args.bucketName, args.fileName) +class B2URIBucketNFolderNameArgMixin: + @classmethod + def _setup_parser(cls, parser): + parser.add_argument('bucketName').completer = bucket_name_completer + parser.add_argument('folderName', nargs='?').completer = file_name_completer + super()._setup_parser(parser) + + def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URI: + return B2URI(args.bucketName, args.folderName) + + +class B2URIMixin: + @classmethod + def _setup_parser(cls, parser): + add_b2_uri_argument(parser) + super()._setup_parser(parser) + + def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URI: + return args.B2_URI + + class UploadModeMixin(Described): """ Use --incrementalMode to allow for incremental file uploads to safe bandwidth. This will only affect files, which @@ -2140,7 +2161,6 @@ class AbstractLsCommand(Command, metaclass=ABCMeta): The ``--recursive`` option will descend into folders, and will select only files, not folders. - The ``--withWildcard`` option will allow using ``*``, ``?`` and ```[]``` characters in ``folderName`` as a greedy wildcard, single character wildcard and range of characters. It requires the ``--recursive`` option. @@ -2152,8 +2172,6 @@ def _setup_parser(cls, parser): parser.add_argument('--versions', action='store_true') parser.add_argument('-r', '--recursive', action='store_true') parser.add_argument('--withWildcard', action='store_true') - parser.add_argument('bucketName').completer = bucket_name_completer - parser.add_argument('folderName', nargs='?').completer = file_name_completer super()._setup_parser(parser) def _print_files(self, args): @@ -2171,13 +2189,12 @@ def _print_file_version( self._print(folder_name or file_version.file_name) def _get_ls_generator(self, args): - start_file_name = args.folderName or '' - - bucket = self.api.get_bucket_by_name(args.bucketName) + b2_uri = self.get_b2_uri_from_arg(args) + bucket = self.api.get_bucket_by_name(b2_uri.bucket_name) try: yield from bucket.ls( - start_file_name, + b2_uri.path, latest_only=not args.versions, recursive=args.recursive, with_wildcard=args.withWildcard, @@ -2187,8 +2204,11 @@ def _get_ls_generator(self, args): # exactly one – `with_wildcard` being passed without `recursive` option. raise B2Error(error.args[0]) + def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URI: + raise NotImplementedError + -class Ls(AbstractLsCommand): +class BaseLs(AbstractLsCommand, metaclass=ABCMeta): """ Using the file naming convention that ``/`` separates folder names from their contents, returns a list of the files @@ -2307,7 +2327,11 @@ def format_ls_entry(self, file_version: FileVersion, replication: bool): return template % tuple(parameters) -class Rm(ThreadsMixin, AbstractLsCommand): +class Ls(B2URIMixin, BaseLs): + __doc__ = BaseLs.__doc__ + + +class BaseRm(ThreadsMixin, AbstractLsCommand, metaclass=ABCMeta): """ Removes a "folder" or a set of files matching a pattern. Use with caution. @@ -2477,6 +2501,7 @@ def _setup_parser(cls, parser): ) parser.add_argument('--noProgress', action='store_true') parser.add_argument('--failFast', action='store_true') + # TODO: --bypassGovernance super()._setup_parser(parser) def _run(self, args): @@ -2514,6 +2539,10 @@ def _run(self, args): return 1 if failed_on_any_file else 0 +class Rm(B2URIMixin, BaseRm): + __doc__ = BaseRm.__doc__ + + class GetUrlBase(Command): """ Prints an URL that can be used to download the given file, if diff --git a/changelog.d/+b2uri-special-characters.fixed.md b/changelog.d/+b2uri-special-characters.fixed.md new file mode 100644 index 000000000..766c58895 --- /dev/null +++ b/changelog.d/+b2uri-special-characters.fixed.md @@ -0,0 +1 @@ +Fixed special characters `?` and `#` in b2uri causing incorrect parsing of the URI. \ No newline at end of file diff --git a/test/unit/_utils/test_uri.py b/test/unit/_utils/test_uri.py index a2443b9cf..34b706dec 100644 --- a/test/unit/_utils/test_uri.py +++ b/test/unit/_utils/test_uri.py @@ -66,6 +66,9 @@ def test_b2fileuri_str(): ("./some/local/path", Path("some/local/path")), ("b2://bucket/path/to/dir/", B2URI(bucket_name="bucket", path="path/to/dir/")), ("b2id://file123", B2FileIdURI(file_id="file123")), + ("b2://bucket/wild[card]", B2URI(bucket_name="bucket", path="wild[card]")), + ("b2://bucket/wild?card", B2URI(bucket_name="bucket", path="wild?card")), + ("b2://bucket/special#char", B2URI(bucket_name="bucket", path="special#char")), ], ) def test_parse_uri(uri, expected): From 79307f2dfbbde9a24aa7834863d46b90358abd38 Mon Sep 17 00:00:00 2001 From: Enam Mijbah Noor Date: Wed, 31 Jan 2024 19:58:11 +0600 Subject: [PATCH 2/4] Make ls and rm tests to also work with B2URI --- b2/_internal/_cli/b2args.py | 3 +- b2/_internal/b2v3/registry.py | 6 +- b2/_internal/b2v3/rm.py | 19 ++++ b2/_internal/console_tool.py | 2 +- test/helpers.py | 15 +++ test/integration/conftest.py | 12 ++- test/integration/helpers.py | 6 +- test/integration/test_b2_command_line.py | 76 ++++++++----- test/unit/conftest.py | 11 ++ test/unit/test_base.py | 2 +- test/unit/test_console_tool.py | 129 +++++++++++++++-------- 11 files changed, 197 insertions(+), 84 deletions(-) create mode 100644 b2/_internal/b2v3/rm.py diff --git a/b2/_internal/_cli/b2args.py b/b2/_internal/_cli/b2args.py index 5880c7234..9188c6d97 100644 --- a/b2/_internal/_cli/b2args.py +++ b/b2/_internal/_cli/b2args.py @@ -49,8 +49,7 @@ def add_b2_uri_argument(parser: argparse.ArgumentParser, name="B2_URI"): parser.add_argument( name, type=B2_URI_ARG_TYPE, - help= - "B2 URI pointing to a bucket, directory or a pattern, " + help="B2 URI pointing to a bucket, directory or a pattern, " "e.g. b2://yourBucket, b2://yourBucket/file.txt, b2://yourBucket/folder, " "b2://yourBucket/*.txt or b2id://fileId", ) diff --git a/b2/_internal/b2v3/registry.py b/b2/_internal/b2v3/registry.py index 1288a27b8..19847711f 100644 --- a/b2/_internal/b2v3/registry.py +++ b/b2/_internal/b2v3/registry.py @@ -10,6 +10,7 @@ # ruff: noqa: F405 from b2._internal._b2v4.registry import * # noqa +from .rm import Rm class Ls(B2URIBucketNFolderNameArgMixin, BaseLs): @@ -17,11 +18,6 @@ class Ls(B2URIBucketNFolderNameArgMixin, BaseLs): # TODO: fix doc -class Rm(B2URIBucketNFolderNameArgMixin, BaseRm): - __doc__ = BaseRm.__doc__ - # TODO: fix doc - - B2.register_subcommand(AuthorizeAccount) B2.register_subcommand(CancelAllUnfinishedLargeFiles) B2.register_subcommand(CancelLargeFile) diff --git a/b2/_internal/b2v3/rm.py b/b2/_internal/b2v3/rm.py new file mode 100644 index 000000000..fac93ca83 --- /dev/null +++ b/b2/_internal/b2v3/rm.py @@ -0,0 +1,19 @@ +###################################################################### +# +# File: b2/_internal/b2v3/rm.py +# +# Copyright 2023 Backblaze Inc. All Rights Reserved. +# +# License https://www.backblaze.com/using_b2_code.html +# +###################################################################### +from __future__ import annotations + +from b2._internal._b2v4.registry import B2URIBucketNFolderNameArgMixin, BaseRm + + +# NOTE: We need to keep v3 Rm in separate file, because we need to import it in +# unit tests without registering any commands. +class Rm(B2URIBucketNFolderNameArgMixin, BaseRm): + __doc__ = BaseRm.__doc__ + # TODO: fix doc diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 74d22a420..6cfd440ae 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -640,7 +640,7 @@ def _setup_parser(cls, parser): super()._setup_parser(parser) def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URI: - return B2URI(args.bucketName, args.folderName) + return B2URI(args.bucketName, args.folderName or '') class B2URIMixin: diff --git a/test/helpers.py b/test/helpers.py index 5b1181a02..ba9c6e531 100644 --- a/test/helpers.py +++ b/test/helpers.py @@ -11,9 +11,24 @@ import pytest +_MISSING = object() + def skip_on_windows(*args, reason='Not supported on Windows', **kwargs): return pytest.mark.skipif( platform.system() == 'Windows', reason=reason, )(*args, **kwargs) + + +def b2_uri_args_v3(bucket_name, path=_MISSING): + if path is _MISSING: + return [bucket_name] + else: + return [bucket_name, path] + + +def b2_uri_args_v4(bucket_name, path=_MISSING): + if path is _MISSING: + path = '' + return [f'b2://{bucket_name}/{path}'] diff --git a/test/integration/conftest.py b/test/integration/conftest.py index f54bb78d0..5bada7f85 100755 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -29,6 +29,7 @@ get_int_version, ) +from ..helpers import b2_uri_args_v3, b2_uri_args_v4 from .helpers import NODE_DESCRIPTION, RNG_SEED, Api, CommandLine, bucket_name_part, random_token logger = logging.getLogger(__name__) @@ -242,7 +243,7 @@ def b2_api( @pytest.fixture(scope='module') def global_b2_tool( request, application_key_id, application_key, realm, this_run_bucket_name_prefix, b2_api, - auto_change_account_info_dir + auto_change_account_info_dir, b2_uri_args ) -> CommandLine: tool = CommandLine( request.config.getoption('--sut'), @@ -252,6 +253,7 @@ def global_b2_tool( this_run_bucket_name_prefix, request.config.getoption('--env-file-cmd-placeholder'), api_wrapper=b2_api, + b2_uri_args=b2_uri_args, ) tool.reauthorize(check_key_capabilities=True) # reauthorize for the first time (with check) yield tool @@ -376,3 +378,11 @@ def pytest_collection_modifyitems(items): for item in items: if SECRET_FIXTURES & set(getattr(item, 'fixturenames', ())): item.add_marker('require_secrets') + + +@pytest.fixture(scope='module') +def b2_uri_args(cli_int_version): + if cli_int_version >= 4: + return b2_uri_args_v4 + else: + return b2_uri_args_v3 diff --git a/test/integration/helpers.py b/test/integration/helpers.py index ca6aee32d..ec2504f73 100755 --- a/test/integration/helpers.py +++ b/test/integration/helpers.py @@ -394,6 +394,7 @@ def __init__( bucket_name_prefix, env_file_cmd_placeholder, api_wrapper: Api, + b2_uri_args, ): self.command = command self.account_id = account_id @@ -403,6 +404,7 @@ def __init__( self.env_file_cmd_placeholder = env_file_cmd_placeholder self.env_var_test_context = EnvVarTestContext(SqliteAccountInfo().filename) self.api_wrapper = api_wrapper + self.b2_uri_args = b2_uri_args def generate_bucket_name(self): return self.api_wrapper.new_bucket_name() @@ -561,7 +563,9 @@ def reauthorize(self, check_key_capabilities=False): assert not missing_capabilities, f'it appears that the raw_api integration test is being run with a non-full key. Missing capabilities: {missing_capabilities}' def list_file_versions(self, bucket_name): - return self.should_succeed_json(['ls', '--json', '--recursive', '--versions', bucket_name]) + return self.should_succeed_json( + ['ls', '--json', '--recursive', '--versions', *self.b2_uri_args(bucket_name)] + ) def cleanup_buckets(self, buckets: dict[str, dict | None]) -> None: for bucket_name, bucket_dict in buckets.items(): diff --git a/test/integration/test_b2_command_line.py b/test/integration/test_b2_command_line.py index 34f74c597..db9a5001a 100755 --- a/test/integration/test_b2_command_line.py +++ b/test/integration/test_b2_command_line.py @@ -82,7 +82,7 @@ def test_download(b2_tool, bucket_name, sample_filepath, uploaded_sample_file, t assert output_b.read_text() == sample_filepath.read_text() -def test_basic(b2_tool, bucket_name, sample_file, tmp_path): +def test_basic(b2_tool, bucket_name, sample_file, tmp_path, b2_uri_args): file_mod_time_str = str(file_mod_time_millis(sample_file)) @@ -95,7 +95,7 @@ def test_basic(b2_tool, bucket_name, sample_file, tmp_path): ) b2_tool.should_succeed(['upload-file', '--quiet', bucket_name, sample_file, 'a']) - b2_tool.should_succeed(['ls', '--long', '--replication', bucket_name]) + b2_tool.should_succeed(['ls', '--long', '--replication', *b2_uri_args(bucket_name)]) b2_tool.should_succeed(['upload-file', '--noProgress', bucket_name, sample_file, 'a']) b2_tool.should_succeed(['upload-file', '--noProgress', bucket_name, sample_file, 'b/1']) b2_tool.should_succeed(['upload-file', '--noProgress', bucket_name, sample_file, 'b/2']) @@ -121,22 +121,26 @@ def test_basic(b2_tool, bucket_name, sample_file, tmp_path): b2_tool.should_succeed(['upload-file', '--noProgress', bucket_name, sample_file, 'rm']) b2_tool.should_succeed(['upload-file', '--noProgress', bucket_name, sample_file, 'rm1']) # with_wildcard allows us to target a single file. rm will be removed, rm1 will be left alone - b2_tool.should_succeed(['rm', '--recursive', '--withWildcard', bucket_name, 'rm']) + b2_tool.should_succeed(['rm', '--recursive', '--withWildcard', *b2_uri_args(bucket_name, 'rm')]) list_of_files = b2_tool.should_succeed_json( - ['ls', '--json', '--recursive', '--withWildcard', bucket_name, 'rm*'] + ['ls', '--json', '--recursive', '--withWildcard', *b2_uri_args(bucket_name, 'rm*')] ) should_equal(['rm1'], [f['fileName'] for f in list_of_files]) - b2_tool.should_succeed(['rm', '--recursive', '--withWildcard', bucket_name, 'rm1']) + b2_tool.should_succeed( + ['rm', '--recursive', '--withWildcard', *b2_uri_args(bucket_name, 'rm1')] + ) b2_tool.should_succeed(['download-file', '--quiet', f'b2://{bucket_name}/b/1', tmp_path / 'a']) b2_tool.should_succeed(['hide-file', bucket_name, 'c']) - list_of_files = b2_tool.should_succeed_json(['ls', '--json', '--recursive', bucket_name]) + list_of_files = b2_tool.should_succeed_json( + ['ls', '--json', '--recursive', *b2_uri_args(bucket_name)] + ) should_equal(['a', 'b/1', 'b/2', 'd'], [f['fileName'] for f in list_of_files]) list_of_files = b2_tool.should_succeed_json( - ['ls', '--json', '--recursive', '--versions', bucket_name] + ['ls', '--json', '--recursive', '--versions', *b2_uri_args(bucket_name)] ) should_equal(['a', 'a', 'b/1', 'b/2', 'c', 'c', 'd'], [f['fileName'] for f in list_of_files]) should_equal( @@ -149,31 +153,36 @@ def test_basic(b2_tool, bucket_name, sample_file, tmp_path): first_c_version = list_of_files[4] second_c_version = list_of_files[5] list_of_files = b2_tool.should_succeed_json( - ['ls', '--json', '--recursive', '--versions', bucket_name, 'c'] + ['ls', '--json', '--recursive', '--versions', *b2_uri_args(bucket_name, 'c')] ) should_equal([], [f['fileName'] for f in list_of_files]) b2_tool.should_succeed(['copy-file-by-id', first_a_version['fileId'], bucket_name, 'x']) - b2_tool.should_succeed(['ls', bucket_name], '^a{0}b/{0}d{0}'.format(os.linesep)) + b2_tool.should_succeed(['ls', *b2_uri_args(bucket_name)], '^a{0}b/{0}d{0}'.format(os.linesep)) # file_id, action, date, time, size(, replication), name b2_tool.should_succeed( - ['ls', '--long', bucket_name], + ['ls', '--long', *b2_uri_args(bucket_name)], '^4_z.* upload .* {1} a{0}.* - .* b/{0}4_z.* upload .* {1} d{0}'.format( os.linesep, len(file_data) ) ) b2_tool.should_succeed( - ['ls', '--long', '--replication', bucket_name], + ['ls', '--long', '--replication', *b2_uri_args(bucket_name)], '^4_z.* upload .* {1} - a{0}.* - .* - b/{0}4_z.* upload .* {1} - d{0}'.format( os.linesep, len(file_data) ) ) b2_tool.should_succeed( - ['ls', '--versions', bucket_name], '^a{0}a{0}b/{0}c{0}c{0}d{0}'.format(os.linesep) + ['ls', '--versions', *b2_uri_args(bucket_name)], + '^a{0}a{0}b/{0}c{0}c{0}d{0}'.format(os.linesep) + ) + b2_tool.should_succeed( + ['ls', *b2_uri_args(bucket_name, 'b')], '^b/1{0}b/2{0}'.format(os.linesep) + ) + b2_tool.should_succeed( + ['ls', *b2_uri_args(bucket_name, 'b/')], '^b/1{0}b/2{0}'.format(os.linesep) ) - b2_tool.should_succeed(['ls', bucket_name, 'b'], '^b/1{0}b/2{0}'.format(os.linesep)) - b2_tool.should_succeed(['ls', bucket_name, 'b/'], '^b/1{0}b/2{0}'.format(os.linesep)) file_info = b2_tool.should_succeed_json(['file-info', f"b2id://{second_c_version['fileId']}"]) expected_info = { @@ -184,7 +193,9 @@ def test_basic(b2_tool, bucket_name, sample_file, tmp_path): should_equal(expected_info, file_info['fileInfo']) b2_tool.should_succeed(['delete-file-version', 'c', first_c_version['fileId']]) - b2_tool.should_succeed(['ls', bucket_name], '^a{0}b/{0}c{0}d{0}'.format(os.linesep)) + b2_tool.should_succeed( + ['ls', *b2_uri_args(bucket_name)], '^a{0}b/{0}c{0}d{0}'.format(os.linesep) + ) b2_tool.should_succeed(['get-url', f"b2id://{second_c_version['fileId']}"]) @@ -277,7 +288,7 @@ def test_bucket(b2_tool, bucket_name): ] -def test_key_restrictions(b2_tool, bucket_name, sample_file, bucket_factory): +def test_key_restrictions(b2_tool, bucket_name, sample_file, bucket_factory, b2_uri_args): # A single file for rm to fail on. b2_tool.should_succeed(['upload-file', '--noProgress', bucket_name, sample_file, 'test']) @@ -315,7 +326,7 @@ def test_key_restrictions(b2_tool, bucket_name, sample_file, bucket_factory): ['authorize-account', '--environment', b2_tool.realm, key_two_id, key_two], ) b2_tool.should_succeed(['get-bucket', bucket_name],) - b2_tool.should_succeed(['ls', bucket_name],) + b2_tool.should_succeed(['ls', *b2_uri_args(bucket_name)],) # Capabilities can be listed in any order. While this regex doesn't confirm that all three are present, # in ensures that there are three in total. @@ -323,16 +334,18 @@ def test_key_restrictions(b2_tool, bucket_name, sample_file, bucket_factory): r'application key with capabilities ' \ r"'(.*listFiles.*|.*listBuckets.*|.*readFiles.*){3}', " \ r"restricted to bucket '%s' \(unauthorized\)" % bucket_name - b2_tool.should_fail(['rm', '--recursive', '--noProgress', bucket_name], failed_bucket_err) + b2_tool.should_fail( + ['rm', '--recursive', '--noProgress', *b2_uri_args(bucket_name)], failed_bucket_err + ) failed_bucket_err = r'ERROR: Application key is restricted to bucket: ' + bucket_name b2_tool.should_fail(['get-bucket', second_bucket_name], failed_bucket_err) failed_list_files_err = r'ERROR: Application key is restricted to bucket: ' + bucket_name - b2_tool.should_fail(['ls', second_bucket_name], failed_list_files_err) + b2_tool.should_fail(['ls', *b2_uri_args(second_bucket_name)], failed_list_files_err) failed_list_files_err = r'ERROR: Application key is restricted to bucket: ' + bucket_name - b2_tool.should_fail(['rm', second_bucket_name], failed_list_files_err) + b2_tool.should_fail(['rm', *b2_uri_args(second_bucket_name)], failed_list_files_err) # reauthorize with more capabilities for clean up b2_tool.should_succeed( @@ -1140,7 +1153,7 @@ def test_default_sse_b2__create_bucket(b2_tool, schedule_bucket_cleanup): should_equal(second_bucket_default_sse, second_bucket_info['defaultServerSideEncryption']) -def test_sse_b2(b2_tool, bucket_name, sample_file, tmp_path): +def test_sse_b2(b2_tool, bucket_name, sample_file, tmp_path, b2_uri_args): b2_tool.should_succeed( [ 'upload-file', '--destinationServerSideEncryption=SSE-B2', '--quiet', bucket_name, @@ -1159,7 +1172,9 @@ def test_sse_b2(b2_tool, bucket_name, sample_file, tmp_path): ] ) - list_of_files = b2_tool.should_succeed_json(['ls', '--json', '--recursive', bucket_name]) + list_of_files = b2_tool.should_succeed_json( + ['ls', '--json', '--recursive', *b2_uri_args(bucket_name)] + ) should_equal( [{ 'algorithm': 'AES256', @@ -1188,7 +1203,9 @@ def test_sse_b2(b2_tool, bucket_name, sample_file, tmp_path): ['copy-file-by-id', not_encrypted_version['fileId'], bucket_name, 'copied_not_encrypted'] ) - list_of_files = b2_tool.should_succeed_json(['ls', '--json', '--recursive', bucket_name]) + list_of_files = b2_tool.should_succeed_json( + ['ls', '--json', '--recursive', *b2_uri_args(bucket_name)] + ) should_equal( [{ 'algorithm': 'AES256', @@ -1211,7 +1228,7 @@ def test_sse_b2(b2_tool, bucket_name, sample_file, tmp_path): should_equal({'mode': 'none'}, file_info['serverSideEncryption']) -def test_sse_c(b2_tool, bucket_name, is_running_on_docker, sample_file, tmp_path): +def test_sse_c(b2_tool, bucket_name, is_running_on_docker, sample_file, tmp_path, b2_uri_args): sse_c_key_id = 'user-generated-key-id \nąóźćż\nœøΩ≈ç\nßäöü' if is_running_on_docker: @@ -1417,7 +1434,9 @@ def test_sse_c(b2_tool, bucket_name, is_running_on_docker, sample_file, tmp_path 'B2_DESTINATION_SSE_C_KEY_ID': 'another-user-generated-key-id', } ) - list_of_files = b2_tool.should_succeed_json(['ls', '--json', '--recursive', bucket_name]) + list_of_files = b2_tool.should_succeed_json( + ['ls', '--json', '--recursive', *b2_uri_args(bucket_name)] + ) should_equal( [ { @@ -2617,7 +2636,7 @@ def _assert_file_lock_configuration( assert legal_hold == actual_legal_hold -def test_upload_file__custom_upload_time(b2_tool, bucket_name, sample_file): +def test_upload_file__custom_upload_time(b2_tool, bucket_name, sample_file, b2_uri_args): file_data = read_file(sample_file) cut = 12345 cut_printable = '1970-01-01 00:00:12' @@ -2637,14 +2656,15 @@ def test_upload_file__custom_upload_time(b2_tool, bucket_name, sample_file): else: # file_id, action, date, time, size(, replication), name b2_tool.should_succeed( - ['ls', '--long', bucket_name], '^4_z.* upload {} +{} a'.format( + ['ls', '--long', *b2_uri_args(bucket_name)], '^4_z.* upload {} +{} a'.format( cut_printable, len(file_data), ) ) # file_id, action, date, time, size(, replication), name b2_tool.should_succeed( - ['ls', '--long', '--replication', bucket_name], '^4_z.* upload {} +{} - a'.format( + ['ls', '--long', '--replication', *b2_uri_args(bucket_name)], + '^4_z.* upload {} +{} - a'.format( cut_printable, len(file_data), ) diff --git a/test/unit/conftest.py b/test/unit/conftest.py index ce5cfd5a1..7a12bb788 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -17,6 +17,7 @@ from b2._internal.console_tool import _TqdmCloser from b2._internal.version_listing import CLI_VERSIONS, UNSTABLE_CLI_VERSION, get_int_version +from ..helpers import b2_uri_args_v3, b2_uri_args_v4 from .helpers import RunOrDieExecutor from .test_console_tool import BaseConsoleToolTest @@ -147,3 +148,13 @@ def uploaded_file(b2_cli, bucket_info, local_file): 'fileId': '9999', 'content': local_file.read_text(), } + + +@pytest.fixture(scope='class') +def b2_uri_args(cli_int_version, request): + if cli_int_version >= 4: + fn = b2_uri_args_v4 + else: + fn = b2_uri_args_v3 + + request.cls.b2_uri_args = staticmethod(fn) diff --git a/test/unit/test_base.py b/test/unit/test_base.py index f454e2c34..1df7f9058 100644 --- a/test/unit/test_base.py +++ b/test/unit/test_base.py @@ -16,7 +16,7 @@ import pytest -@pytest.mark.usefixtures('unit_test_console_tool_class') +@pytest.mark.usefixtures('unit_test_console_tool_class', 'b2_uri_args') class TestBase(unittest.TestCase): console_tool_class: Type diff --git a/test/unit/test_console_tool.py b/test/unit/test_console_tool.py index 20da6d671..dfc5f7f64 100644 --- a/test/unit/test_console_tool.py +++ b/test/unit/test_console_tool.py @@ -32,12 +32,13 @@ from b2sdk.v2.exception import Conflict # Any error for testing fast-fail of the rm command. from more_itertools import one +from b2._internal._b2v4.registry import Rm as v4Rm from b2._internal._cli.const import ( B2_APPLICATION_KEY_ENV_VAR, B2_APPLICATION_KEY_ID_ENV_VAR, B2_ENVIRONMENT_ENV_VAR, ) -from b2._internal.b2v3.registry import Rm +from b2._internal.b2v3.rm import Rm as v3Rm from b2._internal.version import VERSION from .test_base import TestBase @@ -720,7 +721,7 @@ def test_keys(self): self._run_command(['get-bucket', 'my-bucket-a'], expected_json_in_stdout=expected_json) self._run_command( - ['ls', '--json', 'my-bucket-c'], '', + ['ls', '--json', *self.b2_uri_args('my-bucket-c')], '', 'ERROR: Application key is restricted to bucket: my-bucket-a\n', 1 ) @@ -877,7 +878,7 @@ def test_files(self): ] self._run_command( - ['ls', '--json', '--versions', 'my-bucket'], + ['ls', '--json', '--versions', *self.b2_uri_args('my-bucket')], expected_json_in_stdout=expected_json, ) @@ -886,7 +887,9 @@ def test_files(self): [] ''' - self._run_command(['ls', '--json', 'my-bucket'], expected_stdout, '', 0) + self._run_command( + ['ls', '--json', *self.b2_uri_args('my-bucket')], expected_stdout, '', 0 + ) # Delete one file version, passing the name in expected_json = {"action": "delete", "fileId": "9998", "fileName": "file1.txt"} @@ -1061,7 +1064,7 @@ def test_files_encrypted(self): ] self._run_command( - ['ls', '--json', '--versions', 'my-bucket'], + ['ls', '--json', '--versions', *self.b2_uri_args('my-bucket')], expected_json_in_stdout=expected_json, ) @@ -1070,7 +1073,9 @@ def test_files_encrypted(self): [] ''' - self._run_command(['ls', '--json', 'my-bucket'], expected_stdout, '', 0) + self._run_command( + ['ls', '--json', *self.b2_uri_args('my-bucket')], expected_stdout, '', 0 + ) # Delete one file version, passing the name in expected_json = {"action": "delete", "fileId": "9998", "fileName": "file1.txt"} @@ -1927,7 +1932,9 @@ def test_sync_dry_run(self): expected_stdout = ''' [] ''' - self._run_command(['ls', '--json', 'my-bucket'], expected_stdout, '', 0) + self._run_command( + ['ls', '--json', *self.b2_uri_args('my-bucket')], expected_stdout, '', 0 + ) # upload file expected_stdout = ''' @@ -1956,7 +1963,7 @@ def test_sync_dry_run(self): } ] self._run_command( - ['ls', '--json', 'my-bucket'], + ['ls', '--json', *self.b2_uri_args('my-bucket')], expected_json_in_stdout=expected_json, ) @@ -2096,7 +2103,7 @@ def test_ls(self): self._create_my_bucket() # Check with no files - self._run_command(['ls', 'my-bucket'], '', '', 0) + self._run_command(['ls', *self.b2_uri_args('my-bucket')], '', '', 0) # Create some files, including files in a folder bucket = self.b2_api.get_bucket_by_name('my-bucket') @@ -2112,7 +2119,7 @@ def test_ls(self): b/ c ''' - self._run_command(['ls', 'my-bucket'], expected_stdout, '', 0) + self._run_command(['ls', *self.b2_uri_args('my-bucket')], expected_stdout, '', 0) # Recursive output expected_stdout = ''' @@ -2121,8 +2128,10 @@ def test_ls(self): b/b2 c ''' - self._run_command(['ls', '--recursive', 'my-bucket'], expected_stdout, '', 0) - self._run_command(['ls', '-r', 'my-bucket'], expected_stdout, '', 0) + self._run_command( + ['ls', '--recursive', *self.b2_uri_args('my-bucket')], expected_stdout, '', 0 + ) + self._run_command(['ls', '-r', *self.b2_uri_args('my-bucket')], expected_stdout, '', 0) # Check long output. (The format expects full-length file ids, so it causes whitespace here) expected_stdout = ''' @@ -2130,7 +2139,7 @@ def test_ls(self): - - - - 0 b/ 9995 upload 1970-01-01 00:00:05 6 c ''' - self._run_command(['ls', '--long', 'my-bucket'], expected_stdout, '', 0) + self._run_command(['ls', '--long', *self.b2_uri_args('my-bucket')], expected_stdout, '', 0) # Check long versions output (The format expects full-length file ids, so it causes whitespace here) expected_stdout = ''' @@ -2139,14 +2148,19 @@ def test_ls(self): 9995 upload 1970-01-01 00:00:05 6 c 9996 upload 1970-01-01 00:00:05 5 c ''' - self._run_command(['ls', '--long', '--versions', 'my-bucket'], expected_stdout, '', 0) + self._run_command( + ['ls', '--long', '--versions', *self.b2_uri_args('my-bucket')], expected_stdout, '', 0 + ) def test_ls_wildcard(self): self._authorize_account() self._create_my_bucket() # Check with no files - self._run_command(['ls', '--recursive', '--withWildcard', 'my-bucket', '*.txt'], '', '', 0) + self._run_command( + ['ls', '--recursive', '--withWildcard', *self.b2_uri_args('my-bucket', '*.txt')], '', + '', 0 + ) # Create some files, including files in a folder bucket = self.b2_api.get_bucket_by_name('my-bucket') @@ -2162,7 +2176,7 @@ def test_ls_wildcard(self): c/test.tsv ''' self._run_command( - ['ls', '--recursive', '--withWildcard', 'my-bucket', '*.[tc]sv'], + ['ls', '--recursive', '--withWildcard', *self.b2_uri_args('my-bucket', '*.[tc]sv')], expected_stdout, ) @@ -2172,7 +2186,7 @@ def test_ls_wildcard(self): c/test.tsv ''' self._run_command( - ['ls', '--recursive', '--withWildcard', 'my-bucket', '*.tsv'], + ['ls', '--recursive', '--withWildcard', *self.b2_uri_args('my-bucket', '*.tsv')], expected_stdout, ) @@ -2180,7 +2194,10 @@ def test_ls_wildcard(self): b/b1/test.csv ''' self._run_command( - ['ls', '--recursive', '--withWildcard', 'my-bucket', 'b/b?/test.csv'], + [ + 'ls', '--recursive', '--withWildcard', + *self.b2_uri_args('my-bucket', 'b/b?/test.csv') + ], expected_stdout, ) @@ -2191,7 +2208,7 @@ def test_ls_wildcard(self): c/test.tsv ''' self._run_command( - ['ls', '--recursive', '--withWildcard', 'my-bucket', '?/test.?sv'], + ['ls', '--recursive', '--withWildcard', *self.b2_uri_args('my-bucket', '?/test.?sv')], expected_stdout, ) @@ -2200,7 +2217,7 @@ def test_ls_wildcard(self): b/b1/test.csv ''' self._run_command( - ['ls', '--recursive', '--withWildcard', 'my-bucket', '?/*/*.[!t]sv'], + ['ls', '--recursive', '--withWildcard', *self.b2_uri_args('my-bucket', '?/*/*.[!t]sv')], expected_stdout, ) @@ -2210,7 +2227,7 @@ def test_ls_with_wildcard_no_recursive(self): # Check with no files self._run_command( - ['ls', '--withWildcard', 'my-bucket'], + ['ls', '--withWildcard', *self.b2_uri_args('my-bucket')], expected_stderr='ERROR: with_wildcard requires recursive to be turned on as well\n', expected_status=1, ) @@ -2352,7 +2369,7 @@ def test_ls_for_restricted_bucket(self): # Authorize with the key and list the files self._run_command_ignore_output(['authorize-account', 'appKeyId0', 'appKey0'],) self._run_command( - ['ls', 'my-bucket'], + ['ls', *self.b2_uri_args('my-bucket')], '', '', 0, @@ -2499,8 +2516,11 @@ class InstantReporter(ProgressReport): @classmethod def setUpClass(cls) -> None: - cls.original_progress_class = Rm.PROGRESS_REPORT_CLASS - Rm.PROGRESS_REPORT_CLASS = cls.InstantReporter + cls.original_v3_progress_class = v3Rm.PROGRESS_REPORT_CLASS + cls.original_v4_progress_class = v4Rm.PROGRESS_REPORT_CLASS + + v3Rm.PROGRESS_REPORT_CLASS = cls.InstantReporter + v4Rm.PROGRESS_REPORT_CLASS = cls.InstantReporter def setUp(self): super().setUp() @@ -2513,11 +2533,15 @@ def setUp(self): @classmethod def tearDownClass(cls) -> None: - Rm.PROGRESS_REPORT_CLASS = cls.original_progress_class + v3Rm.PROGRESS_REPORT_CLASS = cls.original_v3_progress_class + v4Rm.PROGRESS_REPORT_CLASS = cls.original_v4_progress_class def test_rm_wildcard(self): self._run_command( - ['rm', '--recursive', '--withWildcard', '--noProgress', 'my-bucket', '*.csv'], + [ + 'rm', '--recursive', '--withWildcard', '--noProgress', + *self.b2_uri_args('my-bucket', '*.csv') + ], ) expected_stdout = ''' @@ -2526,14 +2550,17 @@ def test_rm_wildcard(self): b/test.txt c/test.tsv ''' - self._run_command(['ls', '--recursive', 'my-bucket'], expected_stdout) + self._run_command(['ls', '--recursive', *self.b2_uri_args('my-bucket')], expected_stdout) def test_rm_versions(self): # Uploading content of the bucket again to create second version of each file. self._upload_multiple_files(self.bucket) self._run_command( - ['rm', '--versions', '--recursive', '--withWildcard', 'my-bucket', '*.csv'], + [ + 'rm', '--versions', '--recursive', '--withWildcard', + *self.b2_uri_args('my-bucket', '*.csv') + ], ) expected_stdout = ''' @@ -2546,10 +2573,12 @@ def test_rm_versions(self): c/test.tsv c/test.tsv ''' - self._run_command(['ls', '--versions', '--recursive', 'my-bucket'], expected_stdout) + self._run_command( + ['ls', '--versions', '--recursive', *self.b2_uri_args('my-bucket')], expected_stdout + ) def test_rm_no_recursive(self): - self._run_command(['rm', '--noProgress', 'my-bucket', 'b/']) + self._run_command(['rm', '--noProgress', *self.b2_uri_args('my-bucket', 'b/')]) expected_stdout = ''' a/test.csv @@ -2557,7 +2586,7 @@ def test_rm_no_recursive(self): c/test.csv c/test.tsv ''' - self._run_command(['ls', '--recursive', 'my-bucket'], expected_stdout) + self._run_command(['ls', '--recursive', *self.b2_uri_args('my-bucket')], expected_stdout) def test_rm_dry_run(self): expected_stdout = ''' @@ -2567,7 +2596,10 @@ def test_rm_dry_run(self): c/test.csv ''' self._run_command( - ['rm', '--recursive', '--withWildcard', '--dryRun', 'my-bucket', '*.csv'], + [ + 'rm', '--recursive', '--withWildcard', '--dryRun', + *self.b2_uri_args('my-bucket', '*.csv') + ], expected_stdout, ) @@ -2581,11 +2613,14 @@ def test_rm_dry_run(self): c/test.csv c/test.tsv ''' - self._run_command(['ls', '--recursive', 'my-bucket'], expected_stdout) + self._run_command(['ls', '--recursive', *self.b2_uri_args('my-bucket')], expected_stdout) def test_rm_exact_filename(self): self._run_command( - ['rm', '--recursive', '--withWildcard', '--noProgress', 'my-bucket', 'b/b/test.csv'], + [ + 'rm', '--recursive', '--withWildcard', '--noProgress', + *self.b2_uri_args('my-bucket', 'b/b/test.csv') + ], ) expected_stdout = ''' @@ -2597,27 +2632,32 @@ def test_rm_exact_filename(self): c/test.csv c/test.tsv ''' - self._run_command(['ls', '--recursive', 'my-bucket'], expected_stdout) + self._run_command(['ls', '--recursive', *self.b2_uri_args('my-bucket')], expected_stdout) def test_rm_no_name_removes_everything(self): - self._run_command(['rm', '--recursive', '--noProgress', 'my-bucket']) - self._run_command(['ls', '--recursive', 'my-bucket'], '') + self._run_command(['rm', '--recursive', '--noProgress', *self.b2_uri_args('my-bucket')]) + self._run_command(['ls', '--recursive', *self.b2_uri_args('my-bucket')], '') def test_rm_with_wildcard_without_recursive(self): self._run_command( - ['rm', '--withWildcard', 'my-bucket'], + ['rm', '--withWildcard', *self.b2_uri_args('my-bucket')], expected_stderr='ERROR: with_wildcard requires recursive to be turned on as well\n', expected_status=1, ) def test_rm_queue_size_and_number_of_threads(self): - self._run_command(['rm', '--recursive', '--threads', '2', '--queueSize', '4', 'my-bucket']) - self._run_command(['ls', '--recursive', 'my-bucket'], '') + self._run_command( + [ + 'rm', '--recursive', '--threads', '2', '--queueSize', '4', + *self.b2_uri_args('my-bucket') + ] + ) + self._run_command(['ls', '--recursive', *self.b2_uri_args('my-bucket')], '') def test_rm_progress(self): expected_in_stdout = ' count: 4/4 ' self._run_command( - ['rm', '--recursive', '--withWildcard', 'my-bucket', '*.csv'], + ['rm', '--recursive', '--withWildcard', *self.b2_uri_args('my-bucket', '*.csv')], expected_part_of_stdout=expected_in_stdout, ) @@ -2627,7 +2667,7 @@ def test_rm_progress(self): b/test.txt c/test.tsv ''' - self._run_command(['ls', '--recursive', 'my-bucket'], expected_stdout) + self._run_command(['ls', '--recursive', *self.b2_uri_args('my-bucket')], expected_stdout) def _run_problematic_removal( self, @@ -2663,8 +2703,7 @@ def mocked_delete_file_version( '--queueSize', '1', *additional_parameters, - 'my-bucket', - '*', + *self.b2_uri_args('my-bucket', '*'), ], expected_status=1, expected_part_of_stdout=expected_in_stdout, @@ -2686,7 +2725,7 @@ def test_rm_skipping_over_errors(self): expected_stdout = ''' b/b1/test.csv ''' - self._run_command(['ls', '--recursive', 'my-bucket'], expected_stdout) + self._run_command(['ls', '--recursive', *self.b2_uri_args('my-bucket')], expected_stdout) class TestVersionConsoleTool(BaseConsoleToolTest): From d1fcfbf6c99905dda7926149b84e0487ba9e8fa0 Mon Sep 17 00:00:00 2001 From: Enam Mijbah Noor Date: Thu, 1 Feb 2024 17:35:55 +0600 Subject: [PATCH 3/4] Fix `ls`/`rm` docstrings across v3/v4 --- b2/_internal/_cli/b2args.py | 5 +- b2/_internal/b2v3/registry.py | 38 ++++++++- b2/_internal/b2v3/rm.py | 44 +++++++++- b2/_internal/console_tool.py | 153 +++++++++++++++++----------------- 4 files changed, 159 insertions(+), 81 deletions(-) diff --git a/b2/_internal/_cli/b2args.py b/b2/_internal/_cli/b2args.py index 9188c6d97..cb4379d56 100644 --- a/b2/_internal/_cli/b2args.py +++ b/b2/_internal/_cli/b2args.py @@ -44,13 +44,14 @@ def b2_uri(value: str) -> B2URI: def add_b2_uri_argument(parser: argparse.ArgumentParser, name="B2_URI"): """ - Add a B2 URI pointing to a file as an argument to the parser. + Add a B2 URI pointing to a bucket, optionally with a directory + or a pattern as an argument to the parser. """ parser.add_argument( name, type=B2_URI_ARG_TYPE, help="B2 URI pointing to a bucket, directory or a pattern, " - "e.g. b2://yourBucket, b2://yourBucket/file.txt, b2://yourBucket/folder, " + "e.g. b2://yourBucket, b2://yourBucket/file.txt, b2://yourBucket/folder/, " "b2://yourBucket/*.txt or b2id://fileId", ) diff --git a/b2/_internal/b2v3/registry.py b/b2/_internal/b2v3/registry.py index 19847711f..c603a1767 100644 --- a/b2/_internal/b2v3/registry.py +++ b/b2/_internal/b2v3/registry.py @@ -14,8 +14,42 @@ class Ls(B2URIBucketNFolderNameArgMixin, BaseLs): - __doc__ = BaseLs.__doc__ - # TODO: fix doc + """ + {BASELS} + + Examples + + .. note:: + + Note the use of quotes, to ensure that special + characters are not expanded by the shell. + + + List csv and tsv files (in any directory, in the whole bucket): + + .. code-block:: + + {NAME} ls --recursive --withWildcard bucketName "*.[ct]sv" + + + List all info.txt files from buckets bX, where X is any character: + + .. code-block:: + + {NAME} ls --recursive --withWildcard bucketName "b?/info.txt" + + + List all pdf files from buckets b0 to b9 (including sub-directories): + + .. code-block:: + + {NAME} ls --recursive --withWildcard bucketName "b[0-9]/*.pdf" + + + Requires capability: + + - **listFiles** + """ B2.register_subcommand(AuthorizeAccount) diff --git a/b2/_internal/b2v3/rm.py b/b2/_internal/b2v3/rm.py index fac93ca83..de001348e 100644 --- a/b2/_internal/b2v3/rm.py +++ b/b2/_internal/b2v3/rm.py @@ -15,5 +15,45 @@ # NOTE: We need to keep v3 Rm in separate file, because we need to import it in # unit tests without registering any commands. class Rm(B2URIBucketNFolderNameArgMixin, BaseRm): - __doc__ = BaseRm.__doc__ - # TODO: fix doc + """ + {BASERM} + + Examples. + + .. note:: + + Note the use of quotes, to ensure that special + characters are not expanded by the shell. + + + .. note:: + + Use with caution. Running examples presented below can cause data-loss. + + + Remove all csv and tsv files (in any directory, in the whole bucket): + + .. code-block:: + + {NAME} rm --recursive --withWildcard bucketName "*.[ct]sv" + + + Remove all info.txt files from buckets bX, where X is any character: + + .. code-block:: + + {NAME} rm --recursive --withWildcard bucketName "b?/info.txt" + + + Remove all pdf files from buckets b0 to b9 (including sub-directories): + + .. code-block:: + + {NAME} rm --recursive --withWildcard bucketName "b[0-9]/*.pdf" + + + Requires capability: + + - **listFiles** + - **deleteFiles** + """ diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 6cfd440ae..6ab224f44 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -2227,39 +2227,6 @@ class BaseLs(AbstractLsCommand, metaclass=ABCMeta): The ``--replication`` option adds replication status {ABSTRACTLSCOMMAND} - - Examples - - .. note:: - - Note the use of quotes, to ensure that special - characters are not expanded by the shell. - - - List csv and tsv files (in any directory, in the whole bucket): - - .. code-block:: - - {NAME} ls --recursive --withWildcard bucketName "*.[ct]sv" - - - List all info.txt files from buckets bX, where X is any character: - - .. code-block:: - - {NAME} ls --recursive --withWildcard bucketName "b?/info.txt" - - - List all pdf files from buckets b0 to b9 (including sub-directories): - - .. code-block:: - - {NAME} ls --recursive --withWildcard bucketName "b[0-9]/*.pdf" - - - Requires capability: - - - **listFiles** """ # order is file_id, action, date, time, size(, replication), name @@ -2328,7 +2295,42 @@ def format_ls_entry(self, file_version: FileVersion, replication: bool): class Ls(B2URIMixin, BaseLs): - __doc__ = BaseLs.__doc__ + """ + {BASELS} + + Examples + + .. note:: + + Note the use of quotes, to ensure that special + characters are not expanded by the shell. + + + List csv and tsv files (in any directory, in the whole bucket): + + .. code-block:: + + {NAME} ls --recursive --withWildcard "b2://bucketName/*.[ct]sv" + + + List all info.txt files from buckets bX, where X is any character: + + .. code-block:: + + {NAME} ls --recursive --withWildcard "b2://bucketName/b?/info.txt" + + + List all pdf files from buckets b0 to b9 (including sub-directories): + + .. code-block:: + + {NAME} ls --recursive --withWildcard "b2://bucketName/b[0-9]/*.pdf" + + + Requires capability: + + - **listFiles** + """ class BaseRm(ThreadsMixin, AbstractLsCommand, metaclass=ABCMeta): @@ -2368,45 +2370,6 @@ class BaseRm(ThreadsMixin, AbstractLsCommand, metaclass=ABCMeta): Command returns 0 if all files were removed successfully and a value different from 0 if any file was left. - - Examples. - - .. note:: - - Note the use of quotes, to ensure that special - characters are not expanded by the shell. - - - .. note:: - - Use with caution. Running examples presented below can cause data-loss. - - - Remove all csv and tsv files (in any directory, in the whole bucket): - - .. code-block:: - - {NAME} rm --recursive --withWildcard bucketName "*.[ct]sv" - - - Remove all info.txt files from buckets bX, where X is any character: - - .. code-block:: - - {NAME} rm --recursive --withWildcard bucketName "b?/info.txt" - - - Remove all pdf files from buckets b0 to b9 (including sub-directories): - - .. code-block:: - - {NAME} rm --recursive --withWildcard bucketName "b[0-9]/*.pdf" - - - Requires capability: - - - **listFiles** - - **deleteFiles** """ PROGRESS_REPORT_CLASS = ProgressReport @@ -2501,7 +2464,6 @@ def _setup_parser(cls, parser): ) parser.add_argument('--noProgress', action='store_true') parser.add_argument('--failFast', action='store_true') - # TODO: --bypassGovernance super()._setup_parser(parser) def _run(self, args): @@ -2540,7 +2502,48 @@ def _run(self, args): class Rm(B2URIMixin, BaseRm): - __doc__ = BaseRm.__doc__ + """ + {BASERM} + + Examples. + + .. note:: + + Note the use of quotes, to ensure that special + characters are not expanded by the shell. + + + .. note:: + + Use with caution. Running examples presented below can cause data-loss. + + + Remove all csv and tsv files (in any directory, in the whole bucket): + + .. code-block:: + + {NAME} rm --recursive --withWildcard "b2://bucketName/*.[ct]sv" + + + Remove all info.txt files from buckets bX, where X is any character: + + .. code-block:: + + {NAME} rm --recursive --withWildcard "b2://bucketName/b?/info.txt" + + + Remove all pdf files from buckets b0 to b9 (including sub-directories): + + .. code-block:: + + {NAME} rm --recursive --withWildcard "b2://bucketName/b[0-9]/*.pdf" + + + Requires capability: + + - **listFiles** + - **deleteFiles** + """ class GetUrlBase(Command): From c38ccc98722d20bc8ab2d538c3d59af335846dd9 Mon Sep 17 00:00:00 2001 From: Enam Mijbah Noor Date: Thu, 1 Feb 2024 17:44:59 +0600 Subject: [PATCH 4/4] Add changelog --- changelog.d/+ls-rm-b2-uri.changed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/+ls-rm-b2-uri.changed.md diff --git a/changelog.d/+ls-rm-b2-uri.changed.md b/changelog.d/+ls-rm-b2-uri.changed.md new file mode 100644 index 000000000..a8a50b5db --- /dev/null +++ b/changelog.d/+ls-rm-b2-uri.changed.md @@ -0,0 +1 @@ +Change `ls` and `rm` commands to use the `b2://` URI scheme in the pre-release version _b2v4.