agent/fileutil/fileutil.go (374 lines of code) (raw):
// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.
// Package fileutil contains utilities for working with the file system.
package fileutil
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/aws/amazon-ssm-agent/agent/appconfig"
)
type ByteOrderMark uint8
const (
ByteOrderMarkEmit ByteOrderMark = iota
ByteOrderMarkSkip ByteOrderMark = iota
)
func CreateUTF8ByteOrderMark() (result []byte) {
return []byte{0xEF, 0xBB, 0xBF}
}
// DiskSpaceInfo stores the available, free, and total bytes
type DiskSpaceInfo struct {
AvailBytes int64
FreeBytes int64
TotalBytes int64
}
// DeleteFile deletes the specified file
func DeleteFile(filepath string) (err error) {
return fs.Remove(filepath)
}
// DeleteDirectory deletes a directory and all its content.
func DeleteDirectory(dirName string) (err error) {
return os.RemoveAll(dirName)
}
// ReadAllText reads all content from the specified file
func ReadAllText(filePath string) (text string, err error) {
exists, err := LocalFileExist(filePath)
if err != nil || !exists {
return
}
buf := bytes.NewBuffer(nil)
f, _ := os.Open(filePath)
defer f.Close()
_, err = io.Copy(buf, f)
if err != nil {
return
}
text = string(buf.Bytes())
return
}
// AppendToFile appends content to file
func AppendToFile(fileDirectory string, filename string, content string) (filePath string, err error) {
filePath = filepath.Join(fileDirectory, filename)
fileWriter, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, appconfig.ReadWriteAccess)
if err != nil {
err = fmt.Errorf("failed to open the file at %v: %v", filePath, err)
}
if fileWriter.WriteString(content); err != nil {
err = fmt.Errorf("failed to write contents to file")
}
defer fileWriter.Close()
return filePath, err
}
// WriteAllText writes all text content to the specified file
func WriteAllText(filePath string, text string) (err error) {
f, _ := os.Create(filePath)
defer f.Close()
_, err = f.WriteString(text)
return
}
// Exists returns true if the given file exists, false otherwise, ignoring any underlying error
func Exists(filePath string) bool {
exist, _ := LocalFileExist(filePath)
return exist
}
// LocalFileExist returns true if the given file exists, false otherwise.
func LocalFileExist(path string) (bool, error) {
_, err := fs.Stat(path)
if err == nil {
return true, nil
}
if fs.IsNotExist(err) {
return false, nil
}
return false, err
}
// BuildPath joins the orchestration directory path with valid components.
func BuildPath(root string, elements ...string) string {
fullPath := root
for _, element := range elements {
fullPath = filepath.Join(fullPath, removeInvalidColon(element))
}
return fullPath
}
// BuildSafePath joins the directory, but returns the root directory if the subpaths attempt
// to recurse above the provided root
func BuildSafePath(root string, elements ...string) string {
root = filepath.Clean(root)
fullPath := root
for _, element := range elements {
cleanElement := removeInvalidColon(element)
newPath := filepath.Join(fullPath, cleanElement)
newPath = filepath.Clean(newPath)
if !strings.HasPrefix(newPath, root) {
return root
}
fullPath = newPath
}
return fullPath
}
// BuildS3Path joins the root directory path with valid components.
func BuildS3Path(root string, elements ...string) string {
fullPath := root
for _, element := range elements {
fullPath = path.Join(fullPath, removeInvalidColon(element))
}
return fullPath
}
// removeInvalidColon strips any invalid colon from plugin name.
func removeInvalidColon(pluginName string) string {
if pluginName != "" {
return strings.Replace(pluginName, ":", "", -1)
}
return pluginName
}
// MakeDirs create the directories along the path if missing.
func MakeDirs(destinationDir string) (err error) {
// create directory
err = fs.MkdirAll(destinationDir, appconfig.ReadWriteExecuteAccess)
if err != nil {
err = fmt.Errorf("failed to create directory %v. %v", destinationDir, err)
}
return
}
// MakeDirsWithExecuteAccess create the directories along the path if missing.
func MakeDirsWithExecuteAccess(destinationDir string) (err error) {
// create directory
if err = fs.MkdirAll(destinationDir, appconfig.ReadWriteExecuteAccess); err != nil {
err = fmt.Errorf("failed to create directory %v. %v", destinationDir, err)
}
return
}
func GetFileMode(path string) (mode os.FileMode) {
fileInfo, err := fs.Stat(path)
if err != nil {
err = fmt.Errorf("error looking up path information Path: %v, Error: %v", path, err)
return 0
}
return fileInfo.Mode()
}
// IsDirectory returns true or false depending
// if given srcPath is directory or not
func IsDirectory(srcPath string) bool {
srcFileInfo, err := fs.Stat(srcPath)
if err != nil {
err = fmt.Errorf("error looking up path information Path: %v, Error: %v", srcPath, err)
return false
}
return srcFileInfo.Mode().IsDir()
}
// IsFile returns true or false depending if given
// srcPath is a regular file or not
func IsFile(srcPath string) bool {
srcFileInfo, err := fs.Stat(srcPath)
if err != nil {
err = fmt.Errorf("error looking up path information Path: %v, Error: %v", srcPath, err)
return false
}
return srcFileInfo.Mode().IsRegular()
}
// MoveFile moves file from srcPath directory to dstPath directory
// only if both directories exist
func MoveFile(filename, srcPath, dstPath string) (result bool, err error) {
return MoveAndRenameFile(srcPath, filename, dstPath, filename)
}
// MoveAndRenameFile moves a file from the srcPath directory to dstPath directory and gives it a new name
func MoveAndRenameFile(srcPath, originalName, dstPath, newName string) (result bool, err error) {
srcFile := filepath.Join(srcPath, originalName)
dstFile := filepath.Join(dstPath, newName)
if err = fs.Rename(srcFile, dstFile); err != nil {
return false, fmt.Errorf("unexpected error encountered while moving the file. Error details - %v", err)
}
return true, nil
}
// WriteIntoFileWithPermissions writes into file with given file mode permissions
func WriteIntoFileWithPermissions(absolutePath, content string, perm os.FileMode) (result bool, err error) {
return WriteIntoFileWithPermissionsExtended(absolutePath, content, perm, ByteOrderMarkSkip)
}
// WriteIntoFileWithPermissionsExtended writes into file with given file mode permissions
func WriteIntoFileWithPermissionsExtended(absolutePath, content string, perm os.FileMode, byteOrderMark ByteOrderMark) (result bool, err error) {
result = true
if byteOrderMark == ByteOrderMarkEmit {
err = ioUtil.WriteFile(absolutePath, append(CreateUTF8ByteOrderMark(), []byte(content)...), perm)
} else {
err = ioUtil.WriteFile(absolutePath, []byte(content), perm)
}
if err != nil {
err = fmt.Errorf("couldn't write into file - %v", err)
result = false
}
return
}
// GetFileModificationTime returns the modification time of the file
func GetFileModificationTime(srcPath string) (modificationTime time.Time, err error) {
srcFileInfo, err := fs.Stat(srcPath)
if err != nil {
err = fmt.Errorf("error looking up file information: %v, Error: %v", srcPath, err)
return
}
modificationTime = srcFileInfo.ModTime()
return
}
// IsDirEmpty returns true if the given directory is empty else it returns false
func IsDirEmpty(location string) (bool, error) {
f, err := os.Open(location)
if err != nil {
err = fmt.Errorf("couldn't open path - %v", err)
return false, err
}
defer f.Close()
// read in ONLY one file
_, err = f.Readdir(1)
// if file is EOF -> dir is empty
// else -> dir is non-empty
if err == io.EOF {
return true, nil
}
return false, err
}
// getAllDirectoriesWithFileInfo returns directories from the path
func getAllDirectoriesWithFileInfo(srcPath string) ([]os.FileInfo, error) {
f, err := os.Open(srcPath)
if err != nil {
return nil, err
}
list, err := f.Readdir(-1)
f.Close()
if err != nil {
return nil, err
}
return list, nil
}
// GetDirectoryNamesUnsortedOlderThan returns directory name unsorted ioutil.ReadDir uses sorting to sort the directory names
// if date value passed in olderThan param, this function returns all files older than the passed time.
func GetDirectoryNamesUnsortedOlderThan(srcPath string, olderThan *time.Time) ([]string, error) {
var err error
directories := make([]string, 0)
fileInfoList, err := getAllDirectoriesWithFileInfo(srcPath)
if err != nil {
return directories, err
}
for _, fileInfo := range fileInfoList {
if fileInfo.Mode().IsDir() {
if olderThan == nil || (olderThan != nil && fileInfo.ModTime().Before(*olderThan)) {
directories = append(directories, fileInfo.Name())
}
}
}
return directories, nil
}
// GetFileNamesUnsortedLaterThan returns directory name unsorted ioutil.ReadDir uses sorting to sort the directory names
// if date value passed in laterThan param, this function returns all files after the passed time.
func GetFileNamesUnsortedLaterThan(srcPath string, laterThan *time.Time) ([]string, error) {
var err error
fileNames := make([]string, 0)
fileInfoList, err := getAllDirectoriesWithFileInfo(srcPath)
if err != nil {
return fileNames, err
}
for _, fileInfo := range fileInfoList {
if !fileInfo.Mode().IsDir() {
if laterThan == nil || (laterThan != nil && fileInfo.ModTime().After(*laterThan)) {
fileNames = append(fileNames, fileInfo.Name())
}
}
}
return fileNames, nil
}
// GetDirectoryNames returns the names of all directories under a give srcPath
func GetDirectoryNames(srcPath string) (directories []string, err error) {
if list, err := ioutil.ReadDir(srcPath); err == nil {
directories = make([]string, 0)
for _, fileinfo := range list {
if fileinfo.Mode().IsDir() {
directories = append(directories, fileinfo.Name())
}
}
}
return
}
// GetFileNames returns the names of all non-directories under a give srcPath
func GetFileNames(srcPath string) (files []string, err error) {
if list, err := ioutil.ReadDir(srcPath); err == nil {
files = make([]string, 0)
for _, fileinfo := range list {
if !fileinfo.Mode().IsDir() {
files = append(files, fileinfo.Name())
}
}
}
return
}
// CreateFile creates a file with the given name
func CreateFile(name string) (*os.File, error) {
return fs.Create(name)
}
// CreateTempDir creates a new temporary directory in the directory dir with a name beginning with prefix
func CreateTempDir(dir, prefix string) (name string, err error) {
return ioUtil.TempDir(dir, prefix)
}
// ReadDir returns files within the given location
func ReadDir(location string) ([]os.FileInfo, error) {
files := []os.FileInfo{}
if location == "" {
return files, fmt.Errorf("location cannot be empty")
}
return ioutil.ReadDir(location)
}
// isUnderDir determines if a given path is in or under a given parent directory (after accounting for path traversal)
func isUnderDir(childPath, parentDirPath string) bool {
return strings.HasPrefix(filepath.Clean(childPath)+string(filepath.Separator), filepath.Clean(parentDirPath)+string(filepath.Separator))
}
// Unzip unzips the installation package (using platform agnostic zip functionality)
// For platform specific implementation that uses tar.gz on Linux, use Uncompress
func Unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer func() {
if err := r.Close(); err != nil {
return
}
}()
isMacOS := runtime.GOOS == "darwin"
os.MkdirAll(dest, appconfig.ReadWriteExecuteAccess)
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
rc, err := f.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
return
}
}()
path := filepath.Join(dest, f.Name)
// MacOS unzips one folder level deeper then where we look for (unlike Windows & Linux), so we have to manually unzip one level higher
if isMacOS {
path = filepath.Join(dest, filepath.Base(f.Name))
}
if !isUnderDir(path, dest) {
return fmt.Errorf("%v attepts to place files outside %v subtree", f.Name, dest)
}
if f.FileInfo().IsDir() {
os.MkdirAll(path, f.Mode())
} else {
os.MkdirAll(filepath.Dir(path), f.Mode())
f, err := os.OpenFile(path, appconfig.FileFlagsCreateOrTruncate, f.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
return
}
}()
_, err = io.Copy(f, rc)
if err != nil {
return err
}
}
return nil
}
for _, f := range r.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
}
// MoveFiles moves all files and directories from *sourceDir* to *destDir*
func MoveFiles(sourceDir, destDir string) error {
filesInfo, err := ioutil.ReadDir(sourceDir)
if err != nil {
return err
}
for _, fileInfo := range filesInfo {
_, err := MoveAndRenameFile(sourceDir, fileInfo.Name(), destDir, fileInfo.Name())
if err != nil {
return fmt.Errorf("Cannot move contents to destination path: %s", err.Error())
}
}
return nil
}
// CollectFilesAndRebase collects all files and directories of the *sourceDir* changing their relative path to be rooted in *rebaseToDir*. E.g:
// ####
// sourceDir = "/a/b"
// filesInSourceDir = ["/a/b/file.txt", "/a/b/c/another_file.txt"]
// rebaseToDir = "/d"
//
// outputFiles = ["/d/file.txt", "/d/c/another_file.txt"]
// ####
func CollectFilesAndRebase(sourceDir, rebaseToDir string) (files []string, err error) {
err = filepath.Walk(sourceDir,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
// Compute the actual path of the file after the contents will be moved
relParentPath, err := filepath.Rel(sourceDir, filepath.Dir(path))
if err != nil {
return err
}
destFilePath := filepath.Join(rebaseToDir, relParentPath, info.Name())
files = append(files, destFilePath)
}
return err
})
if err != nil {
return nil, fmt.Errorf("Cannot acknowledge downloaded files: %v", err.Error())
}
return files, nil
}