diff --git a/plugins/setting-assets/lang/cs.json b/plugins/setting-assets/lang/cs.json index 80886f8acbc..c229c0e5986 100644 --- a/plugins/setting-assets/lang/cs.json +++ b/plugins/setting-assets/lang/cs.json @@ -135,6 +135,12 @@ "BackupFileDownload": "Stáhnout soubor", "BackupFiles": "Soubory záloh", "BackupNoBackup": "V současné době nejsou k dispozici žádné zálohy.", + "BackupDownloadAll": "Stáhnout úplnou zálohu", + "BackupPreparingDownload": "Příprava zálohy…", + "BackupDownloadAllInfo": "Stáhněte všechny soubory zálohy jako jeden archiv .zip, který si můžete uložit do počítače.", + "BackupCopyScript": "Kopírovat skript pro stažení", + "BackupCopyToken": "Kopírovat token", + "BackupScriptInfo": "Shellový skript, který stáhne všechny soubory zálohy pomocí curl. Uložte jej a spusťte v terminálu; vyžádá si váš token zálohy, takže ve skriptu nejsou uložena žádná tajemství.", "NonBackupedBlobs": "Non Backed up blobs", "Calendar": "Kalendář", "StartOfTheWeek": "Začátek týdne", diff --git a/plugins/setting-assets/lang/de.json b/plugins/setting-assets/lang/de.json index 913c868e6e6..8e71bfdac8b 100644 --- a/plugins/setting-assets/lang/de.json +++ b/plugins/setting-assets/lang/de.json @@ -141,6 +141,12 @@ "BackupFileDownload": "Datei herunterladen", "BackupFiles": "Sicherungsdateien", "BackupNoBackup": "Derzeit sind keine Sicherungen verfügbar.", + "BackupDownloadAll": "Vollständige Sicherung herunterladen", + "BackupPreparingDownload": "Sicherung wird vorbereitet…", + "BackupDownloadAllInfo": "Lade alle Sicherungsdateien als ein einzelnes .zip-Archiv herunter, das du auf deinem Computer aufbewahren kannst.", + "BackupCopyScript": "Download-Skript kopieren", + "BackupCopyToken": "Token kopieren", + "BackupScriptInfo": "Ein Shell-Skript, das alle Sicherungsdateien mit curl herunterlädt. Speichere es und führe es in einem Terminal aus; es fragt nach deinem Sicherungs-Token, sodass keine Geheimnisse im Skript gespeichert werden.", "NonBackupedBlobs": "Nicht gesicherte Blobs", "Calendar": "Kalender", "StartOfTheWeek": "Wochenstart", diff --git a/plugins/setting-assets/lang/en.json b/plugins/setting-assets/lang/en.json index fc68364325e..ddbb8817a54 100644 --- a/plugins/setting-assets/lang/en.json +++ b/plugins/setting-assets/lang/en.json @@ -141,6 +141,12 @@ "BackupFileDownload": "Download File", "BackupFiles": "Backup Files", "BackupNoBackup": "No backups are currently available.", + "BackupDownloadAll": "Download full backup", + "BackupPreparingDownload": "Preparing backup…", + "BackupDownloadAllInfo": "Download every backup file as a single .zip archive you can keep on your computer.", + "BackupCopyScript": "Copy download script", + "BackupCopyToken": "Copy token", + "BackupScriptInfo": "A shell script that downloads every backup file with curl. Save it and run it in a terminal; it prompts for your backup token, so no secrets are stored in the script.", "NonBackupedBlobs": "Non Backed up blobs", "Calendar": "Calendar", "StartOfTheWeek": "Start of the week", diff --git a/plugins/setting-assets/lang/es.json b/plugins/setting-assets/lang/es.json index d1873d126b4..fa317c7f55d 100644 --- a/plugins/setting-assets/lang/es.json +++ b/plugins/setting-assets/lang/es.json @@ -132,6 +132,12 @@ "BackupFileDownload": "Descargar archivo", "BackupFiles": "Archivos de copia de seguridad", "BackupNoBackup": "No hay copias de seguridad disponibles actualmente.", + "BackupDownloadAll": "Descargar copia de seguridad completa", + "BackupPreparingDownload": "Preparando la copia de seguridad…", + "BackupDownloadAllInfo": "Descarga todos los archivos de la copia de seguridad en un único archivo .zip que puedes guardar en tu ordenador.", + "BackupCopyScript": "Copiar script de descarga", + "BackupCopyToken": "Copiar token", + "BackupScriptInfo": "Un script de shell que descarga todos los archivos de la copia de seguridad con curl. Guárdalo y ejecútalo en una terminal; te pedirá tu token de copia de seguridad, por lo que no se almacenan secretos en el script.", "NonBackupedBlobs": "Blobs no respaldados", "Calendar": "Calendario", "StartOfTheWeek": "Inicio de la semana", diff --git a/plugins/setting-assets/lang/fr.json b/plugins/setting-assets/lang/fr.json index 73e77fa3e6a..55edcb74b5d 100644 --- a/plugins/setting-assets/lang/fr.json +++ b/plugins/setting-assets/lang/fr.json @@ -141,6 +141,12 @@ "BackupFileDownload": "Télécharger le fichier", "BackupFiles": "Fichiers de sauvegarde", "BackupNoBackup": "Aucune sauvegarde n'est actuellement disponible.", + "BackupDownloadAll": "Télécharger la sauvegarde complète", + "BackupPreparingDownload": "Préparation de la sauvegarde…", + "BackupDownloadAllInfo": "Téléchargez tous les fichiers de sauvegarde dans une seule archive .zip que vous pouvez conserver sur votre ordinateur.", + "BackupCopyScript": "Copier le script de téléchargement", + "BackupCopyToken": "Copier le jeton", + "BackupScriptInfo": "Un script shell qui télécharge tous les fichiers de sauvegarde avec curl. Enregistrez-le et exécutez-le dans un terminal ; il demande votre jeton de sauvegarde, aucun secret n'est donc stocké dans le script.", "NonBackupedBlobs": "Non Backed up blobs", "Calendar": "Calendrier", "StartOfTheWeek": "Début de semaine", diff --git a/plugins/setting-assets/lang/it.json b/plugins/setting-assets/lang/it.json index 319705e9f5d..4f24589b2e4 100644 --- a/plugins/setting-assets/lang/it.json +++ b/plugins/setting-assets/lang/it.json @@ -141,6 +141,12 @@ "BackupFileDownload": "Scarica file", "BackupFiles": "File di backup", "BackupNoBackup": "Non sono attualmente disponibili backup.", + "BackupDownloadAll": "Scarica backup completo", + "BackupPreparingDownload": "Preparazione del backup…", + "BackupDownloadAllInfo": "Scarica tutti i file di backup in un unico archivio .zip che puoi conservare sul tuo computer.", + "BackupCopyScript": "Copia script di download", + "BackupCopyToken": "Copia token", + "BackupScriptInfo": "Uno script shell che scarica tutti i file di backup con curl. Salvalo ed eseguilo in un terminale; richiederà il tuo token di backup, quindi nello script non vengono memorizzati segreti.", "NonBackupedBlobs": "Non Backed up blobs", "Calendar": "Calendario", "StartOfTheWeek": "Inizio settimana", diff --git a/plugins/setting-assets/lang/ja.json b/plugins/setting-assets/lang/ja.json index 6cac7cc427b..1cda80b5510 100644 --- a/plugins/setting-assets/lang/ja.json +++ b/plugins/setting-assets/lang/ja.json @@ -141,6 +141,12 @@ "BackupFileDownload": "ファイルをダウンロード", "BackupFiles": "バックアップファイル", "BackupNoBackup": "現在、利用可能なバックアップはありません。", + "BackupDownloadAll": "完全バックアップをダウンロード", + "BackupPreparingDownload": "バックアップを準備中…", + "BackupDownloadAllInfo": "すべてのバックアップファイルを 1 つの .zip アーカイブとしてダウンロードし、コンピューターに保存できます。", + "BackupCopyScript": "ダウンロードスクリプトをコピー", + "BackupCopyToken": "トークンをコピー", + "BackupScriptInfo": "curl ですべてのバックアップファイルをダウンロードするシェルスクリプトです。保存してターミナルで実行してください。バックアップトークンの入力を求められるため、スクリプトに機密情報は保存されません。", "NonBackupedBlobs": "バックアップされていないブロブ", "Calendar": "カレンダー", "StartOfTheWeek": "週の開始", diff --git a/plugins/setting-assets/lang/ko.json b/plugins/setting-assets/lang/ko.json index f673b24756f..544115e8322 100644 --- a/plugins/setting-assets/lang/ko.json +++ b/plugins/setting-assets/lang/ko.json @@ -141,6 +141,12 @@ "BackupFileDownload": "파일 다운로드", "BackupFiles": "백업 파일", "BackupNoBackup": "현재 사용 가능한 백업이 없습니다.", + "BackupDownloadAll": "전체 백업 다운로드", + "BackupPreparingDownload": "백업 준비 중…", + "BackupDownloadAllInfo": "모든 백업 파일을 컴퓨터에 보관할 수 있는 단일 .zip 아카이브로 다운로드합니다.", + "BackupCopyScript": "다운로드 스크립트 복사", + "BackupCopyToken": "토큰 복사", + "BackupScriptInfo": "curl로 모든 백업 파일을 다운로드하는 셸 스크립트입니다. 저장한 후 터미널에서 실행하세요. 백업 토큰을 입력하라는 메시지가 표시되므로 스크립트에 비밀 정보가 저장되지 않습니다.", "NonBackupedBlobs": "백업되지 않은 Blob", "Calendar": "캘린더", "StartOfTheWeek": "주 시작일", diff --git a/plugins/setting-assets/lang/pl.json b/plugins/setting-assets/lang/pl.json index 1e0b32e4d79..ce6d6db8a8e 100644 --- a/plugins/setting-assets/lang/pl.json +++ b/plugins/setting-assets/lang/pl.json @@ -141,6 +141,12 @@ "BackupFileDownload": "Pobierz plik", "BackupFiles": "Pliki kopii zapasowej", "BackupNoBackup": "Aktualnie brak kopii zapasowych.", + "BackupDownloadAll": "Pobierz pełną kopię zapasową", + "BackupPreparingDownload": "Przygotowywanie kopii zapasowej…", + "BackupDownloadAllInfo": "Pobierz wszystkie pliki kopii zapasowej jako jedno archiwum .zip, które możesz zachować na komputerze.", + "BackupCopyScript": "Kopiuj skrypt pobierania", + "BackupCopyToken": "Kopiuj token", + "BackupScriptInfo": "Skrypt powłoki, który pobiera wszystkie pliki kopii zapasowej za pomocą curl. Zapisz go i uruchom w terminalu; poprosi o token kopii zapasowej, więc w skrypcie nie są przechowywane żadne sekrety.", "NonBackupedBlobs": "Pliki nieobjęte kopią zapasową", "Calendar": "Kalendarz", "StartOfTheWeek": "Początek tygodnia", diff --git a/plugins/setting-assets/lang/pt-br.json b/plugins/setting-assets/lang/pt-br.json index 94f98fc645d..8dda982a87e 100644 --- a/plugins/setting-assets/lang/pt-br.json +++ b/plugins/setting-assets/lang/pt-br.json @@ -132,6 +132,12 @@ "BackupFileDownload": "Baixar Arquivo", "BackupFiles": "Arquivos de Backup", "BackupNoBackup": "Não há backups disponíveis no momento.", + "BackupDownloadAll": "Baixar backup completo", + "BackupPreparingDownload": "Preparando backup…", + "BackupDownloadAllInfo": "Baixe todos os arquivos de backup em um único arquivo .zip que você pode guardar no seu computador.", + "BackupCopyScript": "Copiar script de download", + "BackupCopyToken": "Copiar token", + "BackupScriptInfo": "Um script de shell que baixa todos os arquivos de backup com curl. Salve-o e execute-o em um terminal; ele solicitará seu token de backup, então nenhum segredo é armazenado no script.", "NonBackupedBlobs": "Blobs sem backup", "Calendar": "Calendário", "StartOfTheWeek": "Início da semana", diff --git a/plugins/setting-assets/lang/pt.json b/plugins/setting-assets/lang/pt.json index 047873476cc..875336df9f6 100644 --- a/plugins/setting-assets/lang/pt.json +++ b/plugins/setting-assets/lang/pt.json @@ -132,6 +132,12 @@ "BackupFileDownload": "Baixar Arquivo", "BackupFiles": "Arquivos de Backup", "BackupNoBackup": "Não há backups disponíveis no momento.", + "BackupDownloadAll": "Transferir cópia de segurança completa", + "BackupPreparingDownload": "A preparar a cópia de segurança…", + "BackupDownloadAllInfo": "Transfira todos os ficheiros da cópia de segurança num único arquivo .zip que pode guardar no seu computador.", + "BackupCopyScript": "Copiar script de transferência", + "BackupCopyToken": "Copiar token", + "BackupScriptInfo": "Um script de shell que transfere todos os ficheiros da cópia de segurança com curl. Guarde-o e execute-o num terminal; irá pedir o seu token da cópia de segurança, pelo que não são armazenados segredos no script.", "NonBackupedBlobs": "Non Backed up blobs", "Calendar": "Calendário", "StartOfTheWeek": "Início da semana", diff --git a/plugins/setting-assets/lang/ru.json b/plugins/setting-assets/lang/ru.json index 5ff77cea48b..d755b952157 100644 --- a/plugins/setting-assets/lang/ru.json +++ b/plugins/setting-assets/lang/ru.json @@ -146,6 +146,12 @@ "BackupFileDownload": "Скачать файл", "BackupFiles": "Файлы резервной копии", "BackupNoBackup": "В настоящее время резервные копии недоступны.", + "BackupDownloadAll": "Скачать полную резервную копию", + "BackupPreparingDownload": "Подготовка резервной копии…", + "BackupDownloadAllInfo": "Скачать все файлы резервной копии одним архивом .zip, который можно сохранить на компьютере.", + "BackupCopyScript": "Копировать скрипт загрузки", + "BackupCopyToken": "Копировать токен", + "BackupScriptInfo": "Скрипт оболочки, который скачивает все файлы резервной копии с помощью curl. Сохраните его и запустите в терминале; он запросит токен резервной копии, поэтому в скрипте не хранятся секреты.", "NonBackupedBlobs": "Не резервируемые блобы", "Calendar": "Календарь", "StartOfTheWeek": "Начало недели", diff --git a/plugins/setting-assets/lang/tr.json b/plugins/setting-assets/lang/tr.json index 2dc39e036c6..3f7b3997f86 100644 --- a/plugins/setting-assets/lang/tr.json +++ b/plugins/setting-assets/lang/tr.json @@ -141,6 +141,12 @@ "BackupFileDownload": "Dosya İndir", "BackupFiles": "Yedek Dosyaları", "BackupNoBackup": "Şu anda kullanılabilir yedek yok.", + "BackupDownloadAll": "Tam yedeği indir", + "BackupPreparingDownload": "Yedek hazırlanıyor…", + "BackupDownloadAllInfo": "Tüm yedek dosyalarını bilgisayarınızda saklayabileceğiniz tek bir .zip arşivi olarak indirin.", + "BackupCopyScript": "İndirme betiğini kopyala", + "BackupCopyToken": "Jetonu kopyala", + "BackupScriptInfo": "Tüm yedek dosyalarını curl ile indiren bir kabuk betiği. Kaydedip bir terminalde çalıştırın; yedek jetonunuzu ister, bu nedenle betikte hiçbir gizli bilgi saklanmaz.", "NonBackupedBlobs": "Yedeklenmemiş blob'lar", "Calendar": "Takvim", "StartOfTheWeek": "Haftanın başlangıcı", diff --git a/plugins/setting-assets/lang/zh.json b/plugins/setting-assets/lang/zh.json index 5fd59f4c169..ac8ff0301ec 100644 --- a/plugins/setting-assets/lang/zh.json +++ b/plugins/setting-assets/lang/zh.json @@ -141,6 +141,12 @@ "BackupFileDownload": "下载文件", "BackupFiles": "备份文件", "BackupNoBackup": "当前没有可用的备份。", + "BackupDownloadAll": "下载完整备份", + "BackupPreparingDownload": "正在准备备份…", + "BackupDownloadAllInfo": "将所有备份文件下载为一个 .zip 压缩包,可保存到您的计算机上。", + "BackupCopyScript": "复制下载脚本", + "BackupCopyToken": "复制令牌", + "BackupScriptInfo": "一个使用 curl 下载所有备份文件的 shell 脚本。保存并在终端中运行;它会提示输入您的备份令牌,因此脚本中不会存储任何机密信息。", "NonBackupedBlobs": "未备份的 Blob", "Calendar": "日历", "StartOfTheWeek": "本周开始", diff --git a/plugins/setting-resources/src/__tests__/backup.test.ts b/plugins/setting-resources/src/__tests__/backup.test.ts new file mode 100644 index 00000000000..ec29f2d59c5 --- /dev/null +++ b/plugins/setting-resources/src/__tests__/backup.test.ts @@ -0,0 +1,156 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { + buildStoreZip, + collectBackupFileNames, + crc32, + generateBackupScript, + generateRestoreReadme +} from '../utils/backup' + +describe('collectBackupFileNames', () => { + it('returns the well-known root files when index is empty', () => { + expect(collectBackupFileNames(undefined)).toEqual(['index.json', 'backup.json.gz', 'blob-info.json.gz']) + }) + + it('includes index files and snapshot files without duplicates', () => { + const names = collectBackupFileNames({ + files: [ + { name: 'backup.json.gz', size: 1 }, + { name: 'tx-0.snp.gz', size: 2 } + ], + info: { + workspace: 'ws', + version: '0.7', + snapshots: [ + { + date: 1, + domains: { + tx: { + storage: ['tx-0.snp.gz', 'tx-1.snp.gz'], + snapshots: ['tx.snapshot.gz'], + added: 0, + updated: 0, + removed: 0 + } + } + } + ] + } + }) + + expect(names).toEqual([ + 'index.json', + 'backup.json.gz', + 'blob-info.json.gz', + 'tx-0.snp.gz', + 'tx-1.snp.gz', + 'tx.snapshot.gz' + ]) + }) +}) + +describe('generateBackupScript', () => { + const script = generateBackupScript({ + baseUrl: 'https://example.com/backup/ws-1/', + files: ['index.json', 'tx-0.snp.gz'] + }) + + it('embeds the base url without a trailing slash', () => { + expect(script).toContain("BASE_URL='https://example.com/backup/ws-1'") + }) + + it('never embeds the token; reads it from env or prompts at runtime', () => { + expect(script).not.toContain("TOKEN='") + expect(script).toContain('HULY_BACKUP_TOKEN') + expect(script).toContain('read -rsp') + }) + + it('emits a download line per file', () => { + expect(script).toContain("download 'index.json'") + expect(script).toContain("download 'tx-0.snp.gz'") + }) + + it('escapes single quotes in values', () => { + const evil = generateBackupScript({ baseUrl: "ho'st", files: [] }) + expect(evil).toContain("BASE_URL='ho'\\''st'") + }) + + it('writes the restore readme when provided', () => { + const withReadme = generateBackupScript({ + baseUrl: 'u', + files: [], + restoreReadme: '# Restore me\n' + }) + expect(withReadme).toContain('RESTORE.md') + expect(withReadme).toContain('# Restore me') + }) +}) + +describe('generateRestoreReadme', () => { + const readme = generateRestoreReadme({ sourceWorkspace: 'ws-uuid-123', fileCount: 7 }) + + it('names the source workspace and file count', () => { + expect(readme).toContain('ws-uuid-123') + expect(readme).toContain('7') + }) + + it('documents the exact restore command', () => { + expect(readme).toContain('tool backup-restore') + }) + + it('warns about blobs excluded from the backup', () => { + expect(readme.toLowerCase()).toContain('video') + expect(readme.toLowerCase()).toContain('blob') + }) +}) + +describe('crc32', () => { + it('matches the standard check value for "123456789"', () => { + expect(crc32(new TextEncoder().encode('123456789'))).toBe(0xcbf43926) + }) + + it('is zero for empty input', () => { + expect(crc32(new Uint8Array(0))).toBe(0) + }) +}) + +describe('buildStoreZip', () => { + it('produces a valid local-file and EOCD signature', () => { + const zip = buildStoreZip([{ name: 'a.txt', data: new TextEncoder().encode('hello') }]) + const view = new DataView(zip.buffer) + expect(view.getUint32(0, true)).toBe(0x04034b50) // local file header + expect(view.getUint32(zip.length - 22, true)).toBe(0x06054b50) // EOCD + }) + + it('records every entry in the central directory', () => { + const zip = buildStoreZip([ + { name: 'a.txt', data: new TextEncoder().encode('hello') }, + { name: 'b.txt', data: new TextEncoder().encode('world!') } + ]) + const view = new DataView(zip.buffer) + const totalEntries = view.getUint16(zip.length - 22 + 10, true) + expect(totalEntries).toBe(2) + }) + + it('stores data uncompressed so the payload is embedded verbatim', () => { + const payload = new TextEncoder().encode('hello') + const zip = buildStoreZip([{ name: 'a.txt', data: payload }]) + // local header is 30 bytes + 5-byte name => data begins at offset 35 + const stored = zip.slice(35, 40) + expect(new TextDecoder().decode(stored)).toBe('hello') + }) +}) diff --git a/plugins/setting-resources/src/components/Backup.svelte b/plugins/setting-resources/src/components/Backup.svelte index 92805f652a1..8a445e6b57a 100644 --- a/plugins/setting-resources/src/components/Backup.svelte +++ b/plugins/setting-resources/src/components/Backup.svelte @@ -15,12 +15,13 @@