From b41c4d2fa6792494fef16dcdfa5d3acd8275fc00 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Thu, 7 Oct 2021 10:41:36 +0200 Subject: [PATCH] GODT-1332: moved cache related functions to separate file. When migrating cache, closing of stored is delayed until the last moment. --- internal/users/cache.go | 241 ++++++++++++++++++++++++++++++++++++++++ internal/users/users.go | 217 ------------------------------------ 2 files changed, 241 insertions(+), 217 deletions(-) create mode 100644 internal/users/cache.go diff --git a/internal/users/cache.go b/internal/users/cache.go new file mode 100644 index 00000000..0767a2d7 --- /dev/null +++ b/internal/users/cache.go @@ -0,0 +1,241 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package users + +import ( + "errors" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" +) + +func (u *Users) EnableCache() error { + // NOTE(GODT-1158): Check for available size before enabling. + + return nil +} + +func (u *Users) DisableCache() error { + // NOTE(GODT-1158): Is it an error if we can't remove a user's cache? + + for _, user := range u.users { + if err := user.store.RemoveCache(); err != nil { + logrus.WithError(err).Error("Failed to remove user's message cache") + } + } + + return nil +} + +// 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 + } + + 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 + } + + for _, user := range u.users { + if err := user.closeStore(); err != nil { + logrus.WithError(err).Error("Failed to close user's store") + } + } + + 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 +} diff --git a/internal/users/users.go b/internal/users/users.go index 02194ba0..9b1005a4 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -20,10 +20,6 @@ package users import ( "context" - "io" - "io/ioutil" - "os" - "path/filepath" "strings" "sync" "time" @@ -435,219 +431,6 @@ func (u *Users) DisallowProxy() { u.clientManager.DisallowProxy() } -func (u *Users) EnableCache() error { - // NOTE(GODT-1158): Check for available size before enabling. - - return nil -} - -func (u *Users) DisableCache() error { - // NOTE(GODT-1158): Is it an error if we can't remove a user's cache? - - for _, user := range u.users { - if err := user.store.RemoveCache(); err != nil { - logrus.WithError(err).Error("Failed to remove user's message cache") - } - } - - return nil -} - -// 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 { - logrus.WithError(err).Error("Failed to close user's store") - } - } - - 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 - } - - 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`. func (u *Users) hasUser(id string) (user *User, ok bool) { for _, u := range u.users {