Skip to content

Commit fb01754

Browse files
Merge pull request Backblaze#978 from reef-technologies/directory-as-download-target
Downloading file to target directory
2 parents f1da5ba + a11baa5 commit fb01754

File tree

4 files changed

+138
-4
lines changed

4 files changed

+138
-4
lines changed

b2/console_tool.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
######################################################################
1212
from __future__ import annotations
1313

14+
import tempfile
15+
1416
from b2._cli.autocomplete_cache import AUTOCOMPLETE # noqa
1517

1618
AUTOCOMPLETE.autocomplete_from_cache()
@@ -1606,10 +1608,50 @@ def _represent_legal_hold(cls, legal_hold: LegalHold):
16061608
def _print_file_attribute(self, label, value):
16071609
self._print((label + ':').ljust(20) + ' ' + value)
16081610

1609-
def get_local_output_filepath(self, filename: str) -> pathlib.Path:
1611+
def get_local_output_filepath(
1612+
self, filename: str, file_request: DownloadedFile
1613+
) -> pathlib.Path:
16101614
if filename == '-':
16111615
return STDOUT_FILEPATH
1612-
return pathlib.Path(filename)
1616+
1617+
output_filepath = pathlib.Path(filename)
1618+
1619+
# As longs as it's not a directory, we're overwriting everything.
1620+
if not output_filepath.is_dir():
1621+
return output_filepath
1622+
1623+
# If the output is directory, we're expected to download the file right there.
1624+
# Normally, we overwrite the target without asking any questions, but in this case
1625+
# user might be oblivious of the actual mistake he's about to commit.
1626+
# If he, e.g.: downloads file by ID, he might not know the name of the file
1627+
# and actually overwrite something unintended.
1628+
output_directory = output_filepath
1629+
output_filepath = output_directory / file_request.download_version.file_name
1630+
# If it doesn't exist, we stop worrying.
1631+
if not output_filepath.exists():
1632+
return output_filepath
1633+
1634+
# If it does exist, we make a unique file prefixed with the actual file name.
1635+
file_name_as_path = pathlib.Path(file_request.download_version.file_name)
1636+
file_name = file_name_as_path.stem
1637+
file_extension = file_name_as_path.suffix
1638+
1639+
# Default permissions are: readable and writable by this user only, executable by noone.
1640+
# This "temporary" file is not automatically removed, but still created in the safest way possible.
1641+
fd_handle, output_filepath_str = tempfile.mkstemp(
1642+
prefix=file_name,
1643+
suffix=file_extension,
1644+
dir=output_directory,
1645+
)
1646+
# Close the handle, so the file is not locked.
1647+
# This file is no longer 100% "safe", but that's acceptable.
1648+
os.close(fd_handle)
1649+
1650+
# "Normal" file created by Python has readable for everyone, writable for user only.
1651+
# We change the permissions, to match the default ones.
1652+
os.chmod(output_filepath_str, 0o644)
1653+
1654+
return pathlib.Path(output_filepath_str)
16131655

16141656

16151657
class DownloadFileBase(
@@ -1645,7 +1687,7 @@ def _run(self, args):
16451687
)
16461688

16471689
self._print_download_info(downloaded_file)
1648-
output_filepath = self.get_local_output_filepath(args.localFileName)
1690+
output_filepath = self.get_local_output_filepath(args.localFileName, downloaded_file)
16491691
downloaded_file.save_to(output_filepath)
16501692
self._print('Download finished')
16511693

@@ -1711,7 +1753,7 @@ def _run(self, args):
17111753
file_request = self.api.download_file_by_uri(
17121754
args.B2_URI, progress_listener=progress_listener, encryption=encryption_setting
17131755
)
1714-
output_filepath = self.get_local_output_filepath(target_filename)
1756+
output_filepath = self.get_local_output_filepath(target_filename, file_request)
17151757
file_request.save_to(output_filepath)
17161758
return 0
17171759

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Whenever target filename is a directory, file is downloaded into that directory.

test/integration/test_b2_command_line.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import json
1818
import os
1919
import os.path
20+
import pathlib
2021
import re
2122
import sys
2223
import time
@@ -2682,6 +2683,46 @@ def test_download_file_stdout(
26822683
).replace("\r", "") == sample_filepath.read_text()
26832684

