feat: add CI/CD pipeline, backup system, and deployment automation
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Has been cancelled
CI/CD Pipeline / Code Quality (pull_request) Has been cancelled
CI Pipeline / Lint Code (pull_request) Has been cancelled
CI Pipeline / Test API Package (pull_request) Has been cancelled
CI Pipeline / Test Web Package (pull_request) Has been cancelled
CI Pipeline / Test Shared Package (pull_request) Has been cancelled
Docker Build & Deploy / Build Docker Images (pull_request) Has been cancelled
E2E Tests / End-to-End Tests (pull_request) Has been cancelled
E2E Tests / E2E Tests (Mobile) (pull_request) Has been cancelled
Security Scanning / NPM Audit (pull_request) Has been cancelled
Security Scanning / Dependency License Check (pull_request) Has been cancelled
Security Scanning / Code Quality Scan (pull_request) Has been cancelled
Security Scanning / Docker Image Security (pull_request) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (pull_request) Has been cancelled
CI Pipeline / Build All Packages (pull_request) Has been cancelled
CI Pipeline / Generate Coverage Report (pull_request) Has been cancelled
Docker Build & Deploy / Push Docker Images (pull_request) Has been cancelled
Docker Build & Deploy / Deploy to Staging (pull_request) Has been cancelled
Docker Build & Deploy / Deploy to Production (pull_request) Has been cancelled
Security Scanning / Security Summary (pull_request) Has been cancelled
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Has been cancelled
CI/CD Pipeline / Code Quality (pull_request) Has been cancelled
CI Pipeline / Lint Code (pull_request) Has been cancelled
CI Pipeline / Test API Package (pull_request) Has been cancelled
CI Pipeline / Test Web Package (pull_request) Has been cancelled
CI Pipeline / Test Shared Package (pull_request) Has been cancelled
Docker Build & Deploy / Build Docker Images (pull_request) Has been cancelled
E2E Tests / End-to-End Tests (pull_request) Has been cancelled
E2E Tests / E2E Tests (Mobile) (pull_request) Has been cancelled
Security Scanning / NPM Audit (pull_request) Has been cancelled
Security Scanning / Dependency License Check (pull_request) Has been cancelled
Security Scanning / Code Quality Scan (pull_request) Has been cancelled
Security Scanning / Docker Image Security (pull_request) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (pull_request) Has been cancelled
CI Pipeline / Build All Packages (pull_request) Has been cancelled
CI Pipeline / Generate Coverage Report (pull_request) Has been cancelled
Docker Build & Deploy / Push Docker Images (pull_request) Has been cancelled
Docker Build & Deploy / Deploy to Staging (pull_request) Has been cancelled
Docker Build & Deploy / Deploy to Production (pull_request) Has been cancelled
Security Scanning / Security Summary (pull_request) Has been cancelled
## Summary - Add complete CI/CD pipeline with Gitea Actions for automated testing, building, and deployment - Implement backup and restore system with full database and file backup to ZIP - Add deployment automation with webhook receiver and systemd service - Enhance recipe editing UI with improved ingredient parsing and cooking mode features - Add comprehensive documentation for CI/CD, deployment, and backup features ## CI/CD Pipeline - New workflow in .gitea/workflows/ci-cd.yml with test, build, and deploy stages - Automated Docker image building and pushing to registry - Webhook-triggered deployments to production servers ## Backup & Restore - New backup service with ZIP creation including database dump and uploads - REST API endpoints for create, list, download, restore, and delete operations - Configurable backup path via BACKUP_PATH environment variable ## Deployment - Automated deployment scripts (deploy.sh, manual-deploy.sh) - Webhook receiver with systemd service for deployment triggers - Environment configuration template (.env.deploy.example) ## Documentation - docs/CI-CD-SETUP.md - Complete CI/CD pipeline setup guide - docs/DEPLOYMENT-QUICK-START.md - Quick deployment reference - docs/BACKUP.md - Backup and restore documentation - docs/REMOTE_DATABASE.md - Remote database configuration guide - scripts/README.md - Deployment scripts documentation ## Web Improvements - Enhanced ingredient parser with better unit and quantity detection - Improved recipe editing interface with unified edit experience - Better cooking mode functionality - Updated dependencies in package.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
258
packages/api/src/routes/backup.routes.ts
Normal file
258
packages/api/src/routes/backup.routes.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { createBackup, restoreBackup, listBackups, deleteBackup } from '../services/backup.service';
|
||||
import multer from 'multer';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Configure multer for backup file uploads
|
||||
const upload = multer({
|
||||
dest: '/tmp/basil-restore/',
|
||||
limits: {
|
||||
fileSize: 1024 * 1024 * 1024, // 1GB max
|
||||
},
|
||||
});
|
||||
|
||||
// Get backup directory from env or use default
|
||||
const getBackupDir = (): string => {
|
||||
return process.env.BACKUP_PATH || path.join(__dirname, '../../../backups');
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /api/backup
|
||||
* Creates a new backup of all data and files
|
||||
*/
|
||||
router.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const backupDir = getBackupDir();
|
||||
await fs.mkdir(backupDir, { recursive: true });
|
||||
|
||||
const backupPath = await createBackup(backupDir);
|
||||
const fileName = path.basename(backupPath);
|
||||
const stats = await fs.stat(backupPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Backup created successfully',
|
||||
backup: {
|
||||
name: fileName,
|
||||
path: backupPath,
|
||||
size: stats.size,
|
||||
created: stats.birthtime,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Backup creation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create backup',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/backup
|
||||
* Lists all available backups
|
||||
*/
|
||||
router.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const backupDir = getBackupDir();
|
||||
const backups = await listBackups(backupDir);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
backups,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error listing backups:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to list backups',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/backup/:filename
|
||||
* Downloads a specific backup file
|
||||
*/
|
||||
router.get('/:filename', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
const backupDir = getBackupDir();
|
||||
const backupPath = path.join(backupDir, filename);
|
||||
|
||||
// Security check: ensure the file is within the backup directory
|
||||
const resolvedPath = path.resolve(backupPath);
|
||||
const resolvedBackupDir = path.resolve(backupDir);
|
||||
if (!resolvedPath.startsWith(resolvedBackupDir)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await fs.access(backupPath);
|
||||
} catch {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Backup file not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Send file
|
||||
res.download(backupPath, filename, (err) => {
|
||||
if (err) {
|
||||
console.error('Error downloading backup:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to download backup',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error downloading backup:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to download backup',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/backup/restore
|
||||
* Restores data from a backup file
|
||||
* Accepts either:
|
||||
* - multipart/form-data with 'backup' file field
|
||||
* - JSON with 'filename' field (for existing backup in backup directory)
|
||||
*/
|
||||
router.post('/restore', upload.single('backup'), async (req: Request, res: Response) => {
|
||||
let backupPath: string | null = null;
|
||||
let isTemporaryFile = false;
|
||||
|
||||
try {
|
||||
const backupDir = getBackupDir();
|
||||
|
||||
// Check if file was uploaded or filename provided
|
||||
if (req.file) {
|
||||
backupPath = req.file.path;
|
||||
isTemporaryFile = true;
|
||||
} else if (req.body.filename) {
|
||||
backupPath = path.join(backupDir, req.body.filename);
|
||||
|
||||
// Security check
|
||||
const resolvedPath = path.resolve(backupPath);
|
||||
const resolvedBackupDir = path.resolve(backupDir);
|
||||
if (!resolvedPath.startsWith(resolvedBackupDir)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await fs.access(backupPath);
|
||||
} catch {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Backup file not found',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'No backup file provided. Either upload a file or specify a filename.',
|
||||
});
|
||||
}
|
||||
|
||||
// Perform restore
|
||||
const metadata = await restoreBackup(backupPath, backupDir);
|
||||
|
||||
// Clean up temporary file if it was uploaded
|
||||
if (isTemporaryFile && backupPath) {
|
||||
try {
|
||||
await fs.unlink(backupPath);
|
||||
} catch (err) {
|
||||
console.warn('Failed to clean up temporary file:', err);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Backup restored successfully',
|
||||
metadata,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Restore error:', error);
|
||||
|
||||
// Clean up temporary file on error
|
||||
if (isTemporaryFile && backupPath) {
|
||||
try {
|
||||
await fs.unlink(backupPath);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to restore backup',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/backup/:filename
|
||||
* Deletes a backup file
|
||||
*/
|
||||
router.delete('/:filename', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
const backupDir = getBackupDir();
|
||||
const backupPath = path.join(backupDir, filename);
|
||||
|
||||
// Security check
|
||||
const resolvedPath = path.resolve(backupPath);
|
||||
const resolvedBackupDir = path.resolve(backupDir);
|
||||
if (!resolvedPath.startsWith(resolvedBackupDir)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await fs.access(backupPath);
|
||||
} catch {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Backup file not found',
|
||||
});
|
||||
}
|
||||
|
||||
await deleteBackup(backupPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Backup deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting backup:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete backup',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user