From b67ab69b3c76ad47d2dae0b5d3e8cb7a8d7942ae Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 07:53:24 +0100 Subject: [PATCH 01/17] implement b2 file unhide command --- b2/_internal/b2v3/registry.py | 1 + b2/_internal/b2v4/registry.py | 1 + b2/_internal/console_tool.py | 59 ++++++++++++++++++++++++ test/integration/test_b2_command_line.py | 14 ++++++ test/unit/test_console_tool.py | 11 ++++- 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/b2/_internal/b2v3/registry.py b/b2/_internal/b2v3/registry.py index 400b7d33..96914802 100644 --- a/b2/_internal/b2v3/registry.py +++ b/b2/_internal/b2v3/registry.py @@ -131,6 +131,7 @@ class UploadFile(HyphenFilenameMixin, UploadFile): B2.register_subcommand(GetDownloadAuth) B2.register_subcommand(GetDownloadUrlWithAuth) B2.register_subcommand(HideFile) +B2.register_subcommand(UnhideFile) B2.register_subcommand(ListBuckets) B2.register_subcommand(ListKeys) B2.register_subcommand(ListParts) diff --git a/b2/_internal/b2v4/registry.py b/b2/_internal/b2v4/registry.py index 32afa3f5..92a0a386 100644 --- a/b2/_internal/b2v4/registry.py +++ b/b2/_internal/b2v4/registry.py @@ -32,6 +32,7 @@ B2.register_subcommand(GetDownloadAuth) B2.register_subcommand(GetDownloadUrlWithAuth) B2.register_subcommand(HideFile) +B2.register_subcommand(UnhideFile) B2.register_subcommand(ListBuckets) B2.register_subcommand(ListKeys) B2.register_subcommand(ListParts) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 15c9177f..66a7ad56 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -2174,6 +2174,54 @@ def _run(self, args): return 0 +class FileUnhideBase(Command): + """ + Delete the "hide marker" for a given file. + + Requires capability: + + - **listFiles** + - **deleteFiles** + + and optionally: + + - **bypassGovernance** + """ + + @classmethod + def _setup_parser(cls, parser): + add_bucket_name_argument(parser) + parser.add_argument('fileName').completer = file_name_completer + add_normalized_argument(parser, '--bypass-governance', action='store_true', default=False) + super()._setup_parser(parser) + + def _run(self, args): + bucket = self.api.get_bucket_by_name(args.bucketName) + # get the latest file version + file_versions = bucket.list_file_versions(file_name=args.fileName, fetch_count=1) + latest_file_version = next(file_versions, None) + if latest_file_version is None: + self._print_stderr(f'ERROR: File not present: "{args.fileName}"') + return 1 + + action = latest_file_version.action + if action == "upload": + self._print_stderr(f'ERROR: File not currently hidden: "{args.fileName}"') + return 1 + elif action == "delete": + self._print_stderr(f'ERROR: File deleted: "{args.fileName}"') + return 1 + elif action != "hide": + self._print_stderr(f'ERROR: Unknown file version action: {action}') + return 1 + + file_id_and_name = bucket.delete_file_version( + latest_file_version.id_, args.fileName, args.bypass_governance + ) + self._print_json(file_id_and_name) + return 0 + + class BucketListBase(Command): """ List all of the buckets in the current account. @@ -5078,6 +5126,12 @@ class FileHide(FileHideBase): COMMAND_NAME = 'hide' +@File.subcommands_registry.register +class FileUnhide(FileUnhideBase): + __doc__ = FileUnhideBase.__doc__ + COMMAND_NAME = 'unhide' + + @File.subcommands_registry.register class FileUpdate(FileUpdateBase): __doc__ = FileUpdateBase.__doc__ @@ -5150,6 +5204,11 @@ class HideFile(CmdReplacedByMixin, FileHideBase): replaced_by_cmd = (File, FileHide) +class UnhideFile(CmdReplacedByMixin, FileUnhideBase): + __doc__ = FileUnhideBase.__doc__ + replaced_by_cmd = (File, FileUnhide) + + class UpdateFileLegalHold(CmdReplacedByMixin, UpdateFileLegalHoldBase): __doc__ = UpdateFileLegalHoldBase.__doc__ replaced_by_cmd = (File, FileUpdate) diff --git a/test/integration/test_b2_command_line.py b/test/integration/test_b2_command_line.py index 242d2485..22f458cb 100755 --- a/test/integration/test_b2_command_line.py +++ b/test/integration/test_b2_command_line.py @@ -357,6 +357,20 @@ def test_basic(b2_tool, bucket_name, sample_file, tmp_path, b2_uri_args, apiver_ ) should_equal(['a', 'b/1', 'b/2', 'd'], [f['fileName'] for f in list_of_files]) + b2_tool.should_succeed(['file', 'unhide', bucket_name, 'c']) + + list_of_files = b2_tool.should_succeed_json( + ['ls', '--json', '--recursive', *b2_uri_args(bucket_name)] + ) + should_equal(['a', 'b/1', 'b/2', 'c', 'd'], [f['fileName'] for f in list_of_files]) + + b2_tool.should_succeed(['file', 'hide', bucket_name, 'c']) + + 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', *b2_uri_args(bucket_name)] ) diff --git a/test/unit/test_console_tool.py b/test/unit/test_console_tool.py index edec02f1..fabf131f 100644 --- a/test/unit/test_console_tool.py +++ b/test/unit/test_console_tool.py @@ -2082,6 +2082,9 @@ def test_get_bucket_with_hidden(self): console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', 'hidden3']) console_tool.run_command(['b2', 'hide-file', 'my-bucket', 'hidden4']) + # unhide one file + console_tool.run_command(['b2', 'file', 'unhide', 'my-bucket', 'hidden2']) + # Now check the output of `bucket get` against the canon. expected_json = { "accountId": self.account_id, @@ -2093,7 +2096,7 @@ def test_get_bucket_with_hidden(self): "defaultServerSideEncryption": { "mode": "none" }, - "fileCount": 10, + "fileCount": 9, "lifecycleRules": [], "options": [], "revision": 1, @@ -2146,6 +2149,10 @@ def test_get_bucket_complex(self): console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/2/hidden3']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/2/hidden3']) + # Unhide a file + console_tool.run_command(['b2', 'file', 'unhide', 'my-bucket', '1/hidden2']) + console_tool.run_command(['b2', 'file', 'unhide', 'my-bucket', '1/hidden2']) + # Now check the output of `bucket get` against the canon. expected_json = { "accountId": self.account_id, @@ -2157,7 +2164,7 @@ def test_get_bucket_complex(self): "defaultServerSideEncryption": { "mode": "none" }, - "fileCount": 29, + "fileCount": 28, "lifecycleRules": [], "options": [], "revision": 1, From 4c86fc5f4477f17ccbe758aa2c6e5900af161afd Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:39:45 +0100 Subject: [PATCH 02/17] remove UnhideFile class --- b2/_internal/b2v3/registry.py | 2 +- b2/_internal/b2v4/registry.py | 2 +- b2/_internal/console_tool.py | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/b2/_internal/b2v3/registry.py b/b2/_internal/b2v3/registry.py index 96914802..e94d3363 100644 --- a/b2/_internal/b2v3/registry.py +++ b/b2/_internal/b2v3/registry.py @@ -131,7 +131,7 @@ class UploadFile(HyphenFilenameMixin, UploadFile): B2.register_subcommand(GetDownloadAuth) B2.register_subcommand(GetDownloadUrlWithAuth) B2.register_subcommand(HideFile) -B2.register_subcommand(UnhideFile) +B2.register_subcommand(FileUnhide) B2.register_subcommand(ListBuckets) B2.register_subcommand(ListKeys) B2.register_subcommand(ListParts) diff --git a/b2/_internal/b2v4/registry.py b/b2/_internal/b2v4/registry.py index 92a0a386..9593d6d8 100644 --- a/b2/_internal/b2v4/registry.py +++ b/b2/_internal/b2v4/registry.py @@ -32,7 +32,7 @@ B2.register_subcommand(GetDownloadAuth) B2.register_subcommand(GetDownloadUrlWithAuth) B2.register_subcommand(HideFile) -B2.register_subcommand(UnhideFile) +B2.register_subcommand(FileUnhide) B2.register_subcommand(ListBuckets) B2.register_subcommand(ListKeys) B2.register_subcommand(ListParts) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 66a7ad56..970da577 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -5204,11 +5204,6 @@ class HideFile(CmdReplacedByMixin, FileHideBase): replaced_by_cmd = (File, FileHide) -class UnhideFile(CmdReplacedByMixin, FileUnhideBase): - __doc__ = FileUnhideBase.__doc__ - replaced_by_cmd = (File, FileUnhide) - - class UpdateFileLegalHold(CmdReplacedByMixin, UpdateFileLegalHoldBase): __doc__ = UpdateFileLegalHoldBase.__doc__ replaced_by_cmd = (File, FileUpdate) From 6993f65ebd5fca5ab32d67552e417a95d36a7529 Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:53:37 +0100 Subject: [PATCH 03/17] add missing changelog entry --- changelog.d/+b2_file_unhide.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/+b2_file_unhide.added.md diff --git a/changelog.d/+b2_file_unhide.added.md b/changelog.d/+b2_file_unhide.added.md new file mode 100644 index 00000000..ae9409e8 --- /dev/null +++ b/changelog.d/+b2_file_unhide.added.md @@ -0,0 +1 @@ +Implement b2 file unhide command. \ No newline at end of file From 45be62df1667a3ab5bf907acd0365997ffbc0875 Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:14:24 +0100 Subject: [PATCH 04/17] use B2URI for file unhide --- b2/_internal/console_tool.py | 18 +++++++++--------- test/integration/test_b2_command_line.py | 2 +- test/unit/test_console_tool.py | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 970da577..34c7dac9 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -2190,33 +2190,33 @@ class FileUnhideBase(Command): @classmethod def _setup_parser(cls, parser): - add_bucket_name_argument(parser) - parser.add_argument('fileName').completer = file_name_completer add_normalized_argument(parser, '--bypass-governance', action='store_true', default=False) super()._setup_parser(parser) def _run(self, args): - bucket = self.api.get_bucket_by_name(args.bucketName) + b2_uri = self.get_b2_uri_from_arg(args) + file_name = b2_uri.path + bucket = self.api.get_bucket_by_name(b2_uri.bucket_name) # get the latest file version - file_versions = bucket.list_file_versions(file_name=args.fileName, fetch_count=1) + file_versions = bucket.list_file_versions(file_name=file_name, fetch_count=1) latest_file_version = next(file_versions, None) if latest_file_version is None: - self._print_stderr(f'ERROR: File not present: "{args.fileName}"') + self._print_stderr(f'ERROR: File not present: "{file_name}"') return 1 action = latest_file_version.action if action == "upload": - self._print_stderr(f'ERROR: File not currently hidden: "{args.fileName}"') + self._print_stderr(f'ERROR: File not currently hidden: "{file_name}"') return 1 elif action == "delete": - self._print_stderr(f'ERROR: File deleted: "{args.fileName}"') + self._print_stderr(f'ERROR: File deleted: "{file_name}"') return 1 elif action != "hide": self._print_stderr(f'ERROR: Unknown file version action: {action}') return 1 file_id_and_name = bucket.delete_file_version( - latest_file_version.id_, args.fileName, args.bypass_governance + latest_file_version.id_, file_name, args.bypass_governance ) self._print_json(file_id_and_name) return 0 @@ -5127,7 +5127,7 @@ class FileHide(FileHideBase): @File.subcommands_registry.register -class FileUnhide(FileUnhideBase): +class FileUnhide(B2URIFileArgMixin, FileUnhideBase): __doc__ = FileUnhideBase.__doc__ COMMAND_NAME = 'unhide' diff --git a/test/integration/test_b2_command_line.py b/test/integration/test_b2_command_line.py index 22f458cb..f799fdfe 100755 --- a/test/integration/test_b2_command_line.py +++ b/test/integration/test_b2_command_line.py @@ -357,7 +357,7 @@ def test_basic(b2_tool, bucket_name, sample_file, tmp_path, b2_uri_args, apiver_ ) should_equal(['a', 'b/1', 'b/2', 'd'], [f['fileName'] for f in list_of_files]) - b2_tool.should_succeed(['file', 'unhide', bucket_name, 'c']) + b2_tool.should_succeed(['file', 'unhide', f'b2://{bucket_name}/c']) list_of_files = b2_tool.should_succeed_json( ['ls', '--json', '--recursive', *b2_uri_args(bucket_name)] diff --git a/test/unit/test_console_tool.py b/test/unit/test_console_tool.py index fabf131f..2a46dcd5 100644 --- a/test/unit/test_console_tool.py +++ b/test/unit/test_console_tool.py @@ -2083,7 +2083,7 @@ def test_get_bucket_with_hidden(self): console_tool.run_command(['b2', 'hide-file', 'my-bucket', 'hidden4']) # unhide one file - console_tool.run_command(['b2', 'file', 'unhide', 'my-bucket', 'hidden2']) + console_tool.run_command(['b2', 'file', 'unhide', f'b2://my-bucket/hidden2']) # Now check the output of `bucket get` against the canon. expected_json = { @@ -2150,8 +2150,8 @@ def test_get_bucket_complex(self): console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/2/hidden3']) # Unhide a file - console_tool.run_command(['b2', 'file', 'unhide', 'my-bucket', '1/hidden2']) - console_tool.run_command(['b2', 'file', 'unhide', 'my-bucket', '1/hidden2']) + console_tool.run_command(['b2', 'file', 'unhide', f'b2://my-bucket/1/hidden2']) + console_tool.run_command(['b2', 'file', 'unhide', f'b2://my-bucket/1/hidden2']) # Now check the output of `bucket get` against the canon. expected_json = { From 5b7815917d50b7a3ab7829f2b93b042faf3053a3 Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:15:00 +0100 Subject: [PATCH 05/17] format --- test/unit/test_console_tool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/test_console_tool.py b/test/unit/test_console_tool.py index 2a46dcd5..621b2461 100644 --- a/test/unit/test_console_tool.py +++ b/test/unit/test_console_tool.py @@ -2083,7 +2083,7 @@ def test_get_bucket_with_hidden(self): console_tool.run_command(['b2', 'hide-file', 'my-bucket', 'hidden4']) # unhide one file - console_tool.run_command(['b2', 'file', 'unhide', f'b2://my-bucket/hidden2']) + console_tool.run_command(['b2', 'file', 'unhide', 'b2://my-bucket/hidden2']) # Now check the output of `bucket get` against the canon. expected_json = { @@ -2150,8 +2150,8 @@ def test_get_bucket_complex(self): console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/2/hidden3']) # Unhide a file - console_tool.run_command(['b2', 'file', 'unhide', f'b2://my-bucket/1/hidden2']) - console_tool.run_command(['b2', 'file', 'unhide', f'b2://my-bucket/1/hidden2']) + console_tool.run_command(['b2', 'file', 'unhide', 'b2://my-bucket/1/hidden2']) + console_tool.run_command(['b2', 'file', 'unhide', 'b2://my-bucket/1/hidden2']) # Now check the output of `bucket get` against the canon. expected_json = { From 92ae1fd2888b28925c74af9aeae2a65824f2f1d8 Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:30:47 +0100 Subject: [PATCH 06/17] remove unnecessary FileUnhide registrations --- b2/_internal/b2v3/registry.py | 1 - b2/_internal/b2v4/registry.py | 1 - 2 files changed, 2 deletions(-) diff --git a/b2/_internal/b2v3/registry.py b/b2/_internal/b2v3/registry.py index e94d3363..400b7d33 100644 --- a/b2/_internal/b2v3/registry.py +++ b/b2/_internal/b2v3/registry.py @@ -131,7 +131,6 @@ class UploadFile(HyphenFilenameMixin, UploadFile): B2.register_subcommand(GetDownloadAuth) B2.register_subcommand(GetDownloadUrlWithAuth) B2.register_subcommand(HideFile) -B2.register_subcommand(FileUnhide) B2.register_subcommand(ListBuckets) B2.register_subcommand(ListKeys) B2.register_subcommand(ListParts) diff --git a/b2/_internal/b2v4/registry.py b/b2/_internal/b2v4/registry.py index 9593d6d8..32afa3f5 100644 --- a/b2/_internal/b2v4/registry.py +++ b/b2/_internal/b2v4/registry.py @@ -32,7 +32,6 @@ B2.register_subcommand(GetDownloadAuth) B2.register_subcommand(GetDownloadUrlWithAuth) B2.register_subcommand(HideFile) -B2.register_subcommand(FileUnhide) B2.register_subcommand(ListBuckets) B2.register_subcommand(ListKeys) B2.register_subcommand(ListParts) From 6f3b175d860578aa5cfa02cd3b015e693690e6ad Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:40:07 +0100 Subject: [PATCH 07/17] use new b2 sdk method bucket.unhide_file --- b2/_internal/console_tool.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 34c7dac9..21ccbc74 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -2195,29 +2195,8 @@ def _setup_parser(cls, parser): def _run(self, args): b2_uri = self.get_b2_uri_from_arg(args) - file_name = b2_uri.path bucket = self.api.get_bucket_by_name(b2_uri.bucket_name) - # get the latest file version - file_versions = bucket.list_file_versions(file_name=file_name, fetch_count=1) - latest_file_version = next(file_versions, None) - if latest_file_version is None: - self._print_stderr(f'ERROR: File not present: "{file_name}"') - return 1 - - action = latest_file_version.action - if action == "upload": - self._print_stderr(f'ERROR: File not currently hidden: "{file_name}"') - return 1 - elif action == "delete": - self._print_stderr(f'ERROR: File deleted: "{file_name}"') - return 1 - elif action != "hide": - self._print_stderr(f'ERROR: Unknown file version action: {action}') - return 1 - - file_id_and_name = bucket.delete_file_version( - latest_file_version.id_, file_name, args.bypass_governance - ) + file_id_and_name = bucket.unhide_file(b2_uri.path, args.bypass_governance) self._print_json(file_id_and_name) return 0 From b91012d196a455ef3de9892d40ea45cca8a2daa6 Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:28:22 +0100 Subject: [PATCH 08/17] b2 file hide parameters changes --- b2/_internal/_cli/b2args.py | 24 +++++++++++++ b2/_internal/console_tool.py | 42 +++++++++++++++++++++-- changelog.d/+b2_file_hide.changed.md | 2 ++ test/unit/_cli/test_autocomplete_cache.py | 4 +-- test/unit/test_console_tool.py | 4 +-- 5 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 changelog.d/+b2_file_hide.changed.md diff --git a/b2/_internal/_cli/b2args.py b/b2/_internal/_cli/b2args.py index ba744f6f..86cfe6fe 100644 --- a/b2/_internal/_cli/b2args.py +++ b/b2/_internal/_cli/b2args.py @@ -78,6 +78,14 @@ def parse_bucket_name(value: str, allow_all_buckets: bool = False) -> str: return str(value) +def b2id_or_file_like_b2_uri_or_bucket_name(value: str) -> Union[B2URIBase, str]: + try: + bucket_name = parse_bucket_name(value) + return bucket_name + except ValueError: + return b2id_or_file_like_b2_uri(value) + + B2ID_URI_ARG_TYPE = wrap_with_argument_type_error(b2id_uri) B2_BUCKET_URI_ARG_TYPE = wrap_with_argument_type_error(b2_bucket_uri) B2ID_OR_B2_URI_ARG_TYPE = wrap_with_argument_type_error(parse_b2_uri) @@ -86,6 +94,9 @@ def parse_bucket_name(value: str, allow_all_buckets: bool = False) -> str: functools.partial(parse_b2_uri, allow_all_buckets=True) ) B2ID_OR_FILE_LIKE_B2_URI_ARG_TYPE = wrap_with_argument_type_error(b2id_or_file_like_b2_uri) +B2ID_OR_FILE_LIKE_B2_URI_OR_BUCKET_NAME_ARG_TYPE = wrap_with_argument_type_error( + b2id_or_file_like_b2_uri_or_bucket_name +) def add_bucket_name_argument( @@ -194,5 +205,18 @@ def add_b2id_or_file_like_b2_uri_argument(parser: argparse.ArgumentParser, name= ).completer = b2uri_file_completer +def add_b2id_or_file_like_b2_uri_or_bucket_name_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=B2ID_OR_FILE_LIKE_B2_URI_OR_BUCKET_NAME_ARG_TYPE, + help="B2 URI pointing to a file, e.g. b2://yourBucket/file.txt or b2id://fileId", + ).completer = b2uri_file_completer + + def get_keyid_and_key_from_env_vars() -> Tuple[Optional[str], Optional[str]]: return environ.get(B2_APPLICATION_KEY_ID_ENV_VAR), environ.get(B2_APPLICATION_KEY_ENV_VAR) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index 21ccbc74..d3511b38 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -140,6 +140,7 @@ add_b2id_or_b2_bucket_uri_argument, add_b2id_or_b2_uri_argument, add_b2id_or_file_like_b2_uri_argument, + add_b2id_or_file_like_b2_uri_or_bucket_name_argument, add_b2id_uri_argument, add_bucket_name_argument, get_keyid_and_key_from_env_vars, @@ -712,6 +713,20 @@ def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URIBase: return args.B2_URI +class B2URIFileOrBucketNameArgMixin: + @classmethod + def _setup_parser(cls, parser): + add_b2id_or_file_like_b2_uri_or_bucket_name_argument(parser) + super()._setup_parser(parser) + + def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URIBase | str: + if isinstance(args.B2_URI, B2URI): + return args.B2_URI + + bucket_name = args.B2_URI + return bucket_name + + class B2URIFileIDArgMixin: @classmethod def _setup_parser(cls, parser): @@ -2161,6 +2176,29 @@ class FileHideBase(Command): - **writeFiles** """ + @classmethod + def _setup_parser(cls, parser): + parser.add_argument( + 'fileName', nargs='?', help=argparse.SUPPRESS + ).completer = file_name_completer + super()._setup_parser(parser) + + def _run(self, args): + b2_uri = self.get_b2_uri_from_arg(args) + if isinstance(b2_uri, B2URI): + bucket_name = b2_uri.bucket_name + file_name = b2_uri.path + else: + bucket_name = b2_uri + file_name = args.fileName + + bucket = self.api.get_bucket_by_name(bucket_name) + file_info = bucket.hide_file(file_name) + self._print_json(file_info) + return 0 + + +class HideFileBase(Command): @classmethod def _setup_parser(cls, parser): add_bucket_name_argument(parser) @@ -5100,7 +5138,7 @@ class FileCopyById(FileCopyByIdBase): @File.subcommands_registry.register -class FileHide(FileHideBase): +class FileHide(B2URIFileOrBucketNameArgMixin, FileHideBase): __doc__ = FileHideBase.__doc__ COMMAND_NAME = 'hide' @@ -5178,7 +5216,7 @@ class CopyFileById(CmdReplacedByMixin, FileCopyByIdBase): replaced_by_cmd = (File, FileCopyById) -class HideFile(CmdReplacedByMixin, FileHideBase): +class HideFile(CmdReplacedByMixin, HideFileBase): __doc__ = FileHideBase.__doc__ replaced_by_cmd = (File, FileHide) diff --git a/changelog.d/+b2_file_hide.changed.md b/changelog.d/+b2_file_hide.changed.md new file mode 100644 index 00000000..2dcf3810 --- /dev/null +++ b/changelog.d/+b2_file_hide.changed.md @@ -0,0 +1,2 @@ +`b2 file hide` supports both the new `b2_uri` and the old `bucket_name file_name` parameters syntax, but the --help text only mentions the new syntax. +b2v3 hide-file remains unchanged and only supports the `bucket_name file_name` syntax. \ No newline at end of file diff --git a/test/unit/_cli/test_autocomplete_cache.py b/test/unit/_cli/test_autocomplete_cache.py index 383e224b..41e0c736 100644 --- a/test/unit/_cli/test_autocomplete_cache.py +++ b/test/unit/_cli/test_autocomplete_cache.py @@ -176,7 +176,7 @@ def test_complete_with_escaped_control_characters( store=autocomplete_cache.HomeCachePickleStore(tmp_path), ) - with autocomplete_runner(f'b2 file hide {bucket} '): + with autocomplete_runner(f'b2 hide-file {bucket} '): exit, argcomplete_output = argcomplete_result() assert exit == 0 assert escaped_cc_file_name in argcomplete_output @@ -200,7 +200,7 @@ def test_complete_with_file_suggestions( tracker=autocomplete_cache.VersionTracker(), store=autocomplete_cache.HomeCachePickleStore(tmp_path), ) - with autocomplete_runner(f'b2 file hide {bucket} '): + with autocomplete_runner(f'b2 hide-file {bucket} '): exit, argcomplete_output = argcomplete_result() assert exit == 0 assert file_name in argcomplete_output diff --git a/test/unit/test_console_tool.py b/test/unit/test_console_tool.py index 621b2461..11be2844 100644 --- a/test/unit/test_console_tool.py +++ b/test/unit/test_console_tool.py @@ -2077,7 +2077,7 @@ def test_get_bucket_with_hidden(self): # something has failed if the output of 'bucket get' does not match the canon. stdout, stderr = self._get_stdouterr() console_tool = self.console_tool_class(stdout, stderr) - console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', 'hidden1']) + console_tool.run_command(['b2', 'file', 'hide', 'b2://my-bucket/hidden1']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', 'hidden2']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', 'hidden3']) console_tool.run_command(['b2', 'hide-file', 'my-bucket', 'hidden4']) @@ -2141,7 +2141,7 @@ def test_get_bucket_complex(self): # something has failed if the output of 'bucket get' does not match the canon. stdout, stderr = self._get_stdouterr() console_tool = self.console_tool_class(stdout, stderr) - console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/hidden1']) + console_tool.run_command(['b2', 'file', 'hide', 'b2://my-bucket/1/hidden1']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/hidden1']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/hidden2']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/2/hidden3']) From f7347d2dc3c0c7c5ca72d0528e834ddb662c29c5 Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:30:57 +0100 Subject: [PATCH 09/17] change test_get_bucket_complex file hide syntax --- test/unit/test_console_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_console_tool.py b/test/unit/test_console_tool.py index 11be2844..9c41322b 100644 --- a/test/unit/test_console_tool.py +++ b/test/unit/test_console_tool.py @@ -2143,7 +2143,7 @@ def test_get_bucket_complex(self): console_tool = self.console_tool_class(stdout, stderr) console_tool.run_command(['b2', 'file', 'hide', 'b2://my-bucket/1/hidden1']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/hidden1']) - console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/hidden2']) + console_tool.run_command(['b2', 'file', 'hide', 'b2://my-bucket/1/hidden2']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/2/hidden3']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/2/hidden3']) console_tool.run_command(['b2', 'file', 'hide', 'my-bucket', '1/2/hidden3']) From 0e5581a0c64c80636dc532b026133bc51624344a Mon Sep 17 00:00:00 2001 From: adil-haritah-reef Date: Thu, 25 Jul 2024 15:59:33 +0100 Subject: [PATCH 10/17] Update changelog.d/+b2_file_hide.changed.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maciej Urbański <122983254+mjurbanski-reef@users.noreply.github.com> --- changelog.d/+b2_file_hide.changed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/+b2_file_hide.changed.md b/changelog.d/+b2_file_hide.changed.md index 2dcf3810..42b77996 100644 --- a/changelog.d/+b2_file_hide.changed.md +++ b/changelog.d/+b2_file_hide.changed.md @@ -1,2 +1,2 @@ -`b2 file hide` supports both the new `b2_uri` and the old `bucket_name file_name` parameters syntax, but the --help text only mentions the new syntax. +`b2 file hide` supports both the new `b2_uri` and deprecated `bucket_name file_name` parameters syntax. b2v3 hide-file remains unchanged and only supports the `bucket_name file_name` syntax. \ No newline at end of file From 088ce05670402bd4050a6a9c896965bd38dc8850 Mon Sep 17 00:00:00 2001 From: adil-haritah-reef Date: Thu, 25 Jul 2024 15:59:45 +0100 Subject: [PATCH 11/17] Update changelog.d/+b2_file_hide.changed.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maciej Urbański <122983254+mjurbanski-reef@users.noreply.github.com> --- changelog.d/+b2_file_hide.changed.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changelog.d/+b2_file_hide.changed.md b/changelog.d/+b2_file_hide.changed.md index 42b77996..af08becd 100644 --- a/changelog.d/+b2_file_hide.changed.md +++ b/changelog.d/+b2_file_hide.changed.md @@ -1,2 +1 @@ -`b2 file hide` supports both the new `b2_uri` and deprecated `bucket_name file_name` parameters syntax. -b2v3 hide-file remains unchanged and only supports the `bucket_name file_name` syntax. \ No newline at end of file +`b2 file hide` supports both the new `b2_uri` and deprecated `bucket_name file_name` parameters syntax. \ No newline at end of file From 4212e423e1c7fe0bebc76a748c40f418267d8b8e Mon Sep 17 00:00:00 2001 From: adil-haritah-reef Date: Thu, 25 Jul 2024 16:00:29 +0100 Subject: [PATCH 12/17] Update changelog.d/+b2_file_unhide.added.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maciej Urbański <122983254+mjurbanski-reef@users.noreply.github.com> --- changelog.d/+b2_file_unhide.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/+b2_file_unhide.added.md b/changelog.d/+b2_file_unhide.added.md index ae9409e8..3e4c7612 100644 --- a/changelog.d/+b2_file_unhide.added.md +++ b/changelog.d/+b2_file_unhide.added.md @@ -1 +1 @@ -Implement b2 file unhide command. \ No newline at end of file +Add `b2 file unhide` command. \ No newline at end of file From 47efa9bfb01b32036161a6dafa9fd545c3d561be Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:20:00 +0100 Subject: [PATCH 13/17] change B2URIFileOrBucketNameFileNameArgMixin --- b2/_internal/console_tool.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index d3511b38..662443d2 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -713,10 +713,11 @@ def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URIBase: return args.B2_URI -class B2URIFileOrBucketNameArgMixin: +class B2URIFileOrBucketNameFileNameArgMixin: @classmethod def _setup_parser(cls, parser): add_b2id_or_file_like_b2_uri_or_bucket_name_argument(parser) + parser.add_argument('fileName', nargs='?', help=argparse.SUPPRESS) super()._setup_parser(parser) def get_b2_uri_from_arg(self, args: argparse.Namespace) -> B2URIBase | str: @@ -2176,13 +2177,6 @@ class FileHideBase(Command): - **writeFiles** """ - @classmethod - def _setup_parser(cls, parser): - parser.add_argument( - 'fileName', nargs='?', help=argparse.SUPPRESS - ).completer = file_name_completer - super()._setup_parser(parser) - def _run(self, args): b2_uri = self.get_b2_uri_from_arg(args) if isinstance(b2_uri, B2URI): @@ -5138,7 +5132,7 @@ class FileCopyById(FileCopyByIdBase): @File.subcommands_registry.register -class FileHide(B2URIFileOrBucketNameArgMixin, FileHideBase): +class FileHide(B2URIFileOrBucketNameFileNameArgMixin, FileHideBase): __doc__ = FileHideBase.__doc__ COMMAND_NAME = 'hide' From 2931ad9b2e566a0339577027c7bcbc5594ba01a9 Mon Sep 17 00:00:00 2001 From: didil <1284255+didil@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:33:37 +0100 Subject: [PATCH 14/17] add file hide legacy syntax format --- test/unit/test_console_tool.py | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/unit/test_console_tool.py b/test/unit/test_console_tool.py index 9c41322b..5335cc8f 100644 --- a/test/unit/test_console_tool.py +++ b/test/unit/test_console_tool.py @@ -1025,6 +1025,72 @@ def test_rm_fileid_v4(self): # Delete one file version self._run_command(['rm', 'b2id://9999']) + def test_hide_file_legacy_syntax(self): + self._authorize_account() + self._run_command(['bucket', 'create', 'my-bucket', 'allPublic'], 'bucket_0\n', '', 0) + + with TempDir() as temp_dir: + local_file1 = self._make_local_file(temp_dir, 'file1.txt') + # For this test, use a mod time without millis. My mac truncates + # millis and just leaves seconds. + mod_time = 1500111222 + os.utime(local_file1, (mod_time, mod_time)) + self.assertEqual(1500111222, os.path.getmtime(local_file1)) + + # Upload a file + self._run_command( + [ + 'file', 'upload', '--no-progress', 'my-bucket', local_file1, 'file1.txt', + '--cache-control=private, max-age=3600' + ], + remove_version=True, + ) + + # Get file info + expected_json = { + "accountId": self.account_id, + "action": "upload", + "bucketId": "bucket_0", + "size": 11, + "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", + "contentType": "b2/x-auto", + "fileId": "9999", + "fileInfo": + { + "src_last_modified_millis": "1500111222000", + "b2-cache-control": "private, max-age=3600" + }, + "fileName": "file1.txt", + "serverSideEncryption": { + "mode": "none" + }, + "uploadTimestamp": 5000 + } + + self._run_command( + ['file', 'info', 'b2id://9999'], + expected_json_in_stdout=expected_json, + ) + + # Hide the file + expected_json = { + "action": "hide", + "contentSha1": "none", + "fileId": "9998", + "fileInfo": {}, + "fileName": "file1.txt", + "serverSideEncryption": { + "mode": "none" + }, + "size": 0, + "uploadTimestamp": 5001 + } + + self._run_command( + ['file', 'hide', 'my-bucket', 'file1.txt'], + expected_json_in_stdout=expected_json, + ) + def test_files(self): self._authorize_account() From 7e525d0eed7f9a1233c314dbc72dcee2e1b68842 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Tue, 30 Jul 2024 23:00:58 +0200 Subject: [PATCH 15/17] limit Github Actions jobs to 90 minutes --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63844271..6b85135b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ env: jobs: lint: + timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -72,6 +73,7 @@ jobs: if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} # TODO: skip this whole job instead run: nox -vs cleanup_buckets test: + timeout-minutes: 90 needs: cleanup_buckets env: B2_TEST_APPLICATION_KEY: ${{ secrets.B2_TEST_APPLICATION_KEY }} @@ -131,6 +133,7 @@ jobs: if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' && contains(fromJSON('["3.7", "pypy3.10", "3.12"]'), matrix.python-version) }} run: nox -vs integration -p ${{ matrix.python-version }} -- -m "require_secrets" --cleanup test-docker: + timeout-minutes: 90 needs: cleanup_buckets env: B2_TEST_APPLICATION_KEY: ${{ secrets.B2_TEST_APPLICATION_KEY }} @@ -163,6 +166,7 @@ jobs: if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} run: nox -vs docker_test -- backblazeit/b2:test test-linux-bundle: + timeout-minutes: 90 needs: cleanup_buckets env: B2_TEST_APPLICATION_KEY: ${{ secrets.B2_TEST_APPLICATION_KEY }} @@ -201,6 +205,7 @@ jobs: if-no-files-found: warn retention-days: 7 test-windows-bundle: + timeout-minutes: 90 needs: cleanup_buckets env: B2_TEST_APPLICATION_KEY: ${{ secrets.B2_TEST_APPLICATION_KEY }} @@ -240,6 +245,7 @@ jobs: if-no-files-found: warn retention-days: 7 doc: + timeout-minutes: 30 needs: build runs-on: ubuntu-latest steps: From 4821d23a8515fc66fb5080087a213e6785176d3f Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Tue, 30 Jul 2024 23:06:25 +0200 Subject: [PATCH 16/17] update b2sdk to fix TruncatedOutput errors (fixes #554) --- changelog.d/554.fixed.md | 1 + pdm.lock | 10 +++++----- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 changelog.d/554.fixed.md diff --git a/changelog.d/554.fixed.md b/changelog.d/554.fixed.md new file mode 100644 index 00000000..c6967573 --- /dev/null +++ b/changelog.d/554.fixed.md @@ -0,0 +1 @@ +Update `b2sdk` to 2.5.0, to fix `TruncatedOutput` download errors when network is congested (e.g., due use of high downloader thread count). diff --git a/pdm.lock b/pdm.lock index 5213fd7c..29913ff8 100644 --- a/pdm.lock +++ b/pdm.lock @@ -4,8 +4,8 @@ [metadata] groups = ["default", "bundle", "doc", "format", "full", "license", "lint", "release", "test"] strategy = ["cross_platform", "inherit_metadata"] -lock_version = "4.4.1" -content_hash = "sha256:5d1f8819c3c22b05f569213b507fa9c43bc13b67e6b2006478541b32945eefc9" +lock_version = "4.4.2" +content_hash = "sha256:c618124f32be53fa248fce6237836afabf3bfe54ac71474f16d509276070b541" [[package]] name = "alabaster" @@ -100,7 +100,7 @@ files = [ [[package]] name = "b2sdk" -version = "2.4.1" +version = "2.5.0" requires_python = ">=3.7" summary = "Backblaze B2 SDK" groups = ["default"] @@ -112,8 +112,8 @@ dependencies = [ "typing-extensions>=4.7.1; python_version < \"3.12\"", ] files = [ - {file = "b2sdk-2.4.1-py3-none-any.whl", hash = "sha256:7ce6301cb119fdbf9826d4a11c7ea783194a2ffbfaf2fb1c437c3643e22bfe36"}, - {file = "b2sdk-2.4.1.tar.gz", hash = "sha256:257510b7c661dc6d513588560b436f2424fcbb4213834fa3ca52c7a5fd8d44b7"}, + {file = "b2sdk-2.5.0-py3-none-any.whl", hash = "sha256:38bf80a6cd2bff5b104f7fd10023cf9e338c6f9d41da6108db5ad68f76f5abaa"}, + {file = "b2sdk-2.5.0.tar.gz", hash = "sha256:d7c20125e64508a730e56307d75284790079cdb88e63851fff820a09b24fb1d9"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index c6c6c18b..c6136c65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ dependencies = [ "argcomplete>=2,<4", "arrow>=1.0.2,<2.0.0", - "b2sdk>=2.4.1,<3", + "b2sdk>=2.5.0,<3", "docutils>=0.18.1", "idna~=3.4; platform_system == 'Java'", "importlib-metadata>=3.3; python_version < '3.8'", From 28ceb191bbc20df5d1131c9cba3593751e3b3af4 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Tue, 30 Jul 2024 23:11:15 +0200 Subject: [PATCH 17/17] rephrase `b2 file hide` args change changelog --- changelog.d/+b2_file_hide.added.md | 1 + changelog.d/+b2_file_hide.changed.md | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/+b2_file_hide.added.md delete mode 100644 changelog.d/+b2_file_hide.changed.md diff --git a/changelog.d/+b2_file_hide.added.md b/changelog.d/+b2_file_hide.added.md new file mode 100644 index 00000000..df657d7f --- /dev/null +++ b/changelog.d/+b2_file_hide.added.md @@ -0,0 +1 @@ +Support new `b2_uri` and deprecated `bucket_name file_name` arguments in `b2 file hide`. diff --git a/changelog.d/+b2_file_hide.changed.md b/changelog.d/+b2_file_hide.changed.md deleted file mode 100644 index af08becd..00000000 --- a/changelog.d/+b2_file_hide.changed.md +++ /dev/null @@ -1 +0,0 @@ -`b2 file hide` supports both the new `b2_uri` and deprecated `bucket_name file_name` parameters syntax. \ No newline at end of file