mirror of
https://github.com/caprover/caprover
synced 2026-08-23 07:56:28 +00:00
Backup tar files currently use a filename that only encodes a
timestamp and the leader's IP, e.g.
caprover-backup-2026_04_10-19_30_00-1744312200000-ip-1_2_3_4.tar
When you run several CapRover instances behind a NAT or with
similar IPs, that filename is not enough to tell the backups apart
after downloading them.
This appends the swarm leader's hostname to the existing filename:
caprover-backup-...-ip-1_2_3_4-host-captain-prod.tar
The hostname is sanitized to a portable charset ([A-Za-z0-9._-]) so
it is safe in filesystems and HTTP Content-Disposition headers, and
the segment is omitted entirely when the leader has no hostname, so
existing single-node setups see no change beyond an additional
suffix when one is available.
The sanitization step is exposed as a small static helper
(`BackupManager.sanitizeHostnameForFilename`) so it can be unit
tested in isolation. Top-level cleanup hooks in the existing test
file are now gated on `process.env.CI`, matching how the
integration tests in the same file are gated, so the new pure
helper tests can run on a developer workstation.
Closes #1257
128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
import { copy, ensureDir, ensureFile, readJson, removeSync } from 'fs-extra'
|
|
import { isDeepStrictEqual } from 'util'
|
|
import { RestoringInfo } from '../src/models/BackupMeta'
|
|
import BackupManager from '../src/user/system/BackupManager'
|
|
import CaptainConstants from '../src/utils/CaptainConstants'
|
|
const BACKUP_FILE_PATH_ABSOLUTE = '/captain/backup.tar'
|
|
|
|
function cleanup() {
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return ensureFile(BACKUP_FILE_PATH_ABSOLUTE)
|
|
})
|
|
.then(function () {
|
|
return removeSync(BACKUP_FILE_PATH_ABSOLUTE)
|
|
})
|
|
.then(function () {
|
|
return ensureDir(CaptainConstants.restoreDirectoryPath)
|
|
})
|
|
.then(function () {
|
|
return removeSync(CaptainConstants.restoreDirectoryPath)
|
|
})
|
|
}
|
|
|
|
if (process.env.CI) {
|
|
beforeEach(() => {
|
|
return cleanup()
|
|
})
|
|
|
|
afterEach(() => {
|
|
return cleanup()
|
|
})
|
|
}
|
|
|
|
describe('BackupManager.sanitizeHostnameForFilename', () => {
|
|
test('returns the hostname unchanged when it only contains safe chars', () => {
|
|
expect(
|
|
BackupManager.sanitizeHostnameForFilename('captain-prod.example.com')
|
|
).toBe('captain-prod.example.com')
|
|
})
|
|
|
|
test('replaces filesystem-unsafe characters with underscores', () => {
|
|
expect(
|
|
BackupManager.sanitizeHostnameForFilename('host/with bad?chars')
|
|
).toBe('host_with_bad_chars')
|
|
})
|
|
|
|
test('returns an empty string for an empty hostname', () => {
|
|
expect(BackupManager.sanitizeHostnameForFilename('')).toBe('')
|
|
})
|
|
|
|
test('returns an empty string for an undefined hostname', () => {
|
|
expect(BackupManager.sanitizeHostnameForFilename(undefined)).toBe('')
|
|
})
|
|
})
|
|
|
|
if (process.env.CI) {
|
|
describe('backup tests [CI only]', backupTests)
|
|
} else {
|
|
describe.skip('backup tests [CI only]', backupTests)
|
|
}
|
|
|
|
function backupTests() {
|
|
test('No backup file', () => {
|
|
const bk = new BackupManager()
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return bk.checkAndPrepareRestoration()
|
|
})
|
|
.then(function (data) {
|
|
expect(data).toBeFalsy()
|
|
})
|
|
})
|
|
|
|
test('Test backup file', () => {
|
|
const bk = new BackupManager()
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return copy(
|
|
`${__dirname}/backup.tar`,
|
|
BACKUP_FILE_PATH_ABSOLUTE
|
|
)
|
|
})
|
|
.then(function () {
|
|
return bk.checkAndPrepareRestoration()
|
|
})
|
|
.then(function () {
|
|
return readJson(
|
|
CaptainConstants.restoreDirectoryPath +
|
|
'/restore-instructions.json'
|
|
)
|
|
})
|
|
.then(function (ret: RestoringInfo) {
|
|
const expectedValue = {
|
|
nodesMapping: [
|
|
{
|
|
newIp: 'CURRENT_NODE_DONT_CHANGE',
|
|
oldIp: '123.123.123.123',
|
|
privateKeyPath: '',
|
|
user: '',
|
|
},
|
|
],
|
|
oldNodesForReference: [
|
|
{
|
|
nodeData: {
|
|
nodeId: '123456789',
|
|
type: 'manager',
|
|
isLeader: true,
|
|
hostname: 'test',
|
|
architecture: 'x86_64',
|
|
operatingSystem: 'linux',
|
|
nanoCpu: 8000000000,
|
|
memoryBytes: 8241434624,
|
|
dockerEngineVersion: '18.09.2',
|
|
ip: '123.123.123.123',
|
|
state: 'ready',
|
|
status: 'active',
|
|
},
|
|
appsLockOnThisNode: ['pers1'],
|
|
},
|
|
],
|
|
}
|
|
expect(isDeepStrictEqual(ret, expectedValue)).toBe(true)
|
|
ret.nodesMapping[0].oldIp += ' '
|
|
expect(isDeepStrictEqual(ret, expectedValue)).toBe(false)
|
|
})
|
|
})
|
|
}
|