Skip to content

Commit

Permalink
[Ref/Fix] Improving checkDiskSpace (#3440)
Browse files Browse the repository at this point in the history
* Move the "Path" schema into its own file

This is going to be used outside Legendary in the next commit

* Remove the check-disk-space package

Doing this within Heroic allows us to use already-established patterns
(dynamic import, helper functions like `genericSpawnWrapper`, validation
using Zod)

* Remove getFirstExistingParentPath

This was a little overcomplicated, and lead to issues on Windows (where
you don't necessarily have an existing root folder)

* Fixup `isWritable`

- As Flavio mentioned, `access` doesn't seem to work on Windows, so I
  replaced that with some PowerShell commands that check the same thing
- We have to call isWritable with the full path, as you can of course
  modify permissions on any folder, not just on a root directory/mount
  point

Since this info is now always accurate, we might want to make the
respective warning on the Frontend an error instead
  • Loading branch information
CommandMC authored Mar 14, 2024
1 parent d6f1873 commit fcd30b4
Show file tree
Hide file tree
Showing 20 changed files with 220 additions and 90 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
"@xhmikosr/decompress": "9.0.1",
"@xhmikosr/decompress-targz": "7.0.0",
"axios": "0.26.1",
"check-disk-space": "3.3.1",
"classnames": "2.3.1",
"compare-versions": "6.1.0",
"crc": "4.3.2",
Expand Down
79 changes: 27 additions & 52 deletions src/backend/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import 'backend/updater'
import { autoUpdater } from 'electron-updater'
import { cpus } from 'os'
import {
access,
constants,
existsSync,
rmSync,
unlinkSync,
Expand All @@ -40,7 +38,6 @@ import {
import Backend from 'i18next-fs-backend'
import i18next from 'i18next'
import { join } from 'path'
import checkDiskSpace from 'check-disk-space'
import { DXVK, Winetricks } from './tools'
import { GameConfig } from './game_config'
import { GlobalConfig } from './config'
Expand All @@ -57,7 +54,6 @@ import {
showItemInFolder,
getFileSize,
detectVCRedist,
getFirstExistingParentPath,
getLatestReleases,
getShellPath,
getCurrentChangelog,
Expand Down Expand Up @@ -566,54 +562,27 @@ ipcMain.on('unlock', () => {
}
})

ipcMain.handle('checkDiskSpace', async (event, folder) => {
const parent = getFirstExistingParentPath(folder)
return new Promise<DiskSpaceData>((res) => {
access(parent, constants.W_OK, async (writeError) => {
const { free, size: diskSize } = await checkDiskSpace(folder).catch(
(checkSpaceError) => {
logError(
[
'Failed to check disk space for',
`"${folder}":`,
checkSpaceError.stack ?? `${checkSpaceError}`
],
LogPrefix.Backend
)
return { free: 0, size: 0 }
}
)
if (writeError) {
logWarning(
[
'Cannot write to',
`"${folder}":`,
writeError.stack ?? `${writeError}`
],
LogPrefix.Backend
)
}

const isValidFlatpakPath = !(
isFlatpak &&
folder.startsWith(process.env.XDG_RUNTIME_DIR || '/run/user/')
)

if (!isValidFlatpakPath) {
logWarning(`Install location was not granted sandbox access!`)
}

const ret = {
free,
diskSize,
message: `${getFileSize(free)} / ${getFileSize(diskSize)}`,
validPath: !writeError,
validFlatpakPath: isValidFlatpakPath
}
logDebug(`${JSON.stringify(ret)}`, LogPrefix.Backend)
res(ret)
})
})
ipcMain.handle('checkDiskSpace', async (_e, folder): Promise<DiskSpaceData> => {
// We only need to look at the root directory for used/free space
// Trying to query this for a directory that doesn't exist (which `folder`
// might be) will not work
const { root } = path.parse(folder)

// FIXME: Propagate errors
const parsedPath = Path.parse(folder)
const parsedRootPath = Path.parse(root)

const { freeSpace, totalSpace } = await getDiskInfo(parsedRootPath)
const pathIsWritable = await isWritable(parsedPath)
const pathIsFlatpakAccessible = isAccessibleWithinFlatpakSandbox(parsedPath)

return {
free: freeSpace,
diskSize: totalSpace,
validPath: pathIsWritable,
validFlatpakPath: pathIsFlatpakAccessible,
message: `${getFileSize(freeSpace)} / ${getFileSize(totalSpace)}`
}
})

ipcMain.handle('isFrameless', () => isFrameless())
Expand Down Expand Up @@ -1744,3 +1713,9 @@ import './wiki_game_info/ipc_handler'
import './recent_games/ipc_handler'
import './tools/ipc_handler'
import './progress_bar'
import {
getDiskInfo,
isAccessibleWithinFlatpakSandbox,
isWritable
} from './utils/filesystem'
import { Path } from './schemas'
10 changes: 10 additions & 0 deletions src/backend/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod'
import path from 'path'

const Path = z
.string()
.refine((val) => path.parse(val).root, 'Path is not valid')
.brand('Path')
type Path = z.infer<typeof Path>

export { Path }
10 changes: 3 additions & 7 deletions src/backend/storeManagers/legendary/commands/base.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { z } from 'zod'
import path from 'path'
import { hasGame } from '../library'
import { existsSync } from 'graceful-fs'

import { Path } from 'backend/schemas'
import { hasGame } from '../library'

export const LegendaryAppName = z
.string()
.refine((val) => hasGame(val), {
Expand All @@ -17,12 +19,6 @@ export type LegendaryPlatform = z.infer<typeof LegendaryPlatform>
export const NonEmptyString = z.string().min(1).brand('NonEmptyString')
export type NonEmptyString = z.infer<typeof NonEmptyString>

export const Path = z
.string()
.refine((val) => path.parse(val).root, 'Path is not valid')
.brand('Path')
export type Path = z.infer<typeof Path>

export const PositiveInteger = z
.number()
.int()
Expand Down
2 changes: 1 addition & 1 deletion src/backend/storeManagers/legendary/commands/egl_sync.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Path } from './base'
import type { Path } from 'backend/schemas'

interface EglSyncCommand {
subcommand: 'egl-sync'
Expand Down
3 changes: 2 additions & 1 deletion src/backend/storeManagers/legendary/commands/eos_overlay.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { LegendaryAppName, NonEmptyString, Path, ValidWinePrefix } from './base'
import type { Path } from 'backend/schemas'
import type { LegendaryAppName, NonEmptyString, ValidWinePrefix } from './base'

const EosOverlayAction = z.enum([
'install',
Expand Down
3 changes: 2 additions & 1 deletion src/backend/storeManagers/legendary/commands/import.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LegendaryAppName, LegendaryPlatform, Path } from './base'
import type { Path } from 'backend/schemas'
import type { LegendaryAppName, LegendaryPlatform } from './base'

interface ImportCommand {
subcommand: 'import'
Expand Down
4 changes: 2 additions & 2 deletions src/backend/storeManagers/legendary/commands/install.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
import type { Path } from 'backend/schemas'
import type {
PositiveInteger,
LegendaryAppName,
NonEmptyString,
Path,
URL,
URI,
LegendaryPlatform
Expand Down
3 changes: 2 additions & 1 deletion src/backend/storeManagers/legendary/commands/launch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LegendaryAppName, NonEmptyString, Path } from './base'
import type { Path } from 'backend/schemas'
import type { LegendaryAppName, NonEmptyString } from './base'

interface LaunchCommand {
subcommand: 'launch'
Expand Down
3 changes: 2 additions & 1 deletion src/backend/storeManagers/legendary/commands/move.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LegendaryAppName, Path } from './base'
import type { Path } from 'backend/schemas'
import type { LegendaryAppName } from './base'

interface MoveCommand {
subcommand: 'move'
Expand Down
3 changes: 2 additions & 1 deletion src/backend/storeManagers/legendary/commands/sync_saves.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LegendaryAppName, Path } from './base'
import type { Path } from 'backend/schemas'
import type { LegendaryAppName } from './base'

interface SyncSavesCommand {
subcommand: 'sync-saves'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { callAbortController } from 'backend/utils/aborthandler/aborthandler'
import { sendGameStatusUpdate } from 'backend/utils'
import { gameManagerMap } from '../..'
import { LegendaryCommand } from '../commands'
import { Path, ValidWinePrefix } from '../commands/base'
import { ValidWinePrefix } from '../commands/base'
import { setCurrentDownloadSize } from '../games'
import { runRunnerCommand as runLegendaryCommand } from '../library'
import { Path } from 'backend/schemas'

import type { Runner } from 'common/types'

Expand Down
2 changes: 1 addition & 1 deletion src/backend/storeManagers/legendary/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ import {
LegendaryAppName,
LegendaryPlatform,
NonEmptyString,
Path,
PositiveInteger
} from './commands/base'
import { LegendaryCommand } from './commands'
import { Path } from 'backend/schemas'

/**
* Alias for `LegendaryLibrary.listUpdateableGames`
Expand Down
3 changes: 2 additions & 1 deletion src/backend/storeManagers/legendary/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ import axios from 'axios'
import { app } from 'electron'
import { copySync } from 'fs-extra'
import { LegendaryCommand } from './commands'
import { LegendaryAppName, LegendaryPlatform, Path } from './commands/base'
import { LegendaryAppName, LegendaryPlatform } from './commands/base'
import { Path } from 'backend/schemas'
import shlex from 'shlex'
import { Entries } from 'type-fest'

Expand Down
13 changes: 0 additions & 13 deletions src/backend/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,18 +679,6 @@ function detectVCRedist(mainWindow: BrowserWindow) {
})
}

function getFirstExistingParentPath(directoryPath: string): string {
let parentDirectoryPath = directoryPath
let parentDirectoryFound = existsSync(parentDirectoryPath)

while (!parentDirectoryFound) {
parentDirectoryPath = normalize(parentDirectoryPath + '/..')
parentDirectoryFound = existsSync(parentDirectoryPath)
}

return parentDirectoryPath !== '.' ? parentDirectoryPath : ''
}

const getLatestReleases = async (): Promise<Release[]> => {
const newReleases: Release[] = []
logInfo('Checking for new Heroic Updates', LogPrefix.Backend)
Expand Down Expand Up @@ -1474,7 +1462,6 @@ export {
shutdownWine,
getInfo,
getShellPath,
getFirstExistingParentPath,
getLatestReleases,
getWineFromProton,
getFileSize,
Expand Down
3 changes: 2 additions & 1 deletion src/backend/utils/compatibility_layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { homedir } from 'os'
import { dirname, join } from 'path'
import { PlistObject, parse as plistParse } from 'plist'
import LaunchCommand from '../storeManagers/legendary/commands/launch'
import { NonEmptyString, Path } from '../storeManagers/legendary/commands/base'
import { NonEmptyString } from '../storeManagers/legendary/commands/base'
import { Path } from 'backend/schemas'

/**
* Loads the default wine installation path and version.
Expand Down
45 changes: 45 additions & 0 deletions src/backend/utils/filesystem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Path } from 'backend/schemas'
import { isFlatpak } from 'backend/constants'

interface DiskInfo {
freeSpace: number
totalSpace: number
}

async function getDiskInfo(path: Path): Promise<DiskInfo> {
switch (process.platform) {
case 'linux':
case 'darwin': {
const { getDiskInfo_unix } = await import('./unix')
return getDiskInfo_unix(path)
}
case 'win32': {
const { getDiskInfo_windows } = await import('./windows')
return getDiskInfo_windows(path)
}
default:
return { freeSpace: 0, totalSpace: 0 }
}
}

async function isWritable(path: Path): Promise<boolean> {
switch (process.platform) {
case 'linux':
case 'darwin': {
const { isWritable_unix } = await import('./unix')
return isWritable_unix(path)
}
case 'win32': {
const { isWritable_windows } = await import('./windows')
return isWritable_windows(path)
}
default:
return false
}
}

const isAccessibleWithinFlatpakSandbox = (path: Path): boolean =>
!isFlatpak || !path.startsWith(process.env.XDG_RUNTIME_DIR || '/run/user/')

export { getDiskInfo, isWritable, isAccessibleWithinFlatpakSandbox }
export type { DiskInfo }
24 changes: 24 additions & 0 deletions src/backend/utils/filesystem/unix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { genericSpawnWrapper } from '../os/processes'
import { access } from 'fs/promises'

import type { Path } from 'backend/schemas'
import type { DiskInfo } from './index'

async function getDiskInfo_unix(path: Path): Promise<DiskInfo> {
const { stdout } = await genericSpawnWrapper('df', ['-P', '-k', path])
const lineSplit = stdout.split('\n')[1].split(/\s+/)
const [, totalSpaceKiBStr, , freeSpaceKiBStr] = lineSplit
return {
totalSpace: Number(totalSpaceKiBStr ?? 0) * 1024,
freeSpace: Number(freeSpaceKiBStr ?? 0) * 1024
}
}

async function isWritable_unix(path: Path): Promise<boolean> {
return access(path).then(
() => true,
() => false
)
}

export { getDiskInfo_unix, isWritable_unix }
Loading

0 comments on commit fcd30b4

Please sign in to comment.