// 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 }