use
5.012;
our
$VERSION
=
'1.0'
;
our
@ISA
=
qw(Exporter)
;
our
@EXPORT_OK
=
qw(construct)
;
sub
construct {
my
%params
;
if
(
@_
== 2 ) {
(
$params
{image},
$params
{dir} ) =
@_
;
}
else
{
%params
=
@_
;
}
croak
"must specify input image tarball 'image'"
unless
$params
{image};
croak
"must specify output directory 'dir'"
unless
$params
{dir};
my
$image
=
$params
{image};
my
$dir
=
$params
{dir};
croak
"file not found: $image"
unless
-f
$image
;
croak
"directory not found: $dir"
unless
-d
$dir
;
my
@imagefiles
= _read_file_list(
$image
);
croak
"this does not seem to be a docker image (missing manifest.json)"
unless
grep
{
$_
eq
'manifest.json'
}
@imagefiles
;
my
%manifest
= %{
decode_json(
_read_file_from_tar(
$image
,
'manifest.json'
)
)->[0]
};
my
%wh
;
for
my
$layer
( @{
$manifest
{Layers}} ) {
my
$layer_abbrev
=
substr
(
$layer
,0,12);
print
STDERR
"reading layer: $layer_abbrev...\n"
unless
$params
{quiet};
$wh
{
$layer
} = [];
my
$layer_fh
= _stream_file_from_tar(
$image
,
$layer
);
my
$filelist
= _exec_tar(
$layer_fh
,
'-t'
);
while
(<
$filelist
>) {
chomp
;
my
(
$dirname
,
$basename
) = (splitpath
$_
)[1,2];
if
(
$basename
=~ /^\.wh\./) {
my
$to_delete
= catfile
$dirname
, (
$basename
=~ s/^\.wh\.//r );
push
@{
$wh
{
$layer
} },
$to_delete
;
}
}
close
$filelist
or croak $! ?
"could not close pipe: $!"
:
"exit code $? from tar"
;
close
$layer_fh
or croak $! ?
"could not close pipe: $!"
:
"exit code $? from tar"
;
}
for
my
$layer
( @{
$manifest
{Layers}} ) {
my
$layer_abbrev
=
substr
$layer
, 0, 12;
print
STDERR
"extracting layer: $layer_abbrev...\n"
unless
$params
{quiet};
my
$layer_fh
= _stream_file_from_tar(
$image
,
$layer
);
my
$extract_fh
= _exec_tar(
$layer_fh
,
'-C'
,
$dir
,
qw'-x --exclude .wh.*'
);
close
$extract_fh
or croak $! ?
"could not close pipe: $!"
:
"exit code $? from tar"
;
close
$layer_fh
or croak $! ?
"could not close pipe: $!"
:
"exit code $? from tar"
;
for
my
$file
( @{
$wh
{
$layer
} }) {
my
$path
= catfile
$dir
,
$file
;
if
(-f
$path
) {
unlink
$path
or carp
"failed to remove file: $path"
;
}
elsif
(-d
$path
) {
remove_tree
$path
;
}
}
}
if
(
$params
{include_config}) {
my
$config
=
$manifest
{Config};
carp
"wanted to include config json but couldn't find it in manifest."
unless
defined
$config
;
print
STDERR
"extracting config: $config...\n"
unless
$params
{quiet};
my
$outfile
= catfile
$dir
,
'config.json'
;
open
(
my
$config_write
,
'>'
,
$outfile
) or croak
"could not open $outfile: $!"
;
my
$config_read
= _exec_tar(
$image
,
'-xO'
,
$config
);
while
(<
$config_read
>) {
print
$config_write
$_
;
}
close
$config_write
or croak
"could not close $outfile: $!"
;
close
$config_read
or croak $! ?
"could not close pipe: $!"
:
"exit code $? from tar"
;
}
print
STDERR
"done.\n"
unless
$params
{quiet};
}
sub
_read_file_list {
my
$fh
= _exec_tar(
shift
,
'-t'
);
my
@filelist
= <
$fh
>;
chomp
@filelist
;
close
$fh
or croak $! ?
"could not close pipe: $!"
:
"exit code $? from tar"
;
return
@filelist
;
}
sub
_read_file_from_tar {
my
$fh
= _stream_file_from_tar(
@_
);
my
$content
;
{
local
$/ =
undef
;
$content
= <
$fh
>;
}
close
$fh
or croak $! ?
"could not close pipe: $!"
:
"exit code $? from tar"
;
return
$content
;
}
sub
_stream_file_from_tar {
my
$input
=
shift
;
my
$path
=
shift
;
return
_exec_tar(
$input
,
'-xO'
,
$path
);
}
sub
_exec_tar {
my
$input
=
shift
;
my
@args
=
@_
;
my
$read_fh
;
if
(openhandle
$input
) {
my
@command
= (
'tar'
,
@args
);
my
$pid
=
open
(
$read_fh
,
'-|'
);
croak
"could not fork"
unless
defined
$pid
;
do
{
open
(STDIN,
'<&'
,
$input
);
exec
@command
; }
unless
$pid
;
}
else
{
my
@command
= (
'tar'
,
'-f'
,
$input
,
@args
);
open
(
$read_fh
,
'-|'
,
@command
) or croak
"could not exec tar"
;
}
return
$read_fh
;
}
1;