Skip to content

Commit 3c67874

Browse files
committed
add --truncate option to save command
When copying to a larger destination file, the --truncate flag allows truncating the destination to match the source file size.
1 parent 6cb3051 commit 3c67874

2 files changed

Lines changed: 54 additions & 17 deletions

File tree

blockcopy.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def main():
121121
p_retrieve.add_argument('--lzma', action='store_true', help='use lzma compression')
122122

123123
p_save.add_argument('destination_file')
124+
p_save.add_argument('--truncate', action='store_true', help='truncate the destination file to the size of the source file')
124125

125126
args = parser.parse_args()
126127

@@ -135,7 +136,7 @@ def main():
135136
elif args.command == 'retrieve':
136137
do_retrieve(args.source_file, stdin.buffer, stdout.buffer, use_lzma=args.lzma)
137138
elif args.command == 'save':
138-
do_save(args.destination_file, stdin.buffer)
139+
do_save(args.destination_file, stdin.buffer, truncate=args.truncate)
139140
else:
140141
raise Exception(f'Not implemented: {args.command}')
141142

@@ -419,7 +420,8 @@ def do_retrieve(file_path, hash_input_stream, block_output_stream, use_lzma):
419420
- 4 bytes: size of the block
420421
- N bytes: block data
421422
- ...
422-
- 4 bytes: command "done"
423+
- 4 bytes: command "Done"
424+
- 8 bytes: size of the source file
423425
'''
424426
if use_lzma:
425427
from lzma import compress as lzma_compress
@@ -687,14 +689,20 @@ def send_worker():
687689
# This should not happen, because that should already trigger the incomplete read exception.
688690
exit('ERROR (retrieve): Received no done command from the checksum side')
689691

692+
with open(file_path, 'rb') as f:
693+
total_size = f.seek(0, SEEK_END)
694+
690695
with block_output_stream_lock:
691-
block_output_stream.write(b'done')
696+
block_output_stream.write(b'Done')
697+
block_output_stream.write(total_size.to_bytes(8, 'big'))
692698
block_output_stream.flush()
693699

694700

695-
def do_save(file_path, block_input_stream):
701+
def do_save(file_path, block_input_stream, truncate=False):
696702
'''
697703
Read blocks from block_input_stream and write them to the file.
704+
705+
If truncate is True, truncate the file to the size of the source file.
698706
'''
699707
lzma_decompress = None
700708

@@ -711,8 +719,20 @@ def do_save(file_path, block_input_stream):
711719
if len(command) != 4:
712720
raise IncompleteReadError('Incomplete read of command from block input stream')
713721
if command == b'done':
722+
# Old variant of the done command - no size of the source file
723+
received_done = True
724+
f.flush()
725+
# Cannot truncate here - we do not know the size of the source file.
726+
# We know only the offset of the end of last changed block.
727+
break
728+
if command == b'Done':
729+
# New variant of the done command - contains the size of the source file
714730
received_done = True
715731
f.flush()
732+
done_pos_b = block_input_stream.read(8)
733+
done_pos = int.from_bytes(done_pos_b, 'big')
734+
if truncate:
735+
f.truncate(done_pos)
716736
break
717737
elif command in (b'data', b'dlzm'):
718738
block_pos_b = block_input_stream.read(8)

tests/test_copy.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from contextlib import ExitStack
2+
from pytest import mark
23
from subprocess import DEVNULL, PIPE, Popen, run
34
from sys import executable
45

@@ -37,7 +38,7 @@ def test_copy_tiny(tmp_path, script_path):
3738
p2 = stack.enter_context(Popen(cmd2, stdin=PIPE, stdout=PIPE))
3839
p2_output, _ = p2.communicate(input=p1_output, timeout=5)
3940
assert p2.wait() == 0
40-
assert p2_output == b'data\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0cHello World!done'
41+
assert p2_output == b'data\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0cHello World!Done\x00\x00\x00\x00\x00\x00\x00\x0c'
4142

4243
p3 = stack.enter_context(Popen(cmd3, stdin=PIPE, stdout=PIPE))
4344
p3_output, _ = p3.communicate(input=p2_output, timeout=5)
@@ -137,7 +138,14 @@ def test_copy_lzma(tmp_path, script_path):
137138
assert dst_path.read_bytes() == test_content
138139

139140

140-
def test_copy_to_larger_file_tiny(tmp_path, script_path):
141+
@mark.parametrize('truncate', [False, True], ids=['no_truncate', 'truncate'])
142+
def test_copy_to_larger_file_tiny(tmp_path, script_path, truncate):
143+
'''
144+
Copying from smaller source file to larger destination file.
145+
146+
Without truncate: the destination file size remains the same.
147+
With truncate: the destination file size will be equal to the source file.
148+
'''
141149
test_content = b'Hello World!'
142150
src_path = tmp_path / 'src_file'
143151
src_path.write_bytes(test_content)
@@ -146,7 +154,7 @@ def test_copy_to_larger_file_tiny(tmp_path, script_path):
146154

147155
cmd1 = [executable, script_path, 'checksum', str(dst_path)]
148156
cmd2 = [executable, script_path, 'retrieve', str(src_path)]
149-
cmd3 = [executable, script_path, 'save', str(dst_path)]
157+
cmd3 = [executable, script_path, 'save', *(['--truncate'] if truncate else []), str(dst_path)]
150158

151159
with ExitStack() as stack:
152160
p1 = stack.enter_context(Popen(cmd1, stdin=DEVNULL, stdout=PIPE))
@@ -168,14 +176,17 @@ def test_copy_to_larger_file_tiny(tmp_path, script_path):
168176
p2 = stack.enter_context(Popen(cmd2, stdin=PIPE, stdout=PIPE))
169177
p2_output, _ = p2.communicate(input=p1_output, timeout=5)
170178
assert p2.wait() == 0
171-
assert p2_output == b'data\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0cHello World!done'
179+
assert p2_output == b'data\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0cHello World!Done\x00\x00\x00\x00\x00\x00\x00\x0c'
172180

173181
p3 = stack.enter_context(Popen(cmd3, stdin=PIPE, stdout=PIPE))
174182
p3_output, _ = p3.communicate(input=p2_output, timeout=5)
175183
assert p3.wait() == 0
176184
assert p3_output == b''
177185

178-
assert dst_path.read_bytes() == test_content + b'extra'
186+
if truncate:
187+
assert dst_path.read_bytes() == test_content
188+
else:
189+
assert dst_path.read_bytes() == test_content + b'extra'
179190

180191

181192
def test_copy_to_larger_file(tmp_path, script_path):
@@ -205,7 +216,8 @@ def test_copy_to_larger_file(tmp_path, script_path):
205216
assert dst_path.read_bytes() == test_content + b'extra'
206217

207218

208-
def test_copy_identical(tmp_path, script_path):
219+
@mark.parametrize('truncate', [False, True], ids=['no_truncate', 'truncate'])
220+
def test_copy_identical(tmp_path, script_path, truncate):
209221
'''
210222
This tests checks that no actual data is copied when source and destination are identical.
211223
'''
@@ -217,7 +229,7 @@ def test_copy_identical(tmp_path, script_path):
217229

218230
cmd1 = [executable, script_path, 'checksum', str(dst_path)]
219231
cmd2 = [executable, script_path, 'retrieve', str(src_path)]
220-
cmd3 = [executable, script_path, 'save', str(dst_path)]
232+
cmd3 = [executable, script_path, 'save', *(['--truncate'] if truncate else []), str(dst_path)]
221233

222234
with ExitStack() as stack:
223235
p1 = stack.enter_context(Popen(cmd1, stdin=DEVNULL, stdout=PIPE))
@@ -233,12 +245,14 @@ def test_copy_identical(tmp_path, script_path):
233245
assert p3.wait() == 0
234246

235247
# retrieve output should be trivial
236-
assert p2_output == b'done'
248+
assert p2_output == b'Done' + len(test_content).to_bytes(8, 'big')
237249

250+
assert src_path.read_bytes() == test_content
238251
assert dst_path.read_bytes() == test_content
239252

240253

241-
def test_copy_to_smaller_file(tmp_path, script_path):
254+
@mark.parametrize('truncate', [False, True], ids=['no_truncate', 'truncate'])
255+
def test_copy_to_smaller_file(tmp_path, script_path, truncate):
242256
'''
243257
This test simulates copying from larger source file to smaller destination file.
244258
This could happen for example when previous copy was interrupted.
@@ -253,7 +267,7 @@ def test_copy_to_smaller_file(tmp_path, script_path):
253267

