feat: 网站日志支持读取 10M 以上文件 (#2072)

This commit is contained in:
zhengkunwang 2023-08-25 23:42:14 +08:00 committed by GitHub
parent 680e93dcd4
commit 9f0d957145
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 33 deletions

View file

@ -141,9 +141,11 @@ type WebsiteNginxUpdate struct {
} }
type WebsiteLogReq struct { type WebsiteLogReq struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"` Operate string `json:"operate" validate:"required"`
LogType string `json:"logType" validate:"required"` LogType string `json:"logType" validate:"required"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
} }
type WebsiteDefaultUpdate struct { type WebsiteDefaultUpdate struct {

View file

@ -41,6 +41,7 @@ type WebsiteHTTPS struct {
type WebsiteLog struct { type WebsiteLog struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Content string `json:"content"` Content string `json:"content"`
End bool `json:"end"`
} }
type PHPConfig struct { type PHPConfig struct {

View file

@ -931,19 +931,12 @@ func (w WebsiteService) OpWebsiteLog(req request.WebsiteLogReq) (*response.Websi
} }
} }
filePath := path.Join(sitePath, "log", req.LogType) filePath := path.Join(sitePath, "log", req.LogType)
fileInfo, err := os.Stat(filePath) lines, end, err := files.ReadFileByLine(filePath, req.Page, req.PageSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if fileInfo.Size() > 20<<20 { res.End = end
return nil, buserr.New(constant.ErrFileToLarge) res.Content = strings.Join(lines, "\n")
}
fileInfo.Size()
content, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
res.Content = string(content)
return res, nil return res, nil
case constant.DisableLog: case constant.DisableLog:
key := "access_log" key := "access_log"

View file

@ -1,6 +1,7 @@
package files package files
import ( import (
"bufio"
"github.com/spf13/afero" "github.com/spf13/afero"
"net/http" "net/http"
"os" "os"
@ -75,3 +76,36 @@ const dotCharacter = 46
func IsHidden(path string) bool { func IsHidden(path string) bool {
return path[0] == dotCharacter return path[0] == dotCharacter
} }
func ReadFileByLine(filename string, page, pageSize int) ([]string, bool, error) {
file, err := os.Open(filename)
if err != nil {
return nil, false, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var lines []string
currentLine := 0
startLine := (page - 1) * pageSize
endLine := startLine + pageSize
for scanner.Scan() {
if currentLine >= startLine && currentLine < endLine {
lines = append(lines, scanner.Text())
}
currentLine++
if currentLine >= endLine {
break
}
}
if err := scanner.Err(); err != nil {
return nil, false, err
}
isEndOfFile := currentLine < endLine
return lines, isEndOfFile, nil
}

View file

@ -82,11 +82,14 @@ export namespace Website {
id: number; id: number;
operate: string; operate: string;
logType: string; logType: string;
page?: number;
pageSize?: number;
} }
export interface WebSiteLog { export interface WebSiteLog {
enable: boolean; enable: boolean;
content: string; content: string;
end: boolean;
} }
export interface Domain { export interface Domain {

View file

@ -23,6 +23,7 @@
</div> </div>
<br /> <br />
<codemirror <codemirror
re="logContainer"
style="height: calc(100vh - 430px); width: 100%" style="height: calc(100vh - 430px); width: 100%"
:autofocus="true" :autofocus="true"
:placeholder="$t('website.noLog')" :placeholder="$t('website.noLog')"
@ -33,7 +34,7 @@
theme="cobalt" theme="cobalt"
:styleActiveLine="true" :styleActiveLine="true"
:extensions="extensions" :extensions="extensions"
v-model="data.content" v-model="content"
:disabled="true" :disabled="true"
@ready="handleReady" @ready="handleReady"
/> />
@ -43,7 +44,7 @@
import { Codemirror } from 'vue-codemirror'; import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import { computed, nextTick, onMounted, onUnmounted, ref, shallowRef } from 'vue'; import { computed, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef } from 'vue';
import { OpWebsiteLog } from '@/api/modules/website'; import { OpWebsiteLog } from '@/api/modules/website';
import { dateFormatForName, downloadWithContent } from '@/utils/util'; import { dateFormatForName, downloadWithContent } from '@/utils/util';
import { useDeleteData } from '@/hooks/use-delete-data'; import { useDeleteData } from '@/hooks/use-delete-data';
@ -74,31 +75,52 @@ const tailLog = ref(false);
let timer: NodeJS.Timer | null = null; let timer: NodeJS.Timer | null = null;
const view = shallowRef(); const view = shallowRef();
const editorContainer = ref<HTMLDivElement | null>(null);
const handleReady = (payload) => { const handleReady = (payload) => {
view.value = payload.view; view.value = payload.view;
editorContainer.value = payload.container;
}; };
const content = ref('');
const end = ref(false);
const lastContent = ref('');
const readReq = reactive({
id: id.value,
operate: 'get',
logType: logType.value,
page: 0,
pageSize: 500,
});
const getContent = () => { const getContent = () => {
const req = { if (!end.value) {
id: id.value, readReq.page += 1;
operate: 'get', }
logType: logType.value, OpWebsiteLog(readReq).then((res) => {
}; if (!end.value && res.data.end) {
loading.value = true; lastContent.value = content.value;
OpWebsiteLog(req) }
.then((res) => { data.value = res.data;
data.value = res.data; if (res.data.content != '') {
nextTick(() => { if (end.value) {
const state = view.value.state; content.value = lastContent.value + '\n' + res.data.content;
view.value.dispatch({ } else {
selection: { anchor: state.doc.length, head: state.doc.length }, if (content.value == '') {
scrollIntoView: true, content.value = res.data.content;
}); } else {
content.value = content.value + '\n' + res.data.content;
}
}
}
end.value = res.data.end;
nextTick(() => {
const state = view.value.state;
view.value.dispatch({
selection: { anchor: state.doc.length, head: state.doc.length },
}); });
}) view.value.focus();
.finally(() => {
loading.value = false;
}); });
});
}; };
const changeTail = () => { const changeTail = () => {
@ -152,8 +174,23 @@ const onCloseLog = async () => {
timer = null; timer = null;
}; };
function isScrolledToBottom(element: HTMLElement): boolean {
return element.scrollTop + element.clientHeight === element.scrollHeight;
}
onMounted(() => { onMounted(() => {
getContent(); getContent();
nextTick(() => {
let editorElement = editorContainer.value.querySelector('.cm-editor');
let scrollerElement = editorElement.querySelector('.cm-scroller') as HTMLElement;
if (scrollerElement) {
scrollerElement.addEventListener('scroll', function () {
if (isScrolledToBottom(scrollerElement)) {
getContent();
}
});
}
});
}); });
onUnmounted(() => { onUnmounted(() => {