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  }