diff --git a/cwd-admin/src/api/admin.ts b/cwd-admin/src/api/admin.ts index 5b6b5cd..c0e0405 100644 --- a/cwd-admin/src/api/admin.ts +++ b/cwd-admin/src/api/admin.ts @@ -408,3 +408,21 @@ export function saveS3Settings(data: S3SettingsResponse): Promise<{ message: str export function triggerS3Backup(): Promise<{ message: string; file: string }> { return post<{ message: string; file: string }>('/admin/backup/s3', {}); } + +export type S3BackupItem = { + key: string; + size: number; + lastModified: string; +}; + +export function fetchS3BackupList(): Promise<{ files: S3BackupItem[] }> { + return get<{ files: S3BackupItem[] }>('/admin/backup/s3/list'); +} + +export function deleteS3Backup(key: string): Promise<{ message: string }> { + return del<{ message: string }>(`/admin/backup/s3?key=${encodeURIComponent(key)}`); +} + +export function downloadS3BackupUrl(key: string): string { + return `/admin/backup/s3/download?key=${encodeURIComponent(key)}`; +} diff --git a/cwd-admin/src/locales/ar.json b/cwd-admin/src/locales/ar.json index f0ed0d9..ba5e697 100644 --- a/cwd-admin/src/locales/ar.json +++ b/cwd-admin/src/locales/ar.json @@ -130,7 +130,15 @@ "saving": "جارٍ الحفظ...", "backup": "النسخ الاحتياطي إلى S3 الآن", "backingUp": "جارٍ النسخ الاحتياطي...", - "success": "نجح النسخ الاحتياطي! الملف: {file}" + "success": "نجح النسخ الاحتياطي! الملف: {file}", + "viewBackups": "عرض النسخ الاحتياطية", + "loadingBackups": "جارٍ التحميل...", + "backupListTitle": "قائمة نسخ S3 الاحتياطية", + "emptyBackupList": "لا توجد ملفات نسخ احتياطي", + "download": "تحميل", + "delete": "حذف", + "confirmDelete": "هل أنت متأكد من أنك تريد حذف النسخ الاحتياطي {file}؟", + "deleteSuccess": "تم الحذف بنجاح" } }, "logs": { diff --git a/cwd-admin/src/locales/de.json b/cwd-admin/src/locales/de.json index b855544..ea23703 100644 --- a/cwd-admin/src/locales/de.json +++ b/cwd-admin/src/locales/de.json @@ -130,7 +130,15 @@ "saving": "Speichert...", "backup": "Jetzt zu S3 sichern", "backingUp": "Sichere...", - "success": "Backup erfolgreich! Datei: {file}" + "success": "Backup erfolgreich! Datei: {file}", + "viewBackups": "Backups anzeigen", + "loadingBackups": "Laden...", + "backupListTitle": "S3 Backup-Liste", + "emptyBackupList": "Keine Backup-Dateien", + "download": "Herunterladen", + "delete": "Löschen", + "confirmDelete": "Sind Sie sicher, dass Sie das Backup {file} löschen möchten?", + "deleteSuccess": "Erfolgreich gelöscht" } }, "logs": { diff --git a/cwd-admin/src/locales/en-US.json b/cwd-admin/src/locales/en-US.json index 5b2b05e..0cb6813 100644 --- a/cwd-admin/src/locales/en-US.json +++ b/cwd-admin/src/locales/en-US.json @@ -130,7 +130,15 @@ "saving": "Saving...", "backup": "Backup to S3 Now", "backingUp": "Backing up...", - "success": "Backup successful! File: {file}" + "success": "Backup successful! File: {file}", + "viewBackups": "View Backups", + "loadingBackups": "Loading...", + "backupListTitle": "S3 Backup List", + "emptyBackupList": "No backup files", + "download": "Download", + "delete": "Delete", + "confirmDelete": "Are you sure you want to delete backup {file}?", + "deleteSuccess": "Deleted successfully" } }, "logs": { diff --git a/cwd-admin/src/locales/es.json b/cwd-admin/src/locales/es.json index c140914..e97c59a 100644 --- a/cwd-admin/src/locales/es.json +++ b/cwd-admin/src/locales/es.json @@ -130,7 +130,15 @@ "saving": "Guardando...", "backup": "Copiar a S3 Ahora", "backingUp": "Copiando...", - "success": "¡Copia de seguridad exitosa! Archivo: {file}" + "success": "¡Copia de seguridad exitosa! Archivo: {file}", + "viewBackups": "Ver copias de seguridad", + "loadingBackups": "Cargando...", + "backupListTitle": "Lista de copias de seguridad S3", + "emptyBackupList": "No hay archivos de copia de seguridad", + "download": "Descargar", + "delete": "Eliminar", + "confirmDelete": "¿Estás seguro de que quieres eliminar la copia de seguridad {file}?", + "deleteSuccess": "Eliminado exitosamente" } }, "logs": { diff --git a/cwd-admin/src/locales/fr.json b/cwd-admin/src/locales/fr.json index 9e192a6..51baeeb 100644 --- a/cwd-admin/src/locales/fr.json +++ b/cwd-admin/src/locales/fr.json @@ -130,7 +130,15 @@ "saving": "Sauvegarde...", "backup": "Sauvegarder vers S3 Maintenant", "backingUp": "Sauvegarde en cours...", - "success": "Sauvegarde réussie ! Fichier: {file}" + "success": "Sauvegarde réussie ! Fichier: {file}", + "viewBackups": "Voir les sauvegardes", + "loadingBackups": "Chargement...", + "backupListTitle": "Liste des sauvegardes S3", + "emptyBackupList": "Aucun fichier de sauvegarde", + "download": "Télécharger", + "delete": "Supprimer", + "confirmDelete": "Êtes-vous sûr de vouloir supprimer la sauvegarde {file} ?", + "deleteSuccess": "Supprimé avec succès" } }, "logs": { diff --git a/cwd-admin/src/locales/hi.json b/cwd-admin/src/locales/hi.json index 514fe4a..cc6bc68 100644 --- a/cwd-admin/src/locales/hi.json +++ b/cwd-admin/src/locales/hi.json @@ -130,7 +130,15 @@ "saving": "सहेजा जा रहा है...", "backup": "अभी S3 पर बैकअप करें", "backingUp": "बैकअप हो रहा है...", - "success": "बैकअप सफल! फ़ाइल: {file}" + "success": "बैकअप सफल! फ़ाइल: {file}", + "viewBackups": "बैकअप देखें", + "loadingBackups": "लोड हो रहा है...", + "backupListTitle": "S3 बैकअप सूची", + "emptyBackupList": "कोई बैकअप फ़ाइल नहीं", + "download": "डाउनलोड", + "delete": "हटाएं", + "confirmDelete": "क्या आप वाकई बैकअप {file} हटाना चाहते हैं?", + "deleteSuccess": "सफलतापूर्वक हटाया गया" } }, "logs": { diff --git a/cwd-admin/src/locales/id.json b/cwd-admin/src/locales/id.json index 39a1852..bf7165c 100644 --- a/cwd-admin/src/locales/id.json +++ b/cwd-admin/src/locales/id.json @@ -130,7 +130,15 @@ "saving": "Menyimpan...", "backup": "Cadangkan ke S3 Sekarang", "backingUp": "Mencadangkan...", - "success": "Cadangan berhasil! File: {file}" + "success": "Cadangan berhasil! File: {file}", + "viewBackups": "Lihat Cadangan", + "loadingBackups": "Memuat...", + "backupListTitle": "Daftar Cadangan S3", + "emptyBackupList": "Tidak ada file cadangan", + "download": "Unduh", + "delete": "Hapus", + "confirmDelete": "Apakah Anda yakin ingin menghapus cadangan {file}?", + "deleteSuccess": "Berhasil dihapus" } }, "logs": { diff --git a/cwd-admin/src/locales/it.json b/cwd-admin/src/locales/it.json index bc90049..3979906 100644 --- a/cwd-admin/src/locales/it.json +++ b/cwd-admin/src/locales/it.json @@ -130,7 +130,15 @@ "saving": "Salvataggio...", "backup": "Backup su S3 Ora", "backingUp": "Backup in corso...", - "success": "Backup riuscito! File: {file}" + "success": "Backup riuscito! File: {file}", + "viewBackups": "Visualizza Backup", + "loadingBackups": "Caricamento...", + "backupListTitle": "Lista Backup S3", + "emptyBackupList": "Nessun file di backup", + "download": "Scarica", + "delete": "Elimina", + "confirmDelete": "Sei sicuro di voler eliminare il backup {file}?", + "deleteSuccess": "Eliminato con successo" } }, "logs": { diff --git a/cwd-admin/src/locales/ja.json b/cwd-admin/src/locales/ja.json index cf0c283..d167a2b 100644 --- a/cwd-admin/src/locales/ja.json +++ b/cwd-admin/src/locales/ja.json @@ -130,7 +130,15 @@ "saving": "保存中...", "backup": "今すぐ S3 にバックアップ", "backingUp": "バックアップ中...", - "success": "バックアップ成功!ファイル: {file}" + "success": "バックアップ成功!ファイル: {file}", + "viewBackups": "バックアップを表示", + "loadingBackups": "読み込み中...", + "backupListTitle": "S3 バックアップ一覧", + "emptyBackupList": "バックアップファイルがありません", + "download": "ダウンロード", + "delete": "削除", + "confirmDelete": "バックアップ {file} を削除してもよろしいですか?", + "deleteSuccess": "削除しました" } }, "logs": { diff --git a/cwd-admin/src/locales/ko.json b/cwd-admin/src/locales/ko.json index f71a4da..25a149c 100644 --- a/cwd-admin/src/locales/ko.json +++ b/cwd-admin/src/locales/ko.json @@ -130,7 +130,15 @@ "saving": "저장 중...", "backup": "지금 S3에 백업", "backingUp": "백업 중...", - "success": "백업 성공! 파일: {file}" + "success": "백업 성공! 파일: {file}", + "viewBackups": "백업 보기", + "loadingBackups": "로딩 중...", + "backupListTitle": "S3 백업 목록", + "emptyBackupList": "백업 파일 없음", + "download": "다운로드", + "delete": "삭제", + "confirmDelete": "백업 {file}을(를) 삭제하시겠습니까?", + "deleteSuccess": "삭제 성공" } }, "logs": { diff --git a/cwd-admin/src/locales/nl.json b/cwd-admin/src/locales/nl.json index aff0f97..cc9a252 100644 --- a/cwd-admin/src/locales/nl.json +++ b/cwd-admin/src/locales/nl.json @@ -130,7 +130,15 @@ "saving": "Opslaan...", "backup": "Nu naar S3 back-uppen", "backingUp": "Back-uppen...", - "success": "Back-up geslaagd! Bestand: {file}" + "success": "Back-up geslaagd! Bestand: {file}", + "viewBackups": "Back-ups bekijken", + "loadingBackups": "Laden...", + "backupListTitle": "S3 Back-up lijst", + "emptyBackupList": "Geen back-up bestanden", + "download": "Downloaden", + "delete": "Verwijderen", + "confirmDelete": "Weet u zeker dat u back-up {file} wilt verwijderen?", + "deleteSuccess": "Succesvol verwijderd" } }, "logs": { diff --git a/cwd-admin/src/locales/pt.json b/cwd-admin/src/locales/pt.json index 5cf84a1..0adf4a3 100644 --- a/cwd-admin/src/locales/pt.json +++ b/cwd-admin/src/locales/pt.json @@ -130,7 +130,15 @@ "saving": "Salvando...", "backup": "Fazer Backup para S3 Agora", "backingUp": "Fazendo Backup...", - "success": "Backup bem-sucedido! Arquivo: {file}" + "success": "Backup bem-sucedido! Arquivo: {file}", + "viewBackups": "Ver Backups", + "loadingBackups": "Carregando...", + "backupListTitle": "Lista de Backups S3", + "emptyBackupList": "Nenhum arquivo de backup", + "download": "Baixar", + "delete": "Excluir", + "confirmDelete": "Tem certeza de que deseja excluir o backup {file}?", + "deleteSuccess": "Excluído com sucesso" } }, "logs": { diff --git a/cwd-admin/src/locales/ru.json b/cwd-admin/src/locales/ru.json index ebc83f3..8ee72dd 100644 --- a/cwd-admin/src/locales/ru.json +++ b/cwd-admin/src/locales/ru.json @@ -130,7 +130,15 @@ "saving": "Сохранение...", "backup": "Сделать бэкап в S3", "backingUp": "Бэкап...", - "success": "Бэкап успешен! Файл: {file}" + "success": "Бэкап успешен! Файл: {file}", + "viewBackups": "Просмотр бэкапов", + "loadingBackups": "Загрузка...", + "backupListTitle": "Список бэкапов S3", + "emptyBackupList": "Нет файлов бэкапов", + "download": "Скачать", + "delete": "Удалить", + "confirmDelete": "Вы уверены, что хотите удалить бэкап {file}?", + "deleteSuccess": "Успешно удалено" } }, "logs": { diff --git a/cwd-admin/src/locales/zh-CN.json b/cwd-admin/src/locales/zh-CN.json index e8ca2e3..7bf01e2 100644 --- a/cwd-admin/src/locales/zh-CN.json +++ b/cwd-admin/src/locales/zh-CN.json @@ -130,7 +130,15 @@ "saving": "保存中...", "backup": "立即备份到 S3", "backingUp": "正在备份...", - "success": "备份成功!文件: {file}" + "success": "备份成功!文件: {file}", + "viewBackups": "查看备份", + "loadingBackups": "加载中...", + "backupListTitle": "S3 备份列表", + "emptyBackupList": "暂无备份文件", + "download": "下载", + "delete": "删除", + "confirmDelete": "确定要删除备份 {file} 吗?", + "deleteSuccess": "删除成功" } }, "logs": { diff --git a/cwd-admin/src/locales/zh-TW.json b/cwd-admin/src/locales/zh-TW.json index cd350ab..d104353 100644 --- a/cwd-admin/src/locales/zh-TW.json +++ b/cwd-admin/src/locales/zh-TW.json @@ -130,7 +130,15 @@ "saving": "保存中...", "backup": "立即備份到 S3", "backingUp": "正在備份...", - "success": "備份成功!文件: {file}" + "success": "備份成功!文件: {file}", + "viewBackups": "查看備份", + "loadingBackups": "加載中...", + "backupListTitle": "S3 備份列表", + "emptyBackupList": "暫無備份文件", + "download": "下載", + "delete": "刪除", + "confirmDelete": "確定要刪除備份 {file} 嗎?", + "deleteSuccess": "刪除成功" } }, "logs": { diff --git a/cwd-admin/src/views/DataView/components/S3BackupModal.vue b/cwd-admin/src/views/DataView/components/S3BackupModal.vue new file mode 100644 index 0000000..3669341 --- /dev/null +++ b/cwd-admin/src/views/DataView/components/S3BackupModal.vue @@ -0,0 +1,314 @@ + + + + + diff --git a/cwd-admin/src/views/DataView/index.vue b/cwd-admin/src/views/DataView/index.vue index 8a0a858..275ab91 100644 --- a/cwd-admin/src/views/DataView/index.vue +++ b/cwd-admin/src/views/DataView/index.vue @@ -162,6 +162,13 @@ s3BackingUp ? t("data.sections.s3.backingUp") : t("data.sections.s3.backup") }} + @@ -213,6 +220,12 @@ + + + @@ -231,8 +244,9 @@ import { fetchS3Settings, saveS3Settings, triggerS3Backup, - type S3SettingsResponse, + S3SettingsResponse, } from "../../api/admin"; +import S3BackupModal from "./components/S3BackupModal.vue"; import { useSite } from "../../composables/useSite"; const { t } = useI18n(); @@ -264,6 +278,7 @@ const s3Config = ref({ }); const s3Saving = ref(false); const s3BackingUp = ref(false); +const showS3BackupModal = ref(false); async function loadS3Config() { try { @@ -298,6 +313,14 @@ async function handleS3Backup() { } } +function handleViewS3Backups() { + showS3BackupModal.value = true; +} + +function handleS3BackupModalClose() { + showS3BackupModal.value = false; +} + onMounted(() => { loadS3Config(); }); @@ -552,4 +575,93 @@ function joinUrl(prefix: string, path: string): string { margin-bottom: 4px; color: var(--text-primary); } + +/* S3 备份弹窗样式 */ +.s3-backup-modal { + max-width: 600px; + width: 90%; + max-height: 70vh; + display: flex; + flex-direction: column; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border-color); + padding-bottom: 5px; +} + +.modal-close { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: var(--text-secondary); + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s; +} + +.modal-close:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.modal-content { + overflow-y: auto; + flex: 1; +} + +.empty-backup-list { + text-align: center; + padding: 40px; + color: var(--text-secondary); +} + +.backup-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.backup-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-secondary); + gap: 12px; +} + +.backup-info { + flex: 1; + min-width: 0; +} + +.backup-name { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 4px; +} + +.backup-meta { + display: flex; + gap: 12px; + font-size: 12px; + color: var(--text-secondary); +} + diff --git a/cwd-api/src/api/admin/triggerS3Backup.ts b/cwd-api/src/api/admin/triggerS3Backup.ts index 365623d..927c091 100644 --- a/cwd-api/src/api/admin/triggerS3Backup.ts +++ b/cwd-api/src/api/admin/triggerS3Backup.ts @@ -5,13 +5,27 @@ import { S3Client } from '../../utils/s3'; import { getConfigs } from './exportConfig'; import { getStatsData } from './exportStats'; +async function getS3Client(c: Context<{ Bindings: Bindings }>): Promise<{ s3: S3Client; settings: any } | Response> { + const settings = await loadS3Settings(c.env); + if (!settings.endpoint || !settings.bucket || !settings.accessKeyId || !settings.secretAccessKey) { + return c.json({ message: 'S3 配置不完整,请先配置 S3 信息' }, 400); + } + const s3 = new S3Client({ + endpoint: settings.endpoint, + accessKeyId: settings.accessKeyId, + secretAccessKey: settings.secretAccessKey, + bucket: settings.bucket, + region: settings.region, + }); + return { s3, settings }; +} + export async function triggerS3Backup(c: Context<{ Bindings: Bindings }>) { try { // 1. Load S3 Settings - const settings = await loadS3Settings(c.env); - if (!settings.endpoint || !settings.bucket || !settings.accessKeyId || !settings.secretAccessKey) { - return c.json({ message: 'S3 配置不完整,请先配置 S3 信息' }, 400); - } + const result = await getS3Client(c); + if (result instanceof Response) return result; + const { s3 } = result; // 2. Gather Backup Data (Logic from exportBackup.ts) const { results: comments } = await c.env.CWD_DB.prepare('SELECT * FROM Comment ORDER BY priority DESC, created DESC').all(); @@ -33,14 +47,6 @@ export async function triggerS3Backup(c: Context<{ Bindings: Bindings }>) { const fileName = `cwd-backup-${dateStr}-${Date.now()}.json`; // 3. Upload to S3 - const s3 = new S3Client({ - endpoint: settings.endpoint, - accessKeyId: settings.accessKeyId, - secretAccessKey: settings.secretAccessKey, - bucket: settings.bucket, - region: settings.region, - }); - await s3.putObject(fileName, jsonString); return c.json({ @@ -52,3 +58,62 @@ export async function triggerS3Backup(c: Context<{ Bindings: Bindings }>) { return c.json({ message: e.message || 'S3 备份失败' }, 500); } } + +export async function listS3Backups(c: Context<{ Bindings: Bindings }>) { + try { + const result = await getS3Client(c); + if (result instanceof Response) return result; + const { s3 } = result; + + const files = await s3.listObjects('cwd-backup-'); + return c.json({ files }); + } catch (e: any) { + console.error('S3 List Backups Error:', e); + return c.json({ message: e.message || '获取备份列表失败' }, 500); + } +} + +export async function deleteS3BackupHandler(c: Context<{ Bindings: Bindings }>) { + try { + const key = c.req.query('key'); + if (!key) { + return c.json({ message: '缺少 key 参数' }, 400); + } + + const result = await getS3Client(c); + if (result instanceof Response) return result; + const { s3 } = result; + + await s3.deleteObject(key); + return c.json({ message: '删除成功' }); + } catch (e: any) { + console.error('S3 Delete Backup Error:', e); + return c.json({ message: e.message || '删除备份失败' }, 500); + } +} + +export async function downloadS3BackupHandler(c: Context<{ Bindings: Bindings }>) { + try { + const key = c.req.query('key'); + if (!key) { + return c.json({ message: '缺少 key 参数' }, 400); + } + + const result = await getS3Client(c); + if (result instanceof Response) return result; + const { s3 } = result; + + const s3Response = await s3.getObject(key); + const body = await s3Response.arrayBuffer(); + + // Set headers for file download + c.header('Content-Type', 'application/json'); + c.header('Content-Disposition', `attachment; filename="${key}"`); + c.header('Content-Length', String(body.byteLength)); + + return c.body(body); + } catch (e: any) { + console.error('S3 Download Backup Error:', e); + return c.json({ message: e.message || '下载备份失败' }, 500); + } +} diff --git a/cwd-api/src/index.ts b/cwd-api/src/index.ts index 93bc5b7..7de8e67 100644 --- a/cwd-api/src/index.ts +++ b/cwd-api/src/index.ts @@ -36,7 +36,7 @@ import { getLikeStats } from './api/admin/likeStats'; import { getFeatureSettings, updateFeatureSettings } from './api/admin/featureSettings'; import { getTelegramSettings, updateTelegramSettings, setupTelegramWebhook, testTelegramMessage } from './api/admin/telegramSettings'; import { getS3Settings, updateS3Settings } from './api/admin/s3Settings'; -import { triggerS3Backup } from './api/admin/triggerS3Backup'; +import { triggerS3Backup, listS3Backups, deleteS3BackupHandler, downloadS3BackupHandler } from './api/admin/triggerS3Backup'; import { telegramWebhook } from './api/telegram/webhook'; import { ensureSchema } from './utils/dbMigration'; @@ -345,6 +345,9 @@ app.post('/admin/settings/telegram/test', testTelegramMessage); app.get('/admin/settings/s3', getS3Settings); app.put('/admin/settings/s3', updateS3Settings); app.post('/admin/backup/s3', triggerS3Backup); +app.get('/admin/backup/s3/list', listS3Backups); +app.delete('/admin/backup/s3', deleteS3BackupHandler); +app.get('/admin/backup/s3/download', downloadS3BackupHandler); app.get('/admin/settings/admin-display', async (c) => { try { diff --git a/cwd-api/src/utils/s3.ts b/cwd-api/src/utils/s3.ts index 5cf6083..739bbee 100644 --- a/cwd-api/src/utils/s3.ts +++ b/cwd-api/src/utils/s3.ts @@ -8,6 +8,12 @@ export interface S3Config { region?: string; } +export interface S3Object { + key: string; + size: number; + lastModified: string; +} + export class S3Client { private client: AwsClient; private bucket: string; @@ -49,4 +55,74 @@ export class S3Client { return res; } + + async listObjects(prefix?: string): Promise { + let url = `${this.endpoint}/${this.bucket}?list-type=2`; + if (prefix) { + url += `&prefix=${encodeURIComponent(prefix)}`; + } + + const res = await this.client.fetch(url, { + method: 'GET', + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`S3 List Failed: ${res.status} ${res.statusText} - ${text}`); + } + + const xml = await res.text(); + return this.parseListObjectsXml(xml); + } + + async deleteObject(key: string): Promise { + const url = `${this.endpoint}/${this.bucket}/${key}`; + const res = await this.client.fetch(url, { + method: 'DELETE', + }); + + if (!res.ok && res.status !== 204) { + const text = await res.text(); + throw new Error(`S3 Delete Failed: ${res.status} ${res.statusText} - ${text}`); + } + } + + async getObject(key: string): Promise { + const url = `${this.endpoint}/${this.bucket}/${key}`; + const res = await this.client.fetch(url, { + method: 'GET', + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`S3 Get Failed: ${res.status} ${res.statusText} - ${text}`); + } + + return res; + } + + private parseListObjectsXml(xml: string): S3Object[] { + const objects: S3Object[] = []; + const contentsRegex = /([\s\S]*?)<\/Contents>/g; + let match; + + while ((match = contentsRegex.exec(xml)) !== null) { + const content = match[1]; + const keyMatch = content.match(/([^<]*)<\/Key>/); + const sizeMatch = content.match(/(\d+)<\/Size>/); + const lastModifiedMatch = content.match(/([^<]*)<\/LastModified>/); + + if (keyMatch && sizeMatch && lastModifiedMatch) { + objects.push({ + key: keyMatch[1], + size: parseInt(sizeMatch[1], 10), + lastModified: lastModifiedMatch[1], + }); + } + } + + return objects.sort((a, b) => + new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime() + ); + } }