Many improvments + install script
This commit is contained in:
202
install-service.sh
Normal file
202
install-service.sh
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/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; }
|
||||
|
||||
echo "Rocketry Service Installer"
|
||||
echo "=========================="
|
||||
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=nobody
|
||||
WorkingDirectory=/opt/rocketry
|
||||
ExecStart=/usr/bin/node /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"
|
||||
Reference in New Issue
Block a user