Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serve a tarball containing the contents of a given directory. #53

Merged
merged 9 commits into from Feb 22, 2022
1 change: 1 addition & 0 deletions changelog.d/53.feature
@@ -0,0 +1 @@
Provide ?format=tar.gz option on directory listings to download tarball.
87 changes: 85 additions & 2 deletions logserver.go
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package main

import (
"archive/tar"
"compress/gzip"
"io"
"log"
Expand Down Expand Up @@ -69,6 +70,7 @@ func (f *logServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

func serveFile(w http.ResponseWriter, r *http.Request, path string) {

michaelkaye marked this conversation as resolved.
Show resolved Hide resolved
d, err := os.Stat(path)
if err != nil {
msg, code := toHTTPError(err)
Expand All @@ -79,9 +81,18 @@ func serveFile(w http.ResponseWriter, r *http.Request, path string) {
// for anti-XSS belt-and-braces, set a very restrictive CSP
w.Header().Set("Content-Security-Policy", "default-src: none")

// if it's a directory, serve a listing
// if it's a directory, serve a listing or a tarball
if d.IsDir() {
log.Println("Serving", path)
format, _ := r.URL.Query()["format"]
if len(format) == 1 && format[0] == "tar.gz" {
log.Println("Serving tarball of", path)
michaelkaye marked this conversation as resolved.
Show resolved Hide resolved
err := serveTarball(w, r, path)
if err != nil {
log.Println("Error", err)
michaelkaye marked this conversation as resolved.
Show resolved Hide resolved
}
return
}
log.Println("Serving directory listing of", path)
http.ServeFile(w, r, path)
return
michaelkaye marked this conversation as resolved.
Show resolved Hide resolved
}
Copy link
Member

Choose a reason for hiding this comment

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

this needs a return ? (or you need to guard the rest of it with an else)

Expand Down Expand Up @@ -125,6 +136,78 @@ func extensionToMimeType(path string) string {
return "application/octet-stream"
}

// Streams a dynamically created tar.gz file with the contents of the given directory
// Will serve a partial, corrupted response if there is a error partway through the
// operation as we stream the response.
//
// The resultant tarball will contain a single directory containing all the files
// so it can unpack cleanly without overwriting other files.
func serveTarball(w http.ResponseWriter, r *http.Request, dir string) error {
directory, err := os.Open(dir)
if err != nil {
return err
}
// "disposition filename"
dfilename := strings.Trim(r.URL.Path,"/")
dfilename = strings.Replace(dfilename, "/","_",-1)
michaelkaye marked this conversation as resolved.
Show resolved Hide resolved

// There is no application/tgz or similar; return a gzip file as best option.
// This tends to trigger archive type tools, which will then use the filename to
// identify the contents correctly.
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Disposition", "attachment; filename=" + dfilename + ".tar.gz")

filenames, err := directory.Readdirnames(-1)
if err != nil {
return err
}

gzip := gzip.NewWriter(w)
defer gzip.Close()
targz := tar.NewWriter(gzip)
defer targz.Close()

for _, filename := range filenames {
path := dir + "/" + filename
err := addToArchive(targz, dfilename, path)
if err != nil {
return err
}
}
return nil
}

// Add a single file into the archive.
func addToArchive(targz *tar.Writer, dfilename string, filename string) error {
file, err := os.Open(filename)
michaelkaye marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
defer file.Close()

info, err := file.Stat()
if err != nil {
return err
}

header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
header.Name = dfilename + "/" + info.Name()

err = targz.WriteHeader(header)
if err != nil {
return err
}

_, err = io.Copy(targz, file)
if err != nil {
return err
}
return nil
}

func serveGzippedFile(w http.ResponseWriter, r *http.Request, path string, size int64) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")

Expand Down