forked from Silverfish/proton-bridge
GODT-1332: moved cache related functions to separate file.
When migrating cache, closing of stored is delayed until the last moment.
This commit is contained in:
241
internal/users/cache.go
Normal file
241
internal/users/cache.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user