import debounce from 'just-debounce-it'; import { watch } from 'node:fs'; import { stat, readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import readline from 'node:readline/promises'; import { parse } from 'ini'; import MDBReader from 'mdb-reader'; import { diff } from 'just-diff'; import { version } from './package.json'; console.log(`FDLogUp Client v${version}`); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function promptToClose(message: string) { await rl.question(`== ${message}\n== Press enter to quit.`); process.exit(0); } // Check for config.ini file const configPath = join(process.cwd(), 'config.ini'); try { const configStat = await stat(configPath); if (configStat.isDirectory()) await promptToClose('The config file must be a file.'); } catch (e) { await writeFile(configPath, [ '; The database file to upload, defaults to C:\\Users\\\\Documents\\Affirmatech\\N3FJP Software\\ARRL-Field-Day\\LogData.mdb', `; database_file = "${process.env.USERNAME ? `C:\\Users\\${process.env.USERNAME}\\Documents\\Affirmatech\\N3FJP Software\\ARRL-Field-Day\\LogData.mdb` : ''}"`, '', '; The endpoint to upload to', 'log_endpoint = "https://example.local/up"' ].join('\n')) await promptToClose('Failed to find config file. A new one was created, so put in your variables in there.'); } let databaseFile: string; let logEndpoint: string; try { const config = parse(await readFile(configPath, { encoding: 'utf-8' })); databaseFile = config.database_file; logEndpoint = config.log_endpoint; if (!logEndpoint) await promptToClose('A log endpoint is required in the config file.'); } catch (e) { databaseFile = ''; await promptToClose('Failed to parse config file. You can remove the file and re-run this program if more problems occur.'); } const dbPath = databaseFile ?? process.env.USERNAME ? join('C:/Users', process.env.USERNAME!, 'Documents/Affirmatech/N3FJP Software/ARRL-Field-Day/LogData.mdb') : ''; // Check for log file try { if (!dbPath) await promptToClose('A log path is needed to use this program.'); const fileStat = await stat(dbPath); if (fileStat.isDirectory()) await promptToClose('The log path must point to a file.'); console.log(`\n== Following changes for the file "${dbPath}"\n== Ctrl-C to exit.\n`); } catch (e) { await promptToClose(`Failed to find log file at "${dbPath}"`); } // Upload logic let lastData: Record = {}; async function _upload() { try { console.log('-- Reading log...'); const reader = new MDBReader(await readFile(dbPath)); if (!reader.getTableNames().includes('tblContacts')) return console.log('!! No "tblContacts" table present.'); const table = reader.getTable('tblContacts'); const tableData = table.getData>(); const data = tableData.reduce((p, r) => ({ ...p, [r.fldPrimaryKey]: r }), {} as Record); const differences = diff(lastData, data); const changedIds = [...new Set(differences.filter((r) => r.op !== 'remove').map((r) => r.path[0]))]; const changedRows = changedIds.map((id) => data[id]); if (changedIds.length === 0) return void console.log('-- No changes found.'); try { console.log(`-> Uploading ${changedIds.length.toLocaleString()} row(s)...`); const response = await fetch(logEndpoint, { method: 'POST', body: JSON.stringify(changedRows), headers: { 'Content-Type': 'application/json' } }); console.log(`<- Recieved ${response.status} (${response.statusText})`); if (response.status === 200) lastData = data; } catch (e) { console.log('-! Failed to send log'); console.error(e); } } catch (e) { console.log('-! Failed to upload'); console.error(e); } } const upload = debounce(_upload, 250); const watcher = watch(dbPath, (event, filename) => { if (event === 'rename') return void console.log(`== ${filename} was renamed, something might break`); upload(); }); process.on("SIGINT", () => { // close watcher when Ctrl-C is pressed console.log('\n== Closing watcher...'); watcher.close(); process.exit(0); }); rl.close(); // "Upload first" option if (process.argv[2] === '--upload') _upload();