From c8304055ccd88774be38679f4c8533df2606159c Mon Sep 17 00:00:00 2001 From: Mircea Roata Date: Sat, 30 Dec 2023 20:47:45 +0200 Subject: [PATCH] Add cache dir setting --- backend/bindings/app.go | 25 ++++ backend/bindings/ficsitcli/wrapper.go | 80 +++++++++++ backend/bindings/settings.go | 22 +++ backend/settings/settings.go | 2 + backend/utils/os.go | 86 ++++++++++++ .../lib/components/left-bar/Settings.svelte | 127 +++++++++++++++++- frontend/src/lib/store/settingsStore.ts | 6 +- main.go | 11 ++ 8 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 backend/utils/os.go diff --git a/backend/bindings/app.go b/backend/bindings/app.go index 9a5f2746..38e4fe50 100644 --- a/backend/bindings/app.go +++ b/backend/bindings/app.go @@ -152,6 +152,31 @@ func (a *App) OpenFileDialog(options OpenDialogOptions) (string, error) { return file, nil } +func (a *App) OpenDirectoryDialog(options OpenDialogOptions) (string, error) { + wailsFilters := make([]wailsRuntime.FileFilter, len(options.Filters)) + for i, filter := range options.Filters { + wailsFilters[i] = wailsRuntime.FileFilter{ + DisplayName: filter.DisplayName, + Pattern: filter.Pattern, + } + } + wailsOptions := wailsRuntime.OpenDialogOptions{ + DefaultDirectory: options.DefaultDirectory, + DefaultFilename: options.DefaultFilename, + Title: options.Title, + Filters: wailsFilters, + ShowHiddenFiles: options.ShowHiddenFiles, + CanCreateDirectories: options.CanCreateDirectories, + ResolvesAliases: options.ResolvesAliases, + TreatPackagesAsDirectories: options.TreatPackagesAsDirectories, + } + file, err := wailsRuntime.OpenDirectoryDialog(a.ctx, wailsOptions) + if err != nil { + return "", errors.Wrap(err, "failed to open directory dialog") + } + return file, nil +} + func (a *App) ExternalInstallMod(modID, version string) { wailsRuntime.EventsEmit(a.ctx, "externalInstallMod", modID, version) } diff --git a/backend/bindings/ficsitcli/wrapper.go b/backend/bindings/ficsitcli/wrapper.go index de8600b3..d292ff27 100644 --- a/backend/bindings/ficsitcli/wrapper.go +++ b/backend/bindings/ficsitcli/wrapper.go @@ -3,15 +3,18 @@ package ficsitcli import ( "context" "log/slog" + "os" "time" "github.com/mitchellh/go-ps" "github.com/pkg/errors" "github.com/satisfactorymodding/ficsit-cli/cli" "github.com/satisfactorymodding/ficsit-cli/cli/provider" + "github.com/spf13/viper" wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime" "github.com/satisfactorymodding/SatisfactoryModManager/backend/settings" + "github.com/satisfactorymodding/SatisfactoryModManager/backend/utils" ) type FicsitCLI struct { @@ -76,3 +79,80 @@ func (f *FicsitCLI) setProgress(p *Progress) { f.progress = p wailsRuntime.EventsEmit(f.ctx, "progress", p) } + +func ValidateCacheDir(dir string) error { + stat, err := os.Stat(dir) + if err != nil { + if !os.IsNotExist(err) { + return errors.Wrapf(err, "failed to stat %s", dir) + } + } else { + if !stat.IsDir() { + return errors.Errorf("%s is not a directory", dir) + } + } + return nil +} + +func MoveCacheDir(newDir string) error { + if newDir == viper.GetString("cache-dir") { + return nil + } + + err := ValidateCacheDir(newDir) + if err != nil { + return err + } + + err = os.MkdirAll(newDir, 0o755) + if err != nil { + if !os.IsExist(err) { + return errors.Wrapf(err, "failed to create %s", newDir) + } + } + + items, err := os.ReadDir(newDir) + if err != nil { + return errors.Wrapf(err, "failed to check if directory %s is empty", newDir) + } + if len(items) > 0 { + return errors.Errorf("directory %s is not empty", newDir) + } + + oldCacheDir := viper.GetString("cache-dir") + // Move contents of oldCacheDir to dir + if oldCacheDir != "" && oldCacheDir != newDir { + err := moveCacheData(oldCacheDir, newDir) + if err != nil { + return err + } + } + + viper.Set("cache-dir", newDir) + return nil +} + +func moveCacheData(oldCacheDir, newDir string) error { + oldStat, err := os.Stat(oldCacheDir) + if err != nil { + if os.IsNotExist(err) { + // Nothing to move + return nil + } + return errors.Wrapf(err, "failed to stat %s", oldCacheDir) + } + if !oldStat.IsDir() { + return errors.Errorf("%s is not a directory", oldCacheDir) + } + + // Perform the move atomically + copySuccess, err := utils.MoveRecursive(oldCacheDir, newDir) + if err != nil { + if !copySuccess { + return err + } + slog.Error("failed to move cache dir", slog.Any("error", err)) + } + + return nil +} diff --git a/backend/bindings/settings.go b/backend/bindings/settings.go index 9a89cf05..28a922de 100644 --- a/backend/bindings/settings.go +++ b/backend/bindings/settings.go @@ -2,9 +2,12 @@ package bindings import ( "context" + "log/slog" + "github.com/spf13/viper" wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime" + "github.com/satisfactorymodding/SatisfactoryModManager/backend/bindings/ficsitcli" "github.com/satisfactorymodding/SatisfactoryModManager/backend/settings" ) @@ -175,3 +178,22 @@ func (s *Settings) SetAnnouncementViewed(announcement string) { _ = settings.SaveSettings() wailsRuntime.EventsEmit(s.ctx, "viewedAnnouncements", settings.Settings.ViewedAnnouncements) } + +func (s *Settings) SetCacheDir(dir string) error { + if dir == "" { + dir = viper.GetString("default-cache-dir") + } + err := ficsitcli.MoveCacheDir(dir) + if err != nil { + slog.Error("failed to set cache dir", slog.Any("error", err)) + return err + } + settings.Settings.CacheDir = dir + _ = settings.SaveSettings() + wailsRuntime.EventsEmit(s.ctx, "cacheDir", dir) + return nil +} + +func (s *Settings) GetCacheDir() string { + return viper.GetString("cache-dir") +} diff --git a/backend/settings/settings.go b/backend/settings/settings.go index 0c2fd1f9..96dc708c 100644 --- a/backend/settings/settings.go +++ b/backend/settings/settings.go @@ -52,6 +52,8 @@ type settings struct { Konami bool `json:"konami"` LaunchButton string `json:"launchButton"` + + CacheDir string `json:"cacheDir,omitempty"` } var Settings = settings{ diff --git a/backend/utils/os.go b/backend/utils/os.go new file mode 100644 index 00000000..4d367e0e --- /dev/null +++ b/backend/utils/os.go @@ -0,0 +1,86 @@ +package utils + +import ( + "io" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +func IsIn(dir, path string) bool { + rel, err := filepath.Rel(dir, path) + if err != nil { + return false + } + return filepath.IsLocal(rel) +} + +func CopyRecursive(from, to string) error { + return filepath.Walk(from, func(path string, info os.FileInfo, err error) error { //nolint:wrapcheck + if err != nil { + return err + } + if IsIn(to, path) { + return nil + } + relPath, err := filepath.Rel(from, path) + if err != nil { + return err //nolint:wrapcheck + } + newPath := filepath.Join(to, relPath) + if info.IsDir() { + err := os.Mkdir(newPath, 0o755) + if err != nil && !os.IsExist(err) { + return err //nolint:wrapcheck + } + return nil + } + f, err := os.Open(path) + if err != nil { + return err //nolint:wrapcheck + } + defer f.Close() + f2, err := os.Create(newPath) + if err != nil { + return err //nolint:wrapcheck + } + defer f2.Close() + _, err = io.Copy(f2, f) + return err //nolint:wrapcheck + }) +} + +func MoveRecursive(from, to string) (bool, error) { + err := CopyRecursive(from, to) + if err != nil { + return false, errors.Wrapf(err, "failed to copy %s to %s", from, to) + } + err = filepath.Walk(from, func(path string, info os.FileInfo, err error) error { + if err != nil { + if !os.IsNotExist(err) { + return err + } + return nil + } + if IsIn(path, to) { + // Skip parent directories of destination + return nil + } + if IsIn(to, path) { + // Skip contents of destination + return nil + } + err = os.RemoveAll(path) + if err != nil { + if !os.IsNotExist(err) { + return err //nolint:wrapcheck + } + } + return nil + }) + if err != nil { + return true, errors.Wrapf(err, "failed to remove %s", from) + } + return true, nil +} diff --git a/frontend/src/lib/components/left-bar/Settings.svelte b/frontend/src/lib/components/left-bar/Settings.svelte index 918513d6..34f6f10c 100644 --- a/frontend/src/lib/components/left-bar/Settings.svelte +++ b/frontend/src/lib/components/left-bar/Settings.svelte @@ -2,16 +2,20 @@ import Button, { Label } from '@smui/button'; import Menu, { SelectionGroup, SelectionGroupIcon } from '@smui/menu'; import List, { Item, PrimaryText, Text, Separator } from '@smui/list'; - import { mdiBug, mdiCheck, mdiChevronRight, mdiClipboard, mdiCog, mdiDownload, mdiTune } from '@mdi/js'; + import { mdiBug, mdiCheck, mdiChevronRight, mdiClipboard, mdiCog, mdiDownload, mdiFolderEdit, mdiTune } from '@mdi/js'; import { getContextClient } from '@urql/svelte'; + import Dialog, { Actions, Content, Title } from '@smui/dialog'; + import Textfield from '@smui/textfield'; + import HelperText from '@smui/textfield/helper-text'; import SvgIcon from '$lib/components/SVGIcon.svelte'; import { GenerateDebugInfo } from '$wailsjs/go/bindings/DebugInfo'; - import { startView, konami, launchButton, queueAutoStart, offline, updateCheckMode } from '$lib/store/settingsStore'; + import { startView, konami, launchButton, queueAutoStart, offline, updateCheckMode, cacheDir } from '$lib/store/settingsStore'; import { manifestMods, lockfileMods } from '$lib/store/ficsitCLIStore'; import { GetModNameDocument } from '$lib/generated'; import type { LaunchButtonType, ViewType } from '$lib/wailsTypesExtensions'; import { OfflineGetMod } from '$wailsjs/go/ficsitcli/FicsitCLI'; + import { OpenDirectoryDialog } from '$lib/generated/wailsjs/go/bindings/App'; let settingsMenu: Menu; let startViewMenu: Menu; @@ -116,6 +120,75 @@ }); navigator.clipboard.writeText(modListString.trim()); } + + let cacheDialog = false; + let cacheError: string | null = null; + let newCacheLocation = $cacheDir; + + let fileDialogOpen = false; + async function pickCacheLocation() { + if(fileDialogOpen) { + return; + } + fileDialogOpen = true; + try { + let result = await OpenDirectoryDialog({ + defaultDirectory: newCacheLocation ?? undefined, + }); + if (result) { + newCacheLocation = result; + } + } catch (e) { + if(e instanceof Error) { + cacheError = e.message; + } else if (typeof e === 'string') { + cacheError = e; + } else { + cacheError = 'Unknown error'; + } + } finally { + fileDialogOpen = false; + } + } + + let cacheMoveInProgress = false; + + async function setCacheLocation() { + try { + cacheMoveInProgress = true; + await cacheDir.asyncSet(newCacheLocation ?? ''); + cacheError = null; + } catch(e) { + if (e instanceof Error) { + cacheError = e.message; + } else if (typeof e === 'string') { + cacheError = e; + } else { + cacheError = 'Unknown error'; + } + } finally { + cacheMoveInProgress = false; + } + } + + async function resetCacheLocation() { + try { + cacheMoveInProgress = true; + await cacheDir.asyncSet(''); + newCacheLocation = $cacheDir; + cacheError = null; + } catch(e) { + if (e instanceof Error) { + cacheError = e.message; + } else if (typeof e === 'string') { + cacheError = e; + } else { + cacheError = 'Unknown error'; + } + } finally { + cacheMoveInProgress = false; + } + }
@@ -256,6 +329,15 @@
+ { cacheDialog = true; settingsMenu.setOpen(false); } }> +
+ + Change cache location + +
+ + + { $offline = !$offline; settingsMenu.setOpen(false); }}>
@@ -307,3 +389,44 @@
+ + + + Change download cache location + +
+
+ pickCacheLocation()} + > + + { cacheError } + + +
+ + +
+
+ + + +
diff --git a/frontend/src/lib/store/settingsStore.ts b/frontend/src/lib/store/settingsStore.ts index b09b0a58..d4a4213a 100644 --- a/frontend/src/lib/store/settingsStore.ts +++ b/frontend/src/lib/store/settingsStore.ts @@ -1,8 +1,8 @@ -import { binding, bindingTwoWayNoExcept } from './wailsStoreBindings'; +import { binding, bindingTwoWay, bindingTwoWayNoExcept } from './wailsStoreBindings'; import type { LaunchButtonType, ViewType } from '$lib/wailsTypesExtensions'; import { GetOffline, SetOffline } from '$wailsjs/go/ficsitcli/FicsitCLI'; -import { GetStartView, SetStartView, GetKonami, SetKonami, GetLaunchButton, SetLaunchButton, GetQueueAutoStart, SetQueueAutoStart, GetUpdateCheckMode, SetUpdateCheckMode, GetViewedAnnouncements, GetIgnoredUpdates } from '$wailsjs/go/bindings/Settings'; +import { GetStartView, SetStartView, GetKonami, SetKonami, GetLaunchButton, SetLaunchButton, GetQueueAutoStart, SetQueueAutoStart, GetUpdateCheckMode, SetUpdateCheckMode, GetViewedAnnouncements, GetIgnoredUpdates, GetCacheDir, SetCacheDir } from '$wailsjs/go/bindings/Settings'; export const startView = bindingTwoWayNoExcept(null, { initialGet: GetStartView }, { updateFunction: SetStartView }); @@ -19,3 +19,5 @@ export const updateCheckMode = bindingTwoWayNoExcept<'launch'|'exit'|'ask'>('lau export const viewedAnnouncements = binding([], { initialGet: GetViewedAnnouncements, updateEvent: 'viewedAnnouncements' }); export const ignoredUpdates = binding>({}, { initialGet: GetIgnoredUpdates, updateEvent: 'ignoredUpdates' }); + +export const cacheDir = bindingTwoWay(null, { initialGet: GetCacheDir, updateEvent: 'cacheDir' }, { updateFunction: SetCacheDir }); diff --git a/main.go b/main.go index f5f81e2a..b0f2012b 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "github.com/satisfactorymodding/SatisfactoryModManager/backend" "github.com/satisfactorymodding/SatisfactoryModManager/backend/autoupdate" "github.com/satisfactorymodding/SatisfactoryModManager/backend/bindings" + "github.com/satisfactorymodding/SatisfactoryModManager/backend/bindings/ficsitcli" "github.com/satisfactorymodding/SatisfactoryModManager/backend/projectfile" "github.com/satisfactorymodding/SatisfactoryModManager/backend/settings" "github.com/satisfactorymodding/SatisfactoryModManager/backend/utils" @@ -60,6 +61,15 @@ func main() { os.Exit(1) } + if settings.Settings.CacheDir != "" { + err = ficsitcli.ValidateCacheDir(settings.Settings.CacheDir) + if err != nil { + slog.Error("failed to set cache dir", slog.Any("error", err)) + } else { + viper.Set("cache-dir", settings.Settings.CacheDir) + } + } + b, err := bindings.MakeBindings() if err != nil { slog.Error("failed to create bindings", slog.Any("error", err)) @@ -142,6 +152,7 @@ func init() { cacheDir := filepath.Clean(filepath.Join(baseCacheDir, "ficsit")) _ = utils.EnsureDirExists(cacheDir) viper.Set("cache-dir", cacheDir) + viper.Set("default-cache-dir", cacheDir) smmCacheDir := filepath.Clean(filepath.Join(baseCacheDir, "SatisfactoryModManager")) _ = utils.EnsureDirExists(smmCacheDir)