Skip to content

Commit

Permalink
Add server picker path browser for local paths
Browse files Browse the repository at this point in the history
  • Loading branch information
mircearoata committed Apr 20, 2024
1 parent e97a648 commit 15eed7e
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 42 deletions.
130 changes: 113 additions & 17 deletions backend/ficsitcli/serverpicker.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
package ficsitcli

import (
"errors"
"fmt"
"io/fs"
"net/url"
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"

"github.com/satisfactorymodding/ficsit-cli/cli/disk"
psUtilDisk "github.com/shirou/gopsutil/v3/disk"
"golang.org/x/sync/errgroup"
)

var ServerPicker = &serverPicker{
disks: make(map[string]disk.Disk),
}

type serverPicker struct {
disks map[string]disk.Disk
disks map[string]diskData
nextServerPickerID int
}

type diskData struct {
disk disk.Disk
isLocal bool
}

var ServerPicker = &serverPicker{
disks: make(map[string]diskData),
}

type PickerDirectory struct {
Name string `json:"name"`
Path string `json:"path"`
IsValidInstall bool `json:"isValidInstall"`
}

Expand All @@ -33,6 +48,25 @@ func (s *serverPicker) getID() string {
return strconv.Itoa(id)
}

func (*serverPicker) GetPathSeparator() string {
return string(filepath.Separator)
}

var remoteSchemes = map[string]struct{}{
"ftp": {},
"sftp": {},
}

func isLocal(path string) (bool, error) {
parsed, err := url.Parse(path)
if err != nil {
return false, fmt.Errorf("failed to parse path: %w", err)
}

_, ok := remoteSchemes[parsed.Scheme]
return !ok, nil
}

func (s *serverPicker) StartPicker(path string) (string, error) {
id := s.getID()

Expand All @@ -41,7 +75,15 @@ func (s *serverPicker) StartPicker(path string) (string, error) {
return "", fmt.Errorf("failed to create: %w", err)
}

s.disks[id] = d
local, err := isLocal(path)
if err != nil {
return "", fmt.Errorf("failed to check if local: %w", err)
}

s.disks[id] = diskData{
disk: d,
isLocal: local,
}

return id, nil
}
Expand All @@ -54,7 +96,7 @@ func (s *serverPicker) StopPicker(id string) error {
return nil
}

