storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/metacache-walk.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2020 MinIO, Inc. 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 cmd 18 19 import ( 20 "context" 21 "io" 22 "net/http" 23 "net/url" 24 "os" 25 "sort" 26 "strconv" 27 "strings" 28 29 "github.com/gorilla/mux" 30 31 "storj.io/minio/cmd/logger" 32 xioutil "storj.io/minio/pkg/ioutil" 33 ) 34 35 // WalkDirOptions provides options for WalkDir operations. 36 type WalkDirOptions struct { 37 // Bucket to scanner 38 Bucket string 39 40 // Directory inside the bucket. 41 BaseDir string 42 43 // Do a full recursive scan. 44 Recursive bool 45 46 // ReportNotFound will return errFileNotFound if all disks reports the BaseDir cannot be found. 47 ReportNotFound bool 48 49 // FilterPrefix will only return results with given prefix within folder. 50 // Should never contain a slash. 51 FilterPrefix string 52 53 // ForwardTo will forward to the given object path. 54 ForwardTo string 55 } 56 57 // WalkDir will traverse a directory and return all entries found. 58 // On success a sorted meta cache stream will be returned. 59 // Metadata has data stripped, if any. 60 func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) error { 61 // Verify if volume is valid and it exists. 62 volumeDir, err := s.getVolDir(opts.Bucket) 63 if err != nil { 64 return err 65 } 66 67 // Stat a volume entry. 68 _, err = os.Lstat(volumeDir) 69 if err != nil { 70 if osIsNotExist(err) { 71 return errVolumeNotFound 72 } else if isSysErrIO(err) { 73 return errFaultyDisk 74 } 75 return err 76 } 77 78 // Use a small block size to start sending quickly 79 w := newMetacacheWriter(wr, 16<<10) 80 defer w.Close() 81 out, err := w.stream() 82 if err != nil { 83 return err 84 } 85 defer close(out) 86 87 // Fast exit track to check if we are listing an object with 88 // a trailing slash, this will avoid to list the object content. 89 if HasSuffix(opts.BaseDir, SlashSeparator) { 90 metadata, err := xioutil.ReadFile(pathJoin(volumeDir, 91 opts.BaseDir[:len(opts.BaseDir)-1]+globalDirSuffix, 92 xlStorageFormatFile)) 93 if err == nil { 94 // if baseDir is already a directory object, consider it 95 // as part of the list call, this is a AWS S3 specific 96 // behavior. 97 out <- metaCacheEntry{ 98 name: opts.BaseDir, 99 metadata: xlMetaV2TrimData(metadata), 100 } 101 } else { 102 if st, err := os.Lstat(pathJoin(volumeDir, opts.BaseDir, xlStorageFormatFile)); err == nil && st.Mode().IsRegular() { 103 return errFileNotFound 104 } 105 } 106 } 107 108 prefix := opts.FilterPrefix 109 forward := opts.ForwardTo 110 var scanDir func(path string) error 111 scanDir = func(current string) error { 112 if contextCanceled(ctx) { 113 return ctx.Err() 114 } 115 entries, err := s.ListDir(ctx, opts.Bucket, current, -1) 116 if err != nil { 117 // Folder could have gone away in-between 118 if err != errVolumeNotFound && err != errFileNotFound { 119 logger.LogIf(ctx, err) 120 } 121 if opts.ReportNotFound && err == errFileNotFound && current == opts.BaseDir { 122 return errFileNotFound 123 } 124 // Forward some errors? 125 return nil 126 } 127 dirObjects := make(map[string]struct{}) 128 for i, entry := range entries { 129 if len(prefix) > 0 && !strings.HasPrefix(entry, prefix) { 130 continue 131 } 132 if len(forward) > 0 && entry < forward { 133 continue 134 } 135 if strings.HasSuffix(entry, slashSeparator) { 136 if strings.HasSuffix(entry, globalDirSuffixWithSlash) { 137 // Add without extension so it is sorted correctly. 138 entry = strings.TrimSuffix(entry, globalDirSuffixWithSlash) + slashSeparator 139 dirObjects[entry] = struct{}{} 140 entries[i] = entry 141 continue 142 } 143 // Trim slash, maybe compiler is clever? 144 entries[i] = entries[i][:len(entry)-1] 145 continue 146 } 147 // Do do not retain the file. 148 entries[i] = "" 149 150 if contextCanceled(ctx) { 151 return ctx.Err() 152 } 153 // If root was an object return it as such. 154 if HasSuffix(entry, xlStorageFormatFile) { 155 var meta metaCacheEntry 156 meta.metadata, err = xioutil.ReadFile(pathJoin(volumeDir, current, entry)) 157 if err != nil { 158 logger.LogIf(ctx, err) 159 continue 160 } 161 meta.metadata = xlMetaV2TrimData(meta.metadata) 162 meta.name = strings.TrimSuffix(entry, xlStorageFormatFile) 163 meta.name = strings.TrimSuffix(meta.name, SlashSeparator) 164 meta.name = pathJoin(current, meta.name) 165 meta.name = decodeDirObject(meta.name) 166 out <- meta 167 return nil 168 } 169 // Check legacy. 170 if HasSuffix(entry, xlStorageFormatFileV1) { 171 var meta metaCacheEntry 172 meta.metadata, err = xioutil.ReadFile(pathJoin(volumeDir, current, entry)) 173 if err != nil { 174 logger.LogIf(ctx, err) 175 continue 176 } 177 meta.name = strings.TrimSuffix(entry, xlStorageFormatFileV1) 178 meta.name = strings.TrimSuffix(meta.name, SlashSeparator) 179 meta.name = pathJoin(current, meta.name) 180 out <- meta 181 return nil 182 } 183 // Skip all other files. 184 } 185 186 // Process in sort order. 187 sort.Strings(entries) 188 dirStack := make([]string, 0, 5) 189 prefix = "" // Remove prefix after first level. 190 191 for _, entry := range entries { 192 if entry == "" { 193 continue 194 } 195 if contextCanceled(ctx) { 196 return ctx.Err() 197 } 198 meta := metaCacheEntry{name: PathJoin(current, entry)} 199 200 // If directory entry on stack before this, pop it now. 201 for len(dirStack) > 0 && dirStack[len(dirStack)-1] < meta.name { 202 pop := dirStack[len(dirStack)-1] 203 out <- metaCacheEntry{name: pop} 204 if opts.Recursive { 205 // Scan folder we found. Should be in correct sort order where we are. 206 forward = "" 207 if len(opts.ForwardTo) > 0 && strings.HasPrefix(opts.ForwardTo, pop) { 208 forward = strings.TrimPrefix(opts.ForwardTo, pop) 209 } 210 logger.LogIf(ctx, scanDir(pop)) 211 } 212 dirStack = dirStack[:len(dirStack)-1] 213 } 214 215 // All objects will be returned as directories, there has been no object check yet. 216 // Check it by attempting to read metadata. 217 _, isDirObj := dirObjects[entry] 218 if isDirObj { 219 meta.name = meta.name[:len(meta.name)-1] + globalDirSuffixWithSlash 220 } 221 222 meta.metadata, err = xioutil.ReadFile(pathJoin(volumeDir, meta.name, xlStorageFormatFile)) 223 switch { 224 case err == nil: 225 // It was an object 226 if isDirObj { 227 meta.name = strings.TrimSuffix(meta.name, globalDirSuffixWithSlash) + slashSeparator 228 } 229 out <- meta 230 case osIsNotExist(err): 231 meta.metadata, err = xioutil.ReadFile(pathJoin(volumeDir, meta.name, xlStorageFormatFileV1)) 232 if err == nil { 233 // Maybe rename? Would make it inconsistent across disks though. 234 // os.Rename(pathJoin(volumeDir, meta.name, xlStorageFormatFileV1), pathJoin(volumeDir, meta.name, xlStorageFormatFile)) 235 // It was an object 236 out <- meta 237 continue 238 } 239 240 // NOT an object, append to stack (with slash) 241 // If dirObject, but no metadata (which is unexpected) we skip it. 242 if !isDirObj { 243 if !isDirEmpty(pathJoin(volumeDir, meta.name+slashSeparator)) { 244 dirStack = append(dirStack, meta.name+slashSeparator) 245 } 246 } 247 case isSysErrNotDir(err): 248 // skip 249 default: 250 logger.LogIf(ctx, err) 251 } 252 } 253 // If directory entry left on stack, pop it now. 254 for len(dirStack) > 0 { 255 pop := dirStack[len(dirStack)-1] 256 out <- metaCacheEntry{name: pop} 257 if opts.Recursive { 258 // Scan folder we found. Should be in correct sort order where we are. 259 forward = "" 260 if len(opts.ForwardTo) > 0 && strings.HasPrefix(opts.ForwardTo, pop) { 261 forward = strings.TrimPrefix(opts.ForwardTo, pop) 262 } 263 logger.LogIf(ctx, scanDir(pop)) 264 } 265 dirStack = dirStack[:len(dirStack)-1] 266 } 267 return nil 268 } 269 270 // Stream output. 271 return scanDir(opts.BaseDir) 272 } 273 274 func (p *xlStorageDiskIDCheck) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) error { 275 defer p.updateStorageMetrics(storageMetricWalkDir)() 276 if err := p.checkDiskStale(); err != nil { 277 return err 278 } 279 return p.storage.WalkDir(ctx, opts, wr) 280 } 281 282 // WalkDir will traverse a directory and return all entries found. 283 // On success a meta cache stream will be returned, that should be closed when done. 284 func (client *storageRESTClient) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) error { 285 values := make(url.Values) 286 values.Set(storageRESTVolume, opts.Bucket) 287 values.Set(storageRESTDirPath, opts.BaseDir) 288 values.Set(storageRESTRecursive, strconv.FormatBool(opts.Recursive)) 289 values.Set(storageRESTReportNotFound, strconv.FormatBool(opts.ReportNotFound)) 290 values.Set(storageRESTPrefixFilter, opts.FilterPrefix) 291 values.Set(storageRESTForwardFilter, opts.ForwardTo) 292 respBody, err := client.call(ctx, storageRESTMethodWalkDir, values, nil, -1) 293 if err != nil { 294 logger.LogIf(ctx, err) 295 return err 296 } 297 return waitForHTTPStream(respBody, wr) 298 } 299 300 // WalkDirHandler - remote caller to list files and folders in a requested directory path. 301 func (s *storageRESTServer) WalkDirHandler(w http.ResponseWriter, r *http.Request) { 302 if !s.IsValid(w, r) { 303 return 304 } 305 vars := mux.Vars(r) 306 volume := vars[storageRESTVolume] 307 dirPath := vars[storageRESTDirPath] 308 recursive, err := strconv.ParseBool(vars[storageRESTRecursive]) 309 if err != nil { 310 s.WriteErrorResponse(w, err) 311 return 312 } 313 314 var reportNotFound bool 315 if v := vars[storageRESTReportNotFound]; v != "" { 316 reportNotFound, err = strconv.ParseBool(v) 317 if err != nil { 318 s.WriteErrorResponse(w, err) 319 return 320 } 321 } 322 323 prefix := r.URL.Query().Get(storageRESTPrefixFilter) 324 forward := r.URL.Query().Get(storageRESTForwardFilter) 325 writer := streamHTTPResponse(w) 326 writer.CloseWithError(s.storage.WalkDir(r.Context(), WalkDirOptions{ 327 Bucket: volume, 328 BaseDir: dirPath, 329 Recursive: recursive, 330 ReportNotFound: reportNotFound, 331 FilterPrefix: prefix, 332 ForwardTo: forward, 333 }, writer)) 334 }