github.com/zntrio/harp/v2@v2.0.9/pkg/sdk/fsutil/targzfs/fs.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package targzfs 19 20 import ( 21 "bytes" 22 "fmt" 23 "io/fs" 24 "sort" 25 "strings" 26 27 "github.com/gobwas/glob" 28 ) 29 30 var ( 31 // Block decompression if the TAR archive is larger than 25MB. 32 maxDecompressedSize = int64(25 * 1024 * 1024) 33 // Maximum file size to load in memory (2MB). 34 maxFileSize = int64(2 * 1024 * 1024) 35 // Block decompression if the archive has more than 1k files. 36 maxFileCount = 1000 37 ) 38 39 type tarGzFs struct { 40 files map[string]*tarEntry 41 rootEntries []fs.DirEntry 42 rootEntry *tarEntry 43 } 44 45 var _ fs.FS = (*tarGzFs)(nil) 46 47 // Open opens the named file. 48 func (gzfs *tarGzFs) Open(name string) (fs.File, error) { 49 // Shortcut if the file is '.' 50 if name == "." { 51 if gzfs.rootEntries == nil { 52 return &rootFile{}, nil 53 } 54 return &tarFile{ 55 tarEntry: *gzfs.rootEntry, 56 r: bytes.NewReader(gzfs.rootEntry.b), 57 readDirPos: 0, 58 }, nil 59 } 60 61 // Lookup file. 62 f, err := gzfs.get(name, "open") 63 if err != nil { 64 return nil, err 65 } 66 67 // Wrapped file content 68 return &tarFile{ 69 tarEntry: *f, 70 r: bytes.NewReader(f.b), 71 readDirPos: 0, 72 }, nil 73 } 74 75 var _ fs.ReadDirFS = (*tarGzFs)(nil) 76 77 // ReadDir is used to enumerate all files from a directory. 78 func (gzfs *tarGzFs) ReadDir(name string) ([]fs.DirEntry, error) { 79 // Shortcut if the file is '.' 80 if name == "." { 81 return gzfs.rootEntries, nil 82 } 83 84 // Lookup file. 85 e, err := gzfs.get(name, "readdir") 86 if err != nil { 87 return nil, err 88 } 89 90 // Only directory should be used. 91 if !e.IsDir() { 92 return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid} 93 } 94 95 // Sort results by name. 96 sort.Slice(e.entries, func(i, j int) bool { 97 return e.entries[i].Name() < e.entries[j].Name() 98 }) 99 100 // Return file entries. 101 return e.entries, nil 102 } 103 104 var _ fs.ReadFileFS = (*tarGzFs)(nil) 105 106 // ReadFile is used to retrieve directly the file content. 107 func (gzfs *tarGzFs) ReadFile(name string) ([]byte, error) { 108 // Shortcut if the file is '.' 109 if name == "." { 110 return nil, &fs.PathError{Op: "readfile", Path: name, Err: fs.ErrInvalid} 111 } 112 113 // Lookup file. 114 e, err := gzfs.get(name, "readfile") 115 if err != nil { 116 return nil, err 117 } 118 119 // Entry must be a file 120 if e.IsDir() { 121 return nil, &fs.PathError{Op: "readfile", Path: name, Err: fs.ErrInvalid} 122 } 123 124 // Copy content 125 buf := make([]byte, len(e.b)) 126 copy(buf, e.b) 127 128 // No error 129 return buf, nil 130 } 131 132 var _ fs.StatFS = (*tarGzFs)(nil) 133 134 // Stat query the in-memory file system to get file info. 135 func (gzfs *tarGzFs) Stat(name string) (fs.FileInfo, error) { 136 // Shortcut if the file is '.' 137 if name == "." { 138 if gzfs.rootEntry == nil { 139 return &rootFile{}, nil 140 } 141 142 // Return root fileinfo 143 return gzfs.rootEntry.Info() 144 } 145 146 // Lookup file. 147 e, err := gzfs.get(name, "stat") 148 if err != nil { 149 return nil, err 150 } 151 152 // Return fileinfo 153 return e.h.FileInfo(), nil 154 } 155 156 var _ fs.GlobFS = (*tarGzFs)(nil) 157 158 func (gzfs *tarGzFs) Glob(pattern string) (matches []string, _ error) { 159 // Compile pattern 160 g, err := glob.Compile(pattern) 161 if err != nil { 162 return nil, fmt.Errorf("unable to compile glob pattern: %w", err) 163 } 164 165 // Iterate over file names 166 for name := range gzfs.files { 167 // Check if pattern match the file name 168 if g.Match(name) { 169 matches = append(matches, name) 170 } 171 } 172 173 // Return results 174 return 175 } 176 177 var _ fs.SubFS = (*tarGzFs)(nil) 178 179 func (gzfs *tarGzFs) Sub(dir string) (fs.FS, error) { 180 if dir == "." { 181 return gzfs, nil 182 } 183 184 // Lookup directory 185 e, err := gzfs.get(dir, "sub") 186 if err != nil { 187 return nil, err 188 } 189 190 // Must be a directory 191 if !e.IsDir() { 192 return nil, &fs.PathError{Op: "sub", Path: dir, Err: fs.ErrInvalid} 193 } 194 195 // Create a sub-filesystem 196 subfs := &tarGzFs{ 197 files: make(map[string]*tarEntry), 198 rootEntries: e.entries, 199 rootEntry: e, 200 } 201 202 // Copy files and remove directory prefix. 203 prefix := dir + "/" 204 for name, file := range gzfs.files { 205 if strings.HasPrefix(name, prefix) { 206 subfs.files[strings.TrimPrefix(name, prefix)] = file 207 } 208 } 209 210 // No error 211 return subfs, nil 212 } 213 214 // ----------------------------------------------------------------------------- 215 216 func (gzfs *tarGzFs) get(name, op string) (*tarEntry, error) { 217 if !fs.ValidPath(name) { 218 return nil, &fs.PathError{Op: op, Path: name, Err: fs.ErrInvalid} 219 } 220 221 // Lookup file 222 e, ok := gzfs.files[name] 223 if !ok { 224 return nil, &fs.PathError{Op: op, Path: name, Err: fs.ErrNotExist} 225 } 226 227 return e, nil 228 }