Skip to content

Commit 32a1552

Browse files
committed
fix occasional issue in backup/restore, revert recent change
1 parent add2af3 commit 32a1552

File tree

4 files changed

+24
-88
lines changed

4 files changed

+24
-88
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ All notable changes to `homebridge-config-ui-x` will be documented in this file.
1818

1919
- improvements to various accessory tiles and modals
2020
- improvements to plugin config validation
21+
- fix occasional issue in backup/restore, revert recent change
2122

2223
### Homebridge Dependencies
2324

src/modules/backup/backup.service.ts

Lines changed: 17 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export class BackupService {
193193
if (this.configService.ui.scheduledBackupPath) {
194194
// If using a custom backup path, check it exists
195195
if (!await pathExists(this.configService.instanceBackupPath)) {
196-
throw new Error(`Custom instance backup path does not exist: ${this.configService.instanceBackupPath}`)
196+
throw new Error('Custom instance backup path does not exist')
197197
}
198198

199199
try {
@@ -308,47 +308,27 @@ export class BackupService {
308308
* Downloads a scheduled backup .tar.gz
309309
*/
310310
async getScheduledBackup(backupId: string): Promise<StreamableFile> {
311-
// Validate backupId to ensure it matches the expected pattern
312-
if (!/^[0-9A-Z]{12}\.\d{9,15}$/i.test(backupId)) {
313-
throw new NotFoundException()
314-
}
311+
const backupPath = resolve(this.configService.instanceBackupPath, `homebridge-backup-${backupId}.tar.gz`)
315312

316-
let backupPath = resolve(this.configService.instanceBackupPath, `homebridge-backup-${backupId}.tar.gz`)
317-
try {
318-
backupPath = await realpath(backupPath)
319-
} catch (e) {
313+
// Check the file exists
314+
if (!await pathExists(backupPath)) {
320315
throw new NotFoundException()
321316
}
322317

323-
// Ensure the backupPath is within the instanceBackupPath
324-
if (!backupPath.startsWith(this.configService.instanceBackupPath)) {
325-
throw new BadRequestException(`Invalid backup path: ${backupPath} does not start with ${this.configService.instanceBackupPath}`)
326-
}
327-
328318
return new StreamableFile(createReadStream(backupPath))
329319
}
330320

331321
/**
332322
* Removes a scheduled backup .tar.gz
333323
*/
334324
async deleteScheduledBackup(backupId: string): Promise<void> {
335-
// Validate backupId to ensure it matches the expected pattern
336-
if (!/^[0-9A-Z]{12}\.\d{9,15}$/i.test(backupId)) {
337-
throw new NotFoundException()
338-
}
325+
const backupPath = resolve(this.configService.instanceBackupPath, `homebridge-backup-${backupId}.tar.gz`)
339326

340-
let backupPath = resolve(this.configService.instanceBackupPath, `homebridge-backup-${backupId}.tar.gz`)
341-
try {
342-
backupPath = await realpath(backupPath)
343-
} catch (e) {
327+
// Check the file exists
328+
if (!await pathExists(backupPath)) {
344329
throw new NotFoundException()
345330
}
346331

347-
// Ensure the backupPath is within the instanceBackupPath
348-
if (!backupPath.startsWith(this.configService.instanceBackupPath)) {
349-
throw new BadRequestException(`Invalid backup path: ${backupPath} does not start with ${this.configService.instanceBackupPath}`)
350-
}
351-
352332
try {
353333
await remove(backupPath)
354334
this.logger.warn(`Scheduled backup ${backupId} deleted by request.`)
@@ -362,23 +342,13 @@ export class BackupService {
362342
* Restore a scheduled backup .tar.gz
363343
*/
364344
async restoreScheduledBackup(backupId: string): Promise<void> {
365-
// Validate backupId to ensure it matches the expected pattern
366-
if (!/^[0-9A-Z]{12}\.\d{9,15}$/i.test(backupId)) {
367-
throw new NotFoundException()
368-
}
345+
const backupPath = resolve(this.configService.instanceBackupPath, `homebridge-backup-${backupId}.tar.gz`)
369346

370-
let backupPath = resolve(this.configService.instanceBackupPath, `homebridge-backup-${backupId}.tar.gz`)
371-
try {
372-
backupPath = await realpath(backupPath)
373-
} catch (e) {
347+
// Check the file exists
348+
if (!await pathExists(backupPath)) {
374349
throw new NotFoundException()
375350
}
376351

377-
// Ensure the backupPath is within the instanceBackupPath
378-
if (!backupPath.startsWith(this.configService.instanceBackupPath)) {
379-
throw new BadRequestException(`Invalid backup path: ${backupPath} does not start with ${this.configService.instanceBackupPath}`)
380-
}
381-
382352
// Clear restore directory
383353
this.restoreDirectory = undefined
384354

@@ -393,18 +363,18 @@ export class BackupService {
393363
this.restoreDirectory = restoreDir
394364
}
395365

396-
// Remove temp files (called when download finished)
397-
async cleanup(backupDir: string) {
398-
await remove(resolve(backupDir))
399-
this.logger.log(`Backup complete, removing ${backupDir}.`)
400-
}
401-
402366
/**
403367
* Create and download backup archive of the current homebridge instance
404368
*/
405369
async downloadBackup(reply: FastifyReply): Promise<StreamableFile> {
406370
const { backupDir, backupPath, backupFileName } = await this.createBackup()
407371

372+
// Remove temp files (called when download finished)
373+
async function cleanup() {
374+
await remove(resolve(backupDir))
375+
this.logger.log(`Backup complete, removing ${backupDir}.`)
376+
}
377+
408378
// Set download headers
409379
reply.raw.setHeader('Content-type', 'application/octet-stream')
410380
reply.raw.setHeader('Content-disposition', `attachment; filename=${backupFileName}`)
@@ -415,10 +385,7 @@ export class BackupService {
415385
reply.raw.setHeader('access-control-allow-origin', 'http://localhost:4200')
416386
}
417387

418-
// Remove temp files (called when download finished)
419-
const cleanup = this.cleanup.bind(this, backupDir)
420-
421-
return new StreamableFile(createReadStream(backupPath).on('close', cleanup))
388+
return new StreamableFile(createReadStream(backupPath).on('close', cleanup.bind(this)))
422389
}
423390

424391
/**

src/modules/plugins/plugins.service.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -929,18 +929,11 @@ export class PluginsService {
929929

930930
const schemaPath = resolve(plugin.installPath, pluginName, 'config.schema.json')
931931

932-
if (!schemaPath.startsWith(plugin.installPath)) {
933-
throw new BadRequestException('Invalid plugin path')
934-
}
935-
936932
let configSchema = await readJson(schemaPath)
937933

938934
// check to see if this plugin implements dynamic schemas
939935
if (configSchema.dynamicSchemaVersion) {
940936
const dynamicSchemaPath = resolve(this.configService.storagePath, `.${pluginName}-v${configSchema.dynamicSchemaVersion}.schema.json`)
941-
if (!dynamicSchemaPath.startsWith(this.configService.storagePath)) {
942-
throw new BadRequestException('Invalid dynamic schema path')
943-
}
944937
this.logger.log(`[${pluginName}] dynamic schema path: ${dynamicSchemaPath}.`)
945938
if (existsSync(dynamicSchemaPath)) {
946939
try {

src/modules/server/server.service.ts

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,8 @@ export class ServerService {
5454
}
5555

5656
private async deleteSingleDeviceAccessories(id: string, cachedAccessoriesDir: string) {
57-
const cachedAccessories = resolve(cachedAccessoriesDir, `cachedAccessories.${id}`)
58-
const cachedAccessoriesBackup = resolve(cachedAccessoriesDir, `.cachedAccessories.${id}.bak`)
59-
60-
if (!cachedAccessories.startsWith(cachedAccessoriesDir) || !cachedAccessoriesBackup.startsWith(cachedAccessoriesDir)) {
61-
throw new BadRequestException('Invalid device ID.')
62-
}
57+
const cachedAccessories = join(cachedAccessoriesDir, `cachedAccessories.${id}`)
58+
const cachedAccessoriesBackup = join(cachedAccessoriesDir, `.cachedAccessories.${id}.bak`)
6359

6460
if (await pathExists(cachedAccessories)) {
6561
await unlink(cachedAccessories)
@@ -73,14 +69,9 @@ export class ServerService {
7369
}
7470

7571
private async deleteSingleDevicePairing(id: string, resetPairingInfo: boolean) {
76-
const persistPath = resolve(this.configService.storagePath, 'persist')
77-
const accessoryInfo = resolve(persistPath, `AccessoryInfo.${id}.json`)
78-
const identifierCache = resolve(persistPath, `IdentifierCache.${id}.json`)
79-
80-
// Validate paths
81-
if (!accessoryInfo.startsWith(persistPath) || !identifierCache.startsWith(persistPath)) {
82-
throw new BadRequestException('Invalid device ID.')
83-
}
72+
const persistPath = join(this.configService.storagePath, 'persist')
73+
const accessoryInfo = join(persistPath, `AccessoryInfo.${id}.json`)
74+
const identifierCache = join(persistPath, `IdentifierCache.${id}.json`)
8475

8576
// Only available for child bridges
8677
if (resetPairingInfo) {
@@ -116,17 +107,11 @@ export class ServerService {
116107
}
117108
}
118109

119-
if (!accessoryInfo.startsWith(persistPath) || !identifierCache.startsWith(persistPath)) {
120-
throw new BadRequestException('Invalid device ID.')
121-
}
122110
if (await pathExists(accessoryInfo)) {
123111
await unlink(accessoryInfo)
124112
this.logger.warn(`Bridge ${id} reset: removed ${accessoryInfo}.`)
125113
}
126114

127-
if (!identifierCache.startsWith(persistPath)) {
128-
throw new BadRequestException('Invalid identifierCache path.')
129-
}
130115
if (await pathExists(identifierCache)) {
131116
await unlink(identifierCache)
132117
this.logger.warn(`Bridge ${id} reset: removed ${identifierCache}.`)
@@ -217,15 +202,10 @@ export class ServerService {
217202
*/
218203
public async getDevicePairingById(deviceId: string, configFile = null) {
219204
const persistPath = join(this.configService.storagePath, 'persist')
220-
const devicePath = resolve(persistPath, `AccessoryInfo.${deviceId}.json`)
221-
222-
if (!devicePath.startsWith(persistPath)) {
223-
throw new BadRequestException('Invalid device ID')
224-
}
225205

226206
let device: any
227207
try {
228-
device = await readJson(devicePath)
208+
device = await readJson(join(persistPath, `AccessoryInfo.${deviceId}.json`))
229209
} catch (e) {
230210
throw new NotFoundException()
231211
}
@@ -390,11 +370,6 @@ export class ServerService {
390370

391371
const cachedAccessoriesPath = resolve(this.configService.storagePath, 'accessories', cacheFile)
392372

393-
if (!cachedAccessoriesPath.startsWith(resolve(this.configService.storagePath, 'accessories'))) {
394-
this.logger.error(`Invalid cache file path: ${cacheFile}`)
395-
throw new BadRequestException('Invalid cache file path')
396-
}
397-
398373
this.logger.warn(`Shutting down Homebridge before removing cached accessory ${uuid}...`)
399374

400375
// Wait for homebridge to stop.

0 commit comments

Comments
 (0)