26842685

2686+
def test_download_file_to_directory(
2687+
b2_tool, bucket_name, sample_filepath, tmp_path, uploaded_sample_file
2688+
):
2689+
downloads_directory = 'downloads'
2690+
target_directory = tmp_path / downloads_directory
2691+
target_directory.mkdir()
2692+
filename_as_path = pathlib.Path(uploaded_sample_file['fileName'])
2693+
2694+
sample_file_content = sample_filepath.read_text()
2695+
b2_tool.should_succeed(
2696+
[
2697+
'download-file',
2698+
'--quiet',
2699+
f"b2://{bucket_name}/{uploaded_sample_file['fileName']}",
2700+
str(target_directory),
2701+
],
2702+
)
2703+
downloaded_file = target_directory / filename_as_path
2704+
assert downloaded_file.read_text() == sample_file_content, \
2705+
f'{downloaded_file}, {downloaded_file.read_text()}, {sample_file_content}'
2706+
2707+
b2_tool.should_succeed(
2708+
[
2709+
'download-file',
2710+
'--quiet',
2711+
f"b2id://{uploaded_sample_file['fileId']}",
2712+
str(target_directory),
2713+
],
2714+
)
2715+
# A second file should be created.
2716+
new_files = [
2717+
filepath
2718+
for filepath in target_directory.glob(f'{filename_as_path.stem}*{filename_as_path.suffix}')
2719+
if filepath.name != filename_as_path.name
2720+
]
2721+
assert len(new_files) == 1, f'{new_files}'
2722+
assert new_files[0].read_text() == sample_file_content, \
2723+
f'{new_files}, {new_files[0].read_text()}, {sample_file_content}'
2724+
2725+
26852726
def test_cat(b2_tool, bucket_name, sample_filepath, tmp_path, uploaded_sample_file):
26862727
assert b2_tool.should_succeed(
26872728
['cat', f"b2://{bucket_name}/{uploaded_sample_file['fileName']}"],

test/unit/test_console_tool.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,56 @@ def test_download_by_name_1_thread(self):
11171117
def test_download_by_name_10_threads(self):
11181118
self._test_download_threads(download_by='name', num_threads=10)
11191119

1120+
def _test_download_to_directory(self, download_by: str):
1121+
self._authorize_account()
1122+
self._create_my_bucket()
1123+
1124+
base_filename = 'file'
1125+
extension = '.txt'
1126+
source_filename = f'{base_filename}{extension}'
1127+
1128+
with TempDir() as temp_dir:
1129+
local_file = self._make_local_file(temp_dir, source_filename)
1130+
local_file_content = self._read_file(local_file)
1131+
1132+
self._run_command(
1133+
['upload-file', '--noProgress', 'my-bucket', local_file, source_filename],
1134+
remove_version=True,
1135+
)
1136+
1137+
b2uri = f'b2://my-bucket/{source_filename}' if download_by == 'name' else 'b2id://9999'
1138+
command = [
1139+
'download-file',
1140+
'--noProgress',
1141+
b2uri,
1142+
]
1143+
1144+
target_directory = os.path.join(temp_dir, 'target')
1145+
os.mkdir(target_directory)
1146+
command += [target_directory]
1147+
self._run_command(command)
1148+
self.assertEqual(
1149+
local_file_content,
1150+
self._read_file(os.path.join(target_directory, source_filename))
1151+
)
1152+
1153+
# Download the file second time, to check the override behavior.
1154+
self._run_command(command)
1155+
# We should get another file.
1156+
target_directory_files = [
1157+
elem
1158+
for elem in pathlib.Path(target_directory).glob(f'{base_filename}*{extension}')
1159+
if elem.name != source_filename
1160+
]
1161+
assert len(target_directory_files) == 1, f'{target_directory_files}'
1162+
self.assertEqual(local_file_content, self._read_file(target_directory_files[0]))
1163+
1164+
def test_download_by_id_to_directory(self):
1165+
self._test_download_to_directory(download_by='id')
1166+
1167+
def test_download_by_name_to_directory(self):
1168+
self._test_download_to_directory(download_by='name')
1169+
11201170
def test_copy_file_by_id(self):
11211171
self._authorize_account()
11221172
self._create_my_bucket()

0 commit comments

Comments
 (0)