Skip to content

Commit 13bb9fb

Browse files
authored
Stream tar extraction to disk and add file-based unpack (#167)
When extracting tar entries to disk, `hex_erl_tar` previously read each file entry fully into memory before writing it to disk. This change makes the disk extraction path stream file entries in chunks (default 64KB) directly to the output file. - `hex_tarball:unpack/2,3` - Added `{file, Path}` as first argument to read tarballs from disk without loading into memory - `hex_tarball:unpack/2,3` - Added `none` as output mode to extract only metadata and checksums, skipping contents - `hex_tarball:unpack/2,3` - Refactored so `Output` drives the outer extraction strategy: `memory` extracts to memory, path/none extracts to a temp dir - `hex_tarball:unpack_docs/2,3` - Added `{file, Path}` as first argument to read doc tarballs from disk without loading into memory - `hex_erl_tar:extract/2` - Added `{chunks, N}` option to control chunk size for streaming extraction to disk
1 parent 3b76158 commit 13bb9fb

4 files changed

Lines changed: 405 additions & 160 deletions

File tree

src/hex_erl_tar.erl

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
%% 3. -doc and -moduledoc attributes removed for OTP 24 compatibility
55
%% 4. safe_link_name/2 fixed to validate symlink targets relative to symlink's
66
%% parent directory instead of in isolation
7+
%% 5. When extracting to disk (cwd option), stream file entries in chunks
8+
%% instead of loading them fully into memory
9+
%% 6. Default chunk_size to 65536 in add_opts instead of 0 with special case
10+
%% 7. Use compressed instead of compressed_one for file:open for OTP 24 compat
711
%%
812
%% OTP commit: 013041bd68c2547848e88963739edea7f0a1a90f
913
%%
@@ -147,12 +151,18 @@ extract1(eof, Reader, _, Acc) ->
147151
extract1(#tar_header{name=Name,size=Size}=Header, Reader0, Opts, Acc0) ->
148152
case check_extract(Name, Opts) of
149153
true ->
150-
case do_read(Reader0, Size) of
151-
{ok, Bin, Reader1} ->
152-
Acc = extract2(Header, Bin, Opts, Acc0),
153-
{ok, Acc, Reader1};
154-
{error, _} = Err ->
155-
throw(Err)
154+
case Opts#read_opts.output of
155+
memory ->
156+
case do_read(Reader0, Size) of
157+
{ok, Bin, Reader1} ->
158+
Acc = extract2(Header, Bin, Opts, Acc0),
159+
{ok, Acc, Reader1};
160+
{error, _} = Err ->
161+
throw(Err)
162+
end;
163+
file ->
164+
Reader1 = extract_to_file(Header, Reader0, Opts),
165+
{ok, Acc0, Reader1}
156166
end;
157167
false ->
158168
{ok, Acc0, skip_file(Reader0)}
@@ -173,6 +183,79 @@ extract2(Header, Bin, Opts, Acc) ->
173183
throw(Err)
174184
end.
175185

186+
extract_to_file(#tar_header{name=Name0}=Header, Reader0, Opts) ->
187+
case typeflag(Header#tar_header.typeflag) of
188+
regular ->
189+
Name1 = make_safe_path(Name0, Opts),
190+
case stream_to_file(Name1, Reader0, Opts) of
191+
{ok, Reader1} ->
192+
read_verbose(Opts, "x ~ts~n", [Name0]),
193+
_ = set_extracted_file_info(Name1, Header),
194+
Reader1;
195+
{error, _} = Err ->
196+
throw(Err)
197+
end;
198+
_ ->
199+
Reader1 = skip_file(Reader0),
200+
_ = write_extracted_element(Header, <<>>, Opts),
201+
Reader1
202+
end.
203+
204+
stream_to_file(Name, Reader0, Opts) ->
205+
Write =
206+
case Opts#read_opts.keep_old_files of
207+
true ->
208+
case file:read_file_info(Name) of
209+
{ok, _} -> false;
210+
_ -> true
211+
end;
212+
false -> true
213+
end,
214+
case Write of
215+
true ->
216+
ChunkSize = Opts#read_opts.chunk_size,
217+
case open_output_file(Name) of
218+
{ok, Fd} ->
219+
try
220+
stream_to_file_loop(Fd, Reader0, ChunkSize)
221+
after
222+
file:close(Fd)
223+
end;
224+
{error, _} = Err ->
225+
Err
226+
end;
227+
false ->
228+
{ok, skip_file(Reader0)}
229+
end.
230+
231+
open_output_file(Name) ->
232+
case file:open(Name, [write, raw, binary]) of
233+
{ok, _} = Ok ->
234+
Ok;
235+
{error, enoent} ->
236+
ok = make_dirs(Name, file),
237+
file:open(Name, [write, raw, binary]);
238+
{error, _} = Err ->
239+
Err
240+
end.
241+
242+
stream_to_file_loop(_Fd, #reg_file_reader{num_bytes=0}=Reader, _ChunkSize) ->
243+
{ok, Reader};
244+
stream_to_file_loop(_Fd, #sparse_file_reader{num_bytes=0}=Reader, _ChunkSize) ->
245+
{ok, Reader};
246+
stream_to_file_loop(Fd, Reader, ChunkSize) ->
247+
case do_read(Reader, ChunkSize) of
248+
{ok, Bin, Reader1} ->
249+
case file:write(Fd, Bin) of
250+
ok ->
251+
stream_to_file_loop(Fd, Reader1, ChunkSize);
252+
{error, _} = Err ->
253+
Err
254+
end;
255+
{error, _} = Err ->
256+
Err
257+
end.
258+
176259
%% Checks if the file Name should be extracted.
177260
check_extract(_, #read_opts{files=all}) ->
178261
true;
@@ -382,13 +465,7 @@ open1({file, Fd}=Handle, read, [raw], Opts) ->
382465
end;
383466
open1({file, _Fd}=Handle, read, [], _Opts) ->
384467
{error, {Handle, {incompatible_option, cooked}}};
385-
open1(Name, Access, Raw, Opts0) when is_list(Name); is_binary(Name) ->
386-
Opts = case lists:member(compressed, Opts0) andalso Access == read of
387-
true ->
388-
[compressed_one | (Opts0 -- [compressed])];
389-
false ->
390-
Opts0
391-
end,
468+
open1(Name, Access, Raw, Opts) when is_list(Name); is_binary(Name) ->
392469
case file:open(Name, Raw ++ [binary, Access|Opts]) of
393470
{ok, File} ->
394471
{ok, #reader{handle=File,access=Access,func=fun file_op/2}};
@@ -1833,9 +1910,6 @@ do_write(#reader{handle=Handle,func=Fun}=Reader0, Data)
18331910
Err
18341911
end.
18351912

1836-
do_copy(#reader{func=Fun}=Reader, Source, #add_opts{chunk_size=0}=Opts)
1837-
when is_function(Fun, 2) ->
1838-
do_copy(Reader, Source, Opts#add_opts{chunk_size=65536});
18391913
do_copy(#reader{func=Fun}=Reader, Source, #add_opts{chunk_size=ChunkSize})
18401914
when is_function(Fun, 2) ->
18411915
case file:open(Source, [read, binary]) of
@@ -2009,6 +2083,8 @@ extract_opts([cooked|Rest], Opts=#read_opts{open_mode=OpenMode}) ->
20092083
extract_opts(Rest, Opts#read_opts{open_mode=[cooked|OpenMode]});
20102084
extract_opts([verbose|Rest], Opts) ->
20112085
extract_opts(Rest, Opts#read_opts{verbose=true});
2086+
extract_opts([{chunks,N}|Rest], Opts) ->
2087+
extract_opts(Rest, Opts#read_opts{chunk_size=N});
20122088
extract_opts([Other|Rest], Opts) ->
20132089
extract_opts(Rest, read_opts([Other], Opts));
20142090
extract_opts([], Opts) ->

src/hex_erl_tar.hrl

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
%% This file is a copy of erl_tar.hrl from OTP with no modifications.
1+
%% This file is a copy of erl_tar.hrl from OTP with the following modifications:
2+
%% 1. Added chunk_size field to #read_opts{} for streaming extraction to disk
3+
%% 2. Added {chunks, pos_integer()} to extract_opt() type
4+
%% 3. Default chunk_size to 65536 in #add_opts{} instead of 0
25
%%
36
%% OTP commit: 013041bd68c2547848e88963739edea7f0a1a90f
47
%%
@@ -25,7 +28,7 @@
2528
%% Options used when adding files to a tar archive.
2629
-record(add_opts, {
2730
read_info, %% Fun to use for read file/link info.
28-
chunk_size = 0, %% For file reading when sending to sftp. 0=do not chunk
31+
chunk_size = 65536, %% Chunk size for reading files.
2932
verbose = false, %% Verbose on/off.
3033
atime = undefined,
3134
mtime = undefined,
@@ -42,7 +45,8 @@
4245
files = all, %% Set of files to extract (or all)
4346
output = file :: 'file' | 'memory',
4447
open_mode = [], %% Open mode options.
45-
verbose = false :: boolean()}). %% Verbose on/off.
48+
verbose = false :: boolean(), %% Verbose on/off.
49+
chunk_size = 65536}). %% Chunk size for streaming to disk.
4650
-type read_opts() :: #read_opts{}.
4751

4852
-type add_opt() :: dereference |
@@ -59,6 +63,7 @@
5963

6064
-type extract_opt() :: {cwd, string()} |
6165
{files, [name_in_archive()]} |
66+
{chunks, pos_integer()} |
6267
compressed |
6368
cooked |
6469
memory |

0 commit comments

Comments
 (0)