@riligar/elysia-backup 1.6.0 → 1.7.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 CHANGED
@@ -1,23 +1,37 @@
1
- # ElysiaJS Backup Plugin
1
+ # @riligar/elysia-backup
2
2
 
3
- Elysia plugin for R2/S3 backup with a built-in UI dashboard. Uses native Bun S3 client for optimal performance.
3
+ [![Open Source](https://img.shields.io/badge/Open%20Source-RiLiGar-blue)](https://riligar.click/)
4
+ [![npm version](https://img.shields.io/npm/v/@riligar/elysia-backup.svg)](https://www.npmjs.com/package/@riligar/elysia-backup)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
4
6
 
5
- ## Features
7
+ > **An open source project by [RiLiGar](https://riligar.click/)**
6
8
 
7
- - 📁 Backup local directories to R2/S3
8
- - 🔄 Scheduled backups with cron expressions
9
- - 🖥️ Built-in UI dashboard
10
- - ⬇️ One-click restore
11
- - 🗑️ Delete remote backups
12
- - ⚙️ Runtime configuration
9
+ A powerful Elysia plugin for R2/S3 backup with a beautiful built-in UI dashboard. Designed for Bun runtime with native S3 client for optimal performance.
13
10
 
14
- ## Installation
11
+ ## ✨ Features
12
+
13
+ - 📁 **Smart Backup** — Backup local directories to R2/S3 with file filtering
14
+ - 🔄 **Scheduled Backups** — Cron-based automatic backups
15
+ - 🖥️ **Beautiful Dashboard** — Modern UI with real-time status
16
+ - 🔐 **Secure Authentication** — Session-based auth with 2FA/TOTP support
17
+ - 🧭 **Guided Onboarding** — Multi-step setup wizard for first-time configuration
18
+ - ⬇️ **One-Click Restore** — Easily restore files from backup
19
+ - 🗑️ **Backup Management** — Delete old backups from the UI
20
+ - ⚙️ **Runtime Configuration** — Update settings without restarting
21
+
22
+ ## 📦 Installation
15
23
 
16
24
  ```bash
17
25
  bun add @riligar/elysia-backup
18
26
  ```
19
27
 
20
- ## Usage
28
+ ### Peer Dependencies
29
+
30
+ ```bash
31
+ bun add elysia @elysiajs/html
32
+ ```
33
+
34
+ ## 🚀 Quick Start
21
35
 
22
36
  ```javascript
23
37
  import { Elysia } from 'elysia'
@@ -26,60 +40,158 @@ import { r2Backup } from '@riligar/elysia-backup'
26
40
  const app = new Elysia()
27
41
  .use(
28
42
  r2Backup({
29
- bucket: process.env.R2_BUCKET,
30
- accessKeyId: process.env.R2_ACCESS_KEY_ID,
31
- secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
32
- endpoint: process.env.R2_ENDPOINT,
33
43
  sourceDir: './data',
34
- prefix: 'backups/',
35
- extensions: ['.db', '.sqlite'],
36
- cronSchedule: '0 0 * * *', // Daily at midnight
37
- cronEnabled: true,
44
+ configPath: './config.json',
38
45
  })
39
46
  )
40
47
  .listen(3000)
41
48
 
42
- console.log('Server running at http://localhost:3000')
43
- console.log('Backup UI at http://localhost:3000/backup')
49
+ console.log('🦊 Server running at http://localhost:3000')
50
+ console.log('📦 Backup UI at http://localhost:3000/backup')
44
51
  ```
45
52
 
46
- ## Configuration
53
+ On first run, you'll be guided through an onboarding wizard to configure:
54
+
55
+ - Storage credentials (R2/S3)
56
+ - Backup schedule
57
+ - Admin authentication
58
+
59
+ ## ⚙️ Configuration
60
+
61
+ ### Plugin Options
47
62
 
48
- | Option | Type | Required | Description |
49
- | ----------------- | -------- | -------- | ------------------------------------------------------ |
50
- | `bucket` | string | ✅ | R2/S3 bucket name |
51
- | `accessKeyId` | string | | R2/S3 Access Key ID |
52
- | `secretAccessKey` | string | ✅ | R2/S3 Secret Access Key |
53
- | `endpoint` | string | ✅ | R2/S3 Endpoint URL |
54
- | `sourceDir` | string | ✅ | Local directory to backup |
55
- | `prefix` | string | ❌ | Prefix for S3 keys (e.g., 'backups/') |
56
- | `extensions` | string[] | ❌ | File extensions to include |
57
- | `cronSchedule` | string | ❌ | Cron expression for scheduled backups |
58
- | `cronEnabled` | boolean | ❌ | Enable/disable scheduled backups |
59
- | `configPath` | string | ❌ | Path to save runtime config (default: './config.json') |
63
+ | Option | Type | Required | Description |
64
+ | ------------ | ------ | -------- | ------------------------------------------------------ |
65
+ | `sourceDir` | string | ✅ | Local directory to backup |
66
+ | `configPath` | string | | Path to save runtime config (default: `./config.json`) |
60
67
 
61
- ## API Endpoints
68
+ ### Runtime Configuration (via UI or config.json)
69
+
70
+ | Option | Type | Description |
71
+ | ----------------- | -------- | ---------------------------------------------- |
72
+ | `bucket` | string | R2/S3 bucket name |
73
+ | `accessKeyId` | string | R2/S3 Access Key ID |
74
+ | `secretAccessKey` | string | R2/S3 Secret Access Key |
75
+ | `endpoint` | string | R2/S3 Endpoint URL |
76
+ | `prefix` | string | Prefix for S3 keys (e.g., `backups/`) |
77
+ | `extensions` | string[] | File extensions to include (empty = all files) |
78
+ | `cronSchedule` | string | Cron expression for scheduled backups |
79
+ | `cronEnabled` | boolean | Enable/disable scheduled backups |
80
+ | `auth.username` | string | Admin username |
81
+ | `auth.password` | string | Admin password |
82
+ | `auth.totpSecret` | string | TOTP secret for 2FA (optional) |
83
+
84
+ ## 🔌 API Endpoints
62
85
 
63
86
  The plugin adds the following routes under `/backup`:
64
87
 
65
- | Method | Path | Description |
66
- | ------ | --------------------- | --------------------- |
67
- | GET | `/backup` | UI Dashboard |
68
- | POST | `/backup/api/run` | Trigger manual backup |
69
- | GET | `/backup/api/files` | List remote files |
70
- | POST | `/backup/api/restore` | Restore a file |
71
- | POST | `/backup/api/delete` | Delete a remote file |
72
- | POST | `/backup/api/config` | Update configuration |
88
+ ### Pages
89
+
90
+ | Method | Path | Description |
91
+ | ------ | -------------------- | ----------------------- |
92
+ | GET | `/backup` | Dashboard UI |
93
+ | GET | `/backup/login` | Login page |
94
+ | GET | `/backup/onboarding` | First-time setup wizard |
95
+
96
+ ### Authentication
97
+
98
+ | Method | Path | Description |
99
+ | ------ | --------------------- | ----------------- |
100
+ | POST | `/backup/auth/login` | Authenticate user |
101
+ | POST | `/backup/auth/logout` | End session |
102
+
103
+ ### Backup Operations
104
+
105
+ | Method | Path | Description |
106
+ | ------ | --------------------- | --------------------------- |
107
+ | POST | `/backup/api/run` | Trigger manual backup |
108
+ | GET | `/backup/api/files` | List remote backup files |
109
+ | POST | `/backup/api/restore` | Restore a specific file |
110
+ | POST | `/backup/api/delete` | Delete a remote backup file |
111
+ | POST | `/backup/api/config` | Update configuration |
112
+
113
+ ### Two-Factor Authentication (TOTP)
114
+
115
+ | Method | Path | Description |
116
+ | ------ | --------------------------- | ----------------------------- |
117
+ | GET | `/backup/api/totp/status` | Check 2FA status |
118
+ | POST | `/backup/api/totp/generate` | Generate new TOTP secret & QR |
119
+ | POST | `/backup/api/totp/verify` | Verify and enable 2FA |
120
+ | POST | `/backup/api/totp/disable` | Disable 2FA |
73
121
 
74
- ## Environment Variables Example
122
+ ## 🔐 Security Features
123
+
124
+ ### Session-Based Authentication
125
+
126
+ - Secure session tokens with configurable expiration
127
+ - HTTP-only cookies for session management
128
+ - Automatic session cleanup
129
+
130
+ ### Two-Factor Authentication (TOTP)
131
+
132
+ - Compatible with any authenticator app (Google Authenticator, Authy, etc.)
133
+ - QR code for easy setup
134
+ - Backup codes support
135
+
136
+ ## 📂 Project Structure
137
+
138
+ ```
139
+ src/
140
+ ├── index.js # Main plugin entry
141
+ ├── assets/ # Static assets (favicon, logo)
142
+ ├── views/
143
+ │ ├── LoginPage.js # Login page
144
+ │ ├── DashboardPage.js # Main dashboard
145
+ │ ├── OnboardingPage.js # Setup wizard
146
+ │ ├── components/ # UI components
147
+ │ │ ├── ActionArea.js
148
+ │ │ ├── FilesTab.js
149
+ │ │ ├── Footer.js
150
+ │ │ ├── Head.js
151
+ │ │ ├── Header.js
152
+ │ │ ├── LoginCard.js
153
+ │ │ ├── OnboardingCard.js
154
+ │ │ ├── SecuritySection.js
155
+ │ │ ├── SettingsTab.js
156
+ │ │ └── StatusCards.js
157
+ │ └── scripts/ # Client-side JavaScript
158
+ │ ├── backupApp.js
159
+ │ ├── loginApp.js
160
+ │ └── onboardingApp.js
161
+ ```
162
+
163
+ ## 🌍 Environment Variables Example
75
164
 
76
165
  ```env
166
+ # Optional - for initial configuration
77
167
  R2_BUCKET=your-bucket-name
78
168
  R2_ACCESS_KEY_ID=your-access-key
79
169
  R2_SECRET_ACCESS_KEY=your-secret-key
80
170
  R2_ENDPOINT=https://your-account.r2.cloudflarestorage.com
81
171
  ```
82
172
 
83
- ## License
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
+ ## 🤝 Contributing
186
+
187
+ Contributions are welcome! See our [GitHub repository](https://github.com/riligar-solutions/elysia-backup) for more information.
188
+
189
+ ## 📄 License
190
+
191
+ MIT © [RiLiGar](https://riligar.click/)
192
+
193
+ ---
84
194
 
85
- MIT
195
+ <p align="center">
196
+ Made with ❤️ by <a href="https://riligar.click/">RiLiGar</a>
197
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riligar/elysia-backup",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Elysia plugin for R2/S3 backup with a built-in UI dashboard",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -132,10 +132,23 @@ export const r2Backup = initialConfig => app => {
132
132
  const dir = dirname(relativePath)
133
133
  const filename = relativePath.split('/').pop()
134
134
 
135
- const timestamp = timestampPrefix || new Date().toISOString()
136
- const newFilename = `${timestamp}_${filename}`
137
-
138
- const finalPath = dir === '.' ? newFilename : join(dir, newFilename)
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
+ }
139
152
  const key = config.prefix ? join(config.prefix, finalPath) : finalPath
140
153
 
141
154
  console.log(`Uploading ${key}...`)
@@ -178,7 +191,6 @@ export const r2Backup = initialConfig => app => {
178
191
  backupJob = new CronJob(
179
192
  config.cronSchedule,
180
193
  async () => {
181
- console.log('Running scheduled backup...')
182
194
  try {
183
195
  const now = new Date()
184
196
  const timestamp =
@@ -248,16 +260,34 @@ export const r2Backup = initialConfig => app => {
248
260
  const arrayBuffer = await file.arrayBuffer()
249
261
  const byteArray = new Uint8Array(arrayBuffer)
250
262
 
263
+ // Remove prefix from key to get relative path
251
264
  const relativePath = config.prefix ? key.replace(config.prefix, '') : key
252
265
  const cleanRelative = relativePath.replace(/^[\/\\]/, '')
253
266
 
254
- const dir = dirname(cleanRelative)
255
- const filename = cleanRelative.split('/').pop()
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
+ }
256
284
 
257
- 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)_/
258
- const originalFilename = filename.replace(timestampRegex, '')
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, '')
259
289
 
260
- const finalLocalRelativePath = dir === '.' ? originalFilename : join(dir, originalFilename)
290
+ const finalLocalRelativePath = subdir ? join(subdir, originalFilename) : originalFilename
261
291
  const localPath = join(config.sourceDir, finalLocalRelativePath)
262
292
 
263
293
  await mkdir(dirname(localPath), { recursive: true })
@@ -320,6 +350,13 @@ export const r2Backup = initialConfig => app => {
320
350
  const session = getSession(sessionToken)
321
351
 
322
352
  if (!session) {
353
+ // Return JSON error for API routes
354
+ if (path.startsWith('/backup/api/')) {
355
+ context.set.status = 401
356
+ return { status: 'error', message: 'Session expired. Please login again.' }
357
+ }
358
+
359
+ // Redirect to login for page routes
323
360
  context.set.status = 302
324
361
  context.set.headers['Location'] = '/backup/login'
325
362
  return new Response('Redirecting to login', {
@@ -17,7 +17,7 @@ export const createBackupService = getConfig => {
17
17
  * Upload a single file to S3
18
18
  * @param {string} filePath - Local file path
19
19
  * @param {string} rootDir - Root directory for relative path calculation
20
- * @param {string} timestampPrefix - Timestamp to prepend to filename
20
+ * @param {string} timestampPrefix - Timestamp to prepend to filename (YYYY-MM-DD_HH-mm-ss)
21
21
  */
22
22
  const uploadFile = async (filePath, rootDir, timestampPrefix) => {
23
23
  const config = getConfig()
@@ -28,15 +28,25 @@ export const createBackupService = getConfig => {
28
28
  const dir = dirname(relativePath)
29
29
  const filename = relativePath.split('/').pop()
30
30
 
31
- // Format: YYYY-MM-DD_HH-mm-ss_filename.ext
32
- const timestamp = timestampPrefix || new Date().toISOString()
33
- const newFilename = `${timestamp}_${filename}`
34
-
35
- // Reconstruct path with new filename
36
- const finalPath = dir === '.' ? newFilename : join(dir, newFilename)
31
+ // Parse timestamp to extract date and time parts
32
+ // Expected format: YYYY-MM-DD_HH-mm-ss
33
+ const timestamp = timestampPrefix || new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
34
+ const datePart = timestamp.slice(0, 10) // YYYY-MM-DD
35
+ const timePart = timestamp.slice(11) || '00-00-00' // HH-mm-ss
36
+
37
+ // Format: YYYY-MM-DD/HH-mm-ss_filename.ext
38
+ const newFilename = `${timePart}_${filename}`
39
+ const dateFolder = datePart
40
+
41
+ // Reconstruct path: prefix/date/[subdir/]time_filename
42
+ let finalPath
43
+ if (dir === '.') {
44
+ finalPath = join(dateFolder, newFilename)
45
+ } else {
46
+ finalPath = join(dateFolder, dir, newFilename)
47
+ }
37
48
  const key = config.prefix ? join(config.prefix, finalPath) : finalPath
38
49
 
39
- console.log(`Uploading ${key}...`)
40
50
  await s3.write(key, fileContent)
41
51
  }
42
52
 
@@ -132,15 +142,30 @@ export const createBackupService = getConfig => {
132
142
  const relativePath = config.prefix ? key.replace(config.prefix, '') : key
133
143
  const cleanRelative = relativePath.replace(/^[\/\\]/, '')
134
144
 
135
- // Extract directory and filename
136
- const dir = dirname(cleanRelative)
137
- const filename = cleanRelative.split('/').pop()
145
+ // New structure: YYYY-MM-DD/[subdir/]HH-mm-ss_filename.ext
146
+ // Old structure: YYYY-MM-DD_HH-mm-ss_filename.ext or timestamp_filename.ext
147
+ const pathParts = cleanRelative.split('/')
148
+ const filename = pathParts.pop()
149
+
150
+ // Check if first part is a date folder (YYYY-MM-DD)
151
+ const dateFolderRegex = /^\d{4}-\d{2}-\d{2}$/
152
+ let subdir = ''
153
+
154
+ if (pathParts.length > 0 && dateFolderRegex.test(pathParts[0])) {
155
+ // New format: remove date folder, keep remaining subdirs
156
+ pathParts.shift() // Remove date folder
157
+ subdir = pathParts.join('/')
158
+ } else {
159
+ // Old format: use dir as-is
160
+ subdir = pathParts.join('/')
161
+ }
138
162
 
139
- // Strip timestamp prefix to restore original filename
140
- 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)_/
141
- const originalFilename = filename.replace(timestampRegex, '')
163
+ // Strip time prefix from filename (HH-mm-ss_filename or old YYYY-MM-DD_HH-mm-ss_filename)
164
+ const timeOnlyRegex = /^\d{2}-\d{2}-\d{2}_/
165
+ 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)_/
166
+ let originalFilename = filename.replace(timeOnlyRegex, '').replace(fullTimestampRegex, '')
142
167
 
143
- const finalLocalRelativePath = dir === '.' ? originalFilename : join(dir, originalFilename)
168
+ const finalLocalRelativePath = subdir ? join(subdir, originalFilename) : originalFilename
144
169
  const localPath = join(config.sourceDir, finalLocalRelativePath)
145
170
 
146
171
  // Ensure directory exists
@@ -11,6 +11,7 @@ import { ActionArea } from './components/ActionArea.js'
11
11
  import { FilesTab } from './components/FilesTab.js'
12
12
  import { SettingsTab } from './components/SettingsTab.js'
13
13
  import { SecuritySection } from './components/SecuritySection.js'
14
+ import { Footer } from './components/Footer.js'
14
15
  import { backupAppScript } from './scripts/backupApp.js'
15
16
 
16
17
  export const DashboardPage = ({ config, jobStatus, hasAuth }) => `
@@ -48,6 +49,9 @@ export const DashboardPage = ({ config, jobStatus, hasAuth }) => `
48
49
  ${SettingsTab()}
49
50
  ${SecuritySection()}
50
51
  </div>
52
+
53
+ <!-- Footer -->
54
+ ${Footer()}
51
55
  </div>
52
56
 
53
57
  ${backupAppScript({ config, jobStatus })}
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { Head } from './components/Head.js'
8
8
  import { LoginCard } from './components/LoginCard.js'
9
+ import { Footer } from './components/Footer.js'
9
10
  import { loginAppScript } from './scripts/loginApp.js'
10
11
 
11
12
  export const LoginPage = ({ totpEnabled = false }) => `
@@ -14,8 +15,14 @@ export const LoginPage = ({ totpEnabled = false }) => `
14
15
  <head>
15
16
  ${Head({ title: 'Login - Backup Manager' })}
16
17
  </head>
17
- <body class="bg-gray-50 min-h-screen flex items-center justify-center p-6 antialiased">
18
- ${LoginCard({ totpEnabled })}
18
+ <body class="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6 antialiased">
19
+ <div class="flex-1 flex items-center justify-center w-full">
20
+ ${LoginCard({ totpEnabled })}
21
+ </div>
22
+
23
+ <div class="w-full max-w-md">
24
+ ${Footer()}
25
+ </div>
19
26
 
20
27
  ${loginAppScript({ totpEnabled })}
21
28
  </body>
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { Head } from './components/Head.js'
7
7
  import { OnboardingCard } from './components/OnboardingCard.js'
8
+ import { Footer } from './components/Footer.js'
8
9
  import { onboardingAppScript } from './scripts/onboardingApp.js'
9
10
 
10
11
  export const OnboardingPage = ({ sourceDir }) => `
@@ -13,8 +14,14 @@ export const OnboardingPage = ({ sourceDir }) => `
13
14
  <head>
14
15
  ${Head({ title: 'Setup - Backup Manager' })}
15
16
  </head>
16
- <body class="bg-gray-50 min-h-screen flex items-center justify-center p-6 antialiased">
17
- ${OnboardingCard({ sourceDir })}
17
+ <body class="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6 antialiased">
18
+ <div class="flex-1 flex items-center justify-center w-full">
19
+ ${OnboardingCard({ sourceDir })}
20
+ </div>
21
+
22
+ <div class="w-full max-w-2xl">
23
+ ${Footer()}
24
+ </div>
18
25
 
19
26
  ${onboardingAppScript({ sourceDir })}
20
27
  </body>
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Footer component with RiLiGar branding
3
+ * Shows open source project attribution
4
+ * @returns {string} HTML string
5
+ */
6
+ export const Footer = () => `
7
+ <footer class="pt-4 text-center">
8
+ <p class="text-sm text-gray-500">
9
+ Open source project by
10
+ <a href="https://riligar.click/" target="_blank" rel="noopener noreferrer"
11
+ class="text-gray-700 hover:text-gray-900 font-medium transition-colors underline underline-offset-2 decoration-gray-300 hover:decoration-gray-600">
12
+ RiLiGar
13
+ </a>
14
+ </p>
15
+ <p class="text-xs text-gray-400 mt-1">
16
+ <a href="https://github.com/riligar/elysia-backup" target="_blank" rel="noopener noreferrer"
17
+ class="hover:text-gray-600 transition-colors inline-flex items-center gap-1">
18
+ <svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
19
+ <path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
20
+ </svg>
21
+ View on GitHub
22
+ </a>
23
+ </p>
24
+ </footer>
25
+ `
@@ -201,6 +201,14 @@ export const backupAppScript = ({ config, jobStatus }) => `
201
201
  headers: { 'Content-Type': 'application/json' },
202
202
  body: JSON.stringify({ timestamp })
203
203
  })
204
+
205
+ // Handle session expiration
206
+ if (res.status === 401) {
207
+ this.addLog('Session expired. Redirecting to login...', 'error')
208
+ setTimeout(() => window.location.href = '/backup/login', 1500)
209
+ return
210
+ }
211
+
204
212
  const data = await res.json()
205
213
  if (data.status === 'success') {
206
214
  this.lastBackup = new Date().toLocaleString()
@@ -220,14 +228,31 @@ export const backupAppScript = ({ config, jobStatus }) => `
220
228
  this.loadingFiles = true
221
229
  try {
222
230
  const res = await fetch('/backup/api/files')
231
+
232
+ // Handle session expiration
233
+ if (res.status === 401) {
234
+ this.addLog('Session expired. Redirecting to login...', 'error')
235
+ setTimeout(() => window.location.href = '/backup/login', 1500)
236
+ return
237
+ }
238
+
223
239
  const data = await res.json()
240
+
241
+ // Check for error response
242
+ if (data.status === 'error') {
243
+ throw new Error(data.message)
244
+ }
245
+
224
246
  if (data.files) {
225
247
  const sortedFiles = data.files.sort((a, b) => b.key.localeCompare(a.key));
226
248
 
227
249
  const groupsMap = {};
228
250
  sortedFiles.forEach(file => {
229
- const match = file.key.match(/(?:^|\\/)(\\d{4}-\\d{2}-\\d{2})[_T]/);
230
- const dateKey = match ? match[1] : 'Others';
251
+ // Try to extract date from folder path first (new format: YYYY-MM-DD/)
252
+ // Then fall back to extracting from filename (legacy format: YYYY-MM-DD_HH-mm-ss_filename)
253
+ const folderMatch = file.key.match(/(?:^|\\/)(\\d{4}-\\d{2}-\\d{2})\\//);
254
+ const filenameMatch = file.key.match(/(?:^|\\/)(\\d{4}-\\d{2}-\\d{2})[_T]/);
255
+ const dateKey = folderMatch ? folderMatch[1] : (filenameMatch ? filenameMatch[1] : 'Others');
231
256
 
232
257
  if (!groupsMap[dateKey]) {
233
258
  groupsMap[dateKey] = [];