mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-10 18:05:48 +02:00
feat: config history
This commit is contained in:
parent
771859d3b8
commit
57b8dfd2f9
15 changed files with 823 additions and 75 deletions
13
api/config/history.go
Normal file
13
api/config/history.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/0xJacky/Nginx-UI/model"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/uozi-tech/cosy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetConfigHistory(c *gin.Context) {
|
||||||
|
cosy.Core[model.ConfigBackup](c).
|
||||||
|
SetEqual("filepath").
|
||||||
|
PagingList()
|
||||||
|
}
|
|
@ -47,6 +47,12 @@ func EditConfig(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = config.CheckAndCreateHistory(absPath, content)
|
||||||
|
if err != nil {
|
||||||
|
cosy.ErrHandler(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if content != "" && content != string(origContent) {
|
if content != "" && content != string(origContent) {
|
||||||
err = os.WriteFile(absPath, []byte(content), 0644)
|
err = os.WriteFile(absPath, []byte(content), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -18,4 +18,6 @@ func InitRouter(r *gin.RouterGroup) {
|
||||||
o.POST("config_mkdir", Mkdir)
|
o.POST("config_mkdir", Mkdir)
|
||||||
o.POST("config_rename", Rename)
|
o.POST("config_rename", Rename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.GET("config_histories", GetConfigHistory)
|
||||||
}
|
}
|
||||||
|
|
2
app/components.d.ts
vendored
2
app/components.d.ts
vendored
|
@ -77,6 +77,8 @@ declare module 'vue' {
|
||||||
ChartUsageProgressLine: typeof import('./src/components/Chart/UsageProgressLine.vue')['default']
|
ChartUsageProgressLine: typeof import('./src/components/Chart/UsageProgressLine.vue')['default']
|
||||||
ChatGPTChatGPT: typeof import('./src/components/ChatGPT/ChatGPT.vue')['default']
|
ChatGPTChatGPT: typeof import('./src/components/ChatGPT/ChatGPT.vue')['default']
|
||||||
CodeEditorCodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default']
|
CodeEditorCodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default']
|
||||||
|
ConfigHistoryConfigHistory: typeof import('./src/components/ConfigHistory/ConfigHistory.vue')['default']
|
||||||
|
ConfigHistoryDiffViewer: typeof import('./src/components/ConfigHistory/DiffViewer.vue')['default']
|
||||||
EnvGroupTabsEnvGroupTabs: typeof import('./src/components/EnvGroupTabs/EnvGroupTabs.vue')['default']
|
EnvGroupTabsEnvGroupTabs: typeof import('./src/components/EnvGroupTabs/EnvGroupTabs.vue')['default']
|
||||||
EnvIndicatorEnvIndicator: typeof import('./src/components/EnvIndicator/EnvIndicator.vue')['default']
|
EnvIndicatorEnvIndicator: typeof import('./src/components/EnvIndicator/EnvIndicator.vue')['default']
|
||||||
FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
|
FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
|
import type { GetListResponse } from '@/api/curd'
|
||||||
import type { ChatComplicationMessage } from '@/api/openai'
|
import type { ChatComplicationMessage } from '@/api/openai'
|
||||||
import Curd from '@/api/curd'
|
import Curd from '@/api/curd'
|
||||||
import http from '@/lib/http'
|
import http from '@/lib/http'
|
||||||
|
|
||||||
|
export interface ModelBase {
|
||||||
|
id: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
name: string
|
name: string
|
||||||
content: string
|
content: string
|
||||||
|
@ -13,6 +20,12 @@ export interface Config {
|
||||||
dir: string
|
dir: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigBackup extends ModelBase {
|
||||||
|
name: string
|
||||||
|
filepath: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
class ConfigCurd extends Curd<Config> {
|
class ConfigCurd extends Curd<Config> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('/configs')
|
super('/configs')
|
||||||
|
@ -34,6 +47,10 @@ class ConfigCurd extends Curd<Config> {
|
||||||
sync_node_ids: syncNodeIds,
|
sync_node_ids: syncNodeIds,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_history(filepath: string) {
|
||||||
|
return http.get<GetListResponse<ConfigBackup>>('/config_histories', { params: { filepath } })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: ConfigCurd = new ConfigCurd()
|
const config: ConfigCurd = new ConfigCurd()
|
||||||
|
|
188
app/src/components/ConfigHistory/ConfigHistory.vue
Normal file
188
app/src/components/ConfigHistory/ConfigHistory.vue
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ConfigBackup } from '@/api/config'
|
||||||
|
import type { GetListResponse } from '@/api/curd'
|
||||||
|
import type { Key } from 'ant-design-vue/es/_util/type'
|
||||||
|
import config from '@/api/config'
|
||||||
|
import StdPagination from '@/components/StdDesign/StdDataDisplay/StdPagination.vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { datetime } from '../StdDesign/StdDataDisplay/StdTableTransformer'
|
||||||
|
import DiffViewer from './DiffViewer.vue'
|
||||||
|
|
||||||
|
// Define props for the component
|
||||||
|
const props = defineProps<{
|
||||||
|
filepath: string
|
||||||
|
currentContent?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Define modal props using defineModel with boolean type
|
||||||
|
const visible = defineModel<boolean>('visible')
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const records = ref<ConfigBackup[]>([])
|
||||||
|
const showDiffViewer = ref(false)
|
||||||
|
const pagination = ref({
|
||||||
|
total: 0,
|
||||||
|
per_page: 10,
|
||||||
|
current_page: 1,
|
||||||
|
total_pages: 0,
|
||||||
|
})
|
||||||
|
const selectedRowKeys = ref<Key[]>([])
|
||||||
|
const selectedRecords = ref<ConfigBackup[]>([])
|
||||||
|
|
||||||
|
// Watch for changes in modal visibility and filepath to fetch data
|
||||||
|
watch(() => [visible.value, props.filepath], ([newVisible, newPath]) => {
|
||||||
|
if (newVisible && newPath) {
|
||||||
|
fetchHistoryList()
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Table column definitions
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: () => $gettext('Created At'),
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
customRender: datetime,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Fetch history records list
|
||||||
|
async function fetchHistoryList() {
|
||||||
|
if (!props.filepath)
|
||||||
|
return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await config.get_history(props.filepath)
|
||||||
|
const data = response as GetListResponse<ConfigBackup>
|
||||||
|
records.value = data.data || []
|
||||||
|
|
||||||
|
if (data.pagination) {
|
||||||
|
pagination.value = data.pagination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
message.error($gettext('Failed to load history records'))
|
||||||
|
console.error('Failed to fetch config backup list:', error)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pagination changes
|
||||||
|
function changePage(page: number, pageSize: number) {
|
||||||
|
pagination.value.current_page = page
|
||||||
|
pagination.value.per_page = pageSize
|
||||||
|
fetchHistoryList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row selection handler
|
||||||
|
const rowSelection = computed(() => ({
|
||||||
|
selectedRowKeys: selectedRowKeys.value,
|
||||||
|
onChange: (keys: Key[], selectedRows: ConfigBackup[]) => {
|
||||||
|
// Limit to maximum of two records
|
||||||
|
if (keys.length > 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedRowKeys.value = keys
|
||||||
|
selectedRecords.value = selectedRows
|
||||||
|
},
|
||||||
|
getCheckboxProps: (record: ConfigBackup) => ({
|
||||||
|
disabled: selectedRowKeys.value.length >= 2 && !selectedRowKeys.value.includes(record.id as Key),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Compare selected records
|
||||||
|
function compareSelected() {
|
||||||
|
if (selectedRecords.value.length > 0) {
|
||||||
|
showDiffViewer.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal and reset selection
|
||||||
|
function handleClose() {
|
||||||
|
showDiffViewer.value = false
|
||||||
|
selectedRowKeys.value = []
|
||||||
|
selectedRecords.value = []
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic button text based on selection count
|
||||||
|
const compareButtonText = computed(() => {
|
||||||
|
if (selectedRowKeys.value.length === 0)
|
||||||
|
return $gettext('Compare')
|
||||||
|
if (selectedRowKeys.value.length === 1)
|
||||||
|
return $gettext('Compare with Current')
|
||||||
|
return $gettext('Compare Selected')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AModal
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="$gettext('Configuration History')"
|
||||||
|
:footer="null"
|
||||||
|
@cancel="handleClose"
|
||||||
|
>
|
||||||
|
<div class="history-container">
|
||||||
|
<ATable
|
||||||
|
:loading="loading"
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="records"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
row-key="id"
|
||||||
|
size="small"
|
||||||
|
:pagination="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="history-footer">
|
||||||
|
<StdPagination
|
||||||
|
:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
@change="changePage"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<AButton
|
||||||
|
type="primary"
|
||||||
|
:disabled="selectedRowKeys.length === 0"
|
||||||
|
@click="compareSelected"
|
||||||
|
>
|
||||||
|
{{ compareButtonText }}
|
||||||
|
</AButton>
|
||||||
|
<AButton @click="handleClose">
|
||||||
|
{{ $gettext('Close') }}
|
||||||
|
</AButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AModal>
|
||||||
|
<DiffViewer
|
||||||
|
v-model:visible="showDiffViewer"
|
||||||
|
:records="selectedRecords"
|
||||||
|
:current-content="currentContent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.history-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
415
app/src/components/ConfigHistory/DiffViewer.vue
Normal file
415
app/src/components/ConfigHistory/DiffViewer.vue
Normal file
|
@ -0,0 +1,415 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ConfigBackup } from '@/api/config'
|
||||||
|
import type { Ace } from 'ace-builds'
|
||||||
|
import { formatDateTime } from '@/lib/helper'
|
||||||
|
import ace from 'ace-builds'
|
||||||
|
import 'ace-builds/src-noconflict/mode-nginx'
|
||||||
|
import 'ace-builds/src-noconflict/theme-monokai'
|
||||||
|
|
||||||
|
// Import required modules
|
||||||
|
import 'ace-builds/src-min-noconflict/ext-language_tools'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
records: ConfigBackup[]
|
||||||
|
currentContent?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Define modal visibility using defineModel with boolean type
|
||||||
|
const visible = defineModel<boolean>('visible')
|
||||||
|
|
||||||
|
const originalText = ref('')
|
||||||
|
const modifiedText = ref('')
|
||||||
|
const diffEditorRef = ref<HTMLElement | null>(null)
|
||||||
|
const editors: { left?: Ace.Editor, right?: Ace.Editor } = {}
|
||||||
|
const originalTitle = ref('')
|
||||||
|
const modifiedTitle = ref('')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
// Check if there is content to display
|
||||||
|
function hasContent() {
|
||||||
|
return originalText.value && modifiedText.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set editor content based on selected records
|
||||||
|
function setContent() {
|
||||||
|
if (!props.records || props.records.length === 0) {
|
||||||
|
errorMessage.value = $gettext('No records selected')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Set content based on number of selected records
|
||||||
|
if (props.records.length === 1) {
|
||||||
|
// Single record - compare with current content
|
||||||
|
originalText.value = props.records[0]?.content || ''
|
||||||
|
modifiedText.value = props.currentContent || ''
|
||||||
|
|
||||||
|
// Ensure both sides have content for comparison
|
||||||
|
if (!originalText.value || !modifiedText.value) {
|
||||||
|
errorMessage.value = $gettext('Cannot compare: Missing content')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
originalTitle.value = `${props.records[0]?.name || ''} (${formatDateTime(props.records[0]?.created_at || '')})`
|
||||||
|
modifiedTitle.value = $gettext('Current Content')
|
||||||
|
}
|
||||||
|
else if (props.records.length === 2) {
|
||||||
|
// Compare two records - sort by time
|
||||||
|
const sorted = [...props.records].sort((a, b) =>
|
||||||
|
new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
|
||||||
|
)
|
||||||
|
originalText.value = sorted[0]?.content || ''
|
||||||
|
modifiedText.value = sorted[1]?.content || ''
|
||||||
|
|
||||||
|
// Ensure both sides have content for comparison
|
||||||
|
if (!originalText.value || !modifiedText.value) {
|
||||||
|
errorMessage.value = $gettext('Cannot compare: Missing content')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
originalTitle.value = `${sorted[0]?.name || ''} (${formatDateTime(sorted[0]?.created_at || '')})`
|
||||||
|
modifiedTitle.value = `${sorted[1]?.name || ''} (${formatDateTime(sorted[1]?.created_at || '')})`
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage.value = ''
|
||||||
|
return hasContent()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error setting content:', error)
|
||||||
|
errorMessage.value = $gettext('Error processing content')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create editors
|
||||||
|
function createEditors() {
|
||||||
|
if (!diffEditorRef.value)
|
||||||
|
return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clear editor area
|
||||||
|
diffEditorRef.value.innerHTML = ''
|
||||||
|
|
||||||
|
// Create left and right editor containers
|
||||||
|
const leftContainer = document.createElement('div')
|
||||||
|
leftContainer.style.width = '50%'
|
||||||
|
leftContainer.style.height = '100%'
|
||||||
|
leftContainer.style.float = 'left'
|
||||||
|
leftContainer.style.position = 'relative'
|
||||||
|
|
||||||
|
const rightContainer = document.createElement('div')
|
||||||
|
rightContainer.style.width = '50%'
|
||||||
|
rightContainer.style.height = '100%'
|
||||||
|
rightContainer.style.float = 'right'
|
||||||
|
rightContainer.style.position = 'relative'
|
||||||
|
|
||||||
|
// Add to DOM
|
||||||
|
diffEditorRef.value.appendChild(leftContainer)
|
||||||
|
diffEditorRef.value.appendChild(rightContainer)
|
||||||
|
|
||||||
|
// Create editors
|
||||||
|
editors.left = ace.edit(leftContainer)
|
||||||
|
editors.left.setTheme('ace/theme/monokai')
|
||||||
|
editors.left.getSession().setMode('ace/mode/nginx')
|
||||||
|
editors.left.setReadOnly(true)
|
||||||
|
editors.left.setOption('showPrintMargin', false)
|
||||||
|
|
||||||
|
editors.right = ace.edit(rightContainer)
|
||||||
|
editors.right.setTheme('ace/theme/monokai')
|
||||||
|
editors.right.getSession().setMode('ace/mode/nginx')
|
||||||
|
editors.right.setReadOnly(true)
|
||||||
|
editors.right.setOption('showPrintMargin', false)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error creating editors:', error)
|
||||||
|
errorMessage.value = $gettext('Error initializing diff viewer')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update editor content
|
||||||
|
function updateEditors() {
|
||||||
|
if (!editors.left || !editors.right) {
|
||||||
|
console.error('Editors not available')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if content is empty
|
||||||
|
if (!originalText.value || !modifiedText.value) {
|
||||||
|
console.error('Empty content detected', {
|
||||||
|
originalLength: originalText.value?.length,
|
||||||
|
modifiedLength: modifiedText.value?.length,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set content
|
||||||
|
editors.left.setValue(originalText.value, -1)
|
||||||
|
editors.right.setValue(modifiedText.value, -1)
|
||||||
|
|
||||||
|
// Scroll to top
|
||||||
|
editors.left.scrollToLine(0, false, false)
|
||||||
|
editors.right.scrollToLine(0, false, false)
|
||||||
|
|
||||||
|
// Highlight differences
|
||||||
|
highlightDiffs()
|
||||||
|
|
||||||
|
// Setup sync scroll
|
||||||
|
setupSyncScroll()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error updating editors:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight differences
|
||||||
|
function highlightDiffs() {
|
||||||
|
if (!editors.left || !editors.right)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const leftSession = editors.left.getSession()
|
||||||
|
const rightSession = editors.right.getSession()
|
||||||
|
|
||||||
|
// Clear previous all marks
|
||||||
|
leftSession.clearBreakpoints()
|
||||||
|
rightSession.clearBreakpoints()
|
||||||
|
|
||||||
|
// Add CSS styles
|
||||||
|
addHighlightStyles()
|
||||||
|
|
||||||
|
// Compare lines
|
||||||
|
const leftLines = originalText.value.split('\n')
|
||||||
|
const rightLines = modifiedText.value.split('\n')
|
||||||
|
|
||||||
|
// Use difference comparison algorithm
|
||||||
|
compareAndHighlightLines(leftSession, rightSession, leftLines, rightLines)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error highlighting diffs:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add highlight styles
|
||||||
|
function addHighlightStyles() {
|
||||||
|
const styleId = 'diff-highlight-style'
|
||||||
|
if (!document.getElementById(styleId)) {
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.id = styleId
|
||||||
|
style.textContent = `
|
||||||
|
.diff-line-deleted {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 100, 100, 0.3);
|
||||||
|
z-index: 5;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.diff-line-added {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(100, 255, 100, 0.3);
|
||||||
|
z-index: 5;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.diff-line-changed {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 255, 100, 0.3);
|
||||||
|
z-index: 5;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare and highlight lines
|
||||||
|
function compareAndHighlightLines(leftSession: Ace.EditSession, rightSession: Ace.EditSession, leftLines: string[], rightLines: string[]) {
|
||||||
|
// Create a mapping table to track which lines have been matched
|
||||||
|
const matchedLeftLines = new Set<number>()
|
||||||
|
const matchedRightLines = new Set<number>()
|
||||||
|
|
||||||
|
// 1. First mark completely identical lines
|
||||||
|
for (let i = 0; i < leftLines.length; i++) {
|
||||||
|
for (let j = 0; j < rightLines.length; j++) {
|
||||||
|
if (leftLines[i] === rightLines[j] && !matchedLeftLines.has(i) && !matchedRightLines.has(j)) {
|
||||||
|
matchedLeftLines.add(i)
|
||||||
|
matchedRightLines.add(j)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Mark lines left deleted
|
||||||
|
for (let i = 0; i < leftLines.length; i++) {
|
||||||
|
if (!matchedLeftLines.has(i)) {
|
||||||
|
leftSession.addGutterDecoration(i, 'ace_gutter-active-line')
|
||||||
|
leftSession.addMarker(
|
||||||
|
new ace.Range(i, 0, i, leftLines[i].length || 1),
|
||||||
|
'diff-line-deleted',
|
||||||
|
'fullLine',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Mark lines right added
|
||||||
|
for (let j = 0; j < rightLines.length; j++) {
|
||||||
|
if (!matchedRightLines.has(j)) {
|
||||||
|
rightSession.addGutterDecoration(j, 'ace_gutter-active-line')
|
||||||
|
rightSession.addMarker(
|
||||||
|
new ace.Range(j, 0, j, rightLines[j].length || 1),
|
||||||
|
'diff-line-added',
|
||||||
|
'fullLine',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup sync scroll
|
||||||
|
function setupSyncScroll() {
|
||||||
|
if (!editors.left || !editors.right)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Sync scroll
|
||||||
|
const leftSession = editors.left.getSession()
|
||||||
|
const rightSession = editors.right.getSession()
|
||||||
|
|
||||||
|
leftSession.on('changeScrollTop', (scrollTop: number) => {
|
||||||
|
rightSession.setScrollTop(scrollTop)
|
||||||
|
})
|
||||||
|
|
||||||
|
rightSession.on('changeScrollTop', (scrollTop: number) => {
|
||||||
|
leftSession.setScrollTop(scrollTop)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize difference comparator
|
||||||
|
async function initDiffViewer() {
|
||||||
|
if (!diffEditorRef.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Reset error message
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
// Set content
|
||||||
|
const hasValidContent = setContent()
|
||||||
|
if (!hasValidContent) {
|
||||||
|
console.error('No valid content to compare')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create editors
|
||||||
|
const editorsCreated = createEditors()
|
||||||
|
if (!editorsCreated) {
|
||||||
|
console.error('Failed to create editors')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for DOM update
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// Update editor content
|
||||||
|
const editorsUpdated = updateEditors()
|
||||||
|
if (!editorsUpdated) {
|
||||||
|
console.error('Failed to update editors')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust size to ensure full display
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (editors.left && editors.right) {
|
||||||
|
editors.left.resize()
|
||||||
|
editors.right.resize()
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for records change
|
||||||
|
watch(() => [props.records, visible.value], async () => {
|
||||||
|
if (visible.value) {
|
||||||
|
// When selected records change, update content
|
||||||
|
await nextTick()
|
||||||
|
initDiffViewer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close dialog handler
|
||||||
|
function handleClose() {
|
||||||
|
visible.value = false
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AModal
|
||||||
|
v-model:open="visible"
|
||||||
|
:title="$gettext('Compare Configurations')"
|
||||||
|
width="100%"
|
||||||
|
:footer="null"
|
||||||
|
@cancel="handleClose"
|
||||||
|
>
|
||||||
|
<div v-if="errorMessage" class="diff-error">
|
||||||
|
<AAlert
|
||||||
|
:message="errorMessage"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="diff-container">
|
||||||
|
<div class="diff-header">
|
||||||
|
<div class="diff-title">
|
||||||
|
{{ originalTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="diff-title">
|
||||||
|
{{ modifiedTitle }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="diffEditorRef"
|
||||||
|
class="diff-editor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.diff-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-error {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-title {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 50%;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-editor {
|
||||||
|
height: 500px;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
5
app/src/components/ConfigHistory/index.ts
Normal file
5
app/src/components/ConfigHistory/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import ConfigHistory from './ConfigHistory.vue'
|
||||||
|
import DiffViewer from './DiffViewer.vue'
|
||||||
|
|
||||||
|
export { ConfigHistory, DiffViewer }
|
||||||
|
export default ConfigHistory
|
|
@ -6,6 +6,7 @@ import config from '@/api/config'
|
||||||
import ngx from '@/api/ngx'
|
import ngx from '@/api/ngx'
|
||||||
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
|
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
|
||||||
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
||||||
|
import { ConfigHistory } from '@/components/ConfigHistory'
|
||||||
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
||||||
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
|
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
|
||||||
import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
|
import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
|
||||||
|
@ -13,7 +14,7 @@ import { formatDateTime } from '@/lib/helper'
|
||||||
import { useSettingsStore } from '@/pinia'
|
import { useSettingsStore } from '@/pinia'
|
||||||
import ConfigName from '@/views/config/components/ConfigName.vue'
|
import ConfigName from '@/views/config/components/ConfigName.vue'
|
||||||
import InspectConfig from '@/views/config/InspectConfig.vue'
|
import InspectConfig from '@/views/config/InspectConfig.vue'
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
import { HistoryOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ const origName = ref('')
|
||||||
const addMode = computed(() => !route.params.name)
|
const addMode = computed(() => !route.params.name)
|
||||||
const errors = ref({})
|
const errors = ref({})
|
||||||
|
|
||||||
|
const showHistory = ref(false)
|
||||||
const basePath = computed(() => {
|
const basePath = computed(() => {
|
||||||
if (route.query.basePath)
|
if (route.query.basePath)
|
||||||
return _.trim(route?.query?.basePath?.toString(), '/')
|
return _.trim(route?.query?.basePath?.toString(), '/')
|
||||||
|
@ -192,6 +194,10 @@ function goBack() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openHistory() {
|
||||||
|
showHistory.value = true
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -202,6 +208,19 @@ function goBack() {
|
||||||
:md="18"
|
:md="18"
|
||||||
>
|
>
|
||||||
<ACard :title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')">
|
<ACard :title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')">
|
||||||
|
<template #extra>
|
||||||
|
<AButton
|
||||||
|
v-if="!addMode && data.filepath"
|
||||||
|
type="link"
|
||||||
|
@click="openHistory"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<HistoryOutlined />
|
||||||
|
</template>
|
||||||
|
{{ $gettext('History') }}
|
||||||
|
</AButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
<InspectConfig
|
<InspectConfig
|
||||||
v-show="!addMode"
|
v-show="!addMode"
|
||||||
ref="refInspectConfig"
|
ref="refInspectConfig"
|
||||||
|
@ -315,6 +334,12 @@ function goBack() {
|
||||||
</ACollapse>
|
</ACollapse>
|
||||||
</ACard>
|
</ACard>
|
||||||
</ACol>
|
</ACol>
|
||||||
|
|
||||||
|
<ConfigHistory
|
||||||
|
v-model:visible="showHistory"
|
||||||
|
:filepath="data.filepath"
|
||||||
|
:current-content="data.content"
|
||||||
|
/>
|
||||||
</ARow>
|
</ARow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,11 @@ import config from '@/api/config'
|
||||||
import ngx from '@/api/ngx'
|
import ngx from '@/api/ngx'
|
||||||
import site from '@/api/site'
|
import site from '@/api/site'
|
||||||
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
||||||
|
import { ConfigHistory } from '@/components/ConfigHistory'
|
||||||
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
||||||
import NgxConfigEditor from '@/views/site/ngx_conf/NgxConfigEditor.vue'
|
import NgxConfigEditor from '@/views/site/ngx_conf/NgxConfigEditor.vue'
|
||||||
import RightSettings from '@/views/site/site_edit/RightSettings.vue'
|
import RightSettings from '@/views/site/site_edit/RightSettings.vue'
|
||||||
|
import { HistoryOutlined } from '@ant-design/icons-vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
@ -40,6 +42,8 @@ const data = ref({}) as Ref<Site>
|
||||||
const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
|
const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
|
const showHistory = ref(false)
|
||||||
|
|
||||||
onMounted(init)
|
onMounted(init)
|
||||||
|
|
||||||
const advanceMode = computed({
|
const advanceMode = computed({
|
||||||
|
@ -156,6 +160,10 @@ async function save() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openHistory() {
|
||||||
|
showHistory.value = true
|
||||||
|
}
|
||||||
|
|
||||||
provide('save_config', save)
|
provide('save_config', save)
|
||||||
provide('configText', configText)
|
provide('configText', configText)
|
||||||
provide('ngx_config', ngx_config)
|
provide('ngx_config', ngx_config)
|
||||||
|
@ -192,23 +200,35 @@ provide('data', data)
|
||||||
</ATag>
|
</ATag>
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="mode-switch">
|
<ASpace>
|
||||||
<div class="switch">
|
<AButton
|
||||||
<ASwitch
|
v-if="filepath"
|
||||||
size="small"
|
type="link"
|
||||||
:disabled="parseErrorStatus"
|
@click="openHistory"
|
||||||
:checked="advanceMode"
|
>
|
||||||
:loading
|
<template #icon>
|
||||||
@change="onModeChange"
|
<HistoryOutlined />
|
||||||
/>
|
</template>
|
||||||
|
{{ $gettext('History') }}
|
||||||
|
</AButton>
|
||||||
|
<div class="mode-switch">
|
||||||
|
<div class="switch">
|
||||||
|
<ASwitch
|
||||||
|
size="small"
|
||||||
|
:disabled="parseErrorStatus"
|
||||||
|
:checked="advanceMode"
|
||||||
|
:loading
|
||||||
|
@change="onModeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template v-if="advanceMode">
|
||||||
|
<div>{{ $gettext('Advance Mode') }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div>{{ $gettext('Basic Mode') }}</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="advanceMode">
|
</ASpace>
|
||||||
<div>{{ $gettext('Advance Mode') }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div>{{ $gettext('Basic Mode') }}</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Transition name="slide-fade">
|
<Transition name="slide-fade">
|
||||||
|
@ -273,6 +293,12 @@ provide('data', data)
|
||||||
</AButton>
|
</AButton>
|
||||||
</ASpace>
|
</ASpace>
|
||||||
</FooterToolBar>
|
</FooterToolBar>
|
||||||
|
|
||||||
|
<ConfigHistory
|
||||||
|
v-model:visible="showHistory"
|
||||||
|
:filepath="filepath"
|
||||||
|
:current-content="configText"
|
||||||
|
/>
|
||||||
</ARow>
|
</ARow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,11 @@ import config from '@/api/config'
|
||||||
import ngx from '@/api/ngx'
|
import ngx from '@/api/ngx'
|
||||||
import stream from '@/api/stream'
|
import stream from '@/api/stream'
|
||||||
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
||||||
|
import { ConfigHistory } from '@/components/ConfigHistory'
|
||||||
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
||||||
import NgxConfigEditor from '@/views/site/ngx_conf/NgxConfigEditor.vue'
|
import NgxConfigEditor from '@/views/site/ngx_conf/NgxConfigEditor.vue'
|
||||||
import RightSettings from '@/views/stream/components/RightSettings.vue'
|
import RightSettings from '@/views/stream/components/RightSettings.vue'
|
||||||
|
import { HistoryOutlined } from '@ant-design/icons-vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
@ -39,6 +41,8 @@ const parseErrorStatus = ref(false)
|
||||||
const parseErrorMessage = ref('')
|
const parseErrorMessage = ref('')
|
||||||
const data = ref<Stream>({} as Stream)
|
const data = ref<Stream>({} as Stream)
|
||||||
|
|
||||||
|
const showHistory = ref(false)
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
const advanceMode = computed({
|
const advanceMode = computed({
|
||||||
|
@ -144,6 +148,10 @@ async function save() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openHistory() {
|
||||||
|
showHistory.value = true
|
||||||
|
}
|
||||||
|
|
||||||
provide('save_config', save)
|
provide('save_config', save)
|
||||||
provide('configText', configText)
|
provide('configText', configText)
|
||||||
provide('ngx_config', ngxConfig)
|
provide('ngx_config', ngxConfig)
|
||||||
|
@ -179,22 +187,34 @@ provide('data', data)
|
||||||
</ATag>
|
</ATag>
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="mode-switch">
|
<ASpace>
|
||||||
<div class="switch">
|
<AButton
|
||||||
<ASwitch
|
v-if="filepath"
|
||||||
size="small"
|
type="link"
|
||||||
:disabled="parseErrorStatus"
|
@click="openHistory"
|
||||||
:checked="advanceMode"
|
>
|
||||||
@change="onModeChange"
|
<template #icon>
|
||||||
/>
|
<HistoryOutlined />
|
||||||
|
</template>
|
||||||
|
{{ $gettext('History') }}
|
||||||
|
</AButton>
|
||||||
|
<div class="mode-switch">
|
||||||
|
<div class="switch">
|
||||||
|
<ASwitch
|
||||||
|
size="small"
|
||||||
|
:disabled="parseErrorStatus"
|
||||||
|
:checked="advanceMode"
|
||||||
|
@change="onModeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template v-if="advanceMode">
|
||||||
|
<div>{{ $gettext('Advance Mode') }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div>{{ $gettext('Basic Mode') }}</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="advanceMode">
|
</ASpace>
|
||||||
<div>{{ $gettext('Advance Mode') }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div>{{ $gettext('Basic Mode') }}</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Transition name="slide-fade">
|
<Transition name="slide-fade">
|
||||||
|
@ -256,16 +276,19 @@ provide('data', data)
|
||||||
</AButton>
|
</AButton>
|
||||||
</ASpace>
|
</ASpace>
|
||||||
</FooterToolBar>
|
</FooterToolBar>
|
||||||
|
|
||||||
|
<ConfigHistory
|
||||||
|
v-model:visible="showHistory"
|
||||||
|
:filepath="filepath"
|
||||||
|
:current-content="configText"
|
||||||
|
/>
|
||||||
</ARow>
|
</ARow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.col-right {
|
.col-right {
|
||||||
position: relative;
|
position: sticky;
|
||||||
|
top: 78px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card {
|
.ant-card {
|
||||||
|
|
51
internal/config/history.go
Normal file
51
internal/config/history.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/0xJacky/Nginx-UI/model"
|
||||||
|
"github.com/0xJacky/Nginx-UI/query"
|
||||||
|
"github.com/uozi-tech/cosy/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckAndCreateHistory compares the provided content with the current content of the file
|
||||||
|
// at the specified path and creates a history record if they are different.
|
||||||
|
// The path must be under nginx.GetConfPath().
|
||||||
|
func CheckAndCreateHistory(path string, content string) error {
|
||||||
|
// Check if path is under nginx.GetConfPath()
|
||||||
|
if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
|
||||||
|
return ErrPathIsNotUnderTheNginxConfDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the current content of the file
|
||||||
|
currentContent, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the contents
|
||||||
|
if string(currentContent) == content {
|
||||||
|
// Contents are identical, no need to create history
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contents are different, create a history record (config backup)
|
||||||
|
backup := &model.ConfigBackup{
|
||||||
|
Name: filepath.Base(path),
|
||||||
|
FilePath: path,
|
||||||
|
Content: string(currentContent),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the backup to the database
|
||||||
|
cb := query.ConfigBackup
|
||||||
|
err = cb.Create(backup)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to create config backup:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/config"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/helper"
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/notification"
|
"github.com/0xJacky/Nginx-UI/internal/notification"
|
||||||
|
@ -23,6 +24,11 @@ func Save(name string, content string, overwrite bool, envGroupId uint64, syncNo
|
||||||
return ErrDstFileExists
|
return ErrDstFileExists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = config.CheckAndCreateHistory(path, content)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = os.WriteFile(path, []byte(content), 0644)
|
err = os.WriteFile(path, []byte(content), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/config"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/helper"
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/notification"
|
"github.com/0xJacky/Nginx-UI/internal/notification"
|
||||||
|
@ -23,6 +24,11 @@ func Save(name string, content string, overwrite bool, syncNodeIds []uint64, pos
|
||||||
return ErrDstFileExists
|
return ErrDstFileExists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = config.CheckAndCreateHistory(path, content)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = os.WriteFile(path, []byte(content), 0644)
|
err = os.WriteFile(path, []byte(content), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,45 +1,8 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/uozi-tech/cosy/logger"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConfigBackup struct {
|
type ConfigBackup struct {
|
||||||
Model
|
Model
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
FilePath string `json:"filepath"`
|
FilePath string `json:"filepath" gorm:"column:filepath"`
|
||||||
Content string `json:"content" gorm:"type:text"`
|
Content string `json:"content" gorm:"type:text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigBackupListItem struct {
|
|
||||||
Model
|
|
||||||
Name string `json:"name"`
|
|
||||||
FilePath string `json:"filepath"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetBackupList(path string) (configs []ConfigBackupListItem) {
|
|
||||||
db.Model(&ConfigBackup{}).
|
|
||||||
Where(&ConfigBackup{FilePath: path}).
|
|
||||||
Find(&configs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetBackup(id int) (config ConfigBackup) {
|
|
||||||
db.First(&config, id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateBackup(path string) {
|
|
||||||
content, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
config := ConfigBackup{Name: filepath.Base(path), FilePath: path, Content: string(content)}
|
|
||||||
result := db.Create(&config)
|
|
||||||
if result.Error != nil {
|
|
||||||
logger.Error(result.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue