github.com/paketoio/libpak@v1.3.1/crush/crush.go (about) 1 /* 2 * Copyright 2018-2020 the original author or 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 * https://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 crush 18 19 import ( 20 "archive/tar" 21 "archive/zip" 22 "compress/gzip" 23 "fmt" 24 "io" 25 "os" 26 "path/filepath" 27 "strings" 28 29 "github.com/xi2/xz" 30 ) 31 32 type Crush struct{} 33 34 // CreateTar writes a TAR to the destination io.Writer containing the directories and files in the source folder. 35 func (c *Crush) CreateTar(destination io.Writer, source string) error { 36 t := tar.NewWriter(destination) 37 defer t.Close() 38 39 if err := filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 40 if err != nil { 41 return err 42 } 43 44 rel, err := filepath.Rel(source, path) 45 if err != nil { 46 return fmt.Errorf("uanble to calculate relative path %s -> %s: %w", source, path, err) 47 } 48 if info.IsDir() { 49 rel = fmt.Sprintf("%s/", rel) 50 } 51 52 if rel == "./" { 53 return nil 54 } 55 56 h, err := tar.FileInfoHeader(info, info.Name()) 57 if err != nil { 58 return fmt.Errorf("unable to create TAR header from %+v: %w", info, err) 59 } 60 h.Name = rel 61 62 if err := t.WriteHeader(h); err != nil { 63 return fmt.Errorf("unable to write header %+v: %w", h, err) 64 } 65 66 if info.IsDir() { 67 return nil 68 } 69 70 in, err := os.Open(path) 71 if err != nil { 72 return fmt.Errorf("unable to open %s: %w", path, err) 73 } 74 defer in.Close() 75 76 if _, err := io.Copy(t, in); err != nil { 77 return fmt.Errorf("unable to copy %s to %s: %w", path, h.Name, err) 78 } 79 80 return nil 81 }); err != nil { 82 return fmt.Errorf("unable to create tar from %s: %w", source, err) 83 } 84 85 return nil 86 } 87 88 // CreateTarGz writes a GZIP'd TAR to the destination io.Writer containing the directories and files in the source 89 // folder. 90 func (c *Crush) CreateTarGz(destination io.Writer, source string) error { 91 gz := gzip.NewWriter(destination) 92 defer gz.Close() 93 94 return c.CreateTar(gz, source) 95 } 96 97 // ExtractTar extracts source TAR file to a destination directory. An arbitrary number of top-level directory 98 // components can be stripped from each path. 99 func (c *Crush) ExtractTar(source io.Reader, destination string, stripComponents int) error { 100 t := tar.NewReader(source) 101 102 for { 103 f, err := t.Next() 104 if err != nil && err == io.EOF { 105 break 106 } else if err != nil { 107 return fmt.Errorf("unable to read TAR file: %w", err) 108 } 109 110 target := c.strippedPath(f.Name, destination, stripComponents) 111 if target == "" { 112 continue 113 } 114 115 info := f.FileInfo() 116 if info.IsDir() { 117 if err := os.MkdirAll(target, 0755); err != nil { 118 return fmt.Errorf("unable to make directory %s: %w", target, err) 119 } 120 } else if info.Mode()&os.ModeSymlink != 0 { 121 if err := c.writeSymlink(f.Linkname, target); err != nil { 122 return err 123 } 124 } else { 125 if err := c.writeFile(t, target, info.Mode()); err != nil { 126 return err 127 } 128 } 129 } 130 131 return nil 132 } 133 134 // ExtractTarGz extracts source GZIP'd TAR file to a destination directory. An arbitrary number of top-level directory 135 // components can be stripped from each path. 136 func (c *Crush) ExtractTarGz(source io.Reader, destination string, stripComponents int) error { 137 gz, err := gzip.NewReader(source) 138 if err != nil { 139 return fmt.Errorf("unable to create GZIP reader: %w", err) 140 } 141 defer gz.Close() 142 143 return c.ExtractTar(gz, destination, stripComponents) 144 } 145 146 // ExtractTarXz extracts source XZ'd TAR file to a destination directory. An arbitrary number of top-level directory 147 // components can be stripped from each path. 148 func (c *Crush) ExtractTarXz(source io.Reader, destination string, stripComponents int) error { 149 xz, err := xz.NewReader(source, 0) 150 if err != nil { 151 return fmt.Errorf("unable to create XZ reader: %w", err) 152 } 153 154 return c.ExtractTar(xz, destination, stripComponents) 155 } 156 157 // ExtractZip extracts source ZIP file to a destination directory. An arbitrary number of top-level directory 158 // components can be stripped from each path. 159 func (c *Crush) ExtractZip(source *os.File, destination string, stripComponents int) error { 160 stat, err := source.Stat() 161 if err != nil { 162 return fmt.Errorf("unable to stat %s: %w", source.Name(), err) 163 } 164 165 z, err := zip.NewReader(source, stat.Size()) 166 if err != nil { 167 return err 168 } 169 170 for _, f := range z.File { 171 target := c.strippedPath(f.Name, destination, stripComponents) 172 if target == "" { 173 continue 174 } 175 176 if f.FileInfo().IsDir() { 177 if err := os.MkdirAll(target, 0755); err != nil { 178 return err 179 } 180 } else { 181 if err := c.writeZipEntry(f, target); err != nil { 182 return err 183 } 184 } 185 } 186 187 return nil 188 } 189 190 func (Crush) strippedPath(source string, destination string, stripComponents int) string { 191 components := strings.Split(source, string(filepath.Separator)) 192 193 if len(components) <= stripComponents { 194 return "" 195 } 196 197 return filepath.Join(append([]string{destination}, components[stripComponents:]...)...) 198 } 199 200 func (Crush) writeFile(source io.Reader, path string, perm os.FileMode) error { 201 file := filepath.Dir(path) 202 if err := os.MkdirAll(file, 0755); err != nil { 203 return fmt.Errorf("unable to create directory %s: %w", file, err) 204 } 205 206 out, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm) 207 if err != nil { 208 return fmt.Errorf("unable to open file %s: %w", path, err) 209 } 210 defer out.Close() 211 212 if _, err := io.Copy(out, source); err != nil { 213 return fmt.Errorf("unable to write data to %s: %w", path, err) 214 } 215 216 return nil 217 } 218 219 func (c Crush) writeZipEntry(file *zip.File, path string) error { 220 in, err := file.Open() 221 if err != nil { 222 return fmt.Errorf("unable to open %s: %w", file.Name, err) 223 } 224 defer in.Close() 225 226 return c.writeFile(in, path, file.Mode()) 227 } 228 229 func (Crush) writeSymlink(oldName string, newName string) error { 230 file := filepath.Dir(newName) 231 if err := os.MkdirAll(file, 0755); err != nil { 232 return fmt.Errorf("unable to create directory %s: %w", file, err) 233 } 234 235 if err := os.Symlink(oldName, newName); err != nil { 236 return fmt.Errorf("unable to create '%s' as symlink to '%s': %v", newName, oldName, err) 237 } 238 239 return nil 240 }