From 4064588585304a3ac275708ad367ff52062411f6 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Thu, 2 Jul 2026 15:59:54 +0700 Subject: [PATCH] Add backup/restore guide (#10945) Signed-off-by: Artyom Savchenko --- README.md | 2 + docs/guides/backup-restore.en.md | 136 ++++++++++++++++++ plugins/setting-assets/lang/cs.json | 2 + plugins/setting-assets/lang/de.json | 2 + plugins/setting-assets/lang/en.json | 2 + plugins/setting-assets/lang/es.json | 2 + plugins/setting-assets/lang/fr.json | 2 + plugins/setting-assets/lang/it.json | 2 + plugins/setting-assets/lang/ja.json | 2 + plugins/setting-assets/lang/ko.json | 2 + plugins/setting-assets/lang/pl.json | 2 + plugins/setting-assets/lang/pt-br.json | 2 + plugins/setting-assets/lang/pt.json | 2 + plugins/setting-assets/lang/ru.json | 2 + plugins/setting-assets/lang/tr.json | 2 + plugins/setting-assets/lang/zh.json | 2 + .../src/__tests__/backup.test.ts | 15 ++ .../src/components/Backup.svelte | 24 +++- plugins/setting-resources/src/utils/backup.ts | 19 ++- plugins/setting/src/index.ts | 2 + plugins/workbench-assets/lang/cs.json | 4 +- plugins/workbench-assets/lang/de.json | 4 +- plugins/workbench-assets/lang/en.json | 4 +- plugins/workbench-assets/lang/es.json | 4 +- plugins/workbench-assets/lang/fr.json | 4 +- plugins/workbench-assets/lang/it.json | 4 +- plugins/workbench-assets/lang/ja.json | 4 +- plugins/workbench-assets/lang/ko.json | 4 +- plugins/workbench-assets/lang/pl.json | 4 +- plugins/workbench-assets/lang/pt-br.json | 4 +- plugins/workbench-assets/lang/pt.json | 4 +- plugins/workbench-assets/lang/ru.json | 4 +- plugins/workbench-assets/lang/tr.json | 4 +- plugins/workbench-assets/lang/zh.json | 4 +- .../src/components/ShutdownNoticePopup.svelte | 13 +- plugins/workbench-resources/src/plugin.ts | 4 +- plugins/workbench-resources/src/utils.ts | 9 ++ 37 files changed, 289 insertions(+), 19 deletions(-) create mode 100644 docs/guides/backup-restore.en.md diff --git a/README.md b/README.md index 85560ed070..e7c0b91396 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ > > The hosted Huly service is being discontinued because its hosting is no longer being funded. If you keep important data on the hosted platform, export and back it up, and migrate as soon as possible — we can help you move to either a [self-hosted setup](https://github.com/hcengineering/huly-selfhost) or a hosted option. > +> Not sure how? Follow the [backup & restore guide](docs/guides/backup-restore.en.md) for step-by-step instructions on downloading your backup and restoring it elsewhere. +> > The service shutdown is expected on **July 20**. Please make sure to export and migrate your data before then rather than wait until the last day. > > Have questions or want updates? Join the [Huly community](https://link.huly.io/slack) to discuss migration and stay informed, or email us at [artem@hardcoreeng.com](mailto:artem@hardcoreeng.com) with any questions. This affects only the hosted **Huly** service — self-hosted deployments are not affected. diff --git a/docs/guides/backup-restore.en.md b/docs/guides/backup-restore.en.md new file mode 100644 index 0000000000..b3c272376e --- /dev/null +++ b/docs/guides/backup-restore.en.md @@ -0,0 +1,136 @@ +# Backup & Restore Guide + +Download a workspace backup and restore it into another Huly instance — +whether that's a self-hosted deployment or one of the hosted options. This is +the recommended way to move your data off a hosted workspace before it +becomes unavailable. + +> [!TIP] +> Don't want to run your own server? You can also move to one of the +> existing hosted Huly forks instead of self-hosting. We're glad to help +> with the data migration either way — just keep in mind that none of these +> hosting options are free, since keeping servers running costs real money. +> See [Need help?](#need-help) below to get in touch. + +## Overview + +1. **Download** — from *Settings → Backup* in the workspace you want to move, + grab a full copy of your data. +2. **Restore** — replay that backup into the destination workspace using the + platform's admin tool (`dev/tool`). + +## Step 1. Download your backup + +Open the workspace and go to: + +``` +/setting/setting/backup +``` + +(*Settings → Backup* in the left sidebar.) The page shows when the backup was +last taken, how many snapshots and files it contains, and its total size. + +> [!NOTE] +> Backups are taken periodically, not continuously, so the latest backup can +> lag behind your live data by some time. Check **Last backup** on the page +> before you rely on it, and give it time to catch up if you just made +> important changes. + +You have two ways to get the files: + +- **Download full backup** — bundles every backup file into a single `.zip` + you can keep on your computer. A `RESTORE.md` with restore instructions is + included inside the archive. +- **Copy download script** (+ **Copy token**) — copies a small shell script + that downloads every file with `curl`. The script is safe to save or share: + it does **not** contain your token — it reads it from the + `HULY_BACKUP_TOKEN` environment variable, or prompts for it when you run it. + +Either option produces a folder that the restore tool can consume directly +(see Step 2). + +> [!TIP] +> **Large workspace?** Prefer the download script over the button. "Download +> full backup" assembles the whole archive in your browser's memory before +> saving it, which can be slow or fail outright once a workspace has a lot of +> data. The script downloads files one by one with `curl`, so it handles +> large backups more reliably. + +You can also expand **Backup Snapshots** or **Backup Files** to download +individual files, e.g. to verify access or fetch a single snapshot. + +> [!IMPORTANT] +> The **Not backed up** section lists blobs that are intentionally excluded +> from the regular backup (video, audio, and any file larger than the +> server's blob-size limit). Their content isn't in the archive or script +> above — if you need them on the destination, download each one individually +> from that list before you migrate. + +## Step 2. Restore into a Huly instance + +You'll need a Huly platform you control to restore into — either +[huly-selfhost](https://github.com/hcengineering/huly-selfhost), a +self-hosted deployment built from this monorepo, or another hosted instance +where you have admin access. Create the destination workspace first if it +doesn't exist yet. + +Restoring is done with the `backup-restore` command of the platform admin +tool (`@hcengineering/tool`, `dev/tool` in this repo). Exactly how you invoke +it depends on how the destination platform is run: + +- **Docker / self-hosted setup** — run it inside your `tool` container + (e.g. `docker compose run --rm tool backup-restore ...` or + `docker compose exec tool ...`, adjusted to your compose service name). +- **Monorepo checkout with Rush** — from `dev/tool`, run it against your + configured environment, e.g. `rushx run-local backup-restore ...` for a + local dev stack, or your own script that sets the same environment + variables against your real databases. + +In both cases the command and its arguments are the same: + +``` +backup-restore [date] --accounts +``` + +- `` — the folder you downloaded/unzipped in Step 1. +- `` — the destination workspace's identifier. +- `--accounts` — also restores the original users (their profile and social + identities) into the target workspace. Without this flag only documents are + restored, and every member has to be invited again by email. +- `--upgrade` — add this if the destination runs a newer model version than + the backup. +- `-m/--merge` — don't delete documents that are missing from the backup + (useful when merging into a workspace that already has data). + +> [!NOTE] +> Restoring accounts (`--accounts`) needs its own connection to the account +> database, configured via the `ACCOUNT_DB_URL` (and optional +> `ACCOUNT_DB_NS`) environment variables. See +> [`server/backup/README.md`](../../server/backup/README.md) for the full +> reference of flags and per-entry-point environment variables. + +Once the restore finishes, users sign in with their original email via a +one-time code (OTP) — make sure the destination platform can send mail. + +## Need help? + +Our team is happy to help with migration. Join the [Huly community](https://link.huly.io/slack) to ask questions and get updates, or email us directly at [artem@hardcoreeng.com](mailto:artem@hardcoreeng.com). + +## Troubleshooting + +- **401 Unauthorized while downloading** — your token expired, or you're not + an owner/admin of the workspace. Get a fresh token from the Backup page. +- **Members are missing after restore** — re-run with `--accounts` and a + valid `ACCOUNT_DB_URL`; without both, only documents are restored. +- **A file/video is missing on the destination** — check the **Not backed + up** list on the Backup page and download it individually; it isn't part + of the regular backup. +- **Restore fails with a model/version error** — add `--upgrade` if the + destination runs a newer platform version than the source. + +## See also + +- [`server/backup/README.md`](../../server/backup/README.md) — full CLI + reference for `backup-restore`, including environment variables. +- [huly-selfhost](https://github.com/hcengineering/huly-selfhost) — official + self-hosted distribution. diff --git a/plugins/setting-assets/lang/cs.json b/plugins/setting-assets/lang/cs.json index c229c0e598..bdcef592cd 100644 --- a/plugins/setting-assets/lang/cs.json +++ b/plugins/setting-assets/lang/cs.json @@ -141,6 +141,8 @@ "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í.", + "BackupRestoreGuide": "Průvodce zálohováním a obnovou", + "BackupRestoreGuideInfo": "Podrobný návod, jak stáhnout tuto zálohu a obnovit ji v jiné instanci Huly.", "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 8e71bfdac8..7408e34277 100644 --- a/plugins/setting-assets/lang/de.json +++ b/plugins/setting-assets/lang/de.json @@ -147,6 +147,8 @@ "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.", + "BackupRestoreGuide": "Anleitung zu Backup & Wiederherstellung", + "BackupRestoreGuideInfo": "Schritt-für-Schritt-Anleitung zum Herunterladen dieses Backups und zur Wiederherstellung in einer anderen Huly-Instanz.", "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 ddbb8817a5..838f5de365 100644 --- a/plugins/setting-assets/lang/en.json +++ b/plugins/setting-assets/lang/en.json @@ -147,6 +147,8 @@ "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.", + "BackupRestoreGuide": "Backup & restore guide", + "BackupRestoreGuideInfo": "Step-by-step instructions for downloading this backup and restoring it into another Huly instance.", "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 fa317c7f55..81bccb2471 100644 --- a/plugins/setting-assets/lang/es.json +++ b/plugins/setting-assets/lang/es.json @@ -138,6 +138,8 @@ "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.", + "BackupRestoreGuide": "Guía de copia de seguridad y restauración", + "BackupRestoreGuideInfo": "Instrucciones paso a paso para descargar esta copia de seguridad y restaurarla en otra instancia de Huly.", "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 55edcb74b5..7ea973f0dd 100644 --- a/plugins/setting-assets/lang/fr.json +++ b/plugins/setting-assets/lang/fr.json @@ -147,6 +147,8 @@ "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.", + "BackupRestoreGuide": "Guide de sauvegarde et restauration", + "BackupRestoreGuideInfo": "Instructions étape par étape pour télécharger cette sauvegarde et la restaurer sur une autre instance Huly.", "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 4f24589b2e..4d556acaeb 100644 --- a/plugins/setting-assets/lang/it.json +++ b/plugins/setting-assets/lang/it.json @@ -147,6 +147,8 @@ "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.", + "BackupRestoreGuide": "Guida a backup e ripristino", + "BackupRestoreGuideInfo": "Istruzioni dettagliate per scaricare questo backup e ripristinarlo in un'altra istanza di Huly.", "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 1cda80b551..f767c713d2 100644 --- a/plugins/setting-assets/lang/ja.json +++ b/plugins/setting-assets/lang/ja.json @@ -147,6 +147,8 @@ "BackupCopyScript": "ダウンロードスクリプトをコピー", "BackupCopyToken": "トークンをコピー", "BackupScriptInfo": "curl ですべてのバックアップファイルをダウンロードするシェルスクリプトです。保存してターミナルで実行してください。バックアップトークンの入力を求められるため、スクリプトに機密情報は保存されません。", + "BackupRestoreGuide": "バックアップと復元ガイド", + "BackupRestoreGuideInfo": "このバックアップをダウンロードし、別の Huly インスタンスに復元するための手順です。", "NonBackupedBlobs": "バックアップされていないブロブ", "Calendar": "カレンダー", "StartOfTheWeek": "週の開始", diff --git a/plugins/setting-assets/lang/ko.json b/plugins/setting-assets/lang/ko.json index 544115e832..c883efbce6 100644 --- a/plugins/setting-assets/lang/ko.json +++ b/plugins/setting-assets/lang/ko.json @@ -147,6 +147,8 @@ "BackupCopyScript": "다운로드 스크립트 복사", "BackupCopyToken": "토큰 복사", "BackupScriptInfo": "curl로 모든 백업 파일을 다운로드하는 셸 스크립트입니다. 저장한 후 터미널에서 실행하세요. 백업 토큰을 입력하라는 메시지가 표시되므로 스크립트에 비밀 정보가 저장되지 않습니다.", + "BackupRestoreGuide": "백업 및 복원 가이드", + "BackupRestoreGuideInfo": "이 백업을 다운로드하여 다른 Huly 인스턴스로 복원하는 단계별 안내입니다.", "NonBackupedBlobs": "백업되지 않은 Blob", "Calendar": "캘린더", "StartOfTheWeek": "주 시작일", diff --git a/plugins/setting-assets/lang/pl.json b/plugins/setting-assets/lang/pl.json index ce6d6db8a8..afdf4caee6 100644 --- a/plugins/setting-assets/lang/pl.json +++ b/plugins/setting-assets/lang/pl.json @@ -147,6 +147,8 @@ "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.", + "BackupRestoreGuide": "Przewodnik po kopii zapasowej i przywracaniu", + "BackupRestoreGuideInfo": "Instrukcja krok po kroku dotycząca pobierania tej kopii zapasowej i przywracania jej w innej instancji Huly.", "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 8dda982a87..35fe91b5d6 100644 --- a/plugins/setting-assets/lang/pt-br.json +++ b/plugins/setting-assets/lang/pt-br.json @@ -138,6 +138,8 @@ "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.", + "BackupRestoreGuide": "Guia de backup e restauração", + "BackupRestoreGuideInfo": "Instruções passo a passo para baixar este backup e restaurá-lo em outra instância do Huly.", "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 875336df9f..50d8f10a37 100644 --- a/plugins/setting-assets/lang/pt.json +++ b/plugins/setting-assets/lang/pt.json @@ -138,6 +138,8 @@ "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.", + "BackupRestoreGuide": "Guia de cópia de segurança e restauro", + "BackupRestoreGuideInfo": "Instruções passo a passo para transferir esta cópia de segurança e restaurá-la noutra instância do Huly.", "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 d755b95215..677a522cc7 100644 --- a/plugins/setting-assets/lang/ru.json +++ b/plugins/setting-assets/lang/ru.json @@ -152,6 +152,8 @@ "BackupCopyScript": "Копировать скрипт загрузки", "BackupCopyToken": "Копировать токен", "BackupScriptInfo": "Скрипт оболочки, который скачивает все файлы резервной копии с помощью curl. Сохраните его и запустите в терминале; он запросит токен резервной копии, поэтому в скрипте не хранятся секреты.", + "BackupRestoreGuide": "Инструкция по резервному копированию и восстановлению", + "BackupRestoreGuideInfo": "Пошаговая инструкция о том, как скачать эту резервную копию и восстановить её в другом инстансе Huly.", "NonBackupedBlobs": "Не резервируемые блобы", "Calendar": "Календарь", "StartOfTheWeek": "Начало недели", diff --git a/plugins/setting-assets/lang/tr.json b/plugins/setting-assets/lang/tr.json index 3f7b3997f8..0a883da16a 100644 --- a/plugins/setting-assets/lang/tr.json +++ b/plugins/setting-assets/lang/tr.json @@ -147,6 +147,8 @@ "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.", + "BackupRestoreGuide": "Yedekleme ve geri yükleme kılavuzu", + "BackupRestoreGuideInfo": "Bu yedeği indirmek ve başka bir Huly örneğine geri yüklemek için adım adım talimatlar.", "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 ac8ff0301e..f1b43b1896 100644 --- a/plugins/setting-assets/lang/zh.json +++ b/plugins/setting-assets/lang/zh.json @@ -147,6 +147,8 @@ "BackupCopyScript": "复制下载脚本", "BackupCopyToken": "复制令牌", "BackupScriptInfo": "一个使用 curl 下载所有备份文件的 shell 脚本。保存并在终端中运行;它会提示输入您的备份令牌,因此脚本中不会存储任何机密信息。", + "BackupRestoreGuide": "备份与恢复指南", + "BackupRestoreGuideInfo": "关于下载此备份并将其恢复到另一个 Huly 实例的分步说明。", "NonBackupedBlobs": "未备份的 Blob", "Calendar": "日历", "StartOfTheWeek": "本周开始", diff --git a/plugins/setting-resources/src/__tests__/backup.test.ts b/plugins/setting-resources/src/__tests__/backup.test.ts index ec29f2d59c..2ba03995fa 100644 --- a/plugins/setting-resources/src/__tests__/backup.test.ts +++ b/plugins/setting-resources/src/__tests__/backup.test.ts @@ -14,6 +14,7 @@ // import { + backupRestoreGuideLink, buildStoreZip, collectBackupFileNames, crc32, @@ -116,6 +117,20 @@ describe('generateRestoreReadme', () => { expect(readme.toLowerCase()).toContain('video') expect(readme.toLowerCase()).toContain('blob') }) + + it('mentions the --accounts flag needed to restore the original users', () => { + expect(readme).toContain('--accounts') + }) + + it('links to the full backup & restore guide', () => { + expect(readme).toContain(backupRestoreGuideLink) + }) +}) + +describe('backupRestoreGuideLink', () => { + it('is an https link', () => { + expect(backupRestoreGuideLink.startsWith('https://')).toBe(true) + }) }) describe('crc32', () => { diff --git a/plugins/setting-resources/src/components/Backup.svelte b/plugins/setting-resources/src/components/Backup.svelte index 8a445e6b57..5ee991314b 100644 --- a/plugins/setting-resources/src/components/Backup.svelte +++ b/plugins/setting-resources/src/components/Backup.svelte @@ -21,7 +21,13 @@ import { onMount } from 'svelte' import setting from '../plugin' import { BackupInfo, BackupSnapshot } from '../types' - import { buildStoreZip, collectBackupFileNames, generateBackupScript, generateRestoreReadme } from '../utils/backup' + import { + backupRestoreGuideLink, + buildStoreZip, + collectBackupFileNames, + generateBackupScript, + generateRestoreReadme + } from '../utils/backup' let loading = true @@ -316,6 +322,22 @@