Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Tar"
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
authors = ["Stefan Karpinski <[email protected]>"]
version = "1.9.0"
version = "1.10.0"

[deps]
ArgTools = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ will also not be copied and will instead be skipped. By default, `extract` will
detect whether symlinks can be created in `dir` or not and will automatically
copy symlinks if they cannot be created.

### `Tar.extract_file`

```jl
extract_file(predicate::Function, tarball, out) -> Vector{Header}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be

Suggested change
extract_file(predicate::Function, tarball, out) -> Vector{Header}
extract_files(predicate::Function, tarball, out) -> Vector{Header}

?

extract_file(predicate::AbstractString, tarball, out) -> Header
```
* `predicate :: Union{Function, AbstractString}`
* `tarball :: Union{AbstractString, AbstractCmd, IO}`
* `out :: Union{AbstractString, AbstractCmd, IO}`

Read file(s) matching the predicate from `tarball` and write to `out`.
Return the [`Header`](@ref)s of the matchin files.

If `predicate::Function` it should take a `Header` as the only input
argument and return `true`/`false`.
If `predicate::String` it is interpreted as a path relative the
tarball root and must only match a single entry.

### Tar.list

```jl
Expand Down
44 changes: 44 additions & 0 deletions src/extract.jl
Original file line number Diff line number Diff line change
Expand Up @@ -590,3 +590,47 @@ function read_data(
r < n && error("premature end of tar file")
return view(buf, 1:size)
end


"""
extract_file(predicate::Funtion, tarball, out) -> Vector{Header}
extract_file(predicate::AbstractString, tarball, out) -> Header

Accepted argument types:
* `predicate :: Union{AbstractString, Function}`
* `tarball :: Union{AbstractString, IO, Cmd}`
* `out :: Union{AbstractString, IO, Cmd}`

Read file(s) matching the predicate from `tarball` and write to `out`.
Return the [`Header`](@ref)s of the matchin files.

If `predicate::Function` it should take a `Header` as the only input
argument and return `true`/`false`.
If `predicate::String` it is interpreted as a path relative the
tarball root and must only match a single entry.
"""
function extract_file(predicate::Function, tarball::ArgRead, out::ArgWrite)::Vector{Header}
headers = Header[]
buf = Vector{UInt8}(undef, DEFAULT_BUFFER_SIZE)
arg_read(tarball) do tar; arg_write(out) do io
read_tarball(predicate, tar) do hdr, _
if hdr.type == :file # TODO: read symlinks??
push!(headers, hdr)
read_data(tar, io; size=hdr.size, buf=buf)
end
end
end end
return headers
end
function extract_file(predicate::AbstractString, tarball::ArgRead, out::ArgWrite)::Header
parts = filter!(x -> x != ".", splitpath(predicate))
headers = extract_file(tarball, out) do hdr
hdr_parts = filter!(x -> x != ".", splitpath(hdr.path))
hdr.type == :file && parts == hdr_parts
end
if length(headers) != 1
s = length(headers) == 0 ? "no" : "multiple"
throw(ArgumentError("$s files in the tarball matches the filename $predicate"))
end
return headers[1]
end
69 changes: 69 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,75 @@ end
end
end

@testset "API: extract_file" begin
mktempdir() do dir
open(joinpath(dir, "file.txt"), "w") do io
write(io, "file at the root")
end
dir2 = mkdir(joinpath(dir, "directory"))
open(joinpath(dir2, "file2.txt"), "w") do io
write(io, "file in directory")
end
tarball = Tar.create(dir)

for tar in (()->tarball, ()->open(tarball))
## predicate::String
io = IOBuffer()
for pred in ("file.txt", "./file.txt")
hdr = Tar.extract_file(pred, tar(), io)
@test hdr.path == "file.txt"
@test hdr.size == 16
@test String(take!(io)) == "file at the root"
end

for pred in ("directory/file2.txt", "./directory/file2.txt")
hdr = Tar.extract_file(pred, tar(), io)
@test hdr.path == "directory/file2.txt"
@test hdr.size == 17
@test String(take!(io)) == "file in directory"
end

@test_throws ArgumentError("no files in the tarball matches the filename nope") Tar.extract_file("nope", tar(), io)

# predicate::Function
hdrs = Tar.extract_file(tar(), io) do hdr
hdr.path == "file.txt"
end
@test length(hdrs) == 1
@test hdrs[1].path == "file.txt"
@test hdrs[1].size == 16
@test String(take!(io)) == "file at the root"

hdrs = Tar.extract_file(hdr -> true, tar(), io)
@test length(hdrs) == 2
str = String(take!(io))
@test occursin("file at the root", str)
@test occursin("file in directory", str)
@test sum(h.size for h in hdrs) == sizeof(str)

hdrs = Tar.extract_file(hdr -> true, tar(), io)
@test length(hdrs) == 2
str = String(take!(io))
@test occursin("file at the root", str)
@test occursin("file in directory", str)
@test sum(h.size for h in hdrs) == sizeof(str)

hdrs = Tar.extract_file(hdr -> false, tar(), io)
@test length(hdrs) == 0
@test sizeof(String(take!(io))) == 0

# Non-IO output
mktempdir() do tmpd
o = joinpath(tmpd, "out.data")
Tar.extract_file("file.txt", tar(), o)
@test read(o, String) == "file at the root"
Tar.extract_file(hdr -> hdr.path == "file.txt", tar(), o)
@test read(o, String) == "file at the root"
end
end
end
end

@testset "API: rewrite" begin
# reference standard tarball
reference, hash₁ = make_test_tarball()
Expand Down