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
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,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
|
+
|