github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/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"
    26  	"io/ioutil"
    27  	"os"
    28  	"path/filepath"
    29  	"sort"
    30  )
    31  
    32  // FileState is an interface for conveying the desired state of a some file.
    33  type FileState interface {
    34  	State() (reader io.ReadCloser, size int64, mode os.FileMode, err error)
    35  }
    36  
    37  // FileReference describes the desired content by referencing an existing file.
    38  type FileReference struct {
    39  	Path string
    40  }
    41  
    42  // State returns a reader of the referenced file, along with other meta-data.
    43  func (fref FileReference) State() (io.ReadCloser, int64, os.FileMode, error) {
    44  	file, err := os.Open(fref.Path)
    45  	if err != nil {
    46  		return nil, 0, os.FileMode(0), err
    47  	}
    48  	fi, err := file.Stat()
    49  	if err != nil {
    50  		return nil, 0, os.FileMode(0), err
    51  	}
    52  	return file, fi.Size(), fi.Mode(), nil
    53  }
    54  
    55  // FileReferencePlusMode describes the desired content by referencing an existing file and providing custom mode.
    56  type FileReferencePlusMode struct {
    57  	FileReference
    58  	Mode os.FileMode
    59  }
    60  
    61  // State returns a reader of the referenced file, substituting the mode.
    62  func (fcref FileReferencePlusMode) State() (io.ReadCloser, int64, os.FileMode, error) {
    63  	reader, size, _, err := fcref.FileReference.State()
    64  	if err != nil {
    65  		return nil, 0, os.FileMode(0), err
    66  	}
    67  	return reader, size, fcref.Mode, nil
    68  }
    69  
    70  // MemoryFileState describes the desired content by providing an in-memory copy.
    71  type MemoryFileState struct {
    72  	Content []byte
    73  	Mode    os.FileMode
    74  }
    75  
    76  // State returns a reader of the in-memory contents of a file, along with other meta-data.
    77  func (blob *MemoryFileState) State() (io.ReadCloser, int64, os.FileMode, error) {
    78  	return ioutil.NopCloser(bytes.NewReader(blob.Content)), int64(len(blob.Content)), blob.Mode, nil
    79  }
    80  
    81  // ErrSameState is returned when the state of a file has not changed.
    82  var ErrSameState = fmt.Errorf("file state has not changed")
    83  
    84  // EnsureDirStateGlobs ensures that directory content matches expectations.
    85  //
    86  // EnsureDirStateGlobs enumerates all the files in the specified directory that
    87  // match the provided set of pattern (globs). Each enumerated file is checked
    88  // to ensure that the contents, permissions are what is desired. Unexpected
    89  // files are removed. Missing files are created and differing files are
    90  // corrected. Files not matching any pattern are ignored.
    91  //
    92  // Note that EnsureDirStateGlobs only checks for permissions and content. Other
    93  // security mechanisms, including file ownership and extended attributes are
    94  // *not* supported.
    95  //
    96  // The content map describes each of the files that are intended to exist in
    97  // the directory.  Map keys must be file names relative to the directory.
    98  // Sub-directories in the name are not allowed.
    99  //
   100  // If writing any of the files fails, EnsureDirStateGlobs switches to erase mode
   101  // where *all* of the files managed by the glob pattern are removed (including
   102  // those that may have been already written). The return value is an empty list
   103  // of changed files, the real list of removed files and the first error.
   104  //
   105  // If an error happens while removing files then such a file is not removed but
   106  // the removal continues until the set of managed files matching the glob is
   107  // exhausted.
   108  //
   109  // In all cases, the function returns the first error it has encountered.
   110  func EnsureDirStateGlobs(dir string, globs []string, content map[string]FileState) (changed, removed []string, err error) {
   111  	// Check syntax before doing anything.
   112  	if _, index, err := matchAny(globs, "foo"); err != nil {
   113  		return nil, nil, fmt.Errorf("internal error: EnsureDirState got invalid pattern %q: %s", globs[index], err)
   114  	}
   115  	for baseName := range content {
   116  		if filepath.Base(baseName) != baseName {
   117  			return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which has a path component", baseName)
   118  		}
   119  		if ok, _, _ := matchAny(globs, baseName); !ok {
   120  			if len(globs) == 1 {
   121  				return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which doesn't match the glob pattern %q", baseName, globs[0])
   122  			}
   123  			return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which doesn't match any glob patterns %q", baseName, globs)
   124  		}
   125  	}
   126  	// Change phase (create/change files described by content)
   127  	var firstErr error
   128  	for baseName, fileState := range content {
   129  		filePath := filepath.Join(dir, baseName)
   130  		err := EnsureFileState(filePath, fileState)
   131  		if err == ErrSameState {
   132  			continue
   133  		}
   134  		if err != nil {
   135  			// On write failure, switch to erase mode. Desired content is set
   136  			// to nothing (no content) changed files are forgotten and the
   137  			// writing loop stops. The subsequent erase loop will remove all
   138  			// the managed content.
   139  			firstErr = err
   140  			content = nil
   141  			changed = nil
   142  			break
   143  		}
   144  		changed = append(changed, baseName)
   145  	}
   146  	// Delete phase (remove files matching the glob that are not in content)
   147  	matches := make(map[string]bool)
   148  	for _, glob := range globs {
   149  		m, err := filepath.Glob(filepath.Join(dir, glob))
   150  		if err != nil {
   151  			sort.Strings(changed)
   152  			return changed, nil, err
   153  		}
   154  		for _, path := range m {
   155  			matches[path] = true
   156  		}
   157  	}
   158  
   159  	for path := range matches {
   160  		baseName := filepath.Base(path)
   161  		if content[baseName] != nil {
   162  			continue
   163  		}
   164  		err := os.Remove(path)
   165  		if err != nil {
   166  			if firstErr == nil {
   167  				firstErr = err
   168  			}
   169  			continue
   170  		}
   171  		removed = append(removed, baseName)
   172  	}
   173  	sort.Strings(changed)
   174  	sort.Strings(removed)
   175  	return changed, removed, firstErr
   176  }
   177  
   178  func matchAny(globs []string, path string) (ok bool, index int, err error) {
   179  	for index, glob := range globs {
   180  		if ok, err := filepath.Match(glob, path); ok || err != nil {
   181  			return ok, index, err
   182  		}
   183  	}
   184  	return false, 0, nil
   185  }
   186  
   187  // EnsureDirState ensures that directory content matches expectations.
   188  //
   189  // This is like EnsureDirStateGlobs but it only supports one glob at a time.
   190  func EnsureDirState(dir string, glob string, content map[string]FileState) (changed, removed []string, err error) {
   191  	return EnsureDirStateGlobs(dir, []string{glob}, content)
   192  }
   193  
   194  // fileStateEqualTo returns whether the file exists in the expected state.
   195  func fileStateEqualTo(filePath string, state FileState) (bool, error) {
   196  	other := &FileReference{Path: filePath}
   197  
   198  	// Open views to both files so that we can compare them.
   199  	readerA, sizeA, modeA, err := state.State()
   200  	if err != nil {
   201  		return false, err
   202  	}
   203  	defer readerA.Close()
   204  
   205  	readerB, sizeB, modeB, err := other.State()
   206  	if err != nil {
   207  		if os.IsNotExist(err) {
   208  			// Not existing is not an error
   209  			return false, nil
   210  		}
   211  		return false, err
   212  	}
   213  	defer readerB.Close()
   214  
   215  	// If the files have different size or different mode they are not
   216  	// identical and need to be re-created. Mode change could be optimized to
   217  	// avoid re-writing the whole file.
   218  	if modeA.Perm() != modeB.Perm() || sizeA != sizeB {
   219  		return false, nil
   220  	}
   221  	// The files have the same size so they might be identical.
   222  	// Do a block-wise comparison to determine that.
   223  	return streamsEqualChunked(readerA, readerB, 0), nil
   224  }
   225  
   226  // EnsureFileState ensures that the file is in the expected state. It will not
   227  // attempt to remove the file if no content is provided.
   228  func EnsureFileState(filePath string, state FileState) error {
   229  	equal, err := fileStateEqualTo(filePath, state)
   230  	if err != nil {
   231  		return err
   232  	}
   233  	if equal {
   234  		// Return a special error if the file doesn't need to be changed
   235  		return ErrSameState
   236  	}
   237  	reader, _, mode, err := state.State()
   238  	if err != nil {
   239  		return err
   240  	}
   241  	return AtomicWrite(filePath, reader, mode, 0)
   242  }