210 lines
5.9 KiB
Bash
210 lines
5.9 KiB
Bash
#!/usr/bin/env bash
|
|
set -e
|
|
|
|
# Rocketry Service Installer
|
|
# Installs the React SPA as a systemd service on the system
|
|
|
|
# Preflight checks
|
|
command -v node >/dev/null || { echo "Error: node not found"; exit 1; }
|
|
command -v npm >/dev/null || { echo "Error: npm not found"; exit 1; }
|
|
command -v sudo >/dev/null || { echo "Error: sudo not found"; exit 1; }
|
|
|
|
# Detect node path and current user
|
|
NODE_PATH=$(which node)
|
|
CURRENT_USER=$(whoami)
|
|
|
|
echo "Rocketry Service Installer"
|
|
echo "=========================="
|
|
echo ""
|
|
echo "Node path: $NODE_PATH"
|
|
echo "Service user: $CURRENT_USER"
|
|
echo ""
|
|
|
|
# Port prompt
|
|
read -p "Port to run on [default: 8080]: " PORT
|
|
PORT=${PORT:-8080}
|
|
|
|
# Validate port
|
|
if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
|
|
echo "Error: Invalid port number. Must be between 1 and 65535."
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo "Building application..."
|
|
npm run build
|
|
|
|
# Install directory
|
|
INSTALL_DIR="/opt/rocketry"
|
|
echo "Installing to $INSTALL_DIR..."
|
|
sudo mkdir -p "$INSTALL_DIR"
|
|
sudo cp -r dist "$INSTALL_DIR/"
|
|
|
|
# Write server.js to /opt/rocketry/server.js
|
|
echo "Writing server.js..."
|
|
sudo tee "$INSTALL_DIR/server.js" > /dev/null <<'SERVEREOF'
|
|
#!/usr/bin/env node
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const PORT = process.env.PORT || 8080;
|
|
const DIST_DIR = path.join(__dirname, 'dist');
|
|
|
|
const MIME_TYPES = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.js': 'text/javascript; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.svg': 'image/svg+xml',
|
|
'.png': 'image/png',
|
|
'.ico': 'image/x-icon',
|
|
'.woff2': 'font/woff2',
|
|
'.json': 'application/json',
|
|
};
|
|
|
|
const server = http.createServer((req, res) => {
|
|
// Normalize URL
|
|
let urlPath = decodeURIComponent(req.url);
|
|
if (urlPath === '/') {
|
|
urlPath = '/index.html';
|
|
}
|
|
|
|
// Resolve file path
|
|
const filePath = path.join(DIST_DIR, urlPath);
|
|
|
|
// Security check: ensure we're within DIST_DIR
|
|
if (!filePath.startsWith(DIST_DIR)) {
|
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
res.end('Forbidden');
|
|
return;
|
|
}
|
|
|
|
fs.stat(filePath, (err, stats) => {
|
|
if (err) {
|
|
// File not found; serve index.html for SPA routing (unless it's a file with extension)
|
|
const ext = path.extname(urlPath);
|
|
if (ext && ext !== '.html') {
|
|
// Likely a missing asset file
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
res.end('Not Found');
|
|
return;
|
|
}
|
|
|
|
// SPA fallback: serve index.html
|
|
const indexPath = path.join(DIST_DIR, 'index.html');
|
|
fs.readFile(indexPath, 'utf-8', (readErr, data) => {
|
|
if (readErr) {
|
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
res.end('Internal Server Error');
|
|
return;
|
|
}
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end(data);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (stats.isDirectory()) {
|
|
// Try to serve index.html from the directory
|
|
const indexPath = path.join(filePath, 'index.html');
|
|
fs.readFile(indexPath, 'utf-8', (readErr, data) => {
|
|
if (readErr) {
|
|
// Directory exists but no index.html; fall back to SPA index
|
|
const mainIndexPath = path.join(DIST_DIR, 'index.html');
|
|
fs.readFile(mainIndexPath, 'utf-8', (mainErr, mainData) => {
|
|
if (mainErr) {
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
res.end('Not Found');
|
|
return;
|
|
}
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end(mainData);
|
|
});
|
|
return;
|
|
}
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end(data);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Serve file with appropriate MIME type
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
|
|
fs.readFile(filePath, (readErr, data) => {
|
|
if (readErr) {
|
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
res.end('Internal Server Error');
|
|
return;
|
|
}
|
|
res.writeHead(200, { 'Content-Type': contentType });
|
|
res.end(data);
|
|
});
|
|
});
|
|
});
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`Rocketry server listening on http://localhost:${PORT}`);
|
|
});
|
|
|
|
server.on('error', (err) => {
|
|
if (err.code === 'EACCES') {
|
|
console.error(`Error: Permission denied. Port ${PORT} may require elevated privileges.`);
|
|
} else if (err.code === 'EADDRINUSE') {
|
|
console.error(`Error: Port ${PORT} is already in use.`);
|
|
} else {
|
|
console.error('Server error:', err.message);
|
|
}
|
|
process.exit(1);
|
|
});
|
|
SERVEREOF
|
|
|
|
# Make server.js executable
|
|
sudo chmod +x "$INSTALL_DIR/server.js"
|
|
|
|
# Write systemd unit file
|
|
echo "Writing systemd service file..."
|
|
sudo bash -c "cat > /etc/systemd/system/rocketry.service" <<UNITEOF
|
|
[Unit]
|
|
Description=Rocketry Equation Solver
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$CURRENT_USER
|
|
WorkingDirectory=/opt/rocketry
|
|
ExecStart=$NODE_PATH /opt/rocketry/server.js
|
|
Environment="PORT=$PORT"
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNITEOF
|
|
|
|
# Enable and start service
|
|
echo "Configuring systemd service..."
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable rocketry
|
|
sudo systemctl restart rocketry
|
|
|
|
echo ""
|
|
echo "✓ Rocketry service installed successfully!"
|
|
echo ""
|
|
echo " Location: $INSTALL_DIR"
|
|
echo " Port: $PORT"
|
|
echo " URL: http://localhost:$PORT"
|
|
echo ""
|
|
echo "Service status:"
|
|
sudo systemctl status rocketry --no-pager | sed 's/^/ /'
|
|
echo ""
|
|
echo "Useful commands:"
|
|
echo " Check status: sudo systemctl status rocketry"
|
|
echo " View logs: sudo journalctl -u rocketry -f"
|
|
echo " Restart: sudo systemctl restart rocketry"
|
|
echo " Stop: sudo systemctl stop rocketry"
|
|
echo " Uninstall: sudo systemctl disable rocketry && sudo rm -rf $INSTALL_DIR /etc/systemd/system/rocketry.service && sudo systemctl daemon-reload"
|