diff --git a/src/backend/utils.ts b/src/backend/utils.ts index 7bd761c60..5548e4756 100644 --- a/src/backend/utils.ts +++ b/src/backend/utils.ts @@ -80,7 +80,7 @@ import { deviceNameCache, vendorNameCache } from './utils/systeminfo/gpu/pci_ids' -import { copyFile, lstat, mkdir, readdir } from 'fs/promises' +import { access, constants, copyFile, lstat, mkdir, readdir } from 'fs/promises' import { GameConfig } from './game_config' const execAsync = promisify(exec) @@ -1306,14 +1306,43 @@ export const writeConfig = (appName: string, config: Partial) => { } } +// Helper function to check permissions +async function checkPermissions(src: string, dest: string) { + try { + // Check if we can read from source + await access(src, constants.R_OK) + + // Check if destination parent directory exists and is writable + const destDir = dirname(dest) + try { + await access(destDir, constants.W_OK) + } catch { + throw new Error( + `No write permission for destination directory: ${destDir}` + ) + } + } catch (error) { + if (error instanceof Error) { + throw new Error(`Permission error: ${error.message}`) + } + throw error + } +} + +const COPY_TIMEOUT_MS = 30000 // wait time before throwing a timeout error export async function copyRecursiveAsync(src: string, dest: string) { if (!existsSync(src) || !src || !dest) { return } + + // Check permissions before proceeding + await checkPermissions(src, dest) + const stats = await lstat(src) if (stats.isSymbolicLink()) { return // Skip symbolic links } + if (stats.isDirectory()) { try { await mkdir(dest, { recursive: true }) @@ -1321,16 +1350,32 @@ export async function copyRecursiveAsync(src: string, dest: string) { for (const file of files) { const srcFile = join(src, file) const destFile = join(dest, file) - await copyRecursiveAsync(srcFile, destFile) + await Promise.race([ + copyRecursiveAsync(srcFile, destFile), + wait(COPY_TIMEOUT_MS).then(() => { + throw new Error( + `Timeout (${COPY_TIMEOUT_MS}ms) copying ${srcFile} to ${destFile}` + ) + }) + ]) } } catch (error) { logError(`Failed to copy ${src} to ${dest}: ${error}`, LogPrefix.Backend) + throw error } } else { try { - await copyFile(src, dest) + await Promise.race([ + copyFile(src, dest), + wait(COPY_TIMEOUT_MS).then(() => { + throw new Error( + `Timeout (${COPY_TIMEOUT_MS}ms) copying ${src} to ${dest}` + ) + }) + ]) } catch (error) { logError(`Failed to copy ${src} to ${dest}: ${error}`, LogPrefix.Backend) + throw error } } }