// custom function to serve files func sendFile(rw http.ResponseWriter, req *http.Request, path string) { log.Infof("serve %v %v", req.URL.Path, path) file, err := os.Open(path) if err != nil { http.NotFound(rw, req) return } defer file.Close() fileinfo, err := file.Stat() if err != nil { http.NotFound(rw, req) return } // for directories, or for files smaller than 1k, use library if fileinfo.Mode().IsDir() || fileinfo.Size() < 1024 { http.ServeFile(rw, req, path) return } // supports If-Modified-Since header if t, err := time.Parse(http.TimeFormat, req.Header.Get("If-Modified-Since")); err == nil && fileinfo.ModTime().Before(t.Add(1*time.Second)) { rw.WriteHeader(http.StatusNotModified) return } // support Accept-Encoding header var writer io.Writer = rw var reader io.Reader = file if !strings.HasSuffix(path, ".gz") { ae := req.Header.Get("Accept-Encoding") if strings.Contains(ae, "gzip") { gz, err := cgzip.NewWriterLevel(rw, cgzip.Z_BEST_SPEED) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } rw.Header().Set("Content-Encoding", "gzip") defer gz.Close() writer = gz } } // add content-length if we know it if writer == rw && reader == file { rw.Header().Set("Content-Length", fmt.Sprintf("%v", fileinfo.Size())) } // and just copy content out rw.Header().Set("Last-Modified", fileinfo.ModTime().UTC().Format(http.TimeFormat)) rw.WriteHeader(http.StatusOK) if _, err := io.Copy(writer, reader); err != nil { log.Warningf("transfer failed %v: %v", path, err) } }
// newSnapshotFile behavior depends on the compress flag: // - if compress is true , it compresses a single file with gzip, and // computes the hash on the compressed version. // - if compress is false, just symlinks and computes the hash on the file // The source file is always left intact. // The path of the returned SnapshotFile will be relative // to root. func newSnapshotFile(srcPath, dstPath, root string, compress bool) (*SnapshotFile, error) { // open the source file srcFile, err := os.OpenFile(srcPath, os.O_RDONLY, 0) if err != nil { return nil, err } defer srcFile.Close() src := bufio.NewReaderSize(srcFile, 2*1024*1024) var hash string var size int64 if compress { log.Infof("newSnapshotFile: starting to compress %v into %v", srcPath, dstPath) // open the temporary destination file dir, filePrefix := path.Split(dstPath) dstFile, err := ioutil.TempFile(dir, filePrefix) if err != nil { return nil, err } defer func() { // try to close and delete the file. in the // success case, the file will already be // closed and renamed, so all of this would // fail anyway, no biggie dstFile.Close() os.Remove(dstFile.Name()) }() dst := bufio.NewWriterSize(dstFile, 2*1024*1024) // create the hasher and the tee on top hasher := newHasher() tee := io.MultiWriter(dst, hasher) // create the gzip compression filter gzip, err := cgzip.NewWriterLevel(tee, cgzip.Z_BEST_SPEED) if err != nil { return nil, err } // copy from the file to gzip to tee to output file and hasher _, err = io.Copy(gzip, src) if err != nil { return nil, err } // close gzip to flush it if err = gzip.Close(); err != nil { return nil, err } // close dst manually to flush all buffers to disk dst.Flush() dstFile.Close() hash = hasher.HashString() // atomically move completed compressed file err = os.Rename(dstFile.Name(), dstPath) if err != nil { return nil, err } // and get its size fi, err := os.Stat(dstPath) if err != nil { return nil, err } size = fi.Size() } else { log.Infof("newSnapshotFile: starting to hash and symlinking %v to %v", srcPath, dstPath) // get the hash hasher := newHasher() _, err = io.Copy(hasher, src) if err != nil { return nil, err } hash = hasher.HashString() // do the symlink err = os.Symlink(srcPath, dstPath) if err != nil { return nil, err } // and get the size fi, err := os.Stat(srcPath) if err != nil { return nil, err } size = fi.Size() } log.Infof("clone data ready %v:%v", dstPath, hash) relativeDst, err := filepath.Rel(root, dstPath) if err != nil { return nil, err } return &SnapshotFile{relativeDst, size, hash, ""}, nil }