seekia/internal/localFilesystem/localFilesystem.go

435 lines
11 KiB
Go

// localFilesystem provides functions to read and write to the filesystem
package localFilesystem
// localFilesystem tries to prevent the concurrent writing of files
// Each filepath and folderpath has its own mutex, which prevents the concurrent write of a file/folder
// This only works properly if each file is deleted individually using DeleteFileOrFolder, as opposed to using something like os.RemoveAll
//
// It is not perfect, because a file could be created within a folder that is being deleted
// To avoid this, we need to avoid deleting folders when the user is signed in
import "seekia/internal/globalSettings"
import "seekia/internal/appMemory"
import goFilepath "path/filepath"
import "sync"
import "os"
import "errors"
var filesystemPathMutexesMapMutex sync.RWMutex
var filesystemPathMutexesMap map[string]*sync.RWMutex = make(map[string]*sync.RWMutex)
func getFilesystemPathMutex(filepath string)*sync.RWMutex{
filesystemPathMutexesMapMutex.RLock()
currentMutex, exists := filesystemPathMutexesMap[filepath]
filesystemPathMutexesMapMutex.RUnlock()
if (exists == true){
return currentMutex
}
newMutex := new(sync.RWMutex)
filesystemPathMutexesMapMutex.Lock()
filesystemPathMutexesMap[filepath] = newMutex
filesystemPathMutexesMapMutex.Unlock()
return newMutex
}
// This must be run upon application startup
// It also must be run before certain tests
func InitializeAppDatastores()error{
seekiaDirectoryPath, err := GetSeekiaDataFolderPath()
if (err != nil) { return err }
_, err = CreateFolder(seekiaDirectoryPath)
if (err != nil) { return err }
err = globalSettings.InitializeGlobalSettingsDatastore()
if (err != nil) { return err }
databaseFolderPath, err := GetAppDatabaseFolderPath()
if (err != nil) { return err }
_, err = CreateFolder(databaseFolderPath)
if (err != nil) { return err }
userDataFolderPath, err := GetAppUsersDataFolderPath()
if (err != nil) { return err }
_, err = CreateFolder(userDataFolderPath)
if (err != nil) { return err }
parametersFolderpath := goFilepath.Join(seekiaDirectoryPath, "Parameters")
parametersNetwork1Folderpath := goFilepath.Join(parametersFolderpath, "Network1")
parametersNetwork2Folderpath := goFilepath.Join(parametersFolderpath, "Network2")
_, err = CreateFolder(parametersFolderpath)
if (err != nil) { return err }
_, err = CreateFolder(parametersNetwork1Folderpath)
if (err != nil) { return err }
_, err = CreateFolder(parametersNetwork2Folderpath)
if (err != nil) { return err }
return nil
}
// This returns the folderpath where Seekia globalSettings and userData is stored
// It may also contain the database, unless the user has manually changed the database location
func GetSeekiaDataFolderPath() (string, error) {
localFileDirectory, err := os.UserConfigDir()
if (err != nil) { return "", err }
seekiaDirectory := goFilepath.Join(localFileDirectory, "SeekiaData")
return seekiaDirectory, nil
}
// This folder stores all app user data within it
// Each user has a folder, the name being the name of the user
func GetAppUsersDataFolderPath()(string, error){
seekiaDirectoryPath, err := GetSeekiaDataFolderPath()
if (err != nil) { return "", err }
appUsersDirectory := goFilepath.Join(seekiaDirectoryPath, "UserData")
return appUsersDirectory, nil
}
// This returns the folder where the currently signed-in user's data is stored
func GetAppUserFolderPath() (string, error) {
exists, appUserName := appMemory.GetMemoryEntry("AppUser")
if (exists == false){
return "", errors.New("GetUserDirectoryPath called when user is not signed in.")
}
appUsersFolderPath, err := GetAppUsersDataFolderPath()
if (err != nil) { return "", err }
userDirectory := goFilepath.Join(appUsersFolderPath, appUserName)
return userDirectory, nil
}
// This folder location is customizable in the app.
func GetAppDatabaseFolderPath()(string, error){
exists, directoryPath, err := globalSettings.GetSetting("DatabaseFolderpath")
if (err != nil) { return "", err }
if (exists == true){
return directoryPath, nil
}
seekiaDirectory, err := GetSeekiaDataFolderPath()
if (err != nil) { return "", err }
databasePath := goFilepath.Join(seekiaDirectory, "SeekiaDatabase")
return databasePath, nil
}
// Function will create new file or overwite existing:
func CreateOrOverwriteFile(content []byte, folderPath string, filename string) error{
_, err := CreateFolder(folderPath)
if (err != nil) { return err }
filepath := goFilepath.Join(folderPath, filename)
newFile, err := os.Create(filepath)
if (err != nil) { return err }
_, err = newFile.Write(content)
if (err != nil) {
newFile.Close()
return err
}
newFile.Close()
return nil
}
//Outputs:
// -bool: File exists
// -[]byte: File contents
// -error
func GetFileContents(filePath string) (bool, []byte, error){
filepathMutex := getFilesystemPathMutex(filePath)
filepathMutex.RLock()
fileContents, err := os.ReadFile(filePath)
filepathMutex.RUnlock()
if (err == nil) {
return true, fileContents, nil
}
isNotExistError := os.IsNotExist(err)
if (isNotExistError == false){
return false, nil, err
}
emptyFileContents := make([]byte, 0)
return false, emptyFileContents, nil
}
//Outputs:
// -[][]byte: List of each file's contents
// -error
func GetAllFilesInFolderAsList(folderPath string)([][]byte, error){
folderMap, err := GetFolderContentsAsMap(folderPath)
if (err != nil) { return nil, err }
filesList := make([][]byte, 0, len(folderMap))
for _, fileContents := range folderMap{
filesList = append(filesList, fileContents)
}
return filesList, nil
}
// Outputs:
// -map[string][]byte: File name -> File Contents
// -error
func GetFolderContentsAsMap(folderPath string)(map[string][]byte, error) {
folderpathMutex := getFilesystemPathMutex(folderPath)
folderpathMutex.RLock()
fileList, err := os.ReadDir(folderPath)
folderpathMutex.RUnlock()
if (err != nil) { return nil, err }
folderMap := make(map[string][]byte)
for _, filesystemObject := range fileList{
filepathIsFolder := filesystemObject.IsDir()
if (filepathIsFolder == true){
continue
}
fileName := filesystemObject.Name()
filePath := goFilepath.Join(folderPath, fileName)
filepathMutex := getFilesystemPathMutex(filePath)
filepathMutex.RLock()
fileBytes, err := os.ReadFile(filePath)
filepathMutex.RUnlock()
if (err != nil){ return nil, err }
folderMap[fileName] = fileBytes
}
return folderMap, nil
}
// This does not work with nested folders
// You must create all parent folders before calling this function on a folderPath
//Outputs:
// -bool: Folder already exists
// -error
func CreateFolder(folderPath string)(bool, error){
folderpathMutex := getFilesystemPathMutex(folderPath)
folderpathMutex.RLock()
filesystemObject, err := os.Open(folderPath)
folderpathMutex.RUnlock()
if (err == nil){
// Folder already exists
filesystemObject.Close()
return true, nil
}
isNotExistErr := os.IsNotExist(err)
if (isNotExistErr == false){
return false, err
}
//Folder does not exist, create folder
folderpathMutex.Lock()
err = os.Mkdir(folderPath, os.ModePerm)
folderpathMutex.Unlock()
if (err != nil){ return false, err }
return false, nil
}
func CheckIfFileExists(filepath string)(bool, error){
filepathMutex := getFilesystemPathMutex(filepath)
filepathMutex.RLock()
_, err := os.Stat(filepath)
filepathMutex.RUnlock()
if (err != nil) {
isNotExistErr := os.IsNotExist(err)
if (isNotExistErr == true) {
// File does not exist
return false, nil
}
return false, err
}
return true, nil
}
// This function works for files and empty directories
//Outputs:
// -bool: File/Folder existed and was deleted
// -error
func DeleteFileOrFolder(filepath string)(bool, error){
filepathMutex := getFilesystemPathMutex(filepath)
filepathMutex.Lock()
err := os.Remove(filepath)
filepathMutex.Unlock()
if (err != nil) {
isNotExistErr := os.IsNotExist(err)
if (isNotExistErr == true) {
return false, nil
}
return false, err
}
return true, nil
}
//Outputs:
// -bool: Folder exists and its contents were deleted
// -error
func DeleteAllFolderContents(inputFolderpath string)(bool, error){
folderpathMutex := getFilesystemPathMutex(inputFolderpath)
folderpathMutex.RLock()
fileList, err := os.ReadDir(inputFolderpath)
folderpathMutex.RUnlock()
if (err != nil) {
isNotExistError := os.IsNotExist(err)
if (isNotExistError == true){
// Folder does not exist, nothing to delete
return false, nil
}
return false, err
}
for _, filesystemObject := range fileList {
fileName := filesystemObject.Name()
filePath := goFilepath.Join(inputFolderpath, fileName)
filepathMutex := getFilesystemPathMutex(filePath)
filepathIsFolder := filesystemObject.IsDir()
if (filepathIsFolder == true){
filepathMutex.RLock()
fileList, err := os.ReadDir(filePath)
filepathMutex.RUnlock()
if (err != nil) { return false, err }
if (len(fileList) != 0){
// Filepath is a folder with items in it
// We recursively delete all items
folderExists, err := DeleteAllFolderContents(filePath)
if (err != nil) { return false, err }
if (folderExists == false){
return false, errors.New("Folder not found after being found already during DeleteAllFolderContents.")
}
}
}
_, err := DeleteFileOrFolder(filePath)
if (err != nil) { return false, err }
}
return true, nil
}
func GetFolderSizeInBytes(folderPath string)(int64, error){
sizeInBytes := int64(0)
filepathMutex := getFilesystemPathMutex(folderPath)
filepathMutex.RLock()
filesList, err := os.ReadDir(folderPath)
filepathMutex.RUnlock()
if (err != nil) { return 0, err }
for _, fileObject := range filesList{
isFolder := fileObject.IsDir()
if (isFolder == true){
subFolderName := fileObject.Name()
subFolderPath := goFilepath.Join(folderPath, subFolderName)
subFolderSize, err := GetFolderSizeInBytes(subFolderPath)
if (err != nil) { return 0, err }
sizeInBytes += subFolderSize
continue
}
fileName := fileObject.Name()
filePath := goFilepath.Join(folderPath, fileName)
fileExists, fileSize, err := GetFileSize(filePath)
if (err != nil) { return 0, err }
if (fileExists == false) {
return 0, errors.New("File not found after being found during GetFolderSizeInBytes.")
}
sizeInBytes += fileSize
}
return sizeInBytes, nil
}
//Outputs:
// -bool: File exists
// -int64: Size in bytes
// -error
func GetFileSize(filepath string)(bool, int64, error){
filepathMutex := getFilesystemPathMutex(filepath)
filepathMutex.RLock()
fileObject, err := os.Stat(filepath)
filepathMutex.RUnlock()
if (err != nil){
isNotExistError := os.IsNotExist(err)
if (isNotExistError == true){
return false, 0, nil
}
return false, 0, err
}
fileSizeBytes := fileObject.Size()
return true, fileSizeBytes, nil
}