@live-change/backup-service 0.9.207 → 0.9.209
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/backup.js +118 -46
- package/index.js +6 -4
- package/package.json +4 -4
package/backup.js
CHANGED
|
@@ -9,6 +9,7 @@ import { exec as cpExec } from 'child_process'
|
|
|
9
9
|
const exec = util.promisify(cpExec)
|
|
10
10
|
import dump from '@live-change/db-client/lib/dump.js'
|
|
11
11
|
import { once } from 'events'
|
|
12
|
+
import { finished } from 'node:stream/promises'
|
|
12
13
|
import os from "os"
|
|
13
14
|
import path from 'path'
|
|
14
15
|
|
|
@@ -17,6 +18,14 @@ function currentBackupPath(backupsDir = './backups/') {
|
|
|
17
18
|
return `${backupsDir}/${dateString.substring(0, dateString.length - 1)}`
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function createBackupLogger(backupsDir) {
|
|
22
|
+
const logPath = path.join(path.resolve(backupsDir), 'backups.log')
|
|
23
|
+
return async function log(message) {
|
|
24
|
+
const line = `${new Date().toISOString()} ${message}\n`
|
|
25
|
+
await fs.promises.appendFile(logPath, line, { encoding: 'utf8' })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
async function writeDbBackup(stream) {
|
|
21
30
|
let drainPromise
|
|
22
31
|
async function write(object) {
|
|
@@ -27,11 +36,10 @@ async function writeDbBackup(stream) {
|
|
|
27
36
|
drainPromise = null
|
|
28
37
|
}
|
|
29
38
|
}
|
|
30
|
-
|
|
39
|
+
try {
|
|
40
|
+
await dump({
|
|
31
41
|
dao: app.dao,
|
|
32
42
|
db: app.databaseName,
|
|
33
|
-
//serverUrl: process.env.DB_URL || 'http://localhost:9417/api/ws',
|
|
34
|
-
//db: process.env.DB_NAME || 'test',
|
|
35
43
|
structure: true,
|
|
36
44
|
verbose: true,
|
|
37
45
|
bucket: 128, // less database stress
|
|
@@ -39,64 +47,128 @@ async function writeDbBackup(stream) {
|
|
|
39
47
|
},
|
|
40
48
|
(method, ...args) => write({ type: 'request', method, parameters: args }),
|
|
41
49
|
() => write({ type: 'sync' })
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
)
|
|
51
|
+
stream.end()
|
|
52
|
+
await finished(stream)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
stream.destroy()
|
|
55
|
+
throw err
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
function parseBackupTimestamp(basename) {
|
|
60
|
+
const match = basename.match(/^(\d{4})_(\d{2})_(\d{2})_(\d{2})_(\d{2})_(\d{2})_(\d+)/)
|
|
61
|
+
if(!match) return null
|
|
62
|
+
const [_full, year, month, day, hour, minute, second, millis] = match
|
|
63
|
+
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}.${millis}Z`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function createBackup(backupPath, log) {
|
|
67
|
+
const resolvedPath = path.resolve(backupPath)
|
|
68
|
+
const parentDir = path.dirname(resolvedPath)
|
|
69
|
+
const base = path.basename(resolvedPath)
|
|
70
|
+
const tarTmpPath = path.join(parentDir, `${base}.tar.gz.tmp`)
|
|
71
|
+
|
|
72
|
+
await log(`createBackup start backupPath=${resolvedPath}`)
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await fs.promises.mkdir(resolvedPath)
|
|
76
|
+
await log('mkdir done')
|
|
77
|
+
|
|
78
|
+
const dbStream = fs.createWriteStream(path.resolve(resolvedPath, 'db.json'))
|
|
79
|
+
await writeDbBackup(dbStream)
|
|
80
|
+
await log('db dump finished (stream closed)')
|
|
81
|
+
|
|
82
|
+
const version = await (
|
|
83
|
+
fs.promises.readFile('./package.json', { encoding: 'utf8' })
|
|
84
|
+
.catch(e => 'unknown')
|
|
85
|
+
.then(data => JSON.parse(data).version)
|
|
86
|
+
)
|
|
87
|
+
const info = {
|
|
88
|
+
version: version,
|
|
89
|
+
hostname: os.hostname(),
|
|
90
|
+
directory: path.resolve('.')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await fs.promises.writeFile(path.resolve(resolvedPath, 'info.json'), JSON.stringify(info, null, ' '))
|
|
94
|
+
await log('info.json written')
|
|
62
95
|
|
|
63
|
-
|
|
96
|
+
await fse.copy("./storage", path.resolve(resolvedPath, "storage"))
|
|
97
|
+
await log('storage copied')
|
|
64
98
|
|
|
65
|
-
|
|
99
|
+
const command = `tar -zcf ../${base}.tar.gz.tmp storage info.json db.json`
|
|
100
|
+
console.log("EXEC TAR COMMAND:", command)
|
|
101
|
+
await log(`tar start command=${command}`)
|
|
102
|
+
await exec(command, { cwd: resolvedPath })
|
|
103
|
+
await log('tar done')
|
|
66
104
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await exec(command, { cwd: backupPath })
|
|
105
|
+
await fs.promises.rename(tarTmpPath, path.join(parentDir, `${base}.tar.gz`))
|
|
106
|
+
await log('archive renamed to .tar.gz')
|
|
70
107
|
|
|
71
|
-
|
|
108
|
+
await fse.remove(resolvedPath)
|
|
109
|
+
await log('temp directory removed')
|
|
72
110
|
|
|
73
|
-
|
|
111
|
+
await log(`createBackup success backupPath=${resolvedPath}`)
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const detail = err && err.stack ? err.stack : String(err)
|
|
114
|
+
await log(`createBackup error: ${detail}`)
|
|
115
|
+
try {
|
|
116
|
+
if(await fse.pathExists(resolvedPath)) {
|
|
117
|
+
await fse.remove(resolvedPath)
|
|
118
|
+
await log(`cleanup removed temp directory ${resolvedPath}`)
|
|
119
|
+
}
|
|
120
|
+
} catch (cleanupErr) {
|
|
121
|
+
await log(`cleanup temp directory failed: ${cleanupErr}`)
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
if(await fse.pathExists(tarTmpPath)) {
|
|
125
|
+
await fse.remove(tarTmpPath)
|
|
126
|
+
await log(`cleanup removed ${tarTmpPath}`)
|
|
127
|
+
}
|
|
128
|
+
} catch (cleanupErr) {
|
|
129
|
+
await log(`cleanup tar tmp failed: ${cleanupErr}`)
|
|
130
|
+
}
|
|
131
|
+
throw err
|
|
132
|
+
}
|
|
74
133
|
}
|
|
75
134
|
|
|
76
|
-
async function removeOldBackups(backupsDir = '../../backups/', maxAge = 10*24*3600*1000, minBackups = 10) {
|
|
77
|
-
const
|
|
135
|
+
async function removeOldBackups(backupsDir = '../../backups/', maxAge = 10*24*3600*1000, minBackups = 10, log) {
|
|
136
|
+
const resolvedDir = path.resolve(backupsDir)
|
|
137
|
+
const dirents = await fs.promises.readdir(resolvedDir, { withFileTypes: true })
|
|
78
138
|
const now = Date.now()
|
|
79
|
-
|
|
80
|
-
const backups =
|
|
81
|
-
|
|
82
|
-
if(!
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// sort by date, from oldest to newest
|
|
139
|
+
|
|
140
|
+
const backups = []
|
|
141
|
+
for(const dirent of dirents) {
|
|
142
|
+
if(!dirent.isFile()) continue
|
|
143
|
+
const file = dirent.name
|
|
144
|
+
if(!file.endsWith('.tar.gz')) continue
|
|
145
|
+
const date = parseBackupTimestamp(file.replace(/\.tar\.gz$/, ''))
|
|
146
|
+
if(!date || Number.isNaN(date.getTime())) continue
|
|
147
|
+
backups.push({ file, date })
|
|
148
|
+
}
|
|
149
|
+
|
|
91
150
|
backups.sort((a, b) => a.date - b.date)
|
|
92
151
|
const olderBackups = backups
|
|
93
|
-
.slice(0, backups.length - minBackups)
|
|
94
|
-
.filter(backup => now - backup.date > maxAge)
|
|
152
|
+
.slice(0, backups.length - minBackups)
|
|
153
|
+
.filter(backup => now - backup.date > maxAge)
|
|
95
154
|
for(const backup of olderBackups) {
|
|
96
|
-
const backupPath = path.
|
|
155
|
+
const backupPath = path.join(resolvedDir, backup.file)
|
|
97
156
|
console.log("REMOVING OLD BACKUP:", backupPath)
|
|
157
|
+
if(log) await log(`removeOldBackups removing archive ${backupPath}`)
|
|
98
158
|
await fse.remove(backupPath)
|
|
99
159
|
}
|
|
160
|
+
|
|
161
|
+
for(const dirent of dirents) {
|
|
162
|
+
if(!dirent.isDirectory()) continue
|
|
163
|
+
const name = dirent.name
|
|
164
|
+
const date = parseBackupTimestamp(name)
|
|
165
|
+
if(!date || Number.isNaN(date.getTime())) continue
|
|
166
|
+
if(now - date <= maxAge) continue
|
|
167
|
+
const dirPath = path.join(resolvedDir, name)
|
|
168
|
+
console.log("REMOVING OLD BACKUP DIRECTORY:", dirPath)
|
|
169
|
+
if(log) await log(`removeOldBackups removing stale directory ${dirPath}`)
|
|
170
|
+
await fse.remove(dirPath)
|
|
171
|
+
}
|
|
100
172
|
}
|
|
101
173
|
|
|
102
|
-
export { createBackup, currentBackupPath, removeOldBackups }
|
|
174
|
+
export { createBackup, createBackupLogger, currentBackupPath, removeOldBackups }
|
package/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import express from 'express'
|
|
|
8
8
|
import basicAuth from 'express-basic-auth'
|
|
9
9
|
const expressApp = express()
|
|
10
10
|
import fs from 'fs'
|
|
11
|
-
import { createBackup, currentBackupPath, removeOldBackups } from './backup.js'
|
|
11
|
+
import { createBackup, createBackupLogger, currentBackupPath, removeOldBackups } from './backup.js'
|
|
12
12
|
import { restoreBackup } from './restore.js'
|
|
13
13
|
import {
|
|
14
14
|
getLastEventId, removeOldEvents, removeOldOpLogs,
|
|
@@ -30,6 +30,8 @@ const backupsDir = config.dir || './backups'
|
|
|
30
30
|
|
|
31
31
|
fs.mkdirSync(backupsDir, { recursive: true })
|
|
32
32
|
|
|
33
|
+
const backupLog = createBackupLogger(backupsDir)
|
|
34
|
+
|
|
33
35
|
let queue
|
|
34
36
|
(async () => {
|
|
35
37
|
const PQueue = (await import('p-queue')).default
|
|
@@ -62,8 +64,8 @@ function doBackup() {
|
|
|
62
64
|
let lastTriggerTimestamp = await getLastTriggerTimestamp()
|
|
63
65
|
let lastCommandTimestamp = await getLastCommandTimestamp()
|
|
64
66
|
|
|
65
|
-
await removeOldBackups(backupsDir, 10 * TWENTY_FOUR_HOURS, 10)
|
|
66
|
-
await createBackup(path)
|
|
67
|
+
await removeOldBackups(backupsDir, 10 * TWENTY_FOUR_HOURS, 10, backupLog)
|
|
68
|
+
await createBackup(path, backupLog)
|
|
67
69
|
|
|
68
70
|
console.log("====== CLEAR STATS:")
|
|
69
71
|
console.log("Last event id:", lastEventId)
|
|
@@ -193,7 +195,7 @@ definition.afterStart(() => {
|
|
|
193
195
|
console.log('Service init backup:')
|
|
194
196
|
await queue.add(() => doBackup())
|
|
195
197
|
console.log('Service init backup completed')
|
|
196
|
-
}, TEN_MINUTES)
|
|
198
|
+
}, 1*60*1000) //TEN_MINUTES)
|
|
197
199
|
|
|
198
200
|
setInterval(async () => {
|
|
199
201
|
console.log('Daily backup:')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/backup-service",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.209",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
"author": "Michał Łaszczewski <michal@emikse.com>",
|
|
10
10
|
"license": "BSD-3-Clause",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@live-change/db-client": "^0.9.
|
|
13
|
-
"@live-change/framework": "^0.9.
|
|
12
|
+
"@live-change/db-client": "^0.9.209",
|
|
13
|
+
"@live-change/framework": "^0.9.209",
|
|
14
14
|
"express": "^4.18.2",
|
|
15
15
|
"express-basic-auth": "^1.2.1",
|
|
16
16
|
"fs-extra": "^11.1.1",
|
|
17
17
|
"p-queue": "8.1.0",
|
|
18
18
|
"progress-stream": "^2.0.0"
|
|
19
19
|
},
|
|
20
|
-
"gitHead": "
|
|
20
|
+
"gitHead": "e1f934934d5d8a119f7ed27aa97bc6b9e9791719",
|
|
21
21
|
"type": "module"
|
|
22
22
|
}
|