UNPKG

node-fstab

Version:

fstab management with node.js

352 lines (321 loc) 11.7 kB
const fs = require('fs').promises; const { createReadStream, createWriteStream } = require('fs'); const { exec } = require('child_process'); // Helper function to run a shell command with exec wrapped in a Promise const runCommand = (command) => { return new Promise((resolve, reject) => { exec(command, (error, stdout, stderr) => { if (error) { reject({ error, stderr }); } else { resolve({ stdout, stderr }); } }); }); }; // Create the mount point if it doesn't exist const createMountPoint = async (path) => { let response = { ok: false }; try { await fs.mkdir(path, { recursive: true }); response.ok = true; } catch (error) { if (error.code !== 'EEXIST') { response.error = `Failed to create mount point: ${error.message}`; } else { response.ok = true; } } return response; }; // Mount the NFS share using a system command with customizable options const mount = async (server, remotePath, localPath, type = 'cifs', options = 'username=username,password=password') => { let response = { ok: false }; const mountCommand = `sudo mount -t ${type} ${server}:${remotePath} ${localPath} ${options}`.trim(); try { const { stderr } = await runCommand(mountCommand); if (stderr) { response.error = `Error mounting: ${stderr}`; } else { response.ok = true; } } catch (error) { response.error = `Failed to mount : ${error.message}`; } return response; }; // Execute `mount -a` to remount all filesystems from /etc/fstab const remountAll = async () => { let response = { ok: false }; try { const { stdout, stderr } = await runCommand('mount -a'); if (stderr) { response.error = `Error remounting all filesystems: ${stderr}`; } else { response.ok = true; response.message = stdout.trim() || "Successfully remounted all filesystems from /etc/fstab"; } } catch (error) { response.error = `Failed to remount all filesystems: ${error.message}`; } return response; }; const remount = async (mountPath) => { let response = { ok: false }; try { const { stdout, stderr } = await runCommand(`mount ${mountPath}`); const { stdout: daemonStdout, stderr: daemonStderr } = await runCommand(`systemctl daemon-reload`); if (stderr) { response.error = `Error remounting ${mountPath}: ${stderr}`; } else { response.ok = true; response.message = `Successfully remounted ${mountPath}`; } } catch (failedResponse) { // console.error(error) response.error = `Failed to remount ${mountPath}: ${failedResponse.error}`; } return response; }; const unmount = async (localPath) => { let response = { ok: false }; try { const { stdout, stderr } = await runCommand(`sudo umount ${localPath}`); if (stderr) { response.error = `Error unmounting ${localPath}: ${stderr}`; } else { response.ok = true; response.message = `Successfully unmounted ${localPath}`; } } catch (error) { response.error = `Failed to unmount ${localPath}: ${error.message}`; } return response; }; // Check and update /etc/fstab with the NFS entry with customizable options const update = async (sourceTarget, localPath, mountType = 'cifs', fstabOptions = 'username=username,password=password,rw,iocharset=utf8,file_mode=0777,dir_mode=0777 0 0') => { let response = { ok: false }; const fstabEntry = `${sourceTarget} ${localPath} ${mountType} ${fstabOptions}\n`; try { // Read the current /etc/fstab file const data = await fs.readFile('/etc/fstab', 'utf8'); // Remove any existing entries with the same localPath const updatedData = data .split('\n') .filter(line => !line.includes(` ${localPath} `)) .join('\n'); // Write the modified data back to /etc/fstab without duplicates await fs.writeFile('/etc/fstab', updatedData, 'utf8'); // Append the new fstab entry await fs.appendFile('/etc/fstab', fstabEntry); response.ok = true; response.message = `Successfully updated /etc/fstab with entry for ${localPath}`; } catch (error) { response.error = `Error updating /etc/fstab: ${error.message}`; } return response; }; // Remove an entry from /etc/fstab by mount point const remove = async (localPath) => { let response = { ok: false }; try { const data = await fs.readFile('/etc/fstab', 'utf8'); const updatedData = data .split('\n') .filter(line => !line.includes(` ${localPath} `)) .join('\n'); if (updatedData !== data) { await fs.writeFile('/etc/fstab', updatedData, 'utf8'); response.ok = true; } else { response.error = `No entry found for ${localPath} in /etc/fstab.`; } } catch (error) { response.error = `Error removing entry from /etc/fstab: ${error.message}`; } return response; }; // fstab mounts const list = async () => { let response = { ok: false, mounts: [] }; try { const data = await fs.readFile('/etc/fstab', 'utf8'); response.ok = true; response.mounts = data .split('\n') .filter(line => line && !line.startsWith('#')) // Ignore empty lines and comments .map(line => { const [device, mountPoint, type, options, dump, pass] = line.split(/\s+/); return { device, // The device or UUID mountPoint, // The mount point on the local machine type, // Filesystem type options: `${options} ${dump} ${pass}`, // Mount options as an array }; }) .filter(item => !!item.mountPoint && !item.device.includes('/dev/disk') && !item.device.includes('/swap') ); } catch (error) { response.error = `Failed to read /etc/fstab: ${error.message}`; } return response; }; const listWithSizes = async () => { let response = { ok: false, mounts: [] }; try { const data = await fs.readFile('/etc/fstab', 'utf8'); response.ok = true; // Parse fstab entries const fstabEntries = data .split('\n') .filter(line => line && !line.startsWith('#')) // Ignore empty lines and comments .map(line => { const [device, mountPoint, type, options, dump, pass] = line.split(/\s+/); return { device, // The device or UUID mountPoint, // The mount point on the local machine type, // Filesystem type options: `${options} ${dump} ${pass}`, // Mount options as an array }; }) .filter(item => !!item.mountPoint && !item.device.includes('/dev/disk') && !item.device.includes('/swap') ); // Get disk usage information for each mount point const mountsWithCapacity = await Promise.all( fstabEntries.map(async (mount) => { try { // Use df command to get disk usage information const { stdout } = await runCommand(`df -k ${mount.mountPoint}`); const lines = stdout.trim().split('\n'); // The last line contains the information for our mount point if (lines.length > 1) { const dfData = lines[lines.length - 1].split(/\s+/); if (dfData.length >= 6) { const total = parseInt(dfData[1]) * 1024; // Convert from 1K blocks to bytes const used = parseInt(dfData[2]) * 1024; // Convert from 1K blocks to bytes const available = parseInt(dfData[3]) * 1024; // Convert from 1K blocks to bytes const capacityPercent = dfData[4]; return { ...mount, capacity: { total, used, available, percent: capacityPercent, humanReadable: { total: formatBytes(total), used: formatBytes(used), available: formatBytes(available) } } }; } } } catch (error) { // If df command fails (e.g., mount point doesn't exist), return mount without capacity console.warn(`Could not get capacity for ${mount.mountPoint}: ${error.message}`); } // Return mount without capacity if df command failed return { ...mount, capacity: null }; }) ); response.mounts = mountsWithCapacity; } catch (error) { response.error = `Failed to read /etc/fstab: ${error.message}`; } return response; }; // Helper function to format bytes to human readable format 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', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } // Get disk usage information using `df -h` as an array of objects const getDiskUsage = async () => { let response = { ok: false, disks: [] }; try { const { stdout, stderr } = await runCommand('df -h'); if (stderr) { response.error = `Error retrieving disk usage: ${stderr}`; } else { const lines = stdout.trim().split('\n'); const headers = lines[0].toLowerCase().split(/\s+/); response.ok = true; response.disks = lines.slice(1).map(line => { const values = line.split(/\s+/); return headers.reduce((obj, header, index) => { obj[header] = values[index]; return obj; }, {}); }); } } catch (error) { response.error = `Failed to retrieve disk usage: ${error.message}`; } return response; }; // Check if a specific local path exists in the disk usage list const checkDiskPathExists = async (localPath) => { let response = { ok: false, exists: false }; try { const diskUsage = await getDiskUsage(); if (!diskUsage.ok) { response.error = `Error retrieving disk usage: ${diskUsage.error}`; } else { response.exists = diskUsage.disks.some(disk => disk.mounted === localPath); response.ok = true; response.message = response.exists ? `Path ${localPath} exists` : `Path ${localPath} does not exist`; } } catch (error) { response.error = `Failed to check if path exists: ${error.message}`; } return response; }; const writeReadStream = async (readerStream, outputFile) => { return new Promise((resolve, reject) => { const response = { ok: false } const writerStream = createWriteStream(outputFile); readerStream.on("error", (error) => { response.err = error.toString(); resolve(response) }); writerStream.on("error", (error) => { response.err = error.toString(); resolve(response) }); writerStream.on("finish", () => { response.ok = true; resolve(response) }); readerStream.pipe(writerStream); }); }; // Export functions module.exports = { createMountPoint, mount, update, remove, list, remountAll, remount, unmount, getDiskUsage, listWithSizes, writeReadStream, checkDiskPathExists, execAsync: runCommand };