254268
cmd1 = [executable, script_path, 'checksum', str(dst_path)]
255269
cmd2 = [executable, script_path, 'retrieve', str(src_path)]
256-
cmd3 = [executable, script_path, 'save', str(dst_path)]
270+
cmd3 = [executable, script_path, 'save', *(['--truncate'] if truncate else []), str(dst_path)]
257271

258272
with ExitStack() as stack:
259273
p1 = stack.enter_context(Popen(cmd1, stdin=DEVNULL, stdout=PIPE))
@@ -266,7 +280,8 @@ def test_copy_to_smaller_file(tmp_path, script_path):
266280
assert dst_path.read_bytes() == test_content
267281

268282

269-
def test_copy_start_offset_tiny(tmp_path, script_path):
283+
@mark.parametrize('truncate', [False, True], ids=['no_truncate', 'truncate'])
284+
def test_copy_start_offset_tiny(tmp_path, script_path, truncate):
270285
test_content = b'Hello World!'
271286
src_path = tmp_path / 'src_file'
272287
src_path.write_bytes(test_content)
@@ -275,7 +290,7 @@ def test_copy_start_offset_tiny(tmp_path, script_path):
275290

276291
cmd1 = [executable, script_path, 'checksum', str(dst_path), '--start', str(5)]
277292
cmd2 = [executable, script_path, 'retrieve', str(src_path)]
278-
cmd3 = [executable, script_path, 'save', str(dst_path)]
293+
cmd3 = [executable, script_path, 'save', *(['--truncate'] if truncate else []), str(dst_path)]
279294

280295
with ExitStack() as stack:
281296
p1 = stack.enter_context(Popen(cmd1, stdin=DEVNULL, stdout=PIPE))
@@ -302,7 +317,8 @@ def test_copy_start_offset_tiny(tmp_path, script_path):
302317
b'\x00\x00\x00\x00\x00\x00\x00\x05'
303318
b'\x00\x00\x00\x07'
304319
b' World!'
305-
b'done'
320+
b'Done'
321+
b'\x00\x00\x00\x00\x00\x00\x00\x0c'
306322
)
307323

308324
p3 = stack.enter_context(Popen(cmd3, stdin=PIPE, stdout=PIPE))
@@ -311,3 +327,4 @@ def test_copy_start_offset_tiny(tmp_path, script_path):
311327
assert p3_output == b''
312328

313329
assert dst_path.read_bytes() == b'-' * 5 + test_content[5:]
330+
assert dst_path.read_bytes() == b'----- World!'

0 commit comments

Comments
 (0)