GODT-1332 moving cache does not work on Windows.

This commit is contained in:
Xavier Michelon
2021-10-04 19:44:37 +02:00
committed by Jakub
parent bc21bb1d8d
commit b6ad1fe490

View File

@ -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`.