github.com/rigado/snapd@v2.42.5-go-mod+incompatible/osutil/syncdir.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     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 General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package osutil
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"sort"
    29  )
    30  
    31  // FileState describes the expected content and meta data of a single file.
    32  type FileState struct {
    33  	Content []byte
    34  	Mode    os.FileMode
    35  }
    36  
    37  // ErrSameState is returned when the state of a file has not changed.
    38  var ErrSameState = fmt.Errorf("file state has not changed")
    39  
    40  // EnsureDirStateGlobs ensures that directory content matches expectations.
    41  //
    42  // EnsureDirStateGlobs enumerates all the files in the specified directory that
    43  // match the provided set of pattern (globs). Each enumerated file is checked
    44  // to ensure that the contents, permissions are what is desired. Unexpected
    45  // files are removed. Missing files are created and differing files are
    46  // corrected. Files not matching any pattern are ignored.
    47  //
    48  // Note that EnsureDirStateGlobs only checks for permissions and content. Other
    49  // security mechanisms, including file ownership and extended attributes are
    50  // *not* supported.
    51  //
    52  // The content map describes each of the files that are intended to exist in
    53  // the directory.  Map keys must be file names relative to the directory.
    54  // Sub-directories in the name are not allowed.
    55  //
    56  // If writing any of the files fails, EnsureDirStateGlobs switches to erase mode
    57  // where *all* of the files managed by the glob pattern are removed (including
    58  // those that may have been already written). The return value is an empty list
    59  // of changed files, the real list of removed files and the first error.
    60  //
    61  // If an error happens while removing files then such a file is not removed but
    62  // the removal continues until the set of managed files matching the glob is
    63  // exhausted.
    64  //
    65  // In all cases, the function returns the first error it has encountered.
    66  func EnsureDirStateGlobs(dir string, globs []string, content map[string]*FileState) (changed, removed []string, err error) {
    67  	// Check syntax before doing anything.
    68  	if _, index, err := matchAny(globs, "foo"); err != nil {
    69  		return nil, nil, fmt.Errorf("internal error: EnsureDirState got invalid pattern %q: %s", globs[index], err)
    70  	}
    71  	for baseName := range content {
    72  		if filepath.Base(baseName) != baseName {
    73  			return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which has a path component", baseName)
    74  		}
    75  		if ok, _, _ := matchAny(globs, baseName); !ok {
    76  			if len(globs) == 1 {
    77  				return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which doesn't match the glob pattern %q", baseName, globs[0])
    78  			}
    79  			return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which doesn't match any glob patterns %q", baseName, globs)
    80  		}
    81  	}
    82  	// Change phase (create/change files described by content)
    83  	var firstErr error
    84  	for baseName, fileState := range content {
    85  		filePath := filepath.Join(dir, baseName)
    86  		err := EnsureFileState(filePath, fileState)
    87  		if err == ErrSameState {
    88  			continue
    89  		}
    90  		if err != nil {
    91  			// On write failure, switch to erase mode. Desired content is set
    92  			// to nothing (no content) changed files are forgotten and the
    93  			// writing loop stops. The subsequent erase loop will remove all
    94  			// the managed content.
    95  			firstErr = err
    96  			content = nil
    97  			changed = nil
    98  			break
    99  		}
   100  		changed = append(changed, baseName)
   101  	}
   102  	// Delete phase (remove files matching the glob that are not in content)
   103  	matches := make(map[string]bool)
   104  	for _, glob := range globs {
   105  		m, err := filepath.Glob(filepath.Join(dir, glob))
   106  		if err != nil {
   107  			sort.Strings(changed)
   108  			return changed, nil, err
   109  		}
   110  		for _, path := range m {
   111  			matches[path] = true
   112  		}
   113  	}
   114  
   115  	for path := range matches {
   116  		baseName := filepath.Base(path)
   117  		if content[baseName] != nil {
   118  			continue
   119  		}
   120  		err := os.Remove(path)
   121  		if err != nil {
   122  			if firstErr == nil {
   123  				firstErr = err
   124  			}
   125  			continue
   126  		}
   127  		removed = append(removed, baseName)
   128  	}
   129  	sort.Strings(changed)
   130  	sort.Strings(removed)
   131  	return changed, removed, firstErr
   132  }
   133  
   134  func matchAny(globs []string, path string) (ok bool, index int, err error) {
   135  	for index, glob := range globs {
   136  		if ok, err := filepath.Match(glob, path); ok || err != nil {
   137  			return ok, index, err
   138  		}
   139  	}
   140  	return false, 0, nil
   141  }
   142  
   143  // EnsureDirState ensures that directory content matches expectations.
   144  //
   145  // This is like EnsureDirStateGlobs but it only supports one glob at a time.
   146  func EnsureDirState(dir string, glob string, content map[string]*FileState) (changed, removed []string, err error) {
   147  	return EnsureDirStateGlobs(dir, []string{glob}, content)
   148  }
   149  
   150  // Equals returns whether the file exists in the expected state.
   151  func (fileState *FileState) Equals(filePath string) (bool, error) {
   152  	stat, err := os.Stat(filePath)
   153  	if err != nil {
   154  		if os.IsNotExist(err) {
   155  			// not existing is not an error
   156  			return false, nil
   157  		}
   158  		return false, err
   159  	}
   160  	if stat.Mode().Perm() == fileState.Mode.Perm() && stat.Size() == int64(len(fileState.Content)) {
   161  		content, err := ioutil.ReadFile(filePath)
   162  		if err != nil {
   163  			return false, err
   164  		}
   165  		if bytes.Equal(content, fileState.Content) {
   166  			return true, nil
   167  		}
   168  	}
   169  	return false, nil
   170  }
   171  
   172  // EnsureFileState ensures that the file is in the expected state. It will not attempt
   173  // to remove the file if no content is provided.
   174  func EnsureFileState(filePath string, fileState *FileState) error {
   175  	equal, err := fileState.Equals(filePath)
   176  	if err != nil {
   177  		return err
   178  	}
   179  	if equal {
   180  		// Return a special error if the file doesn't need to be changed
   181  		return ErrSameState
   182  	}
   183  	return AtomicWriteFile(filePath, fileState.Content, fileState.Mode, 0)
   184  }