Repository navigation
archive/tar: add support for writing tar containing sparse files #13548
Description
I've created a Github Repo with all the needed steps for reproducing this on Ubuntu 12.04 using Go1.5.1. I've also verified that using Go1.5.2 still experiences this error.
Run vagrant create then vagrant provision from repository root.
vagrant create
vagrant provision
Expected Output:
$ vagrant provision
==> default: Running provisioner: shell...
default: Running: inline script
==> default: stdin: is not a tty
==> default: go version go1.5.2 linux/amd64
==> default: Creating Sparse file
==> default: Proving file is truly sparse
==> default: 0 -rw-r--r-- 1 root root 512M Dec 9 15:26 sparse.img
==> default: Compressing in Go without sparse
==> default: Compressing in Go with sparse
==> default: FileInfo File Size: 536870912
==> default: Proving non-sparse in Go gained size on disk
==> default: 512M -rw-r--r-- 1 root root 512M Dec 9 15:26 non_sparse/sparse.img
==> default: Proving sparse in Go DID keep file size on disk
==> default: 0 -rw-r--r-- 1 root root 0 Dec 9 15:26 sparse/sparse.img
==> default: Compressing via tar w/ Sparse Flag set
==> default: Proving sparse via tar DID keep file size on disk
==> default: 0 -rw-r--r-- 1 root root 512M Dec 9 15:26 tar/sparse.img
Actual Output:
$ vagrant provision
==> default: Running provisioner: shell...
default: Running: inline script
==> default: stdin: is not a tty
==> default: go version go1.5.2 linux/amd64
==> default: Creating Sparse file
==> default: Proving file is truly sparse
==> default: 0 -rw-r--r-- 1 root root 512M Dec 9 15:35 sparse.img
==> default: Compressing in Go without sparse
==> default: Compressing in Go with sparse
==> default: Proving non-sparse in Go gained size on disk
==> default: 513M -rw-r--r-- 1 root root 512M Dec 9 15:35 non_sparse/sparse.img
==> default: Proving sparse in Go DID NOT keep file size on disk
==> default: 512M -rw-r--r-- 1 root root 512M Dec 9 15:35 sparse/sparse.img
==> default: Compressing via tar w/ Sparse Flag set
==> default: Proving sparse via tar DID keep file size on disk
==> default: 0 -rw-r--r-- 1 root root 512M Dec 9 15:35 tar/sparse.img
The Vagrantfile supplied in the repository runs the following shell steps:
- Installs Go
- Creates a sparse file via
truncate -s 512M sparse.img - Proves that the file is sparse via
ls -lash sparse.img - Runs
compress.goviago run compress.go - Untars the archives created by
compress.goviatar -xf - Verifies that the extracted files did not maintain sparse files, both with and without the sparse type set in the tar file's header.
ls -lash sparse.img - Uses GNU/Tar to compress the sparse file with the sparse flag set
tar -Scf sparse.tar sparse.img - Extracts the archive created by GNU/Tar
tar -xf sparse.tar - Proves that GNU/Tar maintained sparse files
ls -lash sparse.img
This is somewhat related to #12594.
I could also be creating the archive incorrectly, and have tried a few different methods for creating the tar archive, each one however, did not keep the sparse files intact upon extraction of the archive. This also cannot be replicated in OSX as HGFS+ does not have a concept of sparse files, and instantly destroys any file sparseness, hence the need for running and testing the reproduction case in a vagrant vm.
Any thoughts or hints into this would be greatly appreciated, thanks!
Activity
bradfitz commentedon Dec 9, 2015
/cc @dsnet who's been going crazy on the archive/tar package in the Go 1.6 tree ("master" branch)
dsnet commentedon Dec 9, 2015
This isn't a bug per-say, but more of a feature request. Sparse file support is only provided for tar.Reader, but not tar.Writer. Currently, it's a bit asymmetrical, but supporting sparse files on tar.Writer requires API change, which may take some time to think about.
Also, this is mostly unrelated to #12594. Although, that bug should definitely be fixed before any attempt at this is made. For the time being, I recommend putting this in the "unplanned" milestone, I'll revisit this issue when the other tar bugs are first fixed.
grubernaut commentedon Dec 9, 2015
@dsnet should I keep this here as a feature request, or is there another preferred format for those?
dsnet commentedon Dec 9, 2015
The issue tracker is perfect for that. So this is just fine.
[-]archive/tar: Writing a tarfile does not maintain sparse files[/-][+]archive/tar: no support for writing tar containing sparse files[/+][-]archive/tar: no support for writing tar containing sparse files[/-][+]archive/tar: add support for writing tar containing sparse files[/+]dsnet commentedon Feb 26, 2016
This my proposed addition to the tar API to support sparse writing.
First, we modify tar.Header to have an extra field:
On the reader side, nothing much changes. We already support sparse files. All that's being done is that we're now exporting information about the sparse file through the SparseHoles field.
On the writer side, the user must set the SparseHoles field if they intend to write a sparse file. It is optional for them to set Typeflag to TypeGNUSparse (there are multiple formats to represent sparse files so this is not important). The user then proceeds to write all the data for the file. For sparse holes, they will be required to write Length zeros for that given hole. It is a little inefficient writing zeros for the holes, but I decided on this approach because:
I should note that the tar format represents sparse files by indicating which regions have data, and treating everything else as a hole. The API exposed here does the opposite; it represents sparse files by indicating which regions are holes, and treating everything else as data. The reason for this inversion is because it fits the Go philosophy that the zero value of some be meaningful. The zero value of SparseHoles indicates that there are no holes in the file, and thus it is a normal file; i.e., the default makes sense. If we were to use SparseDatas instead, the zero value of that indicates that there is no data in the file, which is rather odd.
It is a little inefficient requiring that users write zeros and the bottleneck will be the memory bandwidth's ability to transfer potentially large chunks of zeros. Though not necessary, the following methods may be worth adding as well:
Potential example usage: https://play.golang.org/p/Vy63LrOToO
ianlancetaylor commentedon Feb 26, 2016
If Reader and Writer support sparse files transparently, why export SparseHoles? Is the issue that when writing you don't want to introduce a sparse hole that the caller did not explicitly request?
dsnet commentedon Feb 26, 2016
The Reader expands sparse files transparently. The Writer is "transparent" in the sense that a user can just do io.Copy(tw, sparseFile) and so long as the user already specified where there sparse holes are, it will avoid writing the long runs of zeros.
Purely transparent sparse files for Writer cannot easily done since the tar.Header is written before the file data. Thus, the Writer cannot know what sparse map to encode in the header prior to seeing the data itself. Thus, Writer.WriteHeader needs to be told where the sparse holes are.
I don't think tar should automatically create sparse files (for backwards compatibility). As a data point, the tar utilities do not automatically generate sparse files unless the -S flag is passed in. However, it would be nice if the user didn't need to come up with the SparseHoles themselves. Unfortunately, I don't see an easy solution to this.
There are three main ways that sparse files may be written:
I looked at the source for GNU and BSD tar to see what they do:
I'm not too fond of the OS specific things that they do to detect holes (granted archive/tar already has many OS specific things in it). I think it would be nice if tar.Writer provided a way to write spares files, but I think we should delegate detection of sparse holes to the user for now. If possible, we can try and get sparse info during FileInfoHeader, but I'm not sure that os.FileInfo has the necessary information to do the queries that are needed.
AkihiroSuda commentedon Nov 29, 2016
@dsnet Design SGTM (non-binding), do you plan to implement that feature?
dsnet commentedon Dec 1, 2016
I'll try and get this into the Go 1.9 cycle. However, a major refactoring of the tar.Writer implementation needs to happen first.
45 remaining items
follow best practices for creating unprivileged user
Gogowitsch commentedon Feb 11, 2021
Dear Go heros, please try to get sparse support into tar.Writer. Thanks!
Speed up `bin/compose setup` significantly
Speed up `bin/compose setup` significantly (#9404)
bugfix: Avoid exploding Docker image size
fix: avoid exploding Docker image size (#185)
realtebo commentedon Mar 29, 2024
jonathongardner commentedon May 9, 2025
Is there any way to manually write sparse files? I figured if I put the
PAXRecordswithGNU.sparse...in manually and then write a gnu sparse formatted file with mapping etc. to the file part it would work but it seems theGNU.sparseheaders are ignored...glycerine commentedon Aug 31, 2025
@jonathongardner I'm not sure if I'm on the same page because I don't know what PAXRecords and GNU.sparse are referring to, but certainly from Go you can manually make sparse files. I have library and test suite that does that here (and there are others on github of varying quality).
https://github.com/glycerine/rpc25519/tree/master/jsync/sparsified
See also (especially for darwin/MacOS), my comments on a related ticket for some hard won advice on working with sparse files:
#22735 (comment)
johnnybubonic commentedon Jan 20, 2026
@glycerine -
He is talking about the PAX sparse file extension 1.0, which would be a suitable solution to this entire issue if it worked - detecting sparse blocks in Linux is easy, but there's functionally no way to write them in a sparse tar format because the library is ignoring the header extensions.
More accurately, it silently removes them.
Attempting to manually make
archive/tarwrite the 1.0 sparse format requires usingtar.TypeXHeaderfor atar.Header.Format- and attempting to do so explicitly of course returns aarchive/tar: cannot encode header: cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headerserror.If there's one thing that's insufferably infuriating, it's incomplete implementations in libraries that intentionally cripple any attempts of fully implementing. Guess it's time to fork out
archive/tar; it's wild that the bikeshedding from the stdlib team surrounding this has led to not being able to implement a common and ubiquitous use case and spec established 20 years ago.