mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-22 17:45:02 +02:00
UBERF-9107: Add backup list support (#7662)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -202,7 +202,7 @@
|
||||
"name": "docker:build",
|
||||
"phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"],
|
||||
"enableParallelism": true,
|
||||
"incremental": true
|
||||
"incremental": true
|
||||
},
|
||||
{
|
||||
"summary": "docker:rebuild",
|
||||
|
||||
+12
-3
@@ -1,7 +1,7 @@
|
||||
import login, { loginId } from '@hcengineering/login'
|
||||
import { getEmbeddedLabel, getMetadata, setMetadata } from '@hcengineering/platform'
|
||||
import presentation, { closeClient, MessageBox, setDownloadProgress } from '@hcengineering/presentation'
|
||||
import { settingId } from '@hcengineering/setting'
|
||||
import settings, { settingId } from '@hcengineering/setting'
|
||||
import {
|
||||
closePanel,
|
||||
closePopup,
|
||||
@@ -24,7 +24,6 @@ import { isOwnerOrMaintainer } from '@hcengineering/core'
|
||||
import { configurePlatform } from './platform'
|
||||
import { defineScreenShare } from './screenShare'
|
||||
import { IPCMainExposed } from './types'
|
||||
import settings from '@hcengineering/setting'
|
||||
|
||||
defineScreenShare()
|
||||
|
||||
@@ -108,7 +107,17 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
const workspace = getMetadata(presentation.metadata.WorkspaceId)
|
||||
if (isOwnerOrMaintainer()) {
|
||||
if (token != null && endpoint != null && workspace != null) {
|
||||
ipcMain.startBackup(token, endpoint, workspace)
|
||||
// ipcMain.startBackup(token, endpoint, workspace)
|
||||
closePopup()
|
||||
closePanel()
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.fragment = undefined
|
||||
loc.query = undefined
|
||||
loc.path[2] = settingId
|
||||
loc.path[3] = 'setting'
|
||||
loc.path[4] = 'backup'
|
||||
loc.path.length = 5
|
||||
navigate(loc)
|
||||
}
|
||||
} else {
|
||||
showPopup(MessageBox, {
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
"REKONI_URL": "https://rekoni.hc.engineering",
|
||||
"COLLABORATOR_URL": "wss://collaborator.hc.engineering",
|
||||
"STATS_URL": "https://stats.hc.engineering",
|
||||
"PRESENCE_URL": "wss://presence.hc.engineering"
|
||||
"PRESENCE_URL": "wss://presence.hc.engineering",
|
||||
"BACKUP_URL": "https://front.hc.engineering/api/backup"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ import { questionsId } from '@hcengineering/questions'
|
||||
import { recruitId } from '@hcengineering/recruit'
|
||||
import rekoni from '@hcengineering/rekoni'
|
||||
import { requestId } from '@hcengineering/request'
|
||||
import { settingId } from '@hcengineering/setting'
|
||||
import setting, { settingId } from '@hcengineering/setting'
|
||||
import sign from '@hcengineering/sign'
|
||||
import { supportId } from '@hcengineering/support'
|
||||
import { tagsId } from '@hcengineering/tags'
|
||||
@@ -165,6 +165,7 @@ export interface Config {
|
||||
PRESENCE_URL?: string
|
||||
USE_BINARY_PROTOCOL?: boolean,
|
||||
TRANSACTOR_OVERRIDE?: string
|
||||
BACKUP_URL?: string
|
||||
}
|
||||
|
||||
export interface Branding {
|
||||
@@ -433,5 +434,7 @@ export async function configurePlatform() {
|
||||
setMetadata(workbench.metadata.DefaultSpace, myBranding.defaultSpace ?? tracker.project.DefaultProject)
|
||||
setMetadata(workbench.metadata.DefaultSpecial, myBranding.defaultSpecial ?? 'issues')
|
||||
|
||||
setMetadata(setting.metadata.BackupUrl, config.BACKUP_URL ?? '')
|
||||
|
||||
initThemeStore()
|
||||
}
|
||||
|
||||
@@ -226,6 +226,19 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
setting.ids.General
|
||||
)
|
||||
builder.createDoc(
|
||||
setting.class.WorkspaceSettingCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
name: 'backup',
|
||||
label: setting.string.Backup,
|
||||
icon: setting.icon.Setting,
|
||||
component: setting.component.Backup,
|
||||
order: 950,
|
||||
role: AccountRole.Owner
|
||||
},
|
||||
setting.ids.Backup
|
||||
)
|
||||
builder.createDoc(
|
||||
setting.class.WorkspaceSettingCategory,
|
||||
core.space.Model,
|
||||
|
||||
@@ -119,6 +119,17 @@
|
||||
"DeleteSpaceTypeConfirm": "Opravdu chcete tento typ prostoru smazat?",
|
||||
"WorkspaceName": "Název pracovního prostoru",
|
||||
"Workspace": "Pracovní prostor",
|
||||
"OwnerOrMaintainerRequired": "Musíte být vlastníkem nebo správcem pracovního prostoru"
|
||||
"OwnerOrMaintainerRequired": "Musíte být vlastníkem nebo správcem pracovního prostoru",
|
||||
"Backup": "Záloha",
|
||||
"BackupLast": "Poslední záloha",
|
||||
"BackupTotalSnapshots": "Celkem snímků",
|
||||
"BackupTotalFiles": "Soubory",
|
||||
"BackupSize": "Velikost zálohy",
|
||||
"BackupLinkInfo": "Záložní adresář je dostupný na",
|
||||
"BackupBearerTokenInfo": "Pro přístup k záloze je vyžadován přístupový token. Klikněte na tlačítko pro zkopírování tokenu do schránky.",
|
||||
"BackupSnapshots": "Snímky",
|
||||
"BackupFileDownload": "Stáhnout",
|
||||
"BackupFiles": "Soubory",
|
||||
"BackupNoBackup": "Žádná záloha není k dispozici"
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,17 @@
|
||||
"DeleteSpaceTypeConfirm": "Sind Sie sicher, dass Sie diesen Bereichstyp löschen möchten?",
|
||||
"WorkspaceName": "Arbeitsbereich-Name",
|
||||
"Workspace": "Arbeitsbereich",
|
||||
"OwnerOrMaintainerRequired": "Sie müssen Eigentümer oder Betreuer des Arbeitsbereichs sein"
|
||||
"OwnerOrMaintainerRequired": "Sie müssen Eigentümer oder Betreuer des Arbeitsbereichs sein",
|
||||
"Backup": "Sicherung",
|
||||
"BackupLast": "Letzte Sicherung",
|
||||
"BackupTotalSnapshots": "Gesamtanzahl Snapshots",
|
||||
"BackupTotalFiles": "Dateien",
|
||||
"BackupSize": "Sicherungsgröße",
|
||||
"BackupLinkInfo": "Ein Sicherungsverzeichnis ist verfügbar unter",
|
||||
"BackupBearerTokenInfo": "Für den Zugriff auf die Sicherung wird ein Bearer-Token benötigt. Bitte klicken Sie auf die Schaltfläche 'Kopieren', um den Token in die Zwischenablage zu kopieren.",
|
||||
"BackupSnapshots": "Snapshots",
|
||||
"BackupFileDownload": "Herunterladen",
|
||||
"BackupFiles": "Dateien",
|
||||
"BackupNoBackup": "Keine Sicherung verfügbar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,17 @@
|
||||
"DeleteSpaceTypeConfirm": "Are you sure you want to delete this space type?",
|
||||
"WorkspaceName": "Workspace name",
|
||||
"Workspace": "Workspace",
|
||||
"OwnerOrMaintainerRequired": "You need to be a workspace Owner or Maintainer"
|
||||
"OwnerOrMaintainerRequired": "You need to be a workspace Owner or Maintainer",
|
||||
"Backup": "Backup",
|
||||
"BackupLast": "Last backup",
|
||||
"BackupTotalSnapshots": "Total Snapshots",
|
||||
"BackupTotalFiles": "Files",
|
||||
"BackupSize": "Backup size",
|
||||
"BackupLinkInfo": "A backup directory is available at",
|
||||
"BackupBearerTokenInfo": "A bearer token is required to access the backup. Please click copy to clipboard button to copy the token." ,
|
||||
"BackupSnapshots": "Snapshots",
|
||||
"BackupFileDownload": "Download",
|
||||
"BackupFiles": "Files",
|
||||
"BackupNoBackup": "No backup available"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,17 @@
|
||||
"DeleteSpaceTypeConfirm": "¿Estás seguro de que quieres eliminar este tipo de espacio?",
|
||||
"WorkspaceName": "Nombre del espacio de trabajo",
|
||||
"Workspace": "Espacio de trabajo",
|
||||
"OwnerOrMaintainerRequired": "Necesitas ser Propietario o Mantenedor del espacio de trabajo"
|
||||
"OwnerOrMaintainerRequired": "Necesitas ser Propietario o Mantenedor del espacio de trabajo",
|
||||
"Backup": "Copia de seguridad",
|
||||
"BackupLast": "Última copia de seguridad",
|
||||
"BackupTotalSnapshots": "Total de instantáneas",
|
||||
"BackupTotalFiles": "Archivos",
|
||||
"BackupSize": "Tamaño de la copia",
|
||||
"BackupLinkInfo": "El directorio de copias de seguridad está disponible en",
|
||||
"BackupBearerTokenInfo": "Se requiere un token de acceso para acceder a la copia de seguridad. Por favor, haga clic en el botón para copiar el token al portapapeles.",
|
||||
"BackupSnapshots": "Instantáneas",
|
||||
"BackupFileDownload": "Descargar",
|
||||
"BackupFiles": "Archivos",
|
||||
"BackupNoBackup": "No hay copias de seguridad disponibles"
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,17 @@
|
||||
"DeleteSpaceTypeConfirm": "Êtes-vous sûr de vouloir supprimer ce type d'espace ?",
|
||||
"WorkspaceName": "Nom de l'espace de travail",
|
||||
"Workspace": "Espace de travail",
|
||||
"OwnerOrMaintainerRequired": "Vous devez être propriétaire ou responsable d'un espace de travail"
|
||||
"OwnerOrMaintainerRequired": "Vous devez être propriétaire ou responsable d'un espace de travail",
|
||||
"Backup": "Sauvegarde",
|
||||
"BackupLast": "Dernière sauvegarde",
|
||||
"BackupTotalSnapshots": "Total des instantanés",
|
||||
"BackupTotalFiles": "Fichiers",
|
||||
"BackupSize": "Taille de la sauvegarde",
|
||||
"BackupLinkInfo": "Un répertoire de sauvegarde est disponible à",
|
||||
"BackupBearerTokenInfo": "Un jeton d'authentification est nécessaire pour accéder à la sauvegarde. Veuillez cliquer sur le bouton pour copier le jeton dans le presse-papiers.",
|
||||
"BackupSnapshots": "Instantanés",
|
||||
"BackupFileDownload": "Télécharger",
|
||||
"BackupFiles": "Fichiers",
|
||||
"BackupNoBackup": "Aucune sauvegarde disponible"
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,17 @@
|
||||
"DeleteSpaceTypeConfirm": "Sei sicuro di voler eliminare questo tipo di spazio?",
|
||||
"WorkspaceName": "Nome spazio di lavoro",
|
||||
"Workspace": "Spazio di lavoro",
|
||||
"OwnerOrMaintainerRequired": "Devi essere un proprietario o un manutentore dello spazio di lavoro"
|
||||
"OwnerOrMaintainerRequired": "Devi essere un proprietario o un manutentore dello spazio di lavoro",
|
||||
"Backup": "Backup",
|
||||
"BackupLast": "Ultimo backup",
|
||||
"BackupTotalSnapshots": "Totale istantanee",
|
||||
"BackupTotalFiles": "File",
|
||||
"BackupSize": "Dimensione backup",
|
||||
"BackupLinkInfo": "Una directory di backup è disponibile in",
|
||||
"BackupBearerTokenInfo": "È richiesto un token di accesso per accedere al backup. Fare clic sul pulsante per copiare il token negli appunti.",
|
||||
"BackupSnapshots": "Istantanee",
|
||||
"BackupFileDownload": "Scarica",
|
||||
"BackupFiles": "File",
|
||||
"BackupNoBackup": "Nessun backup disponibile"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,17 @@
|
||||
"DeleteSpaceTypeConfirm": "Tem certeza de que deseja excluir este tipo de espaço?",
|
||||
"WorkspaceName": "Nome do espaço de trabalho",
|
||||
"Workspace": "Espaço de trabalho",
|
||||
"OwnerOrMaintainerRequired": "Precisa de ser proprietário ou mantenedor do espaço de trabalho"
|
||||
"OwnerOrMaintainerRequired": "Precisa de ser proprietário ou mantenedor do espaço de trabalho",
|
||||
"Backup": "Backup",
|
||||
"BackupLast": "Último backup",
|
||||
"BackupTotalSnapshots": "Total de instantâneos",
|
||||
"BackupTotalFiles": "Arquivos",
|
||||
"BackupSize": "Tamanho do backup",
|
||||
"BackupLinkInfo": "Um diretório de backup está disponível em",
|
||||
"BackupBearerTokenInfo": "É necessário um token de acesso para acessar o backup. Clique no botão para copiar o token para a área de transferência.",
|
||||
"BackupSnapshots": "Instantâneos",
|
||||
"BackupFileDownload": "Baixar",
|
||||
"BackupFiles": "Arquivos",
|
||||
"BackupNoBackup": "Nenhum backup disponível"
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,17 @@
|
||||
"DeleteSpaceTypeConfirm": "Вы действительно хотите удалить этот тип пространства?",
|
||||
"WorkspaceName": "Название рабочего пространства",
|
||||
"Workspace": "Рабочее пространство",
|
||||
"OwnerOrMaintainerRequired": "Вы должны иметь роль Владельца/Сопровождаемого"
|
||||
"OwnerOrMaintainerRequired": "Вы должны иметь роль Владельца/Сопровождаемого",
|
||||
"Backup": "Резервная копия",
|
||||
"BackupLast": "Последняя резервная копия",
|
||||
"BackupTotalSnapshots": "Всего снимков",
|
||||
"BackupTotalFiles": "Файлы",
|
||||
"BackupSize": "Размер резервной копии",
|
||||
"BackupLinkInfo": "Директория резервных копий доступна по адресу",
|
||||
"BackupBearerTokenInfo": "Для доступа к резервной копии требуется токен авторизации. Пожалуйста, нажмите кнопку, чтобы скопировать токен в буфер обмена.",
|
||||
"BackupSnapshots": "Снимки",
|
||||
"BackupFileDownload": "Скачать",
|
||||
"BackupFiles": "Файлы",
|
||||
"BackupNoBackup": "Резервные копии отсутствуют"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,17 @@
|
||||
"DeleteSpaceTypeConfirm": "您确定要删除此空间类型吗?",
|
||||
"WorkspaceName": "工作区名称",
|
||||
"Workspace": "工作区",
|
||||
"OwnerOrMaintainerRequired": "您需要成为工作空间所有者或维护者"
|
||||
"OwnerOrMaintainerRequired": "您需要成为工作空间所有者或维护者",
|
||||
"Backup": "备份",
|
||||
"BackupLast": "最近备份",
|
||||
"BackupTotalSnapshots": "快照总数",
|
||||
"BackupTotalFiles": "文件",
|
||||
"BackupSize": "备份大小",
|
||||
"BackupLinkInfo": "备份目录位于",
|
||||
"BackupBearerTokenInfo": "访问备份需要访问令牌。请点击复制按钮将令牌复制到剪贴板。",
|
||||
"BackupSnapshots": "快照",
|
||||
"BackupFileDownload": "下载",
|
||||
"BackupFiles": "文件",
|
||||
"BackupNoBackup": "暂无可用备份"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
<!--
|
||||
// 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AccountRole, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { Breadcrumb, Button, Expandable, Header, Label, Link, Loading } from '@hcengineering/ui'
|
||||
import Scroller from '@hcengineering/ui/src/components/Scroller.svelte'
|
||||
import view from '@hcengineering/view'
|
||||
import { onMount } from 'svelte'
|
||||
import setting from '../plugin'
|
||||
import { BackupInfo, BackupSnapshot } from '../types'
|
||||
|
||||
let loading = true
|
||||
|
||||
const backupUrl = getMetadata(setting.metadata.BackupUrl) ?? ''
|
||||
const token = getMetadata(presentation.metadata.Token) ?? ''
|
||||
const owner = hasAccountRole(getCurrentAccount(), AccountRole.Owner)
|
||||
const workspaceId = getMetadata(presentation.metadata.WorkspaceId) ?? ''
|
||||
|
||||
let backupInfo:
|
||||
| {
|
||||
files: { name: string, size: number }[]
|
||||
error?: string
|
||||
info?: BackupInfo
|
||||
}
|
||||
| undefined
|
||||
|
||||
$: fileSizes = new Map(backupInfo?.files?.map((it) => [it.name, it.size ?? 0]))
|
||||
|
||||
function formatDate (timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleString()
|
||||
}
|
||||
|
||||
function getSize (size: number): string {
|
||||
const mbSize = size / (1024 * 1024)
|
||||
|
||||
if (size < 1024) {
|
||||
return `${size}b`
|
||||
}
|
||||
if (size < 1024 * 1024) {
|
||||
return `${Math.round(size / 1024)}kb`
|
||||
}
|
||||
|
||||
return `${Math.round(mbSize * 100) / 100}Mb`
|
||||
}
|
||||
|
||||
function getSnapshotSummary (snapshot: BackupSnapshot): string {
|
||||
let size = 0
|
||||
|
||||
Object.values(snapshot.domains).forEach((domain) => {
|
||||
domain.storage?.forEach((snapshot) => {
|
||||
size += fileSizes.get(snapshot) ?? 0
|
||||
})
|
||||
})
|
||||
return getSize(size)
|
||||
}
|
||||
|
||||
function getBackupSize (files: { name: string, size: number }[]): string {
|
||||
let size = 0
|
||||
|
||||
files.forEach((file) => {
|
||||
size += file.size
|
||||
})
|
||||
|
||||
return getSize(size)
|
||||
}
|
||||
|
||||
async function updateBackupInfo (): Promise<void> {
|
||||
if (owner && backupUrl !== '') {
|
||||
try {
|
||||
const response = await fetch(`${backupUrl}/${workspaceId}/index.json`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
backupInfo = await response.json()
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch backup info:', err)
|
||||
}
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void updateBackupInfo()
|
||||
})
|
||||
|
||||
function formatAgoHours (timestamp: number): string {
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
||||
|
||||
if (diffHours < 24) {
|
||||
if (diffHours === 0) {
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60))
|
||||
return `- ${diffMinutes} minutes ago`
|
||||
}
|
||||
return `- ${diffHours} hours ago`
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
function getFileUrl (filename: string): string {
|
||||
return `${backupUrl}/${workspaceId}/${filename}`
|
||||
}
|
||||
|
||||
async function downloadFile (filename: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(getFileUrl(filename), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (!response.ok) throw new Error('Download failed')
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
} catch (err) {
|
||||
console.error('Failed to download:', err)
|
||||
}
|
||||
}
|
||||
let copied = false
|
||||
|
||||
$: snapshots = backupInfo?.info?.snapshots ?? []
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent p-2 flex-no-shrink">
|
||||
<Header adaptive={'disabled'}>
|
||||
<Breadcrumb icon={setting.icon.Setting} label={setting.string.Backup} size={'large'} isCurrent />
|
||||
</Header>
|
||||
|
||||
<Scroller noStretch>
|
||||
{#if loading}
|
||||
<Loading size={'small'} />
|
||||
{:else if backupInfo == null}
|
||||
<div class="no-backups">
|
||||
<Label label={setting.string.BackupNoBackup} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-row flex-gap-4 p-2">
|
||||
<span class="flex-row-center">
|
||||
<Label label={setting.string.BackupLast} /> : {snapshots.length > 0
|
||||
? formatAgoHours(snapshots[snapshots.length - 1].date)
|
||||
: ''}
|
||||
</span>
|
||||
<span class="flex-row-center">
|
||||
<Label label={setting.string.BackupTotalSnapshots} />: {backupInfo?.info?.snapshots?.length}
|
||||
<Label label={setting.string.BackupTotalFiles} />: {backupInfo?.files?.length ?? 0}
|
||||
</span>
|
||||
<span class="flex-row-center">
|
||||
<Label label={setting.string.BackupSize} />: {getBackupSize(backupInfo?.files ?? [])}
|
||||
</span>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="flex-row-center mt-2">
|
||||
<Label label={setting.string.BackupLinkInfo} />:
|
||||
<a href={getFileUrl('index.html')} target="_blank" rel="noopener noreferrer">
|
||||
<Link label="index.html">
|
||||
{getFileUrl('index.html')}
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<Label label={setting.string.BackupBearerTokenInfo} />
|
||||
</div>
|
||||
<Button
|
||||
label={copied ? view.string.Copied : view.string.CopyToClipboard}
|
||||
on:click={() => {
|
||||
void navigator.clipboard.writeText(token).then(() => {
|
||||
copied = true
|
||||
setTimeout(() => {
|
||||
copied = false
|
||||
}, 2500)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Expandable bordered>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-no-shrink flex-row-center">
|
||||
<Label label={setting.string.BackupSnapshots} />:
|
||||
{snapshots.length}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{#if backupInfo?.info?.snapshots}
|
||||
{#each backupInfo.info.snapshots as snapshot, i}
|
||||
<div class="flex-no-shrink">
|
||||
<Expandable expandable bordered>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center">
|
||||
#{i}
|
||||
{formatDate(snapshot.date)}
|
||||
{formatAgoHours(snapshot.date)} -
|
||||
{getSnapshotSummary(snapshot)}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="p-2">
|
||||
{#if snapshot.domains}
|
||||
{#each Object.entries(snapshot.domains) as [domain, data]}
|
||||
{#if data.storage?.length}
|
||||
<div class="domain-files">
|
||||
<div class="domain-header">{domain}</div>
|
||||
<div class="files-list">
|
||||
{#each data.storage as filename}
|
||||
<div class="file-item">
|
||||
<div class="file-name">{filename}</div>
|
||||
<div class="file-info">
|
||||
<span class="file-size">
|
||||
{getSize(fileSizes.get(filename) ?? 0)}
|
||||
</span>
|
||||
<div class="file-actions">
|
||||
<Button
|
||||
label={setting.string.BackupFileDownload}
|
||||
on:click={() => downloadFile(filename)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Expandable>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="no-backups">
|
||||
<Label label={setting.string.BackupNoBackup} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if backupInfo?.error}
|
||||
<div class="error">{backupInfo.error}</div>
|
||||
{/if}
|
||||
</Expandable>
|
||||
|
||||
<Expandable bordered>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-no-shrink flex-row-center">
|
||||
<Label label={setting.string.BackupFiles} />:
|
||||
{backupInfo?.files.length}
|
||||
{getBackupSize(backupInfo?.files ?? [])}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{#each backupInfo?.files ?? [] as file}
|
||||
<div class="file-item">
|
||||
<div class="file-name">{file.name}</div>
|
||||
<div class="file-info">
|
||||
<span class="file-size">
|
||||
{getSize(file.size ?? 0)}
|
||||
</span>
|
||||
<div class="file-actions">
|
||||
<Button label={setting.string.BackupFileDownload} on:click={() => downloadFile(file.name)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Expandable>
|
||||
{/if}
|
||||
</Scroller>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.error {
|
||||
color: var(--theme-error-color);
|
||||
padding: 1rem;
|
||||
background-color: var(--theme-bg-accent);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.domain-files {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.domain-header {
|
||||
font-weight: 500;
|
||||
color: var(--theme-content-accent);
|
||||
}
|
||||
|
||||
.files-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--theme-bg-accent);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--theme-content-accent);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.file-link,
|
||||
.download-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
text-decoration: none;
|
||||
color: var(--theme-content-accent);
|
||||
background: var(--theme-button-bg);
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-button-bg-hover);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -24,6 +24,7 @@ import EditEnum from './components/EditEnum.svelte'
|
||||
import EnumSetting from './components/EnumSetting.svelte'
|
||||
import Integrations from './components/Integrations.svelte'
|
||||
import General from './components/General.svelte'
|
||||
import Backup from './components/Backup.svelte'
|
||||
import Owners from './components/Owners.svelte'
|
||||
import Password from './components/Password.svelte'
|
||||
import Privacy from './components/Privacy.svelte'
|
||||
@@ -110,6 +111,7 @@ export default async (): Promise<Resources> => ({
|
||||
EditEnum,
|
||||
EnumSetting,
|
||||
General,
|
||||
Backup,
|
||||
Owners,
|
||||
CreateMixin,
|
||||
InviteSetting,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Snapshot {
|
||||
added: Map<string, string>
|
||||
updated: Map<string, string>
|
||||
removed: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SnapshotV6 {
|
||||
added: Record<string, string>
|
||||
updated: Record<string, string>
|
||||
removed: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DomainData {
|
||||
snapshot?: string // 0.6 json snapshot
|
||||
snapshots?: string[]
|
||||
storage?: string[]
|
||||
|
||||
// Some statistics
|
||||
added: number
|
||||
updated: number
|
||||
removed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface BackupSnapshot {
|
||||
// _id => hash of added items.
|
||||
domains: Record<string, DomainData>
|
||||
date: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface BackupInfo {
|
||||
workspace: string
|
||||
version: string
|
||||
snapshots: BackupSnapshot[]
|
||||
snapshotsIndex?: number
|
||||
lastTxId?: string
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import type { Account, AccountRole, Blob, Class, Configuration, Doc, Mixin, Ref } from '@hcengineering/core'
|
||||
import type { Plugin } from '@hcengineering/platform'
|
||||
import type { Metadata, Plugin } from '@hcengineering/platform'
|
||||
import { Asset, IntlString, Resource, plugin } from '@hcengineering/platform'
|
||||
import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
@@ -133,7 +133,8 @@ export default plugin(settingId, {
|
||||
InviteSettings: '' as Ref<Doc>,
|
||||
WorkspaceSetting: '' as Ref<Doc>,
|
||||
ManageSpaces: '' as Ref<Doc>,
|
||||
Spaces: '' as Ref<Doc>
|
||||
Spaces: '' as Ref<Doc>,
|
||||
Backup: '' as Ref<Doc>
|
||||
},
|
||||
mixin: {
|
||||
Editable: '' as Ref<Mixin<Editable>>,
|
||||
@@ -166,7 +167,8 @@ export default plugin(settingId, {
|
||||
SpaceTypeRolesSectionEditor: '' as AnyComponent,
|
||||
RoleEditor: '' as AnyComponent,
|
||||
RoleAssignmentEditor: '' as AnyComponent,
|
||||
RelationSetting: '' as AnyComponent
|
||||
RelationSetting: '' as AnyComponent,
|
||||
Backup: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Settings: '' as IntlString,
|
||||
@@ -206,7 +208,18 @@ export default plugin(settingId, {
|
||||
Collections: '' as IntlString,
|
||||
SpaceTypes: '' as IntlString,
|
||||
Roles: '' as IntlString,
|
||||
OwnerOrMaintainerRequired: '' as IntlString
|
||||
OwnerOrMaintainerRequired: '' as IntlString,
|
||||
Backup: '' as IntlString,
|
||||
BackupLast: '' as IntlString,
|
||||
BackupTotalSnapshots: '' as IntlString,
|
||||
BackupTotalFiles: '' as IntlString,
|
||||
BackupSize: '' as IntlString,
|
||||
BackupLinkInfo: '' as IntlString,
|
||||
BackupBearerTokenInfo: '' as IntlString,
|
||||
BackupSnapshots: '' as IntlString,
|
||||
BackupFileDownload: '' as IntlString,
|
||||
BackupFiles: '' as IntlString,
|
||||
BackupNoBackup: '' as IntlString
|
||||
},
|
||||
icon: {
|
||||
AccountSettings: '' as Asset,
|
||||
@@ -234,5 +247,8 @@ export default plugin(settingId, {
|
||||
OwnerLastName: '' as Ref<TemplateField>,
|
||||
OwnerPosition: '' as Ref<TemplateField>,
|
||||
Value: '' as Ref<TemplateField>
|
||||
},
|
||||
metadata: {
|
||||
BackupUrl: '' as Metadata<string>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -46,5 +46,6 @@ startFront(metricsContext, {
|
||||
ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL,
|
||||
AI_URL: process.env.AI_URL,
|
||||
TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_URL,
|
||||
STATS_URL: process.env.STATS_API ?? process.env.STATS_URL
|
||||
STATS_URL: process.env.STATS_API ?? process.env.STATS_URL,
|
||||
BACKUP_URL: process.env.BACKUP_URL
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user