Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9977f24bc9 | ||
|
|
4ebd5d8b34 | ||
|
|
14b4534c1e | ||
|
|
6320a285ae | ||
|
|
0fdd8e0bd6 | ||
|
|
b75f7d6a96 | ||
|
|
5aa8152802 | ||
|
|
1a9628a5dc | ||
|
|
71c02266bc | ||
|
|
4b71e288f3 | ||
|
|
523ee64931 | ||
|
|
422aacdc09 | ||
|
|
07fc49db5c | ||
|
|
666161d37e | ||
|
|
11b22da339 | ||
|
|
bd9a45078b | ||
|
|
629ad782c4 | ||
|
|
5e63f5e69d | ||
|
|
33afbd351d | ||
|
|
eaeeaaafb9 | ||
|
|
ae3fb35435 | ||
|
|
c1d73f1a45 | ||
|
|
fd7981b4f9 | ||
|
|
9f77fb7b3f | ||
|
|
f201499945 | ||
|
|
d7736abb25 | ||
|
|
3c4fa6d08a | ||
|
|
ccb5e4546e | ||
|
|
6ce6411da0 | ||
|
|
cb2ed7cb82 | ||
|
|
ea566d1650 | ||
|
|
88dccca295 | ||
|
|
cb11a2b8db | ||
|
|
ed03a264f0 | ||
|
|
aee4194c71 | ||
|
|
b69248f6a4 | ||
|
|
24bfe528b2 | ||
|
|
23e35dcefa | ||
|
|
f4c70b46c1 | ||
|
|
e5100fa018 | ||
|
|
91e1731909 | ||
|
|
4abe528742 | ||
|
|
be0280ac56 | ||
|
|
a0b41d08bd | ||
|
|
8fcabd5e15 | ||
|
|
0a410f09f3 | ||
|
|
0fe849ae67 | ||
|
|
c06a32a268 | ||
|
|
79fccb958c | ||
|
|
8e2679e49f | ||
|
|
52fe7303f3 | ||
|
|
616248d33f | ||
|
|
6e1965a764 | ||
|
|
27bc610a31 | ||
|
|
b53fc91ce7 | ||
|
|
720387ecd5 | ||
|
|
88f23bd1a0 | ||
|
|
6a2d720d36 | ||
|
|
95e55ee13c | ||
|
|
ca293a4933 | ||
|
|
c82d73ca34 | ||
|
|
433064de00 | ||
|
|
9d71ca8198 | ||
|
|
7c938bf024 | ||
|
|
65fcc69efa | ||
|
|
d06bab72a5 | ||
|
|
95900eec1a | ||
|
|
dfae78891d | ||
|
|
c515e33e2c | ||
|
|
5b12dd625b | ||
|
|
9b918caccd | ||
|
|
2be53379e1 | ||
|
|
944e27c58a | ||
|
|
92baf970bc | ||
|
|
64a754167a | ||
|
|
af2a20c34a | ||
|
|
f28d0e77e0 | ||
|
|
9a1b4db61a | ||
|
|
c9d04aded3 | ||
|
|
ee51f678cb | ||
|
|
434c1bf168 | ||
|
|
d2de603957 | ||
|
|
e7fdf6b1db | ||
|
|
305ae0ee70 | ||
|
|
a5749fdab6 |
208
.github/workflows/release.yml
vendored
@@ -1,65 +1,169 @@
|
||||
name: release image & asset
|
||||
name: Release Image & Asset
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write # for SLSA provenance
|
||||
attestations: write # for attestations
|
||||
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
GOPROXY: https://goproxy.io,direct
|
||||
|
||||
jobs:
|
||||
package:
|
||||
# Job 1: 打包源码 tar 文件
|
||||
package-source:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag: ${{ steps.vars.outputs.tag }}
|
||||
sha: ${{ steps.vars.outputs.sha }}
|
||||
date: ${{ steps.vars.outputs.date }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Extract metadata
|
||||
id: vars
|
||||
run: |
|
||||
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
echo "sha=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
|
||||
echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Docker tag
|
||||
id: vars
|
||||
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:10})
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
- name: Package tar archive
|
||||
run: scripts/dist_tar.sh ${{ steps.vars.outputs.tag }}
|
||||
|
||||
- name: Build and push eiblog
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ./build/package/eiblog.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: |
|
||||
deepzz0/eiblog:${{ steps.vars.outputs.tag }}
|
||||
deepzz0/eiblog:latest
|
||||
- name: Upload tar artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-archives
|
||||
path: "*.tar.gz"
|
||||
retention-days: 7
|
||||
|
||||
- name: Build and push backup
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ./build/package/backup.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: |
|
||||
deepzz0/backup:${{ steps.vars.outputs.tag }}
|
||||
deepzz0/backup:latest
|
||||
# Job 2: 构建并推送 Docker 镜像
|
||||
build-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: package-source
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
app: [eiblog, backup]
|
||||
include:
|
||||
- app: eiblog
|
||||
port: 9000
|
||||
- app: backup
|
||||
port: 9001
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Package tar
|
||||
env:
|
||||
GOPROXY: https://goproxy.io,direct
|
||||
run: scripts/dist_tar.sh ${{ steps.vars.outputs.tag }}
|
||||
- name: Release push
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
files: |
|
||||
*.tar.gz
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/deepzz0/${{ matrix.app }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
labels: |
|
||||
org.opencontainers.image.title=${{ matrix.app }}
|
||||
org.opencontainers.image.description=eiblog ${{ matrix.app }} service
|
||||
org.opencontainers.image.vendor=deepzz
|
||||
org.opencontainers.image.revision=${{ needs.package-source.outputs.sha }}
|
||||
org.opencontainers.image.created=${{ needs.package-source.outputs.date }}
|
||||
|
||||
- name: Build and push ${{ matrix.app }} image
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./build/package/${{ matrix.app }}/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.app }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.app }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: Generate SLSA attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/deepzz0/${{ matrix.app }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
|
||||
# Job 3: 创建 GitHub Release
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [package-source, build-images]
|
||||
steps:
|
||||
- name: Download tar artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-archives
|
||||
|
||||
- name: Create release summary
|
||||
run: |
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
# 🎉 Release ${{ needs.package-source.outputs.tag }} Created!
|
||||
|
||||
## 📦 Docker Images
|
||||
- **eiblog**: `deepzz0/eiblog:${{ needs.package-source.outputs.tag }}`
|
||||
- **backup**: `deepzz0/backup:${{ needs.package-source.outputs.tag }}`
|
||||
|
||||
## 🏗️ Build Info
|
||||
- **Tag**: ${{ needs.package-source.outputs.tag }}
|
||||
- **Commit**: ${{ needs.package-source.outputs.sha }}
|
||||
- **Built**: ${{ needs.package-source.outputs.date }}
|
||||
- **Platforms**: linux/amd64, linux/arm64, linux/arm/v7
|
||||
|
||||
## 🔐 Security
|
||||
- ✅ SLSA Build Provenance
|
||||
- ✅ SBOM (Software Bill of Materials)
|
||||
- ✅ Container Signing
|
||||
EOF
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: "*.tar.gz"
|
||||
generate_release_notes: true
|
||||
name: Release ${{ needs.package-source.outputs.tag }}
|
||||
body: |
|
||||
## Docker Images
|
||||
```bash
|
||||
docker pull deepzz0/eiblog:${{ needs.package-source.outputs.tag }}
|
||||
docker pull deepzz0/backup:${{ needs.package-source.outputs.tag }}
|
||||
```
|
||||
|
||||
## Multi-Architecture Support
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
|
||||
Built with commit ${{ needs.package-source.outputs.sha }} on ${{ needs.package-source.outputs.date }}
|
||||
|
||||
5
.gitignore
vendored
@@ -7,7 +7,6 @@
|
||||
*.DS_Store
|
||||
*.tar.gz
|
||||
*.db
|
||||
backend
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
@@ -18,5 +17,7 @@ backend
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
bin
|
||||
assets/*.*
|
||||
cmd/eiblog/etc/assets/*.xml
|
||||
cmd/eiblog/etc/assets/*.txt
|
||||
db.sqlite
|
||||
cmd/*/backend
|
||||
151
CHANGELOG.md
@@ -2,6 +2,157 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [3.0.11](https://github.com/eiblog/eiblog/compare/v3.0.10...v3.0.11) (2026-01-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* title in xml not escape ([14b4534](https://github.com/eiblog/eiblog/commit/14b4534c1e034684b19808168ad0fbf5464e18ef))
|
||||
|
||||
### [3.0.10](https://github.com/eiblog/eiblog/compare/v3.0.9...v3.0.10) (2025-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default RUN_MODE=prod ([5aa8152](https://github.com/eiblog/eiblog/commit/5aa81528027eca648e430503a1f5a04118ca650e))
|
||||
|
||||
### [3.0.9](https://github.com/eiblog/eiblog/compare/v3.0.8...v3.0.9) (2025-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* image RUN_MODE ([71c0226](https://github.com/eiblog/eiblog/commit/71c02266bc0173a4af1ffc876dc36051377e1af0))
|
||||
|
||||
### [3.0.8](https://github.com/eiblog/eiblog/compare/v3.0.7...v3.0.8) (2025-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* upload file path ([523ee64](https://github.com/eiblog/eiblog/commit/523ee64931a0dc1f9f743682fe84b1a1f250686c))
|
||||
|
||||
### [3.0.7](https://github.com/eiblog/eiblog/compare/v3.0.6...v3.0.7) (2025-07-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* feed not generate ([11b22da](https://github.com/eiblog/eiblog/commit/11b22da339c542eb6d697cb3ac7bc78a401c6420))
|
||||
|
||||
### [3.0.6](https://github.com/eiblog/eiblog/compare/v3.0.5...v3.0.6) (2025-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* feed & sitemap not generate ([629ad78](https://github.com/eiblog/eiblog/commit/629ad782c45fae7af9efdf99513bafdf87e7758c))
|
||||
|
||||
### [3.0.5](https://github.com/eiblog/eiblog/compare/v3.0.4...v3.0.5) (2025-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* admin login session ([c1d73f1](https://github.com/eiblog/eiblog/commit/c1d73f1a453af62d15c25a79c382d9cefd8a3d2e))
|
||||
* mongodb uri error ([33afbd3](https://github.com/eiblog/eiblog/commit/33afbd351d2b41f9edf36959908c3f183745d903))
|
||||
* RUN_MODE error ([eaeeaaa](https://github.com/eiblog/eiblog/commit/eaeeaaafb98a4aa0a42dba74b411b1d361faf1d5))
|
||||
|
||||
### [3.0.4](https://github.com/eiblog/eiblog/compare/v3.0.3...v3.0.4) (2025-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** golang version ([9f77fb7](https://github.com/eiblog/eiblog/commit/9f77fb7b3f283aacb9d9c2a0e2f79c8a6c412edb))
|
||||
|
||||
### [3.0.2](https://github.com/eiblog/eiblog/compare/v3.0.1...v3.0.2) (2025-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ci ([ccb5e45](https://github.com/eiblog/eiblog/commit/ccb5e4546e224182c949e72e9eae82fbbe1a02fe))
|
||||
|
||||
### [3.0.1](https://github.com/eiblog/eiblog/compare/v3.0.0...v3.0.1) (2025-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* dist tar ([cb2ed7c](https://github.com/eiblog/eiblog/commit/cb2ed7cb8244dda8cbd8c5966c7ed02e177777e5))
|
||||
|
||||
## [3.0.0](https://github.com/eiblog/eiblog/compare/v2.2.17...v3.0.0) (2025-07-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support custom page ([b69248f](https://github.com/eiblog/eiblog/commit/b69248f6a4937f8157d2393bd44c6c55174ae3e7))
|
||||
* twofactor bind ([e5100fa](https://github.com/eiblog/eiblog/commit/e5100fa018e2955c3d649b70245e3a379658d7ba))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ci ([f4c70b4](https://github.com/eiblog/eiblog/commit/f4c70b46c19d8761c252b767c92016c195299c0c))
|
||||
* ci dist tar ([24bfe52](https://github.com/eiblog/eiblog/commit/24bfe528b28b995c3d5b2c13314c8849a036942b))
|
||||
* custom page support embed ([aee4194](https://github.com/eiblog/eiblog/commit/aee4194c71dd98a7a84da96810a060f716d1ea57))
|
||||
|
||||
### [2.2.16](https://github.com/eiblog/eiblog/compare/v2.2.15...v2.2.16) (2025-03-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* empty "beian" display issue ([79fccb9](https://github.com/eiblog/eiblog/commit/79fccb958c3eb8e21615389359c1d613c5fb079b))
|
||||
|
||||
### [2.2.15](https://github.com/eiblog/eiblog/compare/v2.2.14...v2.2.15) (2024-12-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disqus list posts ([52fe730](https://github.com/eiblog/eiblog/commit/52fe7303f3345421c0f2e2989a6c174d5b1a689e))
|
||||
* disqus thread not store ([616248d](https://github.com/eiblog/eiblog/commit/616248d33fdf44dbc3aed41e92adae001a4f5577))
|
||||
|
||||
### [2.2.14](https://github.com/eiblog/eiblog/compare/v2.2.13...v2.2.14) (2024-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 1. bei_an cannot update error ([b53fc91](https://github.com/eiblog/eiblog/commit/b53fc91ce7b67c3811c232ad6236898f84bc391b)), closes [#43](https://github.com/eiblog/eiblog/issues/43) [#44](https://github.com/eiblog/eiblog/issues/44)
|
||||
* **serie:** update serie did not rerender ([88f23bd](https://github.com/eiblog/eiblog/commit/88f23bd1a0d6183d6de484cb79303506f0506d15))
|
||||
|
||||
### [2.2.13](https://github.com/eiblog/eiblog/compare/v2.2.12...v2.2.13) (2024-01-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* load more comments ([95e55ee](https://github.com/eiblog/eiblog/commit/95e55ee13c1af13e2bb2149ccbe60013c93e4e69))
|
||||
|
||||
### [2.2.12](https://github.com/eiblog/eiblog/compare/v2.2.11...v2.2.12) (2024-01-02)
|
||||
|
||||
### [2.2.11](https://github.com/eiblog/eiblog/compare/v2.2.10...v2.2.11) (2024-01-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **disqus:** fix returned posts list not have parent post ([9d71ca8](https://github.com/eiblog/eiblog/commit/9d71ca81988bfc614d13fcb02079f0dba9ef43cc))
|
||||
|
||||
### [2.2.10](https://github.com/eiblog/eiblog/compare/v2.2.9...v2.2.10) (2023-12-22)
|
||||
|
||||
### [2.2.9](https://github.com/eiblog/eiblog/compare/v2.2.8...v2.2.9) (2023-09-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* google analytics not work, supported ga4 measurement protocol ([9b918ca](https://github.com/eiblog/eiblog/commit/9b918caccd9fd7faff7d253693e075ccd0150c17))
|
||||
|
||||
### [2.2.8](https://github.com/eiblog/eiblog/compare/v2.2.7...v2.2.8) (2023-07-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backup:** restore db and tests ([f28d0e7](https://github.com/eiblog/eiblog/commit/f28d0e77e06dd435dc13a1867f18a011e34b8f53))
|
||||
|
||||
### [2.2.7](https://github.com/eiblog/eiblog/compare/v2.2.6...v2.2.7) (2023-07-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backup:** 数据恢复错误 ([ee51f67](https://github.com/eiblog/eiblog/commit/ee51f678cbf93332fedccf927704e78a6063289c))
|
||||
|
||||
### [2.2.6](https://github.com/eiblog/eiblog/compare/v2.2.5...v2.2.6) (2023-06-19)
|
||||
|
||||
### [2.2.5](https://github.com/eiblog/eiblog/compare/v2.2.4...v2.2.5) (2023-05-26)
|
||||
|
||||
|
||||
|
||||
4
Makefile
@@ -22,10 +22,6 @@ backup:
|
||||
dist:
|
||||
@scripts/dist_tar.sh $(tag)
|
||||
|
||||
# clean
|
||||
clean:
|
||||
@rm -rf bin && rm -f *.tar.gz && rm -f backend
|
||||
|
||||
# protoc
|
||||
protoc:
|
||||
@cd pkg/proto && make protoc
|
||||
|
||||
17
README.md
@@ -1,8 +1,6 @@
|
||||
# EiBlog [](https://travis-ci.org/eiblog/eiblog) [](LICENSE.md) [](https://github.com/eiblog/eiblog/releases)
|
||||
# EiBlog [](LICENSE.md) [](https://github.com/eiblog/eiblog/releases)
|
||||
|
||||
> 博客项目结构参考模版:https://github.com/deepzz0/appdemo
|
||||
|
||||
用过其它博客系统,不喜欢,不够轻,不够快!这是我开发的第二款博客系统,也实在不想再在这件事情上过多纠结了。`EiBlog` 是一个比较稳定的博客系统,现已迭代至 `2.0` 版本,稳定性和维护你是不用担心的。
|
||||
用过其它博客系统,不喜欢,不够轻,不够快!这是我开发的第二款博客系统,也实在不想再在这件事情上过多纠结了。`EiBlog` 是一个比较稳定的博客系统,博主已稳定使用多年。这里是 `3.0` 版本,如果仍在使用 `2.0` [请点击这里](https://github.com/eiblog/eiblog/tree/v2)。
|
||||
|
||||
但它有着部署简单(上线复杂!)的特点,不推荐没有计算机知识的朋友搭建,欢迎咨询。该博客的个中优点(简洁、轻快,安全),等你体验。
|
||||
|
||||
@@ -12,6 +10,13 @@ Docker镜像地址:
|
||||
* 博客搜索:[deepzz0/elasticsearch](https://hub.docker.com/r/deepzz0/elasticsearch)
|
||||
* 数据备份:[deepzz0/backup](https://hub.docker.com/r/deepzz0/backup)
|
||||
|
||||
### 功能变化
|
||||
|
||||
- [x] 增对 **TOTP 双因素认证** 的支持,配置开启后后台绑定
|
||||
- [x] 新增 **自定义页面**,支持独立页面和内嵌页面
|
||||
- [x] 优化项目结构,更加清晰,各个 app 之间配置独立
|
||||
- [ ] 支持多搜索引擎,如数据库原生、[zincsearch](https://github.com/zincsearch/zincsearch)、bleve 等
|
||||
|
||||
### 快速体验
|
||||
|
||||
**二进制**
|
||||
@@ -33,8 +38,6 @@ $ docker run --name eiblog \
|
||||
参考项目根目录下的 [docker-compose.yml](https://github.com/eiblog/eiblog/blob/v2/docker-compose.yml),修改相关配置:
|
||||
|
||||
```
|
||||
$ docker compose up -d
|
||||
或
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
@@ -46,7 +49,7 @@ $ docker-compose up -d
|
||||
|
||||
| 类型(driver) | 地址(source)示例 |
|
||||
| -------------- | ------------------------------------------------------------ |
|
||||
| mongodb | mongodb://localhost:27017 |
|
||||
| mongodb | mongodb://localhost:27017/eiblog |
|
||||
| mysql | user:password@tcp(localhost:3306)/eiblog?charset=utf8mb4&parseTime=True&loc=Local |
|
||||
| postgres | host=localhost port=5432 user=user password=password dbname=eiblog sslmode=disable |
|
||||
| sqlite | /path/eiblog.db |
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
FROM golang:1.20 AS builder
|
||||
FROM golang:1.23 AS builder
|
||||
|
||||
WORKDIR /eiblog
|
||||
COPY . .
|
||||
RUN ./scripts/run_build.sh backup
|
||||
|
||||
RUN scripts/run_build.sh backup
|
||||
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="deepzz.qi@gmail.com"
|
||||
|
||||
RUN apk add --update --no-cache tzdata ca-certificates \
|
||||
mongodb-tools libc6-compat gcompat
|
||||
mongodb-tools libc6-compat gcompat
|
||||
|
||||
COPY README.md /app/README.md
|
||||
COPY CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY LICENSE /app/LICENSE
|
||||
|
||||
COPY --from=builder /eiblog/bin/backend /app/backend
|
||||
COPY conf /app/conf
|
||||
COPY --from=builder /eiblog/cmd/backup/backend /app/backend
|
||||
COPY cmd/backup/etc /app/etc
|
||||
|
||||
EXPOSE 9001
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
FROM golang:1.20 AS builder
|
||||
FROM golang:1.23 AS builder
|
||||
|
||||
WORKDIR /eiblog
|
||||
COPY . .
|
||||
RUN ./scripts/run_build.sh eiblog
|
||||
|
||||
RUN scripts/run_build.sh eiblog
|
||||
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="deepzz.qi@gmail.com"
|
||||
|
||||
RUN apk add --update --no-cache tzdata
|
||||
RUN apk add --update --no-cache tzdata ca-certificates
|
||||
|
||||
COPY README.md /app/README.md
|
||||
COPY CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY LICENSE /app/LICENSE
|
||||
|
||||
COPY --from=builder /eiblog/bin/backend /app/backend
|
||||
COPY conf /app/conf
|
||||
COPY website /app/website
|
||||
COPY assets /app/assets
|
||||
COPY --from=builder /eiblog/cmd/eiblog/backend /app/backend
|
||||
COPY cmd/eiblog/etc /app/etc
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
70
cmd/backup/config/config.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config config
|
||||
type Config struct {
|
||||
config.APIMode
|
||||
|
||||
Database config.Database // 数据库配置
|
||||
BackupTo string // 备份到, default: qiniu
|
||||
Interval string // 备份周期, default: 7d
|
||||
Validity int // 备份保留时间, default: 60
|
||||
Qiniu config.Qiniu // 七牛OSS配置
|
||||
}
|
||||
|
||||
// Conf 配置
|
||||
var Conf Config
|
||||
|
||||
// load config file
|
||||
func init() {
|
||||
// run mode
|
||||
mode := config.RunModeProd
|
||||
if m := os.Getenv("RUN_MODE"); m != "" {
|
||||
mode = config.RunMode(m)
|
||||
if !mode.IsRunMode() {
|
||||
panic("config: unsupported env RUN_MODE: " + mode)
|
||||
}
|
||||
}
|
||||
logrus.Infof("Run mode:%s", mode)
|
||||
|
||||
// 加载配置文件
|
||||
etc, err := config.WorkEtcPath()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
path := filepath.Join(etc, "app.yml")
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = yaml.Unmarshal(data, &Conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Conf.RunMode = mode
|
||||
|
||||
// read env
|
||||
readDatabaseEnv()
|
||||
}
|
||||
|
||||
func readDatabaseEnv() {
|
||||
key := strings.ToUpper(Conf.Name) + "_DB_DRIVER"
|
||||
if d := os.Getenv(key); d != "" {
|
||||
Conf.Database.Driver = d
|
||||
}
|
||||
key = strings.ToUpper(Conf.Name) + "_DB_SOURCE"
|
||||
if s := os.Getenv(key); s != "" {
|
||||
Conf.Database.Source = s
|
||||
}
|
||||
}
|
||||
60
cmd/backup/docs/docs.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"get": {
|
||||
"description": "ping",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"ping"
|
||||
],
|
||||
"summary": "ping",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "it's ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "1.0",
|
||||
Host: "",
|
||||
BasePath: "",
|
||||
Schemes: []string{},
|
||||
Title: "backup API",
|
||||
Description: "This is a backup server.",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
||||
34
cmd/backup/docs/swagger.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "This is a backup server.",
|
||||
"title": "backup API",
|
||||
"contact": {},
|
||||
"version": "1.0"
|
||||
},
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"get": {
|
||||
"description": "ping",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"ping"
|
||||
],
|
||||
"summary": "ping",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "it's ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
cmd/backup/docs/swagger.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
info:
|
||||
contact: {}
|
||||
description: This is a backup server.
|
||||
title: backup API
|
||||
version: "1.0"
|
||||
paths:
|
||||
/ping:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: ping
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: it's ok
|
||||
schema:
|
||||
type: string
|
||||
summary: ping
|
||||
tags:
|
||||
- ping
|
||||
swagger: "2.0"
|
||||
14
cmd/backup/etc/app.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
apimode:
|
||||
name: cmd-backup
|
||||
listen: 0.0.0.0:9000
|
||||
database: # 数据库配置
|
||||
driver: mongodb
|
||||
source: mongodb://localhost:27017/eiblog
|
||||
backupto: qiniu # 备份到, default: qiniu
|
||||
interval: 7d # 备份周期, default: 7d
|
||||
validity: 60 # 备份保留时间, default: 60
|
||||
qiniu: # 七牛OSS
|
||||
bucket: eiblog
|
||||
domain: st.deepzz.com
|
||||
accesskey: MB6AXl_Sj_mmFsL-Lt59Dml2Vmy2o8XMmiCbbSeC
|
||||
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf
|
||||
17
cmd/backup/handler/internal/internal.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/eiblog/eiblog/cmd/backup/config"
|
||||
"github.com/eiblog/eiblog/pkg/third/qiniu"
|
||||
)
|
||||
|
||||
// QiniuClient 七牛客户端
|
||||
var QiniuClient *qiniu.QiniuClient
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
QiniuClient, err = qiniu.NewQiniuClient(config.Conf.Qiniu)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package ping provides ...
|
||||
package ping
|
||||
|
||||
import (
|
||||
@@ -13,6 +12,13 @@ func RegisterRoutes(group gin.IRoutes) {
|
||||
}
|
||||
|
||||
// handlePing ping
|
||||
// @Summary ping
|
||||
// @Description ping
|
||||
// @Tags ping
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "it's ok"
|
||||
// @Router /ping [get]
|
||||
func handlePing(c *gin.Context) {
|
||||
c.String(http.StatusOK, "it's ok")
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
// Package swag provides ...
|
||||
package swag
|
||||
|
||||
import (
|
||||
_ "github.com/eiblog/eiblog/pkg/core/backup/docs" // docs
|
||||
_ "github.com/eiblog/eiblog/cmd/eiblog/docs" // docs
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/swaggo/gin-swagger/swaggerFiles"
|
||||
)
|
||||
|
||||
// RegisterRoutes register routes
|
||||
7
cmd/backup/handler/timer/db/db.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package db
|
||||
|
||||
// Storage 备份恢复器
|
||||
type Storage interface {
|
||||
Backup(name string) (string, error)
|
||||
Restore(path string) error
|
||||
}
|
||||
75
cmd/backup/handler/timer/db/mgodb.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/cmd/backup/config"
|
||||
pdb "github.com/eiblog/eiblog/pkg/connector/db"
|
||||
)
|
||||
|
||||
// MongoStorage 备份恢复器
|
||||
type MongoStorage struct{}
|
||||
|
||||
// Backup 备份
|
||||
func (r MongoStorage) Backup(name string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
||||
defer cancel()
|
||||
|
||||
// dump
|
||||
u, err := url.Parse(config.Conf.Database.Source)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if u.Path == "" {
|
||||
return "", fmt.Errorf("no database specified")
|
||||
}
|
||||
arg := fmt.Sprintf("mongodump -h %s -d %s -o /tmp", u.Host, u.Path)
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", arg)
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// tar
|
||||
arg = fmt.Sprintf("tar czf /tmp/%s -C /tmp %s", name, u.Path)
|
||||
cmd = exec.CommandContext(ctx, "sh", "-c", arg)
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "/tmp/" + name, nil
|
||||
}
|
||||
|
||||
// Restore 恢复
|
||||
func (r MongoStorage) Restore(path string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
||||
defer cancel()
|
||||
|
||||
// drop database
|
||||
database, err := pdb.NewMDB(ctx, config.Conf.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = database.Drop(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// unarchive
|
||||
arg := fmt.Sprintf("tar xzf %s -C /tmp", path)
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", arg)
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// restore
|
||||
u, err := url.Parse(config.Conf.Database.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arg = fmt.Sprintf("mongorestore -h %s -d %s /tmp/%s", u.Host, u.Path, u.Path)
|
||||
cmd = exec.CommandContext(ctx, "sh", "-c", arg)
|
||||
return cmd.Run()
|
||||
}
|
||||
95
cmd/backup/handler/timer/timer.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package timer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/cmd/backup/config"
|
||||
"github.com/eiblog/eiblog/cmd/backup/handler/timer/db"
|
||||
"github.com/eiblog/eiblog/cmd/backup/handler/timer/to"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Start to backup with ticker
|
||||
func Start(restore bool) (err error) {
|
||||
var (
|
||||
storage db.Storage
|
||||
backupTo to.BackupRestorer
|
||||
)
|
||||
|
||||
// backup from
|
||||
switch config.Conf.Database.Driver {
|
||||
case "mongodb":
|
||||
storage = db.MongoStorage{}
|
||||
|
||||
default:
|
||||
return errors.New("timer: unknown backup from driver: " +
|
||||
config.Conf.Database.Driver)
|
||||
}
|
||||
|
||||
// backup to
|
||||
switch config.Conf.BackupTo {
|
||||
case "qiniu":
|
||||
backupTo = to.QiniuBackupRestorer{}
|
||||
|
||||
default:
|
||||
return errors.New("timer: unknown backup to driver: " +
|
||||
config.Conf.BackupTo)
|
||||
}
|
||||
|
||||
// restore
|
||||
if restore {
|
||||
path, err := backupTo.Download()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = storage.Restore(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Info("timer: Restore success")
|
||||
}
|
||||
|
||||
// backup
|
||||
interval, err := ParseDuration(config.Conf.Interval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t := time.NewTicker(interval)
|
||||
for now := range t.C {
|
||||
name := fmt.Sprintf("eiblog-%s.tar.gz", now.Format("2006-01-02"))
|
||||
path, err := storage.Backup(name)
|
||||
if err != nil {
|
||||
logrus.Error("timer: Start.Backup: ", now.Format(time.RFC3339), err)
|
||||
continue
|
||||
}
|
||||
err = backupTo.Upload(path)
|
||||
if err != nil {
|
||||
logrus.Error("timer: Start.Backup: ", now.Format(time.RFC3339), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseDuration parse string to duration
|
||||
func ParseDuration(d string) (time.Duration, error) {
|
||||
if len(d) == 0 {
|
||||
return 0, errors.New("timer: incorrect duration input")
|
||||
}
|
||||
|
||||
length := len(d)
|
||||
switch d[length-1] {
|
||||
case 's', 'm', 'h':
|
||||
return time.ParseDuration(d)
|
||||
case 'd':
|
||||
di, err := strconv.Atoi(d[:length-1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.Duration(di) * time.Hour * 24, nil
|
||||
}
|
||||
return 0, errors.New("timer: unsupported duration:" + d)
|
||||
}
|
||||
66
cmd/backup/handler/timer/to/qiniu.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package to
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/eiblog/eiblog/cmd/backup/config"
|
||||
"github.com/eiblog/eiblog/cmd/backup/handler/internal"
|
||||
"github.com/eiblog/eiblog/pkg/third/qiniu"
|
||||
)
|
||||
|
||||
// QiniuBackupRestorer qiniu backup restorer
|
||||
type QiniuBackupRestorer struct{}
|
||||
|
||||
// Upload implements timer.BackupRestorer
|
||||
func (s QiniuBackupRestorer) Upload(path string) error {
|
||||
name := filepath.Base(path)
|
||||
|
||||
// upload file
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadParams := qiniu.UploadParams{
|
||||
Name: filepath.Join("blog", name), // blog/eiblog-xx.tar.gz
|
||||
Size: fi.Size(),
|
||||
Data: f,
|
||||
NoCompletePath: true,
|
||||
}
|
||||
_, err = internal.QiniuClient.Upload(uploadParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// after days delete
|
||||
deleteParams := qiniu.DeleteParams{
|
||||
Name: filepath.Join("blog", name), // blog/eiblog-xx.tar.gz
|
||||
Days: config.Conf.Validity,
|
||||
NoCompletePath: true,
|
||||
}
|
||||
return internal.QiniuClient.Delete(deleteParams)
|
||||
}
|
||||
|
||||
// Download implements timer.BackupRestorer
|
||||
func (s QiniuBackupRestorer) Download() (string, error) {
|
||||
// backup file
|
||||
params := qiniu.ContentParams{
|
||||
Prefix: "blog/",
|
||||
}
|
||||
raw, err := internal.QiniuClient.Content(params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := filepath.Join("/tmp", "eiblog.tar.gz")
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, _ = f.Write(raw)
|
||||
defer f.Close()
|
||||
return path, nil
|
||||
}
|
||||
7
cmd/backup/handler/timer/to/to.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package to
|
||||
|
||||
// BackupRestorer 备份存储接口
|
||||
type BackupRestorer interface {
|
||||
Upload(path string) error
|
||||
Download() (path string, err error)
|
||||
}
|
||||
@@ -3,16 +3,20 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/core/backup/ping"
|
||||
"github.com/eiblog/eiblog/pkg/core/backup/swag"
|
||||
"github.com/eiblog/eiblog/pkg/core/backup/timer"
|
||||
"github.com/eiblog/eiblog/cmd/backup/config"
|
||||
"github.com/eiblog/eiblog/cmd/backup/handler/ping"
|
||||
"github.com/eiblog/eiblog/cmd/backup/handler/swag"
|
||||
"github.com/eiblog/eiblog/cmd/backup/handler/timer"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// @title backup API
|
||||
// @version 1.0
|
||||
// @description This is a backup server.
|
||||
|
||||
var restore bool
|
||||
|
||||
func init() {
|
||||
@@ -20,15 +24,15 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hi, it's App " + config.Conf.BackupApp.Name)
|
||||
flag.Parse()
|
||||
logrus.Info("Hi, it's App " + config.Conf.Name)
|
||||
|
||||
flag.Parse()
|
||||
endRun := make(chan error, 1)
|
||||
|
||||
runCommand(restore, endRun)
|
||||
|
||||
runHTTPServer(endRun)
|
||||
fmt.Println(<-endRun)
|
||||
logrus.Fatal(<-endRun)
|
||||
}
|
||||
|
||||
func runCommand(restore bool, endRun chan error) {
|
||||
@@ -38,11 +42,7 @@ func runCommand(restore bool, endRun chan error) {
|
||||
}
|
||||
|
||||
func runHTTPServer(endRun chan error) {
|
||||
if !config.Conf.BackupApp.EnableHTTP {
|
||||
return
|
||||
}
|
||||
|
||||
if config.Conf.RunMode == config.ModeProd {
|
||||
if config.Conf.RunMode.IsReleaseMode() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
e := gin.Default()
|
||||
@@ -54,9 +54,8 @@ func runHTTPServer(endRun chan error) {
|
||||
ping.RegisterRoutes(e)
|
||||
|
||||
// start
|
||||
address := fmt.Sprintf(":%d", config.Conf.BackupApp.HTTPPort)
|
||||
go func() {
|
||||
endRun <- e.Run(address)
|
||||
endRun <- e.Run(config.Conf.Listen)
|
||||
}()
|
||||
fmt.Println("HTTP server running on: " + address)
|
||||
logrus.Info("HTTP server running on: " + config.Conf.Listen)
|
||||
}
|
||||
|
||||
86
cmd/eiblog/config/config.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// Conf 配置
|
||||
Conf Config
|
||||
// EtcDir 工作目录
|
||||
EtcDir string
|
||||
)
|
||||
|
||||
// Config config
|
||||
type Config struct {
|
||||
config.APIMode
|
||||
// 静态资源版本, 每次更改了 js/css 都需要提高该值
|
||||
StaticVersion int
|
||||
// 数据库配置
|
||||
Database config.Database
|
||||
// 热词, 手动指定, 用于搜索
|
||||
HotWords []string
|
||||
// Elasticsearch 配置
|
||||
ESHost string
|
||||
|
||||
General config.General
|
||||
Disqus config.Disqus
|
||||
Google config.Google
|
||||
Qiniu config.Qiniu
|
||||
Twitter config.Twitter
|
||||
FeedRPC config.FeedRPC
|
||||
Account config.Account
|
||||
Pages []config.CustomPage
|
||||
}
|
||||
|
||||
// init 初始化配置
|
||||
func init() {
|
||||
// run mode
|
||||
mode := config.RunModeProd
|
||||
if rm := os.Getenv("RUN_MODE"); rm != "" {
|
||||
mode = config.RunMode(rm)
|
||||
if !mode.IsRunMode() {
|
||||
panic("config: unsupported env RUN_MODE: " + mode)
|
||||
}
|
||||
}
|
||||
logrus.Infof("Run mode:%s", mode)
|
||||
|
||||
// 加载配置文件
|
||||
var err error
|
||||
EtcDir, err = config.WorkEtcPath()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
path := filepath.Join(EtcDir, "app.yml")
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = yaml.Unmarshal(data, &Conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Conf.RunMode = mode
|
||||
|
||||
// 读取环境变量配置
|
||||
readDatabaseEnv()
|
||||
}
|
||||
|
||||
func readDatabaseEnv() {
|
||||
key := strings.ToUpper(Conf.Name) + "_DB_DRIVER"
|
||||
if d := os.Getenv(key); d != "" {
|
||||
Conf.Database.Driver = d
|
||||
}
|
||||
key = strings.ToUpper(Conf.Name) + "_DB_SOURCE"
|
||||
if s := os.Getenv(key); s != "" {
|
||||
Conf.Database.Source = s
|
||||
}
|
||||
}
|
||||
36
cmd/eiblog/docs/docs.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "",
|
||||
Host: "",
|
||||
BasePath: "",
|
||||
Schemes: []string{},
|
||||
Title: "",
|
||||
Description: "",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
||||
7
cmd/eiblog/docs/swagger.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"contact": {}
|
||||
},
|
||||
"paths": {}
|
||||
}
|
||||
4
cmd/eiblog/docs/swagger.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
info:
|
||||
contact: {}
|
||||
paths: {}
|
||||
swagger: "2.0"
|
||||
59
cmd/eiblog/etc/app.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
apimode:
|
||||
name: cmd-eiblog
|
||||
listen: 0.0.0.0:9000
|
||||
host: example.com
|
||||
database: # 数据库配置
|
||||
driver: sqlite
|
||||
source: ./db.sqlite
|
||||
hotwords: # 热搜词
|
||||
- docker
|
||||
- mongodb
|
||||
- curl
|
||||
- dns
|
||||
staticversion: 1
|
||||
eshost: # http://elasticsearch:9200
|
||||
pages:
|
||||
- name: 自定义页面
|
||||
path: /page/custom.html
|
||||
showinnav: true
|
||||
isembed: false
|
||||
- name: 自定义页面2
|
||||
path: /page/custom2.html
|
||||
showinnav: true
|
||||
isembed: true
|
||||
general: # 常规配置
|
||||
pagenum: 10 # 首页展示文章数量
|
||||
pagesize: 20 # 管理界面
|
||||
descprefix: "Desc:" # 文章描述前缀
|
||||
identifier: <!--more--> # 截取预览标识
|
||||
length: 400 # 自动截取预览, 字符数
|
||||
timezone: Asia/Shanghai # 时区
|
||||
twofactor: true # 是否启用两步验证
|
||||
disqus: # 评论相关
|
||||
shortname: xxxxxx
|
||||
publickey: wdSgxRm9rdGAlLKFcFdToBe3GT4SibmV7Y8EjJQ0r4GWXeKtxpopMAeIeoI2dTEg
|
||||
accesstoken: 50023908f39f4607957e909b495326af
|
||||
google: # 谷歌分析
|
||||
url: https://www.google-analytics.com/g/collect
|
||||
tid: G-xxxxxxxxxx
|
||||
v: "2"
|
||||
adsense: <script async src="https://pagead2.googlesyndication.com/xxx" crossorigin="anonymous"></script>
|
||||
qiniu: # 七牛OSS
|
||||
bucket: eiblog
|
||||
domain: st.deepzz.cn
|
||||
accesskey: MB6AXl_Sj_mmFsL-Lt59Dml2Vmy2o8XMmiCbbSeC
|
||||
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf
|
||||
twitter: # twitter card
|
||||
card: summary
|
||||
site: deepzz02
|
||||
image: st.deepzz.cn/static/img/avatar.jpg
|
||||
address: twitter.com/deepzz02
|
||||
feedrpc: # rss ping
|
||||
feedrurl: https://deepzz.superfeedr.com/
|
||||
pingrpc:
|
||||
- http://ping.baidu.com/ping/RPC2
|
||||
- http://rpc.pingomatic.com/
|
||||
# 数据初始化操作,可到博客后台修改
|
||||
account:
|
||||
username: deepzz # *后台登录用户名
|
||||
password: deepzz # *登录明文密码
|
||||
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 847 B |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
8
cmd/eiblog/etc/page/custom.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Custom Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Custom Page</h1>
|
||||
</body>
|
||||
</html>
|
||||
10
cmd/eiblog/etc/page/custom2.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{{define "custom2.html"}}
|
||||
<div id=content class=inner>
|
||||
<article class="post" itemscope itemtype="http://schema.org/Article">
|
||||
<h1 class=title itemprop=headline>Custom Page</h1>
|
||||
<div class="entry-content" itemprop=articleBody>
|
||||
<p>Custom Page</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -22,10 +22,10 @@
|
||||
<label for=password class="sr-only">密码</label>
|
||||
<input type=password id=password name=password class="text-l w-100" placeholder="密码">
|
||||
</p>
|
||||
<!-- <p>
|
||||
{{if .TwoFactor}}<p>
|
||||
<label for=code class="sr-only">两步验证</label>
|
||||
<input type=text id=code name=code class="text-l w-100" placeholder="两步验证">
|
||||
</p> -->
|
||||
</p>{{end}}
|
||||
<p class=submit>
|
||||
<button type=submit class="btn btn-l w-100 primary">登录</button>
|
||||
</p>
|
||||
@@ -7,11 +7,37 @@
|
||||
<div class="row typecho-page-main">
|
||||
<div class="col-mb-12 col-tb-3">
|
||||
<p>
|
||||
<img class="profile-avatar" src="//{{$.Qiniu.Domain}}/static/img/avatar.png" alt="{{.Blogger.BlogName}}" />
|
||||
<img class="profile-avatar" src="//{{$.Qiniu.Domain}}/static/img/avatar.png" alt="{{.Blogger.BlogName}}" />
|
||||
</p>
|
||||
<h2>{{.Blogger.BlogName}}</h2>
|
||||
<p>{{.Blogger.SubTitle}}</p>
|
||||
<p>最后登录: {{dateformat .Account.LoginAt "2006/01/02 15:04"}}</p>
|
||||
{{if .Account.TwoFactorSecret}}
|
||||
<strong>2FA 已绑定 <a class="unbind-2fa" href="/admin/profile?unbind=true">解绑</a></strong>
|
||||
{{end}}
|
||||
{{if $.TwoFactorSecret}}
|
||||
<strong>2FA 绑定</strong>
|
||||
<form action="/admin/api/twofactor" method="post" enctype="application/x-www-form-urlencoded">
|
||||
<ul class="typecho-option">
|
||||
<li>
|
||||
<img src="{{$.TwoFactorSecret}}" alt="2fa code" style="width:180px;height:180px;">
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="typecho-option">
|
||||
<li>
|
||||
<input id="2fa-0-1" name="code" type="text" class="text" />
|
||||
<p class="description">
|
||||
输入 2FA 验证码, 用于确认绑定.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="typecho-option typecho-option-submit">
|
||||
<li>
|
||||
<button type="submit" class="btn primary">
|
||||
确认绑定</button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-mb-12 col-tb-6 col-tb-offset-1 typecho-content-panel" role="form">
|
||||
<section>
|
||||
@@ -1 +1 @@
|
||||
<!DOCTYPE html><html lang="zh-cn"><head><meta charset="utf-8"><meta content="width=device-width,minimum-scale=1.0" name="viewport"><meta name="format-detection" content="telephone=no"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="referrer" content="always"><title>{{.Title}}</title>{{.AdSense}}<script>!function(n,t){function e(){o("nls",1)}function c(){t.documentElement.style.display="none",u(),location.reload()}function r(n){var t="";try{t=f[n]||"",t.length<99&&c()}catch(e){u()}return t}function i(n,t){try{f[n]=t,t!==f[n]&&u()}catch(e){u()}}function o(n,e){var c=999;e||(c=-1),c=new Date(+new Date+864e5*c).toGMTString();var r=n+"="+e+";path=/;secure;expires="+c;t.cookie=r}function a(n){var e=t.getElementById(n).innerHTML;i(n,e)}function l(e,c){var i=r(e),o=t.createElement(c?"script":"style");return n.execScript&&c?n.execScript(i):(o.innerHTML=i,void t.head.appendChild(o))}function u(){o("v",0)}var f,h=function(){},d=n.L={h:h,l:h,c:h};try{f=localStorage,d.h=a,d.l=l,d.c=o}catch(p){e()}}(this,document);</script>{{if .Version}}<script>L.c('v', {{.Version}})</script>{{end}}<link rel="apple-touch-icon" href="//{{.Qiniu.Domain}}/static/img/favicon.ico"><link rel="search" type="application/opensearchdescription+xml" href="//{{.Domain}}/opensearch.xml" title="{{.BTitle}}">{{if .Version}}<style id="blog_css">{{template "blog_css" .}}</style><script>L.h('blog_css')</script>{{else}}<script>L.l("blog_css")</script>{{end}}<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="//{{.Domain}}/rss.html"><meta name="description" content="{{.Description}}"><meta name="twitter:card" content={{.Twitter.Card}}><meta name="twitter:site" content="@{{.Twitter.Site}}"><meta name="twitter:title" content="{{.Title}}"><meta name="twitter:description" content="{{.Description}}"><meta name="twitter:image" content="https://{{.Twitter.Image}}"></head><body><div class="container"><div class="left-col"><div class="intrude-less"><header id="header" class="inner"><div class="profilepic"><a href="/"></a></div><h1><a href="/">{{.BlogName}}</a></h1><p class="subtitle">{{.SubTitle}}</p><nav id="main-nav"><ul><li {{if eq .Path "/"}}class="on" {{end}}><a href="/"><span>首页</span></a></li><li {{if eq .Path "/series.html"}}class="on" {{end}}><a href="/series.html"><span>专题</span></a></li><li {{if eq .Path "/archives.html"}}class="on" {{end}}><a href="/archives.html"><span>归档</span></a></li><li {{if eq .Path "/post/blogroll.html"}}class="on" {{end}}><a href="/post/blogroll.html"><span>友链</span></a></li><li {{if eq .Path "/post/about.html"}}class="on" {{end}}><a href="/post/about.html"><span>关于</span></a></li></ul></nav><nav id="sub-nav"><div class="social"><a target="_blank" class="twitter external" rel="nofollow" href="//{{.Twitter.Address}}" title="Twitter" aria-label="Twitter">Twitter</a><a target="_blank" class="rss" href="//{{.Domain}}/rss.html" title="RSS 订阅" aria-label="RSS 订阅">RSS</a><a class="search" href="/search.html" title="站内搜索" aria-label="站内搜索">Search</a></div></nav></header></div></div><div class="mid-col"><div class="mid-col-container">{{.LayoutContent}}</div><footer id=footer class=inner>© {{.CopyYear}} - {{.BTitle}} - <a target="_blank" rel="nofollow designer" class="external beian" href="https://beian.miit.gov.cn">{{.BeiAn}}</a><br>Powered by <a target=_blank href="//github.com/eiblog/eiblog">Eiblog</a> & <a target=_blank rel="nofollow designer" class=external href="//github.com/deepzz0">Deepzz</a></footer></div></div><input type=hidden id=CURRENT_PAGE value="{{.CurrentPage}}"><input type=hidden id=CDN_DOMAIN value="//{{.Qiniu.Domain}}">{{if .Version}}<script id="ana_js">{{template "ana_js"}}</script><script>L.h('ana_js')</script>{{else}}<script>L.l('ana_js', 1)</script>{{end}}{{if .Version}}<script id="jq_js">{{template "jq_js"}}</script><script>L.h('jq_js')</script>{{else}}<script>L.l('jq_js', 1)</script>{{end}}{{if .Version}}<script id="hl_js">{{template "hl_js"}}</script><script>L.h('hl_js')</script>{{else}}<script>L.l('hl_js', 1)</script>{{end}}{{if .Version}}<script id="blog_js">{{template "blog_js" .}}</script><script>L.h('blog_js')</script>{{else}}<script>L.l('blog_js', 1)</script>{{end}}</body></html>
|
||||
<!DOCTYPE html><html lang="zh-cn"><head><meta charset="utf-8"><meta content="width=device-width,minimum-scale=1.0" name="viewport"><meta name="format-detection" content="telephone=no"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="referrer" content="always"><title>{{.Title}}</title>{{.AdSense}}<script>!function(n,t){function e(){o("nls",1)}function c(){t.documentElement.style.display="none",u(),location.reload()}function r(n){var t="";try{t=f[n]||"",t.length<99&&c()}catch(e){u()}return t}function i(n,t){try{f[n]=t,t!==f[n]&&u()}catch(e){u()}}function o(n,e){var c=999;e||(c=-1),c=new Date(+new Date+864e5*c).toGMTString();var r=n+"="+e+";path=/;secure;expires="+c;t.cookie=r}function a(n){var e=t.getElementById(n).innerHTML;i(n,e)}function l(e,c){var i=r(e),o=t.createElement(c?"script":"style");return n.execScript&&c?n.execScript(i):(o.innerHTML=i,void t.head.appendChild(o))}function u(){o("v",0)}var f,h=function(){},d=n.L={h:h,l:h,c:h};try{f=localStorage,d.h=a,d.l=l,d.c=o}catch(p){e()}}(this,document);</script>{{if .Version}}<script>L.c('v', {{.Version}})</script>{{end}}<link rel="apple-touch-icon" href="//{{.Qiniu.Domain}}/static/img/favicon.ico"><link rel="search" type="application/opensearchdescription+xml" href="//{{.Domain}}/opensearch.xml" title="{{.BTitle}}">{{if .Version}}<style id="blog_css">{{template "blog_css" .}}</style><script>L.h('blog_css')</script>{{else}}<script>L.l("blog_css")</script>{{end}}<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="//{{.Domain}}/rss.html"><meta name="description" content="{{.Description}}"><meta name="twitter:card" content={{.Twitter.Card}}><meta name="twitter:site" content="@{{.Twitter.Site}}"><meta name="twitter:title" content="{{.Title}}"><meta name="twitter:description" content="{{.Description}}"><meta name="twitter:image" content="https://{{.Twitter.Image}}"></head><body><div class="container"><div class="left-col"><div class="intrude-less"><header id="header" class="inner"><div class="profilepic"><a href="/"></a></div><h1><a href="/">{{.BlogName}}</a></h1><p class="subtitle">{{.SubTitle}}</p><nav id="main-nav"><ul><li {{if eq .Path "/"}}class="on" {{end}}><a href="/"><span>首页</span></a></li><li {{if eq .Path "/series.html"}}class="on" {{end}}><a href="/series.html"><span>专题</span></a></li><li {{if eq .Path "/archives.html"}}class="on" {{end}}><a href="/archives.html"><span>归档</span></a></li><li {{if eq .Path "/post/blogroll.html"}}class="on" {{end}}><a href="/post/blogroll.html"><span>友链</span></a></li><li {{if eq .Path "/post/about.html"}}class="on" {{end}}><a href="/post/about.html"><span>关于</span></a></li>{{range $index, $element := .Pages}}{{if .ShowInNav}}<li {{if eq $.Path .Path}}class="on" {{end}}><a href="{{.Path}}" {{if not .IsEmbed}}target="_blank"{{end}}><span>{{.Name}}</span></a></li>{{end}}{{end}}</ul></nav><nav id="sub-nav"><div class="social"><a target="_blank" class="twitter external" rel="nofollow" href="//{{.Twitter.Address}}" title="Twitter" aria-label="Twitter">Twitter</a><a target="_blank" class="rss" href="//{{.Domain}}/rss.html" title="RSS 订阅" aria-label="RSS 订阅">RSS</a><a class="search" href="/search.html" title="站内搜索" aria-label="站内搜索">Search</a></div></nav></header></div></div><div class="mid-col"><div class="mid-col-container">{{.LayoutContent}}</div><footer id=footer class=inner>© {{.CopyYear}} - {{.BTitle}}{{if .BeiAn}} - <a target="_blank" rel="nofollow designer" class="external beian" href="https://beian.miit.gov.cn">{{.BeiAn}}</a>{{end}}<br>Powered by <a target=_blank href="//github.com/eiblog/eiblog">Eiblog</a> & <a target=_blank rel="nofollow designer" class=external href="//github.com/deepzz0">Deepzz</a></footer></div></div><input type=hidden id=CURRENT_PAGE value="{{.CurrentPage}}"><input type=hidden id=CDN_DOMAIN value="//{{.Qiniu.Domain}}">{{if .Version}}<script id="ana_js">{{template "ana_js"}}</script><script>L.h('ana_js')</script>{{else}}<script>L.l('ana_js', 1)</script>{{end}}{{if .Version}}<script id="jq_js">{{template "jq_js"}}</script><script>L.h('jq_js')</script>{{else}}<script>L.l('jq_js', 1)</script>{{end}}{{if .Version}}<script id="hl_js">{{template "hl_js"}}</script><script>L.h('hl_js')</script>{{else}}<script>L.l('hl_js', 1)</script>{{end}}{{if .Version}}<script id="blog_js">{{template "blog_js" .}}</script><script>L.h('blog_js')</script>{{else}}<script>L.l('blog_js', 1)</script>{{end}}</body></html>
|
||||
3
cmd/eiblog/etc/template/st_ana.js
Normal file
@@ -0,0 +1,3 @@
|
||||
{{define "ana_js"}}
|
||||
!function(e,n,o){var t=e.screen,a=encodeURIComponent,r=["dt="+a(n.title),"dr="+a(n.referrer),"ul="+(o.language||o.browserLanguage).toLowerCase(),"sd="+t.colorDepth+"-bit","sr="+t.width+"x"+t.height,"_="+ +new Date],i="?"+r.join("&");e.__beacon_img=new Image,e.__beacon_img.src="/beacon.html"+i}(window,document,navigator,location);
|
||||
{{end}}
|
||||
3
cmd/eiblog/etc/template/st_blog.css
Normal file
@@ -1,16 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>{{.Title}}</title>
|
||||
<title>{{xmlescape .Title}}</title>
|
||||
<link>https://{{.Host}}</link>
|
||||
<description>{{.SubTitle}}</description>
|
||||
<description>{{xmlescape .SubTitle}}</description>
|
||||
<atom:link href="https://{{.Host}}/rss.html" rel="self" />
|
||||
<atom:link href="{{.FeedrURL}}" rel="hub" />
|
||||
<language>zh-CN</language>
|
||||
<lastBuildDate>{{.BuildDate}}</lastBuildDate>
|
||||
{{range .Articles}}
|
||||
<item>
|
||||
<title>{{.Title}}</title>
|
||||
<title>{{xmlescape .Title}}</title>
|
||||
<link>https://{{$.Host}}/post/{{.Slug}}.html</link>
|
||||
<comments>https://{{$.Host}}/post/{{.Slug}}.html#comments</comments>
|
||||
<guid>https://{{$.Host}}/post/{{.Slug}}.html</guid>
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package admin provides ...
|
||||
package admin
|
||||
|
||||
import (
|
||||
@@ -10,12 +9,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/core/eiblog"
|
||||
"github.com/eiblog/eiblog/pkg/internal"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/internal"
|
||||
"github.com/eiblog/eiblog/pkg/middleware"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
"github.com/eiblog/eiblog/pkg/third/qiniu"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -48,36 +48,74 @@ func RegisterRoutesAuthz(group gin.IRoutes) {
|
||||
group.POST("/api/trash-recover", handleAPITrashRecover)
|
||||
group.POST("/api/file-upload", handleAPIQiniuUpload)
|
||||
group.POST("/api/file-delete", handleAPIQiniuDelete)
|
||||
group.POST("/api/twofactor", handleAPITwoFactor)
|
||||
}
|
||||
|
||||
// handleAcctLogin 登录接口
|
||||
func handleAcctLogin(c *gin.Context) {
|
||||
user := c.PostForm("user")
|
||||
pwd := c.PostForm("password")
|
||||
// code := c.PostForm("code") // 二次验证
|
||||
code := c.PostForm("code")
|
||||
if user == "" || pwd == "" {
|
||||
logrus.Warnf("参数错误: %s %s", user, pwd)
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
if cache.Ei.Account.Username != user ||
|
||||
cache.Ei.Account.Password != tools.EncryptPasswd(user, pwd) {
|
||||
if internal.Ei.Account.Username != user ||
|
||||
internal.Ei.Account.Password != tools.EncryptPasswd(user, pwd) {
|
||||
logrus.Warnf("账号或密码错误 %s, %s", user, pwd)
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
// 两步验证
|
||||
if config.Conf.General.TwoFactor &&
|
||||
internal.Ei.Account.TwoFactorSecret != "" {
|
||||
valid := totp.Validate(code, internal.Ei.Account.TwoFactorSecret)
|
||||
if !valid {
|
||||
logrus.Warnf("两步验证: %s", code)
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
}
|
||||
// 登录成功
|
||||
eiblog.SetLogin(c, user)
|
||||
middleware.SetLogin(c, user)
|
||||
|
||||
cache.Ei.Account.LoginIP = c.ClientIP()
|
||||
cache.Ei.Account.LoginAt = time.Now()
|
||||
cache.Ei.UpdateAccount(context.Background(), user, map[string]interface{}{
|
||||
"login_ip": cache.Ei.Account.LoginIP,
|
||||
"login_at": cache.Ei.Account.LoginAt,
|
||||
internal.Ei.Account.LoginIP = c.ClientIP()
|
||||
internal.Ei.Account.LoginAt = time.Now()
|
||||
internal.Store.UpdateAccount(context.Background(), user, map[string]interface{}{
|
||||
"login_ip": internal.Ei.Account.LoginIP,
|
||||
"login_at": internal.Ei.Account.LoginAt,
|
||||
})
|
||||
c.Redirect(http.StatusFound, "/admin/profile")
|
||||
}
|
||||
|
||||
// handleAPITwoFactor 两步验证
|
||||
func handleAPITwoFactor(c *gin.Context) {
|
||||
code := c.PostForm("code")
|
||||
if code == "" {
|
||||
responseNotice(c, NoticeNotice, "验证码不能为空", "")
|
||||
return
|
||||
}
|
||||
valid := totp.Validate(code, internal.TwoFactorSecret)
|
||||
if !valid {
|
||||
responseNotice(c, NoticeNotice, "验证码错误", "")
|
||||
return
|
||||
}
|
||||
err := internal.Store.UpdateAccount(context.Background(), internal.Ei.Account.Username,
|
||||
map[string]interface{}{
|
||||
"two_factor_secret": internal.TwoFactorSecret,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error("handleAPITwoFactor.UpdateAccount: ", err)
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
internal.Ei.Account.TwoFactorSecret = internal.TwoFactorSecret
|
||||
internal.TwoFactorSecret = ""
|
||||
c.Request.Header.Set("Referer", "/admin/profile")
|
||||
responseNotice(c, NoticeSuccess, "绑定成功", "")
|
||||
}
|
||||
|
||||
// handleAPIBlogger 更新博客信息
|
||||
func handleAPIBlogger(c *gin.Context) {
|
||||
bn := c.PostForm("blogName")
|
||||
@@ -91,9 +129,10 @@ func handleAPIBlogger(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := cache.Ei.UpdateBlogger(context.Background(), map[string]interface{}{
|
||||
err := internal.Store.UpdateBlogger(context.Background(), map[string]interface{}{
|
||||
"blog_name": bn,
|
||||
"b_title": bt,
|
||||
"bei_an": ba,
|
||||
"sub_title": st,
|
||||
"series_say": ss,
|
||||
"archives_say": as,
|
||||
@@ -103,14 +142,14 @@ func handleAPIBlogger(c *gin.Context) {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
cache.Ei.Blogger.BlogName = bn
|
||||
cache.Ei.Blogger.BTitle = bt
|
||||
cache.Ei.Blogger.BeiAn = ba
|
||||
cache.Ei.Blogger.SubTitle = st
|
||||
cache.Ei.Blogger.SeriesSay = ss
|
||||
cache.Ei.Blogger.ArchivesSay = as
|
||||
cache.PagesCh <- cache.PageSeries
|
||||
cache.PagesCh <- cache.PageArchive
|
||||
internal.Ei.Blogger.BlogName = bn
|
||||
internal.Ei.Blogger.BTitle = bt
|
||||
internal.Ei.Blogger.BeiAn = ba
|
||||
internal.Ei.Blogger.SubTitle = st
|
||||
internal.Ei.Blogger.SeriesSay = ss
|
||||
internal.Ei.Blogger.ArchivesSay = as
|
||||
internal.PagesCh <- internal.PageSeries
|
||||
internal.PagesCh <- internal.PageArchive
|
||||
responseNotice(c, NoticeSuccess, "更新成功", "")
|
||||
}
|
||||
|
||||
@@ -125,7 +164,7 @@ func handleAPIAccount(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := cache.Ei.UpdateAccount(context.Background(), cache.Ei.Account.Username,
|
||||
err := internal.Store.UpdateAccount(context.Background(), internal.Ei.Account.Username,
|
||||
map[string]interface{}{
|
||||
"email": e,
|
||||
"phone_n": pn,
|
||||
@@ -136,9 +175,9 @@ func handleAPIAccount(c *gin.Context) {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
cache.Ei.Account.Email = e
|
||||
cache.Ei.Account.PhoneN = pn
|
||||
cache.Ei.Account.Address = ad
|
||||
internal.Ei.Account.Email = e
|
||||
internal.Ei.Account.PhoneN = pn
|
||||
internal.Ei.Account.Address = ad
|
||||
responseNotice(c, NoticeSuccess, "更新成功", "")
|
||||
}
|
||||
|
||||
@@ -155,13 +194,13 @@ func handleAPIPassword(c *gin.Context) {
|
||||
responseNotice(c, NoticeNotice, "密码格式错误", "")
|
||||
return
|
||||
}
|
||||
if cache.Ei.Account.Password != tools.EncryptPasswd(cache.Ei.Account.Username, od) {
|
||||
if internal.Ei.Account.Password != tools.EncryptPasswd(internal.Ei.Account.Username, od) {
|
||||
responseNotice(c, NoticeNotice, "原始密码不正确", "")
|
||||
return
|
||||
}
|
||||
newPwd := tools.EncryptPasswd(cache.Ei.Account.Username, nw)
|
||||
newPwd := tools.EncryptPasswd(internal.Ei.Account.Username, nw)
|
||||
|
||||
err := cache.Ei.UpdateAccount(context.Background(), cache.Ei.Account.Username,
|
||||
err := internal.Store.UpdateAccount(context.Background(), internal.Ei.Account.Username,
|
||||
map[string]interface{}{
|
||||
"password": newPwd,
|
||||
})
|
||||
@@ -170,7 +209,7 @@ func handleAPIPassword(c *gin.Context) {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
cache.Ei.Account.Password = newPwd
|
||||
internal.Ei.Account.Password = newPwd
|
||||
responseNotice(c, NoticeSuccess, "更新成功", "")
|
||||
}
|
||||
|
||||
@@ -182,7 +221,7 @@ func handleDraftDelete(c *gin.Context) {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.RemoveArticle(context.Background(), id)
|
||||
err = internal.Store.RemoveArticle(context.Background(), id)
|
||||
if err != nil {
|
||||
logrus.Error("handleDraftDelete.RemoveArticle: ", err)
|
||||
responseNotice(c, NoticeNotice, "删除失败", "")
|
||||
@@ -197,11 +236,11 @@ func handleAPIPostDelete(c *gin.Context) {
|
||||
var ids []int
|
||||
for _, v := range c.PostFormArray("cid[]") {
|
||||
id, err := strconv.Atoi(v)
|
||||
if err != nil || id < config.Conf.EiBlogApp.General.StartID {
|
||||
if err != nil || id < internal.ArticleStartID {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.DelArticle(id)
|
||||
err = internal.Ei.DelArticle(id)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIPostDelete.DelArticle: ", err)
|
||||
|
||||
@@ -211,12 +250,14 @@ func handleAPIPostDelete(c *gin.Context) {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
// elasticsearch
|
||||
err := internal.ElasticDelIndex(ids)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIPostDelete.ElasticDelIndex: ", err)
|
||||
if internal.ESClient != nil {
|
||||
err := internal.ESClient.ElasticDelIndex(ids)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIPostDelete.ElasticDelIndex: ", err)
|
||||
}
|
||||
}
|
||||
// TODO disqus delete
|
||||
responseNotice(c, NoticeSuccess, "删除成功", "")
|
||||
responseNotice(c, NoticeSuccess, "删除成功,已移入到回收箱", "")
|
||||
}
|
||||
|
||||
// handleAPIPostCreate 创建文章
|
||||
@@ -270,7 +311,7 @@ func handleAPIPostCreate(c *gin.Context) {
|
||||
Content: text,
|
||||
Slug: slug,
|
||||
IsDraft: do != "publish",
|
||||
Author: cache.Ei.Account.Username,
|
||||
Author: internal.Ei.Account.Username,
|
||||
SerieID: serieid,
|
||||
Tags: tags,
|
||||
CreatedAt: date,
|
||||
@@ -278,7 +319,7 @@ func handleAPIPostCreate(c *gin.Context) {
|
||||
cid, err = strconv.Atoi(c.PostForm("cid"))
|
||||
// 新文章
|
||||
if err != nil || cid < 1 {
|
||||
err = cache.Ei.AddArticle(article)
|
||||
err = internal.Ei.AddArticle(article)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIPostCreate.AddArticle: ", err)
|
||||
return
|
||||
@@ -287,21 +328,23 @@ func handleAPIPostCreate(c *gin.Context) {
|
||||
cid = article.ID
|
||||
|
||||
if !article.IsDraft {
|
||||
// disqus
|
||||
internal.DisqusClient.ThreadCreate(article, internal.Ei.Blogger.BTitle)
|
||||
// 异步执行,快
|
||||
go func() {
|
||||
// elastic
|
||||
internal.ElasticAddIndex(article)
|
||||
if internal.ESClient != nil {
|
||||
internal.ESClient.ElasticAddIndex(article)
|
||||
}
|
||||
// rss
|
||||
internal.PingFunc(cache.Ei.Blogger.BTitle, slug)
|
||||
// disqus
|
||||
internal.ThreadCreate(article, cache.Ei.Blogger.BTitle)
|
||||
internal.Pinger.PingFunc(internal.Ei.Blogger.BTitle, slug)
|
||||
}()
|
||||
}
|
||||
return
|
||||
}
|
||||
// 旧文章
|
||||
article.ID = cid
|
||||
artc, _ := cache.Ei.FindArticleByID(article.ID) // cache
|
||||
artc, _ := internal.Ei.FindArticleByID(article.ID) // cache
|
||||
if artc != nil {
|
||||
article.IsDraft = false
|
||||
article.Count = artc.Count
|
||||
@@ -311,7 +354,7 @@ func handleAPIPostCreate(c *gin.Context) {
|
||||
article.UpdatedAt = time.Now()
|
||||
}
|
||||
// 数据库更新
|
||||
err = cache.Ei.UpdateArticle(context.Background(), article.ID, map[string]interface{}{
|
||||
err = internal.Store.UpdateArticle(context.Background(), article.ID, map[string]interface{}{
|
||||
"title": article.Title,
|
||||
"content": article.Content,
|
||||
"serie_id": article.SerieID,
|
||||
@@ -325,17 +368,19 @@ func handleAPIPostCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if !article.IsDraft {
|
||||
cache.Ei.RepArticle(artc, article)
|
||||
internal.Ei.RepArticle(artc, article)
|
||||
// disqus
|
||||
if artc == nil {
|
||||
internal.DisqusClient.ThreadCreate(article, internal.Ei.Blogger.BTitle)
|
||||
}
|
||||
// 异步执行,快
|
||||
go func() {
|
||||
// elastic
|
||||
internal.ElasticAddIndex(article)
|
||||
// rss
|
||||
internal.PingFunc(cache.Ei.Blogger.BTitle, slug)
|
||||
// disqus
|
||||
if artc == nil {
|
||||
internal.ThreadCreate(article, cache.Ei.Blogger.BTitle)
|
||||
if internal.ESClient != nil {
|
||||
internal.ESClient.ElasticAddIndex(article)
|
||||
}
|
||||
// rss
|
||||
internal.Pinger.PingFunc(internal.Ei.Blogger.BTitle, slug)
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -348,7 +393,7 @@ func handleAPISerieDelete(c *gin.Context) {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.DelSerie(id)
|
||||
err = internal.Ei.DelSerie(id)
|
||||
if err != nil {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
@@ -375,7 +420,7 @@ func handleAPISerieCreate(c *gin.Context) {
|
||||
mid, err := strconv.Atoi(c.PostForm("mid"))
|
||||
if err == nil && mid > 0 {
|
||||
var serie *model.Serie
|
||||
for _, v := range cache.Ei.Series {
|
||||
for _, v := range internal.Ei.Series {
|
||||
if v.ID == mid {
|
||||
serie = v
|
||||
break
|
||||
@@ -385,7 +430,7 @@ func handleAPISerieCreate(c *gin.Context) {
|
||||
responseNotice(c, NoticeNotice, "专题不存在", "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.UpdateSerie(context.Background(), mid, map[string]interface{}{
|
||||
err = internal.Store.UpdateSerie(context.Background(), mid, map[string]interface{}{
|
||||
"slug": slug,
|
||||
"name": name,
|
||||
"desc": desc,
|
||||
@@ -398,8 +443,9 @@ func handleAPISerieCreate(c *gin.Context) {
|
||||
serie.Slug = slug
|
||||
serie.Name = name
|
||||
serie.Desc = desc
|
||||
internal.PagesCh <- internal.PageSeries
|
||||
} else {
|
||||
err = cache.Ei.AddSerie(&model.Serie{
|
||||
err = internal.Ei.AddSerie(&model.Serie{
|
||||
Slug: slug,
|
||||
Name: name,
|
||||
Desc: desc,
|
||||
@@ -422,7 +468,7 @@ func handleAPITrashDelete(c *gin.Context) {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.RemoveArticle(context.Background(), id)
|
||||
err = internal.Store.RemoveArticle(context.Background(), id)
|
||||
if err != nil {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
@@ -440,7 +486,7 @@ func handleAPITrashRecover(c *gin.Context) {
|
||||
return
|
||||
|
||||
}
|
||||
err = cache.Ei.UpdateArticle(context.Background(), id, map[string]interface{}{
|
||||
err = internal.Store.UpdateArticle(context.Background(), id, map[string]interface{}{
|
||||
"deleted_at": time.Time{},
|
||||
"is_draft": true,
|
||||
})
|
||||
@@ -471,14 +517,12 @@ func handleAPIQiniuUpload(c *gin.Context) {
|
||||
}
|
||||
filename := strings.ToLower(header.Filename)
|
||||
|
||||
params := internal.UploadParams{
|
||||
params := qiniu.UploadParams{
|
||||
Name: filename,
|
||||
Size: s.Size(),
|
||||
Data: file,
|
||||
|
||||
Conf: config.Conf.EiBlogApp.Qiniu,
|
||||
}
|
||||
url, err := internal.QiniuUpload(params)
|
||||
url, err := internal.QiniuClient.Upload(params)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIQiniuUpload.QiniuUpload: ", err)
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
@@ -503,12 +547,10 @@ func handleAPIQiniuDelete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
params := internal.DeleteParams{
|
||||
params := qiniu.DeleteParams{
|
||||
Name: name,
|
||||
|
||||
Conf: config.Conf.EiBlogApp.Qiniu,
|
||||
}
|
||||
err := internal.QiniuDelete(params)
|
||||
err := internal.QiniuClient.Delete(params)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIQiniuDelete.QiniuDelete: ", err)
|
||||
}
|
||||
69
cmd/eiblog/handler/file/file.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterRoutes register routes
|
||||
func RegisterRoutes(e *gin.Engine) {
|
||||
e.GET("/rss.html", handleFeed())
|
||||
e.GET("/feed", handleFeed())
|
||||
e.GET("/opensearch.xml", handleOpensearch())
|
||||
e.GET("/sitemap.xml", handleSitemap())
|
||||
e.GET("/robots.txt", handleRobots())
|
||||
e.GET("/crossdomain.xml", handleCrossDomain())
|
||||
e.GET("/favicon.ico", handleFavicon())
|
||||
}
|
||||
|
||||
// handleFeed feed.xml
|
||||
func handleFeed() gin.HandlerFunc {
|
||||
path := filepath.Join(config.EtcDir, "assets", "feed.xml")
|
||||
return func(c *gin.Context) {
|
||||
http.ServeFile(c.Writer, c.Request, path)
|
||||
}
|
||||
}
|
||||
|
||||
// handleOpensearch opensearch.xml
|
||||
func handleOpensearch() gin.HandlerFunc {
|
||||
path := filepath.Join(config.EtcDir, "assets", "opensearch.xml")
|
||||
return func(c *gin.Context) {
|
||||
http.ServeFile(c.Writer, c.Request, path)
|
||||
}
|
||||
}
|
||||
|
||||
// handleRobots robotx.txt
|
||||
func handleRobots() gin.HandlerFunc {
|
||||
path := filepath.Join(config.EtcDir, "assets", "robots.txt")
|
||||
return func(c *gin.Context) {
|
||||
http.ServeFile(c.Writer, c.Request, path)
|
||||
}
|
||||
}
|
||||
|
||||
// handleSitemap sitemap.xml
|
||||
func handleSitemap() gin.HandlerFunc {
|
||||
path := filepath.Join(config.EtcDir, "assets", "sitemap.xml")
|
||||
return func(c *gin.Context) {
|
||||
http.ServeFile(c.Writer, c.Request, path)
|
||||
}
|
||||
}
|
||||
|
||||
// handleCrossDomain crossdomain.xml
|
||||
func handleCrossDomain() gin.HandlerFunc {
|
||||
path := filepath.Join(config.EtcDir, "assets", "crossdomain.xml")
|
||||
return func(c *gin.Context) {
|
||||
http.ServeFile(c.Writer, c.Request, path)
|
||||
}
|
||||
}
|
||||
|
||||
// handleFavicon favicon.ico
|
||||
func handleFavicon() gin.HandlerFunc {
|
||||
path := filepath.Join(config.EtcDir, "assets", "favicon.ico")
|
||||
return func(c *gin.Context) {
|
||||
http.ServeFile(c.Writer, c.Request, path)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package cache provides ...
|
||||
package cache
|
||||
// Package internal provides ...
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -11,20 +11,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache/render"
|
||||
"github.com/eiblog/eiblog/pkg/cache/store"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/internal"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/internal/store"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
// Ei eiblog cache
|
||||
Ei *Cache
|
||||
|
||||
// PagesCh regenerate pages chan
|
||||
PagesCh = make(chan string, 2)
|
||||
// PageSeries the page series regenerate flag
|
||||
@@ -34,45 +27,11 @@ var (
|
||||
|
||||
// ArticleStartID article start id
|
||||
ArticleStartID = 11
|
||||
// TrashArticleExp trash article timeout
|
||||
TrashArticleExp = time.Duration(-48) * time.Hour
|
||||
)
|
||||
|
||||
func init() {
|
||||
// init timezone
|
||||
var err error
|
||||
tools.TimeLocation, err = time.LoadLocation(
|
||||
config.Conf.EiBlogApp.General.Timezone)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// init store
|
||||
logrus.Info("store drivers: ", store.Drivers())
|
||||
store, err := store.NewStore(config.Conf.Database.Driver,
|
||||
config.Conf.Database.Source)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Ei init
|
||||
Ei = &Cache{
|
||||
lock: sync.Mutex{},
|
||||
Store: store,
|
||||
TagArticles: make(map[string]model.SortedArticles),
|
||||
ArticlesMap: make(map[string]*model.Article),
|
||||
}
|
||||
err = Ei.loadOrInit()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go Ei.regeneratePages()
|
||||
go Ei.timerClean()
|
||||
go Ei.timerDisqus()
|
||||
}
|
||||
|
||||
// Cache 整站缓存
|
||||
type Cache struct {
|
||||
lock sync.Mutex
|
||||
store.Store
|
||||
|
||||
// load from db
|
||||
Blogger *model.Blogger
|
||||
@@ -88,13 +47,30 @@ type Cache struct {
|
||||
ArticlesMap map[string]*model.Article // slug:article
|
||||
}
|
||||
|
||||
// NewCache 缓存整个博客数据
|
||||
func NewCache() (*Cache, error) {
|
||||
// Ei init
|
||||
cache := &Cache{
|
||||
lock: sync.Mutex{},
|
||||
TagArticles: make(map[string]model.SortedArticles),
|
||||
ArticlesMap: make(map[string]*model.Article),
|
||||
}
|
||||
err := cache.loadOrInit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 异步渲染series,archive页面
|
||||
go cache.regeneratePages()
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
// AddArticle 添加文章
|
||||
func (c *Cache) AddArticle(article *model.Article) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
func (cache *Cache) AddArticle(article *model.Article) error {
|
||||
cache.lock.Lock()
|
||||
defer cache.lock.Unlock()
|
||||
|
||||
// store
|
||||
err := c.InsertArticle(context.Background(), article, ArticleStartID)
|
||||
err := Store.InsertArticle(context.Background(), article, ArticleStartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -103,77 +79,77 @@ func (c *Cache) AddArticle(article *model.Article) error {
|
||||
return nil
|
||||
}
|
||||
// 正式发布文章
|
||||
c.refreshCache(article, false)
|
||||
cache.refreshCache(article, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RepArticle 替换文章
|
||||
func (c *Cache) RepArticle(oldArticle, newArticle *model.Article) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
func (cache *Cache) RepArticle(oldArticle, newArticle *model.Article) {
|
||||
cache.lock.Lock()
|
||||
defer cache.lock.Unlock()
|
||||
|
||||
c.ArticlesMap[newArticle.Slug] = newArticle
|
||||
render.GenerateExcerptMarkdown(newArticle)
|
||||
cache.ArticlesMap[newArticle.Slug] = newArticle
|
||||
GenerateExcerptMarkdown(newArticle)
|
||||
if newArticle.ID < ArticleStartID {
|
||||
return
|
||||
}
|
||||
if oldArticle != nil { // 移除旧文章
|
||||
c.refreshCache(oldArticle, true)
|
||||
cache.refreshCache(oldArticle, true)
|
||||
}
|
||||
c.refreshCache(newArticle, false)
|
||||
cache.refreshCache(newArticle, false)
|
||||
}
|
||||
|
||||
// DelArticle 删除文章
|
||||
func (c *Cache) DelArticle(id int) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
func (cache *Cache) DelArticle(id int) error {
|
||||
cache.lock.Lock()
|
||||
defer cache.lock.Unlock()
|
||||
|
||||
article, _ := c.FindArticleByID(id)
|
||||
article, _ := cache.FindArticleByID(id)
|
||||
if article == nil {
|
||||
return nil
|
||||
}
|
||||
// set delete
|
||||
err := c.UpdateArticle(context.Background(), id, map[string]interface{}{
|
||||
err := Store.UpdateArticle(context.Background(), id, map[string]interface{}{
|
||||
"deleted_at": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// drop from tags,series,archives
|
||||
c.refreshCache(article, true)
|
||||
cache.refreshCache(article, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSerie 添加专题
|
||||
func (c *Cache) AddSerie(serie *model.Serie) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
func (cache *Cache) AddSerie(serie *model.Serie) error {
|
||||
cache.lock.Lock()
|
||||
defer cache.lock.Unlock()
|
||||
|
||||
err := c.InsertSerie(context.Background(), serie)
|
||||
err := Store.InsertSerie(context.Background(), serie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Series = append(c.Series, serie)
|
||||
cache.Series = append(cache.Series, serie)
|
||||
PagesCh <- PageSeries
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelSerie 删除专题
|
||||
func (c *Cache) DelSerie(id int) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
func (cache *Cache) DelSerie(id int) error {
|
||||
cache.lock.Lock()
|
||||
defer cache.lock.Unlock()
|
||||
|
||||
for i, serie := range c.Series {
|
||||
for i, serie := range cache.Series {
|
||||
if serie.ID == id {
|
||||
if len(serie.Articles) > 0 {
|
||||
return errors.New("请删除该专题下的所有文章")
|
||||
}
|
||||
err := c.RemoveSerie(context.Background(), id)
|
||||
err := Store.RemoveSerie(context.Background(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Series[i] = nil
|
||||
c.Series = append(c.Series[:i], c.Series[i+1:]...)
|
||||
cache.Series[i] = nil
|
||||
cache.Series = append(cache.Series[:i], cache.Series[i+1:]...)
|
||||
PagesCh <- PageSeries
|
||||
break
|
||||
}
|
||||
@@ -182,12 +158,12 @@ func (c *Cache) DelSerie(id int) error {
|
||||
}
|
||||
|
||||
// PageArticleFE 文章翻页
|
||||
func (c *Cache) PageArticleFE(page int, pageSize int) (prev,
|
||||
func (cache *Cache) PageArticleFE(page int, pageSize int) (prev,
|
||||
next int, articles []*model.Article) {
|
||||
|
||||
var l int
|
||||
for l = len(c.Articles); l > 0; l-- {
|
||||
if c.Articles[l-1].ID >= ArticleStartID {
|
||||
for l = len(cache.Articles); l > 0; l-- {
|
||||
if cache.Articles[l-1].ID >= ArticleStartID {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -212,12 +188,12 @@ func (c *Cache) PageArticleFE(page int, pageSize int) (prev,
|
||||
if e > l {
|
||||
e = l
|
||||
}
|
||||
articles = c.Articles[s:e]
|
||||
articles = cache.Articles[s:e]
|
||||
return
|
||||
}
|
||||
|
||||
// PageArticleBE 后台文章分页
|
||||
func (c *Cache) PageArticleBE(se int, kw string, draft, del bool, p,
|
||||
func (cache *Cache) PageArticleBE(se int, kw string, draft, del bool, p,
|
||||
n int) ([]*model.Article, int) {
|
||||
|
||||
search := store.SearchArticles{
|
||||
@@ -238,7 +214,7 @@ func (c *Cache) PageArticleBE(se int, kw string, draft, del bool, p,
|
||||
search.Fields[store.SearchArticleTitle] = kw
|
||||
}
|
||||
}
|
||||
articles, count, err := c.LoadArticleList(context.Background(), search)
|
||||
articles, count, err := Store.LoadArticleList(context.Background(), search)
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
@@ -250,8 +226,8 @@ func (c *Cache) PageArticleBE(se int, kw string, draft, del bool, p,
|
||||
}
|
||||
|
||||
// FindArticleByID 通过ID查找文章
|
||||
func (c *Cache) FindArticleByID(id int) (*model.Article, int) {
|
||||
for i, article := range c.Articles {
|
||||
func (cache *Cache) FindArticleByID(id int) (*model.Article, int) {
|
||||
for i, article := range cache.Articles {
|
||||
if article.ID == id {
|
||||
return article, i
|
||||
}
|
||||
@@ -260,32 +236,32 @@ func (c *Cache) FindArticleByID(id int) (*model.Article, int) {
|
||||
}
|
||||
|
||||
// refreshCache 刷新缓存
|
||||
func (c *Cache) refreshCache(article *model.Article, del bool) {
|
||||
func (cache *Cache) refreshCache(article *model.Article, del bool) {
|
||||
if del {
|
||||
_, idx := c.FindArticleByID(article.ID)
|
||||
_, idx := cache.FindArticleByID(article.ID)
|
||||
|
||||
delete(c.ArticlesMap, article.Slug)
|
||||
c.Articles = append(c.Articles[:idx], c.Articles[idx+1:]...)
|
||||
delete(cache.ArticlesMap, article.Slug)
|
||||
cache.Articles = append(cache.Articles[:idx], cache.Articles[idx+1:]...)
|
||||
// 从链表移除
|
||||
c.recalcLinkedList(article, true)
|
||||
cache.recalcLinkedList(article, true)
|
||||
// 从tag、serie、archive移除
|
||||
c.redelArticle(article)
|
||||
cache.redelArticle(article)
|
||||
return
|
||||
}
|
||||
// 添加文章
|
||||
defer render.GenerateExcerptMarkdown(article)
|
||||
defer GenerateExcerptMarkdown(article)
|
||||
|
||||
c.ArticlesMap[article.Slug] = article
|
||||
c.Articles = append([]*model.Article{article}, c.Articles...)
|
||||
sort.Sort(c.Articles)
|
||||
cache.ArticlesMap[article.Slug] = article
|
||||
cache.Articles = append([]*model.Article{article}, cache.Articles...)
|
||||
sort.Sort(cache.Articles)
|
||||
// 从链表添加
|
||||
c.recalcLinkedList(article, false)
|
||||
cache.recalcLinkedList(article, false)
|
||||
// 从tag、serie、archive添加
|
||||
c.readdArticle(article, true)
|
||||
cache.readdArticle(article, true)
|
||||
}
|
||||
|
||||
// recalcLinkedList 重算文章链表
|
||||
func (c *Cache) recalcLinkedList(article *model.Article, del bool) {
|
||||
func (cache *Cache) recalcLinkedList(article *model.Article, del bool) {
|
||||
// 删除操作
|
||||
if del {
|
||||
if article.Prev == nil && article.Next != nil {
|
||||
@@ -299,56 +275,56 @@ func (c *Cache) recalcLinkedList(article *model.Article, del bool) {
|
||||
return
|
||||
}
|
||||
// 添加操作
|
||||
_, idx := c.FindArticleByID(article.ID)
|
||||
if idx == 0 && c.Articles[idx+1].ID >= ArticleStartID {
|
||||
article.Next = c.Articles[idx+1]
|
||||
c.Articles[idx+1].Prev = article
|
||||
} else if idx > 0 && c.Articles[idx-1].ID >= ArticleStartID {
|
||||
article.Prev = c.Articles[idx-1]
|
||||
if c.Articles[idx-1].Next != nil {
|
||||
article.Next = c.Articles[idx-1].Next
|
||||
c.Articles[idx-1].Next.Prev = article
|
||||
_, idx := cache.FindArticleByID(article.ID)
|
||||
if idx == 0 && cache.Articles[idx+1].ID >= ArticleStartID {
|
||||
article.Next = cache.Articles[idx+1]
|
||||
cache.Articles[idx+1].Prev = article
|
||||
} else if idx > 0 && cache.Articles[idx-1].ID >= ArticleStartID {
|
||||
article.Prev = cache.Articles[idx-1]
|
||||
if cache.Articles[idx-1].Next != nil {
|
||||
article.Next = cache.Articles[idx-1].Next
|
||||
cache.Articles[idx-1].Next.Prev = article
|
||||
}
|
||||
c.Articles[idx-1].Next = article
|
||||
cache.Articles[idx-1].Next = article
|
||||
}
|
||||
}
|
||||
|
||||
// readdArticle 添加文章到tag、series、archive
|
||||
func (c *Cache) readdArticle(article *model.Article, needSort bool) {
|
||||
func (cache *Cache) readdArticle(article *model.Article, needSort bool) {
|
||||
// tag
|
||||
for _, tag := range article.Tags {
|
||||
c.TagArticles[tag] = append(c.TagArticles[tag], article)
|
||||
cache.TagArticles[tag] = append(cache.TagArticles[tag], article)
|
||||
if needSort {
|
||||
sort.Sort(c.TagArticles[tag])
|
||||
sort.Sort(cache.TagArticles[tag])
|
||||
}
|
||||
}
|
||||
// series
|
||||
for i, serie := range c.Series {
|
||||
for i, serie := range cache.Series {
|
||||
if serie.ID != article.SerieID {
|
||||
continue
|
||||
}
|
||||
c.Series[i].Articles = append(c.Series[i].Articles, article)
|
||||
cache.Series[i].Articles = append(cache.Series[i].Articles, article)
|
||||
if needSort {
|
||||
sort.Sort(c.Series[i].Articles)
|
||||
sort.Sort(cache.Series[i].Articles)
|
||||
PagesCh <- PageSeries // 重建专题
|
||||
}
|
||||
}
|
||||
// archive
|
||||
y, m, _ := article.CreatedAt.Date()
|
||||
for i, archive := range c.Archives {
|
||||
for i, archive := range cache.Archives {
|
||||
ay, am, _ := archive.Time.Date()
|
||||
if y != ay || m != am {
|
||||
continue
|
||||
}
|
||||
c.Archives[i].Articles = append(c.Archives[i].Articles, article)
|
||||
cache.Archives[i].Articles = append(cache.Archives[i].Articles, article)
|
||||
if needSort {
|
||||
sort.Sort(c.Archives[i].Articles)
|
||||
sort.Sort(cache.Archives[i].Articles)
|
||||
PagesCh <- PageArchive // 重建归档
|
||||
}
|
||||
return
|
||||
}
|
||||
// 新建归档
|
||||
c.Archives = append(c.Archives, &model.Archive{
|
||||
cache.Archives = append(cache.Archives, &model.Archive{
|
||||
Time: article.CreatedAt,
|
||||
Articles: model.SortedArticles{article},
|
||||
})
|
||||
@@ -358,25 +334,25 @@ func (c *Cache) readdArticle(article *model.Article, needSort bool) {
|
||||
}
|
||||
|
||||
// redelArticle 从tag、series、archive删除文章
|
||||
func (c *Cache) redelArticle(article *model.Article) {
|
||||
func (cache *Cache) redelArticle(article *model.Article) {
|
||||
// tag
|
||||
for _, tag := range article.Tags {
|
||||
for i, v := range c.TagArticles[tag] {
|
||||
for i, v := range cache.TagArticles[tag] {
|
||||
if v == article {
|
||||
c.TagArticles[tag] = append(c.TagArticles[tag][0:i], c.TagArticles[tag][i+1:]...)
|
||||
if len(c.TagArticles[tag]) == 0 {
|
||||
delete(c.TagArticles, tag)
|
||||
cache.TagArticles[tag] = append(cache.TagArticles[tag][0:i], cache.TagArticles[tag][i+1:]...)
|
||||
if len(cache.TagArticles[tag]) == 0 {
|
||||
delete(cache.TagArticles, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// serie
|
||||
for i, serie := range c.Series {
|
||||
for i, serie := range cache.Series {
|
||||
if serie.ID == article.SerieID {
|
||||
for j, v := range serie.Articles {
|
||||
if v == article {
|
||||
c.Series[i].Articles = append(c.Series[i].Articles[0:j],
|
||||
c.Series[i].Articles[j+1:]...)
|
||||
cache.Series[i].Articles = append(cache.Series[i].Articles[0:j],
|
||||
cache.Series[i].Articles[j+1:]...)
|
||||
PagesCh <- PageSeries
|
||||
break
|
||||
}
|
||||
@@ -384,15 +360,15 @@ func (c *Cache) redelArticle(article *model.Article) {
|
||||
}
|
||||
}
|
||||
// archive
|
||||
for i, archive := range c.Archives {
|
||||
for i, archive := range cache.Archives {
|
||||
ay, am, _ := archive.Time.Date()
|
||||
if y, m, _ := article.CreatedAt.Date(); ay == y && am == m {
|
||||
for j, v := range archive.Articles {
|
||||
if v == article {
|
||||
c.Archives[i].Articles = append(c.Archives[i].Articles[0:j],
|
||||
c.Archives[i].Articles[j+1:]...)
|
||||
if len(c.Archives[i].Articles) == 0 {
|
||||
c.Archives = append(c.Archives[:i], c.Archives[i+1:]...)
|
||||
cache.Archives[i].Articles = append(cache.Archives[i].Articles[0:j],
|
||||
cache.Archives[i].Articles[j+1:]...)
|
||||
if len(cache.Archives[i].Articles) == 0 {
|
||||
cache.Archives = append(cache.Archives[:i], cache.Archives[i+1:]...)
|
||||
}
|
||||
PagesCh <- PageArchive
|
||||
break
|
||||
@@ -403,81 +379,79 @@ func (c *Cache) redelArticle(article *model.Article) {
|
||||
}
|
||||
|
||||
// loadOrInit 读取数据或初始化
|
||||
func (c *Cache) loadOrInit() error {
|
||||
blogapp := config.Conf.EiBlogApp
|
||||
func (cache *Cache) loadOrInit() error {
|
||||
// blogger
|
||||
blogger := &model.Blogger{
|
||||
BlogName: strings.Title(blogapp.Account.Username),
|
||||
BlogName: strings.Title(config.Conf.Account.Username),
|
||||
SubTitle: "Rome was not built in one day.",
|
||||
BeiAn: "蜀ICP备xxxxxxxx号-1",
|
||||
BTitle: fmt.Sprintf("%s's Blog", strings.Title(blogapp.Account.Username)),
|
||||
BTitle: fmt.Sprintf("%s's Blog", strings.Title(config.Conf.Account.Username)),
|
||||
Copyright: `本站使用「<a href="//creativecommons.org/licenses/by/4.0/">署名 4.0 国际</a>」创作共享协议,转载请注明作者及原网址。`,
|
||||
}
|
||||
created, err := c.LoadInsertBlogger(context.Background(), blogger)
|
||||
created, err := Store.LoadInsertBlogger(context.Background(), blogger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Blogger = blogger
|
||||
cache.Blogger = blogger
|
||||
if created { // init articles: about blogroll
|
||||
about := &model.Article{
|
||||
ID: 1, // 固定ID
|
||||
Author: blogapp.Account.Username,
|
||||
Author: config.Conf.Account.Username,
|
||||
Title: "关于",
|
||||
Slug: "about",
|
||||
CreatedAt: time.Time{}.AddDate(0, 0, 1),
|
||||
}
|
||||
err = c.InsertArticle(context.Background(), about, ArticleStartID)
|
||||
err = Store.InsertArticle(context.Background(), about, ArticleStartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 推送到 disqus
|
||||
go internal.ThreadCreate(about, blogger.BTitle)
|
||||
go DisqusClient.ThreadCreate(about, blogger.BTitle)
|
||||
blogroll := &model.Article{
|
||||
ID: 2, // 固定ID
|
||||
Author: blogapp.Account.Username,
|
||||
Author: config.Conf.Account.Username,
|
||||
Title: "友情链接",
|
||||
Slug: "blogroll",
|
||||
CreatedAt: time.Time{}.AddDate(0, 0, 7),
|
||||
}
|
||||
err = c.InsertArticle(context.Background(), blogroll, ArticleStartID)
|
||||
err = Store.InsertArticle(context.Background(), blogroll, ArticleStartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// account
|
||||
pwd := tools.EncryptPasswd(blogapp.Account.Username,
|
||||
blogapp.Account.Password)
|
||||
pwd := tools.EncryptPasswd(config.Conf.Account.Username, config.Conf.Account.Password)
|
||||
|
||||
account := &model.Account{
|
||||
Username: blogapp.Account.Username,
|
||||
Username: config.Conf.Account.Username,
|
||||
Password: pwd,
|
||||
}
|
||||
_, err = c.LoadInsertAccount(context.Background(), account)
|
||||
_, err = Store.LoadInsertAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Account = account
|
||||
cache.Account = account
|
||||
// series
|
||||
series, err := c.LoadAllSerie(context.Background())
|
||||
series, err := Store.LoadAllSerie(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Series = series
|
||||
cache.Series = series
|
||||
// all articles
|
||||
search := store.SearchArticles{
|
||||
Page: 1,
|
||||
Limit: 9999,
|
||||
Fields: map[string]interface{}{store.SearchArticleDraft: false},
|
||||
}
|
||||
articles, _, err := c.LoadArticleList(context.Background(), search)
|
||||
articles, _, err := Store.LoadArticleList(context.Background(), search)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, v := range articles {
|
||||
// 渲染页面
|
||||
render.GenerateExcerptMarkdown(v)
|
||||
GenerateExcerptMarkdown(v)
|
||||
|
||||
c.ArticlesMap[v.Slug] = v
|
||||
cache.ArticlesMap[v.Slug] = v
|
||||
// 分析文章
|
||||
if v.ID < ArticleStartID {
|
||||
continue
|
||||
@@ -489,9 +463,9 @@ func (c *Cache) loadOrInit() error {
|
||||
articles[i+1].ID >= ArticleStartID {
|
||||
v.Next = articles[i+1]
|
||||
}
|
||||
c.readdArticle(v, false)
|
||||
cache.readdArticle(v, false)
|
||||
}
|
||||
Ei.Articles = articles
|
||||
cache.Articles = articles
|
||||
// 重建专题与归档
|
||||
PagesCh <- PageSeries
|
||||
PagesCh <- PageArchive
|
||||
@@ -499,15 +473,15 @@ func (c *Cache) loadOrInit() error {
|
||||
}
|
||||
|
||||
// regeneratePages 重新生成series,archive页面
|
||||
func (c *Cache) regeneratePages() {
|
||||
func (cache *Cache) regeneratePages() {
|
||||
for {
|
||||
switch page := <-PagesCh; page {
|
||||
case PageSeries:
|
||||
sort.Sort(c.Series)
|
||||
sort.Sort(cache.Series)
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString(c.Blogger.SeriesSay)
|
||||
buf.WriteString(cache.Blogger.SeriesSay)
|
||||
buf.WriteString("\n\n")
|
||||
for _, series := range c.Series {
|
||||
for _, series := range cache.Series {
|
||||
buf.WriteString(fmt.Sprintf("### %s{#toc-%d}", series.Name, series.ID))
|
||||
buf.WriteByte('\n')
|
||||
buf.WriteString(series.Desc)
|
||||
@@ -520,16 +494,16 @@ func (c *Cache) regeneratePages() {
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
c.PageSeries = string(render.PageRender(buf.Bytes()))
|
||||
cache.PageSeries = string(PageRender(buf.Bytes()))
|
||||
case PageArchive:
|
||||
sort.Sort(c.Archives)
|
||||
sort.Sort(cache.Archives)
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString(c.Blogger.ArchivesSay + "\n")
|
||||
buf.WriteString(cache.Blogger.ArchivesSay + "\n")
|
||||
var (
|
||||
currentYear string
|
||||
gt12Month = len(c.Archives) > 12
|
||||
gt12Month = len(cache.Archives) > 12
|
||||
)
|
||||
for _, archive := range c.Archives {
|
||||
for _, archive := range cache.Archives {
|
||||
t := archive.Time.In(tools.TimeLocation)
|
||||
if gt12Month {
|
||||
year := t.Format("2006 年")
|
||||
@@ -553,32 +527,7 @@ func (c *Cache) regeneratePages() {
|
||||
}
|
||||
}
|
||||
}
|
||||
c.PageArchives = string(render.PageRender(buf.Bytes()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// timerClean 定时清理文章
|
||||
func (c *Cache) timerClean() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
|
||||
for now := range ticker.C {
|
||||
exp := now.Add(TrashArticleExp)
|
||||
err := c.CleanArticles(context.Background(), exp)
|
||||
if err != nil {
|
||||
logrus.Error("cache.timerClean.CleanArticles: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// timerDisqus disqus定时操作
|
||||
func (c *Cache) timerDisqus() {
|
||||
ticker := time.NewTicker(5 * time.Hour)
|
||||
|
||||
for range ticker.C {
|
||||
err := internal.PostsCount(c.ArticlesMap)
|
||||
if err != nil {
|
||||
logrus.Error("cache.timerDisqus.PostsCount: ", err)
|
||||
cache.PageArchives = string(PageRender(buf.Bytes()))
|
||||
}
|
||||
}
|
||||
}
|
||||
98
cmd/eiblog/handler/internal/internal.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/internal/store"
|
||||
"github.com/eiblog/eiblog/pkg/third/disqus"
|
||||
"github.com/eiblog/eiblog/pkg/third/es"
|
||||
"github.com/eiblog/eiblog/pkg/third/pinger"
|
||||
"github.com/eiblog/eiblog/pkg/third/qiniu"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
XMLTemplate *template.Template // template/xml模板
|
||||
HTMLTemplate *template.Template // page/html | website/html模板
|
||||
|
||||
Store store.Store // 数据库存储
|
||||
Ei *Cache // 博客数据缓存
|
||||
TwoFactorSecret string // 缓存两步验证密钥
|
||||
|
||||
ESClient *es.ESClient // es 客户端
|
||||
DisqusClient *disqus.DisqusClient // disqus 客户端
|
||||
QiniuClient *qiniu.QiniuClient // qiniu客户端
|
||||
Pinger *pinger.Pinger // pinger 客户端
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
tools.TimeLocation, err = time.LoadLocation(config.Conf.General.Timezone)
|
||||
if err != nil {
|
||||
logrus.Fatal("init timezone: ", err)
|
||||
}
|
||||
// 模板解析初始化
|
||||
root := filepath.Join(config.EtcDir, "xml", "*.xml")
|
||||
XMLTemplate, err = template.New("eiblog").Funcs(tools.TplFuncMap).ParseGlob(root)
|
||||
if err != nil {
|
||||
logrus.Fatal("init xml template: ", err)
|
||||
}
|
||||
root = filepath.Join(config.EtcDir, "template")
|
||||
files := tools.ReadDirFiles(root, func(fi fs.DirEntry) bool {
|
||||
// should not read dir & .DS_Store
|
||||
return strings.HasPrefix(fi.Name(), ".")
|
||||
})
|
||||
root = filepath.Join(config.EtcDir, "page")
|
||||
pageFiles := tools.ReadDirFiles(root, func(fi fs.DirEntry) bool {
|
||||
return !strings.HasSuffix(fi.Name(), ".html")
|
||||
})
|
||||
files = append(files, pageFiles...)
|
||||
HTMLTemplate, err = template.New("eiblog").Funcs(tools.TplFuncMap).ParseFiles(files...)
|
||||
if err != nil {
|
||||
logrus.Fatal("init html template: ", err)
|
||||
}
|
||||
|
||||
// 数据库初始化
|
||||
logrus.Info("store drivers: ", store.Drivers())
|
||||
Store, err = store.NewStore(config.Conf.Database)
|
||||
if err != nil {
|
||||
logrus.Fatal("init store: ", err)
|
||||
}
|
||||
|
||||
Ei, err = NewCache()
|
||||
if err != nil {
|
||||
logrus.Fatal("init blog cache: ", err)
|
||||
}
|
||||
|
||||
if config.Conf.ESHost != "" {
|
||||
ESClient, err = es.NewESClient(config.Conf.ESHost)
|
||||
if err != nil {
|
||||
logrus.Fatal("init es client: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
DisqusClient, err = disqus.NewDisqusClient(config.Conf.Host, config.Conf.Disqus)
|
||||
if err != nil {
|
||||
logrus.Fatal("init disqus client: ", err)
|
||||
}
|
||||
|
||||
QiniuClient, err = qiniu.NewQiniuClient(config.Conf.Qiniu)
|
||||
if err != nil {
|
||||
logrus.Fatal("init qiniu client: ", err)
|
||||
}
|
||||
|
||||
Pinger, err = pinger.NewPinger(config.Conf.Host, config.Conf.FeedRPC)
|
||||
if err != nil {
|
||||
logrus.Fatal("init pinger: ", err)
|
||||
}
|
||||
|
||||
// 启动定时器
|
||||
go startTimer()
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
// Package render provides ...
|
||||
package render
|
||||
package internal
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
@@ -37,7 +36,7 @@ const (
|
||||
|
||||
var (
|
||||
// 渲染markdown操作和截取摘要操作
|
||||
regIdentifier = regexp.MustCompile(config.Conf.EiBlogApp.General.Identifier)
|
||||
regIdentifier = regexp.MustCompile(config.Conf.General.Identifier)
|
||||
// header
|
||||
regHeader = regexp.MustCompile("</nav></div>")
|
||||
)
|
||||
@@ -50,11 +49,9 @@ func PageRender(md []byte) []byte {
|
||||
|
||||
// GenerateExcerptMarkdown 生成预览和描述
|
||||
func GenerateExcerptMarkdown(article *model.Article) {
|
||||
blogapp := config.Conf.EiBlogApp
|
||||
|
||||
if strings.HasPrefix(article.Content, blogapp.General.DescPrefix) {
|
||||
if strings.HasPrefix(article.Content, config.Conf.General.DescPrefix) {
|
||||
index := strings.Index(article.Content, "\r\n")
|
||||
prefix := article.Content[len(blogapp.General.DescPrefix):index]
|
||||
prefix := article.Content[len(config.Conf.General.DescPrefix):index]
|
||||
|
||||
article.Desc = tools.IgnoreHTMLTag(prefix)
|
||||
article.Content = article.Content[index:]
|
||||
@@ -77,7 +74,7 @@ func GenerateExcerptMarkdown(article *model.Article) {
|
||||
return
|
||||
}
|
||||
uc := []rune(article.Content)
|
||||
length := blogapp.General.Length
|
||||
length := config.Conf.General.Length
|
||||
if len(uc) < length {
|
||||
length = len(uc)
|
||||
}
|
||||
@@ -6,17 +6,18 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
pdb "github.com/eiblog/eiblog/pkg/connector/db"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
// example:
|
||||
// driver: mongodb
|
||||
// source: mongodb://localhost:27017
|
||||
// source: mongodb://localhost:27017/eiblog
|
||||
|
||||
const (
|
||||
mongoDBName = "eiblog"
|
||||
@@ -31,44 +32,39 @@ const (
|
||||
)
|
||||
|
||||
type mongodb struct {
|
||||
*mongo.Client
|
||||
*mongo.Database
|
||||
}
|
||||
|
||||
// Init init mongodb client
|
||||
func (db *mongodb) Init(name, source string) (Store, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
||||
defer cancel()
|
||||
|
||||
opts := options.Client().ApplyURI(source)
|
||||
client, err := mongo.Connect(ctx, opts)
|
||||
database, err := pdb.NewMDB(ctx, config.Conf.Database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = client.Ping(ctx, readpref.Primary())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.Client = client
|
||||
db.Database = database
|
||||
// create index
|
||||
indexModel := mongo.IndexModel{
|
||||
Keys: bson.D{bson.E{Key: "username", Value: 1}},
|
||||
Options: options.Index().SetUnique(true).SetSparse(true),
|
||||
}
|
||||
db.Database(mongoDBName).Collection(collectionAccount).
|
||||
db.Database.Collection(collectionAccount).
|
||||
Indexes().
|
||||
CreateOne(context.Background(), indexModel)
|
||||
indexModel = mongo.IndexModel{
|
||||
Keys: bson.D{bson.E{Key: "slug", Value: 1}},
|
||||
Options: options.Index().SetUnique(true).SetSparse(true),
|
||||
}
|
||||
db.Database(mongoDBName).Collection(collectionArticle).
|
||||
db.Database.Collection(collectionArticle).
|
||||
Indexes().
|
||||
CreateOne(context.Background(), indexModel)
|
||||
indexModel = mongo.IndexModel{
|
||||
Keys: bson.D{bson.E{Key: "slug", Value: 1}},
|
||||
Options: options.Index().SetUnique(true).SetSparse(true),
|
||||
}
|
||||
db.Database(mongoDBName).Collection(collectionSerie).
|
||||
db.Database.Collection(collectionSerie).
|
||||
Indexes().
|
||||
CreateOne(context.Background(), indexModel)
|
||||
return db, nil
|
||||
@@ -78,7 +74,7 @@ func (db *mongodb) Init(name, source string) (Store, error) {
|
||||
func (db *mongodb) LoadInsertBlogger(ctx context.Context,
|
||||
blogger *model.Blogger) (created bool, err error) {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionBlogger)
|
||||
collection := db.Database.Collection(collectionBlogger)
|
||||
|
||||
filter := bson.M{}
|
||||
result := collection.FindOne(ctx, filter)
|
||||
@@ -99,7 +95,7 @@ func (db *mongodb) LoadInsertBlogger(ctx context.Context,
|
||||
func (db *mongodb) UpdateBlogger(ctx context.Context,
|
||||
fields map[string]interface{}) error {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionBlogger)
|
||||
collection := db.Database.Collection(collectionBlogger)
|
||||
|
||||
filter := bson.M{}
|
||||
params := bson.M{}
|
||||
@@ -115,7 +111,7 @@ func (db *mongodb) UpdateBlogger(ctx context.Context,
|
||||
func (db *mongodb) LoadInsertAccount(ctx context.Context,
|
||||
acct *model.Account) (created bool, err error) {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionAccount)
|
||||
collection := db.Database.Collection(collectionAccount)
|
||||
|
||||
filter := bson.M{"username": acct.Username}
|
||||
result := collection.FindOne(ctx, filter)
|
||||
@@ -136,7 +132,7 @@ func (db *mongodb) LoadInsertAccount(ctx context.Context,
|
||||
func (db *mongodb) UpdateAccount(ctx context.Context, name string,
|
||||
fields map[string]interface{}) error {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionAccount)
|
||||
collection := db.Database.Collection(collectionAccount)
|
||||
|
||||
filter := bson.M{"username": name}
|
||||
params := bson.M{}
|
||||
@@ -150,7 +146,7 @@ func (db *mongodb) UpdateAccount(ctx context.Context, name string,
|
||||
|
||||
// InsertSerie 创建专题
|
||||
func (db *mongodb) InsertSerie(ctx context.Context, serie *model.Serie) error {
|
||||
collection := db.Database(mongoDBName).Collection(collectionSerie)
|
||||
collection := db.Database.Collection(collectionSerie)
|
||||
|
||||
serie.ID = db.nextValue(ctx, counterNameSerie)
|
||||
_, err := collection.InsertOne(ctx, serie)
|
||||
@@ -159,7 +155,7 @@ func (db *mongodb) InsertSerie(ctx context.Context, serie *model.Serie) error {
|
||||
|
||||
// RemoveSerie 删除专题
|
||||
func (db *mongodb) RemoveSerie(ctx context.Context, id int) error {
|
||||
collection := db.Database(mongoDBName).Collection(collectionSerie)
|
||||
collection := db.Database.Collection(collectionSerie)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
_, err := collection.DeleteOne(ctx, filter)
|
||||
@@ -170,7 +166,7 @@ func (db *mongodb) RemoveSerie(ctx context.Context, id int) error {
|
||||
func (db *mongodb) UpdateSerie(ctx context.Context, id int,
|
||||
fields map[string]interface{}) error {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionSerie)
|
||||
collection := db.Database.Collection(collectionSerie)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
params := bson.M{}
|
||||
@@ -184,7 +180,7 @@ func (db *mongodb) UpdateSerie(ctx context.Context, id int,
|
||||
|
||||
// LoadAllSerie 查询所有专题
|
||||
func (db *mongodb) LoadAllSerie(ctx context.Context) (model.SortedSeries, error) {
|
||||
collection := db.Database(mongoDBName).Collection(collectionSerie)
|
||||
collection := db.Database.Collection(collectionSerie)
|
||||
|
||||
opts := options.Find().SetSort(bson.M{"id": -1})
|
||||
filter := bson.M{}
|
||||
@@ -218,14 +214,14 @@ func (db *mongodb) InsertArticle(ctx context.Context, article *model.Article, st
|
||||
}
|
||||
}
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
collection := db.Database.Collection(collectionArticle)
|
||||
_, err := collection.InsertOne(ctx, article)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveArticle 硬删除文章
|
||||
func (db *mongodb) RemoveArticle(ctx context.Context, id int) error {
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
collection := db.Database.Collection(collectionArticle)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
_, err := collection.DeleteOne(ctx, filter)
|
||||
@@ -234,7 +230,7 @@ func (db *mongodb) RemoveArticle(ctx context.Context, id int) error {
|
||||
|
||||
// CleanArticles 清理回收站文章
|
||||
func (db *mongodb) CleanArticles(ctx context.Context, exp time.Time) error {
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
collection := db.Database.Collection(collectionArticle)
|
||||
|
||||
// 超过两天自动删除
|
||||
filter := bson.M{"deleted_at": bson.M{"$gt": time.Time{}, "$lt": exp}}
|
||||
@@ -246,7 +242,7 @@ func (db *mongodb) CleanArticles(ctx context.Context, exp time.Time) error {
|
||||
func (db *mongodb) UpdateArticle(ctx context.Context, id int,
|
||||
fields map[string]interface{}) error {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
collection := db.Database.Collection(collectionArticle)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
params := bson.M{}
|
||||
@@ -260,7 +256,7 @@ func (db *mongodb) UpdateArticle(ctx context.Context, id int,
|
||||
|
||||
// LoadArticle 查找文章
|
||||
func (db *mongodb) LoadArticle(ctx context.Context, id int) (*model.Article, error) {
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
collection := db.Database.Collection(collectionArticle)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
result := collection.FindOne(ctx, filter)
|
||||
@@ -276,7 +272,7 @@ func (db *mongodb) LoadArticle(ctx context.Context, id int) (*model.Article, err
|
||||
// LoadArticleList 获取文章列表
|
||||
func (db *mongodb) LoadArticleList(ctx context.Context, search SearchArticles) (
|
||||
model.SortedArticles, int, error) {
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
collection := db.Database.Collection(collectionArticle)
|
||||
|
||||
filter := bson.M{}
|
||||
for k, v := range search.Fields {
|
||||
@@ -334,7 +330,7 @@ type counter struct {
|
||||
|
||||
// nextValue counter value
|
||||
func (db *mongodb) nextValue(ctx context.Context, name string) int {
|
||||
collection := db.Database(mongoDBName).Collection(collectionCounter)
|
||||
collection := db.Database.Collection(collectionCounter)
|
||||
|
||||
opts := options.FindOneAndUpdate().SetUpsert(true).
|
||||
SetReturnDocument(options.After)
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
)
|
||||
|
||||
@@ -13,24 +14,27 @@ var (
|
||||
store Store
|
||||
acct *model.Account
|
||||
blogger *model.Blogger
|
||||
series *model.Series
|
||||
series *model.Serie
|
||||
article *model.Article
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
store, err = NewStore("mongodb", "mongodb://127.0.0.1:27017")
|
||||
store, err = NewStore(config.Database{
|
||||
Driver: "mongodb",
|
||||
Source: "mongodb://127.0.0.1:27017",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// account
|
||||
acct = &model.Account{
|
||||
Username: "deepzz",
|
||||
Password: "deepzz",
|
||||
Email: "deepzz@example.com",
|
||||
PhoneN: "12345678900",
|
||||
Address: "address",
|
||||
CreateTime: time.Now(),
|
||||
Username: "deepzz",
|
||||
Password: "deepzz",
|
||||
Email: "deepzz@example.com",
|
||||
PhoneN: "12345678900",
|
||||
Address: "address",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
// blogger
|
||||
blogger = &model.Blogger{
|
||||
@@ -41,11 +45,11 @@ func init() {
|
||||
Copyright: "Copyright",
|
||||
}
|
||||
// series
|
||||
series = &model.Series{
|
||||
Slug: "slug",
|
||||
Name: "series name",
|
||||
Desc: "series desc",
|
||||
CreateTime: time.Now(),
|
||||
series = &model.Serie{
|
||||
Slug: "slug",
|
||||
Name: "series name",
|
||||
Desc: "series desc",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
// article
|
||||
article = &model.Article{
|
||||
@@ -55,21 +59,20 @@ func init() {
|
||||
Count: 0,
|
||||
Content: "### count",
|
||||
SerieID: 0,
|
||||
Tags: "",
|
||||
Tags: nil,
|
||||
IsDraft: false,
|
||||
|
||||
UpdateTime: time.Now(),
|
||||
CreateTime: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInsertAccount(t *testing.T) {
|
||||
acct2, err := store.LoadInsertAccount(context.Background(), acct)
|
||||
ok, err := store.LoadInsertAccount(context.Background(), acct)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(acct2)
|
||||
t.Log(acct == acct2)
|
||||
t.Log(ok)
|
||||
}
|
||||
|
||||
func TestUpdateAccount(t *testing.T) {
|
||||
@@ -86,12 +89,11 @@ func TestUpdateAccount(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadInsertBlogger(t *testing.T) {
|
||||
blogger2, err := store.LoadInsertBlogger(context.Background(), blogger)
|
||||
ok, err := store.LoadInsertBlogger(context.Background(), blogger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(blogger2)
|
||||
t.Log(blogger == blogger2)
|
||||
t.Log(ok)
|
||||
}
|
||||
|
||||
func TestUpdateBlogger(t *testing.T) {
|
||||
@@ -104,21 +106,21 @@ func TestUpdateBlogger(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInsertSeries(t *testing.T) {
|
||||
err := store.InsertSeries(context.Background(), series)
|
||||
err := store.InsertSerie(context.Background(), series)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveSeries(t *testing.T) {
|
||||
err := store.RemoveSeries(context.Background(), 1)
|
||||
err := store.RemoveSerie(context.Background(), 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSeries(t *testing.T) {
|
||||
err := store.UpdateSeries(context.Background(), 2, map[string]interface{}{
|
||||
err := store.UpdateSerie(context.Background(), 2, map[string]interface{}{
|
||||
"desc": "update desc",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -127,7 +129,7 @@ func TestUpdateSeries(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadAllSeries(t *testing.T) {
|
||||
series, err := store.LoadAllSeries(context.Background())
|
||||
series, err := store.LoadAllSerie(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -136,7 +138,7 @@ func TestLoadAllSeries(t *testing.T) {
|
||||
|
||||
func TestInsertArticle(t *testing.T) {
|
||||
article.ID = 12
|
||||
err := store.InsertArticle(context.Background(), article)
|
||||
err := store.InsertArticle(context.Background(), article, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -150,14 +152,14 @@ func TestRemoveArticle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteArticle(t *testing.T) {
|
||||
err := store.DeleteArticle(context.Background(), 12)
|
||||
err := store.RemoveArticle(context.Background(), 12)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanArticles(t *testing.T) {
|
||||
err := store.CleanArticles(context.Background())
|
||||
err := store.CleanArticles(context.Background(), time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -173,33 +175,13 @@ func TestUpdateArticle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverArticle(t *testing.T) {
|
||||
err := store.RecoverArticle(context.Background(), 12)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAllArticle(t *testing.T) {
|
||||
articles, err := store.LoadAllArticle(context.Background())
|
||||
_, total, err := store.LoadArticleList(context.Background(), SearchArticles{
|
||||
Page: 1,
|
||||
Limit: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("load all articles: %d", len(articles))
|
||||
}
|
||||
|
||||
func TestLoadTrashArticles(t *testing.T) {
|
||||
articles, err := store.LoadTrashArticles(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("load trash articles: %d", len(articles))
|
||||
}
|
||||
|
||||
func TestLoadDraftArticles(t *testing.T) {
|
||||
articles, err := store.LoadDraftArticles(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("load draft articles: %d", len(articles))
|
||||
t.Logf("load all articles: %d", total)
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/driver/clickhouse"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
@@ -62,12 +64,15 @@ func (db *rdbms) Init(name, source string) (Store, error) {
|
||||
return nil, err
|
||||
}
|
||||
// auto migrate
|
||||
gormDB.AutoMigrate(
|
||||
err = gormDB.AutoMigrate(
|
||||
&model.Account{},
|
||||
&model.Blogger{},
|
||||
&model.Article{},
|
||||
&model.Serie{},
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Error("rdbms.AutoMigrate: ", err)
|
||||
}
|
||||
db.DB = gormDB
|
||||
return db, nil
|
||||
}
|
||||
@@ -129,7 +134,7 @@ func (db *rdbms) InsertArticle(ctx context.Context, article *model.Article, star
|
||||
if id < startID {
|
||||
id = startID
|
||||
} else {
|
||||
id += 1
|
||||
id++
|
||||
}
|
||||
article.ID = id
|
||||
}
|
||||
@@ -143,7 +148,7 @@ func (db *rdbms) RemoveArticle(ctx context.Context, id int) error {
|
||||
|
||||
// CleanArticles 清理回收站文章
|
||||
func (db *rdbms) CleanArticles(ctx context.Context, exp time.Time) error {
|
||||
return db.Where("deleted_at BETWEEN ? AND ?", time.Time{}, exp).Delete(model.Article{}).Error
|
||||
return db.Where("deleted_at > ? AND deleted_at < ?", time.Time{}, exp).Delete(model.Article{}).Error
|
||||
}
|
||||
|
||||
// UpdateArticle 更新文章
|
||||
@@ -190,6 +195,11 @@ func (db *rdbms) LoadArticleList(ctx context.Context, search SearchArticles) (mo
|
||||
return articles, int(count), err
|
||||
}
|
||||
|
||||
// DropDatabase drop eiblog database
|
||||
func (db *rdbms) DropDatabase(ctx context.Context) error {
|
||||
return errors.New("can not drop eiblog database in rdbms")
|
||||
}
|
||||
|
||||
// register store
|
||||
func init() {
|
||||
Register("mysql", &rdbms{})
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
)
|
||||
|
||||
@@ -99,13 +100,14 @@ func Drivers() []string {
|
||||
}
|
||||
|
||||
// NewStore 新建存储
|
||||
func NewStore(name string, source string) (Store, error) {
|
||||
func NewStore(conf config.Database) (Store, error) {
|
||||
storeMu.RLock()
|
||||
driver, ok := stores[name]
|
||||
driver, ok := stores[conf.Driver]
|
||||
storeMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("store: unknown driver %q (forgotten import?)", name)
|
||||
return nil, fmt.Errorf("store: unknown driver %q (forgotten import?)", conf.Driver)
|
||||
}
|
||||
|
||||
return driver.Init(name, source)
|
||||
return driver.Init(conf.Driver, conf.Source)
|
||||
}
|
||||
161
cmd/eiblog/handler/internal/timer.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func startTimer() {
|
||||
err := generateOpensearch()
|
||||
if err != nil {
|
||||
logrus.Error("startTimer.generateOpensearch: ", err)
|
||||
}
|
||||
err = generateRobots()
|
||||
if err != nil {
|
||||
logrus.Error("startTimer.generateRobots: ", err)
|
||||
}
|
||||
err = generateCrossdomain()
|
||||
if err != nil {
|
||||
logrus.Error("startTimer.generateCrossdomain: ", err)
|
||||
}
|
||||
// 定时刷新
|
||||
refreshFeedAndSitemap()
|
||||
}
|
||||
|
||||
// generateOpensearch 生成opensearch.xml
|
||||
func generateOpensearch() error {
|
||||
tpl := XMLTemplate.Lookup("opensearchTpl.xml")
|
||||
if tpl == nil {
|
||||
return errors.New("not found: opensearchTpl.xml")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"BTitle": Ei.Blogger.BTitle,
|
||||
"SubTitle": Ei.Blogger.SubTitle,
|
||||
"Host": config.Conf.Host,
|
||||
}
|
||||
path := filepath.Join(config.EtcDir, "assets", "opensearch.xml")
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return tpl.Execute(f, params)
|
||||
}
|
||||
|
||||
// generateRobots 生成robots.txt
|
||||
func generateRobots() error {
|
||||
tpl := XMLTemplate.Lookup("robotsTpl.xml")
|
||||
if tpl == nil {
|
||||
return errors.New("not found: robotsTpl.xml")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"Host": config.Conf.Host,
|
||||
}
|
||||
path := filepath.Join(config.EtcDir, "assets", "robots.txt")
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return tpl.Execute(f, params)
|
||||
}
|
||||
|
||||
// generateCrossdomain 生成crossdomain.xml
|
||||
func generateCrossdomain() error {
|
||||
tpl := XMLTemplate.Lookup("crossdomainTpl.xml")
|
||||
if tpl == nil {
|
||||
return errors.New("not found: crossdomainTpl.xml")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"Host": config.Conf.Host,
|
||||
}
|
||||
path := filepath.Join(config.EtcDir, "assets", "crossdomain.xml")
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return tpl.Execute(f, params)
|
||||
}
|
||||
|
||||
// refreshFeedAndSitemap 定时刷新feed和sitemap
|
||||
func refreshFeedAndSitemap() {
|
||||
defer time.AfterFunc(time.Hour*4, refreshFeedAndSitemap)
|
||||
|
||||
now := time.Now()
|
||||
// generate feed & sitemap
|
||||
err := generateFeed()
|
||||
if err != nil {
|
||||
logrus.Error("startTimer.generateFeed: ", err)
|
||||
}
|
||||
err = generateSitemap()
|
||||
if err != nil {
|
||||
logrus.Error("startTimer.generateSitemap: ", err)
|
||||
}
|
||||
// clean expired articles
|
||||
exp := now.Add(-48 * time.Hour)
|
||||
err = Store.CleanArticles(context.Background(), exp)
|
||||
if err != nil {
|
||||
logrus.Error("startTimer.CleanArticles: ", err)
|
||||
}
|
||||
// fetch disqus count
|
||||
err = DisqusClient.PostsCount(Ei.ArticlesMap)
|
||||
if err != nil {
|
||||
logrus.Error("startTimer.PostsCount: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// generateFeed 定时刷新feed
|
||||
func generateFeed() error {
|
||||
tpl := XMLTemplate.Lookup("feedTpl.xml")
|
||||
if tpl == nil {
|
||||
return errors.New("not found: feedTpl.xml")
|
||||
}
|
||||
|
||||
_, _, articles := Ei.PageArticleFE(1, 20)
|
||||
params := map[string]interface{}{
|
||||
"Title": Ei.Blogger.BTitle,
|
||||
"SubTitle": Ei.Blogger.SubTitle,
|
||||
"Host": config.Conf.Host,
|
||||
"FeedrURL": config.Conf.FeedRPC.FeedrURL,
|
||||
"BuildDate": time.Now().Format(time.RFC1123Z),
|
||||
"Articles": articles,
|
||||
}
|
||||
path := filepath.Join(config.EtcDir, "assets", "feed.xml")
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return tpl.Execute(f, params)
|
||||
}
|
||||
|
||||
// generateSitemap 定时刷新sitemap
|
||||
func generateSitemap() error {
|
||||
tpl := XMLTemplate.Lookup("sitemapTpl.xml")
|
||||
if tpl == nil {
|
||||
return errors.New("not found: sitemapTpl.xml")
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"Articles": Ei.Articles,
|
||||
"Host": config.Conf.Host,
|
||||
}
|
||||
path := filepath.Join(config.EtcDir, "assets", "sitemap.xml")
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return tpl.Execute(f, params)
|
||||
}
|
||||
@@ -1,29 +1,31 @@
|
||||
// Package page provides ...
|
||||
package page
|
||||
package pages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
htemplate "html/template"
|
||||
"html/template"
|
||||
"image/png"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache"
|
||||
"github.com/eiblog/eiblog/pkg/cache/store"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/core/eiblog"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/internal"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/internal/store"
|
||||
"github.com/eiblog/eiblog/pkg/middleware"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// baseBEParams 基础参数
|
||||
func baseBEParams(c *gin.Context) gin.H {
|
||||
func baseBEParams(_ *gin.Context) gin.H {
|
||||
return gin.H{
|
||||
"Author": cache.Ei.Account.Username,
|
||||
"Qiniu": config.Conf.EiBlogApp.Qiniu,
|
||||
"Author": internal.Ei.Account.Username,
|
||||
"Qiniu": config.Conf.Qiniu,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,22 +33,51 @@ func baseBEParams(c *gin.Context) gin.H {
|
||||
func handleLoginPage(c *gin.Context) {
|
||||
logout := c.Query("logout")
|
||||
if logout == "true" {
|
||||
eiblog.SetLogout(c)
|
||||
} else if eiblog.IsLogined(c) {
|
||||
middleware.SetLogout(c)
|
||||
} else if middleware.IsLogined(c) {
|
||||
c.Redirect(http.StatusFound, "/admin/profile")
|
||||
return
|
||||
}
|
||||
params := gin.H{"BTitle": cache.Ei.Blogger.BTitle}
|
||||
params := gin.H{
|
||||
"BTitle": internal.Ei.Blogger.BTitle,
|
||||
"TwoFactor": config.Conf.General.TwoFactor &&
|
||||
internal.Ei.Account.TwoFactorSecret != "",
|
||||
}
|
||||
renderHTMLAdminLayout(c, "login.html", params)
|
||||
}
|
||||
|
||||
// handleAdminProfile 个人配置
|
||||
func handleAdminProfile(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "个人配置 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "个人配置 | " + internal.Ei.Blogger.BTitle
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["Console"] = true
|
||||
params["Ei"] = cache.Ei
|
||||
params["Ei"] = internal.Ei
|
||||
if c.Query("unbind") == "true" {
|
||||
internal.Ei.Account.TwoFactorSecret = ""
|
||||
_ = internal.Store.UpdateAccount(context.Background(), internal.Ei.Account.Username,
|
||||
map[string]interface{}{
|
||||
"two_factor_secret": "",
|
||||
})
|
||||
}
|
||||
if config.Conf.General.TwoFactor &&
|
||||
internal.Ei.Account.TwoFactorSecret == "" {
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: config.Conf.Host,
|
||||
AccountName: internal.Ei.Account.Username,
|
||||
})
|
||||
internal.TwoFactorSecret = key.Secret()
|
||||
if err == nil {
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(200, 200)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
png.Encode(&buf, img)
|
||||
b64 := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
params["TwoFactorSecret"] = "data:image/png;base64," + b64
|
||||
}
|
||||
}
|
||||
renderHTMLAdminLayout(c, "admin-profile", params)
|
||||
}
|
||||
|
||||
@@ -61,20 +92,20 @@ func handleAdminPost(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
id, err := strconv.Atoi(c.Query("cid"))
|
||||
if err == nil && id > 0 {
|
||||
article, _ := cache.Ei.LoadArticle(context.Background(), id)
|
||||
article, _ := internal.Store.LoadArticle(context.Background(), id)
|
||||
if article != nil {
|
||||
params["Title"] = "编辑文章 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "编辑文章 | " + internal.Ei.Blogger.BTitle
|
||||
params["Edit"] = article
|
||||
}
|
||||
}
|
||||
if params["Title"] == nil {
|
||||
params["Title"] = "撰写文章 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "撰写文章 | " + internal.Ei.Blogger.BTitle
|
||||
}
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["Domain"] = config.Conf.EiBlogApp.Host
|
||||
params["Series"] = cache.Ei.Series
|
||||
params["Domain"] = config.Conf.Host
|
||||
params["Series"] = internal.Ei.Series
|
||||
var tags []T
|
||||
for tag := range cache.Ei.TagArticles {
|
||||
for tag := range internal.Ei.TagArticles {
|
||||
tags = append(tags, T{tag, tag})
|
||||
}
|
||||
str, _ := json.Marshal(tags)
|
||||
@@ -97,15 +128,15 @@ func handleAdminPosts(c *gin.Context) {
|
||||
vals := c.Request.URL.Query()
|
||||
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "文章管理 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "文章管理 | " + internal.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["Series"] = cache.Ei.Series
|
||||
params["Series"] = internal.Ei.Series
|
||||
params["Serie"] = se
|
||||
params["KW"] = kw
|
||||
var max int
|
||||
params["List"], max = cache.Ei.PageArticleBE(se, kw, false, false,
|
||||
pg, config.Conf.EiBlogApp.General.PageSize)
|
||||
params["List"], max = internal.Ei.PageArticleBE(se, kw, false, false,
|
||||
pg, config.Conf.General.PageSize)
|
||||
if pg < max {
|
||||
vals.Set("page", fmt.Sprint(pg+1))
|
||||
params["Next"] = vals.Encode()
|
||||
@@ -126,10 +157,10 @@ func handleAdminPosts(c *gin.Context) {
|
||||
// handleAdminSeries 专题列表
|
||||
func handleAdminSeries(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "专题管理 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "专题管理 | " + internal.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["List"] = cache.Ei.Series
|
||||
params["List"] = internal.Ei.Series
|
||||
renderHTMLAdminLayout(c, "admin-series", params)
|
||||
}
|
||||
|
||||
@@ -138,11 +169,11 @@ func handleAdminSerie(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
|
||||
id, err := strconv.Atoi(c.Query("mid"))
|
||||
params["Title"] = "新增专题 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "新增专题 | " + internal.Ei.Blogger.BTitle
|
||||
if err == nil && id > 0 {
|
||||
for _, v := range cache.Ei.Series {
|
||||
for _, v := range internal.Ei.Series {
|
||||
if v.ID == id {
|
||||
params["Title"] = "编辑专题 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "编辑专题 | " + internal.Ei.Blogger.BTitle
|
||||
params["Edit"] = v
|
||||
break
|
||||
}
|
||||
@@ -156,10 +187,10 @@ func handleAdminSerie(c *gin.Context) {
|
||||
// handleAdminTags 标签列表
|
||||
func handleAdminTags(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "标签管理 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "标签管理 | " + internal.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["List"] = cache.Ei.TagArticles
|
||||
params["List"] = internal.Ei.TagArticles
|
||||
renderHTMLAdminLayout(c, "admin-tags", params)
|
||||
}
|
||||
|
||||
@@ -170,7 +201,7 @@ func handleDraftDelete(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
err = cache.Ei.RemoveArticle(context.Background(), id)
|
||||
err = internal.Store.RemoveArticle(context.Background(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "删除错误"})
|
||||
return
|
||||
@@ -182,7 +213,7 @@ func handleDraftDelete(c *gin.Context) {
|
||||
func handleAdminDraft(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
|
||||
params["Title"] = "草稿箱 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "草稿箱 | " + internal.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
var err error
|
||||
@@ -191,7 +222,7 @@ func handleAdminDraft(c *gin.Context) {
|
||||
Limit: 9999,
|
||||
Fields: map[string]interface{}{store.SearchArticleDraft: true},
|
||||
}
|
||||
params["List"], _, err = cache.Ei.LoadArticleList(context.Background(), search)
|
||||
params["List"], _, err = internal.Store.LoadArticleList(context.Background(), search)
|
||||
if err != nil {
|
||||
logrus.Error("handleDraft.LoadDraftArticles: ", err)
|
||||
c.Status(http.StatusBadRequest)
|
||||
@@ -204,7 +235,7 @@ func handleAdminDraft(c *gin.Context) {
|
||||
// handleAdminTrash 回收箱页
|
||||
func handleAdminTrash(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "回收箱 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "回收箱 | " + internal.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
var err error
|
||||
@@ -213,7 +244,7 @@ func handleAdminTrash(c *gin.Context) {
|
||||
Limit: 9999,
|
||||
Fields: map[string]interface{}{store.SearchArticleTrash: true},
|
||||
}
|
||||
params["List"], _, err = cache.Ei.LoadArticleList(context.Background(), search)
|
||||
params["List"], _, err = internal.Store.LoadArticleList(context.Background(), search)
|
||||
if err != nil {
|
||||
logrus.Error("handleTrash.LoadArticleList: ", err)
|
||||
}
|
||||
@@ -223,7 +254,7 @@ func handleAdminTrash(c *gin.Context) {
|
||||
// handleAdminGeneral 基本设置
|
||||
func handleAdminGeneral(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "基本设置 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "基本设置 | " + internal.Ei.Blogger.BTitle
|
||||
params["Setting"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
renderHTMLAdminLayout(c, "admin-general", params)
|
||||
@@ -232,7 +263,7 @@ func handleAdminGeneral(c *gin.Context) {
|
||||
// handleAdminDiscussion 阅读设置
|
||||
func handleAdminDiscussion(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "阅读设置 | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = "阅读设置 | " + internal.Ei.Blogger.BTitle
|
||||
params["Setting"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
renderHTMLAdminLayout(c, "admin-discussion", params)
|
||||
@@ -243,19 +274,19 @@ func renderHTMLAdminLayout(c *gin.Context, name string, data gin.H) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
// special page
|
||||
if name == "login.html" {
|
||||
err := htmlTmpl.ExecuteTemplate(c.Writer, name, data)
|
||||
err := internal.HTMLTemplate.ExecuteTemplate(c.Writer, name, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
err := htmlTmpl.ExecuteTemplate(&buf, name, data)
|
||||
err := internal.HTMLTemplate.ExecuteTemplate(&buf, name, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
data["LayoutContent"] = htemplate.HTML(buf.String())
|
||||
err = htmlTmpl.ExecuteTemplate(c.Writer, "adminLayout.html", data)
|
||||
data["LayoutContent"] = template.HTML(buf.String())
|
||||
err = internal.HTMLTemplate.ExecuteTemplate(c.Writer, "adminLayout.html", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
// Package page provides ...
|
||||
package page
|
||||
package pages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
htemplate "html/template"
|
||||
"io/ioutil"
|
||||
"html/template"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/internal"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/internal"
|
||||
pconfig "github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/third/disqus"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -27,21 +28,22 @@ func baseFEParams(c *gin.Context) gin.H {
|
||||
|
||||
cookie, err := c.Request.Cookie("v")
|
||||
if err != nil || cookie.Value !=
|
||||
fmt.Sprint(config.Conf.EiBlogApp.StaticVersion) {
|
||||
version = config.Conf.EiBlogApp.StaticVersion
|
||||
fmt.Sprint(config.Conf.StaticVersion) {
|
||||
version = config.Conf.StaticVersion
|
||||
}
|
||||
return gin.H{
|
||||
"BlogName": cache.Ei.Blogger.BlogName,
|
||||
"SubTitle": cache.Ei.Blogger.SubTitle,
|
||||
"BTitle": cache.Ei.Blogger.BTitle,
|
||||
"BeiAn": cache.Ei.Blogger.BeiAn,
|
||||
"Domain": config.Conf.EiBlogApp.Host,
|
||||
"BlogName": internal.Ei.Blogger.BlogName,
|
||||
"SubTitle": internal.Ei.Blogger.SubTitle,
|
||||
"BTitle": internal.Ei.Blogger.BTitle,
|
||||
"BeiAn": internal.Ei.Blogger.BeiAn,
|
||||
"Domain": config.Conf.Host,
|
||||
"CopyYear": time.Now().Year(),
|
||||
"Twitter": config.Conf.EiBlogApp.Twitter,
|
||||
"Qiniu": config.Conf.EiBlogApp.Qiniu,
|
||||
"Disqus": config.Conf.EiBlogApp.Disqus,
|
||||
"AdSense": config.Conf.EiBlogApp.Google.AdSense,
|
||||
"Twitter": config.Conf.Twitter,
|
||||
"Qiniu": config.Conf.Qiniu,
|
||||
"Disqus": config.Conf.Disqus,
|
||||
"AdSense": config.Conf.Google.AdSense,
|
||||
"Version": version,
|
||||
"Pages": config.Conf.Pages,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,16 +60,16 @@ func handleNotFound(c *gin.Context) {
|
||||
// handleHomePage 首页
|
||||
func handleHomePage(c *gin.Context) {
|
||||
params := baseFEParams(c)
|
||||
params["Title"] = cache.Ei.Blogger.BTitle + " | " + cache.Ei.Blogger.SubTitle
|
||||
params["Description"] = "博客首页," + cache.Ei.Blogger.SubTitle
|
||||
params["Title"] = internal.Ei.Blogger.BTitle + " | " + internal.Ei.Blogger.SubTitle
|
||||
params["Description"] = "博客首页," + internal.Ei.Blogger.SubTitle
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["CurrentPage"] = "blog-home"
|
||||
pn, err := strconv.Atoi(c.Query("pn"))
|
||||
if err != nil || pn < 1 {
|
||||
pn = 1
|
||||
}
|
||||
params["Prev"], params["Next"], params["List"] = cache.Ei.PageArticleFE(pn,
|
||||
config.Conf.EiBlogApp.General.PageNum)
|
||||
params["Prev"], params["Next"], params["List"] = internal.Ei.PageArticleFE(pn,
|
||||
config.Conf.General.PageNum)
|
||||
|
||||
renderHTMLHomeLayout(c, "home", params)
|
||||
}
|
||||
@@ -75,13 +77,13 @@ func handleHomePage(c *gin.Context) {
|
||||
// handleArticlePage 文章页
|
||||
func handleArticlePage(c *gin.Context) {
|
||||
slug := c.Param("slug")
|
||||
if !strings.HasSuffix(slug, ".html") || cache.Ei.ArticlesMap[slug[:len(slug)-5]] == nil {
|
||||
if !strings.HasSuffix(slug, ".html") || internal.Ei.ArticlesMap[slug[:len(slug)-5]] == nil {
|
||||
handleNotFound(c)
|
||||
return
|
||||
}
|
||||
article := cache.Ei.ArticlesMap[slug[:len(slug)-5]]
|
||||
article := internal.Ei.ArticlesMap[slug[:len(slug)-5]]
|
||||
params := baseFEParams(c)
|
||||
params["Title"] = article.Title + " | " + cache.Ei.Blogger.BTitle
|
||||
params["Title"] = article.Title + " | " + internal.Ei.Blogger.BTitle
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["CurrentPage"] = "post-" + article.Slug
|
||||
params["Article"] = article
|
||||
@@ -90,21 +92,21 @@ func handleArticlePage(c *gin.Context) {
|
||||
switch slug {
|
||||
case "blogroll.html":
|
||||
name = "blogroll"
|
||||
params["Description"] = "友情连接," + cache.Ei.Blogger.SubTitle
|
||||
params["Description"] = "友情连接," + internal.Ei.Blogger.SubTitle
|
||||
case "about.html":
|
||||
name = "about"
|
||||
params["Description"] = "关于作者," + cache.Ei.Blogger.SubTitle
|
||||
params["Description"] = "关于作者," + internal.Ei.Blogger.SubTitle
|
||||
default:
|
||||
params["Description"] = article.Desc + "," + cache.Ei.Blogger.SubTitle
|
||||
params["Description"] = article.Desc + "," + internal.Ei.Blogger.SubTitle
|
||||
name = "article"
|
||||
params["Copyright"] = cache.Ei.Blogger.Copyright
|
||||
params["Copyright"] = internal.Ei.Blogger.Copyright
|
||||
if !article.UpdatedAt.IsZero() {
|
||||
params["Days"] = int(time.Now().Sub(article.UpdatedAt).Hours()) / 24
|
||||
params["Days"] = int(time.Since(article.UpdatedAt).Hours()) / 24
|
||||
} else {
|
||||
params["Days"] = int(time.Now().Sub(article.CreatedAt).Hours()) / 24
|
||||
params["Days"] = int(time.Since(article.CreatedAt).Hours()) / 24
|
||||
}
|
||||
if article.SerieID > 0 {
|
||||
for _, series := range cache.Ei.Series {
|
||||
for _, series := range internal.Ei.Series {
|
||||
if series.ID == article.SerieID {
|
||||
params["Serie"] = series
|
||||
}
|
||||
@@ -117,65 +119,65 @@ func handleArticlePage(c *gin.Context) {
|
||||
// handleSeriesPage 专题页
|
||||
func handleSeriesPage(c *gin.Context) {
|
||||
params := baseFEParams(c)
|
||||
params["Title"] = "专题 | " + cache.Ei.Blogger.BTitle
|
||||
params["Description"] = "专题列表," + cache.Ei.Blogger.SubTitle
|
||||
params["Title"] = "专题 | " + internal.Ei.Blogger.BTitle
|
||||
params["Description"] = "专题列表," + internal.Ei.Blogger.SubTitle
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["CurrentPage"] = "series"
|
||||
params["Article"] = cache.Ei.PageSeries
|
||||
params["Article"] = internal.Ei.PageSeries
|
||||
renderHTMLHomeLayout(c, "series", params)
|
||||
}
|
||||
|
||||
// handleArchivePage 归档页
|
||||
func handleArchivePage(c *gin.Context) {
|
||||
params := baseFEParams(c)
|
||||
params["Title"] = "归档 | " + cache.Ei.Blogger.BTitle
|
||||
params["Description"] = "博客归档," + cache.Ei.Blogger.SubTitle
|
||||
params["Title"] = "归档 | " + internal.Ei.Blogger.BTitle
|
||||
params["Description"] = "博客归档," + internal.Ei.Blogger.SubTitle
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["CurrentPage"] = "archives"
|
||||
params["Article"] = cache.Ei.PageArchives
|
||||
params["Article"] = internal.Ei.PageArchives
|
||||
renderHTMLHomeLayout(c, "archives", params)
|
||||
}
|
||||
|
||||
// handleSearchPage 搜索页
|
||||
func handleSearchPage(c *gin.Context) {
|
||||
params := baseFEParams(c)
|
||||
params["Title"] = "站内搜索 | " + cache.Ei.Blogger.BTitle
|
||||
params["Description"] = "站内搜索," + cache.Ei.Blogger.SubTitle
|
||||
params["Title"] = "站内搜索 | " + internal.Ei.Blogger.BTitle
|
||||
params["Description"] = "站内搜索," + internal.Ei.Blogger.SubTitle
|
||||
params["Path"] = ""
|
||||
params["CurrentPage"] = "search-post"
|
||||
|
||||
q := strings.TrimSpace(c.Query("q"))
|
||||
if q != "" {
|
||||
params["Word"] = q
|
||||
|
||||
if q != "" && internal.ESClient != nil {
|
||||
start, err := strconv.Atoi(c.Query("start"))
|
||||
if start < 1 || err != nil {
|
||||
start = 1
|
||||
}
|
||||
params["Word"] = q
|
||||
|
||||
vals := c.Request.URL.Query()
|
||||
result, err := internal.ElasticSearch(q, config.Conf.EiBlogApp.General.PageNum, start-1)
|
||||
result, err := internal.ESClient.ElasticSearch(q, config.Conf.General.PageNum, start-1)
|
||||
if err != nil {
|
||||
logrus.Error("HandleSearchPage.ElasticSearch: ", err)
|
||||
} else {
|
||||
result.Took /= 1000
|
||||
for i, v := range result.Hits.Hits {
|
||||
article := cache.Ei.ArticlesMap[v.Source.Slug]
|
||||
article := internal.Ei.ArticlesMap[v.Source.Slug]
|
||||
if len(v.Highlight.Content) == 0 && article != nil {
|
||||
result.Hits.Hits[i].Highlight.Content = []string{article.Excerpt}
|
||||
}
|
||||
}
|
||||
params["SearchResult"] = result
|
||||
if num := start - config.Conf.EiBlogApp.General.PageNum; num > 0 {
|
||||
if num := start - config.Conf.General.PageNum; num > 0 {
|
||||
vals.Set("start", fmt.Sprint(num))
|
||||
params["Prev"] = vals.Encode()
|
||||
}
|
||||
if num := start + config.Conf.EiBlogApp.General.PageNum; result.Hits.Total >= num {
|
||||
if num := start + config.Conf.General.PageNum; result.Hits.Total >= num {
|
||||
vals.Set("start", fmt.Sprint(num))
|
||||
params["Next"] = vals.Encode()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
params["HotWords"] = config.Conf.EiBlogApp.HotWords
|
||||
params["HotWords"] = config.Conf.HotWords
|
||||
}
|
||||
renderHTMLHomeLayout(c, "search", params)
|
||||
}
|
||||
@@ -199,11 +201,11 @@ func handleDisqusList(c *gin.Context) {
|
||||
|
||||
slug := c.Param("slug")
|
||||
cursor := c.Query("cursor")
|
||||
artc := cache.Ei.ArticlesMap[slug]
|
||||
artc := internal.Ei.ArticlesMap[slug]
|
||||
if artc != nil {
|
||||
dcs.Data.Thread = artc.Thread
|
||||
}
|
||||
postsList, err := internal.PostsList(slug, cursor)
|
||||
postsList, err := internal.DisqusClient.PostsList(artc, cursor)
|
||||
if err != nil {
|
||||
logrus.Error("hadnleDisqusList.PostsList: ", err)
|
||||
dcs.ErrNo = 0
|
||||
@@ -235,10 +237,10 @@ func handleDisqusList(c *gin.Context) {
|
||||
if artc != nil && artc.Thread == "" {
|
||||
if dcs.Data.Thread != "" {
|
||||
artc.Thread = dcs.Data.Thread
|
||||
} else if internal.ThreadDetails(artc) == nil {
|
||||
} else if internal.DisqusClient.ThreadDetails(artc) == nil {
|
||||
dcs.Data.Thread = artc.Thread
|
||||
}
|
||||
cache.Ei.UpdateArticle(context.Background(), artc.ID,
|
||||
internal.Store.UpdateArticle(context.Background(), artc.ID,
|
||||
map[string]interface{}{
|
||||
"thread": artc.Thread,
|
||||
})
|
||||
@@ -252,9 +254,9 @@ func handleDisqusPage(c *gin.Context) {
|
||||
c.String(http.StatusOK, "出错啦。。。")
|
||||
return
|
||||
}
|
||||
article := cache.Ei.ArticlesMap[array[0]]
|
||||
article := internal.Ei.ArticlesMap[array[0]]
|
||||
params := gin.H{
|
||||
"Title": "发表评论 | " + cache.Ei.Blogger.BTitle,
|
||||
"Title": "发表评论 | " + internal.Ei.Blogger.BTitle,
|
||||
"ATitle": article.Title,
|
||||
"Thread": array[1],
|
||||
"Slug": article.Slug,
|
||||
@@ -299,7 +301,7 @@ func handleDisqusCreate(c *gin.Context) {
|
||||
}
|
||||
logrus.Infof("email: %s comments: %s", email, thread)
|
||||
|
||||
comment := internal.PostComment{
|
||||
comment := disqus.PostComment{
|
||||
Message: msg,
|
||||
Parent: c.PostForm("parent"),
|
||||
Thread: thread,
|
||||
@@ -308,14 +310,14 @@ func handleDisqusCreate(c *gin.Context) {
|
||||
Identifier: identifier,
|
||||
IPAddress: c.ClientIP(),
|
||||
}
|
||||
postDetail, err := internal.PostCreate(&comment)
|
||||
postDetail, err := internal.DisqusClient.PostCreate(&comment)
|
||||
if err != nil {
|
||||
logrus.Error("handleDisqusCreate.PostCreate: ", err)
|
||||
resp.ErrNo = 1
|
||||
resp.ErrMsg = "提交评论失败,请重试"
|
||||
return
|
||||
}
|
||||
err = internal.PostApprove(postDetail.Response.ID)
|
||||
err = internal.DisqusClient.PostApprove(postDetail.Response.ID)
|
||||
if err != nil {
|
||||
logrus.Error("handleDisqusCreate.PostApprove: ", err)
|
||||
resp.ErrNo = 1
|
||||
@@ -335,34 +337,41 @@ func handleDisqusCreate(c *gin.Context) {
|
||||
}
|
||||
|
||||
// handleBeaconPage 服务端推送谷歌统计
|
||||
// https://www.thyngster.com/ga4-measurement-protocol-cheatsheet/
|
||||
func handleBeaconPage(c *gin.Context) {
|
||||
ua := c.Request.UserAgent()
|
||||
|
||||
vals := c.Request.URL.Query()
|
||||
vals.Set("v", config.Conf.EiBlogApp.Google.V)
|
||||
vals.Set("tid", config.Conf.EiBlogApp.Google.Tid)
|
||||
vals.Set("t", config.Conf.EiBlogApp.Google.T)
|
||||
vals.Set("v", config.Conf.Google.V)
|
||||
vals.Set("tid", config.Conf.Google.Tid)
|
||||
cookie, _ := c.Cookie("u")
|
||||
vals.Set("cid", cookie)
|
||||
|
||||
vals.Set("dl", c.Request.Referer())
|
||||
vals.Set("uip", c.ClientIP())
|
||||
vals.Set("dl", c.Request.Referer()) // document location
|
||||
vals.Set("en", "page_view") // event name
|
||||
vals.Set("sct", "1") // Session Count
|
||||
vals.Set("seg", "1") // Session Engagment
|
||||
vals.Set("_uip", c.ClientIP()) // user ip
|
||||
vals.Set("_p", fmt.Sprint(201226219+rand.Intn(499999999))) // random page load hash
|
||||
vals.Set("_ee", "1") // external event
|
||||
go func() {
|
||||
req, err := http.NewRequest("POST", config.Conf.EiBlogApp.Google.URL,
|
||||
strings.NewReader(vals.Encode()))
|
||||
url := config.Conf.Google.URL + "?" + vals.Encode()
|
||||
req, err := http.NewRequest("POST", url, nil)
|
||||
if err != nil {
|
||||
logrus.Error("HandleBeaconPage.NewRequest: ", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-Agent", ua)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Sec-Ch-Ua", c.GetHeader("Sec-Ch-Ua"))
|
||||
req.Header.Set("Sec-Ch-Ua-Platform", c.GetHeader("Sec-Ch-Ua-Platform"))
|
||||
req.Header.Set("Sec-Ch-Ua-Mobile", c.GetHeader("Sec-Ch-Ua-Mobile"))
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
logrus.Error("HandleBeaconPage.Do: ", err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logrus.Error("HandleBeaconPage.ReadAll: ", err)
|
||||
return
|
||||
@@ -374,24 +383,61 @@ func handleBeaconPage(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// handleCustomPage 自定义页面
|
||||
func handleCustomPage(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
if !strings.HasSuffix(path, ".html") {
|
||||
handleNotFound(c)
|
||||
return
|
||||
}
|
||||
// find config
|
||||
var page *pconfig.CustomPage
|
||||
for _, p := range config.Conf.Pages {
|
||||
if p.Path == path {
|
||||
page = &p
|
||||
break
|
||||
}
|
||||
}
|
||||
if page == nil {
|
||||
handleNotFound(c)
|
||||
return
|
||||
}
|
||||
var params gin.H
|
||||
if page.IsEmbed {
|
||||
params = baseFEParams(c)
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["Title"] = page.Name + " | " + internal.Ei.Blogger.SubTitle
|
||||
}
|
||||
// serve custom page
|
||||
name := c.Param("path")
|
||||
renderHTMLHomeLayout(c, name, params)
|
||||
}
|
||||
|
||||
// renderHTMLHomeLayout homelayout html
|
||||
func renderHTMLHomeLayout(c *gin.Context, name string, data gin.H) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
// special page
|
||||
if name == "disqus.html" {
|
||||
err := htmlTmpl.ExecuteTemplate(c.Writer, name, data)
|
||||
switch {
|
||||
case name == "disqus.html":
|
||||
err := internal.HTMLTemplate.ExecuteTemplate(c.Writer, name, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
case data == nil:
|
||||
err := internal.HTMLTemplate.ExecuteTemplate(c.Writer, name, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
err := htmlTmpl.ExecuteTemplate(&buf, name, data)
|
||||
err := internal.HTMLTemplate.ExecuteTemplate(&buf, name, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
data["LayoutContent"] = htemplate.HTML(buf.String())
|
||||
err = htmlTmpl.ExecuteTemplate(c.Writer, "homeLayout.html", data)
|
||||
data["LayoutContent"] = template.HTML(buf.String())
|
||||
err = internal.HTMLTemplate.ExecuteTemplate(c.Writer, "homeLayout.html", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -1,38 +1,12 @@
|
||||
// Package page provides ...
|
||||
package page
|
||||
package pages
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// htmlTmpl html template cache
|
||||
var htmlTmpl *template.Template
|
||||
|
||||
func init() {
|
||||
htmlTmpl = template.New("eiblog").Funcs(tools.TplFuncMap)
|
||||
root := filepath.Join(config.WorkDir, "website")
|
||||
files := tools.ReadDirFiles(root, func(fi fs.FileInfo) bool {
|
||||
name := fi.Name()
|
||||
if name == ".DS_Store" {
|
||||
return true
|
||||
}
|
||||
// should not read template dir
|
||||
if fi.IsDir() && name == "template" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
_, err := htmlTmpl.ParseFiles(files...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// RegisterRoutesCustomPages 注册自定义页面
|
||||
func RegisterRoutesCustomPages(e *gin.Engine) {
|
||||
e.GET("/page/:path", handleCustomPage)
|
||||
}
|
||||
|
||||
// RegisterRoutes register routes
|
||||
@@ -1,12 +1,11 @@
|
||||
// Package swag provides ...
|
||||
package swag
|
||||
|
||||
import (
|
||||
_ "github.com/eiblog/eiblog/pkg/core/eiblog/docs" // docs
|
||||
_ "github.com/eiblog/eiblog/cmd/eiblog/docs" // docs
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/swaggo/gin-swagger/swaggerFiles"
|
||||
)
|
||||
|
||||
// RegisterRoutes register routes
|
||||
@@ -2,71 +2,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/core/eiblog"
|
||||
"github.com/eiblog/eiblog/pkg/core/eiblog/admin"
|
||||
"github.com/eiblog/eiblog/pkg/core/eiblog/file"
|
||||
"github.com/eiblog/eiblog/pkg/core/eiblog/page"
|
||||
"github.com/eiblog/eiblog/pkg/core/eiblog/swag"
|
||||
"github.com/eiblog/eiblog/pkg/mid"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/config"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/admin"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/file"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/pages"
|
||||
"github.com/eiblog/eiblog/cmd/eiblog/handler/swag"
|
||||
"github.com/eiblog/eiblog/pkg/middleware"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hi, it's App " + config.Conf.EiBlogApp.Name)
|
||||
logrus.Info("Hi, it's App " + config.Conf.Name)
|
||||
|
||||
endRun := make(chan error, 1)
|
||||
|
||||
runHTTPServer(endRun)
|
||||
fmt.Println(<-endRun)
|
||||
logrus.Fatal(<-endRun)
|
||||
}
|
||||
|
||||
func runHTTPServer(endRun chan error) {
|
||||
if !config.Conf.EiBlogApp.EnableHTTP {
|
||||
return
|
||||
}
|
||||
|
||||
if config.Conf.RunMode == config.ModeProd {
|
||||
if config.Conf.RunMode.IsReleaseMode() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
e := gin.Default()
|
||||
// middleware
|
||||
e.Use(mid.UserMiddleware())
|
||||
e.Use(mid.SessionMiddleware(mid.SessionOpts{
|
||||
Name: "su",
|
||||
Secure: config.Conf.RunMode == config.ModeProd,
|
||||
Secret: []byte("ZGlzvcmUoMTAsICI="),
|
||||
}))
|
||||
e.Use(middleware.UserMiddleware())
|
||||
e.Use(middleware.SessionMiddleware(
|
||||
middleware.SessionOpts{
|
||||
Name: "su",
|
||||
Secure: config.Conf.RunMode.IsReleaseMode(),
|
||||
Secret: tools.CryptoRand(16),
|
||||
}))
|
||||
|
||||
// swag
|
||||
swag.RegisterRoutes(e)
|
||||
|
||||
// static files, page
|
||||
root := filepath.Join(config.WorkDir, "assets")
|
||||
e.Static("/static", root)
|
||||
e.Static("/static", filepath.Join(config.EtcDir, "assets"))
|
||||
|
||||
// custom pages
|
||||
pages.RegisterRoutesCustomPages(e)
|
||||
|
||||
// static files
|
||||
file.RegisterRoutes(e)
|
||||
// frontend pages
|
||||
page.RegisterRoutes(e)
|
||||
pages.RegisterRoutes(e)
|
||||
// unauthz api
|
||||
admin.RegisterRoutes(e)
|
||||
|
||||
// admin router
|
||||
group := e.Group("/admin", eiblog.AuthFilter)
|
||||
group := e.Group("/admin", middleware.AuthFilter)
|
||||
{
|
||||
page.RegisterRoutesAuthz(group)
|
||||
pages.RegisterRoutesAuthz(group)
|
||||
admin.RegisterRoutesAuthz(group)
|
||||
}
|
||||
|
||||
// start
|
||||
address := fmt.Sprintf(":%d", config.Conf.EiBlogApp.HTTPPort)
|
||||
go func() {
|
||||
endRun <- e.Run(address)
|
||||
endRun <- e.Run(config.Conf.Listen)
|
||||
}()
|
||||
fmt.Println("HTTP server running on: " + address)
|
||||
logrus.Info("HTTP server running on: " + config.Conf.Listen)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
version: '3'
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:3.2
|
||||
image: mongo:3.6
|
||||
volumes:
|
||||
- ${PWD}/mgodb:/data/db
|
||||
restart: always
|
||||
@@ -10,10 +9,11 @@ services:
|
||||
volumes:
|
||||
- ${PWD}/esdata:/usr/share/elasticsearch/data
|
||||
restart: always
|
||||
|
||||
eiblog:
|
||||
image: deepzz0/eiblog:latest
|
||||
volumes:
|
||||
- ${PWD}/conf:/app/conf
|
||||
- ${PWD}/eiblog/etc/app.yml:/app/etc/app.yml
|
||||
extra_hosts:
|
||||
- "disqus.com:151.101.192.134"
|
||||
- "deepzz.disqus.com:151.101.192.134"
|
||||
@@ -25,10 +25,14 @@ services:
|
||||
ports:
|
||||
- 127.0.0.1:9000:9000
|
||||
restart: always
|
||||
|
||||
backup:
|
||||
image: deepzz0/backup:latest
|
||||
#command: ./backend --restore true
|
||||
volumes:
|
||||
- ${PWD}/conf:/app/conf
|
||||
- ${PWD}/backup/etc/app.yml:/app/etc/app.yml
|
||||
links:
|
||||
- mongodb
|
||||
environment:
|
||||
- RUN_MODE=prod
|
||||
restart: always
|
||||
@@ -1,3 +0,0 @@
|
||||
Configuration file templates or default configs.
|
||||
|
||||
Put your confd or consul-template template files here.
|
||||
67
conf/app.yml
@@ -1,67 +0,0 @@
|
||||
appname: eiblog
|
||||
database:
|
||||
driver: sqlite
|
||||
source: ./db.sqlite
|
||||
eshost:
|
||||
eiblogapp:
|
||||
mode:
|
||||
name: cmd-eiblog
|
||||
enablehttp: true
|
||||
httpport: 9000
|
||||
host: example.com
|
||||
staticversion: 1 # 静态文件版本
|
||||
hotwords: # 热搜词
|
||||
- docker
|
||||
- mongodb
|
||||
- curl
|
||||
- dns
|
||||
general: # 常规配置
|
||||
pagenum: 10 # 首页展示文章数量
|
||||
pagesize: 20 # 管理界面
|
||||
startid: 11 # 起始ID,预留id不时之需, 不用管
|
||||
descprefix: "Desc:" # 文章描述前缀
|
||||
identifier: <!--more--> # 截取预览标识
|
||||
length: 400 # 自动截取预览, 字符数
|
||||
timezone: Asia/Shanghai # 时区
|
||||
disqus: # 评论相关
|
||||
shortname: xxxxxx
|
||||
publickey: wdSgxRm9rdGAlLKFcFdToBe3GT4SibmV7Y8EjJQ0r4GWXeKtxpopMAeIeoI2dTEg
|
||||
accesstoken: 50023908f39f4607957e909b495326af
|
||||
google: # 谷歌分析
|
||||
url: https://www.google-analytics.com/collect
|
||||
tid: UA-xxxxxx-1
|
||||
v: "1"
|
||||
t: pageview
|
||||
adsense: <script data-ad-client="ca-pub-5384494508691406" async src=""></script>
|
||||
qiniu: # 七牛OSS
|
||||
bucket: eiblog
|
||||
domain: st.deepzz.com
|
||||
accesskey: MB6AXl_Sj_mmFsL-Lt59Dml2Vmy2o8XMmiCbbSeC
|
||||
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf
|
||||
twitter: # twitter card
|
||||
card: summary
|
||||
site: deepzz02
|
||||
image: st.deepzz.com/static/img/avatar.jpg
|
||||
address: twitter.com/deepzz02
|
||||
feedrpc: # rss ping
|
||||
feedrurl: https://deepzz.superfeedr.com/
|
||||
pingrpc:
|
||||
- http://ping.baidu.com/ping/RPC2
|
||||
- http://rpc.pingomatic.com/
|
||||
# 数据初始化操作,可到博客后台修改
|
||||
account:
|
||||
username: deepzz # *后台登录用户名
|
||||
password: deepzz # *登录明文密码
|
||||
backupapp:
|
||||
mode:
|
||||
name: cmd-backup
|
||||
enablehttp: true
|
||||
httpport: 9001
|
||||
backupto: qiniu # 备份到七牛云
|
||||
interval: 7d # 多久备份一次
|
||||
validity: 60 # 保存时长days
|
||||
qiniu: # 七牛OSS
|
||||
bucket: backup
|
||||
domain: st.deepzz.com
|
||||
accesskey: MB6AXl_Sj_mmFsL-Lt59Dml2Vmy2o8XMmiCbbSeC
|
||||
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf
|
||||
@@ -7,19 +7,20 @@ EiBlog 镜像仓库地址:https://hub.docker.com/u/deepzz0,备份镜像为
|
||||
目前仅支持同步 mongodb 数据到七牛云,参考 `app.yml`:
|
||||
|
||||
```
|
||||
backupapp:
|
||||
mode:
|
||||
name: cmd-backup
|
||||
enablehttp: true
|
||||
httpport: 9001
|
||||
backupto: qiniu # 备份到七牛云
|
||||
interval: 7d # 多久备份一次
|
||||
validity: 60 # 保存时长days
|
||||
qiniu: # 七牛OSS
|
||||
bucket: backup
|
||||
domain: st.deepzz.com
|
||||
accesskey: MB6AXl_Sj_mmFsL-Lt59Dml2Vmy2o8XMmiCbbSeC
|
||||
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf
|
||||
apimode:
|
||||
name: cmd-backup
|
||||
listen: 0.0.0.0:9000
|
||||
database: # 数据库配置
|
||||
driver: mongodb
|
||||
source: mongodb://localhost:27017/eiblog
|
||||
backupto: qiniu # 备份到, default: qiniu
|
||||
interval: 7d # 备份周期, default: 7d
|
||||
validity: 60 # 备份保留时间, default: 60
|
||||
qiniu: # 七牛OSS
|
||||
bucket: eiblog
|
||||
domain: st.deepzz.cn
|
||||
accesskey: MB6AXl_Sj_mmFsL-Lt59Dml2Vmy2o8XMmiCbbSeC
|
||||
secretkey: BIrMy0fsZ0_SHNceNXk3eDuo7WmVYzj2-zrmd5Tf
|
||||
```
|
||||
|
||||
|
||||
@@ -36,7 +37,7 @@ $ docker pull deepzz0/backup
|
||||
|
||||
```
|
||||
$ docker run --name backup \
|
||||
-v ${PWD}/conf:/app/conf
|
||||
-v ${PWD}/etc/app.yml:/app/etc/app.yml
|
||||
```
|
||||
|
||||
Docker-compose 请参考项目根目录下的 `docker-compose.yml` 文件。
|
||||
|
||||