github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/os-reliable.go (about)

     1  // Copyright (c) 2015-2021 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  	"fmt"
    22  	"os"
    23  	"path"
    24  )
    25  
    26  // Wrapper functions to os.RemoveAll, which calls reliableRemoveAll
    27  // this is to ensure that if there is a racy parent directory
    28  // create in between we can simply retry the operation.
    29  func removeAll(dirPath string) (err error) {
    30  	if dirPath == "" {
    31  		return errInvalidArgument
    32  	}
    33  
    34  	if err = checkPathLength(dirPath); err != nil {
    35  		return err
    36  	}
    37  
    38  	if err = reliableRemoveAll(dirPath); err != nil {
    39  		switch {
    40  		case isSysErrNotDir(err):
    41  			// File path cannot be verified since one of
    42  			// the parents is a file.
    43  			return errFileAccessDenied
    44  		case isSysErrPathNotFound(err):
    45  			// This is a special case should be handled only for
    46  			// windows, because windows API does not return "not a
    47  			// directory" error message. Handle this specifically
    48  			// here.
    49  			return errFileAccessDenied
    50  		}
    51  	}
    52  	return err
    53  }
    54  
    55  // Reliably retries os.RemoveAll if for some reason os.RemoveAll returns
    56  // syscall.ENOTEMPTY (children has files).
    57  func reliableRemoveAll(dirPath string) (err error) {
    58  	i := 0
    59  	for {
    60  		// Removes all the directories and files.
    61  		if err = RemoveAll(dirPath); err != nil {
    62  			// Retry only for the first retryable error.
    63  			if isSysErrNotEmpty(err) && i == 0 {
    64  				i++
    65  				continue
    66  			}
    67  		}
    68  		break
    69  	}
    70  	return err
    71  }
    72  
    73  // Wrapper functions to os.MkdirAll, which calls reliableMkdirAll
    74  // this is to ensure that if there is a racy parent directory
    75  // delete in between we can simply retry the operation.
    76  func mkdirAll(dirPath string, mode os.FileMode, baseDir string) (err error) {
    77  	if dirPath == "" {
    78  		return errInvalidArgument
    79  	}
    80  
    81  	if err = checkPathLength(dirPath); err != nil {
    82  		return err
    83  	}
    84  
    85  	if err = reliableMkdirAll(dirPath, mode, baseDir); err != nil {
    86  		// File path cannot be verified since one of the parents is a file.
    87  		if isSysErrNotDir(err) {
    88  			return errFileAccessDenied
    89  		} else if isSysErrPathNotFound(err) {
    90  			// This is a special case should be handled only for
    91  			// windows, because windows API does not return "not a
    92  			// directory" error message. Handle this specifically here.
    93  			return errFileAccessDenied
    94  		}
    95  		return osErrToFileErr(err)
    96  	}
    97  
    98  	return nil
    99  }
   100  
   101  // Reliably retries os.MkdirAll if for some reason os.MkdirAll returns
   102  // syscall.ENOENT (parent does not exist).
   103  func reliableMkdirAll(dirPath string, mode os.FileMode, baseDir string) (err error) {
   104  	i := 0
   105  	for {
   106  		// Creates all the parent directories, with mode 0777 mkdir honors system umask.
   107  		if err = osMkdirAll(dirPath, mode, baseDir); err != nil {
   108  			// Retry only for the first retryable error.
   109  			if osIsNotExist(err) && i == 0 {
   110  				i++
   111  				continue
   112  			}
   113  		}
   114  		break
   115  	}
   116  	return err
   117  }
   118  
   119  // Wrapper function to os.Rename, which calls reliableMkdirAll
   120  // and reliableRenameAll. This is to ensure that if there is a
   121  // racy parent directory delete in between we can simply retry
   122  // the operation.
   123  func renameAll(srcFilePath, dstFilePath, baseDir string) (err error) {
   124  	if srcFilePath == "" || dstFilePath == "" {
   125  		return errInvalidArgument
   126  	}
   127  
   128  	if err = checkPathLength(srcFilePath); err != nil {
   129  		return err
   130  	}
   131  	if err = checkPathLength(dstFilePath); err != nil {
   132  		return err
   133  	}
   134  
   135  	if err = reliableRename(srcFilePath, dstFilePath, baseDir); err != nil {
   136  		switch {
   137  		case isSysErrNotDir(err) && !osIsNotExist(err):
   138  			// Windows can have both isSysErrNotDir(err) and osIsNotExist(err) returning
   139  			// true if the source file path contains an non-existent directory. In that case,
   140  			// we want to return errFileNotFound instead, which will honored in subsequent
   141  			// switch cases
   142  			return errFileAccessDenied
   143  		case isSysErrPathNotFound(err):
   144  			// This is a special case should be handled only for
   145  			// windows, because windows API does not return "not a
   146  			// directory" error message. Handle this specifically here.
   147  			return errFileAccessDenied
   148  		case isSysErrCrossDevice(err):
   149  			return fmt.Errorf("%w (%s)->(%s)", errCrossDeviceLink, srcFilePath, dstFilePath)
   150  		case osIsNotExist(err):
   151  			return errFileNotFound
   152  		case osIsExist(err):
   153  			// This is returned only when destination is a directory and we
   154  			// are attempting a rename from file to directory.
   155  			return errIsNotRegular
   156  		default:
   157  			return err
   158  		}
   159  	}
   160  	return nil
   161  }
   162  
   163  // Reliably retries os.RenameAll if for some reason os.RenameAll returns
   164  // syscall.ENOENT (parent does not exist).
   165  func reliableRename(srcFilePath, dstFilePath, baseDir string) (err error) {
   166  	if err = reliableMkdirAll(path.Dir(dstFilePath), 0o777, baseDir); err != nil {
   167  		return err
   168  	}
   169  
   170  	i := 0
   171  	for {
   172  		// After a successful parent directory create attempt a renameAll.
   173  		if err = Rename(srcFilePath, dstFilePath); err != nil {
   174  			// Retry only for the first retryable error.
   175  			if osIsNotExist(err) && i == 0 {
   176  				i++
   177  				continue
   178  			}
   179  		}
   180  		break
   181  	}
   182  	return err
   183  }