From 00d15218cb4c3e53e48258991c1e5d2f05d6eb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Thu, 22 Oct 2026 23:47:19 +0800 Subject: [PATCH] feat: allow editing the current site in site replication The current site's endpoint is recorded once, when site replication is set up, from the address the setup request used. When the server's address changes, every site keeps the old endpoint and replication toward this site stops, but the Console only offered "Managed automatically" for the current site, so operators had to edit it from a peer's Console or the API. Offer Edit Site on the current site's row (resync and remove stay limited to remote peers) and explain in the dialog that this is the address other sites use. After a saved endpoint change, recommend the resync that recovers writes made while the old address was stale: from this site toward a moved peer (with a Start Resync action), or from every peer toward a moved current site. --- app/(dashboard)/site-replication/page.tsx | 58 ++++++++++++++++++--- components/site-replication/edit-form.tsx | 25 ++++++--- i18n/locales/ar-MA.json | 5 ++ i18n/locales/de-DE.json | 5 ++ i18n/locales/en-US.json | 5 ++ i18n/locales/es-ES.json | 5 ++ i18n/locales/fr-FR.json | 5 ++ i18n/locales/id-ID.json | 5 ++ i18n/locales/it-IT.json | 5 ++ i18n/locales/ja-JP.json | 5 ++ i18n/locales/ko-KR.json | 5 ++ i18n/locales/pt-BR.json | 5 ++ i18n/locales/ru-RU.json | 5 ++ i18n/locales/tr-TR.json | 5 ++ i18n/locales/vi-VN.json | 5 ++ i18n/locales/zh-CN.json | 5 ++ lib/site-replication-edit.ts | 33 ++++++++++++ tests/lib/site-replication-edit.test.js | 62 +++++++++++++++++++++++ 18 files changed, 235 insertions(+), 13 deletions(-) create mode 100644 lib/site-replication-edit.ts create mode 100644 tests/lib/site-replication-edit.test.js diff --git a/app/(dashboard)/site-replication/page.tsx b/app/(dashboard)/site-replication/page.tsx index 77ce88e5..444deae7 100644 --- a/app/(dashboard)/site-replication/page.tsx +++ b/app/(dashboard)/site-replication/page.tsx @@ -39,6 +39,7 @@ import { useSystem } from "@/hooks/use-system" import { formatDateTime, niceBytes } from "@/lib/functions" import { useDialog } from "@/lib/feedback/dialog" import { useMessage } from "@/lib/feedback/message" +import { siteReplicationResyncAdvice } from "@/lib/site-replication-edit" import { extractServerDeploymentId, resolveSiteReplicationLocalSite } from "@/lib/site-replication-local-site" import { isHttpsSiteReplicationEndpoint } from "@/lib/site-replication-tls" import type { @@ -240,6 +241,43 @@ export default function SiteReplicationPage() { [loadData, message, resyncSiteReplication, t], ) + // Edits do not replay writes made while the old address was unreachable, so + // an endpoint change is followed by the resync that recovers them. + const handleEditSuccess = (peer: SiteReplicationPeerInfo, nextEndpoint: string) => { + void loadData() + + const advice = siteReplicationResyncAdvice({ + isLocal: peer.deploymentId === localDeploymentId, + previousEndpoint: peer.endpoint, + nextEndpoint, + }) + + if (advice === "resync-from-peers") { + dialog.warning({ + title: t("Resync Recommended"), + content: t( + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.", + ), + positiveText: t("Close"), + }) + } else if (advice === "resync-peer") { + dialog.warning({ + title: t("Resync Recommended"), + content: t( + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.", + { name: peer.name || peer.endpoint }, + ), + ...(canResyncSites + ? { + positiveText: t("Start Resync"), + negativeText: t("Later"), + onPositiveClick: () => void handleResync({ ...peer, endpoint: nextEndpoint }, "start"), + } + : { positiveText: t("Close") }), + }) + } + } + const confirmRemoveSite = useCallback( (peer: SiteReplicationPeerInfo) => { dialog.error({ @@ -379,7 +417,10 @@ export default function SiteReplicationPage() { const isBusy = pendingRemoveDeploymentId === peer.deploymentId || pendingResyncDeploymentId === peer.deploymentId - if (isLocal) { + // The local site's stored endpoint is only inferred when replication + // is set up, so it must stay editable for address changes; resync and + // removal still only make sense for remote peers. + if (isLocal && !canManageSites) { return {t("Managed automatically")} } @@ -395,28 +436,28 @@ export default function SiteReplicationPage() { /> - {peer.name || t("Peer site")} + {peer.name || (isLocal ? t("Current Site") : t("Peer site"))} {canManageSites ? ( setEditingPeer(peer)}> {t("Edit Site")} ) : null} - {canResyncSites ? ( + {canResyncSites && !isLocal ? ( void handleResync(peer, "start")}> {t("Start Resync")} ) : null} - {canResyncSites ? ( + {canResyncSites && !isLocal ? ( void handleResync(peer, "cancel")}> {t("Cancel Resync")} ) : null} - {canRemoveSites ? : null} - {canRemoveSites ? ( + {canRemoveSites && !isLocal ? : null} + {canRemoveSites && !isLocal ? ( confirmRemoveSite(peer)}> {t("Remove Site")} @@ -795,7 +836,10 @@ export default function SiteReplicationPage() { } }} peer={editingPeer} - onSuccess={() => void loadData()} + isLocal={!!editingPeer && editingPeer.deploymentId === localDeploymentId} + onSuccess={(endpoint) => { + if (editingPeer) handleEditSuccess(editingPeer, endpoint) + }} /> ) diff --git a/components/site-replication/edit-form.tsx b/components/site-replication/edit-form.tsx index f0504b98..eebd0997 100644 --- a/components/site-replication/edit-form.tsx +++ b/components/site-replication/edit-form.tsx @@ -31,10 +31,19 @@ interface SiteReplicationEditFormProps { open: boolean onOpenChange: (open: boolean) => void peer: SiteReplicationPeerInfo | null - onSuccess?: () => void + /** True when `peer` is the site this console is connected to. */ + isLocal?: boolean + /** Called with the saved endpoint after the server accepted the edit. */ + onSuccess?: (endpoint: string) => void } -export function SiteReplicationEditForm({ open, onOpenChange, peer, onSuccess }: SiteReplicationEditFormProps) { +export function SiteReplicationEditForm({ + open, + onOpenChange, + peer, + isLocal = false, + onSuccess, +}: SiteReplicationEditFormProps) { const { t } = useTranslation() const message = useMessage() const { editSiteReplication } = useSiteReplication() @@ -98,7 +107,7 @@ export function SiteReplicationEditForm({ open, onOpenChange, peer, onSuccess }: } message.success(response.status || t("Save Success")) - onSuccess?.() + onSuccess?.(endpoint.trim()) onOpenChange(false) } catch (error) { message.error((error as Error).message || t("Save Failed")) @@ -137,9 +146,13 @@ export function SiteReplicationEditForm({ open, onOpenChange, peer, onSuccess }: {t("Edit Site")} - {t( - "Update the peer details that RustFS uses for site replication management. Fields not shown here are preserved from the current configuration.", - )} + {isLocal + ? t( + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.", + ) + : t( + "Update the peer details that RustFS uses for site replication management. Fields not shown here are preserved from the current configuration.", + )} diff --git a/i18n/locales/ar-MA.json b/i18n/locales/ar-MA.json index c293e768..17c10bee 100644 --- a/i18n/locales/ar-MA.json +++ b/i18n/locales/ar-MA.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "يُوصى بإعادة المزامنة", + "Later": "لاحقًا", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "هذا هو العنوان الذي تستخدمه المواقع الأخرى للوصول إلى هذا الموقع. حدّثه بعد تغيّر عنوان هذا الخادم؛ وستتلقى جميع المواقع النظيرة العنوان الجديد.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "تغيّر عنوان هذا الموقع. لم تُنسخ إلى هنا الكائنات التي كُتبت على المواقع الأخرى أثناء تعذّر الوصول إلى العنوان القديم. ابدأ إعادة مزامنة نحو هذا الموقع من وحدة تحكم كل موقع نظير.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "تغيّر عنوان {name}. لم تُنسخ إليه الكائنات التي كُتبت على هذا الموقع أثناء تعذّر الوصول إلى العنوان القديم. شغّل إعادة مزامنة لهذا الموقع لنسخها.", "Path": "المسار", "Force Cleanup Folder": "تنظيف المجلد بالقوة", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "حذف جميع الكائنات والإصدارات وعلامات الحذف والملفات المتبقية في هذا المجلد نهائيًا. لا يمكن التراجع عن هذا الإجراء.", diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json index 40b40014..a412fc2a 100644 --- a/i18n/locales/de-DE.json +++ b/i18n/locales/de-DE.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Neusynchronisierung empfohlen", + "Later": "Später", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "Unter dieser Adresse erreichen andere Sites diese Site. Aktualisieren Sie sie, nachdem sich die Adresse dieses Servers geändert hat; alle Peer-Sites erhalten die neue Adresse.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "Die Adresse dieser Site hat sich geändert. Objekte, die auf anderen Sites geschrieben wurden, während die alte Adresse nicht erreichbar war, wurden nicht hierher repliziert. Starten Sie in der Konsole jeder Peer-Site eine Neusynchronisierung zu dieser Site.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "Die Adresse von {name} hat sich geändert. Objekte, die auf dieser Site geschrieben wurden, während die alte Adresse nicht erreichbar war, wurden nicht dorthin repliziert. Führen Sie für diese Site eine Neusynchronisierung aus, um sie zu kopieren.", "Path": "Pfad", "Force Cleanup Folder": "Ordner zwangsweise bereinigen", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Alle Objekte, Versionen, Löschmarkierungen und verbliebenen Dateien in diesem Ordner dauerhaft löschen. Dies kann nicht rückgängig gemacht werden.", diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index 7d1f570e..df63feab 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Resync Recommended", + "Later": "Later", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.", "Path": "Path", "Force Cleanup Folder": "Force Cleanup Folder", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.", diff --git a/i18n/locales/es-ES.json b/i18n/locales/es-ES.json index e807fc96..fd543795 100644 --- a/i18n/locales/es-ES.json +++ b/i18n/locales/es-ES.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Se recomienda resincronizar", + "Later": "Más tarde", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "Esta es la dirección que usan los demás sitios para acceder a este sitio. Actualízala cuando cambie la dirección de este servidor; todos los sitios pares recibirán la nueva dirección.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "La dirección de este sitio ha cambiado. Los objetos escritos en otros sitios mientras la dirección anterior no era accesible no se replicaron aquí. Inicia una resincronización hacia este sitio desde la consola de cada sitio par.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "La dirección de {name} ha cambiado. Los objetos escritos en este sitio mientras la dirección anterior no era accesible no se replicaron allí. Ejecuta una resincronización para este sitio para copiarlos.", "Path": "Ruta", "Force Cleanup Folder": "Forzar limpieza de carpeta", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Eliminar permanentemente todos los objetos, versiones, marcadores de eliminación y archivos residuales de esta carpeta. Esta acción no se puede deshacer.", diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 80d72f17..8001970f 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Resynchronisation recommandée", + "Later": "Plus tard", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "Il s'agit de l'adresse que les autres sites utilisent pour joindre ce site. Mettez-la à jour après un changement d'adresse de ce serveur ; tous les sites pairs reçoivent la nouvelle adresse.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "L'adresse de ce site a changé. Les objets écrits sur les autres sites pendant que l'ancienne adresse était injoignable n'ont pas été répliqués ici. Lancez une resynchronisation vers ce site depuis la console de chaque site pair.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "L'adresse de {name} a changé. Les objets écrits sur ce site pendant que l'ancienne adresse était injoignable n'y ont pas été répliqués. Lancez une resynchronisation pour ce site afin de les copier.", "Path": "Chemin", "Force Cleanup Folder": "Forcer le nettoyage du dossier", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Supprimer définitivement tous les objets, versions, marqueurs de suppression et fichiers résiduels de ce dossier. Cette action est irréversible.", diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json index 141b8939..a93b718f 100644 --- a/i18n/locales/id-ID.json +++ b/i18n/locales/id-ID.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Sinkronisasi ulang disarankan", + "Later": "Nanti", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "Ini adalah alamat yang digunakan situs lain untuk menjangkau situs ini. Perbarui setelah alamat server ini berubah; semua situs peer akan menerima alamat baru.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "Alamat situs ini berubah. Objek yang ditulis di situs lain selama alamat lama tidak dapat dijangkau tidak direplikasi ke sini. Mulai sinkronisasi ulang ke situs ini dari konsol setiap situs peer.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "Alamat {name} berubah. Objek yang ditulis di situs ini selama alamat lama tidak dapat dijangkau tidak direplikasi ke sana. Jalankan sinkronisasi ulang untuk situs ini untuk menyalinnya.", "Path": "Jalur", "Force Cleanup Folder": "Bersihkan Folder Secara Paksa", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Hapus secara permanen semua objek, versi, penanda penghapusan, dan berkas sisa dalam folder ini. Tindakan ini tidak dapat dibatalkan.", diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index dad754da..7dfff2f5 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Risincronizzazione consigliata", + "Later": "Più tardi", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "Questo è l'indirizzo che gli altri siti usano per raggiungere questo sito. Aggiornalo dopo che l'indirizzo di questo server è cambiato; tutti i siti peer ricevono il nuovo indirizzo.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "L'indirizzo di questo sito è cambiato. Gli oggetti scritti sugli altri siti mentre il vecchio indirizzo non era raggiungibile non sono stati replicati qui. Avvia una risincronizzazione verso questo sito dalla console di ogni sito peer.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "L'indirizzo di {name} è cambiato. Gli oggetti scritti su questo sito mentre il vecchio indirizzo non era raggiungibile non sono stati replicati lì. Esegui una risincronizzazione per questo sito per copiarli.", "Path": "Percorso", "Force Cleanup Folder": "Forza pulizia cartella", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Elimina definitivamente tutti gli oggetti, le versioni, i marcatori di eliminazione e i file residui in questa cartella. Questa operazione è irreversibile.", diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index 7ee516f2..d341b11c 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "再同期を推奨", + "Later": "後で", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "これは他のサイトがこのサイトに接続する際のアドレスです。このサーバーのアドレスが変わったら更新してください。新しいアドレスはすべてのピアサイトに反映されます。", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "このサイトのアドレスが変更されました。旧アドレスに到達できなかった間に他のサイトへ書き込まれたオブジェクトは、このサイトにレプリケートされていません。各ピアサイトのコンソールから、このサイトへの再同期を開始してください。", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "{name} のアドレスが変更されました。旧アドレスに到達できなかった間にこのサイトへ書き込まれたオブジェクトは、そのサイトにレプリケートされていません。このサイトの再同期を実行してコピーしてください。", "Path": "パス", "Force Cleanup Folder": "フォルダーを強制クリーンアップ", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "このフォルダー内のすべてのオブジェクト、バージョン、削除マーカー、残存ファイルを完全に削除します。この操作は取り消せません。", diff --git a/i18n/locales/ko-KR.json b/i18n/locales/ko-KR.json index 239c7d0b..0a751857 100644 --- a/i18n/locales/ko-KR.json +++ b/i18n/locales/ko-KR.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "재동기화 권장", + "Later": "나중에", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "다른 사이트가 이 사이트에 접속할 때 사용하는 주소입니다. 이 서버의 주소가 변경되면 업데이트하세요. 새 주소는 모든 피어 사이트에 전달됩니다.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "이 사이트의 주소가 변경되었습니다. 이전 주소에 연결할 수 없던 동안 다른 사이트에 기록된 객체는 이 사이트로 복제되지 않았습니다. 각 피어 사이트의 콘솔에서 이 사이트로의 재동기화를 시작하세요.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "{name}의 주소가 변경되었습니다. 이전 주소에 연결할 수 없던 동안 이 사이트에 기록된 객체는 해당 사이트로 복제되지 않았습니다. 이 사이트에 대해 재동기화를 실행하여 복사하세요.", "Path": "경로", "Force Cleanup Folder": "폴더 강제 정리", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "이 폴더의 모든 객체, 버전, 삭제 마커 및 잔여 파일을 영구적으로 삭제합니다. 이 작업은 되돌릴 수 없습니다.", diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index e03b09be..b028c906 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Ressincronização recomendada", + "Later": "Mais tarde", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "Este é o endereço que os outros sites usam para acessar este site. Atualize-o depois que o endereço deste servidor mudar; todos os sites pares recebem o novo endereço.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "O endereço deste site mudou. Objetos gravados em outros sites enquanto o endereço antigo estava inacessível não foram replicados para cá. Inicie uma ressincronização para este site a partir do console de cada site par.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "O endereço de {name} mudou. Objetos gravados neste site enquanto o endereço antigo estava inacessível não foram replicados para ele. Execute uma ressincronização para este site para copiá-los.", "Path": "Caminho", "Force Cleanup Folder": "Forçar limpeza da pasta", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Excluir permanentemente todos os objetos, versões, marcadores de exclusão e arquivos residuais desta pasta. Esta ação não pode ser desfeita.", diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index 221e475e..eabdf520 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Рекомендуется повторная синхронизация", + "Later": "Позже", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "По этому адресу другие сайты подключаются к этому сайту. Обновите его после смены адреса этого сервера; новый адрес получат все одноранговые сайты.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "Адрес этого сайта изменился. Объекты, записанные на других сайтах, пока старый адрес был недоступен, не были реплицированы сюда. Запустите повторную синхронизацию в сторону этого сайта из консоли каждого однорангового сайта.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "Адрес {name} изменился. Объекты, записанные на этом сайте, пока старый адрес был недоступен, не были реплицированы туда. Запустите повторную синхронизацию для этого сайта, чтобы скопировать их.", "Path": "Путь", "Force Cleanup Folder": "Принудительная очистка папки", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Безвозвратно удалить все объекты, версии, маркеры удаления и оставшиеся файлы в этой папке. Это действие нельзя отменить.", diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index 96bac012..1608db92 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Yeniden eşitleme önerilir", + "Later": "Daha sonra", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "Bu, diğer sitelerin bu siteye ulaşmak için kullandığı adrestir. Bu sunucunun adresi değiştikten sonra güncelleyin; tüm eş siteler yeni adresi alır.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "Bu sitenin adresi değişti. Eski adrese ulaşılamadığı sırada diğer sitelere yazılan nesneler buraya çoğaltılmadı. Her eş sitenin konsolundan bu siteye doğru yeniden eşitleme başlatın.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "{name} sitesinin adresi değişti. Eski adrese ulaşılamadığı sırada bu siteye yazılan nesneler oraya çoğaltılmadı. Bunları kopyalamak için bu site için yeniden eşitleme çalıştırın.", "Path": "Yol", "Force Cleanup Folder": "Klasörü Zorla Temizle", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Bu klasördeki tüm nesneleri, sürümleri, silme işaretlerini ve artık dosyaları kalıcı olarak silin. Bu işlem geri alınamaz.", diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index 1c16b3cb..5f2057d0 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "Nên đồng bộ lại", + "Later": "Để sau", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "Đây là địa chỉ mà các site khác dùng để kết nối tới site này. Hãy cập nhật sau khi địa chỉ của máy chủ này thay đổi; mọi site ngang hàng sẽ nhận địa chỉ mới.", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "Địa chỉ của site này đã thay đổi. Các đối tượng được ghi trên site khác trong lúc không truy cập được địa chỉ cũ chưa được sao chép về đây. Hãy bắt đầu đồng bộ lại hướng tới site này từ console của từng site ngang hàng.", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "Địa chỉ của {name} đã thay đổi. Các đối tượng được ghi trên site này trong lúc không truy cập được địa chỉ cũ chưa được sao chép sang đó. Hãy chạy đồng bộ lại cho site này để sao chép chúng.", "Path": "Đường dẫn", "Force Cleanup Folder": "Buộc dọn dẹp thư mục", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "Xóa vĩnh viễn tất cả đối tượng, phiên bản, dấu xóa và tệp còn sót lại trong thư mục này. Không thể hoàn tác thao tác này.", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index c4b932e2..d3967ca6 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -1,4 +1,9 @@ { + "Resync Recommended": "建议执行重同步", + "Later": "稍后", + "This is the address other sites use to reach this site. Update it after this server's address changes; every peer site receives the new address.": "这是其他站点访问本站点使用的地址。本服务器地址变更后请在此更新,新地址会同步给所有对端站点。", + "This site's address changed. Objects written on other sites while the old address was unreachable were not replicated here. Start a resync toward this site from each peer site's console.": "本站点地址已变更。旧地址不可达期间写入其他站点的对象未复制到本站点。请在每个对端站点的控制台上发起指向本站点的重同步。", + "The address of {name} changed. Objects written on this site while the old address was unreachable were not replicated to it. Run a resync for this site to copy them.": "{name} 的地址已变更。旧地址不可达期间写入本站点的对象未复制到该站点。请对该站点执行重同步以补齐这些对象。", "Path": "路径", "Force Cleanup Folder": "强制清理目录", "Permanently delete all objects, versions, delete markers, and residual files in this folder. This cannot be undone.": "永久删除此目录下的所有对象、版本、删除标记及残留文件。此操作无法撤销。", diff --git a/lib/site-replication-edit.ts b/lib/site-replication-edit.ts new file mode 100644 index 00000000..83a02043 --- /dev/null +++ b/lib/site-replication-edit.ts @@ -0,0 +1,33 @@ +/** + * What an operator should do after a site's endpoint changes. Writes made + * while the old address was unreachable are not replayed automatically: + * - "resync-peer": a peer moved, so this site's writes may be missing there; + * this console can start the resync itself. + * - "resync-from-peers": this site moved, so the peers' writes may be missing + * here; the resync has to be started from each peer site. + */ +export type SiteReplicationResyncAdvice = "none" | "resync-peer" | "resync-from-peers" + +function normalizeEndpoint(endpoint: string): string { + return endpoint.trim().replace(/\/+$/, "").toLowerCase() +} + +export function siteReplicationEndpointChanged(previousEndpoint: string, nextEndpoint: string): boolean { + return normalizeEndpoint(previousEndpoint) !== normalizeEndpoint(nextEndpoint) +} + +export function siteReplicationResyncAdvice({ + isLocal, + previousEndpoint, + nextEndpoint, +}: { + isLocal: boolean + previousEndpoint: string + nextEndpoint: string +}): SiteReplicationResyncAdvice { + if (!siteReplicationEndpointChanged(previousEndpoint, nextEndpoint)) { + return "none" + } + + return isLocal ? "resync-from-peers" : "resync-peer" +} diff --git a/tests/lib/site-replication-edit.test.js b/tests/lib/site-replication-edit.test.js new file mode 100644 index 00000000..f487f5c6 --- /dev/null +++ b/tests/lib/site-replication-edit.test.js @@ -0,0 +1,62 @@ +import assert from "node:assert/strict" +import test from "node:test" +import { readFile } from "node:fs/promises" + +const loadEditHelpers = () => import(new URL("../../lib/site-replication-edit.ts", import.meta.url).href) + +test("site replication endpoint change ignores case, whitespace, and trailing slashes", async () => { + const { siteReplicationEndpointChanged } = await loadEditHelpers() + + assert.equal(siteReplicationEndpointChanged("http://10.0.0.1:9000", " HTTP://10.0.0.1:9000/ "), false) + assert.equal(siteReplicationEndpointChanged("http://10.0.0.1:9000", "http://10.0.1.1:9000"), true) + assert.equal(siteReplicationEndpointChanged("http://10.0.0.1:9000", "https://10.0.0.1:9000"), true) +}) + +test("site replication edit without an endpoint change needs no resync", async () => { + const { siteReplicationResyncAdvice } = await loadEditHelpers() + + assert.equal( + siteReplicationResyncAdvice({ + isLocal: false, + previousEndpoint: "http://10.0.0.2:9000", + nextEndpoint: "http://10.0.0.2:9000/", + }), + "none", + ) +}) + +test("moving a peer site asks this site to resync toward it", async () => { + const { siteReplicationResyncAdvice } = await loadEditHelpers() + + assert.equal( + siteReplicationResyncAdvice({ + isLocal: false, + previousEndpoint: "http://10.0.0.2:9000", + nextEndpoint: "http://192.168.0.2:9000", + }), + "resync-peer", + ) +}) + +test("moving the current site asks the peer sites to resync toward it", async () => { + const { siteReplicationResyncAdvice } = await loadEditHelpers() + + assert.equal( + siteReplicationResyncAdvice({ + isLocal: true, + previousEndpoint: "http://10.0.0.1:9000", + nextEndpoint: "http://192.168.0.1:9000", + }), + "resync-from-peers", + ) +}) + +test("site replication overview lets managers edit the current site but not resync or remove it", async () => { + const source = await readFile(new URL("../../app/(dashboard)/site-replication/page.tsx", import.meta.url), "utf8") + + assert.match(source, /if \(isLocal && !canManageSites\)/) + assert.match(source, /canResyncSites && !isLocal/) + assert.match(source, /canRemoveSites && !isLocal/) + assert.match(source, /isLocal=\{/) + assert.match(source, /siteReplicationResyncAdvice\(/) +})