workwatch
Version:
A Linux terminal program for honest worktime tracking and billing.
171 lines (160 loc) • 6.05 kB
JavaScript
/**
* This file is part of the WorkWatch, a Linux terminal program for honest
* worktime tracking and billing.
*
* Copyright (C) 2020-2025 by Artur Rutkowski
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* WorkWatch is beeing developped and maintained by Artur (locust) Rutkwoski
* <locust@mailbox.org>
*/
/**
* This is a module responsible for reading and setting configuration.
* It uses a config file in JSON format, environment variables and predefined
* values. It is also used to set configuration passed by the CLI arguments.
*/
const fs = require("node:fs");
const process = require("node:process");
const utils = require("./utils.js");
// Holds a current configuration.
const configuration = {
dataDir: (
process.env.WORKWATCH_DATA_DIR
)? process.env.WORKWATCH_DATA_DIR : `${process.env.HOME}/.workwatch`,
measurementFilename: (
process.env.WORKWATCH_MEASUREMENT_FILENAME
)? process.env.WORKWATCH_MEASUREMENT_FILENAME : "measurement",
logFilename: (
process.env.WORKWATCH_LOG_FILENAME
)? process.env.WORKWATCH_LOG_FILENAME : "log"
};
// Predefined possible config file locations.
const configFileLocations = [
process.env.WORKWATCH_CONFIG_FILE,
`${process.env.HOME}/.config/workwatch/config.json`,
`${configuration.dataDir}/config.json`
];
// Configuration management object.
function Config() {
// Getters to obtain the current config data from earlier defined configuration object.
// Getters also verify their data but data directory is verified later in verify method.
Object.defineProperty(this, "dataDir", {
get: function () {
return configuration.dataDir;
},
enumerable: true,
configurable: false
});
Object.defineProperty(this, "measurementFilename", {
get: function () {
if (utils.verifyFilenameValue(configuration.measurementFilename)) {
return configuration.measurementFilename;
} else {
const error = new Error("The measurement filename is wrong.");
error.code = "ERR_CONFIG_MEASUREMENT_FILENAME_INCORRECT";
throw error;
}
},
enumerable: true,
configurable: false
});
Object.defineProperty(this, "logFilename", {
get: function () {
if (utils.verifyFilenameValue(configuration.logFilename)) {
return configuration.logFilename;
} else {
const error = new Error("The log filename is wrong.");
error.code = "ERR_CONFIG_LOG_FILENAME_INCORRECT";
throw error;
}
},
enumerable: true,
configurable: false
});
// Verifies the config data by checking data directory existence and
// by invoking getters to run their verification code.
Object.defineProperty(this, "verify", {
value: () => {
return new Promise((resolve, reject) => {
fs.realpath(configuration.dataDir, "utf8", (pathError, resolvedPath) => {
if (pathError && pathError.code !== "ENOENT") reject(pathError);
if (resolvedPath === undefined) {
const error = new Error(`The data directory doesn't exist at ${configuration.dataDir}.`);
error.code = "ERR_CONFIG_DATA_DIR_NOT_EXISTS";
reject(error);
}
let {measurementFilename, logFilename} = this;
resolve(true);
});
});
},
writeable: false,
enumerable: false,
configurable: false
});
// Sets configuration with provided object.
Object.defineProperty(this, "set", {
value: ({dataDir, measurementFilename, logFilename}) => {
return new Promise((resolve, reject) => {
const parametersToSet = {dataDir, measurementFilename, logFilename};
Object.keys(parametersToSet).forEach(key => {
if (parametersToSet[key] !== configuration[key]) {
configuration[key] = parametersToSet[key];
}
});
this.verify()
.then(verifySuccess => resolve(true))
.catch(newSettingsVerifyError => reject(newSettingsVerifyError));
});
},
writeable: false,
enumerable: false,
configurable: false
});
Object.preventExtensions(this);
}
// Load a config file from predefined or given location.
Config.load = function (path = null) {
return new Promise((resolve, reject) => {
let locations = (path)? [path] : [...configFileLocations].filter(
item => item !== undefined && item !== null
);
locations.forEach(location => {
fs.realpath(location, "utf8", (error, resolvedPath) => {
if (error && error.code !== "ENOENT") reject(error);
if (
resolvedPath === undefined
&& locations.indexOf(location) === locations.length - 1
) {
const loadError = new Error(
(locations.length === 1)? `There is no config file at the given path: ${location}` : "No config file found."
);
loadError.code = (locations.length === 1)? "ERR_NO_CONFIG_FILE" : "ERR_NO_CONFIG";
reject(loadError);
} else if (resolvedPath !== undefined) {
fs.readFile(resolvedPath, "utf8", (err, configData) => {
if (err) reject(err);
const configJSON = JSON.parse(configData);
const config = new Config();
config.set(configJSON)
.then(successfullyConfigured => resolve(config))
.catch(configurationError => reject(configurationError));
});
}
});
});
});
};
module.exports = {Config};