main #2

Merged
jingrow merged 250 commits from main into v1 2026-01-13 22:45:50 +08:00
3 changed files with 190 additions and 158 deletions
Showing only changes of commit 057d8164f5 - Show all commits

View File

@ -45,7 +45,7 @@ function getSiteActionHandler(action) {
'使用文件恢复': defineAsyncComponent(() =>
import('./SiteDatabaseRestoreDialog.vue')
),
'从现有站点恢复': defineAsyncComponent(() =>
'从指定站点恢复': defineAsyncComponent(() =>
import('./site/SiteDatabaseRestoreFromURLDialog.vue')
),
'管理数据库用户': defineAsyncComponent(() =>

View File

@ -1,156 +1,188 @@
<template>
<Dialog
:options="{
title: '从现有站点恢复'
}"
v-model="showRestoreDialog"
>
<template #body-content>
<div
class="mb-6 flex items-center rounded border border-gray-200 bg-gray-100 p-4 text-sm text-gray-600"
>
<i-lucide-alert-triangle class="mr-4 inline-block h-6 w-6" />
<div>
此操作将用备份中的<b>数据</b><b>应用</b>替换您站点中的当前内容
</div>
</div>
<div class="space-y-4">
<FormControl label="站点URL" v-model="siteURL" />
<FormControl label="邮箱" v-model="email" />
<FormControl label="密码" type="password" v-model="password" />
<div class="flex text-base" v-if="$resources.getBackupLinks.data">
<GreenCheckIcon class="mr-2 w-4" />
找到来自 {{ fetchedBackupFileTimestamp }} 的最新备份
</div>
<Button
v-else
@click="$resources.getBackupLinks.submit()"
:loading="$resources.getBackupLinks.loading"
>
获取备份
</Button>
</div>
<div class="mt-3">
<FormControl
label="跳过失败的补丁(如果有任何补丁失败)"
type="checkbox"
v-model="skipFailingPatches"
/>
</div>
<ErrorMessage
class="mt-2"
:message="
$resources.restoreBackup.error || $resources.getBackupLinks.error
"
/>
</template>
<template #actions>
<Button
class="w-full"
label="恢复"
variant="solid"
theme="red"
:loading="$resources.restoreBackup.loading"
:disabled="!$resources.getBackupLinks.data"
@click="$resources.restoreBackup.submit"
/>
</template>
</Dialog>
</template>
<script>
import { date } from '../../utils/format';
import { DashboardError } from '../../utils/error';
export default {
name: 'SiteDatabaseRestoreDialog',
props: {
site: {
type: String,
required: true
}
},
data() {
return {
siteURL: '',
email: '',
password: '',
selectedFiles: {
database: null,
public: null,
private: null
},
showRestoreDialog: true,
skipFailingPatches: false
};
},
resources: {
getBackupLinks() {
return {
url: 'jcloud.api.site.get_backup_links',
params: {
url: this.siteURL,
email: this.email,
password: this.password
},
validate() {
if (!this.siteURL) {
throw new DashboardError('站点URL是必填项');
}
if (!this.email) {
throw new DashboardError('邮箱是必填项');
}
if (!this.password) {
throw new DashboardError('密码是必填项');
}
},
onSuccess(remoteFiles) {
for (let file of remoteFiles) {
this.selectedFiles[file.type] = file.remote_file;
}
}
};
},
restoreBackup() {
return {
url: 'jcloud.api.site.restore',
params: {
name: this.site,
files: this.selectedFiles,
skip_failing_patches: this.skipFailingPatches
},
validate() {
if (!this.selectedFiles.database) {
throw new DashboardError(
'从站点获取备份时出错'
);
}
},
onSuccess() {
this.siteURL = '';
this.email = '';
this.password = '';
this.showRestoreDialog = false;
this.$router.push({
name: 'Site Jobs',
params: { name: this.site }
});
}
};
}
},
computed: {
fetchedBackupFileTimestamp() {
if (!this.$resources.getBackupLinks.data) return '';
let backup = this.$resources.getBackupLinks.data[0];
let timestamp_string = backup.file_name
.split('-')[0]
.split('_')
.join('T');
return date(timestamp_string);
}
}
};
<template>
<Dialog
:options="{
title: '从指定站点恢复'
}"
v-model="showRestoreDialog"
>
<template #body-content>
<div
class="mb-6 flex items-center rounded border border-gray-200 bg-gray-100 p-4 text-sm text-gray-600"
>
<i-lucide-alert-triangle class="mr-4 inline-block h-6 w-6" />
<div>
此操作将用备份中的<b>数据</b><b>应用</b>替换您站点中的当前内容
</div>
</div>
<div class="space-y-4">
<FormControl label="站点URL" v-model="siteURL" />
<FormControl label="邮箱" v-model="email" />
<FormControl label="密码" type="password" v-model="password" />
<div class="flex text-base" v-if="$resources.getBackupLinks.data">
<GreenCheckIcon class="mr-2 w-4" />
找到来自 {{ fetchedBackupFileTimestamp }} 的最新备份
</div>
<Button
v-else
@click="$resources.getBackupLinks.submit()"
:loading="$resources.getBackupLinks.loading"
>
获取备份
</Button>
</div>
<div class="mt-3">
<FormControl
label="跳过失败的补丁(如果有任何补丁失败)"
type="checkbox"
v-model="skipFailingPatches"
/>
</div>
<ErrorMessage
class="mt-2"
:message="
$resources.restoreBackup.error || $resources.getBackupLinks.error
"
/>
</template>
<template #actions>
<Button
class="w-full"
label="恢复"
variant="solid"
theme="red"
:loading="$resources.restoreBackup.loading"
:disabled="!$resources.getBackupLinks.data"
@click="$resources.restoreBackup.submit"
/>
</template>
</Dialog>
</template>
<script>
import { date } from '../../utils/format';
import { DashboardError } from '../../utils/error';
export default {
name: 'SiteDatabaseRestoreDialog',
props: {
site: {
type: String,
required: true
}
},
data() {
return {
siteURL: '',
email: '',
password: '',
selectedFiles: {
database: null,
public: null,
private: null
},
showRestoreDialog: true,
skipFailingPatches: false
};
},
resources: {
getBackupLinks() {
return {
url: 'jcloud.api.site.get_backup_links',
params: {
url: this.siteURL,
email: this.email,
password: this.password
},
validate() {
if (!this.siteURL) {
throw new DashboardError('站点URL是必填项');
}
if (!this.email) {
throw new DashboardError('邮箱是必填项');
}
if (!this.password) {
throw new DashboardError('密码是必填项');
}
},
onSuccess(remoteFiles) {
for (let file of remoteFiles) {
this.selectedFiles[file.type] = file.remote_file;
}
}
};
},
restoreBackup() {
return {
url: 'jcloud.api.site.restore',
params: {
name: this.site,
files: this.selectedFiles,
skip_failing_patches: this.skipFailingPatches
},
validate() {
if (!this.selectedFiles.database) {
throw new DashboardError(
'从站点获取备份时出错'
);
}
},
onSuccess() {
this.siteURL = '';
this.email = '';
this.password = '';
this.showRestoreDialog = false;
this.$router.push({
name: 'Site Jobs',
params: { name: this.site }
});
}
};
}
},
computed: {
fetchedBackupFileTimestamp() {
if (!this.$resources.getBackupLinks.data) return '';
let backup = this.$resources.getBackupLinks.data[0];
if (!backup || !backup.file_name) return '';
let timestamp_string = '';
// YYYYMMDD_HHMMSS
let timestampMatch = backup.file_name.match(/(\d{8}_\d{6})/);
if (timestampMatch) {
let match = timestampMatch[1];
let year = match.substring(0, 4);
let month = match.substring(4, 6);
let day = match.substring(6, 8);
let hour = match.substring(9, 11);
let minute = match.substring(11, 13);
let second = match.substring(13, 15);
timestamp_string = `${year}-${month}-${day}T${hour}:${minute}:${second}`;
} else {
// YYYY-MM-DD_HH-MM-SS
let oldFormatMatch = backup.file_name.match(/(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})/);
if (oldFormatMatch) {
timestamp_string = oldFormatMatch[1].replace('_', 'T').replace(/-/g, '-');
} else {
// YYYY-MM-DD
let dateMatch = backup.file_name.match(/(\d{4}-\d{2}-\d{2})/);
if (dateMatch) {
timestamp_string = dateMatch[1] + 'T00:00:00';
} else {
//
return backup.file_name;
}
}
}
try {
return date(timestamp_string);
} catch (e) {
//
return backup.file_name;
}
}
}
};
</script>

View File

@ -2783,8 +2783,8 @@ class Site(Page, TagHelpers):
"group": "危险操作",
},
{
"action": "现有站点恢复",
"description": "另一个站点恢复数据库、公共和私有文件",
"action": "指定站点恢复",
"description": "指定站点的备份文件恢复数据库、公共和私有文件",
"button_label": "恢复",
"pg_method": "restore_site_from_files",
"group": "危险操作",