github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/archive/compression/compression.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package compression 18 19 import ( 20 "bufio" 21 "bytes" 22 "compress/gzip" 23 "context" 24 "fmt" 25 "io" 26 "os" 27 "os/exec" 28 "strconv" 29 "sync" 30 31 "github.com/containerd/containerd/log" 32 ) 33 34 type ( 35 // Compression is the state represents if compressed or not. 36 Compression int 37 ) 38 39 const ( 40 // Uncompressed represents the uncompressed. 41 Uncompressed Compression = iota 42 // Gzip is gzip compression algorithm. 43 Gzip 44 ) 45 46 const disablePigzEnv = "CONTAINERD_DISABLE_PIGZ" 47 48 var ( 49 initPigz sync.Once 50 unpigzPath string 51 ) 52 53 var ( 54 bufioReader32KPool = &sync.Pool{ 55 New: func() interface{} { return bufio.NewReaderSize(nil, 32*1024) }, 56 } 57 ) 58 59 // DecompressReadCloser include the stream after decompress and the compress method detected. 60 type DecompressReadCloser interface { 61 io.ReadCloser 62 // GetCompression returns the compress method which is used before decompressing 63 GetCompression() Compression 64 } 65 66 type readCloserWrapper struct { 67 io.Reader 68 compression Compression 69 closer func() error 70 } 71 72 func (r *readCloserWrapper) Close() error { 73 if r.closer != nil { 74 return r.closer() 75 } 76 return nil 77 } 78 79 func (r *readCloserWrapper) GetCompression() Compression { 80 return r.compression 81 } 82 83 type writeCloserWrapper struct { 84 io.Writer 85 closer func() error 86 } 87 88 func (w *writeCloserWrapper) Close() error { 89 if w.closer != nil { 90 w.closer() 91 } 92 return nil 93 } 94 95 type bufferedReader struct { 96 buf *bufio.Reader 97 } 98 99 func newBufferedReader(r io.Reader) *bufferedReader { 100 buf := bufioReader32KPool.Get().(*bufio.Reader) 101 buf.Reset(r) 102 return &bufferedReader{buf} 103 } 104 105 func (r *bufferedReader) Read(p []byte) (n int, err error) { 106 if r.buf == nil { 107 return 0, io.EOF 108 } 109 n, err = r.buf.Read(p) 110 if err == io.EOF { 111 r.buf.Reset(nil) 112 bufioReader32KPool.Put(r.buf) 113 r.buf = nil 114 } 115 return 116 } 117 118 func (r *bufferedReader) Peek(n int) ([]byte, error) { 119 if r.buf == nil { 120 return nil, io.EOF 121 } 122 return r.buf.Peek(n) 123 } 124 125 // DetectCompression detects the compression algorithm of the source. 126 func DetectCompression(source []byte) Compression { 127 for compression, m := range map[Compression][]byte{ 128 Gzip: {0x1F, 0x8B, 0x08}, 129 } { 130 if len(source) < len(m) { 131 // Len too short 132 continue 133 } 134 if bytes.Equal(m, source[:len(m)]) { 135 return compression 136 } 137 } 138 return Uncompressed 139 } 140 141 // DecompressStream decompresses the archive and returns a ReaderCloser with the decompressed archive. 142 func DecompressStream(archive io.Reader) (DecompressReadCloser, error) { 143 buf := newBufferedReader(archive) 144 bs, err := buf.Peek(10) 145 if err != nil && err != io.EOF { 146 // Note: we'll ignore any io.EOF error because there are some odd 147 // cases where the layer.tar file will be empty (zero bytes) and 148 // that results in an io.EOF from the Peek() call. So, in those 149 // cases we'll just treat it as a non-compressed stream and 150 // that means just create an empty layer. 151 // See Issue docker/docker#18170 152 return nil, err 153 } 154 155 switch compression := DetectCompression(bs); compression { 156 case Uncompressed: 157 return &readCloserWrapper{ 158 Reader: buf, 159 compression: compression, 160 }, nil 161 case Gzip: 162 ctx, cancel := context.WithCancel(context.Background()) 163 gzReader, err := gzipDecompress(ctx, buf) 164 if err != nil { 165 cancel() 166 return nil, err 167 } 168 169 return &readCloserWrapper{ 170 Reader: gzReader, 171 compression: compression, 172 closer: func() error { 173 cancel() 174 return gzReader.Close() 175 }, 176 }, nil 177 178 default: 179 return nil, fmt.Errorf("unsupported compression format %s", (&compression).Extension()) 180 } 181 } 182 183 // CompressStream compresses the dest with specified compression algorithm. 184 func CompressStream(dest io.Writer, compression Compression) (io.WriteCloser, error) { 185 switch compression { 186 case Uncompressed: 187 return &writeCloserWrapper{dest, nil}, nil 188 case Gzip: 189 return gzip.NewWriter(dest), nil 190 default: 191 return nil, fmt.Errorf("unsupported compression format %s", (&compression).Extension()) 192 } 193 } 194 195 // Extension returns the extension of a file that uses the specified compression algorithm. 196 func (compression *Compression) Extension() string { 197 switch *compression { 198 case Gzip: 199 return "gz" 200 } 201 return "" 202 } 203 204 func gzipDecompress(ctx context.Context, buf io.Reader) (io.ReadCloser, error) { 205 initPigz.Do(func() { 206 if unpigzPath = detectPigz(); unpigzPath != "" { 207 log.L.Debug("using pigz for decompression") 208 } 209 }) 210 211 if unpigzPath == "" { 212 return gzip.NewReader(buf) 213 } 214 215 return cmdStream(exec.CommandContext(ctx, unpigzPath, "-d", "-c"), buf) 216 } 217 218 func cmdStream(cmd *exec.Cmd, in io.Reader) (io.ReadCloser, error) { 219 reader, writer := io.Pipe() 220 221 cmd.Stdin = in 222 cmd.Stdout = writer 223 224 var errBuf bytes.Buffer 225 cmd.Stderr = &errBuf 226 227 if err := cmd.Start(); err != nil { 228 return nil, err 229 } 230 231 go func() { 232 if err := cmd.Wait(); err != nil { 233 writer.CloseWithError(fmt.Errorf("%s: %s", err, errBuf.String())) 234 } else { 235 writer.Close() 236 } 237 }() 238 239 return reader, nil 240 } 241 242 func detectPigz() string { 243 path, err := exec.LookPath("unpigz") 244 if err != nil { 245 log.L.WithError(err).Debug("unpigz not found, falling back to go gzip") 246 return "" 247 } 248 249 // Check if pigz disabled via CONTAINERD_DISABLE_PIGZ env variable 250 value := os.Getenv(disablePigzEnv) 251 if value == "" { 252 return path 253 } 254 255 disable, err := strconv.ParseBool(value) 256 if err != nil { 257 log.L.WithError(err).Warnf("could not parse %s: %s", disablePigzEnv, value) 258 return path 259 } 260 261 if disable { 262 return "" 263 } 264 265 return path 266 }