node-fstab
Version:
fstab management with node.js
352 lines (321 loc) • 11.7 kB
JavaScript
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
};