@riligar/elysia-backup 1.7.0 → 1.8.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/README.md +0 -24
- package/package.json +1 -1
- package/src/core/scheduler.js +20 -18
- package/src/index.js +28 -268
- package/src/services/s3-client.js +0 -8
- package/src/views/scripts/backupApp.js +26 -0
package/README.md
CHANGED
|
@@ -160,28 +160,6 @@ src/
|
|
|
160
160
|
│ └── onboardingApp.js
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
## 🌍 Environment Variables Example
|
|
164
|
-
|
|
165
|
-
```env
|
|
166
|
-
# Optional - for initial configuration
|
|
167
|
-
R2_BUCKET=your-bucket-name
|
|
168
|
-
R2_ACCESS_KEY_ID=your-access-key
|
|
169
|
-
R2_SECRET_ACCESS_KEY=your-secret-key
|
|
170
|
-
R2_ENDPOINT=https://your-account.r2.cloudflarestorage.com
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
> **Note:** All configuration can be set via the onboarding wizard. Environment variables are optional.
|
|
174
|
-
|
|
175
|
-
## 📸 Screenshots
|
|
176
|
-
|
|
177
|
-
The dashboard provides:
|
|
178
|
-
|
|
179
|
-
- **Status Overview** — Cron status, next run time, bucket info
|
|
180
|
-
- **Quick Actions** — Manual backup trigger with real-time feedback
|
|
181
|
-
- **Files Browser** — View, restore, and delete backup files
|
|
182
|
-
- **Settings** — Update storage and backup configuration
|
|
183
|
-
- **Security** — Enable/disable 2FA
|
|
184
|
-
|
|
185
163
|
## 🤝 Contributing
|
|
186
164
|
|
|
187
165
|
Contributions are welcome! See our [GitHub repository](https://github.com/riligar-solutions/elysia-backup) for more information.
|
|
@@ -190,8 +168,6 @@ Contributions are welcome! See our [GitHub repository](https://github.com/riliga
|
|
|
190
168
|
|
|
191
169
|
MIT © [RiLiGar](https://riligar.click/)
|
|
192
170
|
|
|
193
|
-
---
|
|
194
|
-
|
|
195
171
|
<p align="center">
|
|
196
172
|
Made with ❤️ by <a href="https://riligar.click/">RiLiGar</a>
|
|
197
173
|
</p>
|
package/package.json
CHANGED
package/src/core/scheduler.js
CHANGED
|
@@ -31,27 +31,29 @@ export const createScheduler = onBackup => {
|
|
|
31
31
|
backupJob = new CronJob(
|
|
32
32
|
cronSchedule,
|
|
33
33
|
async () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
// Generate timestamp for the backup
|
|
35
|
+
const now = new Date()
|
|
36
|
+
const timestamp =
|
|
37
|
+
now.getFullYear() +
|
|
38
|
+
'-' +
|
|
39
|
+
String(now.getMonth() + 1).padStart(2, '0') +
|
|
40
|
+
'-' +
|
|
41
|
+
String(now.getDate()).padStart(2, '0') +
|
|
42
|
+
'_' +
|
|
43
|
+
String(now.getHours()).padStart(2, '0') +
|
|
44
|
+
'-' +
|
|
45
|
+
String(now.getMinutes()).padStart(2, '0') +
|
|
46
|
+
'-' +
|
|
47
|
+
String(now.getSeconds()).padStart(2, '0')
|
|
48
|
+
|
|
49
|
+
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
|
|
50
|
+
console.log(`📦 Running scheduled backup at ${timeStr}`)
|
|
50
51
|
|
|
52
|
+
try {
|
|
51
53
|
await onBackup(timestamp)
|
|
52
|
-
console.log(
|
|
54
|
+
console.log(`✅ Scheduled backup completed at ${timeStr}`)
|
|
53
55
|
} catch (e) {
|
|
54
|
-
console.error(
|
|
56
|
+
console.error(`❌ Scheduled backup failed at ${timeStr}:`, e)
|
|
55
57
|
}
|
|
56
58
|
},
|
|
57
59
|
null,
|
package/src/index.js
CHANGED
|
@@ -1,65 +1,24 @@
|
|
|
1
1
|
// https://bun.com/docs/runtime/s3
|
|
2
2
|
// https://elysiajs.com/plugins/html
|
|
3
3
|
import { Elysia, t } from 'elysia'
|
|
4
|
-
import { S3Client } from 'bun'
|
|
5
|
-
import { CronJob } from 'cron'
|
|
6
4
|
import { authenticator } from 'otplib'
|
|
7
5
|
import QRCode from 'qrcode'
|
|
8
|
-
import {
|
|
9
|
-
import { readFileSync } from 'node:fs'
|
|
10
|
-
import { existsSync } from 'node:fs'
|
|
11
|
-
import { join, relative, dirname } from 'node:path'
|
|
6
|
+
import { writeFile } from 'node:fs/promises'
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
12
8
|
import { html } from '@elysiajs/html'
|
|
13
9
|
|
|
10
|
+
// Import core modules
|
|
11
|
+
import { createSessionManager } from './core/session.js'
|
|
12
|
+
import { createScheduler } from './core/scheduler.js'
|
|
13
|
+
import { createBackupService } from './services/backup.service.js'
|
|
14
|
+
|
|
14
15
|
// Import page components
|
|
15
16
|
import { LoginPage } from './views/LoginPage.js'
|
|
16
17
|
import { DashboardPage } from './views/DashboardPage.js'
|
|
17
18
|
import { OnboardingPage } from './views/OnboardingPage.js'
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Generate a secure random session token
|
|
24
|
-
*/
|
|
25
|
-
const generateSessionToken = () => {
|
|
26
|
-
const array = new Uint8Array(32)
|
|
27
|
-
crypto.getRandomValues(array)
|
|
28
|
-
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Create a new session for a user
|
|
33
|
-
*/
|
|
34
|
-
const createSession = (username, sessionDuration = 24 * 60 * 60 * 1000) => {
|
|
35
|
-
const token = generateSessionToken()
|
|
36
|
-
const expiresAt = Date.now() + sessionDuration
|
|
37
|
-
sessions.set(token, { username, expiresAt })
|
|
38
|
-
return { token, expiresAt }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Validate and return session data
|
|
43
|
-
*/
|
|
44
|
-
const getSession = token => {
|
|
45
|
-
if (!token) return null
|
|
46
|
-
const session = sessions.get(token)
|
|
47
|
-
if (!session) return null
|
|
48
|
-
if (Date.now() > session.expiresAt) {
|
|
49
|
-
sessions.delete(token)
|
|
50
|
-
return null
|
|
51
|
-
}
|
|
52
|
-
return session
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Delete a session
|
|
57
|
-
*/
|
|
58
|
-
const deleteSession = token => {
|
|
59
|
-
if (token) {
|
|
60
|
-
sessions.delete(token)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
20
|
+
// Create session manager instance
|
|
21
|
+
const sessionManager = createSessionManager()
|
|
63
22
|
|
|
64
23
|
/**
|
|
65
24
|
* Elysia Plugin for R2/S3 Backup with UI (using native Bun.s3)
|
|
@@ -92,7 +51,6 @@ export const r2Backup = initialConfig => app => {
|
|
|
92
51
|
}
|
|
93
52
|
|
|
94
53
|
let config = { ...initialConfig, ...savedConfig }
|
|
95
|
-
let backupJob = null
|
|
96
54
|
|
|
97
55
|
// Helper to check if config.json exists and has required fields
|
|
98
56
|
const hasValidConfig = () => {
|
|
@@ -107,214 +65,16 @@ export const r2Backup = initialConfig => app => {
|
|
|
107
65
|
}
|
|
108
66
|
}
|
|
109
67
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
bucket: config.bucket,
|
|
113
|
-
endpoint: config.endpoint,
|
|
114
|
-
accessKeyId: config.accessKeyId ? '***' + config.accessKeyId.slice(-4) : 'missing',
|
|
115
|
-
hasSecret: !!config.secretAccessKey,
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
return new S3Client({
|
|
119
|
-
accessKeyId: config.accessKeyId,
|
|
120
|
-
secretAccessKey: config.secretAccessKey,
|
|
121
|
-
endpoint: config.endpoint,
|
|
122
|
-
bucket: config.bucket,
|
|
123
|
-
region: 'auto',
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const uploadFile = async (filePath, rootDir, timestampPrefix) => {
|
|
128
|
-
const s3 = getS3Client()
|
|
129
|
-
const fileContent = await readFile(filePath)
|
|
130
|
-
|
|
131
|
-
const relativePath = relative(rootDir, filePath)
|
|
132
|
-
const dir = dirname(relativePath)
|
|
133
|
-
const filename = relativePath.split('/').pop()
|
|
134
|
-
|
|
135
|
-
// Parse timestamp to extract date and time parts
|
|
136
|
-
// Expected format: YYYY-MM-DD_HH-mm-ss
|
|
137
|
-
const timestamp = timestampPrefix || new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
138
|
-
const datePart = timestamp.slice(0, 10) // YYYY-MM-DD
|
|
139
|
-
const timePart = timestamp.slice(11) || '00-00-00' // HH-mm-ss
|
|
140
|
-
|
|
141
|
-
// Format: YYYY-MM-DD/HH-mm-ss_filename.ext
|
|
142
|
-
const newFilename = `${timePart}_${filename}`
|
|
143
|
-
const dateFolder = datePart
|
|
144
|
-
|
|
145
|
-
// Reconstruct path: prefix/date/[subdir/]time_filename
|
|
146
|
-
let finalPath
|
|
147
|
-
if (dir === '.') {
|
|
148
|
-
finalPath = join(dateFolder, newFilename)
|
|
149
|
-
} else {
|
|
150
|
-
finalPath = join(dateFolder, dir, newFilename)
|
|
151
|
-
}
|
|
152
|
-
const key = config.prefix ? join(config.prefix, finalPath) : finalPath
|
|
153
|
-
|
|
154
|
-
console.log(`Uploading ${key}...`)
|
|
155
|
-
await s3.write(key, fileContent)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const processDirectory = async (dir, timestampPrefix) => {
|
|
159
|
-
const files = await readdir(dir)
|
|
160
|
-
for (const file of files) {
|
|
161
|
-
const fullPath = join(dir, file)
|
|
162
|
-
const stats = await stat(fullPath)
|
|
163
|
-
if (stats.isDirectory()) {
|
|
164
|
-
await processDirectory(fullPath, timestampPrefix)
|
|
165
|
-
} else {
|
|
166
|
-
const allowedExtensions = config.extensions || []
|
|
167
|
-
const hasExtension = allowedExtensions.some(ext => file.endsWith(ext))
|
|
168
|
-
|
|
169
|
-
const timestampRegex = /^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)_/
|
|
170
|
-
if (timestampRegex.test(file)) {
|
|
171
|
-
console.log(`Skipping backup-like file: ${file}`)
|
|
172
|
-
continue
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (allowedExtensions.length === 0 || hasExtension) {
|
|
176
|
-
await uploadFile(fullPath, config.sourceDir, timestampPrefix)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const setupCron = () => {
|
|
183
|
-
if (backupJob) {
|
|
184
|
-
backupJob.stop()
|
|
185
|
-
backupJob = null
|
|
186
|
-
}
|
|
68
|
+
// Create backup service with config getter
|
|
69
|
+
const backupService = createBackupService(() => config)
|
|
187
70
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
config.cronSchedule,
|
|
193
|
-
async () => {
|
|
194
|
-
try {
|
|
195
|
-
const now = new Date()
|
|
196
|
-
const timestamp =
|
|
197
|
-
now.getFullYear() +
|
|
198
|
-
'-' +
|
|
199
|
-
String(now.getMonth() + 1).padStart(2, '0') +
|
|
200
|
-
'-' +
|
|
201
|
-
String(now.getDate()).padStart(2, '0') +
|
|
202
|
-
'_' +
|
|
203
|
-
String(now.getHours()).padStart(2, '0') +
|
|
204
|
-
'-' +
|
|
205
|
-
String(now.getMinutes()).padStart(2, '0') +
|
|
206
|
-
'-' +
|
|
207
|
-
String(now.getSeconds()).padStart(2, '0')
|
|
208
|
-
|
|
209
|
-
await processDirectory(config.sourceDir, timestamp)
|
|
210
|
-
console.log('Scheduled backup completed')
|
|
211
|
-
} catch (e) {
|
|
212
|
-
console.error('Scheduled backup failed:', e)
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
null,
|
|
216
|
-
true
|
|
217
|
-
)
|
|
218
|
-
} catch (e) {
|
|
219
|
-
console.error('Invalid cron schedule:', e.message)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
setupCron()
|
|
225
|
-
|
|
226
|
-
const listRemoteFiles = async () => {
|
|
227
|
-
const s3 = getS3Client()
|
|
228
|
-
try {
|
|
229
|
-
const response = await s3.list({ prefix: config.prefix || '' })
|
|
230
|
-
if (Array.isArray(response)) {
|
|
231
|
-
return response.map(f => ({
|
|
232
|
-
Key: f.key || f.name,
|
|
233
|
-
Size: f.size,
|
|
234
|
-
LastModified: f.lastModified,
|
|
235
|
-
}))
|
|
236
|
-
}
|
|
237
|
-
if (response.contents) {
|
|
238
|
-
return response.contents.map(f => ({
|
|
239
|
-
Key: f.key,
|
|
240
|
-
Size: f.size,
|
|
241
|
-
LastModified: f.lastModified,
|
|
242
|
-
}))
|
|
243
|
-
}
|
|
244
|
-
console.log('Unknown list response structure:', response)
|
|
245
|
-
return []
|
|
246
|
-
} catch (e) {
|
|
247
|
-
console.error('Error listing files with Bun.s3:', e)
|
|
248
|
-
return []
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const restoreFile = async key => {
|
|
253
|
-
const s3 = getS3Client()
|
|
254
|
-
const file = s3.file(key)
|
|
255
|
-
|
|
256
|
-
if (!(await file.exists())) {
|
|
257
|
-
throw new Error(`File ${key} not found in bucket`)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const arrayBuffer = await file.arrayBuffer()
|
|
261
|
-
const byteArray = new Uint8Array(arrayBuffer)
|
|
262
|
-
|
|
263
|
-
// Remove prefix from key to get relative path
|
|
264
|
-
const relativePath = config.prefix ? key.replace(config.prefix, '') : key
|
|
265
|
-
const cleanRelative = relativePath.replace(/^[\/\\]/, '')
|
|
266
|
-
|
|
267
|
-
// New structure: YYYY-MM-DD/[subdir/]HH-mm-ss_filename.ext
|
|
268
|
-
// Old structure: YYYY-MM-DD_HH-mm-ss_filename.ext or timestamp_filename.ext
|
|
269
|
-
const pathParts = cleanRelative.split('/')
|
|
270
|
-
const filename = pathParts.pop()
|
|
271
|
-
|
|
272
|
-
// Check if first part is a date folder (YYYY-MM-DD)
|
|
273
|
-
const dateFolderRegex = /^\d{4}-\d{2}-\d{2}$/
|
|
274
|
-
let subdir = ''
|
|
275
|
-
|
|
276
|
-
if (pathParts.length > 0 && dateFolderRegex.test(pathParts[0])) {
|
|
277
|
-
// New format: remove date folder, keep remaining subdirs
|
|
278
|
-
pathParts.shift() // Remove date folder
|
|
279
|
-
subdir = pathParts.join('/')
|
|
280
|
-
} else {
|
|
281
|
-
// Old format: use dir as-is
|
|
282
|
-
subdir = pathParts.join('/')
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Strip time prefix from filename (HH-mm-ss_filename or old YYYY-MM-DD_HH-mm-ss_filename)
|
|
286
|
-
const timeOnlyRegex = /^\d{2}-\d{2}-\d{2}_/
|
|
287
|
-
const fullTimestampRegex = /^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)_/
|
|
288
|
-
let originalFilename = filename.replace(timeOnlyRegex, '').replace(fullTimestampRegex, '')
|
|
289
|
-
|
|
290
|
-
const finalLocalRelativePath = subdir ? join(subdir, originalFilename) : originalFilename
|
|
291
|
-
const localPath = join(config.sourceDir, finalLocalRelativePath)
|
|
292
|
-
|
|
293
|
-
await mkdir(dirname(localPath), { recursive: true })
|
|
294
|
-
await writeFile(localPath, byteArray)
|
|
295
|
-
return localPath
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const deleteFile = async key => {
|
|
299
|
-
const s3 = getS3Client()
|
|
300
|
-
await s3.delete(key)
|
|
301
|
-
}
|
|
71
|
+
// Create scheduler with backup callback
|
|
72
|
+
const scheduler = createScheduler(async timestamp => {
|
|
73
|
+
await backupService.processDirectory(config.sourceDir, timestamp)
|
|
74
|
+
})
|
|
302
75
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
let nextRun = null
|
|
306
|
-
if (isRunning && backupJob) {
|
|
307
|
-
try {
|
|
308
|
-
const nextDate = backupJob.nextDate()
|
|
309
|
-
if (nextDate) {
|
|
310
|
-
nextRun = nextDate.toJSDate().toISOString()
|
|
311
|
-
}
|
|
312
|
-
} catch (e) {
|
|
313
|
-
console.error('Error getting next date', e)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return { isRunning, nextRun }
|
|
317
|
-
}
|
|
76
|
+
// Setup initial cron schedule
|
|
77
|
+
scheduler.setup(config.cronSchedule, config.cronEnabled !== false)
|
|
318
78
|
|
|
319
79
|
return app.use(html()).group('/backup', app => {
|
|
320
80
|
// Authentication Middleware
|
|
@@ -347,7 +107,7 @@ export const r2Backup = initialConfig => app => {
|
|
|
347
107
|
const sessionMatch = cookies.match(/backup-session=([^;]+)/)
|
|
348
108
|
const sessionToken = sessionMatch ? sessionMatch[1] : null
|
|
349
109
|
|
|
350
|
-
const session =
|
|
110
|
+
const session = sessionManager.get(sessionToken)
|
|
351
111
|
|
|
352
112
|
if (!session) {
|
|
353
113
|
// Return JSON error for API routes
|
|
@@ -410,7 +170,7 @@ export const r2Backup = initialConfig => app => {
|
|
|
410
170
|
}
|
|
411
171
|
|
|
412
172
|
const sessionDuration = config.auth.sessionDuration || 24 * 60 * 60 * 1000
|
|
413
|
-
const { token, expiresAt } =
|
|
173
|
+
const { token, expiresAt } = sessionManager.create(username, sessionDuration)
|
|
414
174
|
|
|
415
175
|
const expiresDate = new Date(expiresAt)
|
|
416
176
|
set.headers['Set-Cookie'] = `backup-session=${token}; Path=/backup; HttpOnly; SameSite=Lax; Expires=${expiresDate.toUTCString()}`
|
|
@@ -437,7 +197,7 @@ export const r2Backup = initialConfig => app => {
|
|
|
437
197
|
const sessionToken = sessionMatch ? sessionMatch[1] : null
|
|
438
198
|
|
|
439
199
|
if (sessionToken) {
|
|
440
|
-
|
|
200
|
+
sessionManager.delete(sessionToken)
|
|
441
201
|
}
|
|
442
202
|
|
|
443
203
|
set.headers['Set-Cookie'] = `backup-session=; Path=/backup; HttpOnly; SameSite=Lax; Max-Age=0`
|
|
@@ -545,7 +305,7 @@ export const r2Backup = initialConfig => app => {
|
|
|
545
305
|
try {
|
|
546
306
|
const { timestamp } = body || {}
|
|
547
307
|
console.log(`Starting backup of ${config.sourceDir} to ${config.bucket} with timestamp ${timestamp}`)
|
|
548
|
-
await processDirectory(config.sourceDir, timestamp)
|
|
308
|
+
await backupService.processDirectory(config.sourceDir, timestamp)
|
|
549
309
|
return {
|
|
550
310
|
status: 'success',
|
|
551
311
|
message: 'Backup completed successfully',
|
|
@@ -569,7 +329,7 @@ export const r2Backup = initialConfig => app => {
|
|
|
569
329
|
// API: List Files
|
|
570
330
|
.get('/api/files', async ({ set }) => {
|
|
571
331
|
try {
|
|
572
|
-
const files = await listRemoteFiles()
|
|
332
|
+
const files = await backupService.listRemoteFiles()
|
|
573
333
|
return {
|
|
574
334
|
files: files.map(f => ({
|
|
575
335
|
key: f.Key,
|
|
@@ -590,7 +350,7 @@ export const r2Backup = initialConfig => app => {
|
|
|
590
350
|
try {
|
|
591
351
|
const { key } = body
|
|
592
352
|
if (!key) throw new Error('Key is required')
|
|
593
|
-
const localPath = await restoreFile(key)
|
|
353
|
+
const localPath = await backupService.restoreFile(key)
|
|
594
354
|
return { status: 'success', message: `Restored to ${localPath}` }
|
|
595
355
|
} catch (error) {
|
|
596
356
|
set.status = 500
|
|
@@ -611,7 +371,7 @@ export const r2Backup = initialConfig => app => {
|
|
|
611
371
|
try {
|
|
612
372
|
const { key } = body
|
|
613
373
|
if (!key) throw new Error('Key is required')
|
|
614
|
-
await deleteFile(key)
|
|
374
|
+
await backupService.deleteFile(key)
|
|
615
375
|
return { status: 'success', message: `Deleted ${key}` }
|
|
616
376
|
} catch (error) {
|
|
617
377
|
set.status = 500
|
|
@@ -644,11 +404,11 @@ export const r2Backup = initialConfig => app => {
|
|
|
644
404
|
console.error('Failed to save config:', e)
|
|
645
405
|
}
|
|
646
406
|
|
|
647
|
-
|
|
407
|
+
scheduler.setup(config.cronSchedule, config.cronEnabled)
|
|
648
408
|
return {
|
|
649
409
|
status: 'success',
|
|
650
410
|
config: { ...config, secretAccessKey: '***' },
|
|
651
|
-
jobStatus:
|
|
411
|
+
jobStatus: scheduler.getStatus(config.cronEnabled),
|
|
652
412
|
}
|
|
653
413
|
},
|
|
654
414
|
{
|
|
@@ -739,7 +499,7 @@ export const r2Backup = initialConfig => app => {
|
|
|
739
499
|
config = { ...config, ...initialConfigData }
|
|
740
500
|
|
|
741
501
|
// Setup cron if enabled
|
|
742
|
-
|
|
502
|
+
scheduler.setup(config.cronSchedule, config.cronEnabled)
|
|
743
503
|
|
|
744
504
|
return { status: 'success', message: 'Configuration saved successfully' }
|
|
745
505
|
} catch (e) {
|
|
@@ -773,7 +533,7 @@ export const r2Backup = initialConfig => app => {
|
|
|
773
533
|
return
|
|
774
534
|
}
|
|
775
535
|
|
|
776
|
-
const jobStatus =
|
|
536
|
+
const jobStatus = scheduler.getStatus(config.cronEnabled)
|
|
777
537
|
const hasAuth = !!(config.auth && config.auth.username && config.auth.password)
|
|
778
538
|
return DashboardPage({ config, jobStatus, hasAuth })
|
|
779
539
|
})
|
|
@@ -10,14 +10,6 @@ import { S3Client } from 'bun'
|
|
|
10
10
|
* @returns {S3Client} Configured S3 client
|
|
11
11
|
*/
|
|
12
12
|
export const createS3Client = config => {
|
|
13
|
-
// Debug config (masked for security)
|
|
14
|
-
console.log('S3 Config:', {
|
|
15
|
-
bucket: config.bucket,
|
|
16
|
-
endpoint: config.endpoint,
|
|
17
|
-
accessKeyId: config.accessKeyId ? '***' + config.accessKeyId.slice(-4) : 'missing',
|
|
18
|
-
hasSecret: !!config.secretAccessKey,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
13
|
return new S3Client({
|
|
22
14
|
accessKeyId: config.accessKeyId,
|
|
23
15
|
secretAccessKey: config.secretAccessKey,
|
|
@@ -285,6 +285,14 @@ export const backupAppScript = ({ config, jobStatus }) => `
|
|
|
285
285
|
headers: { 'Content-Type': 'application/json' },
|
|
286
286
|
body: JSON.stringify({ key })
|
|
287
287
|
})
|
|
288
|
+
|
|
289
|
+
// Handle session expiration
|
|
290
|
+
if (res.status === 401) {
|
|
291
|
+
this.addLog('Session expired. Redirecting to login...', 'error')
|
|
292
|
+
setTimeout(() => window.location.href = '/backup/login', 1500)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
288
296
|
const data = await res.json()
|
|
289
297
|
if (data.status === 'success') {
|
|
290
298
|
this.addLog(data.message, 'success')
|
|
@@ -303,6 +311,14 @@ export const backupAppScript = ({ config, jobStatus }) => `
|
|
|
303
311
|
headers: { 'Content-Type': 'application/json' },
|
|
304
312
|
body: JSON.stringify({ key })
|
|
305
313
|
})
|
|
314
|
+
|
|
315
|
+
// Handle session expiration
|
|
316
|
+
if (res.status === 401) {
|
|
317
|
+
this.addLog('Session expired. Redirecting to login...', 'error')
|
|
318
|
+
setTimeout(() => window.location.href = '/backup/login', 1500)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
306
322
|
const data = await res.json()
|
|
307
323
|
if (data.status === 'success') {
|
|
308
324
|
this.addLog(data.message, 'success')
|
|
@@ -322,12 +338,22 @@ export const backupAppScript = ({ config, jobStatus }) => `
|
|
|
322
338
|
headers: { 'Content-Type': 'application/json' },
|
|
323
339
|
body: JSON.stringify(this.configForm)
|
|
324
340
|
})
|
|
341
|
+
|
|
342
|
+
// Handle session expiration
|
|
343
|
+
if (res.status === 401) {
|
|
344
|
+
this.addLog('Session expired. Redirecting to login...', 'error')
|
|
345
|
+
setTimeout(() => window.location.href = '/backup/login', 1500)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
325
349
|
const data = await res.json()
|
|
326
350
|
if (data.status === 'success') {
|
|
327
351
|
this.config = data.config
|
|
328
352
|
if (data.jobStatus) this.cronStatus = data.jobStatus
|
|
329
353
|
this.addLog('Configuration updated', 'success')
|
|
330
354
|
this.activeTab = 'dashboard'
|
|
355
|
+
} else {
|
|
356
|
+
throw new Error(data.message || 'Failed to save config')
|
|
331
357
|
}
|
|
332
358
|
} catch (err) {
|
|
333
359
|
this.addLog('Failed to save config: ' + err.message, 'error')
|