gin framework

This commit is contained in:
2021-05-18 13:06:40 +08:00
parent 968c92d479
commit ae93d82716
18 changed files with 506 additions and 1 deletions

5
.gitignore vendored
View File

@@ -14,4 +14,7 @@
# Dependency directories (remove the comment below to include it)
# vendor/
*.db
/wiki
/page/*
__debug_bin

19
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,19 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "exec",
"program": "${workspaceFolder}/${workspaceFolderBasename}",
"preLaunchTask": "build",
"env": {
"GIN_MODE": "release"
}
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"go.testFlags": ["-v"]
}

15
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "go",
"args": [
"build", "-v", "-o", "${workspaceFolderBasename}"
],
}
]
}

BIN
assets/avatars/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/avatars/user.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

1
assets/css/ace.min.css vendored Normal file

File diff suppressed because one or more lines are too long

5
assets/js/ace-elements.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
assets/js/ace-extra.min.js vendored Normal file

File diff suppressed because one or more lines are too long

5
assets/js/ace.min.js vendored Normal file

File diff suppressed because one or more lines are too long

10
go.mod Normal file
View File

@@ -0,0 +1,10 @@
module wiki
go 1.16
require (
github.com/fsnotify/fsnotify v1.4.9
github.com/gin-gonic/gin v1.7.2
github.com/unknwon/i18n v0.0.0-20210321134014-0ebbf2df1c44
golang.org/x/text v0.3.3
)

78
go.sum Normal file
View File

@@ -0,0 +1,78 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
github.com/unknwon/i18n v0.0.0-20210321134014-0ebbf2df1c44 h1:7bSo/vjZKVYUoZfxpYh9Y9JV+9hi1gYJp4WIykhrZag=
github.com/unknwon/i18n v0.0.0-20210321134014-0ebbf2df1c44/go.mod h1:+5rDk6sDGpl3azws3O+f+GpFSyN9GVr0K8cvQLQM2ZQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

6
i18n/en-US.ini Normal file
View File

@@ -0,0 +1,6 @@
[nav]
settings = Settings
logout = Logout
home = Home
search = Search...
welcome = Welcome

6
i18n/zh-CN.ini Normal file
View File

@@ -0,0 +1,6 @@
[nav]
settings = 设置
logout = 注销
home = 首页
search = 搜索...
welcome = 欢迎

0
page/.gitkeep Normal file
View File

21
views/404.html Normal file
View File

@@ -0,0 +1,21 @@
<div class="row">
<div class="col-xs-12">
<div class="error-container">
<div class="well">
<h1 class="grey lighter smaller">
<span class="blue bigger-125">
<i class="ace-icon fa fa-sitemap"></i> {{.Code}}
</span>
页面未找到
</h1>
<hr />
<h3 class="lighter smaller">We looked everywhere but we couldn't find it!</h3>
</div>
</div>
</div><!-- /.col -->
</div><!-- /.row -->
<script type="text/javascript">
$('.page-content-area').ace_ajax('loadScripts', scripts, function () {
//inline scripts related to this page
});
</script>

176
views/layout.html Normal file
View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>Go Wiki</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<!-- bootstrap & fontawesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- ace styles -->
<link rel="stylesheet" href="/assets/css/ace.min.css" class="ace-main-stylesheet" />
<script src="/assets/js/ace-extra.min.js"></script>
</head>
<body class="no-skin">
<div id="navbar" class="navbar navbar-default">
<div class="navbar-container ace-save-state" id="navbar-container">
<button type="button" class="navbar-toggle menu-toggler pull-left" id="menu-toggler" data-target="#sidebar">
<span class="sr-only">Toggle sidebar</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<div class="navbar-header pull-left">
<a href="#" class="navbar-brand">
<small><i class="fa fa-leaf"></i> Go Wiki</small>
</a>
</div>
<div class="navbar-buttons navbar-header pull-right" role="navigation">
<ul class="nav ace-nav">
<li class="light-blue dropdown-modal">
<a data-toggle="dropdown" href="#" class="dropdown-toggle">
<img class="nav-user-photo" src="/assets/avatars/user.jpg" alt="Jason's Photo" />
<span class="user-info"><small>{{$.i18n.Tr "nav.welcome"}}</small> Admin</span>
<i class="ace-icon fa fa-caret-down"></i>
</a>
<ul class="user-menu dropdown-menu-right dropdown-menu dropdown-yellow dropdown-caret dropdown-close">
<li>
<a href="#">
<i class="ace-icon fa fa-cog"></i> {{$.i18n.Tr "nav.settings"}}
</a>
</li>
<li class="divider"></li>
<li>
<a href="#">
<i class="ace-icon fa fa-power-off"></i> {{$.i18n.Tr "nav.logout"}}
</a>
</li>
</ul>
</li>
</ul>
</div>
</div><!-- /.navbar-container -->
</div>
<div class="main-container ace-save-state" id="main-container">
<script type="text/javascript">
try { ace.settings.check('main-container', 'fixed') } catch (e) {}
</script>
<div id="sidebar" class="sidebar responsive ace-save-state">
<script type="text/javascript">
try { ace.settings.loadState('sidebar') } catch (e) { }
</script>
<div class="sidebar-shortcuts" id="sidebar-shortcuts">
<div class="sidebar-shortcuts-large" id="sidebar-shortcuts-large">
<button class="btn btn-success">
<i class="ace-icon fa fa-signal"></i>
</button>
<button class="btn btn-info">
<i class="ace-icon fa fa-pencil"></i>
</button>
<button class="btn btn-warning">
<i class="ace-icon fa fa-users"></i>
</button>
<button class="btn btn-danger">
<i class="ace-icon fa fa-cogs"></i>
</button>
</div>
<div class="sidebar-shortcuts-mini" id="sidebar-shortcuts-mini">
<span class="btn btn-success"></span>
<span class="btn btn-info"></span>
<span class="btn btn-warning"></span>
<span class="btn btn-danger"></span>
</div>
</div><!-- /.sidebar-shortcuts -->
<ul class="nav nav-list">
{{- template "menu" .Menu}}
</ul><!-- /.nav-list -->
<div class="sidebar-toggle sidebar-collapse" id="sidebar-collapse">
<i id="sidebar-toggle-icon" class="ace-icon fa fa-angle-double-left ace-save-state" data-icon1="ace-icon fa fa-angle-double-left" data-icon2="ace-icon fa fa-angle-double-right"></i>
</div>
</div>
<div class="main-content">
<div class="main-content-inner">
<div class="breadcrumbs ace-save-state" id="breadcrumbs">
<ul class="breadcrumb">
<li>
<i class="ace-icon fa fa-home home-icon"></i>
<a href="#">{{$.i18n.Tr "nav.home"}}</a>
</li>
</ul><!-- /.breadcrumb -->
<div class="nav-search" id="nav-search">
<form class="form-search">
<span class="input-icon">
<input type="text" placeholder="{{$.i18n.Tr "nav.search"}}" class="nav-search-input" id="nav-search-input" autocomplete="off" />
<i class="ace-icon fa fa-search nav-search-icon"></i>
</span>
</form>
</div><!-- /.nav-search -->
</div>
<div class="page-content">
<div class="page-content-area" data-ajax-content="true"></div>
</div>
</div>
</div>
<div class="footer">
<div class="footer-inner">
<div class="footer-content">
<span class="bigger-120">
<span class="blue bolder">Ace</span>
Application &copy; 2021
</span>
&nbsp; &nbsp;
<span class="action-buttons">
<a href="#">
<i class="ace-icon fa fa-twitter-square light-blue bigger-150"></i>
</a>
<a href="#">
<i class="ace-icon fa fa-facebook-square text-primary bigger-150"></i>
</a>
<a href="#">
<i class="ace-icon fa fa-rss-square orange bigger-150"></i>
</a>
</span>
</div>
</div>
</div>
<a href="#" id="btn-scroll-up" class="btn-scroll-up btn btn-sm btn-inverse">
<i class="ace-icon fa fa-angle-double-up icon-only bigger-110"></i>
</a>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script type="text/javascript">
if ('ontouchstart' in document.documentElement) document.write("<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery-mobile/1.4.1/jquery.mobile.min.js'>" + "<" + "/script>");
</script>
<script src="/assets/js/ace-elements.min.js"></script>
<script src="/assets/js/ace.min.js"></script>
</body>
</html>
{{define "menu"}}
{{- range .}}
{{- if not .Status}}
{{- else if and .Child (not .Path)}}
<li>
<a href="#" class="dropdown-toggle">
<i class="menu-icon {{.Icon}}"></i>
<span class="menu-text"> {{.Name}} </span>
<b class="arrow fa fa-angle-down"></b>
</a>
<b class="arrow"></b>
<ul class="submenu">
{{- template "menu" .Child}}
</ul>
{{- else}}
<li>
<a data-url="{{.Path}}" href="#{{.Path}}">
<i class="menu-icon {{.Icon}}"></i>
<span class="menu-text"> {{.Name}} </span>
</a>
</li>
{{- end}}
</li>
{{- end}}
{{- end}}

156
wiki.go Normal file
View File

@@ -0,0 +1,156 @@
package main
import (
"context"
"embed"
"fmt"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"path"
"path/filepath"
"strings"
"syscall"
_ "time/tzdata"
"github.com/fsnotify/fsnotify"
"github.com/gin-gonic/gin"
"github.com/unknwon/i18n"
"golang.org/x/text/language"
)
var (
//go:embed assets/*/* views/*.html i18n/*.ini
content embed.FS
root Menu
)
// Node 节点模型
type Node struct {
Name string `gorm:"size:64;not null"`
Icon string `gorm:"size:32;default:fa fa-circlo-o" yaml:",omitempty"`
Path string `gorm:"size:255"`
Child []Node `gorm:"-" yaml:",omitempty"`
Status bool `gorm:"default:false;not null"`
}
type Menu []Node
func main() {
// 加载语言文件
localizer, err := loadI18n("i18n/*.ini")
if err != nil {
log.Fatalf("load i18n %v", err)
}
// 加载模板
t, err := template.ParseFS(content, "views/*.html")
if err != nil {
log.Fatalf("parse template %v", err)
}
r := gin.New()
// 路由日志格式化
r.Use(gin.LoggerWithFormatter(func(p gin.LogFormatterParams) string {
return fmt.Sprintf("%s %s %d %s %d %s (%s)\n%s",
p.TimeStamp.Format("2006/01/02 15:04:05"), p.Method, p.StatusCode,
p.Path, p.BodySize, p.ClientIP, p.Latency, p.ErrorMessage,
)
}), localizer, gin.Recovery())
r.SetHTMLTemplate(t)
// 静态文件
r.GET("/assets/*filepath", func(c *gin.Context) {
c.Header("Cache-Control", "public, max-age=3600")
c.FileFromFS(c.Request.URL.Path, http.FS(content))
})
r.GET("/", func(c *gin.Context) {
page := strings.TrimSpace(c.Query("page"))
if len(page) == 0 {
c.Set("Notify", make([]string, 5))
c.Set("Menu", root)
c.HTML(http.StatusOK, "layout.html", c.Keys)
return
}
c.Set("Code", http.StatusNotFound)
c.HTML(http.StatusOK, "404.html", c.Keys)
})
ctx, cancel := context.WithCancel(context.Background())
if err = watchDocs(ctx, "page/*md"); err != nil {
log.Fatalf("load markdown %v", err)
}
// 监听并在 0.0.0.0:8080 上启动服务
srv := &http.Server{Addr: ":8080", Handler: r}
srv.RegisterOnShutdown(cancel)
// 监听进程退出
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, syscall.SIGTERM, syscall.SIGINT)
go srv.ListenAndServe()
log.Printf("Listening and serving HTTP on %s", srv.Addr)
<-sigint
if err := srv.Shutdown(context.Background()); err != nil {
log.Printf("HTTP server Shutdown: %v", err)
}
}
func loadI18n(pattern string) (gin.HandlerFunc, error) {
list, err := fs.Glob(content, pattern)
if err != nil {
return nil, err
}
// 加载语言文件
for _, name := range list {
f, err := content.ReadFile(name)
if err != nil {
return nil, err
}
base, ext := filepath.Base(name), filepath.Ext(name)
i18n.SetMessage(strings.TrimSuffix(base, ext), f)
}
return func(c *gin.Context) {
tags, _, _ := language.ParseAcceptLanguage(c.GetHeader("Accept-Language"))
local := new(i18n.Locale)
c.Set("i18n", local)
for _, tag := range tags {
local.Lang = tag.String()
if i18n.IsExist(local.Lang) {
break
}
}
c.Next()
}, nil
}
func watchDocs(ctx context.Context, pattern string) error {
// 文件监控
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("create watcher: %w", err)
}
go func() {
for {
select {
case <-ctx.Done():
log.Print("watch exit")
return
case e := <-watcher.Events:
log.Printf("load %s %v", filepath.Base(e.Name), e.Op)
case err := <-watcher.Errors:
log.Printf("Watcher error: %v", err) // No need to exit here
}
}
}()
return watcher.Add(path.Dir(pattern))
}