diff --git a/internal/users/users.go b/internal/users/users.go index be80a333..02194ba0 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -20,6 +20,8 @@ package users import ( "context" + "io" + "io/ioutil" "os" "path/filepath" "strings" @@ -451,8 +453,169 @@ func (u *Users) DisableCache() error { return nil } -func (u *Users) MigrateCache(from, to string) error { - // NOTE(GODT-1158): Is it enough to just close the store? Do we need to force-close the cacher too? +// isFolderEmpty checks whether a folder is empty. +// path must point to an existing folder. +func isFolderEmpty(path string) (bool, error) { + files, err := ioutil.ReadDir(path) + if err != nil { + return true, err + } + return len(files) == 0, nil +} + +// checkFolderIsSuitableDestinationForCache determine if a folder is a suitable destination as a cache +// if it is suitable (non existing, or empty and deletable) the folder is deleted. +func checkFolderIsSuitableDestinationForCache(path string) error { + // Ensure the parent directory exists. + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return err + } + + // if the folder does not exists, its suitable + fileInfo, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + if !fileInfo.IsDir() { + return errors.New("the destination folder for message cache exists and is a file") + } + + empty, err := isFolderEmpty(path) + if err != nil { + return err + } + + if !empty { + return errors.New("the destination folder is not empty") + } + return os.Remove(path) +} + +// copyFolder recursively copy folder at srcPath to dstPath. +// srcPath must be an existing folder. +// dstPath must point to a non-existing folder. +func copyFolder(srcPath, dstPath string) error { + fiFrom, err := os.Stat(srcPath) + if err != nil { + return err + } + + _, err = os.Stat(dstPath) + if !os.IsNotExist(err) { + return errors.New("the destination folder already exists") + } + + if !fiFrom.IsDir() { + return errors.New("source is not an existing folder") + } + + if err = os.MkdirAll(dstPath, 0700); err != nil { + return err + } + files, err := ioutil.ReadDir(srcPath) + if err != nil { + return err + } + // copy only regular files and folders + for _, fileInfo := range files { + mode := fileInfo.Mode() + if mode&os.ModeSymlink != 0 { + continue // we skip symbolic links to avoid potential endless recursion + } + srcSubPath := srcPath + "/" + fileInfo.Name() + dstSubPath := dstPath + "/" + fileInfo.Name() + + if mode.IsDir() { + if err = copyFolder(srcSubPath, dstSubPath); err != nil { + return err + } + continue + } + + if mode.IsRegular() { + if err = copyFile(srcSubPath, dstSubPath); err != nil { + return err + } + continue // unnecessary but safer if we had code below + } + } + return nil +} + +// isSubfolderOf check whether path is subfolder of refPath or is the same. +// RefPath must exist otherwise the function returns false. +func isSubfolderOf(path, refPath string) bool { + refInfo, err := os.Stat(refPath) + if (err != nil) || (!refInfo.IsDir()) { + return false // refpath does not exist. Not acceptable as we use os.SameFile for testing identity + } + + // we check path and all its parent folder to verify if it is refPath. + prevPath := "" + for path != prevPath { + pathInfo, err := os.Stat(path) // path may not exist, and it's acceptable, so wo keep going event if err != nil + if err == nil && os.SameFile(pathInfo, refInfo) { + return true + } + prevPath = path + path = filepath.Dir(path) + } + return false +} + +// copyFile copies file srcPath to dstPath. both path are files names. srcPath must exist, dstPath will be overwritten +// if it exists and is a file. +func copyFile(srcPath, dstPath string) error { + srcInfo, err := os.Stat(srcPath) + if err != nil { + return errors.New("could not open source file") + } + if !srcInfo.Mode().IsRegular() { + return errors.New("source file is not a regular file") + } + + dstInfo, err := os.Stat(dstPath) + if err == nil && !dstInfo.Mode().IsRegular() { + return errors.New("destination exists and is not a regular file") + } + + src, err := os.Open(filepath.Clean(srcPath)) + if err != nil { + return err + } + defer func() { + err = src.Close() + }() + + dst, err := os.OpenFile(filepath.Clean(dstPath), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer func() { + err = dst.Close() + }() + _, err = io.Copy(dst, src) + return err +} + +// MigrateCache moves the message cache folder from folder srcPath to folder dstPath. +// srcPath must point to an existing folder. dstPath must be an empty folder or not exist. +func (u *Users) MigrateCache(srcPath, dstPath string) error { + // NOTE(GODT-1158): Is it enough dstPath just close the store? Do we need dstPath force-close the cacher too? + + fiSrc, err := os.Stat(srcPath) + if os.IsNotExist(err) { + logrus.WithError(err).Error("unknown source for cache migration") + return err + } + if !fiSrc.IsDir() { + logrus.WithError(err).Error("cache migration cannot be perform srcPath a file") + return err + } for _, user := range u.users { if err := user.closeStore(); err != nil { @@ -460,12 +623,29 @@ func (u *Users) MigrateCache(from, to string) error { } } - // Ensure the parent directory exists. - if err := os.MkdirAll(filepath.Dir(to), 0700); err != nil { + if isSubfolderOf(dstPath, srcPath) { + return errors.New("the destination folder is a subfolder of the source folder") + } + + if err = checkFolderIsSuitableDestinationForCache(dstPath); err != nil { + logrus.WithError(err).Error("destination folder is not suitable for cache migration") return err } - return os.Rename(from, to) + err = os.Rename(srcPath, dstPath) + if err == nil { + return nil + } + + // Rename failed let's try an actual copy/delete + if err = copyFolder(srcPath, dstPath); err != nil { + return err + } + + if err = os.RemoveAll(srcPath); err != nil { // we don't care much about error there. + logrus.Info("Original cache folder could not be entirely removed") + } + return nil } // hasUser returns whether the struct currently has a user with ID `id`.