Skip to content

Commit fcd30b4

Browse files
authored
[Ref/Fix] Improving checkDiskSpace (#3440)
* 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
1 parent d6f1873 commit fcd30b4

20 files changed

+220
-90
lines changed

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
"@xhmikosr/decompress": "9.0.1",
6767
"@xhmikosr/decompress-targz": "7.0.0",
6868
"axios": "0.26.1",
69-
"check-disk-space": "3.3.1",
7069
"classnames": "2.3.1",
7170
"compare-versions": "6.1.0",
7271
"crc": "4.3.2",

src/backend/main.ts

+27-52
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import 'backend/updater'
2626
import { autoUpdater } from 'electron-updater'
2727
import { cpus } from 'os'
2828
import {
29-
access,
30-
constants,
3129
existsSync,
3230
rmSync,
3331
unlinkSync,
@@ -40,7 +38,6 @@ import {
4038
import Backend from 'i18next-fs-backend'
4139
import i18next from 'i18next'
4240
import { join } from 'path'
43-
import checkDiskSpace from 'check-disk-space'
4441
import { DXVK, Winetricks } from './tools'
4542
import { GameConfig } from './game_config'
4643
import { GlobalConfig } from './config'
@@ -57,7 +54,6 @@ import {
5754
showItemInFolder,
5855
getFileSize,
5956
detectVCRedist,
60-
getFirstExistingParentPath,
6157
getLatestReleases,
6258
getShellPath,
6359
getCurrentChangelog,
@@ -566,54 +562,27 @@ ipcMain.on('unlock', () => {
566562
}
567563
})
568564

569-
ipcMain.handle('checkDiskSpace', async (event, folder) => {
570-
const parent = getFirstExistingParentPath(folder)
571-
return new Promise<DiskSpaceData>((res) => {
572-
access(parent, constants.W_OK, async (writeError) => {
573-
const { free, size: diskSize } = await checkDiskSpace(folder).catch(
574-
(checkSpaceError) => {
575-
logError(
576-
[
577-
'Failed to check disk space for',
578-
`"${folder}":`,
579-
checkSpaceError.stack ?? `${checkSpaceError}`
580-
],
581-
LogPrefix.Backend
582-
)
583-
return { free: 0, size: 0 }
584-
}
585-
)
586-
if (writeError) {
587-
logWarning(
588-
[
589-
'Cannot write to',
590-
`"${folder}":`,
591-
writeError.stack ?? `${writeError}`
592-
],
593-
LogPrefix.Backend
594-
)
595-
}
596-
597-
const isValidFlatpakPath = !(
598-
isFlatpak &&
599-
folder.startsWith(process.env.XDG_RUNTIME_DIR || '/run/user/')
600-
)
601-
602-
if (!isValidFlatpakPath) {
603-
logWarning(`Install location was not granted sandbox access!`)
604-
}
605-
606-
const ret = {
607-
free,
608-
diskSize,
609-
message: `${getFileSize(free)} / ${getFileSize(diskSize)}`,
610-
validPath: !writeError,
611-
validFlatpakPath: isValidFlatpakPath
612-
}
613-
logDebug(`${JSON.stringify(ret)}`, LogPrefix.Backend)
614-
res(ret)
615-
})
616-
})
565+
ipcMain.handle('checkDiskSpace', async (_e, folder): Promise<DiskSpaceData> => {
566+
// We only need to look at the root directory for used/free space
567+
// Trying to query this for a directory that doesn't exist (which `folder`
568+
// might be) will not work
569+
const { root } = path.parse(folder)
570+
571+
// FIXME: Propagate errors
572+
const parsedPath = Path.parse(folder)
573+
const parsedRootPath = Path.parse(root)
574+
575+
const { freeSpace, totalSpace } = await getDiskInfo(parsedRootPath)
576+
const pathIsWritable = await isWritable(parsedPath)
577+
const pathIsFlatpakAccessible = isAccessibleWithinFlatpakSandbox(parsedPath)
578+
579+
return {
580+
free: freeSpace,
581+
diskSize: totalSpace,
582+
validPath: pathIsWritable,
583+
validFlatpakPath: pathIsFlatpakAccessible,
584+
message: `${getFileSize(freeSpace)} / ${getFileSize(totalSpace)}`
585+
}
617586
})
618587

619588
ipcMain.handle('isFrameless', () => isFrameless())
@@ -1744,3 +1713,9 @@ import './wiki_game_info/ipc_handler'
17441713
import './recent_games/ipc_handler'
17451714
import './tools/ipc_handler'
17461715
import './progress_bar'
1716+
import {
1717+
getDiskInfo,
1718+
isAccessibleWithinFlatpakSandbox,
1719+
isWritable
1720+
} from './utils/filesystem'
1721+
import { Path } from './schemas'

src/backend/schemas.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { z } from 'zod'
2+
import path from 'path'
3+
4+
const Path = z
5+
.string()
6+
.refine((val) => path.parse(val).root, 'Path is not valid')
7+
.brand('Path')
8+
type Path = z.infer<typeof Path>
9+
10+
export { Path }

src/backend/storeManagers/legendary/commands/base.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { z } from 'zod'
22
import path from 'path'
3-
import { hasGame } from '../library'
43
import { existsSync } from 'graceful-fs'
54

5+
import { Path } from 'backend/schemas'
6+
import { hasGame } from '../library'
7+
68
export const LegendaryAppName = z
79
.string()
810
.refine((val) => hasGame(val), {
@@ -17,12 +19,6 @@ export type LegendaryPlatform = z.infer<typeof LegendaryPlatform>
1719
export const NonEmptyString = z.string().min(1).brand('NonEmptyString')
1820
export type NonEmptyString = z.infer<typeof NonEmptyString>
1921

20-
export const Path = z
21-
.string()
22-
.refine((val) => path.parse(val).root, 'Path is not valid')
23-
.brand('Path')
24-
export type Path = z.infer<typeof Path>
25-
2622
export const PositiveInteger = z
2723
.number()
2824
.int()

src/backend/storeManagers/legendary/commands/egl_sync.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Path } from './base'
1+
import type { Path } from 'backend/schemas'
22

33
interface EglSyncCommand {
44
subcommand: 'egl-sync'

src/backend/storeManagers/legendary/commands/eos_overlay.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod'
2-
import { LegendaryAppName, NonEmptyString, Path, ValidWinePrefix } from './base'
2+
import type { Path } from 'backend/schemas'
3+
import type { LegendaryAppName, NonEmptyString, ValidWinePrefix } from './base'
34

45
const EosOverlayAction = z.enum([
56
'install',

src/backend/storeManagers/legendary/commands/import.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { LegendaryAppName, LegendaryPlatform, Path } from './base'
1+
import type { Path } from 'backend/schemas'
2+
import type { LegendaryAppName, LegendaryPlatform } from './base'
23

34
interface ImportCommand {
45
subcommand: 'import'

src/backend/storeManagers/legendary/commands/install.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {
1+
import type { Path } from 'backend/schemas'
2+
import type {
23
PositiveInteger,
34
LegendaryAppName,
45
NonEmptyString,
5-
Path,
66
URL,
77
URI,
88
LegendaryPlatform

src/backend/storeManagers/legendary/commands/launch.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { LegendaryAppName, NonEmptyString, Path } from './base'
1+
import type { Path } from 'backend/schemas'
2+
import type { LegendaryAppName, NonEmptyString } from './base'
23

34
interface LaunchCommand {
45
subcommand: 'launch'

src/backend/storeManagers/legendary/commands/move.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { LegendaryAppName, Path } from './base'
1+
import type { Path } from 'backend/schemas'
2+
import type { LegendaryAppName } from './base'
23

34
interface MoveCommand {
45
subcommand: 'move'

src/backend/storeManagers/legendary/commands/sync_saves.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { LegendaryAppName, Path } from './base'
1+
import type { Path } from 'backend/schemas'
2+
import type { LegendaryAppName } from './base'
23

34
interface SyncSavesCommand {
45
subcommand: 'sync-saves'

src/backend/storeManagers/legendary/eos_overlay/eos_overlay.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { callAbortController } from 'backend/utils/aborthandler/aborthandler'
99
import { sendGameStatusUpdate } from 'backend/utils'
1010
import { gameManagerMap } from '../..'
1111
import { LegendaryCommand } from '../commands'
12-
import { Path, ValidWinePrefix } from '../commands/base'
12+
import { ValidWinePrefix } from '../commands/base'
1313
import { setCurrentDownloadSize } from '../games'
1414
import { runRunnerCommand as runLegendaryCommand } from '../library'
15+
import { Path } from 'backend/schemas'
1516

1617
import type { Runner } from 'common/types'
1718

src/backend/storeManagers/legendary/games.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ import {
7979
LegendaryAppName,
8080
LegendaryPlatform,
8181
NonEmptyString,
82-
Path,
8382
PositiveInteger
8483
} from './commands/base'
8584
import { LegendaryCommand } from './commands'
85+
import { Path } from 'backend/schemas'
8686

8787
/**
8888
* Alias for `LegendaryLibrary.listUpdateableGames`

src/backend/storeManagers/legendary/library.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ import axios from 'axios'
5353
import { app } from 'electron'
5454
import { copySync } from 'fs-extra'
5555
import { LegendaryCommand } from './commands'
56-
import { LegendaryAppName, LegendaryPlatform, Path } from './commands/base'
56+
import { LegendaryAppName, LegendaryPlatform } from './commands/base'
57+
import { Path } from 'backend/schemas'
5758
import shlex from 'shlex'
5859
import { Entries } from 'type-fest'
5960

src/backend/utils.ts

-13
Original file line numberDiff line numberDiff line change
@@ -679,18 +679,6 @@ function detectVCRedist(mainWindow: BrowserWindow) {
679679
})
680680
}
681681

682-
function getFirstExistingParentPath(directoryPath: string): string {
683-
let parentDirectoryPath = directoryPath
684-
let parentDirectoryFound = existsSync(parentDirectoryPath)
685-
686-
while (!parentDirectoryFound) {
687-
parentDirectoryPath = normalize(parentDirectoryPath + '/..')
688-
parentDirectoryFound = existsSync(parentDirectoryPath)
689-
}
690-
691-
return parentDirectoryPath !== '.' ? parentDirectoryPath : ''
692-
}
693-
694682
const getLatestReleases = async (): Promise<Release[]> => {
695683
const newReleases: Release[] = []
696684
logInfo('Checking for new Heroic Updates', LogPrefix.Backend)
@@ -1474,7 +1462,6 @@ export {
14741462
shutdownWine,
14751463
getInfo,
14761464
getShellPath,
1477-
getFirstExistingParentPath,
14781465
getLatestReleases,
14791466
getWineFromProton,
14801467
getFileSize,

src/backend/utils/compatibility_layers.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { homedir } from 'os'
1515
import { dirname, join } from 'path'
1616
import { PlistObject, parse as plistParse } from 'plist'
1717
import LaunchCommand from '../storeManagers/legendary/commands/launch'
18-
import { NonEmptyString, Path } from '../storeManagers/legendary/commands/base'
18+
import { NonEmptyString } from '../storeManagers/legendary/commands/base'
19+
import { Path } from 'backend/schemas'
1920

2021
/**
2122
* Loads the default wine installation path and version.

src/backend/utils/filesystem/index.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Path } from 'backend/schemas'
2+
import { isFlatpak } from 'backend/constants'
3+
4+
interface DiskInfo {
5+
freeSpace: number
6+
totalSpace: number
7+
}
8+
9+
async function getDiskInfo(path: Path): Promise<DiskInfo> {
10+
switch (process.platform) {
11+
case 'linux':
12+
case 'darwin': {
13+
const { getDiskInfo_unix } = await import('./unix')
14+
return getDiskInfo_unix(path)
15+
}
16+
case 'win32': {
17+
const { getDiskInfo_windows } = await import('./windows')
18+
return getDiskInfo_windows(path)
19+
}
20+
default:
21+
return { freeSpace: 0, totalSpace: 0 }
22+
}
23+
}
24+
25+
async function isWritable(path: Path): Promise<boolean> {
26+
switch (process.platform) {
27+
case 'linux':
28+
case 'darwin': {
29+
const { isWritable_unix } = await import('./unix')
30+
return isWritable_unix(path)
31+
}
32+
case 'win32': {
33+
const { isWritable_windows } = await import('./windows')
34+
return isWritable_windows(path)
35+
}
36+
default:
37+
return false
38+
}
39+
}
40+
41+
const isAccessibleWithinFlatpakSandbox = (path: Path): boolean =>
42+
!isFlatpak || !path.startsWith(process.env.XDG_RUNTIME_DIR || '/run/user/')
43+
44+
export { getDiskInfo, isWritable, isAccessibleWithinFlatpakSandbox }
45+
export type { DiskInfo }

src/backend/utils/filesystem/unix.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { genericSpawnWrapper } from '../os/processes'
2+
import { access } from 'fs/promises'
3+
4+
import type { Path } from 'backend/schemas'
5+
import type { DiskInfo } from './index'
6+
7+
async function getDiskInfo_unix(path: Path): Promise<DiskInfo> {
8+
const { stdout } = await genericSpawnWrapper('df', ['-P', '-k', path])
9+
const lineSplit = stdout.split('\n')[1].split(/\s+/)
10+
const [, totalSpaceKiBStr, , freeSpaceKiBStr] = lineSplit
11+
return {
12+
totalSpace: Number(totalSpaceKiBStr ?? 0) * 1024,
13+
freeSpace: Number(freeSpaceKiBStr ?? 0) * 1024
14+
}
15+
}
16+
17+
async function isWritable_unix(path: Path): Promise<boolean> {
18+
return access(path).then(
19+
() => true,
20+
() => false
21+
)
22+
}
23+
24+
export { getDiskInfo_unix, isWritable_unix }

0 commit comments

Comments
 (0)