@tmlmobilidade/backupd 20251229.1441.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +55 -0
- package/README.md +125 -0
- package/config.example.yaml +106 -0
- package/eslint.config.mjs +14 -0
- package/package.json +40 -0
- package/src/backup/backup.service.ts +84 -0
- package/src/config/config-loader.ts +168 -0
- package/src/config/config-types.ts +35 -0
- package/src/database/database.factory.ts +35 -0
- package/src/database/database.interface.ts +21 -0
- package/src/database/mongo.service.ts +172 -0
- package/src/database/postgres.service.ts +95 -0
- package/src/index.ts +61 -0
- package/src/mailer/mailer.service.ts +71 -0
- package/tsconfig.json +12 -0
package/Dockerfile
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# # # # # # # # #
|
|
2
|
+
|
|
3
|
+
FROM node:alpine AS base
|
|
4
|
+
|
|
5
|
+
ENV APP=backupd
|
|
6
|
+
|
|
7
|
+
RUN npm install -g turbo@^2
|
|
8
|
+
|
|
9
|
+
# Install MongoDB
|
|
10
|
+
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.6/main' >> /etc/apk/repositories
|
|
11
|
+
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.6/community' >> /etc/apk/repositories
|
|
12
|
+
RUN apk update
|
|
13
|
+
RUN apk add mongodb-tools
|
|
14
|
+
|
|
15
|
+
# # # # # # # # #
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# # #
|
|
19
|
+
# BUILDER STAGE
|
|
20
|
+
|
|
21
|
+
FROM base AS pruner
|
|
22
|
+
|
|
23
|
+
WORKDIR /app
|
|
24
|
+
|
|
25
|
+
COPY . .
|
|
26
|
+
|
|
27
|
+
RUN turbo prune --scope=@tmlmobilidade/${APP} --docker
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# # #
|
|
31
|
+
# INSTALLER STAGE
|
|
32
|
+
|
|
33
|
+
FROM base AS builder
|
|
34
|
+
|
|
35
|
+
WORKDIR /app
|
|
36
|
+
|
|
37
|
+
# First install the dependencies (as they change less often)
|
|
38
|
+
COPY --from=pruner /app/out/json/ .
|
|
39
|
+
RUN npm install
|
|
40
|
+
|
|
41
|
+
# Build the app
|
|
42
|
+
COPY --from=pruner /app/out/full/ .
|
|
43
|
+
RUN turbo run build --filter=@tmlmobilidade/${APP}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# # #
|
|
47
|
+
# RUNNER STAGE
|
|
48
|
+
|
|
49
|
+
FROM base AS runner
|
|
50
|
+
|
|
51
|
+
WORKDIR /app
|
|
52
|
+
|
|
53
|
+
COPY --from=builder /app .
|
|
54
|
+
|
|
55
|
+
CMD node configs/${APP}/dist/index.js
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
The service will run continuously, performing backups at the interval specified in the configuration file.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
This service uses a configuration file in YAML format. There is an example file in the repository (`config.example.yaml`).
|
|
8
|
+
|
|
9
|
+
In addition to the configuration file, this service uses the environment variable `BACKUPD_CONFIG_PATH` to locate the configuration file.
|
|
10
|
+
|
|
11
|
+
## Configuration File Template
|
|
12
|
+
|
|
13
|
+
The configuration file includes **storage**, **database**,**backup schedule**, and optional **email notification** settings. This file enables you to specify the exact setup for these operations.
|
|
14
|
+
|
|
15
|
+
### Structure of `config.yaml`
|
|
16
|
+
|
|
17
|
+
- **Storage Configuration**
|
|
18
|
+
- **Database Configuration**
|
|
19
|
+
- **Backup Configuration**
|
|
20
|
+
- **Email Configuration**
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### Configuration Parameters
|
|
25
|
+
|
|
26
|
+
#### 1. Storage Configuration
|
|
27
|
+
Defines the storage service for backups.
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
storage:
|
|
31
|
+
type: STORAGE_TYPE
|
|
32
|
+
aws_config:
|
|
33
|
+
aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID
|
|
34
|
+
aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
|
|
35
|
+
bucket_name: YOUR_BUCKET_NAME
|
|
36
|
+
region: YOUR_REGION
|
|
37
|
+
oci_config:
|
|
38
|
+
tenancy: YOUR_TENANCY
|
|
39
|
+
user: YOUR_USER
|
|
40
|
+
fingerprint: YOUR_FINGERPRINT
|
|
41
|
+
private_key_path: YOUR_PRIVATE_KEY_PATH
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `type`: Set to `"aws"` for AWS S3 or `"oci"` for Oracle Cloud Storage.
|
|
45
|
+
- **AWS Config**: (Required if `type` is `"aws"`)
|
|
46
|
+
- `aws_access_key_id`, `aws_secret_access_key`: AWS credentials.
|
|
47
|
+
- `bucket_name`, `region`: Define the S3 bucket and region.
|
|
48
|
+
- **OCI Config**: (Required if `type` is `"oci"`)
|
|
49
|
+
- `tenancy`, `user`, `fingerprint`, `private_key_path`: Required fields for Oracle Cloud access.
|
|
50
|
+
|
|
51
|
+
#### 2. Database Configuration
|
|
52
|
+
Specifies the database type and connection parameters.
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
database:
|
|
56
|
+
type: DATABASE_TYPE
|
|
57
|
+
mongodb_config:
|
|
58
|
+
uri: YOUR_MONGODB_URI
|
|
59
|
+
options:
|
|
60
|
+
connectTimeoutMS: 10000
|
|
61
|
+
...
|
|
62
|
+
postgres_config:
|
|
63
|
+
uri: YOUR_POSTGRES_URI
|
|
64
|
+
options:
|
|
65
|
+
max: 10
|
|
66
|
+
min: 5
|
|
67
|
+
...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- `type`: Database service to use (`mongodb` or `postgres`).
|
|
71
|
+
- **MongoDB Config**: (Required if `type` is `"mongodb"`)
|
|
72
|
+
- `uri`: MongoDB connection URI.
|
|
73
|
+
- `options`: Optional connection parameters (timeout, pooling, and read preferences).
|
|
74
|
+
- **PostgreSQL Config**: (Required if `type` is `"postgres"`)
|
|
75
|
+
- `uri`: PostgreSQL connection URI.
|
|
76
|
+
- `options`: Pooling options.
|
|
77
|
+
|
|
78
|
+
#### 3. Backup Configuration
|
|
79
|
+
Manages backup intervals, destinations, and retention.
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
backup:
|
|
83
|
+
interval: 360
|
|
84
|
+
destination: backup/PROJECT_NAME/
|
|
85
|
+
max_remote_backups: 10
|
|
86
|
+
max_local_backups: 10
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `interval`: Frequency of backups in minutes (e.g., `360` for every 6 hours).
|
|
90
|
+
- `destination`: Local directory path for storing backups.
|
|
91
|
+
- `max_remote_backups`: Set retention limits for remote backups. (If set to 0, there will be no limit stored in the remote storage.)
|
|
92
|
+
- `max_local_backups`: Set retention limits for local backups. (If set to 0, all local backups will be deleted.)
|
|
93
|
+
|
|
94
|
+
#### 4. Email Configuration (Optional)
|
|
95
|
+
Enables email notifications upon backup success or failure.
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
email:
|
|
99
|
+
send_success: true
|
|
100
|
+
send_failure: true
|
|
101
|
+
mail_options:
|
|
102
|
+
from: FROM_EMAIL_ADDRESS
|
|
103
|
+
to: TO_EMAIL_ADDRESS
|
|
104
|
+
subject: BACKUP_REPORT_SUBJECT
|
|
105
|
+
smtp:
|
|
106
|
+
host: YOUR_SMTP_HOST
|
|
107
|
+
port: YOUR_SMTP_PORT
|
|
108
|
+
auth:
|
|
109
|
+
user: YOUR_SMTP_USER
|
|
110
|
+
pass: YOUR_SMTP_PASSWORD
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `send_success`, `send_failure`: Set to `true` to enable email notifications.
|
|
114
|
+
- `mail_options`: Sender and recipient details.
|
|
115
|
+
- `smtp`: SMTP server configuration for outgoing emails.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### Usage
|
|
120
|
+
1. Copy this template file as `config.yaml`.
|
|
121
|
+
2. Replace the placeholder values with your actual configurations.
|
|
122
|
+
3. Save and start the `backupd` service.
|
|
123
|
+
|
|
124
|
+
This file enables `backupd` to seamlessly back up and monitor your databases across different storage services.
|
|
125
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# This is an example configuration file for backupd.
|
|
2
|
+
# It is not used by the application, but it is a template for the configuration file needed to run the backupd service
|
|
3
|
+
|
|
4
|
+
# The configuration file is a YAML file, which is a human-readable data serialization format.
|
|
5
|
+
# It is used to configure the backupd service.
|
|
6
|
+
|
|
7
|
+
# The Storage Configuration
|
|
8
|
+
storage:
|
|
9
|
+
# The type of storage service to use. Options: "aws" (AWS) | "oci" (Oracle Cloud Infrastructure Object Storage)
|
|
10
|
+
type: STORAGE_TYPE
|
|
11
|
+
|
|
12
|
+
# The AWS configuration
|
|
13
|
+
aws_config:
|
|
14
|
+
aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID
|
|
15
|
+
aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
|
|
16
|
+
bucket_name: YOUR_BUCKET_NAME
|
|
17
|
+
region: YOUR_REGION
|
|
18
|
+
|
|
19
|
+
# The Oracle Cloud Infrastructure Object Storage configuration
|
|
20
|
+
oci_config:
|
|
21
|
+
user: OCI_USER
|
|
22
|
+
fingerprint: OCI_FINGERPRINT
|
|
23
|
+
tenancy: OCI_TENANCY
|
|
24
|
+
region: OCI_REGION
|
|
25
|
+
private_key: OCI_PRIVATE_KEY # The private key of the user in PEM format
|
|
26
|
+
namespace: OCI_NAMESPACE
|
|
27
|
+
bucket_name: OCI_BUCKET_NAME
|
|
28
|
+
|
|
29
|
+
# The Cloudflare R2 configuration
|
|
30
|
+
r2_config:
|
|
31
|
+
account_id: YOUR_ACCOUNT_ID
|
|
32
|
+
access_key_id: YOUR_ACCESS_KEY_ID
|
|
33
|
+
secret_access_key: YOUR_SECRET_ACCESS_KEY
|
|
34
|
+
bucket_name: YOUR_BUCKET_NAME
|
|
35
|
+
endpoint: YOUR_ENDPOINT # https://<account_id>.r2.cloudflarestorage.com
|
|
36
|
+
|
|
37
|
+
# The Database Configuration
|
|
38
|
+
database:
|
|
39
|
+
# The type of database service to use. Options: "mongodb" (MongoDB) | "postgres" (PostgreSQL)
|
|
40
|
+
type: DATABASE_TYPE
|
|
41
|
+
|
|
42
|
+
# The MongoDB configuration (required if type is mongodb)
|
|
43
|
+
mongodb_config:
|
|
44
|
+
uri: YOUR_MONGODB_URI
|
|
45
|
+
options: # (Optional) The options to pass to the MongoDB client.
|
|
46
|
+
# The time in milliseconds to attempt a connection before timing out.
|
|
47
|
+
connectTimeoutMS: 10000
|
|
48
|
+
# Whether to use direct connection to the MongoDB server.
|
|
49
|
+
directConnection: true
|
|
50
|
+
# The maximum number of connections in the connection pool.
|
|
51
|
+
maxPoolSize: 10
|
|
52
|
+
# The minimum number of connections in the connection pool.
|
|
53
|
+
minPoolSize: 5
|
|
54
|
+
# The read preference to use. Options: "primary" | "primaryPreferred" | "secondary" | "secondaryPreferred" | "nearest"
|
|
55
|
+
readPreference: YOUR_READ_PREFERENCE
|
|
56
|
+
# The time in milliseconds to wait for a server to respond to a request before timing out.
|
|
57
|
+
serverSelectionTimeoutMS: 10000
|
|
58
|
+
# (Optional) The MongoDB dump options.
|
|
59
|
+
dump_options:
|
|
60
|
+
# (Optional) The database name to dump.
|
|
61
|
+
database: YOUR_DATABASE_NAME
|
|
62
|
+
# (Optional) The collections to exclude from the dump.
|
|
63
|
+
exclude_collections:
|
|
64
|
+
- YOUR_COLLECTION_NAME
|
|
65
|
+
- YOUR_COLLECTION_NAME
|
|
66
|
+
|
|
67
|
+
# The PostgreSQL configuration (required if type is postgres)
|
|
68
|
+
postgres_config:
|
|
69
|
+
uri: YOUR_POSTGRES_URI
|
|
70
|
+
options: # (Optional) The options to pass to the PostgreSQL client.
|
|
71
|
+
# The maximum number of connections in the connection pool.
|
|
72
|
+
max: 10
|
|
73
|
+
# The minimum number of connections in the connection pool.
|
|
74
|
+
min: 5
|
|
75
|
+
|
|
76
|
+
# The Backup Configuration
|
|
77
|
+
backup:
|
|
78
|
+
# The interval between backups in minutes.
|
|
79
|
+
interval: 360 # 6 hour
|
|
80
|
+
# Backup destination directory.
|
|
81
|
+
destination: PATH_TO_BACKUP_DESTINATION_DIRECTORY
|
|
82
|
+
# The remote destination directory.
|
|
83
|
+
remote_destination: PATH_TO_REMOTE_DESTINATION_DIRECTORY
|
|
84
|
+
# The maximum number of backups to keep. (If undefined or 0, no backups will be deleted.)
|
|
85
|
+
max_remote_backups: 10
|
|
86
|
+
# The maximum number of backup files to keep in the storage. (If set to 0, no backups will be stored in the device storage.)
|
|
87
|
+
max_local_backups: 10
|
|
88
|
+
|
|
89
|
+
# The Email Configuration (if not set, no email will be sent)
|
|
90
|
+
email:
|
|
91
|
+
# Whether to send the backup success report.
|
|
92
|
+
send_success: true
|
|
93
|
+
# Whether to send the backup failure report.
|
|
94
|
+
send_failure: true
|
|
95
|
+
# The mail options.
|
|
96
|
+
mail_options:
|
|
97
|
+
from: FROM_EMAIL_ADDRESS
|
|
98
|
+
to: TO_EMAIL_ADDRESS
|
|
99
|
+
subject: BACKUP_REPORT_SUBJECT
|
|
100
|
+
# The SMTP server configuration.
|
|
101
|
+
smtp:
|
|
102
|
+
host: YOUR_SMTP_HOST
|
|
103
|
+
port: YOUR_SMTP_PORT
|
|
104
|
+
auth:
|
|
105
|
+
user: YOUR_SMTP_USER
|
|
106
|
+
pass: YOUR_SMTP_PASSWORD
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tmlmobilidade/backupd",
|
|
3
|
+
"description": "A backup service for databases.",
|
|
4
|
+
"version": "20251229.1441.35",
|
|
5
|
+
"author": {
|
|
6
|
+
"email": "iso@tmlmobilidade.pt",
|
|
7
|
+
"name": "TML-ISO"
|
|
8
|
+
},
|
|
9
|
+
"license": "AGPL-3.0-or-later",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && resolve-tspaths",
|
|
14
|
+
"lint": "eslint ./src && tsc --noEmit",
|
|
15
|
+
"start": "npm run build && node dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@tmlmobilidade/consts": "*",
|
|
19
|
+
"@tmlmobilidade/dates": "*",
|
|
20
|
+
"@tmlmobilidade/emails": "*",
|
|
21
|
+
"@tmlmobilidade/interfaces": "*",
|
|
22
|
+
"@tmlmobilidade/logger": "*",
|
|
23
|
+
"@tmlmobilidade/types": "*",
|
|
24
|
+
"@tmlmobilidade/utils": "*",
|
|
25
|
+
"@types/archiver": "7.0.0",
|
|
26
|
+
"archiver": "7.0.1",
|
|
27
|
+
"mongodb": "7.0.0",
|
|
28
|
+
"nodemailer": "7.0.12",
|
|
29
|
+
"pg": "8.16.3",
|
|
30
|
+
"react": "19.2.3",
|
|
31
|
+
"resolve-tspaths": "0.8.23",
|
|
32
|
+
"yaml": "2.8.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@tmlmobilidade/tsconfig": "*",
|
|
36
|
+
"@types/node": "25.0.3",
|
|
37
|
+
"@types/nodemailer": "7.0.4",
|
|
38
|
+
"typescript": "5.9.3"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { IDatabaseService } from '@/database/database.interface.js';
|
|
2
|
+
import { IStorageProvider } from '@tmlmobilidade/interfaces';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export interface BackupConfig {
|
|
7
|
+
destination: string
|
|
8
|
+
interval: number
|
|
9
|
+
max_local_backups: number
|
|
10
|
+
max_remote_backups: number
|
|
11
|
+
remote_destination: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class BackupService {
|
|
15
|
+
private readonly config: BackupConfig;
|
|
16
|
+
private readonly databaseService: IDatabaseService;
|
|
17
|
+
private readonly storageService: IStorageProvider;
|
|
18
|
+
|
|
19
|
+
constructor(config: BackupConfig, databaseService: IDatabaseService, storageService: IStorageProvider) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.databaseService = databaseService;
|
|
22
|
+
this.storageService = storageService;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Perform a backup of the database and upload it to the storage.
|
|
27
|
+
*/
|
|
28
|
+
public async backup(): Promise<void> {
|
|
29
|
+
// Create a backup directory with the current timestamp
|
|
30
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
31
|
+
const backupDir = path.join(this.config.destination, timestamp);
|
|
32
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
// Perform backup
|
|
35
|
+
await this.databaseService.backup(backupDir);
|
|
36
|
+
|
|
37
|
+
// Upload backup to storage
|
|
38
|
+
const backupFile = fs.readFileSync(backupDir + '.zip');
|
|
39
|
+
await this.storageService.uploadFile(path.join(this.config.remote_destination, timestamp + '.zip'), backupFile);
|
|
40
|
+
|
|
41
|
+
// Delete old backups
|
|
42
|
+
console.log('Deleting old backups...', {
|
|
43
|
+
max_local_backups: this.config.max_local_backups,
|
|
44
|
+
max_remote_backups: this.config.max_remote_backups,
|
|
45
|
+
});
|
|
46
|
+
if (this.config.max_local_backups > 0) {
|
|
47
|
+
await this.deleteLocalBackups();
|
|
48
|
+
}
|
|
49
|
+
else if (this.config.max_local_backups === 0) { // Delete all local backups
|
|
50
|
+
fs.rmSync(this.config.destination, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.config.max_remote_backups > 0) {
|
|
54
|
+
await this.deleteRemoteBackups();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Delete old backups from the local storage.
|
|
60
|
+
*/
|
|
61
|
+
private async deleteLocalBackups(): Promise<void> {
|
|
62
|
+
// Delete old local backups
|
|
63
|
+
const localBackups = fs.readdirSync(this.config.destination).sort();
|
|
64
|
+
if (localBackups.length > this.config.max_local_backups) {
|
|
65
|
+
// Delete oldest local backups first
|
|
66
|
+
const localBackupsToDelete = localBackups.slice(0, localBackups.length - this.config.max_local_backups);
|
|
67
|
+
for (const backup of localBackupsToDelete) {
|
|
68
|
+
fs.rmSync(path.join(this.config.destination, backup), { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete old backups from the local storage.
|
|
75
|
+
*/
|
|
76
|
+
private async deleteRemoteBackups(): Promise<void> {
|
|
77
|
+
const backups = await this.storageService.listFiles(this.config.remote_destination);
|
|
78
|
+
backups.sort(); // Sort by timestamp since they're ISO format strings
|
|
79
|
+
if (backups.length > this.config.max_remote_backups) {
|
|
80
|
+
const backupsToDelete = backups.slice(0, backups.length - this.config.max_remote_backups);
|
|
81
|
+
await Promise.all(backupsToDelete.map(backup => this.storageService.deleteFile(backup)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// configLoader.ts
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as yaml from 'yaml';
|
|
4
|
+
|
|
5
|
+
import { AppConfig } from './config-types.js';
|
|
6
|
+
|
|
7
|
+
function validateConfig(config: AppConfig) {
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
//
|
|
11
|
+
// Validate storage configuration
|
|
12
|
+
if (!config.storage) {
|
|
13
|
+
throw new Error('Missing required field \'storage\' configuration.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (config.storage.type !== 'oci') {
|
|
17
|
+
throw new Error('Invalid storage type. Supported types are \'oci\'.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (config.storage.type === 'oci') {
|
|
21
|
+
if (!config.storage.oci_config) {
|
|
22
|
+
throw new Error('Storage type is \'oci\' but \'oci_config\' is missing.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const keys_missing: string[] = [];
|
|
26
|
+
for (const key of ['user', 'fingerprint', 'tenancy', 'region', 'private_key', 'namespace', 'bucket_name']) {
|
|
27
|
+
if (!config.storage.oci_config[key]) {
|
|
28
|
+
keys_missing.push(key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (keys_missing.length > 0) {
|
|
32
|
+
throw new Error(`Missing required fields in 'oci_config'. Ensure ${keys_missing.join(', ')} are set.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//
|
|
37
|
+
// Validate database configuration
|
|
38
|
+
if (!config.database) {
|
|
39
|
+
throw new Error('Missing required field \'database\' configuration.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (config.database.type !== 'mongodb' && config.database.type !== 'postgres') {
|
|
43
|
+
throw new Error('Invalid database type. Supported types are \'mongodb\' and \'postgres\'.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (config.database.type === 'mongodb') {
|
|
47
|
+
if (!config.database.mongodb_config) {
|
|
48
|
+
throw new Error('Database type is \'mongodb\' but \'mongodb_config\' is missing.');
|
|
49
|
+
}
|
|
50
|
+
if (!config.database.mongodb_config.uri) {
|
|
51
|
+
throw new Error('Missing required field \'uri\' in \'mongodb_config\'.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (config.database.type === 'postgres') {
|
|
55
|
+
if (!config.database.postgres_config) {
|
|
56
|
+
throw new Error('Database type is \'postgres\' but \'postgres_config\' is missing.');
|
|
57
|
+
}
|
|
58
|
+
if (!config.database.postgres_config.uri) {
|
|
59
|
+
throw new Error('Missing required field \'uri\' in \'postgres_config\'.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//
|
|
64
|
+
// Validate Backup configuration
|
|
65
|
+
if (!config.backup) {
|
|
66
|
+
throw new Error('Missing required field \'backup\' configuration.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!config.backup.interval) {
|
|
70
|
+
throw new Error('Missing required field \'interval\' in \'backup\' configuration.');
|
|
71
|
+
}
|
|
72
|
+
else if (config.backup.interval <= 0 || typeof config.backup.interval !== 'number') {
|
|
73
|
+
throw new Error('\'interval\' must be a number greater than 0 in \'backup\' configuration.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!config.backup.destination) {
|
|
77
|
+
throw new Error('Missing required field \'destination\' in \'backup\' configuration.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!config.backup.max_local_backups) {
|
|
81
|
+
console.warn('\'max_local_backups\' is not set in \'backup\' configuration. No backups will be deleted.');
|
|
82
|
+
}
|
|
83
|
+
else if (config.backup.max_local_backups <= 0 || typeof config.backup.max_local_backups !== 'number') {
|
|
84
|
+
throw new Error('\'max_local_backups\' must be a number greater than 0 in \'backup\' configuration.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!config.backup.max_remote_backups) {
|
|
88
|
+
console.warn('\'max_remote_backups\' is not set in \'backup\' configuration. No backups will be stored in the device storage.');
|
|
89
|
+
}
|
|
90
|
+
else if (config.backup.max_remote_backups <= 0 || typeof config.backup.max_remote_backups !== 'number') {
|
|
91
|
+
throw new Error('\'max_remote_backups\' must be a number greater than 0 in \'backup\' configuration.');
|
|
92
|
+
}
|
|
93
|
+
else if (!config.backup.remote_destination) {
|
|
94
|
+
throw new Error('\'remote_destination\' is not set in \'backup\' configuration.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//
|
|
98
|
+
// Validate Email configuration
|
|
99
|
+
if (!config.email) {
|
|
100
|
+
console.warn('\'email\' configuration is not set. No email will be sent.');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const { mail_options, send_failure, send_success, smtp } = config.email;
|
|
104
|
+
if (typeof send_success !== 'boolean') {
|
|
105
|
+
throw new Error('\'email.send_success\' should be a boolean.');
|
|
106
|
+
}
|
|
107
|
+
if (typeof send_failure !== 'boolean') {
|
|
108
|
+
throw new Error('\'email.send_failure\' should be a boolean.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!mail_options) {
|
|
112
|
+
throw new Error('Missing \'mail_options\' in email configuration.');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
if (!mail_options.from || typeof mail_options.from !== 'string') {
|
|
116
|
+
throw new Error('\'from\' in \'mail_options\' configuration should be a string.');
|
|
117
|
+
}
|
|
118
|
+
if (!mail_options.subject || typeof mail_options.subject !== 'string') {
|
|
119
|
+
throw new Error('\'subject\' in \'mail_options\' configuration should be a string.');
|
|
120
|
+
}
|
|
121
|
+
if (!mail_options.to || (typeof mail_options.to !== 'string' && !Array.isArray(mail_options.to))) {
|
|
122
|
+
throw new Error('\'to\' in \'mail_options\' configuration should be a string or an array of strings.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!smtp) {
|
|
127
|
+
throw new Error('Missing \'smtp\' configuration in email.');
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const { host, port } = smtp;
|
|
131
|
+
if (!host) {
|
|
132
|
+
throw new Error('Missing \'host\' in \'smtp\' configuration.');
|
|
133
|
+
}
|
|
134
|
+
if (typeof port !== 'number' || port <= 0) {
|
|
135
|
+
throw new Error('\'smtp.port\' should be a positive number.');
|
|
136
|
+
}
|
|
137
|
+
if (!smtp.auth.user) {
|
|
138
|
+
throw new Error('Missing \'user\' in \'smtp.auth\' configuration.');
|
|
139
|
+
}
|
|
140
|
+
if (!smtp.auth.pass) {
|
|
141
|
+
throw new Error('Missing \'password\' in \'smtp.auth\' configuration.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function loadConfig(path: string): AppConfig {
|
|
148
|
+
try {
|
|
149
|
+
const fileContents = fs.readFileSync(path, 'utf8');
|
|
150
|
+
const config = yaml.parse(fileContents) as AppConfig;
|
|
151
|
+
|
|
152
|
+
// Validate the parsed configuration
|
|
153
|
+
validateConfig(config);
|
|
154
|
+
|
|
155
|
+
return config;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error instanceof SyntaxError) {
|
|
159
|
+
throw new Error(`YAML Syntax Error in config file: ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
else if (error.code === 'ENOENT') {
|
|
162
|
+
throw new Error(`Configuration file not found at path: ${path}`);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
throw new Error(`Failed to load configuration: ${error.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BackupConfig } from '@/backup/backup.service.js';
|
|
2
|
+
import { MongoDbConfig } from '@/database/mongo.service.js';
|
|
3
|
+
import { PostgresConfig } from '@/database/postgres.service.js';
|
|
4
|
+
import { EmailConfig } from '@/mailer/mailer.service.js';
|
|
5
|
+
import { OCIStorageProviderConfiguration } from '@tmlmobilidade/interfaces';
|
|
6
|
+
|
|
7
|
+
export interface StorageConfig {
|
|
8
|
+
oci_config?: OCIStorageProviderConfiguration
|
|
9
|
+
type: 'oci'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MongoDBOptions {
|
|
13
|
+
connectTimeoutMS: number
|
|
14
|
+
directConnection: boolean
|
|
15
|
+
dump_options?: {
|
|
16
|
+
exclude_collections?: string[]
|
|
17
|
+
}
|
|
18
|
+
maxPoolSize: number
|
|
19
|
+
minPoolSize: number
|
|
20
|
+
readPreference: string
|
|
21
|
+
serverSelectionTimeoutMS: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DatabaseConfig {
|
|
25
|
+
mongodb_config?: MongoDbConfig
|
|
26
|
+
postgres_config?: PostgresConfig
|
|
27
|
+
type: 'mongodb' | 'postgres'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AppConfig {
|
|
31
|
+
backup: BackupConfig
|
|
32
|
+
database: DatabaseConfig
|
|
33
|
+
email?: EmailConfig
|
|
34
|
+
storage: StorageConfig
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
|
2
|
+
import { IDatabaseService } from './database.interface.js';
|
|
3
|
+
import { MongoDbConfig, MongoDbService } from './mongo.service.js';
|
|
4
|
+
import { PostgresConfig, PostgresService } from './postgres.service.js';
|
|
5
|
+
|
|
6
|
+
export interface DatabaseConfiguration {
|
|
7
|
+
mongodb_config?: MongoDbConfig
|
|
8
|
+
postgres_config?: PostgresConfig
|
|
9
|
+
type: 'mongodb' | 'postgres'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class DatabaseFactory {
|
|
13
|
+
/**
|
|
14
|
+
* Creates and returns an instance of a database service based on the provided configuration.
|
|
15
|
+
*
|
|
16
|
+
* @param config - The database configuration object.
|
|
17
|
+
* @returns An instance of a class that implements IDatabaseService.
|
|
18
|
+
*/
|
|
19
|
+
public static create(config: DatabaseConfiguration): IDatabaseService {
|
|
20
|
+
switch (config.type) {
|
|
21
|
+
case 'mongodb':
|
|
22
|
+
if (!config.mongodb_config || !config.mongodb_config.uri) {
|
|
23
|
+
throw new Error('MongoDB configuration is missing or incomplete.');
|
|
24
|
+
}
|
|
25
|
+
return MongoDbService.getInstance(config.mongodb_config);
|
|
26
|
+
case 'postgres':
|
|
27
|
+
if (!config.postgres_config || !config.postgres_config.uri) {
|
|
28
|
+
throw new Error('PostgreSQL configuration is missing or incomplete.');
|
|
29
|
+
}
|
|
30
|
+
return new PostgresService(config.postgres_config);
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unsupported database type: ${config.type}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface IDatabaseService {
|
|
2
|
+
/**
|
|
3
|
+
* Backs up the database.
|
|
4
|
+
*/
|
|
5
|
+
backup(outputPath: string): Promise<void>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Connects to the database.
|
|
9
|
+
*/
|
|
10
|
+
connect(): Promise<unknown>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Disconnects from the database.
|
|
14
|
+
*/
|
|
15
|
+
disconnect(): Promise<unknown>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Restores the database from the latest backup.
|
|
19
|
+
*/
|
|
20
|
+
restore(backupPath: string): Promise<void>
|
|
21
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import archiver from 'archiver';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { Collection, Db, DbOptions, MongoClient, MongoClientOptions } from 'mongodb';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
import { IDatabaseService } from './database.interface.js';
|
|
8
|
+
|
|
9
|
+
export interface MongoDbConfig {
|
|
10
|
+
/** Options to control dump behavior */
|
|
11
|
+
dump_options?: {
|
|
12
|
+
database?: string
|
|
13
|
+
exclude_collections?: string[]
|
|
14
|
+
}
|
|
15
|
+
options?: MongoClientOptions
|
|
16
|
+
uri: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class MongoDbService implements IDatabaseService {
|
|
20
|
+
private static _instance: MongoDbService;
|
|
21
|
+
get client(): MongoClient {
|
|
22
|
+
return this._client;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private _client: MongoClient;
|
|
26
|
+
|
|
27
|
+
private _dumpOptions?: MongoDbConfig['dump_options'];
|
|
28
|
+
private _uri: string;
|
|
29
|
+
|
|
30
|
+
constructor(config: MongoDbConfig) {
|
|
31
|
+
this._client = new MongoClient(config.uri, config.options);
|
|
32
|
+
this._uri = config.uri;
|
|
33
|
+
this._dumpOptions = config.dump_options;
|
|
34
|
+
|
|
35
|
+
this._client.on('close', () => {
|
|
36
|
+
console.warn('MongoDB connection closed unexpectedly.');
|
|
37
|
+
});
|
|
38
|
+
this._client.on('reconnect', () => {
|
|
39
|
+
console.log('MongoDB reconnected.');
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the singleton instance of MongoDbService.
|
|
45
|
+
*/
|
|
46
|
+
public static getInstance(config?: MongoDbConfig): MongoDbService {
|
|
47
|
+
if (!MongoDbService._instance) {
|
|
48
|
+
if (!config?.uri) {
|
|
49
|
+
throw new Error('MongoDB URI is required');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
MongoDbService._instance = new MongoDbService(config);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return MongoDbService._instance;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Perform a MongoDB dump using mongodump command.
|
|
60
|
+
* @param outputPath - The directory path where the dump will be saved.
|
|
61
|
+
* @returns A promise that resolves on successful dump or rejects on error.
|
|
62
|
+
*/
|
|
63
|
+
async backup(outputPath: string): Promise<void> {
|
|
64
|
+
const dumpDir = path.resolve(outputPath);
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const excludeFlags = this._dumpOptions?.exclude_collections?.map(c => `--excludeCollection="${c}"`).join(' ') || '';
|
|
68
|
+
const databaseFlag = this._dumpOptions?.database ? `--db="${this._dumpOptions.database}"` : '';
|
|
69
|
+
|
|
70
|
+
const command = `mongodump --uri="${this._uri}" --out="${dumpDir}" ${excludeFlags} ${databaseFlag}`.trim();
|
|
71
|
+
|
|
72
|
+
exec(command, (error, stdout, stderr) => {
|
|
73
|
+
if (error) {
|
|
74
|
+
console.error(`⤷ Error running mongodump: ${stderr}`);
|
|
75
|
+
reject(new Error(`Mongodump failed: ${stderr}`));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log(`⤷ Mongodump completed successfully`);
|
|
79
|
+
|
|
80
|
+
// Create a file to stream archive data to.
|
|
81
|
+
const output = fs.createWriteStream(`${dumpDir}.zip`);
|
|
82
|
+
const archive = archiver('zip', {
|
|
83
|
+
zlib: { level: 9 }, // Sets the compression level.
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
archive.on('error', (err) => {
|
|
87
|
+
console.error(`⤷ Error creating zip: ${err.message}`);
|
|
88
|
+
reject(err);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Pipe archive data to the file.
|
|
92
|
+
archive.pipe(output);
|
|
93
|
+
|
|
94
|
+
// Append files from the dump directory.
|
|
95
|
+
archive.directory(dumpDir, false);
|
|
96
|
+
|
|
97
|
+
// Finalize the archive (i.e., finish the compression).
|
|
98
|
+
archive.finalize();
|
|
99
|
+
|
|
100
|
+
// Remove the dump directory after zipping.
|
|
101
|
+
output.on('close', () => {
|
|
102
|
+
fs.rmSync(dumpDir, { recursive: true });
|
|
103
|
+
console.log(`⤷ Backup has been zipped successfully. Total bytes: ${archive.pointer()}`);
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Connect to MongoDB and return the database instance.
|
|
113
|
+
*/
|
|
114
|
+
async connect(): Promise<MongoClient> {
|
|
115
|
+
if (!this._client) {
|
|
116
|
+
try {
|
|
117
|
+
await this._client.connect();
|
|
118
|
+
console.log('⤷ Connected to MongoDB.');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new Error('Error connecting to MongoDB', { cause: error });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return this._client;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a new Db instance sharing the current socket connections.
|
|
129
|
+
*
|
|
130
|
+
* @param dbName - The name of the database we want to use. If not provided, use database name from connection string.
|
|
131
|
+
* @param options - Optional settings for Db construction
|
|
132
|
+
*/
|
|
133
|
+
db(dbName?: string, options?: DbOptions): Db {
|
|
134
|
+
return this._client.db(dbName, options);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Close the MongoDB connection.
|
|
139
|
+
*/
|
|
140
|
+
async disconnect(): Promise<void> {
|
|
141
|
+
if (this._client) {
|
|
142
|
+
await this._client.close();
|
|
143
|
+
console.log('⤷ Disconnected from MongoDB.');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get a specific collection by name.
|
|
149
|
+
* @param db - The database instance.
|
|
150
|
+
* @param collectionName - The name of the collection to retrieve.
|
|
151
|
+
* @returns The collection instance.
|
|
152
|
+
*/
|
|
153
|
+
async getCollection<T>(db: Db, collectionName: string): Promise<Collection<T>> {
|
|
154
|
+
return db.collection<T>(collectionName);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Restores the database from the provided backup file.
|
|
159
|
+
*/
|
|
160
|
+
async restore(backupPath: string): Promise<void> {
|
|
161
|
+
const command = `mongorestore --uri="${this._uri}" --drop "${backupPath}"`;
|
|
162
|
+
|
|
163
|
+
exec(command, (error, stdout, stderr) => {
|
|
164
|
+
if (error) {
|
|
165
|
+
console.error(`⤷ Error running mongorestore: ${stderr}`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.log(`⤷ Mongorestore completed successfully:\n${stdout}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
|
|
3
|
+
import { IDatabaseService } from '@/database/database.interface.js';
|
|
4
|
+
import { exec, spawn } from 'child_process';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { Client, type ClientConfig } from 'pg';
|
|
8
|
+
|
|
9
|
+
/* * */
|
|
10
|
+
|
|
11
|
+
export interface PostgresConfig {
|
|
12
|
+
options?: ClientConfig
|
|
13
|
+
uri: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class PostgresService implements IDatabaseService {
|
|
17
|
+
private client: InstanceType<typeof Client>;
|
|
18
|
+
|
|
19
|
+
constructor(config: PostgresConfig) {
|
|
20
|
+
this.client = new Client({ connectionString: config.uri, ...config.options });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Backs up the database to the specified output path.
|
|
25
|
+
*/
|
|
26
|
+
async backup(outputPath: string): Promise<void> {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
// Ensure the output directory exists
|
|
29
|
+
const outputDir = path.dirname(outputPath);
|
|
30
|
+
if (!fs.existsSync(outputDir)) {
|
|
31
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Prepare the pg_dump command arguments
|
|
35
|
+
const args = [
|
|
36
|
+
'--dbname', this.client.database,
|
|
37
|
+
'--format', 'custom',
|
|
38
|
+
'--file', outputPath,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Spawn the pg_dump process
|
|
42
|
+
const dump = spawn('pg_dump', args);
|
|
43
|
+
|
|
44
|
+
dump.stdout.on('data', (data) => {
|
|
45
|
+
console.log(`pg_dump stdout: ${data}`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
dump.stderr.on('data', (data) => {
|
|
49
|
+
console.error(`pg_dump stderr: ${data}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
dump.on('close', (code) => {
|
|
53
|
+
if (code === 0) {
|
|
54
|
+
console.log(`Backup completed successfully. File saved to ${outputPath}`);
|
|
55
|
+
resolve();
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
reject(new Error(`pg_dump exited with code ${code}`));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
dump.on('error', (err) => {
|
|
63
|
+
reject(err);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Connects to the PostgreSQL database.
|
|
70
|
+
*/
|
|
71
|
+
async connect(): Promise<void> {
|
|
72
|
+
await this.client.connect();
|
|
73
|
+
console.log('Connected to PostgreSQL.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Disconnects from the PostgreSQL database.
|
|
78
|
+
*/
|
|
79
|
+
async disconnect(): Promise<void> {
|
|
80
|
+
await this.client.end();
|
|
81
|
+
console.log('Disconnected from PostgreSQL.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Restores the database from the provided backup file.
|
|
86
|
+
*/
|
|
87
|
+
async restore(backupPath: string): Promise<void> {
|
|
88
|
+
const command = `pg_restore --dbname="${this.client.database}" --file="${backupPath}"`;
|
|
89
|
+
exec(command, (error, stdout, stderr) => {
|
|
90
|
+
if (error) {
|
|
91
|
+
console.error(`⤷ Error running pg_restore: ${stderr}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { StorageConfiguration, StorageFactory } from '@tmlmobilidade/interfaces';
|
|
2
|
+
|
|
3
|
+
import { BackupService } from './backup/backup.service.js';
|
|
4
|
+
import { loadConfig } from './config/config-loader.js';
|
|
5
|
+
import { DatabaseConfiguration, DatabaseFactory } from './database/database.factory.js';
|
|
6
|
+
import { MailerService } from './mailer/mailer.service.js';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const config = loadConfig(process.env.CONFIG_PATH || './config.yaml');
|
|
10
|
+
|
|
11
|
+
const databaseConfig: DatabaseConfiguration = {
|
|
12
|
+
mongodb_config: config.database.mongodb_config,
|
|
13
|
+
postgres_config: config.database.postgres_config,
|
|
14
|
+
type: config.database.type,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const storageConfig: StorageConfiguration = {
|
|
18
|
+
oci_config: config.storage.oci_config,
|
|
19
|
+
type: config.storage.type,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Create database and storage services
|
|
23
|
+
const database = DatabaseFactory.create(databaseConfig);
|
|
24
|
+
const storage = StorageFactory.create(storageConfig);
|
|
25
|
+
const backup = new BackupService(config.backup, database, storage);
|
|
26
|
+
const mailer = new MailerService(config.email);
|
|
27
|
+
|
|
28
|
+
console.log('Running backup...');
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Connect to the database
|
|
32
|
+
await database.connect();
|
|
33
|
+
|
|
34
|
+
// Perform backup
|
|
35
|
+
await backup.backup();
|
|
36
|
+
|
|
37
|
+
if (config.email?.send_success) {
|
|
38
|
+
await mailer.sendSuccessMail();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error('Error during backup:', error);
|
|
43
|
+
|
|
44
|
+
if (config.email?.send_failure) {
|
|
45
|
+
await mailer.sendFailureMail(error.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
// Disconnect from the database
|
|
50
|
+
await database.disconnect();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, config.backup.interval * 1000 * 60));
|
|
54
|
+
main();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
main().catch(async (err) => {
|
|
58
|
+
console.error(err);
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * 60)); // Wait for 1 minute before running again
|
|
60
|
+
main();
|
|
61
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Dates } from '@tmlmobilidade/dates';
|
|
2
|
+
import { RenderFailedBackupEmail } from '@tmlmobilidade/emails';
|
|
3
|
+
import nodemailer, { Transporter } from 'nodemailer';
|
|
4
|
+
|
|
5
|
+
export interface MailOptions {
|
|
6
|
+
from: string
|
|
7
|
+
html?: string
|
|
8
|
+
subject: string
|
|
9
|
+
text?: string
|
|
10
|
+
to: string | string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SmtpConfig {
|
|
14
|
+
auth: {
|
|
15
|
+
pass: string
|
|
16
|
+
user: string
|
|
17
|
+
}
|
|
18
|
+
host: string
|
|
19
|
+
port: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EmailConfig {
|
|
23
|
+
mail_options: Omit<MailOptions, 'html' | 'text'>
|
|
24
|
+
send_failure: boolean
|
|
25
|
+
send_success: boolean
|
|
26
|
+
smtp: SmtpConfig
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class MailerService {
|
|
30
|
+
private transporter: Transporter;
|
|
31
|
+
|
|
32
|
+
constructor(private config: EmailConfig) {
|
|
33
|
+
this.transporter = nodemailer.createTransport(this.config.smtp);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async sendFailureMail(error: string): Promise<void> {
|
|
37
|
+
const emailHtml = await RenderFailedBackupEmail({
|
|
38
|
+
backup_service: this.config.mail_options.subject,
|
|
39
|
+
error_message: error,
|
|
40
|
+
failure_time: Dates.now('Europe/Lisbon').toLocaleString(Dates.FORMATS.DATETIME_FULL_WITH_SECONDS),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const mail_options = {
|
|
44
|
+
...this.config.mail_options,
|
|
45
|
+
html: emailHtml,
|
|
46
|
+
subject: `${this.config.mail_options.subject}: Falha na execução do backup`,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
await this.sendMail(mail_options);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async sendSuccessMail(): Promise<void> {
|
|
53
|
+
const mail_options = {
|
|
54
|
+
...this.config.mail_options,
|
|
55
|
+
subject: `${this.config.mail_options.subject}: Backup successful`,
|
|
56
|
+
text: 'Backup was successful',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
await this.sendMail(mail_options);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async sendMail(mail_options: MailOptions): Promise<void> {
|
|
63
|
+
try {
|
|
64
|
+
const info = await this.transporter.sendMail(mail_options);
|
|
65
|
+
console.log(`Email sent: ${info.messageId}`);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('Error sending email:', error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|