db-data-gd-uploader 1.0.0
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/LICENSE +21 -0
- package/README.md +276 -0
- package/config/service-account.json +13 -0
- package/package.json +66 -0
- package/src/app.js +69 -0
- package/src/config/database.js +38 -0
- package/src/config/environment.js +55 -0
- package/src/index.js +14 -0
- package/src/routes/authRoutes.js +15 -0
- package/src/routes/backupRoutes.js +38 -0
- package/src/scripts/backup.js +44 -0
- package/src/scripts/scheduler.js +15 -0
- package/src/server.js +54 -0
- package/src/services/backupScheduler.js +142 -0
- package/src/services/backupService.js +85 -0
- package/src/services/googleAuth.js +12 -0
- package/src/services/googleDrive.js +245 -0
- package/src/utils/dataExtract.js +175 -0
- package/src/utils/excelFormatter.js +153 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import mongoose from 'mongoose'
|
|
2
|
+
import { readdir } from 'node:fs/promises'
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_EXCLUDED_MODELS = new Set(['system.indexes'])
|
|
6
|
+
const EXCLUDED_SYSTEM_COLLECTIONS = new Set(['system.indexes', 'system.profile', 'system.views'])
|
|
7
|
+
|
|
8
|
+
function serializeValue(value) {
|
|
9
|
+
if (value === null || value === undefined) return value
|
|
10
|
+
if (value instanceof mongoose.Types.ObjectId) return value.toString()
|
|
11
|
+
if (value instanceof Date) return value.toISOString()
|
|
12
|
+
if (Buffer.isBuffer(value)) return value.toString('base64')
|
|
13
|
+
if (Array.isArray(value)) return value.map(serializeValue)
|
|
14
|
+
|
|
15
|
+
if (typeof value === 'object') {
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
Object.entries(value).map(([key, nestedValue]) => [key, serializeValue(nestedValue)]),
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function importModelFiles(modelsDirectory) {
|
|
25
|
+
try {
|
|
26
|
+
const modelFiles = await readdir(modelsDirectory)
|
|
27
|
+
|
|
28
|
+
await Promise.all(
|
|
29
|
+
modelFiles
|
|
30
|
+
.filter((file) => /\.(js|mjs)$/.test(file))
|
|
31
|
+
.map((file) => import(pathToFileURL(`${modelsDirectory}/${file}`).href)),
|
|
32
|
+
)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error.code !== 'ENOENT') throw error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function extractAllModels({
|
|
39
|
+
modelsDirectory = fileURLToPath(new URL('../models/', import.meta.url)),
|
|
40
|
+
excludedModels = DEFAULT_EXCLUDED_MODELS,
|
|
41
|
+
modelNames = [],
|
|
42
|
+
queryOptions = {},
|
|
43
|
+
} = {}) {
|
|
44
|
+
await importModelFiles(modelsDirectory)
|
|
45
|
+
|
|
46
|
+
let selectedModels = mongoose.modelNames()
|
|
47
|
+
|
|
48
|
+
if (Array.isArray(modelNames) && modelNames.length > 0) {
|
|
49
|
+
selectedModels = selectedModels.filter((name) => modelNames.includes(name))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
selectedModels = selectedModels.filter((name) => !excludedModels.has(name))
|
|
53
|
+
|
|
54
|
+
const extractedData = []
|
|
55
|
+
|
|
56
|
+
for (const modelName of selectedModels) {
|
|
57
|
+
const model = mongoose.model(modelName)
|
|
58
|
+
const documents = await model.find({}, null, queryOptions).lean().exec()
|
|
59
|
+
|
|
60
|
+
extractedData.push({
|
|
61
|
+
modelName,
|
|
62
|
+
collectionName: model.collection.name,
|
|
63
|
+
data: documents.map(serializeValue),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return extractedData
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract data from all collections across all databases in MongoDB
|
|
72
|
+
* @param {string} mongoUri - MongoDB connection URI
|
|
73
|
+
* @param {Array<string>} excludedDatabases - Databases to exclude from extraction
|
|
74
|
+
* @returns {Promise<Array>} Array of objects containing database name, collection name, and documents
|
|
75
|
+
*/
|
|
76
|
+
export async function extractAllDatabasesCollections(
|
|
77
|
+
mongoUri = process.env.MONGODB_URI,
|
|
78
|
+
excludedDatabases = ['admin', 'config', 'local'],
|
|
79
|
+
) {
|
|
80
|
+
const adminDb = mongoose.connection.getClient().db('admin')
|
|
81
|
+
const databases = await adminDb.admin().listDatabases()
|
|
82
|
+
|
|
83
|
+
const extractedData = []
|
|
84
|
+
|
|
85
|
+
for (const dbInfo of databases.databases) {
|
|
86
|
+
const dbName = dbInfo.name
|
|
87
|
+
|
|
88
|
+
// Skip excluded databases
|
|
89
|
+
if (excludedDatabases.includes(dbName)) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const db = mongoose.connection.getClient().db(dbName)
|
|
95
|
+
const collections = await db.listCollections().toArray()
|
|
96
|
+
|
|
97
|
+
for (const collectionInfo of collections) {
|
|
98
|
+
const collectionName = collectionInfo.name
|
|
99
|
+
|
|
100
|
+
// Skip system collections
|
|
101
|
+
if (EXCLUDED_SYSTEM_COLLECTIONS.has(collectionName)) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const collection = db.collection(collectionName)
|
|
106
|
+
const documents = await collection.find({}).toArray()
|
|
107
|
+
|
|
108
|
+
extractedData.push({
|
|
109
|
+
database: dbName,
|
|
110
|
+
collectionName,
|
|
111
|
+
documentCount: documents.length,
|
|
112
|
+
data: documents.map(serializeValue),
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`Error extracting data from database ${dbName}:`, error.message)
|
|
117
|
+
extractedData.push({
|
|
118
|
+
database: dbName,
|
|
119
|
+
error: error.message,
|
|
120
|
+
collectionName: null,
|
|
121
|
+
data: [],
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return extractedData
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract data from specific databases only
|
|
131
|
+
* @param {string} mongoUri - MongoDB connection URI
|
|
132
|
+
* @param {Array<string>} databaseNames - Specific databases to extract from
|
|
133
|
+
* @returns {Promise<Array>} Array of objects containing database name, collection name, and documents
|
|
134
|
+
*/
|
|
135
|
+
export async function extractSpecificDatabases(mongoUri = process.env.MONGODB_URI, databaseNames = []) {
|
|
136
|
+
const extractedData = []
|
|
137
|
+
|
|
138
|
+
for (const dbName of databaseNames) {
|
|
139
|
+
try {
|
|
140
|
+
const db = mongoose.connection.getClient().db(dbName)
|
|
141
|
+
const collections = await db.listCollections().toArray()
|
|
142
|
+
|
|
143
|
+
for (const collectionInfo of collections) {
|
|
144
|
+
const collectionName = collectionInfo.name
|
|
145
|
+
|
|
146
|
+
// Skip system collections
|
|
147
|
+
if (EXCLUDED_SYSTEM_COLLECTIONS.has(collectionName)) {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const collection = db.collection(collectionName)
|
|
152
|
+
const documents = await collection.find({}).toArray()
|
|
153
|
+
|
|
154
|
+
extractedData.push({
|
|
155
|
+
database: dbName,
|
|
156
|
+
collectionName,
|
|
157
|
+
documentCount: documents.length,
|
|
158
|
+
data: documents.map(serializeValue),
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`Error extracting data from database ${dbName}:`, error.message)
|
|
163
|
+
extractedData.push({
|
|
164
|
+
database: dbName,
|
|
165
|
+
error: error.message,
|
|
166
|
+
collectionName: null,
|
|
167
|
+
data: [],
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return extractedData
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { serializeValue }
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import ExcelJS from 'exceljs'
|
|
2
|
+
|
|
3
|
+
function flattenObject(value, prefix = '') {
|
|
4
|
+
if (value === null || value === undefined) return { [prefix || 'value']: '' }
|
|
5
|
+
if (Array.isArray(value)) return { [prefix || 'value']: JSON.stringify(value) }
|
|
6
|
+
if (typeof value !== 'object' || value instanceof Date) return { [prefix || 'value']: value }
|
|
7
|
+
|
|
8
|
+
return Object.entries(value).reduce(
|
|
9
|
+
(result, [key, nestedValue]) => ({
|
|
10
|
+
...result,
|
|
11
|
+
...flattenObject(nestedValue, prefix ? `${prefix}.${key}` : key),
|
|
12
|
+
}),
|
|
13
|
+
{},
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeCellValue(value) {
|
|
18
|
+
if (value === null || value === undefined) return ''
|
|
19
|
+
if (typeof value === 'object' && !(value instanceof Date)) return JSON.stringify(value)
|
|
20
|
+
return value
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function createExcelWorkbook(extractedData, { workbookName = 'database-export' } = {}) {
|
|
24
|
+
if (!Array.isArray(extractedData)) throw new TypeError('extractedData must be an array')
|
|
25
|
+
|
|
26
|
+
const workbook = new ExcelJS.Workbook()
|
|
27
|
+
workbook.creator = 'GD Uploader - Complete Backup'
|
|
28
|
+
workbook.created = new Date()
|
|
29
|
+
|
|
30
|
+
// Group collections by database
|
|
31
|
+
const groupedByDatabase = {}
|
|
32
|
+
for (const item of extractedData) {
|
|
33
|
+
if (!groupedByDatabase[item.database]) {
|
|
34
|
+
groupedByDatabase[item.database] = []
|
|
35
|
+
}
|
|
36
|
+
groupedByDatabase[item.database].push(item)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create one sheet per database
|
|
40
|
+
for (const [database, collections] of Object.entries(groupedByDatabase)) {
|
|
41
|
+
const worksheet = workbook.addWorksheet(database.slice(0, 31))
|
|
42
|
+
|
|
43
|
+
if (collections.length === 0) continue
|
|
44
|
+
|
|
45
|
+
// Row 1: Database name (merged header)
|
|
46
|
+
const totalColumns = collections.length * 2
|
|
47
|
+
worksheet.mergeCells(1, 1, 1, totalColumns)
|
|
48
|
+
const dbHeaderCell = worksheet.getCell(1, 1)
|
|
49
|
+
dbHeaderCell.value = database
|
|
50
|
+
dbHeaderCell.font = { bold: true, size: 14, color: { argb: 'FFFFFFFF' } }
|
|
51
|
+
dbHeaderCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1F4E78' } }
|
|
52
|
+
dbHeaderCell.alignment = { horizontal: 'center', vertical: 'center' }
|
|
53
|
+
worksheet.getRow(1).height = 25
|
|
54
|
+
|
|
55
|
+
// Row 2: Collection names
|
|
56
|
+
let colIdx = 1
|
|
57
|
+
for (const collection of collections) {
|
|
58
|
+
const rows = Array.isArray(collection.data) ? collection.data : []
|
|
59
|
+
worksheet.mergeCells(2, colIdx, 2, colIdx + 1)
|
|
60
|
+
const collCell = worksheet.getCell(2, colIdx)
|
|
61
|
+
collCell.value = `${collection.collectionName} (${rows.length})`
|
|
62
|
+
collCell.font = { bold: true, size: 11, color: { argb: 'FFFFFFFF' } }
|
|
63
|
+
collCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } }
|
|
64
|
+
collCell.alignment = { horizontal: 'center', vertical: 'center' }
|
|
65
|
+
colIdx += 2
|
|
66
|
+
}
|
|
67
|
+
worksheet.getRow(2).height = 20
|
|
68
|
+
|
|
69
|
+
// Row 3: Key/Value headers
|
|
70
|
+
colIdx = 1
|
|
71
|
+
for (const _collection of collections) {
|
|
72
|
+
worksheet.getCell(3, colIdx).value = 'key'
|
|
73
|
+
worksheet.getCell(3, colIdx + 1).value = 'value'
|
|
74
|
+
for (let i = colIdx; i <= colIdx + 1; i++) {
|
|
75
|
+
const cell = worksheet.getCell(3, i)
|
|
76
|
+
cell.font = { bold: true, color: { argb: 'FFFFFFFF' } }
|
|
77
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF70AD47' } }
|
|
78
|
+
cell.alignment = { horizontal: 'center', vertical: 'center' }
|
|
79
|
+
}
|
|
80
|
+
colIdx += 2
|
|
81
|
+
}
|
|
82
|
+
worksheet.getRow(3).height = 18
|
|
83
|
+
|
|
84
|
+
// Calculate max rows
|
|
85
|
+
const maxRows = Math.max(...collections.map(c => (Array.isArray(c.data) ? c.data.length : 0)), 0)
|
|
86
|
+
|
|
87
|
+
// Data rows - each document gets multiple rows (one per key-value pair)
|
|
88
|
+
let currentExcelRow = 4
|
|
89
|
+
|
|
90
|
+
for (let docIdx = 0; docIdx < maxRows; docIdx++) {
|
|
91
|
+
// Find max fields needed for this document index across all collections
|
|
92
|
+
let maxFieldsThisDoc = 0
|
|
93
|
+
for (const collection of collections) {
|
|
94
|
+
const rows = Array.isArray(collection.data) ? collection.data : []
|
|
95
|
+
if (docIdx < rows.length) {
|
|
96
|
+
const flatDoc = flattenObject(rows[docIdx])
|
|
97
|
+
maxFieldsThisDoc = Math.max(maxFieldsThisDoc, Object.keys(flatDoc).length)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Create rows for each field
|
|
102
|
+
for (let fieldIdx = 0; fieldIdx < maxFieldsThisDoc; fieldIdx++) {
|
|
103
|
+
colIdx = 1
|
|
104
|
+
|
|
105
|
+
for (const collection of collections) {
|
|
106
|
+
const rows = Array.isArray(collection.data) ? collection.data : []
|
|
107
|
+
|
|
108
|
+
if (docIdx < rows.length) {
|
|
109
|
+
const flatDoc = flattenObject(rows[docIdx])
|
|
110
|
+
const entries = Object.entries(flatDoc)
|
|
111
|
+
|
|
112
|
+
if (fieldIdx < entries.length) {
|
|
113
|
+
const [key, value] = entries[fieldIdx]
|
|
114
|
+
worksheet.getCell(currentExcelRow, colIdx).value = key
|
|
115
|
+
worksheet.getCell(currentExcelRow, colIdx + 1).value = normalizeCellValue(value)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
colIdx += 2
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
currentExcelRow++
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Set column widths
|
|
127
|
+
for (let col = 1; col <= totalColumns; col++) {
|
|
128
|
+
worksheet.getColumn(col).width = 22
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add borders to all cells
|
|
132
|
+
for (let row = 1; row < currentExcelRow; row++) {
|
|
133
|
+
for (let col = 1; col <= totalColumns; col++) {
|
|
134
|
+
const cell = worksheet.getCell(row, col)
|
|
135
|
+
cell.border = {
|
|
136
|
+
top: { style: 'thin' },
|
|
137
|
+
left: { style: 'thin' },
|
|
138
|
+
bottom: { style: 'thin' },
|
|
139
|
+
right: { style: 'thin' },
|
|
140
|
+
}
|
|
141
|
+
cell.alignment = { wrapText: true, vertical: 'top' }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (workbook.worksheets.length === 0) workbook.addWorksheet('No data')
|
|
147
|
+
const buffer = await workbook.xlsx.writeBuffer()
|
|
148
|
+
return {
|
|
149
|
+
buffer: Buffer.from(buffer),
|
|
150
|
+
fileName: `${workbookName.replace(/[^a-z0-9-_]/gi, '-').toLowerCase()}.xlsx`,
|
|
151
|
+
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
152
|
+
}
|
|
153
|
+
}
|