@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.
Files changed (3) hide show
  1. package/backup.js +118 -46
  2. package/index.js +6 -4
  3. 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
- await dump({
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
- stream.end()
50
+ )
51
+ stream.end()
52
+ await finished(stream)
53
+ } catch (err) {
54
+ stream.destroy()
55
+ throw err
56
+ }
44
57
  }
45
58
 
46
- async function createBackup(backupPath = currentBackupPath()) {
47
- await fs.promises.mkdir(backupPath)
48
-
49
- const dbStream = fs.createWriteStream(path.resolve(backupPath, 'db.json'))
50
- await writeDbBackup(dbStream)
51
-
52
- const version = await (
53
- fs.promises.readFile('./package.json', { encoding: 'utf8' })
54
- .catch(e => 'unknown')
55
- .then(data => JSON.parse(data).version)
56
- )
57
- const info = {
58
- version: version,
59
- hostname: os.hostname(),
60
- directory: path.resolve('.')
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
- await fs.promises.writeFile(path.resolve(backupPath, 'info.json'), JSON.stringify(info, null, ' '))
96
+ await fse.copy("./storage", path.resolve(resolvedPath, "storage"))
97
+ await log('storage copied')
64
98
 
65
- await fse.copy("./storage", path.resolve(backupPath, "storage"))
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
- const command = `tar -zcf ../${path.basename(backupPath)}.tar.gz.tmp storage info.json db.json`
68
- console.log("EXEC TAR COMMAND:", command)
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
- await fs.promises.rename(backupPath + '.tar.gz.tmp', backupPath + '.tar.gz')
108
+ await fse.remove(resolvedPath)
109
+ await log('temp directory removed')
72
110
 
73
- await fse.remove(backupPath)
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 backupFiles = await fs.promises.readdir(backupsDir)
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
- // read backup times from filenames
80
- const backups = backupFiles.map(file => {
81
- const match = file.match(/(\d{4})_(\d{2})_(\d{2})_(\d{2})_(\d{2})_(\d{2})_(\d+)\.tar\.gz/)
82
- if(!match) {
83
- console.error("BACKUP FILE NAME DOES NOT MATCH:", file)
84
- return null
85
- }
86
- const [_full, year, month, day, hour, minute, second, millis] = match
87
- const date = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}.${millis}Z`)
88
- return { file, date }
89
- }).filter(backup => backup !== null)
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) // leave at least minBackups
94
- .filter(backup => now - backup.date > maxAge) // leave only backups older than 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.resolve(backupsDir, backup.file)
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.207",
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.207",
13
- "@live-change/framework": "^0.9.207",
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": "1937f8f9798d5df011b38341119a25afe0def6d1",
20
+ "gitHead": "e1f934934d5d8a119f7ed27aa97bc6b9e9791719",
21
21
  "type": "module"
22
22
  }