@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
334 lines (311 loc) • 11.2 kB
JavaScript
/**
* sync-in-server.js - Command-line interface for:
* - Starting the server (with optional daemon mode)
* - Stopping the server
* - Checking server status
* - Displaying the server version
* - Running database migrations
* - Updating the server version
* - Displaying help information
*/
const [nodeMajorVersion] = process.versions.node.split('.').map((num) => parseInt(num, 10))
const { spawn, spawnSync, exec } = require('child_process')
const { promisify } = require('util')
const path = require('path')
const fs = require('fs')
// Paths relative to this script
const PKILL_GRACEFUL_TIMEOUT = 10000 // 10s timeout
const ROOT_DIR = path.resolve(__dirname)
const CREATE_USER_SCRIPT = path.join(ROOT_DIR, 'server', 'infrastructure', 'database', 'scripts', 'create-user.js')
const SERVER_ENTRY = path.join(ROOT_DIR, 'server', 'main.js')
const ENV_DIST_FILE = path.join(ROOT_DIR, 'environment', 'environment.dist.yaml')
const ENV_DIST_MIN_FILE = path.join(ROOT_DIR, 'environment', 'environment.dist.min.yaml')
const USER_DIST_FILE = path.join(ROOT_DIR, '../../../environment.yaml')
const PID_FILE = path.join(ROOT_DIR, 'server.pid')
const LOG_FILE = path.join(ROOT_DIR, '../../../logs/server.log')
const DB_CONF_FILE = path.join(ROOT_DIR, 'server', 'infrastructure', 'database', 'configuration.js')
const CMD_DB_ARGS = ['drizzle-kit', 'migrate', `--config=${DB_CONF_FILE}`]
function printHelp() {
console.log(`
Usage: npx sync-in-server <command> [options]
Available commands:
init Copy initial configuration into the server directory
start [-d] Start the server; use -d for daemon (detached) mode
stop Stop the server
status Show server status (daemon mode only)
version Show installed sync-in-server version
migrate-db Run database migrations
create-user Create a user or administrator in the database
update Update the server version
help Show this help message
Examples:
npx sync-in-server init # copy default environment.yaml
npx sync-in-server start # attached mode
npx sync-in-server start -d # daemon mode
npx sync-in-server create-user # create default administrator account : sync-in/sync-in
npx sync-in-server create-user --role admin --login "userLogin" --password "userPassword"
npx sync-in-server version
npx sync-in-server help
`)
}
function init() {
if (!fs.existsSync(ENV_DIST_MIN_FILE)) {
console.error(`❌ Default configuration not found at: ${ENV_DIST_MIN_FILE}`)
process.exit(1)
}
if (fs.existsSync(USER_DIST_FILE)) {
console.error(`❌ Configuration file already exists: ${USER_DIST_FILE}`)
process.exit(1)
}
try {
fs.copyFileSync(ENV_DIST_MIN_FILE, USER_DIST_FILE)
console.log(`✅ Default configuration has been copied to: ${USER_DIST_FILE}`)
console.log('⚠️ Remember to set your secrets before running the server.')
console.log(`ℹ️ For the full list of options: \n see ${ENV_DIST_FILE} \n or visit https://sync-in.com/docs/setup-guide/server`)
console.log(`ℹ️ For environment variable setup instructions, visit https://sync-in.com/docs/setup-guide/docker#during-execution`)
} catch (e) {
console.error(`❌ Unable to copy initial configuration: ${e}`)
process.exit(1)
}
}
function startServer(detached = false) {
if (!fs.existsSync(SERVER_ENTRY)) {
console.error(`❌ Sync-in Server entry point not found: ${SERVER_ENTRY}`)
process.exit(1)
}
if (statusServer(true)) {
console.log('⚠️ Use \x1b[1mnpx sync-in-server stop\x1b[0m to stop it')
process.exit(0)
}
try {
if (detached) {
console.log('🚀 Starting Sync-in Server in daemon mode...')
const env = {
...{
SYNCIN_LOGGER_STDOUT: false,
SYNCIN_LOGGER_COLORIZE: false,
SYNCIN_LOGGER_FILEPATH: LOG_FILE
},
...process.env
}
const child = spawn('node', [SERVER_ENTRY], { detached: true, stdio: 'ignore', env: env })
child.unref()
fs.writeFileSync(PID_FILE, String(child.pid))
console.log(`✅ Sync-in Server started with PID ${child.pid}`)
console.log(`📝 Logging to file → ${env.SYNCIN_LOGGER_FILEPATH}`)
process.exit(0)
} else {
console.log('🚀 Starting Sync-in Server attached...')
const child = spawn('node', [SERVER_ENTRY], { stdio: 'inherit' })
// forward exit code
child.on('exit', (code) => process.exit(code))
}
} catch (e) {
console.error(`❌ Sync-in Server failed to start: ${e}`)
process.exit(1)
}
}
async function stopServer() {
if (!fs.existsSync(PID_FILE)) {
console.error('❌ PID file not found. Is the server running?')
process.exit(1)
}
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10)
if (isNaN(pid)) {
console.error(`❌ Invalid PID in file: ${PID_FILE}`)
process.exit(1)
}
try {
// Send SIGTERM
process.kill(pid, 'SIGTERM')
console.log(`🛑 Sent SIGTERM to process ${pid}, waiting for graceful shutdown...`)
// Wait for process to exit (poll every 100ms)
const success = await waitForProcessExit(pid)
if (!success) {
console.error(`⏱️ Process ${pid} did not exit after ${PKILL_GRACEFUL_TIMEOUT}ms`)
process.exit(1)
}
fs.unlinkSync(PID_FILE)
console.log(`✅ Sync-in Server process ${pid} stopped and PID file removed.`)
} catch (e) {
if (e.code === 'ESRCH') {
console.warn(`⚠️ Process ${pid} not found. Removing stale PID file.`)
fs.unlinkSync(PID_FILE)
return
}
console.error(`❌ Failed to stop process ${pid}: ${e}`)
process.exit(1)
}
}
function statusServer(returnStatus = false) {
if (!fs.existsSync(PID_FILE)) {
if (returnStatus) return false
console.log('ℹ️ Sync-in Server is not running (no PID file).')
process.exit(1)
}
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10)
if (isNaN(pid)) {
if (returnStatus) return false
console.error(`❌ Invalid PID in ${PID_FILE}`)
process.exit(1)
}
try {
// signal 0 does not kill the process; it only checks if it exists
process.kill(pid, 0)
console.log(`✅ Sync-in Server is running (PID ${pid}).`)
if (returnStatus) return true
process.exit(0)
} catch (err) {
if (returnStatus) return false
if (err.code === 'ESRCH') {
console.log(`⚠️ No process found at PID ${pid}. Cleaning up stale PID file.`)
fs.unlinkSync(PID_FILE)
process.exit(1)
} else if (err.code === 'EPERM') {
console.log(`✅ Sync-in Server is running, but no permission to signal PID ${pid}.`)
process.exit(0)
} else {
console.error(`❌ Error checking status of PID ${pid}:`, err.message)
process.exit(1)
}
}
}
function showVersion() {
const pkgJson = getPackageJson()
console.log(`🔖 Sync-in Server version: ${pkgJson.version}`)
}
function migrateDatabase() {
console.log('🗄️ Running database migrations...')
const result = spawnSync('npx', CMD_DB_ARGS, { stdio: 'inherit' })
if (result.status !== 0) {
console.error('❌ Database migrations failed. Please verify database connectivity and credentials.')
process.exit(result.status)
}
console.log('✅ Database migrations completed successfully.')
}
async function updateVersion() {
const currentPackage = getPackageJson()
let latestVersion
console.log('📡 Checking Sync-in Server version...')
try {
const execAsync = promisify(exec)
const { stdout, stderr } = await execAsync(`npm view ${currentPackage.name} version`)
// npm sometimes writes warnings to stderr
if (stderr) console.warn(`⚠️ Warnings: ${stderr}`)
latestVersion = stdout.trim()
if (currentPackage.version === latestVersion) {
console.log('✅ Sync-in Server is up to date.')
process.exit(0)
}
} catch (e) {
console.error(`❌ Unable to check version: ${e.stderr || e.message}`)
process.exit(1)
}
console.log(`🔄 Updating Sync-in Server ${currentPackage.version} to ${latestVersion} ...`)
const result = spawnSync('npm', ['update', currentPackage.name], { stdio: 'inherit' })
if (result.status !== 0) {
console.error('❌ Failed to update Sync-in Server.')
process.exit(result.status)
}
migrateDatabase()
console.log('✅ Sync-in Server updated.')
}
async function createUser(args) {
let userType = 'User'
if (args.length === 0 || (args.includes('--role') && args.includes('admin'))) {
userType = 'Administrator'
console.log(`👤 Creating ${args.length === 0 ? 'default' : ''} ${userType} in database...`)
} else {
console.log(`👤 Creating ${userType} in database...`)
}
const scriptArgs = [CREATE_USER_SCRIPT, ...args]
const result = spawnSync('node', scriptArgs, { stdio: 'inherit' })
if (result.status !== 0) {
console.error(`❌ ${userType} creation failed.`)
process.exit(result.status)
}
console.log(`✅ ${userType} created successfully.`)
}
function getPackageJson() {
try {
const pkgPath = path.join(ROOT_DIR, 'package.json')
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
} catch (e) {
console.error(`❌ Unable to parse ${path.join(ROOT_DIR, 'package.json')} : ${e}`)
process.exit(1)
}
}
function waitForProcessExit(pid) {
const interval = 500
let waited = 0
return new Promise((resolve) => {
const timer = setInterval(() => {
try {
process.kill(pid, 0) // does not kill, just checks existence
waited += interval
if (waited >= PKILL_GRACEFUL_TIMEOUT) {
clearInterval(timer)
resolve(false)
}
} catch (e) {
if (e.code === 'ESRCH') {
clearInterval(timer)
resolve(true) // process is gone
} else {
clearInterval(timer)
resolve(false) // some unexpected error
}
}
}, interval)
})
}
;(function main() {
// Ensure Nodejs.version
if (nodeMajorVersion < 22) {
console.error(`❌ Sync-in Server requires Node.js >= 22.x. Detected version: ${process.versions.node}`)
process.exit(1)
}
// Ensure CLI isn’t run on Windows
if (process.platform === 'win32') {
console.error('❌ Sync-in Server is not supported on Windows')
process.exit(1)
}
const args = process.argv.slice(2)
const command = args[0]
switch (command) {
case 'init':
init()
break
case 'start':
const detached = args.includes('-d') || args.includes('--daemon')
startServer(detached)
break
case 'stop':
stopServer().catch(console.error)
break
case 'status':
statusServer()
break
case 'version':
showVersion()
break
case 'migrate-db':
migrateDatabase()
break
case 'create-user':
createUser(args.slice(1)).catch(console.error)
break
case 'update':
updateVersion().catch(console.error)
break
case 'help':
case undefined:
printHelp()
break
default:
console.error(`Unknown command: ${command}`)
printHelp()
process.exit(1)
}
})()