github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/metacache-walk.go (about) 1 // Copyright (c) 2015-2023 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "context" 22 "io" 23 "sort" 24 "strings" 25 26 "github.com/minio/minio/internal/grid" 27 xioutil "github.com/minio/minio/internal/ioutil" 28 "github.com/minio/minio/internal/logger" 29 "github.com/valyala/bytebufferpool" 30 ) 31 32 //go:generate msgp -file $GOFILE 33 34 // WalkDirOptions provides options for WalkDir operations. 35 type WalkDirOptions struct { 36 // Bucket to scanner 37 Bucket string 38 39 // Directory inside the bucket. 40 BaseDir string 41 42 // Do a full recursive scan. 43 Recursive bool 44 45 // ReportNotFound will return errFileNotFound if all disks reports the BaseDir cannot be found. 46 ReportNotFound bool 47 48 // FilterPrefix will only return results with given prefix within folder. 49 // Should never contain a slash. 50 FilterPrefix string 51 52 // ForwardTo will forward to the given object path. 53 ForwardTo string 54 55 // Limit the number of returned objects if > 0. 56 Limit int 57 58 // DiskID contains the disk ID of the disk. 59 // Leave empty to not check disk ID. 60 DiskID string 61 } 62 63 // WalkDir will traverse a directory and return all entries found. 64 // On success a sorted meta cache stream will be returned. 65 // Metadata has data stripped, if any. 66 func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) (err error) { 67 // Verify if volume is valid and it exists. 68 volumeDir, err := s.getVolDir(opts.Bucket) 69 if err != nil { 70 return err 71 } 72 73 if !skipAccessChecks(opts.Bucket) { 74 // Stat a volume entry. 75 if err = Access(volumeDir); err != nil { 76 return convertAccessError(err, errVolumeAccessDenied) 77 } 78 } 79 80 s.RLock() 81 legacy := s.formatLegacy 82 s.RUnlock() 83 84 // Use a small block size to start sending quickly 85 w := newMetacacheWriter(wr, 16<<10) 86 w.reuseBlocks = true // We are not sharing results, so reuse buffers. 87 defer w.Close() 88 out, err := w.stream() 89 if err != nil { 90 return err 91 } 92 defer xioutil.SafeClose(out) 93 var objsReturned int 94 95 objReturned := func(metadata []byte) { 96 if opts.Limit <= 0 { 97 return 98 } 99 if m, _, _ := isIndexedMetaV2(metadata); m != nil && !m.AllHidden(true) { 100 objsReturned++ 101 } 102 } 103 send := func(entry metaCacheEntry) error { 104 objReturned(entry.metadata) 105 select { 106 case <-ctx.Done(): 107 return ctx.Err() 108 case out <- entry: 109 } 110 return nil 111 } 112 113 // Fast exit track to check if we are listing an object with 114 // a trailing slash, this will avoid to list the object content. 115 if HasSuffix(opts.BaseDir, SlashSeparator) { 116 metadata, err := s.readMetadata(ctx, pathJoin(volumeDir, 117 opts.BaseDir[:len(opts.BaseDir)-1]+globalDirSuffix, 118 xlStorageFormatFile)) 119 diskHealthCheckOK(ctx, err) 120 if err == nil { 121 // if baseDir is already a directory object, consider it 122 // as part of the list call, this is AWS S3 specific 123 // behavior. 124 if err := send(metaCacheEntry{ 125 name: opts.BaseDir, 126 metadata: metadata, 127 }); err != nil { 128 return err 129 } 130 } else { 131 st, sterr := Lstat(pathJoin(volumeDir, opts.BaseDir, xlStorageFormatFile)) 132 if sterr == nil && st.Mode().IsRegular() { 133 return errFileNotFound 134 } 135 } 136 } 137 138 prefix := opts.FilterPrefix 139 var scanDir func(path string) error 140 141 scanDir = func(current string) error { 142 // Skip forward, if requested... 143 sb := bytebufferpool.Get() 144 defer func() { 145 sb.Reset() 146 bytebufferpool.Put(sb) 147 }() 148 149 forward := "" 150 if len(opts.ForwardTo) > 0 && strings.HasPrefix(opts.ForwardTo, current) { 151 forward = strings.TrimPrefix(opts.ForwardTo, current) 152 // Trim further directories and trailing slash. 153 if idx := strings.IndexByte(forward, '/'); idx > 0 { 154 forward = forward[:idx] 155 } 156 } 157 if contextCanceled(ctx) { 158 return ctx.Err() 159 } 160 if opts.Limit > 0 && objsReturned >= opts.Limit { 161 return nil 162 } 163 164 if s.walkMu != nil { 165 s.walkMu.Lock() 166 } 167 entries, err := s.ListDir(ctx, "", opts.Bucket, current, -1) 168 if s.walkMu != nil { 169 s.walkMu.Unlock() 170 } 171 if err != nil { 172 // Folder could have gone away in-between 173 if err != errVolumeNotFound && err != errFileNotFound { 174 logger.LogOnceIf(ctx, err, "metacache-walk-scan-dir") 175 } 176 if opts.ReportNotFound && err == errFileNotFound && current == opts.BaseDir { 177 err = errFileNotFound 178 } else { 179 err = nil 180 } 181 diskHealthCheckOK(ctx, err) 182 return err 183 } 184 diskHealthCheckOK(ctx, err) 185 if len(entries) == 0 { 186 return nil 187 } 188 dirObjects := make(map[string]struct{}) 189 190 // Avoid a bunch of cleanup when joining. 191 current = strings.Trim(current, SlashSeparator) 192 for i, entry := range entries { 193 if opts.Limit > 0 && objsReturned >= opts.Limit { 194 return nil 195 } 196 if len(prefix) > 0 && !strings.HasPrefix(entry, prefix) { 197 // Do not retain the file, since it doesn't 198 // match the prefix. 199 entries[i] = "" 200 continue 201 } 202 if len(forward) > 0 && entry < forward { 203 // Do not retain the file, since its 204 // lexially smaller than 'forward' 205 entries[i] = "" 206 continue 207 } 208 if hasSuffixByte(entry, SlashSeparatorChar) { 209 if strings.HasSuffix(entry, globalDirSuffixWithSlash) { 210 // Add without extension so it is sorted correctly. 211 entry = strings.TrimSuffix(entry, globalDirSuffixWithSlash) + slashSeparator 212 dirObjects[entry] = struct{}{} 213 entries[i] = entry 214 continue 215 } 216 // Trim slash, since we don't know if this is folder or object. 217 entries[i] = entries[i][:len(entry)-1] 218 continue 219 } 220 // Do not retain the file. 221 entries[i] = "" 222 223 if contextCanceled(ctx) { 224 return ctx.Err() 225 } 226 // If root was an object return it as such. 227 if HasSuffix(entry, xlStorageFormatFile) { 228 var meta metaCacheEntry 229 if s.walkReadMu != nil { 230 s.walkReadMu.Lock() 231 } 232 meta.metadata, err = s.readMetadata(ctx, pathJoinBuf(sb, volumeDir, current, entry)) 233 if s.walkReadMu != nil { 234 s.walkReadMu.Unlock() 235 } 236 diskHealthCheckOK(ctx, err) 237 if err != nil { 238 // It is totally possible that xl.meta was overwritten 239 // while being concurrently listed at the same time in 240 // such scenarios the 'xl.meta' might get truncated 241 if !IsErrIgnored(err, io.EOF, io.ErrUnexpectedEOF) { 242 logger.LogOnceIf(ctx, err, "metacache-walk-read-metadata") 243 } 244 continue 245 } 246 meta.name = strings.TrimSuffix(entry, xlStorageFormatFile) 247 meta.name = strings.TrimSuffix(meta.name, SlashSeparator) 248 meta.name = pathJoinBuf(sb, current, meta.name) 249 meta.name = decodeDirObject(meta.name) 250 251 return send(meta) 252 } 253 // Check legacy. 254 if HasSuffix(entry, xlStorageFormatFileV1) && legacy { 255 var meta metaCacheEntry 256 meta.metadata, err = xioutil.ReadFile(pathJoinBuf(sb, volumeDir, current, entry)) 257 diskHealthCheckOK(ctx, err) 258 if err != nil { 259 if !IsErrIgnored(err, io.EOF, io.ErrUnexpectedEOF) { 260 logger.LogIf(ctx, err) 261 } 262 continue 263 } 264 meta.name = strings.TrimSuffix(entry, xlStorageFormatFileV1) 265 meta.name = strings.TrimSuffix(meta.name, SlashSeparator) 266 meta.name = pathJoinBuf(sb, current, meta.name) 267 268 return send(meta) 269 } 270 // Skip all other files. 271 } 272 273 // Process in sort order. 274 sort.Strings(entries) 275 dirStack := make([]string, 0, 5) 276 prefix = "" // Remove prefix after first level as we have already filtered the list. 277 if len(forward) > 0 { 278 // Conservative forwarding. Entries may be either objects or prefixes. 279 for i, entry := range entries { 280 if entry >= forward || strings.HasPrefix(forward, entry) { 281 entries = entries[i:] 282 break 283 } 284 } 285 } 286 287 for _, entry := range entries { 288 if opts.Limit > 0 && objsReturned >= opts.Limit { 289 return nil 290 } 291 if entry == "" { 292 continue 293 } 294 if contextCanceled(ctx) { 295 return ctx.Err() 296 } 297 meta := metaCacheEntry{name: pathJoinBuf(sb, current, entry)} 298 299 // If directory entry on stack before this, pop it now. 300 for len(dirStack) > 0 && dirStack[len(dirStack)-1] < meta.name { 301 pop := dirStack[len(dirStack)-1] 302 select { 303 case <-ctx.Done(): 304 return ctx.Err() 305 case out <- metaCacheEntry{name: pop}: 306 } 307 if opts.Recursive { 308 // Scan folder we found. Should be in correct sort order where we are. 309 err := scanDir(pop) 310 if err != nil && !IsErrIgnored(err, context.Canceled) { 311 logger.LogIf(ctx, err) 312 } 313 } 314 dirStack = dirStack[:len(dirStack)-1] 315 } 316 317 // All objects will be returned as directories, there has been no object check yet. 318 // Check it by attempting to read metadata. 319 _, isDirObj := dirObjects[entry] 320 if isDirObj { 321 meta.name = meta.name[:len(meta.name)-1] + globalDirSuffixWithSlash 322 } 323 324 if s.walkReadMu != nil { 325 s.walkReadMu.Lock() 326 } 327 meta.metadata, err = s.readMetadata(ctx, pathJoinBuf(sb, volumeDir, meta.name, xlStorageFormatFile)) 328 if s.walkReadMu != nil { 329 s.walkReadMu.Unlock() 330 } 331 diskHealthCheckOK(ctx, err) 332 switch { 333 case err == nil: 334 // It was an object 335 if isDirObj { 336 meta.name = strings.TrimSuffix(meta.name, globalDirSuffixWithSlash) + slashSeparator 337 } 338 if err := send(meta); err != nil { 339 return err 340 } 341 case osIsNotExist(err), isSysErrIsDir(err): 342 if legacy { 343 meta.metadata, err = xioutil.ReadFile(pathJoinBuf(sb, volumeDir, meta.name, xlStorageFormatFileV1)) 344 diskHealthCheckOK(ctx, err) 345 if err == nil { 346 // It was an object 347 if err := send(meta); err != nil { 348 return err 349 } 350 continue 351 } 352 } 353 354 // NOT an object, append to stack (with slash) 355 // If dirObject, but no metadata (which is unexpected) we skip it. 356 if !isDirObj { 357 if !isDirEmpty(pathJoinBuf(sb, volumeDir, meta.name)) { 358 dirStack = append(dirStack, meta.name+slashSeparator) 359 } 360 } 361 case isSysErrNotDir(err): 362 // skip 363 } 364 } 365 366 // If directory entry left on stack, pop it now. 367 for len(dirStack) > 0 { 368 if opts.Limit > 0 && objsReturned >= opts.Limit { 369 return nil 370 } 371 if contextCanceled(ctx) { 372 return ctx.Err() 373 } 374 pop := dirStack[len(dirStack)-1] 375 select { 376 case <-ctx.Done(): 377 return ctx.Err() 378 case out <- metaCacheEntry{name: pop}: 379 } 380 if opts.Recursive { 381 // Scan folder we found. Should be in correct sort order where we are. 382 logger.LogIf(ctx, scanDir(pop)) 383 } 384 dirStack = dirStack[:len(dirStack)-1] 385 } 386 return nil 387 } 388 389 // Stream output. 390 return scanDir(opts.BaseDir) 391 } 392 393 func (p *xlStorageDiskIDCheck) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) (err error) { 394 if err := p.checkID(opts.DiskID); err != nil { 395 return err 396 } 397 ctx, done, err := p.TrackDiskHealth(ctx, storageMetricWalkDir, opts.Bucket, opts.BaseDir) 398 if err != nil { 399 return err 400 } 401 defer done(&err) 402 403 return p.storage.WalkDir(ctx, opts, wr) 404 } 405 406 // WalkDir will traverse a directory and return all entries found. 407 // On success a meta cache stream will be returned, that should be closed when done. 408 func (client *storageRESTClient) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) error { 409 // Ensure remote has the same disk ID. 410 opts.DiskID = client.diskID 411 b, err := opts.MarshalMsg(grid.GetByteBuffer()[:0]) 412 if err != nil { 413 return toStorageErr(err) 414 } 415 416 st, err := client.gridConn.NewStream(ctx, grid.HandlerWalkDir, b) 417 if err != nil { 418 return toStorageErr(err) 419 } 420 return toStorageErr(st.Results(func(in []byte) error { 421 _, err := wr.Write(in) 422 return err 423 })) 424 } 425 426 // WalkDirHandler - remote caller to list files and folders in a requested directory path. 427 func (s *storageRESTServer) WalkDirHandler(ctx context.Context, payload []byte, _ <-chan []byte, out chan<- []byte) (gerr *grid.RemoteErr) { 428 var opts WalkDirOptions 429 _, err := opts.UnmarshalMsg(payload) 430 if err != nil { 431 return grid.NewRemoteErr(err) 432 } 433 434 if !s.checkID(opts.DiskID) { 435 return grid.NewRemoteErr(errDiskNotFound) 436 } 437 438 ctx, cancel := context.WithCancel(ctx) 439 defer cancel() 440 return grid.NewRemoteErr(s.getStorage().WalkDir(ctx, opts, grid.WriterToChannel(ctx, out))) 441 }