├── .github ├── assets.jpg ├── dependabot.yml ├── dockerhub.jpg └── workflows │ ├── codecov.yml │ ├── codeql.yml │ └── release.yaml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── config.json ├── docker-compose.yml ├── docker ├── Dockerfile └── Dockerfile.goreleaser ├── docs └── README.md ├── go.mod ├── go.sum ├── internal ├── config │ ├── config.go │ ├── config_test.go │ └── define.go ├── fn │ ├── fn.go │ └── fn_test.go ├── network │ └── network.go ├── server │ ├── internal.go │ ├── public.go │ └── server.go └── version │ └── version.go └── main.go /.github/assets.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulteary/docker-quick-docs/92f45bdb6ec7eaf7360ed6cee01597bb1d4bbd0f/.github/assets.jpg -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/dockerhub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulteary/docker-quick-docs/92f45bdb6ec7eaf7360ed6cee01597bb1d4bbd0f/.github/dockerhub.jpg -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | 7 | env: 8 | GO_VERSION: "1.22.2" 9 | GO111MODULE: on 10 | 11 | jobs: 12 | CodeCov: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ env.GO_VERSION }} 25 | 26 | - name: Cache Go modules 27 | uses: actions/cache@v4 28 | with: 29 | path: ~/go/pkg/mod 30 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 31 | restore-keys: | 32 | ${{ runner.os }}-go- 33 | 34 | - name: Run coverage 35 | run: go test ./... -coverprofile=coverage.out -covermode=atomic 36 | 37 | - name: Upload coverage to Codecov 38 | uses: codecov/codecov-action@v3 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | schedule: 9 | - cron: "0 0 * * *" 10 | 11 | env: 12 | GO_VERSION: "1.22.2" 13 | 14 | permissions: 15 | actions: read 16 | contents: read 17 | security-events: write 18 | 19 | jobs: 20 | analyze: 21 | name: Analyze 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | language: ["go"] 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | - name: Set up Go 34 | uses: actions/setup-go@v5 35 | with: 36 | go-version: ${{ env.GO_VERSION }} 37 | 38 | - name: Initialize CodeQL 39 | uses: github/codeql-action/init@v3 40 | with: 41 | languages: ${{ matrix.language }} 42 | 43 | - name: Autobuild 44 | uses: github/codeql-action/autobuild@v3 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v3 48 | with: 49 | category: "/language:${{matrix.language}}" 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "main" 8 | tags: 9 | - "v*" 10 | 11 | env: 12 | GO_VERSION: "1.23.0" 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | env: 21 | GO111MODULE: on 22 | DOCKER_CLI_EXPERIMENTAL: "enabled" 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | - name: Set up Go 29 | uses: actions/setup-go@v5 30 | with: 31 | go-version: ${{ env.GO_VERSION }} 32 | - name: Set up QEMU 33 | uses: docker/setup-qemu-action@v1 34 | - name: Cache Go modules 35 | uses: actions/cache@v4 36 | with: 37 | path: ~/go/pkg/mod 38 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 39 | restore-keys: | 40 | ${{ runner.os }}-go- 41 | - name: Tests 42 | run: | 43 | go mod tidy 44 | go test -v ./... 45 | - name: Login to Docker Hub 46 | if: github.event_name != 'pull_request' 47 | uses: docker/login-action@v2 48 | with: 49 | username: ${{ secrets.DOCKERHUB_USERNAME }} 50 | password: ${{ secrets.DOCKERHUB_TOKEN }} 51 | - name: Run GoReleaser 52 | uses: goreleaser/goreleaser-action@v5 53 | if: success() && startsWith(github.ref, 'refs/tags/') 54 | with: 55 | version: "latest" 56 | args: release --clean 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.html 2 | coverage.out 3 | docs/* 4 | !docs/README.md 5 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: quick-docs 2 | 3 | builds: 4 | - <<: &build_defaults 5 | env: 6 | - CGO_ENABLED=0 7 | ldflags: 8 | - -X "github.com/soulteary/docker-quick-docs/internal/version.Version={{ .Tag }}" 9 | id: macos 10 | goos: [ darwin ] 11 | goarch: [ amd64, arm64 ] 12 | 13 | - <<: *build_defaults 14 | id: linux 15 | goos: [linux] 16 | goarch: ["386", arm, amd64, arm64] 17 | goarm: 18 | - "7" 19 | - "6" 20 | 21 | dockers: 22 | 23 | - image_templates: 24 | - "soulteary/docker-quick-docs:linux-amd64-{{ .Tag }}" 25 | - "soulteary/docker-quick-docs:linux-amd64" 26 | dockerfile: docker/Dockerfile.goreleaser 27 | use: buildx 28 | goarch: amd64 29 | build_flag_templates: 30 | - "--pull" 31 | - "--platform=linux/amd64" 32 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 33 | - "--label=org.opencontainers.image.description={{ .ProjectName }}" 34 | - "--label=org.opencontainers.image.url=https://github.com/soulteary/docker-quick-docs" 35 | - "--label=org.opencontainers.image.source=https://github.com/soulteary/docker-quick-docs" 36 | - "--label=org.opencontainers.image.version={{ .Version }}" 37 | - "--label=org.opencontainers.image.created={{ .Date }}" 38 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 39 | - "--label=org.opencontainers.image.licenses=Apache-V2" 40 | 41 | - image_templates: 42 | - "soulteary/docker-quick-docs:linux-arm64-{{ .Tag }}" 43 | - "soulteary/docker-quick-docs:linux-arm64" 44 | dockerfile: docker/Dockerfile.goreleaser 45 | use: buildx 46 | goos: linux 47 | goarch: arm64 48 | goarm: '' 49 | build_flag_templates: 50 | - "--pull" 51 | - "--platform=linux/arm64" 52 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 53 | - "--label=org.opencontainers.image.description={{ .ProjectName }}" 54 | - "--label=org.opencontainers.image.url=https://github.com/soulteary/docker-quick-docs" 55 | - "--label=org.opencontainers.image.source=https://github.com/soulteary/docker-quick-docs" 56 | - "--label=org.opencontainers.image.version={{ .Version }}" 57 | - "--label=org.opencontainers.image.created={{ .Date }}" 58 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 59 | - "--label=org.opencontainers.image.licenses=Apache-V2" 60 | 61 | - image_templates: 62 | - "soulteary/docker-quick-docs:linux-armv7-{{ .Tag }}" 63 | - "soulteary/docker-quick-docs:linux-armv7" 64 | dockerfile: docker/Dockerfile.goreleaser 65 | use: buildx 66 | goos: linux 67 | goarch: arm 68 | goarm: "7" 69 | build_flag_templates: 70 | - "--pull" 71 | - "--platform=linux/arm/v7" 72 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 73 | - "--label=org.opencontainers.image.description={{ .ProjectName }}" 74 | - "--label=org.opencontainers.image.url=https://github.com/soulteary/docker-quick-docs" 75 | - "--label=org.opencontainers.image.source=https://github.com/soulteary/docker-quick-docs" 76 | - "--label=org.opencontainers.image.version={{ .Version }}" 77 | - "--label=org.opencontainers.image.created={{ .Date }}" 78 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 79 | - "--label=org.opencontainers.image.licenses=Apache-V2" 80 | 81 | - image_templates: 82 | - "soulteary/docker-quick-docs:linux-armv6-{{ .Tag }}" 83 | - "soulteary/docker-quick-docs:linux-armv6" 84 | dockerfile: docker/Dockerfile.goreleaser 85 | use: buildx 86 | goos: linux 87 | goarch: arm 88 | goarm: "6" 89 | build_flag_templates: 90 | - "--pull" 91 | - "--platform=linux/arm/v6" 92 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 93 | - "--label=org.opencontainers.image.description={{ .ProjectName }}" 94 | - "--label=org.opencontainers.image.url=https://github.com/soulteary/docker-quick-docs" 95 | - "--label=org.opencontainers.image.source=https://github.com/soulteary/docker-quick-docs" 96 | - "--label=org.opencontainers.image.version={{ .Version }}" 97 | - "--label=org.opencontainers.image.created={{ .Date }}" 98 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 99 | - "--label=org.opencontainers.image.licenses=Apache-V2" 100 | 101 | 102 | docker_manifests: 103 | - name_template: "soulteary/docker-quick-docs:{{ .Tag }}" 104 | image_templates: 105 | - "soulteary/docker-quick-docs:linux-amd64-{{ .Tag }}" 106 | - "soulteary/docker-quick-docs:linux-arm64-{{ .Tag }}" 107 | - "soulteary/docker-quick-docs:linux-armv7-{{ .Tag }}" 108 | - "soulteary/docker-quick-docs:linux-armv6-{{ .Tag }}" 109 | skip_push: "false" 110 | 111 | - name_template: "soulteary/docker-quick-docs:latest" 112 | image_templates: 113 | - "soulteary/docker-quick-docs:linux-amd64-{{ .Tag }}" 114 | - "soulteary/docker-quick-docs:linux-arm64-{{ .Tag }}" 115 | - "soulteary/docker-quick-docs:linux-armv7-{{ .Tag }}" 116 | - "soulteary/docker-quick-docs:linux-armv6-{{ .Tag }}" 117 | skip_push: "false" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023-2025 Su Yang (soulteary) 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Quick Docs 2 | 3 | [![Release](https://github.com/soulteary/docker-quick-docs/actions/workflows/release.yaml/badge.svg)](https://github.com/soulteary/docker-quick-docs/actions/workflows/release.yaml) [![Codecov](https://github.com/soulteary/docker-quick-docs/actions/workflows/codecov.yml/badge.svg)](https://github.com/soulteary/docker-quick-docs/actions/workflows/codecov.yml) [![CodeQL](https://github.com/soulteary/docker-quick-docs/actions/workflows/codeql.yml/badge.svg)](https://github.com/soulteary/docker-quick-docs/actions/workflows/codeql.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/soulteary/docker-quick-docs)](https://goreportcard.com/report/github.com/soulteary/docker-quick-docs) 4 | 5 | 本地部署、能够快速访问的文档工具,用来改善 GitHub Pages 文档访问体验。 6 | 7 | ## 下载工具 8 | 9 | 在 GitHub [发布页面](https://github.com/soulteary/docker-quick-docs/releases) 下载适合你操作系统的可执行文件。 10 | 11 | ![](.github/assets.jpg) 12 | 13 | 或者使用 Docker 下载工具: 14 | 15 | ![](.github/dockerhub.jpg) 16 | 17 | ```bash 18 | docker pull soulteary/docker-quick-docs:v0.1.7 19 | # 或者 20 | docker pull soulteary/docker-quick-docs 21 | ``` 22 | 23 | ## 使用工具 24 | 25 | 以 [baidu/san](http://github.com/baidu/san) 为例,我们将这个软件的 GitHub 进行本地化部署。 26 | 27 | 首先,获取想本地部署的软件的文档代码,GitHub Pages,通常需要下载 GitHub 的 `gh-pages` 分支: 28 | 29 | ```bash 30 | git clone http://github.com/baidu/san --depth 1 --branch=gh-pages 31 | Cloning into 'san'... 32 | warning: redirecting to https://github.com/baidu/san/ 33 | remote: Enumerating objects: 405, done. 34 | remote: Counting objects: 100% (405/405), done. 35 | remote: Compressing objects: 100% (197/197), done. 36 | remote: Total 405 (delta 154), reused 303 (delta 65), pack-reused 0 37 | Receiving objects: 100% (405/405), 2.17 MiB | 5.18 MiB/s, done. 38 | Resolving deltas: 100% (154/154), done. 39 | ``` 40 | 41 | 然后,我们可以将这些文档放到 `docs` 目录中。 42 | 43 | ```bash 44 | mv san docs/ 45 | ``` 46 | 47 | 然后我们执行程序即可: 48 | 49 | ```bash 50 | # 如果你选择使用可执行文件 51 | ./quick-docs 52 | 53 | # 如果你选择使用 docker 54 | docker run --rm -it -v `pwd`/docs:/app/docs -p 8080:8080 soulteary/docker-quick-docs:v0.1.7 55 | ``` 56 | 57 | 程序执行完毕,我们将看到类似下面的日志: 58 | 59 | ```bash 60 | 2024/01/04 10:38:31 Quick Docs 61 | 2024/01/04 10:38:31 未设置环境变量 `PORT`,使用默认端口:8080 62 | ``` 63 | 64 | 此时访问浏览器,就能够快速的访问文档啦。 65 | 66 | ## 配置 67 | 68 | 如果你想调整端口,可以设置 `PORT` 环境变量。 69 | 70 | 例如: 71 | 72 | ```bash 73 | PORT=8080 ./quick-docs 74 | # 或 75 | docker run --rm -it -e PORT=8080 -v `pwd`/docs:/app/docs -p 8080:8080 soulteary/docker-quick-docs:v0.1.7 76 | ``` 77 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "from": "https://ecomfe.github.io/san/", 4 | "to": "/san/", 5 | "type": "html", 6 | "dir": "/san/" 7 | }, 8 | { 9 | "from": "https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css", 10 | "to": "/cdn/docsearch.min.css" 11 | }, 12 | { 13 | "from": "https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js", 14 | "to": "/cdn/docsearch.min.js" 15 | }, 16 | { 17 | "from": "https://ghbtns.com/github-btn.html", 18 | "to": "img/logo-colorful.svg" 19 | }, 20 | { 21 | "from": "https://github.com/baidu/san-router", 22 | "to": "/san-router/" 23 | }, 24 | { 25 | "from": "https://ecomfe.github.io/santd/", 26 | "to": "/santd/" 27 | } 28 | ] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | docker-quick-docs: 5 | image: soulteary/docker-quick-docs:v0.1.7 6 | restart: always 7 | ports: 8 | - 8080:8080 9 | volumes: 10 | - ./docs:/app/docs 11 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.2-alpine3.19 as builder 2 | RUN echo '' > /etc/apk/repositories && \ 3 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.19/main" >> /etc/apk/repositories && \ 4 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.19/community" >> /etc/apk/repositories && \ 5 | echo "Asia/Shanghai" > /etc/timezone 6 | RUN apk add upx 7 | WORKDIR /build 8 | ENV CGO_ENABLED=0 9 | COPY . . 10 | RUN go build -ldflags "-w -s" 11 | RUN upx -9 -o quick-docs.minify quick-docs && mv quick-docs.minify quick-docs 12 | 13 | FROM alpine:3.19 14 | RUN echo '' > /etc/apk/repositories && \ 15 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.19/main" >> /etc/apk/repositories && \ 16 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.19/community" >> /etc/apk/repositories && \ 17 | echo "Asia/Shanghai" > /etc/timezone 18 | RUN apk add openssl && rm -rf /var/cache/apk/* 19 | WORKDIR /app 20 | COPY --from=Builder /build/quick-docs /bin/quick-docs 21 | ENTRYPOINT ["quick-docs"] -------------------------------------------------------------------------------- /docker/Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 2 | RUN apk add openssl && rm -rf /var/cache/apk/* 3 | WORKDIR /app 4 | COPY quick-docs /bin/quick-docs 5 | ENTRYPOINT ["quick-docs"] -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 文档目录 2 | 3 | 这个目录用来保存需要提供服务的网站文档。 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/soulteary/docker-quick-docs 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/gin-contrib/gzip v1.2.2 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/soulteary/gin-static v0.2.5 9 | github.com/stretchr/testify v1.10.0 10 | ) 11 | 12 | require ( 13 | github.com/bytedance/sonic v1.12.10 // indirect 14 | github.com/bytedance/sonic/loader v0.2.3 // indirect 15 | github.com/cloudwego/base64x v0.1.5 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 18 | github.com/gin-contrib/sse v1.0.0 // indirect 19 | github.com/go-playground/locales v0.14.1 // indirect 20 | github.com/go-playground/universal-translator v0.18.1 // indirect 21 | github.com/go-playground/validator/v10 v10.25.0 // indirect 22 | github.com/goccy/go-json v0.10.5 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 32 | github.com/ugorji/go/codec v1.2.12 // indirect 33 | golang.org/x/arch v0.15.0 // indirect 34 | golang.org/x/crypto v0.36.0 // indirect 35 | golang.org/x/net v0.37.0 // indirect 36 | golang.org/x/sys v0.31.0 // indirect 37 | golang.org/x/text v0.23.0 // indirect 38 | google.golang.org/protobuf v1.36.5 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI= 2 | github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= 3 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 4 | github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= 5 | github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 6 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 7 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 13 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 14 | github.com/gin-contrib/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEliI= 15 | github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s= 16 | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= 17 | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= 18 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 19 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 20 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 21 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 22 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 23 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 24 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 25 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 26 | github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= 27 | github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= 28 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 29 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 30 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 31 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 34 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 35 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 36 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 37 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 38 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 39 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 40 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 41 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 42 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 43 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 46 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 47 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 48 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 49 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/soulteary/gin-static v0.2.5 h1:bTp0nwYm/S520vLTLiNiIrp+FO5UJ7MejqS0IGNt2vM= 53 | github.com/soulteary/gin-static v0.2.5/go.mod h1:QaiaS5jwUrWY94YgvHBEPLDWScvlelpI093PRmhiyfk= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 56 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 57 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 58 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 59 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 61 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 62 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 63 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 67 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 68 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 69 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 70 | golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= 71 | golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= 72 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 73 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 74 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 75 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 76 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 78 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 79 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 80 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 82 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 84 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 91 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "encoding/json" 21 | "log" 22 | "os" 23 | "strings" 24 | 25 | "github.com/soulteary/docker-quick-docs/internal/fn" 26 | ) 27 | 28 | func ReadConfigFile(defaultFile string) ([]byte, error) { 29 | configFile := strings.TrimSpace(os.Getenv("CONFIG")) 30 | if configFile == "" { 31 | configFile = defaultFile 32 | } 33 | buf, err := os.ReadFile(configFile) 34 | if err != nil { 35 | if configFile != defaultFile { 36 | log.Println("读取配置文件失败,确认文件路径是否正确:", configFile) 37 | } 38 | return []byte(""), err 39 | } 40 | return buf, nil 41 | } 42 | 43 | type PostRule struct { 44 | From string `json:"from"` 45 | To string `json:"to"` 46 | Dir string `json:"dir,omitempty"` 47 | Type string `json:"type,omitempty"` 48 | } 49 | 50 | var PostRules []PostRule 51 | 52 | func GetConfig() { 53 | buf, err := ReadConfigFile(DOCS_DEFAULT_CONFIG) 54 | if err != nil { 55 | return 56 | } 57 | err = json.Unmarshal(buf, &PostRules) 58 | if err != nil { 59 | return 60 | } 61 | 62 | if len(PostRules) > 0 { 63 | log.Println("解析配置文件成功,规则数量:", len(PostRules)) 64 | var rules []PostRule 65 | for _, rule := range PostRules { 66 | // skip empty from rules 67 | if rule.From == "" { 68 | continue 69 | } 70 | // fill default values 71 | if rule.Dir == "" { 72 | rule.Dir = "*" 73 | } 74 | // fill default values 75 | if rule.Type == "" { 76 | rule.Type = "html" 77 | } 78 | rule.Type = fn.FixResType(rule.Type) 79 | rules = append(rules, rule) 80 | } 81 | PostRules = rules 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config_test 18 | 19 | import ( 20 | "encoding/json" 21 | "os" 22 | "testing" 23 | 24 | "github.com/soulteary/docker-quick-docs/internal/config" 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | // TestReadConfigFileSuccess tests if ReadConfigFile can read the default config file. 29 | func TestReadConfigFileSuccess(t *testing.T) { 30 | const configFileContent = `[ 31 | { 32 | "from": "https://ecomfe.github.io/san/", 33 | "to": "/san/", 34 | "type": "html", 35 | "dir": "/san/" 36 | } 37 | ]` 38 | 39 | // Create a temporary file simulating the config.json 40 | tmpfile, err := os.CreateTemp(".", "config.json") 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | defer os.Remove(tmpfile.Name()) // Clean up 45 | 46 | // Write data to the tmp config file 47 | if _, err := tmpfile.Write([]byte(configFileContent)); err != nil { 48 | t.Fatal(err) 49 | } 50 | if err := tmpfile.Close(); err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | // Test ReadConfigFile 55 | data, err := config.ReadConfigFile(tmpfile.Name()) 56 | 57 | assert.NoError(t, err) 58 | assert.Equal(t, configFileContent, string(data)) 59 | config.PostRules = []config.PostRule{} 60 | } 61 | 62 | // TestReadConfigFileEnvVar tests if ReadConfigFile correctly reads from CONFIG environment variable. 63 | func TestReadConfigFileEnvVar(t *testing.T) { 64 | const configFileContent = `{"from":"env_from","to":"env_to"}` 65 | // Create a temporary file simulating the config.json 66 | tmpfile, err := os.CreateTemp(".", "env_config.json") 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | defer os.Remove(tmpfile.Name()) // clean up 71 | 72 | // Write data to the tmp config file 73 | if _, err := tmpfile.Write([]byte(configFileContent)); err != nil { 74 | t.Fatal(err) 75 | } 76 | if err := tmpfile.Close(); err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | // Set the environment variable to point to our temp file 81 | os.Setenv("CONFIG", tmpfile.Name()) 82 | defer os.Unsetenv("CONFIG") 83 | 84 | data, err := config.ReadConfigFile(config.DOCS_DEFAULT_CONFIG) 85 | assert.NoError(t, err) 86 | assert.Equal(t, configFileContent, string(data)) 87 | config.PostRules = []config.PostRule{} 88 | } 89 | 90 | // TestReadConfigFileNoFile tests ReadConfigFile's response when no file is present. 91 | func TestReadConfigFileNoFile(t *testing.T) { 92 | os.Unsetenv("CONFIG") 93 | data, err := config.ReadConfigFile("not-exist.json") 94 | assert.Error(t, err) 95 | assert.Equal(t, []byte(""), data) 96 | } 97 | 98 | // TestGetConfigSuccess tests if GetConfig successfully parses the configuration. 99 | func TestGetConfigSuccess(t *testing.T) { 100 | const configFileContent = `[ 101 | { 102 | "from": "/source", 103 | "to": "/destination" 104 | } 105 | ]` 106 | 107 | // Create a temporary file simulating the config.json 108 | tmpfile, err := os.CreateTemp(".", "config.json") 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | defer os.Remove(tmpfile.Name()) // Clean up 113 | 114 | // Write data to the tmp config file 115 | if _, err := tmpfile.Write([]byte(configFileContent)); err != nil { 116 | t.Fatal(err) 117 | } 118 | if err := tmpfile.Close(); err != nil { 119 | t.Fatal(err) 120 | } 121 | 122 | os.Setenv("CONFIG", tmpfile.Name()) 123 | defer os.Unsetenv("CONFIG") 124 | 125 | config.GetConfig() 126 | 127 | var expected []config.PostRule 128 | err = json.Unmarshal([]byte(configFileContent), &expected) 129 | expected[0].Type = "text/html" 130 | expected[0].Dir = "*" 131 | assert.NoError(t, err) 132 | assert.Equal(t, expected, config.PostRules) 133 | config.PostRules = []config.PostRule{} 134 | } 135 | 136 | // TestGetConfigParseError tests if GetConfig handles JSON unmarshalling errors. 137 | func TestGetConfigParseError(t *testing.T) { 138 | const configFileContent = `invalid JSON content` 139 | 140 | // Create a temporary file simulating the config.json 141 | tmpfile, err := os.CreateTemp(".", "config.json") 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | defer os.Remove(tmpfile.Name()) // Clean up 146 | 147 | // Write data to the tmp config file 148 | if _, err := tmpfile.Write([]byte(configFileContent)); err != nil { 149 | t.Fatal(err) 150 | } 151 | if err := tmpfile.Close(); err != nil { 152 | t.Fatal(err) 153 | } 154 | 155 | os.Setenv("CONFIG", tmpfile.Name()) 156 | defer os.Unsetenv("CONFIG") 157 | 158 | config.GetConfig() 159 | 160 | assert.Equal(t, len(config.PostRules), 0) 161 | } 162 | -------------------------------------------------------------------------------- /internal/config/define.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | const ( 20 | DOCS_DIR_ROOT = "docs" 21 | DOCS_PUBLIC_HOST = "0.0.0.0" 22 | DOCS_INTERNAL_HOST = "127.0.0.1" 23 | DOCS_DEFAULT_CONFIG = "./config.json" 24 | ) 25 | -------------------------------------------------------------------------------- /internal/fn/fn.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fn 18 | 19 | import ( 20 | "log" 21 | "os" 22 | "strconv" 23 | "strings" 24 | ) 25 | 26 | func GetPort() int { 27 | defaultPort := 8080 28 | portStr := os.Getenv("PORT") 29 | 30 | if portStr == "" { 31 | log.Printf("未设置环境变量 `PORT`,使用默认端口:%d\n", defaultPort) 32 | return defaultPort 33 | } 34 | 35 | port, err := strconv.Atoi(portStr) 36 | if err != nil { 37 | log.Printf("环境变量 `PORT` 设置不正确,使用默认端口:%d\n", defaultPort) 38 | return defaultPort 39 | } 40 | 41 | if port < 1 || port > 65535 { 42 | log.Printf("环境变量 `PORT` 设置不正确,使用默认端口:%d\n", defaultPort) 43 | return defaultPort 44 | } 45 | return port 46 | } 47 | 48 | func IsEmbedMode() bool { 49 | return strings.ToLower(strings.TrimSpace(os.Getenv("EMBED"))) == "on" 50 | } 51 | 52 | func FixResType(typed string) string { 53 | typed = strings.ToLower(typed) 54 | switch typed { 55 | case "html": 56 | return "text/html" 57 | case "css": 58 | return "text/css" 59 | case "js": 60 | return "application/javascript" 61 | case "json": 62 | return "application/json" 63 | } 64 | return typed 65 | } 66 | -------------------------------------------------------------------------------- /internal/fn/fn_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fn_test 18 | 19 | import ( 20 | "os" 21 | "testing" 22 | 23 | "github.com/soulteary/docker-quick-docs/internal/fn" 24 | ) 25 | 26 | func TestGetPort(t *testing.T) { 27 | // Test with no environment variable set 28 | os.Unsetenv("PORT") 29 | port := fn.GetPort() 30 | if port != 8080 { 31 | t.Errorf("GetPort() without PORT set = %d; want 8080", port) 32 | } 33 | 34 | // Test with invalid integer value 35 | os.Setenv("PORT", "invalid") 36 | port = fn.GetPort() 37 | if port != 8080 { 38 | t.Errorf("GetPort() with invalid PORT = %d; want 8080", port) 39 | } 40 | 41 | // Test with valid integer but out of range value 42 | os.Setenv("PORT", "-1") 43 | port = fn.GetPort() 44 | if port != 8080 { 45 | t.Errorf("GetPort() with out-of-range PORT = %d; want 8080", port) 46 | } 47 | 48 | // Test with valid port number 49 | os.Setenv("PORT", "5000") 50 | port = fn.GetPort() 51 | if port != 5000 { 52 | t.Errorf("GetPort() with valid PORT = %d; want 5000", port) 53 | } 54 | } 55 | 56 | func TestIsEmbedMode(t *testing.T) { 57 | cases := []struct { 58 | env string 59 | value bool 60 | }{ 61 | {"on", true}, 62 | {"ON", true}, 63 | {"oN", true}, 64 | {"off", false}, 65 | {"OFF", false}, 66 | {"invalid", false}, 67 | {"", false}, 68 | {" on ", true}, 69 | {"on\n", true}, 70 | {"\non\t", true}, 71 | } 72 | 73 | for _, c := range cases { 74 | os.Setenv("EMBED", c.env) 75 | result := fn.IsEmbedMode() 76 | if result != c.value { 77 | t.Errorf("IsEmbedMode() with EMBED=%q = %v; want %v", c.env, result, c.value) 78 | } 79 | } 80 | } 81 | 82 | func TestFixResType(t *testing.T) { 83 | tests := []struct { 84 | name string 85 | input string 86 | expectedType string 87 | }{ 88 | {"HTML lower case", "html", "text/html"}, 89 | {"HTML upper case", "HTML", "text/html"}, 90 | {"CSS lower case", "css", "text/css"}, 91 | {"CSS mixed case", "CsS", "text/css"}, 92 | {"JS lower case", "js", "application/javascript"}, 93 | {"JS upper case", "JS", "application/javascript"}, 94 | {"JSON lower case", "json", "application/json"}, 95 | {"JSON mixed case", "Json", "application/json"}, 96 | {"Plain text", "txt", "txt"}, 97 | {"Image", "jpg", "jpg"}, 98 | {"Random string", "randomstring", "randomstring"}, 99 | } 100 | 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | result := fn.FixResType(tt.input) 104 | if result != tt.expectedType { 105 | t.Errorf("FixResType(%v): expected %v, got %v", tt.input, tt.expectedType, result) 106 | } 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /internal/network/network.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package network 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "net/http/httputil" 25 | "net/url" 26 | "strconv" 27 | "strings" 28 | 29 | "github.com/gin-gonic/gin" 30 | "github.com/soulteary/docker-quick-docs/internal/config" 31 | ) 32 | 33 | func MuteNoise(body []byte) []byte { 34 | content := UpdateBody(body, []byte("https://www.googletagmanager.com/gtag/js"), []byte("/keep-quiet.js")) 35 | content = UpdateBody(content, []byte("https://www.google-analytics.com/analytics.js"), []byte("/keep-quiet.js")) 36 | content = UpdateBody(content, []byte("https://hm.baidu.com/hm.js"), []byte("/keep-quiet.js")) 37 | return content 38 | } 39 | 40 | func UpdateBody(content []byte, from []byte, to []byte) []byte { 41 | return bytes.ReplaceAll(content, from, to) 42 | } 43 | 44 | type UpdateJob struct { 45 | From string 46 | To string 47 | } 48 | 49 | func Forward(port int) func(c *gin.Context) { 50 | target := fmt.Sprintf("http://%s:%d", config.DOCS_INTERNAL_HOST, port) 51 | url, _ := url.Parse(target) 52 | internal := httputil.NewSingleHostReverseProxy(url) 53 | return func(c *gin.Context) { 54 | internal.ModifyResponse = func(response *http.Response) error { 55 | if len(config.PostRules) == 0 || response.ContentLength == 0 || response.Body == nil { 56 | return nil 57 | } 58 | 59 | mimeType := strings.ToLower(response.Header.Get("Content-Type")) 60 | needUpdate := false 61 | 62 | var jobs []UpdateJob 63 | for _, rule := range config.PostRules { 64 | // match rule type 65 | if strings.HasPrefix(mimeType, rule.Type) { 66 | // match rule dir 67 | if rule.Dir == "*" || strings.HasPrefix(c.Request.URL.Path, rule.Dir) { 68 | needUpdate = true 69 | var job UpdateJob 70 | job.From = rule.From 71 | job.To = rule.To 72 | jobs = append(jobs, job) 73 | } 74 | } 75 | } 76 | 77 | // only allow html or need update content 78 | if mimeType != "text/html" && !needUpdate { 79 | return nil 80 | } 81 | 82 | body, err := io.ReadAll(response.Body) 83 | if err != nil { 84 | return err 85 | } 86 | response.Body.Close() 87 | bodyUpdated := MuteNoise(body) 88 | 89 | for _, job := range jobs { 90 | bodyUpdated = UpdateBody(bodyUpdated, []byte(job.From), []byte(job.To)) 91 | } 92 | 93 | bodyLength := len(bodyUpdated) 94 | response.Body = io.NopCloser(bytes.NewReader(bodyUpdated)) 95 | response.ContentLength = int64(bodyLength) 96 | response.Header.Set("Content-Length", strconv.Itoa(bodyLength)) 97 | return nil 98 | } 99 | 100 | internal.ServeHTTP(c.Writer, c.Request) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /internal/server/internal.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "embed" 21 | "fmt" 22 | "net/http" 23 | "os" 24 | 25 | "github.com/gin-gonic/gin" 26 | static "github.com/soulteary/gin-static" 27 | ) 28 | 29 | func InternalServer(host string, port int, dirRoot string, embedFS embed.FS, embedMode bool) { 30 | r := gin.New() 31 | 32 | if embedMode { 33 | r.NoRoute(static.ServeEmbed(dirRoot, embedFS)) 34 | } else { 35 | os.MkdirAll(dirRoot, os.ModePerm) 36 | r.Use(static.Serve("/", static.LocalFile(dirRoot, true))) 37 | } 38 | 39 | r.GET("/keep-quiet.js", func(c *gin.Context) { 40 | c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte("console.log('Hello, world!')")) 41 | }) 42 | 43 | r.Run(fmt.Sprintf("%s:%d", host, port)) 44 | } 45 | -------------------------------------------------------------------------------- /internal/server/public.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/gin-contrib/gzip" 23 | "github.com/gin-gonic/gin" 24 | ) 25 | 26 | func PublicServer(host string, port int, forwarder func(c *gin.Context)) { 27 | r := gin.New() 28 | r.Use(gin.Recovery()) 29 | r.Use(gzip.Gzip(gzip.DefaultCompression)) 30 | r.Any("/*path", forwarder) 31 | r.Run(fmt.Sprintf("%s:%d", host, port)) 32 | } 33 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package server 18 | 19 | import ( 20 | "embed" 21 | 22 | "github.com/gin-gonic/gin" 23 | "github.com/soulteary/docker-quick-docs/internal/config" 24 | "github.com/soulteary/docker-quick-docs/internal/fn" 25 | "github.com/soulteary/docker-quick-docs/internal/network" 26 | ) 27 | 28 | func Launch(embedFS embed.FS) { 29 | isEmbedMode := fn.IsEmbedMode() 30 | publicPort := fn.GetPort() 31 | config.GetConfig() 32 | internalPort := publicPort - 1 33 | gin.SetMode(gin.ReleaseMode) 34 | forwarder := network.Forward(internalPort) 35 | go InternalServer(config.DOCS_INTERNAL_HOST, internalPort, config.DOCS_DIR_ROOT, embedFS, isEmbedMode) 36 | PublicServer(config.DOCS_PUBLIC_HOST, publicPort, forwarder) 37 | } 38 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package version 18 | 19 | var Version = "dev" 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023-2025 Su Yang (soulteary) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "embed" 21 | "log" 22 | 23 | "github.com/soulteary/docker-quick-docs/internal/server" 24 | "github.com/soulteary/docker-quick-docs/internal/version" 25 | ) 26 | 27 | //go:embed docs 28 | var EmbedFS embed.FS 29 | 30 | func main() { 31 | log.Println("Quick Docs", version.Version) 32 | server.Launch(EmbedFS) 33 | } 34 | --------------------------------------------------------------------------------