diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5c6c301..f34b7bba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Deprecated +* Support of `-` as a valid filename in `download-file-by-name` or `download-file-by-id` command. In future `-` will be an alias for standard output. + ### Fixed * `--quiet` now will implicitly set `--noProgress` option as well +* Fix failing open non-seekable file ## [3.11.0] - 2023-10-04 diff --git a/b2/console_tool.py b/b2/console_tool.py index 2c31de244..a785fba4a 100644 --- a/b2/console_tool.py +++ b/b2/console_tool.py @@ -1386,10 +1386,31 @@ def _print_file_attribute(self, label, value): self._print((label + ':').ljust(20) + ' ' + value) +class DownloadFileMixin( + ThreadsMixin, + ProgressMixin, + SourceSseMixin, + WriteBufferSizeMixin, + SkipHashVerificationMixin, + MaxDownloadStreamsMixin, + DownloadCommand, +): + STDOUT_FILE_PATH = 'CON' if platform.system() == 'Windows' else '/dev/stdout' + + def _correct_local_filename(self, filename: str): + if filename == '-': + if os.path.exists('-'): + self._print_stderr( + "WARNING: Filename `-` won't be supported in the future and will be treated as stdout alias." + ) + return filename + return self.STDOUT_FILE_PATH + return filename + + @B2.register_subcommand class DownloadFileById( - ThreadsMixin, ProgressMixin, SourceSseMixin, WriteBufferSizeMixin, SkipHashVerificationMixin, - MaxDownloadStreamsMixin, DownloadCommand + DownloadFileMixin, ): """ Downloads the given file, and stores it in the given local file. @@ -1414,6 +1435,7 @@ def _setup_parser(cls, parser): super()._setup_parser(parser) def run(self, args): + args.localFileName = self._correct_local_filename(args.localFileName) super().run(args) progress_listener = make_progress_listener( args.localFileName, args.noProgress or args.quiet @@ -1433,13 +1455,7 @@ def run(self, args): @B2.register_subcommand class DownloadFileByName( - ProgressMixin, - ThreadsMixin, - SourceSseMixin, - WriteBufferSizeMixin, - SkipHashVerificationMixin, - MaxDownloadStreamsMixin, - DownloadCommand, + DownloadFileMixin, ): """ Downloads the given file, and stores it in the given local file. @@ -1464,6 +1480,7 @@ def _setup_parser(cls, parser): super()._setup_parser(parser) def run(self, args): + args.localFileName = self._correct_local_filename(args.localFileName) super().run(args) self._set_threads_from_args(args) bucket = self.api.get_bucket_by_name(args.bucketName) diff --git a/test/unit/test_console_tool.py b/test/unit/test_console_tool.py index 642c44728..688d5f91c 100644 --- a/test/unit/test_console_tool.py +++ b/test/unit/test_console_tool.py @@ -1210,6 +1210,38 @@ def _test_download_threads(self, download_by, num_threads): self._run_command(command) self.assertEqual(b'hello world', self._read_file(local_download)) + def test_download_to_non_seekable_file(self): + self._authorize_account() + self._create_my_bucket() + + # Create a pipe: r_end and w_end are file descriptors. + r_end, w_end = os.pipe() + + # Save the current stdout file descriptor for later restoration + stdout_fd = os.dup(1) + + # Duplicate the write end of the pipe to stdout file descriptor (1) + os.dup2(w_end, 1) + os.close(w_end) + + with TempDir() as temp_dir: + local_file = self._make_local_file(temp_dir, 'file.txt') + self._run_command( + ['upload-file', '--noProgress', 'my-bucket', local_file, 'file.txt'], + remove_version=True, + ) + command = ['download-file-by-name', 'my-bucket', 'file.txt', '-'] + self._run_command(command) + + # Restore original stdout + os.dup2(stdout_fd, 1) + os.close(stdout_fd) + + # Read from the read end of the pipe + with os.fdopen(r_end, "rb") as f: + captured_output = f.read() + self.assertEqual('hello world', captured_output.decode()) + def test_download_by_id_1_thread(self): self._test_download_threads(download_by='id', num_threads=1)