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/src/server.js ADDED
@@ -0,0 +1,54 @@
1
+ import { connectDatabase, disconnectDatabase } from './config/database.js'
2
+ import config from './config/environment.js'
3
+ import { createApp } from './app.js'
4
+ import { startBackupScheduler } from './services/backupScheduler.js'
5
+
6
+ async function startServer() {
7
+ try {
8
+ // Connect to database
9
+ await connectDatabase()
10
+
11
+ // Create Express app
12
+ const app = createApp()
13
+
14
+ // Start HTTP server
15
+ const server = app.listen(config.port, () => {
16
+ console.log(`\n${'═'.repeat(60)}`)
17
+ console.log(`πŸš€ Backend Server Started`)
18
+ console.log(`${'═'.repeat(60)}`)
19
+ console.log(` URL: http://localhost:${config.port}`)
20
+ console.log(` Environment: ${config.nodeEnv}`)
21
+ console.log(` Database: Connected`)
22
+ console.log(`${'═'.repeat(60)}\n`)
23
+
24
+ // Start automatic backup scheduler if enabled
25
+ if (config.backup.enabled) {
26
+ console.log(`πŸ“¦ Automatic backup scheduler enabled`)
27
+ console.log(` Interval: Every ${config.backup.intervalMinutes} minute(s)\n`)
28
+ startBackupScheduler(config.backup.intervalMinutes)
29
+ } else {
30
+ console.log(`ℹ️ Automatic backup disabled`)
31
+ console.log(` Set ENABLE_AUTO_BACKUP=true to enable\n`)
32
+ }
33
+ })
34
+
35
+ // Graceful shutdown
36
+ const shutdown = async (signal) => {
37
+ console.log(`\n⏸️ Received ${signal}, shutting down gracefully...`)
38
+ server.close(async () => {
39
+ console.log('πŸ›‘ Server closed')
40
+ await disconnectDatabase()
41
+ console.log('βœ… Shutdown complete')
42
+ process.exit(0)
43
+ })
44
+ }
45
+
46
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
47
+ process.on('SIGINT', () => shutdown('SIGINT'))
48
+ } catch (error) {
49
+ console.error('❌ Server startup failed:', error.message)
50
+ process.exitCode = 1
51
+ }
52
+ }
53
+
54
+ startServer()
@@ -0,0 +1,142 @@
1
+ import mongoose from 'mongoose'
2
+ import { createAndUploadCompleteBackup } from './backupService.js'
3
+ import dotenv from 'dotenv'
4
+
5
+ dotenv.config()
6
+
7
+ let isBackupRunning = false
8
+ let lastBackupTime = null
9
+ let backupCount = 0
10
+ let totalBackups = 0
11
+
12
+ /**
13
+ * Connect to MongoDB
14
+ */
15
+ async function connectToDatabase() {
16
+ if (mongoose.connection.readyState === 1) {
17
+ return // Already connected
18
+ }
19
+ try {
20
+ await mongoose.connect(process.env.MONGODB_URI, {
21
+ maxPoolSize: 10,
22
+ serverSelectionTimeoutMS: 5000,
23
+ })
24
+ console.log('βœ… MongoDB connected')
25
+ } catch (error) {
26
+ console.error('❌ MongoDB connection failed:', error.message)
27
+ throw error
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Perform backup operation
33
+ */
34
+ async function performBackup() {
35
+ if (isBackupRunning) {
36
+ console.log('⚠️ Backup already in progress, skipping...')
37
+ return
38
+ }
39
+
40
+ isBackupRunning = true
41
+ const startTime = Date.now()
42
+ const timestamp = new Date().toLocaleString()
43
+
44
+ try {
45
+ console.log(`\n${'═'.repeat(60)}`)
46
+ console.log(`πŸ“¦ BACKUP #${++totalBackups} - ${timestamp}`)
47
+ console.log(`${'═'.repeat(60)}`)
48
+
49
+ await connectToDatabase()
50
+
51
+ const result = await createAndUploadCompleteBackup({
52
+ uploadToDrive: true,
53
+ excludedDatabases: ['admin', 'config', 'local'],
54
+ })
55
+
56
+ lastBackupTime = new Date()
57
+ backupCount++
58
+
59
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2)
60
+
61
+ console.log('\nπŸ“Š BACKUP SUMMARY:')
62
+ console.log(` └─ Databases: ${result.databases.join(', ')}`)
63
+ console.log(` └─ Collections: ${result.collections.length}`)
64
+ console.log(` └─ Total Records: ${result.totalRecords.toLocaleString()}`)
65
+ console.log(` └─ File Size: ${(result.driveFile?.size || 0).toLocaleString()} bytes`)
66
+ console.log(` └─ Duration: ${duration}s`)
67
+ console.log(` └─ Status: βœ… SUCCESS`)
68
+
69
+ if (result.uploaded && result.driveFile?.webViewLink) {
70
+ console.log(`\n☁️ Google Drive Link: ${result.driveFile.webViewLink}`)
71
+ }
72
+ } catch (error) {
73
+ console.error('\n❌ BACKUP FAILED:')
74
+ console.error(` Error: ${error.message}`)
75
+ } finally {
76
+ isBackupRunning = false
77
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2)
78
+ console.log(`\n⏱️ Execution time: ${duration}s`)
79
+ console.log(`${'═'.repeat(60)}\n`)
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Start scheduler with specified interval
85
+ * @param {number} intervalMinutes - Interval in minutes
86
+ */
87
+ export function startBackupScheduler(intervalMinutes = 60) {
88
+ console.log(`\n${'β•”' + '═'.repeat(58) + 'β•—'}`)
89
+ console.log(`β•‘ πŸ• BACKUP SCHEDULER STARTED β•‘`)
90
+ console.log(`β•‘ Interval: Every ${intervalMinutes} minute(s)${' '.repeat(32 - String(intervalMinutes).length)}β•‘`)
91
+ console.log(`β•‘ Next backup: ${new Date(Date.now() + intervalMinutes * 60000).toLocaleString()}${' '.repeat(23 - new Date(Date.now() + intervalMinutes * 60000).toLocaleString().length)}β•‘`)
92
+ console.log(`β•š${'═'.repeat(58)}╝\n`)
93
+
94
+ // First backup immediately
95
+ performBackup()
96
+
97
+ // Then schedule periodic backups
98
+ const intervalMs = intervalMinutes * 60 * 1000
99
+ const scheduledInterval = setInterval(() => {
100
+ performBackup()
101
+ }, intervalMs)
102
+
103
+ // Handle graceful shutdown
104
+ process.on('SIGINT', async () => {
105
+ console.log('\n\n⏸️ Stopping backup scheduler...')
106
+ clearInterval(scheduledInterval)
107
+
108
+ // Print statistics
109
+ console.log(`\nπŸ“ˆ SCHEDULER STATISTICS:`)
110
+ console.log(` └─ Total backups: ${totalBackups}`)
111
+ console.log(` └─ Successful: ${backupCount}`)
112
+ console.log(` └─ Failed: ${totalBackups - backupCount}`)
113
+ if (lastBackupTime) {
114
+ console.log(` └─ Last backup: ${lastBackupTime.toLocaleString()}`)
115
+ }
116
+
117
+ await mongoose.disconnect()
118
+ console.log('βœ… Scheduler stopped\n')
119
+ process.exit(0)
120
+ })
121
+
122
+ return scheduledInterval
123
+ }
124
+
125
+ /**
126
+ * Get scheduler status
127
+ */
128
+ export function getBackupStatus() {
129
+ return {
130
+ isRunning: isBackupRunning,
131
+ lastBackupTime,
132
+ totalBackups,
133
+ successfulBackups: backupCount,
134
+ failedBackups: totalBackups - backupCount,
135
+ }
136
+ }
137
+
138
+ // If run directly
139
+ if (import.meta.url === `file://${process.argv[1]}`) {
140
+ const interval = process.env.BACKUP_INTERVAL_MINUTES || 60
141
+ startBackupScheduler(parseInt(interval))
142
+ }
@@ -0,0 +1,85 @@
1
+ import { createExcelWorkbook } from '../utils/excelFormatter.js'
2
+ import { extractAllDatabasesCollections } from '../utils/dataExtract.js'
3
+ import { uploadExcelToGoogleDrive } from './googleDrive.js'
4
+
5
+ /**
6
+ * Extract data from ALL databases and collections ONLY
7
+ * This is the main function - extracts everything from MongoDB
8
+ */
9
+ export async function createAndUploadCompleteBackup(options = {}) {
10
+ const {
11
+ uploadToDrive = true,
12
+ workbookName,
13
+ folderId,
14
+ excludedDatabases = ['admin', 'config', 'local'],
15
+ } = options
16
+
17
+ console.log('πŸ” Extracting ALL databases and collections...')
18
+ const extractedData = await extractAllDatabasesCollections(
19
+ process.env.MONGODB_URI,
20
+ excludedDatabases,
21
+ )
22
+
23
+ if (extractedData.length === 0) {
24
+ throw new Error('No data found in any database')
25
+ }
26
+
27
+ console.log(`βœ… Found ${extractedData.length} collections`)
28
+
29
+ // Format data for Excel creation - each collection gets its own sheet
30
+ const formattedData = extractedData.map((item) => ({
31
+ modelName: `${item.database}/${item.collectionName}`,
32
+ collectionName: item.collectionName,
33
+ database: item.database,
34
+ data: item.data || [],
35
+ }))
36
+
37
+ const fileName =
38
+ workbookName || `complete-backup-${new Date().toISOString().slice(0, 10)}`
39
+ const workbook = await createExcelWorkbook(formattedData, {
40
+ workbookName: fileName,
41
+ })
42
+
43
+ const result = {
44
+ uploaded: false,
45
+ uploadStatus: uploadToDrive === false ? 'skipped' : 'pending',
46
+ fileName: workbook.fileName,
47
+ databases: [...new Set(extractedData.map((d) => d.database))],
48
+ collections: extractedData.map(({ database, collectionName, documentCount, data }) => ({
49
+ database,
50
+ collectionName,
51
+ records: documentCount || data.length,
52
+ })),
53
+ totalRecords: extractedData.reduce((sum, item) => sum + (item.documentCount || item.data.length), 0),
54
+ sheetCount: extractedData.length,
55
+ }
56
+
57
+ if (uploadToDrive === false) {
58
+ console.log('βœ… Backup created (upload skipped)')
59
+ return result
60
+ }
61
+
62
+ console.log(`πŸ“€ Uploading to Google Drive...`)
63
+ const driveFile = await uploadExcelToGoogleDrive({
64
+ ...workbook,
65
+ folderId: folderId || process.env.GOOGLE_DRIVE_FOLDER_ID,
66
+ })
67
+
68
+ console.log(`βœ… Backup uploaded: ${driveFile.name}`)
69
+
70
+ return {
71
+ ...result,
72
+ uploaded: true,
73
+ uploadStatus: 'uploaded',
74
+ driveFile,
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Old function - kept for backward compatibility (but not recommended)
80
+ * Use createAndUploadCompleteBackup instead
81
+ */
82
+ export async function createAndUploadDatabaseExport(options = {}) {
83
+ console.warn('⚠️ createAndUploadDatabaseExport is deprecated. Use createAndUploadCompleteBackup instead.')
84
+ return createAndUploadCompleteBackup(options)
85
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Google Authentication Service
3
+ * Uses Service Account - No OAuth needed
4
+ */
5
+
6
+ export function getAuthStatus() {
7
+ return {
8
+ authenticated: true,
9
+ method: 'Service Account',
10
+ message: 'Direct upload to Google Drive enabled'
11
+ }
12
+ }
@@ -0,0 +1,245 @@
1
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { Readable } from 'node:stream'
3
+ import { google } from 'googleapis'
4
+ import { join } from 'node:path'
5
+
6
+ const EXPORTS_DIR = './exports'
7
+
8
+ // Create exports directory if it doesn't exist
9
+ if (!existsSync(EXPORTS_DIR)) {
10
+ mkdirSync(EXPORTS_DIR, { recursive: true })
11
+ }
12
+
13
+ /**
14
+ * Get authenticated Google Drive client
15
+ * 1. Checks for OAuth 2.0 tokens (google-tokens.json) -> Works with Personal Google Drive (15 GB free quota)
16
+ * 2. Fallback to Service Account (service-account.json) -> For Google Workspace / Shared Drives
17
+ */
18
+ async function getDriveClient() {
19
+ const tokensPath = process.env.GOOGLE_TOKENS_PATH || join(process.cwd(), 'google-tokens.json')
20
+
21
+ // 1. Try OAuth 2.0 first if tokens exist
22
+ if (
23
+ process.env.GOOGLE_OAUTH_CLIENT_ID &&
24
+ process.env.GOOGLE_OAUTH_CLIENT_SECRET &&
25
+ existsSync(tokensPath)
26
+ ) {
27
+ try {
28
+ console.log('πŸ” Authenticating with Google OAuth 2.0 (User Account)...')
29
+ const tokens = JSON.parse(readFileSync(tokensPath, 'utf-8'))
30
+
31
+ const oauth2Client = new google.auth.OAuth2(
32
+ process.env.GOOGLE_OAUTH_CLIENT_ID,
33
+ process.env.GOOGLE_OAUTH_CLIENT_SECRET,
34
+ process.env.GOOGLE_OAUTH_REDIRECT_URI || 'http://localhost:5000/auth/google/callback'
35
+ )
36
+
37
+ oauth2Client.setCredentials(tokens)
38
+
39
+ // Auto-save refreshed tokens when Google issues new tokens
40
+ oauth2Client.on('tokens', (newTokens) => {
41
+ try {
42
+ const currentTokens = existsSync(tokensPath)
43
+ ? JSON.parse(readFileSync(tokensPath, 'utf-8'))
44
+ : {}
45
+ const mergedTokens = { ...currentTokens, ...newTokens }
46
+ writeFileSync(tokensPath, JSON.stringify(mergedTokens, null, 2))
47
+ console.log('πŸ”„ Google OAuth tokens refreshed and saved.')
48
+ } catch (saveErr) {
49
+ console.warn('⚠️ Could not auto-save refreshed tokens:', saveErr.message)
50
+ }
51
+ })
52
+
53
+ const drive = google.drive({ version: 'v3', auth: oauth2Client })
54
+ return { drive, authType: 'oauth' }
55
+ } catch (oauthError) {
56
+ console.warn('⚠️ OAuth authentication failed, trying Service Account fallback:', oauthError.message)
57
+ }
58
+ }
59
+
60
+ // 2. Service Account Auth (fallback)
61
+ let serviceAccount
62
+
63
+ const serviceAccountPath = join(process.cwd(), 'config', 'service-account.json')
64
+
65
+ if (existsSync(serviceAccountPath)) {
66
+ console.log('πŸ“‚ Loading Service Account from config file...')
67
+ const fileContent = readFileSync(serviceAccountPath, 'utf-8')
68
+ serviceAccount = JSON.parse(fileContent)
69
+ } else if (process.env.GOOGLE_SERVICE_ACCOUNT_JSON) {
70
+ console.log('πŸ“‚ Loading Service Account from .env...')
71
+ try {
72
+ serviceAccount = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON)
73
+ } catch (error) {
74
+ throw new Error(
75
+ 'Service Account JSON parse error in .env'
76
+ )
77
+ }
78
+ } else {
79
+ throw new Error(
80
+ 'Authentication failed! No valid Google OAuth tokens (google-tokens.json) or Service Account found.'
81
+ )
82
+ }
83
+
84
+ const auth = new google.auth.GoogleAuth({
85
+ credentials: serviceAccount,
86
+ scopes: ['https://www.googleapis.com/auth/drive.file'],
87
+ })
88
+
89
+ const client = await auth.getClient()
90
+ const drive = google.drive({ version: 'v3', auth: client })
91
+ return { drive, authType: 'service_account' }
92
+ }
93
+
94
+ /**
95
+ * Upload Excel file to Google Drive
96
+ */
97
+ export async function uploadExcelToGoogleDrive({ buffer, fileName, contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', folderId } = {}) {
98
+ if (!Buffer.isBuffer(buffer)) {
99
+ throw new TypeError('buffer must be a Buffer')
100
+ }
101
+ if (!fileName) {
102
+ throw new Error('fileName is required')
103
+ }
104
+
105
+ // Ensure fileName has .xlsx extension
106
+ if (!fileName.endsWith('.xlsx')) {
107
+ fileName = `${fileName}.xlsx`
108
+ }
109
+
110
+ // Step 1: Save locally first (always backup locally)
111
+ const localPath = join(EXPORTS_DIR, fileName)
112
+ const writeStream = createWriteStream(localPath)
113
+
114
+ await new Promise((resolve, reject) => {
115
+ writeStream.on('finish', resolve)
116
+ writeStream.on('error', reject)
117
+ writeStream.write(buffer)
118
+ writeStream.end()
119
+ })
120
+
121
+ console.log(`πŸ“ File saved locally: ${localPath}`)
122
+
123
+ // Step 2: Upload to Google Drive
124
+ try {
125
+ const { drive, authType } = await getDriveClient()
126
+ const targetFolderId = folderId || process.env.GOOGLE_DRIVE_FOLDER_ID
127
+
128
+ console.log(`πŸ“€ Uploading to Google Drive (${authType}): ${fileName}`)
129
+
130
+ // Create readable stream from buffer
131
+ const bufferStream = Readable.from(buffer)
132
+
133
+ const requestBody = {
134
+ name: fileName,
135
+ ...(targetFolderId ? { parents: [targetFolderId] } : {}),
136
+ }
137
+
138
+ // Upload file
139
+ const response = await drive.files.create({
140
+ requestBody,
141
+ media: {
142
+ mimeType: contentType,
143
+ body: bufferStream,
144
+ },
145
+ fields: 'id, name, webViewLink, size',
146
+ supportsAllDrives: true,
147
+ })
148
+
149
+ const file = response.data
150
+
151
+ console.log(`βœ… Uploaded to Google Drive!`)
152
+ console.log(` File ID: ${file.id}`)
153
+ console.log(` View: ${file.webViewLink}`)
154
+
155
+ return {
156
+ id: file.id,
157
+ name: file.name,
158
+ webViewLink: file.webViewLink,
159
+ localPath: localPath,
160
+ downloadPath: `/download/${fileName}`,
161
+ status: 'uploaded',
162
+ message: 'File uploaded to Google Drive successfully!',
163
+ size: file.size || buffer.length,
164
+ }
165
+ } catch (error) {
166
+ // Check if it's a quota error
167
+ if (error.message && error.message.includes('storage quota')) {
168
+ console.error('❌ Service Account has no storage quota!')
169
+ console.error('')
170
+ console.error('ℹ️ Google Service Accounts have 0 MB storage quota on personal Google Drive (@gmail.com).')
171
+ console.error(' Please use OAuth 2.0 authentication (google-tokens.json) or a Google Workspace Shared Drive.')
172
+ console.error('')
173
+ } else {
174
+ console.error('❌ Google Drive upload failed:', error.message)
175
+ }
176
+
177
+ // Return local file info even if Drive upload fails
178
+ return {
179
+ id: 'local-only',
180
+ name: fileName,
181
+ webViewLink: null,
182
+ localPath: localPath,
183
+ downloadPath: `/download/${fileName}`,
184
+ status: 'local_only',
185
+ message: `Saved locally. Drive upload failed: ${error.message}`,
186
+ size: buffer.length,
187
+ error: error.message,
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * List files in Google Drive folder
194
+ */
195
+ export async function listDriveFiles(folderId, maxResults = 100) {
196
+ try {
197
+ const { drive } = await getDriveClient()
198
+ const targetFolderId = folderId || process.env.GOOGLE_DRIVE_FOLDER_ID
199
+
200
+ const response = await drive.files.list({
201
+ q: targetFolderId ? `'${targetFolderId}' in parents` : undefined,
202
+ pageSize: maxResults,
203
+ fields: 'files(id, name, createdTime, size, webViewLink)',
204
+ orderBy: 'createdTime desc',
205
+ supportsAllDrives: true,
206
+ includeItemsFromAllDrives: true,
207
+ })
208
+
209
+ return response.data.files
210
+ } catch (error) {
211
+ console.error('Failed to list files:', error.message)
212
+ return []
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Delete old backup files (older than X days)
218
+ */
219
+ export async function deleteOldBackups(folderId, daysOld = 30) {
220
+ try {
221
+ const { drive } = await getDriveClient()
222
+ const files = await listDriveFiles(folderId)
223
+
224
+ const cutoffDate = new Date()
225
+ cutoffDate.setDate(cutoffDate.getDate() - daysOld)
226
+
227
+ let deletedCount = 0
228
+
229
+ for (const file of files) {
230
+ const createdDate = new Date(file.createdTime)
231
+ if (createdDate < cutoffDate) {
232
+ await drive.files.delete({ fileId: file.id, supportsAllDrives: true })
233
+ console.log(`πŸ—‘οΈ Deleted old backup: ${file.name}`)
234
+ deletedCount++
235
+ }
236
+ }
237
+
238
+ console.log(`βœ… Deleted ${deletedCount} old backup(s)`)
239
+ return deletedCount
240
+ } catch (error) {
241
+ console.error('Failed to delete old backups:', error.message)
242
+ return 0
243
+ }
244
+ }
245
+