Skip to content

Commit 394ecee

Browse files
authored
[Fix] Make checkDiskSpace look at the correct drive again (#3654)
* Look at the correct drive on Unix * Add tests
1 parent 91cd627 commit 394ecee

File tree

5 files changed

+149
-8
lines changed

5 files changed

+149
-8
lines changed

src/backend/main.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -563,16 +563,10 @@ ipcMain.on('unlock', () => {
563563
})
564564

565565
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-
571566
// FIXME: Propagate errors
572567
const parsedPath = Path.parse(folder)
573-
const parsedRootPath = Path.parse(root)
574568

575-
const { freeSpace, totalSpace } = await getDiskInfo(parsedRootPath)
569+
const { freeSpace, totalSpace } = await getDiskInfo(parsedPath)
576570
const pathIsWritable = await isWritable(parsedPath)
577571
const pathIsFlatpakAccessible = isAccessibleWithinFlatpakSandbox(parsedPath)
578572

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import fs from 'fs'
2+
import * as util_os_processes from '../../os/processes'
3+
import { getDiskInfo_unix } from '../unix'
4+
import type { Path } from 'backend/schemas'
5+
6+
describe('getDiskInfo_unix', () => {
7+
it('Works with root path', async () => {
8+
const accessSyp = jest
9+
.spyOn(fs.promises, 'access')
10+
.mockImplementation(async () => Promise.resolve())
11+
const spawnWrapperSpy = jest
12+
.spyOn(util_os_processes, 'genericSpawnWrapper')
13+
.mockImplementation(async () => {
14+
return Promise.resolve({
15+
stdout:
16+
'Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda1 100 90 10 90% /',
17+
stderr: '',
18+
exitCode: null,
19+
signalName: null
20+
})
21+
})
22+
23+
const ret = await getDiskInfo_unix('/' as Path)
24+
expect(ret.totalSpace).toBe(100 * 1024)
25+
expect(ret.freeSpace).toBe(10 * 1024)
26+
expect(accessSyp).toHaveBeenCalledTimes(1)
27+
expect(spawnWrapperSpy).toHaveBeenCalledWith('df', ['-P', '-k', '/'])
28+
expect(spawnWrapperSpy).toHaveBeenCalledTimes(1)
29+
})
30+
31+
it('Works with nested path', async () => {
32+
const accessSyp = jest
33+
.spyOn(fs.promises, 'access')
34+
.mockImplementation(async (path) => {
35+
if (path === '/foo/bar/baz') {
36+
return Promise.reject()
37+
}
38+
return Promise.resolve()
39+
})
40+
const spawnWrapperSpy = jest
41+
.spyOn(util_os_processes, 'genericSpawnWrapper')
42+
.mockImplementation(async () => {
43+
return Promise.resolve({
44+
stdout:
45+
'Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda1 100 90 10 90% /foo/bar',
46+
stderr: '',
47+
exitCode: null,
48+
signalName: null
49+
})
50+
})
51+
52+
const ret = await getDiskInfo_unix('/foo/bar/baz' as Path)
53+
expect(ret.totalSpace).toBe(100 * 1024)
54+
expect(ret.freeSpace).toBe(10 * 1024)
55+
expect(accessSyp).toHaveBeenCalledTimes(2)
56+
expect(spawnWrapperSpy).toHaveBeenCalledWith('df', ['-P', '-k', '/foo/bar'])
57+
expect(spawnWrapperSpy).toHaveBeenCalledTimes(1)
58+
})
59+
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as util_os_processes from '../../os/processes'
2+
import { getDiskInfo_windows } from '../windows'
3+
import type { Path } from 'backend/schemas'
4+
5+
describe('getDiskInfo_windows', () => {
6+
it('Works with root path', async () => {
7+
const spawnWrapperSpy = jest
8+
.spyOn(util_os_processes, 'genericSpawnWrapper')
9+
.mockImplementation(async () => {
10+
return Promise.resolve({
11+
stdout: JSON.stringify([{ Caption: 'C:', FreeSpace: 10, Size: 100 }]),
12+
stderr: '',
13+
exitCode: null,
14+
signalName: null
15+
})
16+
})
17+
18+
const ret = await getDiskInfo_windows('C:' as Path)
19+
expect(ret.totalSpace).toBe(100)
20+
expect(ret.freeSpace).toBe(10)
21+
expect(spawnWrapperSpy).toHaveBeenCalledWith('powershell', [
22+
'Get-CimInstance',
23+
'-Class',
24+
'Win32_LogicalDisk',
25+
'-Property',
26+
'Caption,FreeSpace,Size',
27+
'|',
28+
'Select-Object',
29+
'Caption,FreeSpace,Size',
30+
'|',
31+
'ConvertTo-Json',
32+
'-Compress'
33+
])
34+
expect(spawnWrapperSpy).toHaveBeenCalledTimes(1)
35+
})
36+
37+
it('Works with nested path', async () => {
38+
const spawnWrapperSpy = jest
39+
.spyOn(util_os_processes, 'genericSpawnWrapper')
40+
.mockImplementation(async () => {
41+
return Promise.resolve({
42+
stdout: JSON.stringify([{ Caption: 'C:', FreeSpace: 10, Size: 100 }]),
43+
stderr: '',
44+
exitCode: null,
45+
signalName: null
46+
})
47+
})
48+
49+
const ret = await getDiskInfo_windows('C:/foo/bar/baz' as Path)
50+
expect(ret.totalSpace).toBe(100)
51+
expect(ret.freeSpace).toBe(10)
52+
expect(spawnWrapperSpy).toHaveBeenCalledWith('powershell', [
53+
'Get-CimInstance',
54+
'-Class',
55+
'Win32_LogicalDisk',
56+
'-Property',
57+
'Caption,FreeSpace,Size',
58+
'|',
59+
'Select-Object',
60+
'Caption,FreeSpace,Size',
61+
'|',
62+
'ConvertTo-Json',
63+
'-Compress'
64+
])
65+
expect(spawnWrapperSpy).toHaveBeenCalledTimes(1)
66+
})
67+
})

src/backend/utils/filesystem/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ interface DiskInfo {
66
totalSpace: number
77
}
88

9+
/**
10+
* Gathers information about the disk `path` is on.
11+
* `path` does not have to exist.
12+
*/
913
async function getDiskInfo(path: Path): Promise<DiskInfo> {
1014
switch (process.platform) {
1115
case 'linux':

src/backend/utils/filesystem/unix.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { genericSpawnWrapper } from '../os/processes'
22
import { access } from 'fs/promises'
3+
import { join } from 'path'
34

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

78
async function getDiskInfo_unix(path: Path): Promise<DiskInfo> {
8-
const { stdout } = await genericSpawnWrapper('df', ['-P', '-k', path])
9+
const rootPath = await findFirstExistingPath(path)
10+
11+
const { stdout } = await genericSpawnWrapper('df', ['-P', '-k', rootPath])
912
const lineSplit = stdout.split('\n')[1].split(/\s+/)
1013
const [, totalSpaceKiBStr, , freeSpaceKiBStr] = lineSplit
1114
return {
@@ -14,6 +17,20 @@ async function getDiskInfo_unix(path: Path): Promise<DiskInfo> {
1417
}
1518
}
1619

20+
/**
21+
* Finds the first existing path in the path's hierarchy
22+
* @example
23+
* findFirstExistingPath('/foo/bar/baz')
24+
* // => '/foo/bar/baz' if it exists, otherwise '/foo/bar', otherwise '/foo', otherwise '/'
25+
*/
26+
async function findFirstExistingPath(path: Path): Promise<Path> {
27+
let maybeExistingPath = path
28+
while (!(await isWritable_unix(maybeExistingPath))) {
29+
maybeExistingPath = join(maybeExistingPath, '..') as Path
30+
}
31+
return maybeExistingPath
32+
}
33+
1734
async function isWritable_unix(path: Path): Promise<boolean> {
1835
return access(path).then(
1936
() => true,

0 commit comments

Comments
 (0)