Compare commits

..

2 Commits

Author SHA1 Message Date
3ca730ecee Add data directory management and enhance command line output
All checks were successful
farmcontrol/farmcontrol-server/pipeline/head This commit looks good
- Introduce functions to determine and create default data directories for both production and development environments in config.js.
- Update commandline.js to display the configured data directory and config path in the command line output.
- Modify filemanager.js to ensure the existence of a dedicated files directory within the data directory.
- Enhance localserver.js to include the data directory and config path in the server response, providing better context for users.
2026-07-27 18:15:29 +01:00
cb799ae8b7 Add command line handling for server status and authentication
- Introduce a new command line utility in commandline.js to manage server status checks and OTP authentication.
- Refactor index.js to utilize the new command line handling, improving clarity and separation of concerns.
- Enhance localserver.js to include connection status in the server response, providing more detailed information about the server state.
2026-07-27 18:09:50 +01:00
6 changed files with 201 additions and 97 deletions

1
.gitignore vendored
View File

@ -137,6 +137,7 @@ build
.nova
temp_files/*
data/*
dist/*

98
src/commandline.js Normal file
View File

@ -0,0 +1,98 @@
import {
authenticateWithOtp,
checkRunning,
isInfo,
otpCode,
printAuthCommand,
printerHostInfo,
startWaiting,
stopWaiting,
} from "./commandlineutils.js";
export async function handleCommandLine(cli, logger) {
const isCommandLines = otpCode != undefined || isInfo == true;
let runningServer;
try {
startWaiting("Checking if Farm Control Server is running...", logger);
runningServer = await checkRunning();
await stopWaiting();
if (runningServer == false && isCommandLines == true) {
cli.failure("Farm Control Server is not running.");
cli.blank();
return { handled: true, result: false };
}
} catch (err) {
await stopWaiting();
cli.failure("Failed to check if Farm Control Server is running:", err);
cli.blank();
return { handled: true, result: false };
}
if (isCommandLines == true) {
cli.success("Farm Control Server is running.");
}
if (isInfo) {
const buildNumber =
runningServer.buildNumber ?? runningServer.build ?? "n/a";
cli.blank();
cli.sectionHeader("FarmControl Server");
cli.label(
"Version:",
runningServer.version ? `v${runningServer.version}` : "n/a",
);
cli.label("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`);
cli.label("Config:", runningServer.configPath || "n/a");
cli.label("Data Dir:", runningServer.dataDir || "n/a");
cli.label("Connected:", cli.yesNo(runningServer.connected == true));
cli.label("Authenticated:", cli.yesNo(runningServer.authenticated == true));
cli.sectionFooter();
cli.blank();
if (
runningServer.authenticated == false &&
runningServer.connected == true
) {
cli.sectionHeader("Authenticate");
printAuthCommand(cli);
cli.sectionFooter();
} else if (
runningServer.connected == true &&
runningServer.authenticated == true
) {
cli.sectionHeader("Host");
printerHostInfo(runningServer.host, cli);
cli.sectionFooter();
}
cli.blank();
return { handled: true, result: runningServer };
}
if (otpCode != undefined) {
cli.blank();
startWaiting("Authenticating with OTP...", logger);
let authenticatedServer;
try {
authenticatedServer = await authenticateWithOtp(otpCode);
await stopWaiting();
} catch (err) {
await stopWaiting();
throw err;
}
if (authenticatedServer.valid == false) {
cli.failure("Failed to authenticate!");
cli.failure(authenticatedServer.error);
} else {
cli.success("Authenticated with OTP.");
cli.blank();
cli.sectionHeader("Host");
printerHostInfo(authenticatedServer.host, cli);
cli.sectionFooter();
cli.blank();
return { handled: true, result: authenticatedServer };
}
cli.blank();
return { handled: true, result: authenticatedServer };
}
return { handled: false, runningServer };
}

View File

@ -79,6 +79,57 @@ const CONFIG_PATH =
const logger = log4js.getLogger("Config");
logger.level = "info";
function getDefaultProductionDataDir() {
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
switch (process.platform) {
case "win32":
return path.join(
process.env.PROGRAMDATA || "C:\\ProgramData",
"farmcontrol-server",
"data",
);
case "darwin":
return path.join(
homeDir,
"Library",
"Application Support",
"farmcontrol-server",
"data",
);
case "linux":
default:
return "/var/lib/farmcontrol-server";
}
}
function getDefaultDevelopmentDataDir() {
if (isPackaged) {
return path.join(path.dirname(getDevelopmentConfigPath()), "data");
}
return path.resolve(__dirname, "../data");
}
function resolveDataDir(envConfig) {
let dataDir = envConfig.dataDir;
const wasMissing = !dataDir;
if (!dataDir) {
dataDir =
NODE_ENV === "production"
? getDefaultProductionDataDir()
: getDefaultDevelopmentDataDir();
}
dataDir = path.resolve(dataDir);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
logger.info(`Created data directory at ${dataDir}`);
}
return { dataDir, shouldPersist: wasMissing && NODE_ENV === "production" };
}
const DEFAULT_CONFIG = {
development: {
logLevel: "debug",
@ -127,6 +178,14 @@ export function loadConfig() {
);
}
const { dataDir, shouldPersist } = resolveDataDir(config[NODE_ENV]);
config[NODE_ENV] = { ...config[NODE_ENV], dataDir };
if (shouldPersist) {
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
logger.info(`Set data directory to ${dataDir} in config`);
}
return config[NODE_ENV];
} catch (err) {
logger.error("Error loading config:", err);
@ -167,3 +226,25 @@ export async function saveConfig(newConfig) {
export function getEnvironment() {
return NODE_ENV;
}
// Get the path to the active config.json file
export function getConfigPath() {
return CONFIG_PATH;
}
// Get the configured data directory, optionally with subpaths
export function getDataDir(...subpaths) {
const config = loadConfig();
return subpaths.length > 0
? path.join(config.dataDir, ...subpaths)
: config.dataDir;
}
// Ensure a subdirectory exists within the data directory
export function ensureDataDir(...subpaths) {
const dir = getDataDir(...subpaths);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return dir;
}

View File

@ -1,11 +1,10 @@
// filemanager.js - Manages file downloads and caching
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import axios from "axios";
import log4js from "log4js";
import _ from "lodash";
import { loadConfig } from "../config.js";
import { loadConfig, ensureDataDir } from "../config.js";
import { sendIPC } from "../electron/ipc.js";
const config = loadConfig();
@ -13,31 +12,25 @@ const config = loadConfig();
const logger = log4js.getLogger("File Manager");
logger.level = config.logLevel;
// Configure paths relative to this file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const TEMP_FILES_DIR = path.resolve(__dirname, "../../temp_files");
const FILES_DIR = ensureDataDir("files");
export class FileManager {
constructor(socketClient) {
this.socketClient = socketClient;
this.files = [];
this.downloadingFiles = new Map(); // Track ongoing downloads by fileId
this.ensureTempFilesDirectory();
this.ensureFilesDirectory();
this.progressCallbacks = new Map();
}
/**
* Ensure the temp_files directory exists
* Ensure the files directory exists within the data directory
*/
ensureTempFilesDirectory() {
ensureFilesDirectory() {
try {
if (!fs.existsSync(TEMP_FILES_DIR)) {
fs.mkdirSync(TEMP_FILES_DIR, { recursive: true });
logger.info(`Created temp_files directory at ${TEMP_FILES_DIR}`);
}
ensureDataDir("files");
} catch (error) {
logger.error(`Failed to create temp_files directory: ${error.message}`);
logger.error(`Failed to create files directory: ${error.message}`);
}
}
@ -70,14 +63,14 @@ export class FileManager {
}
/**
* Get the file path in temp_files directory
* Get the file path in the files directory
*/
getFilePath(fileId) {
return path.join(TEMP_FILES_DIR, fileId);
return path.join(FILES_DIR, fileId);
}
/**
* Check if file exists in temp_files directory
* Check if file exists in the files directory
*/
fileExistsInCache(fileId) {
const filePath = this.getFilePath(fileId);
@ -201,7 +194,7 @@ export class FileManager {
/**
* Get file by ID
* Checks temp_files directory first, then downloads from API if not found
* Checks the files directory first, then downloads from API if not found
* If a download is already in progress for the same fileId, waits for that download
*/
async getFile(fileId, onProgress) {

View File

@ -7,17 +7,12 @@ import { setupIPC } from "./electron/ipc.js";
import { LocalServer } from "./localserver/localserver.js";
import { SocketClient } from "./socket/socketclient.js";
import {
authenticateWithOtp,
checkRunning,
createCliLogger,
isHeadless,
isInfo,
otpCode,
printAuthCommand,
printerHostInfo,
startWaiting,
stopWaiting,
} from "./commandlineutils.js";
import { handleCommandLine } from "./commandline.js";
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
@ -33,80 +28,13 @@ const cli = createCliLogger(logger);
export async function init(options = {}) {
const headless = options.headless ?? isHeadless;
cli.blank();
startWaiting("Checking if Farm Control Server is running...", logger);
const isCommandLines = otpCode != undefined || isInfo == true;
let runningServer;
try {
runningServer = await checkRunning();
await stopWaiting();
if (runningServer == false && isCommandLines == true) {
cli.failure("Farm Control Server is not running.");
cli.blank();
return false;
}
} catch (err) {
await stopWaiting();
cli.failure("Failed to check if Farm Control Server is running:", err);
cli.blank();
return false;
const commandLineResult = await handleCommandLine(cli, logger);
if (commandLineResult.handled) {
return commandLineResult.result;
}
if (isCommandLines == true) {
cli.success("Farm Control Server is running.");
}
if (isInfo) {
const buildNumber =
runningServer.buildNumber ?? runningServer.build ?? "n/a";
cli.blank();
cli.sectionHeader("FarmControl Server");
cli.label(
"Version:",
runningServer.version ? `v${runningServer.version}` : "n/a",
);
cli.label("Build:", buildNumber === "dev" ? "dev" : `b${buildNumber}`);
cli.label("Authenticated:", cli.yesNo(runningServer.authenticated == true));
cli.sectionFooter();
cli.blank();
if (runningServer.authenticated == false) {
cli.sectionHeader("Authenticate");
printAuthCommand(cli);
cli.sectionFooter();
} else {
cli.sectionHeader("Host");
printerHostInfo(runningServer.host, cli);
cli.sectionFooter();
}
cli.blank();
return runningServer;
}
if (otpCode != undefined) {
cli.blank();
startWaiting("Authenticating with OTP...", logger);
let authenticatedServer;
try {
authenticatedServer = await authenticateWithOtp(otpCode);
await stopWaiting();
} catch (err) {
await stopWaiting();
throw err;
}
if (authenticatedServer.valid == false) {
cli.failure("Failed to authenticate!");
cli.failure(authenticatedServer.error);
} else {
cli.success("Authenticated with OTP.");
cli.blank();
cli.sectionHeader("Host");
printerHostInfo(authenticatedServer.host, cli);
cli.sectionFooter();
cli.blank();
return authenticatedServer;
}
cli.blank();
return authenticatedServer;
}
const runningServer = commandLineResult.runningServer;
if (runningServer != false) {
cli.warn("✘ Farm Control Server is already running.");

View File

@ -1,7 +1,7 @@
import express from "express";
import log4js from "log4js";
import { notPrompting } from "../utils.js";
import { loadConfig } from "../config.js";
import { getConfigPath, loadConfig } from "../config.js";
import { getServerVersionInfo } from "../serverVersion.js";
import { startWaiting, stopWaiting } from "../spinner.js";
@ -27,7 +27,10 @@ export class LocalServer {
res.json({
running: true,
authenticated: this.socketClient.authenticated,
connected: this.socketClient.connected,
host: this.socketClient.host,
configPath: getConfigPath(),
dataDir: config.dataDir,
...getServerVersionInfo(),
});
});