hana-cli
Version:
HANA Developer Command Line Interface
296 lines (266 loc) • 9.5 kB
JavaScript
// @ts-check
import * as baseLite from '../utils/base-lite.js'
import * as fs from 'fs'
import * as path from 'path'
import { homedir } from 'os'
import { buildDocEpilogue } from '../utils/doc-linker.js'
export const command = 'backupList [directory]'
export const aliases = ['blist', 'listBackups', 'backups']
export const describe = baseLite.bundle.getText("backupList")
export const builder = (yargs) => yargs.options(baseLite.getBuilder({
directory: {
alias: ['dir'],
type: 'string',
desc: baseLite.bundle.getText("backupListDirectory")
},
backupType: {
alias: ['type'],
choices: ["table", "schema", "database", "all"],
default: "all",
type: 'string',
desc: baseLite.bundle.getText("backupType")
},
sortBy: {
alias: ['sort'],
choices: ["name", "date", "size", "type"],
default: "date",
type: 'string',
desc: baseLite.bundle.getText("backupListSortBy")
},
order: {
alias: ['o'],
choices: ["asc", "desc"],
default: "desc",
type: 'string',
desc: baseLite.bundle.getText("backupListOrder")
},
limit: {
alias: ['l'],
type: 'number',
default: 50,
desc: baseLite.bundle.getText("limit")
},
showDetails: {
alias: ['details'],
type: 'boolean',
default: false,
desc: baseLite.bundle.getText("backupListShowDetails")
}
})).wrap(160).example('hana-cli backupList --backupPath /backups', baseLite.bundle.getText("backupListExample")).wrap(160).epilog(buildDocEpilogue('backupList', 'backup-recovery', ['backup', 'backupStatus', 'restore']))
export let inputPrompts = {
directory: {
description: baseLite.bundle.getText("backupListDirectory"),
type: 'string',
required: false
},
backupType: {
description: baseLite.bundle.getText("backupType"),
type: 'string',
required: false
},
sortBy: {
description: baseLite.bundle.getText("backupListSortBy"),
type: 'string',
required: false
},
order: {
description: baseLite.bundle.getText("backupListOrder"),
type: 'string',
required: false
},
limit: {
description: baseLite.bundle.getText("limit"),
type: 'number',
required: false
},
showDetails: {
description: baseLite.bundle.getText("backupListShowDetails"),
type: 'boolean',
required: false
}
}
/**
* Command handler function
* @param {object} argv - Command line arguments from yargs
* @returns {Promise<void>}
*/
export async function handler(argv) {
const base = await import('../utils/base.js')
base.promptHandler(argv, listBackups, inputPrompts)
}
/**
* List available backups
* @param {object} prompts - Input prompts with list configuration
* @returns {Promise<Array>} - Array of backup information
*/
export async function listBackups(prompts) {
const base = await import('../utils/base.js')
try {
base.debug('listBackups')
// Determine backup directory
const defaultBackupDir = path.join(homedir(), '.hana-cli', 'backups')
const backupDir = prompts.directory || defaultBackupDir
// Check if directory exists
if (!fs.existsSync(backupDir)) {
console.log(base.bundle.getText("backupListNoDirectory", [backupDir]))
return []
}
console.log(base.bundle.getText("backupListScanning", [backupDir]))
// Scan for backup files
const files = fs.readdirSync(backupDir)
const backupFiles = files.filter(file =>
file.endsWith('.backup') ||
file.endsWith('.backup.meta.json') ||
file.endsWith('.manifest.json')
)
// Extract unique backup names
const backupNames = new Set()
backupFiles.forEach(file => {
const baseName = file
.replace('.meta.json', '')
.replace('.manifest.json', '')
.replace('.backup', '')
backupNames.add(baseName)
})
// Collect backup information
const backups = []
for (const backupName of backupNames) {
const backupPath = path.join(backupDir, `${backupName}.backup`)
const metadataPath = `${backupPath}.meta.json`
const manifestPath = `${backupPath}.manifest.json`
let backupInfo = {
name: backupName,
path: backupPath,
exists: fs.existsSync(backupPath)
}
// Load metadata if available
if (fs.existsSync(metadataPath)) {
try {
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'))
backupInfo = { ...backupInfo, ...metadata }
} catch (error) {
backupInfo.metadataError = error.message
}
}
// Check for manifest (schema or database backup)
if (fs.existsSync(manifestPath)) {
backupInfo.hasManifest = true
try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
if (manifest.tables) {
backupInfo.tableCount = manifest.tables.length
}
if (manifest.schemas) {
backupInfo.schemaCount = manifest.schemas.length
}
} catch (error) {
backupInfo.manifestError = error.message
}
}
// Get file size
if (backupInfo.exists) {
const stats = fs.statSync(backupPath)
backupInfo.size = stats.size
backupInfo.sizeFormatted = formatBytes(stats.size)
backupInfo.created = stats.birthtime
backupInfo.modified = stats.mtime
}
// Check for CSV files
const csvPath = backupPath.replace('.backup', '.csv')
const csvGzPath = `${csvPath}.gz`
if (fs.existsSync(csvGzPath)) {
backupInfo.dataFile = csvGzPath
backupInfo.dataSize = fs.statSync(csvGzPath).size
backupInfo.dataSizeFormatted = formatBytes(backupInfo.dataSize)
} else if (fs.existsSync(csvPath)) {
backupInfo.dataFile = csvPath
backupInfo.dataSize = fs.statSync(csvPath).size
backupInfo.dataSizeFormatted = formatBytes(backupInfo.dataSize)
}
backups.push(backupInfo)
}
// Filter by type if specified
let filteredBackups = backups
if (prompts.backupType && prompts.backupType !== 'all') {
filteredBackups = backups.filter(b => b.type === prompts.backupType)
}
// Sort backups
filteredBackups.sort((a, b) => {
let comparison = 0
switch (prompts.sortBy) {
case 'name':
comparison = a.name.localeCompare(b.name)
break
case 'date':
comparison = new Date(a.timestamp || a.created || 0).getTime() - new Date(b.timestamp || b.created || 0).getTime()
break
case 'size':
comparison = (a.size || 0) - (b.size || 0)
break
case 'type':
comparison = (a.type || '').localeCompare(b.type || '')
break
}
return prompts.order === 'asc' ? comparison : -comparison
})
// Limit results
const limitedBackups = filteredBackups.slice(0, prompts.limit)
// Display results
console.log(`\n${base.bundle.getText("backupListFound", [limitedBackups.length, backups.length])}`)
if (prompts.showDetails) {
// Show detailed view
limitedBackups.forEach(backup => {
console.log('\n' + '='.repeat(80))
console.log(`${base.bundle.getText("backupName")}: ${backup.name}`)
console.log(`${base.bundle.getText("backupType")}: ${backup.type || 'Unknown'}`)
console.log(`${base.bundle.getText("backupTarget")}: ${backup.target || 'N/A'}`)
console.log(`${base.bundle.getText("backupSchema")}: ${backup.schema || 'N/A'}`)
console.log(`${base.bundle.getText("backupTimestamp")}: ${backup.timestamp || backup.created || 'Unknown'}`)
console.log(`${base.bundle.getText("backupStatus")}: ${backup.status || 'Unknown'}`)
console.log(`${base.bundle.getText("backupSize")}: ${backup.sizeFormatted || 'N/A'}`)
if (backup.dataSizeFormatted) {
console.log(`${base.bundle.getText("backupDataSize")}: ${backup.dataSizeFormatted}`)
}
if (backup.tableCount) {
console.log(`${base.bundle.getText("backupTableCount")}: ${backup.tableCount}`)
}
if (backup.schemaCount) {
console.log(`${base.bundle.getText("backupSchemaCount")}: ${backup.schemaCount}`)
}
console.log(`${base.bundle.getText("backupPath")}: ${backup.path}`)
})
console.log('\n' + '='.repeat(80))
} else {
// Show summary table
const summaryBackups = limitedBackups.map(b => ({
NAME: b.name,
TYPE: b.type || 'Unknown',
TARGET: b.target || 'N/A',
TIMESTAMP: b.timestamp ? new Date(b.timestamp).toLocaleString() : 'Unknown',
STATUS: b.status || 'Unknown',
SIZE: b.sizeFormatted || 'N/A',
TABLES: b.tableCount || '-',
SCHEMAS: b.schemaCount || '-'
}))
base.outputTableFancy(summaryBackups)
}
return limitedBackups
} catch (error) {
console.error(base.bundle.getText("backupListFailed", [error.message]))
throw error
}
}
/**
* Format bytes to human-readable string
* @param {number} bytes - Number of bytes
* @param {number} decimals - Number of decimal places
* @returns {string} - Formatted string
*/
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}