// Copyright (c) 2022 Proton AG // // This file is part of Proton Mail Bridge. // // Proton Mail 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. // // Proton Mail 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 Proton Mail Bridge. If not, see . package users import ( "errors" "io" "os" "path/filepath" "github.com/sirupsen/logrus" ) // isFolderEmpty checks whether a folder is empty. // path must point to an existing folder. func isFolderEmpty(path string) (bool, error) { files, err := os.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), 0o700); 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, 0o700); err != nil { return err } files, err := os.ReadDir(srcPath) if err != nil { return err } // copy only regular files and folders for _, fileInfo := range files { mode := fileInfo.Type() 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 { if !dstInfo.Mode().IsRegular() { return errors.New("destination exists and is not a regular file") } if os.SameFile(srcInfo, dstInfo) { return errors.New("source and destination are the same") } } 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, 0o600) if err != nil { return err } defer func() { err = dst.Close() }() _, err = io.Copy(dst, src) return err } func (u *Users) EnableCache() error { // NOTE(GODT-1158): Check for available size before enabling. return nil } func (u *Users) DisableCache() error { 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 } // 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 { fiSrc, err := os.Stat(srcPath) if os.IsNotExist(err) { logrus.WithError(err).Warn("Skipping migration: unknown source for cache migration") return nil } if !fiSrc.IsDir() { logrus.WithError(err).Warn("Skipping migration: srcPath is not a dir") return nil } if isSubfolderOf(dstPath, srcPath) { return errors.New("destination folder is a subfolder of the source folder") } if err = checkFolderIsSuitableDestinationForCache(dstPath); err != nil { logrus.WithError(err).Error("The 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") } } // GODT-1381 Edge case: read-only source migration: prevent re-naming // (read-only is conserved). Do copy instead. tmp, err := os.CreateTemp(srcPath, "tmp") if err == nil { // Removal of tmp file cannot be deferred, as we are going to try to move the containing folder. if err = tmp.Close(); err == nil { if err = os.Remove(tmp.Name()); err == nil { if err = os.Rename(srcPath, dstPath); err == nil { return nil } } } } logrus.WithError(err).Warn("Cannot write to source: do copy to new destination instead of rename") // 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.WithError(err).Warn("Original cache folder could not be entirely removed") } return nil }