func (s *serverPicker) TryPick(id string, path string) (PickerResult, error) {
func (s *serverPicker) TryPick(id string, pickPath string) (PickerResult, error) {
d, ok := s.disks[id]
if !ok {
return PickerResult{}, fmt.Errorf("no such disk: %s", id)
Expand All @@ -64,33 +106,87 @@ func (s *serverPicker) TryPick(id string, path string) (PickerResult, error) {
Items: make([]PickerDirectory, 0),
}

if d.isLocal && pickPath == "\\" && runtime.GOOS == "windows" {
// On windows, the root does not exist, and instead we need to list partitions
partitions, err := psUtilDisk.Partitions(false)
if err != nil {
return PickerResult{}, fmt.Errorf("failed to get partitions: %w", err)
}
for _, partition := range partitions {
validInstall, err := isValidInstall(d.disk, filepath.Join(pickPath, partition.Mountpoint))
if err != nil {
return PickerResult{}, fmt.Errorf("failed to check if valid install: %w", err)
}

result.Items = append(result.Items, PickerDirectory{
Name: partition.Mountpoint,
Path: partition.Mountpoint,
IsValidInstall: validInstall,
})
}
return result, nil
}

var err error

result.IsValidInstall, err = isValidInstall(d, path)
result.IsValidInstall, err = isValidInstall(d.disk, pickPath)
if err != nil {
return PickerResult{}, fmt.Errorf("failed to check if valid install: %w", err)
}

entries, err := d.ReadDir(path)
entries, err := d.disk.ReadDir(pickPath)
if err != nil {
return PickerResult{}, fmt.Errorf("failed reading directory: %w", err)
}

var wg errgroup.Group
// We only read the channel after all items have been added,
// so it must be buffered to avoid a deadlock
itemsChan := make(chan PickerDirectory, len(entries))

for _, entry := range entries {
if !entry.IsDir() {
continue
}

validInstall, err := isValidInstall(d, filepath.Join(path, entry.Name()))
if err != nil {
return PickerResult{}, fmt.Errorf("failed to check if valid install: %w", err)
}

result.Items = append(result.Items, PickerDirectory{
Name: entry.Name(),
IsValidInstall: validInstall,
wg.TryGo(func() error {
validInstall, err := isValidInstall(d.disk, filepath.Join(pickPath, entry.Name()))
if err != nil {
if errors.Is(err, fs.ErrPermission) {
return nil
}
return fmt.Errorf("failed to check if valid install: %w", err)
}

var fullPath string
if d.isLocal {
fullPath = filepath.Join(pickPath, entry.Name())
} else {
fullPath = path.Join(pickPath, entry.Name())
}

itemsChan <- PickerDirectory{
Name: entry.Name(),
Path: fullPath,
IsValidInstall: validInstall,
}
return nil
})
}

if err := wg.Wait(); err != nil {
return PickerResult{}, err //nolint:wrapCheck
}
close(itemsChan)

for item := range itemsChan {
result.Items = append(result.Items, item)
}

slices.SortFunc(result.Items, func(i, j PickerDirectory) int {
return strings.Compare(i.Name, j.Name)
})

return result, nil
}

Expand Down
56 changes: 36 additions & 20 deletions frontend/src/lib/components/RemoteServerPicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,41 @@
import { onDestroy } from 'svelte';
import SvgIcon from '$lib/components/SVGIcon.svelte';
import { StartPicker, StopPicker, TryPick } from '$lib/generated/wailsjs/go/ficsitcli/serverPicker';
import { GetPathSeparator, StartPicker, StopPicker, TryPick } from '$lib/generated/wailsjs/go/ficsitcli/serverPicker';
import type { ficsitcli } from '$lib/generated/wailsjs/go/models';
export let basePath: string;
export let disabled = false;
let currentBasePath = '';
let currentBasePath: string | null = null;
let pickerId = '';
let platformPathSeparator = '/';
GetPathSeparator().then((sep) => {
platformPathSeparator = sep;
});
$: isLocalPath = (() => {
try {
const url = new URL(basePath);
if (url.protocol === 'ftp:' || url.protocol === 'sftp:') {
return false;
}
return true;
} catch {
// If not parsable as a URL, it is definitely a local path
return true;
}
})();
$: pathSeparator = isLocalPath ? platformPathSeparator : '/';
function parentPath(path: string) {
return path.split(pathSeparator).slice(0, -1).join(pathSeparator);
}
async function stopPicker() {
if (pickerId) {
await StopPicker(pickerId);
Expand All @@ -39,19 +65,13 @@
$: if (disabled) {
stopPicker();
currentBasePath = '';
currentBasePath = null;
}
onDestroy(() => {
stopPicker();
});
interface PickerDirectory {
path: string;
name: string;
isValidInstall: boolean;
}
let displayedItems: { path: string; name: string; isValidInstall: boolean }[] = [];
export let path = '';
Expand All @@ -64,7 +84,7 @@
const forPath = path;
const forPickerId = pickerId;
try {
let { isValidInstall } = await TryPick(pickerId, path + '/');
let { isValidInstall } = await TryPick(pickerId, path + pathSeparator);
// If the path has changed since the request was made,
// or the picker has been stopped, ignore the response
if (path !== forPath || pickerId !== forPickerId) {
Expand All @@ -90,11 +110,11 @@
checkValid();
}
$: trimmedPath = path.endsWith('/') ? path.slice(0, -1) : path;
$: trimmedPath = path.endsWith(pathSeparator) ? path.slice(0, -1) : path;
let displayedPath = '';
$: if (!pendingValidCheck) {
displayedPath = valid ? trimmedPath.split('/').slice(0, -1).join('/') : trimmedPath;
displayedPath = valid ? parentPath(trimmedPath) : trimmedPath;
}
let pendingDisplay = false;
Expand All @@ -106,17 +126,13 @@
const forPath = displayedPath;
const forPickerId = pickerId;
try {
let { items } = await TryPick(pickerId, displayedPath + '/');
let { items } = await TryPick(pickerId, displayedPath + pathSeparator);
// If the path has changed since the request was made,
// or the picker has been stopped, ignore the response
if (displayedPath !== forPath || pickerId !== forPickerId) {
return;
}
displayedItems = items.map((d) => ({
path: displayedPath + '/' + d.name,
name: d.name,
isValidInstall: d.isValidInstall,
}));
displayedItems = items;
error = null;
} catch (e) {
// If the path has changed since the request was made,
Expand All @@ -135,7 +151,7 @@
updateDisplay();
}
function select(item: PickerDirectory) {
function select(item: ficsitcli.PickerDirectory) {
valid = item.isValidInstall;
path = item.path;
}
Expand All @@ -146,7 +162,7 @@

<div class="relative">
<div class="flex flex-col w-full card bg-surface-200-700-token">
<button class="w-full btn !scale-100" disabled={displayedPath.length <= 1 || pendingDisplay || pendingValidCheck} on:click={() => { path = displayedPath.split('/').slice(0, -1).join('/'); valid = false; }}>
<button class="w-full btn !scale-100" disabled={displayedPath.length <= 1 || pendingDisplay || pendingValidCheck} on:click={() => { path = parentPath(displayedPath); valid = false; }}>
<SvgIcon
class="h-5 w-5"
icon={mdiSubdirectoryArrowLeft} />
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/lib/components/modals/ServerManager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,13 @@
placeholder="C:\Path\To\Server"
type="text"
bind:value={newServerPath}/>
<div class="sm:col-start-2 col-span-2">
<RemoteServerPicker
basePath=""
bind:path={newServerPath}
bind:valid={isPathValid}
/>
</div>
{/if}
<button
class="btn h-full text-sm bg-primary-600 text-secondary-900 col-start-2 sm:col-start-4 row-start-1"
Expand Down
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ require (
github.com/samber/slog-multi v1.0.2
github.com/satisfactorymodding/ficsit-cli v0.5.1-0.20240201233422-5e9fe2aebbd5
github.com/satisfactorymodding/ficsit-resolver v0.0.2
github.com/shirou/gopsutil/v3 v3.24.3
github.com/spf13/viper v1.18.1
github.com/tawesoft/golib/v2 v2.10.0
github.com/wailsapp/wails/v2 v2.8.0
github.com/zishang520/engine.io v1.5.12
github.com/zishang520/socket.io v1.3.2
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611
golang.org/x/sys v0.16.0
golang.org/x/sync v0.5.0
golang.org/x/sys v0.18.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
howett.net/plist v1.0.1
)
Expand Down Expand Up @@ -63,6 +65,7 @@ require (
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/sftp v1.13.6 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/pterm/pterm v0.12.72 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
Expand All @@ -82,12 +85,12 @@ require (
github.com/wailsapp/go-webview2 v1.0.10 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zishang520/engine.io-go-parser v1.2.3 // indirect
github.com/zishang520/socket.io-go-parser v1.0.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
Loading

0 comments on commit 15eed7e

Please sign in to comment.