├── .gitignore ├── LICENSE ├── README.md ├── asset └── fileupdater.service ├── build-mac.sh ├── build.sh ├── cmd ├── example.yaml └── main.go ├── go.mod ├── go.sum ├── logo ├── fileupdater.ico └── fileupdater.png ├── pkg ├── config │ └── config.go ├── core │ ├── core.go │ ├── core_test.go │ └── types.go ├── listeners │ └── sig.go ├── process │ └── process.go └── server │ ├── exec.go │ ├── exec_test.go │ ├── getContent.go │ ├── getUpdater.go │ ├── getUpdaters.go │ ├── server.go │ ├── setup.go │ └── update.go ├── test ├── api.http ├── config.json ├── config.yaml └── test.json ├── ui.png └── ui ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── app └── store.js ├── components ├── dashboard-layout.js ├── dashboard-navbar.js ├── dashboard-sidebar.js ├── editor │ ├── Editor.js │ └── editor-layout.js └── nav-item.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js ├── setupProxy.js ├── setupTests.js ├── theme └── index.js └── updaters └── updaterSlice.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | config.json 3 | config.yaml 4 | .DS_Store 5 | temp 6 | tmp 7 | ui/node_modules/ 8 | cmd/cmd 9 | bin 10 | *.log 11 | cmd/build 12 | ui/build 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fileUpdater 2 | 3 | 4 | 5 | The file updater helps you use a web page with an editor to update your files and trigger the related command hooks. 6 | Just a single binary file (thanks golang!) 7 | 8 | 9 | ## features 10 | * easy deploy (single binary file) 11 | * pre command hook 12 | * post command hook 13 | * command exec timeout (todo) 14 | * auto backup 15 | * daemon 16 | * update itself 17 | * low memory require 18 | * exec shell command 19 | * tiny init (simple superviosr)(todo) 20 | * basic auth (todo) 21 | ## How to use it 22 | 23 | >| Linux 24 | 25 | 1. get the binary 26 | ```bash 27 | wget https://github.com/GoSome/fileUpdater/releases/download/v0.2.3/fileupdater-amd64-linux 28 | chmod +x fileupdater-amd64-linux 29 | ``` 30 | 2. create simple config 31 | 32 | config.yaml 33 | ```yaml 34 | 35 | server_port: "8080" 36 | server_host: "0.0.0.0" 37 | updaters: 38 | - name: test1 39 | path: /tmp/test.txt 40 | backup: false 41 | pre_hook: 42 | mode: strict 43 | commands: 44 | - ls -lha 45 | 46 | ``` 47 | 48 | 3. just run 49 | 50 | ```yaml 51 | ./fileupdater-amd64-linux -i -config config.yaml 52 | ``` 53 | ## UI 54 | 55 | ![fileUpdater](./ui.png) 56 | 57 | 58 | ## Hook Seq 59 | 60 | ```bash 61 | PRE -> WRITE -> POST 62 | ``` 63 | -------------------------------------------------------------------------------- /asset/fileupdater.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=fileupdater 3 | After=network.target 4 | Wants=network.target 5 | 6 | [Service] 7 | 8 | LimitNOFILE=1000000 9 | ExecStart=/usr/local/sbin/fileupdater -config /etc/fileupdater/config.yaml 10 | KillMode=process 11 | Restart=on-failure 12 | RestartSec=50s 13 | 14 | [Install] 15 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /build-mac.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "go get" 5 | go get -v ./... 6 | 7 | # ui 8 | cd ui 9 | npm install 10 | npm run build 11 | 12 | echo "move file for go binding data" 13 | rm -rf ../cmd/build 14 | mv build ../cmd 15 | 16 | # cmd 17 | echo "build server" 18 | cd ../cmd 19 | GOOS=darwin GOARCH=amd64 go build -o ../bin/fileupdater-amd64-darwin 20 | GOOS=darwin GOARCH=arm64 go build -o ../bin/fileupdater-arm64-darwin 21 | echo "clean statics" 22 | 23 | rm -rf build 24 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "go get" 5 | go get -v ./... 6 | 7 | # ui 8 | cd ui 9 | npm install 10 | npm run build 11 | 12 | echo "move file for go binding data" 13 | rm -rf ../cmd/build 14 | mv build ../cmd 15 | 16 | # cmd 17 | echo "build server" 18 | cd ../cmd 19 | GOOS=linux GOARCH=amd64 go build -o ../bin/fileupdater-amd64-linux 20 | GOOS=linux GOARCH=arm64 go build -o ../bin/fileupdater-arm64-linux 21 | echo "clean statics" 22 | 23 | rm -rf build 24 | 25 | cd ../bin 26 | # Push binaries to GitHub release 27 | echo "Pushing binaries to GitHub release" 28 | VERSION=$(git describe --tags --abbrev=0) 29 | gh release create $VERSION fileupdater-amd64-linux fileupdater-arm64-linux --generate-notes 30 | 31 | echo "Release $VERSION created and binaries uploaded successfully" 32 | 33 | cd .. -------------------------------------------------------------------------------- /cmd/example.yaml: -------------------------------------------------------------------------------- 1 | server_port: "8090" 2 | server_host: "0.0.0.0" 3 | disable_ui: false 4 | updaters: 5 | - name: test1 6 | path: /tmp/test.txt 7 | backup: false 8 | pre_hook: 9 | commands: 10 | - echo `date` > /tmp/test.txt 11 | - name: test2 12 | path: /tmp/test2.txt 13 | backup: false 14 | pre_hook: 15 | commands: 16 | - echo `date` > /tmp/test2.txt 17 | - name: test3 18 | path: /tmp/test2.txt 19 | backup: false 20 | pre_hook: 21 | commands: 22 | - echo `date` > /tmp/test2.txt 23 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | @Description: just go 3 | @Author: skipper 4 | @Date: 2020/1/13 5 | @Time: 5:49 PM 6 | @ProjectName fileUpdater 7 | */ 8 | package main 9 | 10 | import ( 11 | "embed" 12 | _ "embed" 13 | "flag" 14 | "log" 15 | 16 | "github.com/GoSome/fileUpdater/pkg/config" 17 | "github.com/GoSome/fileUpdater/pkg/listeners" 18 | "github.com/GoSome/fileUpdater/pkg/server" 19 | "github.com/sevlyar/go-daemon" 20 | ) 21 | 22 | //go:embed build/* 23 | var uiContent embed.FS 24 | 25 | func main() { 26 | flag.StringVar(&config.Path, "config", "config.json", "server config file path") 27 | flag.BoolVar(&config.DaemonZ, "d", false, "daemon") 28 | flag.BoolVar(&config.IncludeSelf, "i", false, "include config file to updaters") 29 | flag.BoolVar(&config.DisableHotReload, "disable-reload", false, "disable hot reload config file") 30 | flag.StringVar(&config.PidPath, "pid", "", "pid path work in daemon") 31 | flag.StringVar(&config.LogFile, "log", "", "log path work in daemon") 32 | flag.Parse() 33 | 34 | config.Parse(true) 35 | 36 | if !config.DisableHotReload { 37 | listeners.ListenSIGUSR2() 38 | go config.Watch() 39 | } 40 | 41 | if config.DaemonZ { 42 | cntxt := &daemon.Context{ 43 | PidFileName: config.PidPath, 44 | PidFilePerm: 0644, 45 | LogFileName: config.LogFile, 46 | LogFilePerm: 0640, 47 | WorkDir: "./", 48 | Umask: 027, 49 | Args: flag.Args(), 50 | } 51 | 52 | d, err := cntxt.Reborn() 53 | if err != nil { 54 | log.Fatal("Unable to run: ", err) 55 | } 56 | if d != nil { 57 | return 58 | } 59 | defer cntxt.Release() 60 | 61 | log.Print("- - - - - - - - - - - - - - -") 62 | log.Print("daemon started") 63 | } 64 | config.Config.SetHttpData(uiContent) 65 | server.Run(config.Config) 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoSome/fileUpdater 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.7 7 | github.com/gin-gonic/gin v1.7.0 8 | github.com/sevlyar/go-daemon v0.1.5 9 | gopkg.in/djherbis/times.v1 v1.2.0 10 | gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 11 | ) 12 | 13 | require ( 14 | github.com/gin-contrib/sse v0.1.0 // indirect 15 | github.com/go-playground/locales v0.13.0 // indirect 16 | github.com/go-playground/universal-translator v0.17.0 // indirect 17 | github.com/go-playground/validator/v10 v10.4.1 // indirect 18 | github.com/golang/protobuf v1.3.3 // indirect 19 | github.com/json-iterator/go v1.1.9 // indirect 20 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 21 | github.com/kr/pretty v0.1.0 // indirect 22 | github.com/leodido/go-urn v1.2.0 // indirect 23 | github.com/mattn/go-isatty v0.0.12 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 25 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect 26 | github.com/ugorji/go/codec v1.1.7 // indirect 27 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect 28 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 29 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 30 | gopkg.in/yaml.v2 v2.2.8 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 5 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 6 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 7 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 8 | github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU= 9 | github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 10 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 11 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 12 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 13 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 14 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 15 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 16 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 17 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 18 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 19 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 20 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 21 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 22 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 23 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= 24 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 25 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 26 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 27 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 28 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 29 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 30 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 31 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 32 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 33 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 34 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 35 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 36 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 37 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/sevlyar/go-daemon v0.1.5 h1:Zy/6jLbM8CfqJ4x4RPr7MJlSKt90f00kNM1D401C+Qk= 41 | github.com/sevlyar/go-daemon v0.1.5/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE= 42 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 43 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 44 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 45 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 46 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 47 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 48 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 49 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 50 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 51 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 52 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= 57 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 59 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 60 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 61 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 62 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 63 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 64 | gopkg.in/djherbis/times.v1 v1.2.0 h1:UCvDKl1L/fmBygl2Y7hubXCnY7t4Yj46ZrBFNUipFbM= 65 | gopkg.in/djherbis/times.v1 v1.2.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= 66 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 67 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 68 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 69 | gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 h1:XZx7nhd5GMaZpmDaEHFVafUZC7ya0fuo7cSJ3UCKYmM= 70 | gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 71 | -------------------------------------------------------------------------------- /logo/fileupdater.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoSome/fileUpdater/41ffed0e4c7d436c94fac30b00f6fe74e651881a/logo/fileupdater.ico -------------------------------------------------------------------------------- /logo/fileupdater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoSome/fileUpdater/41ffed0e4c7d436c94fac30b00f6fe74e651881a/logo/fileupdater.png -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "path" 8 | 9 | "github.com/GoSome/fileUpdater/pkg/core" 10 | "github.com/fsnotify/fsnotify" 11 | "github.com/gin-gonic/gin" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | var Path string 16 | var Config core.ServerConfigs 17 | var DaemonZ bool 18 | var PidPath string 19 | var LogFile string 20 | var IncludeSelf bool 21 | var DisableHotReload bool 22 | 23 | func Parse(init bool) { 24 | logFunc := log.Printf 25 | if init { 26 | logFunc = log.Fatalf 27 | } 28 | if _, err := os.Stat(Path); os.IsNotExist(err) { 29 | logFunc("config file \"%s\" not exist", Path) 30 | return 31 | } 32 | 33 | configFile, err := os.Open(Path) 34 | if err != nil { 35 | logFunc("open config file \"%s\" failed", Path) 36 | return 37 | } 38 | defer configFile.Close() 39 | 40 | switch path.Ext(Path) { 41 | case ".json": 42 | err = json.NewDecoder(configFile).Decode(&Config) 43 | if err != nil { 44 | logFunc("%s", err) 45 | } 46 | case ".yml": 47 | fallthrough 48 | case ".yaml": 49 | err := yaml.NewDecoder(configFile).Decode(&Config) 50 | if err != nil { 51 | logFunc("%s", err) 52 | } 53 | default: 54 | logFunc("config file path must end with .json or .yaml") 55 | } 56 | 57 | if IncludeSelf { 58 | Config.FileUpdaters = append(Config.FileUpdaters, core.FileUpdater{Name: "selfConfig", FilePath: Path}) 59 | } 60 | } 61 | 62 | // Inject injects config into gin's context. 63 | func Inject(c *gin.Context) { 64 | c.Set("cfg", Config) 65 | c.Next() 66 | } 67 | 68 | // Watch watches config file, configs will be reloaded when config file is changed. 69 | func Watch() { 70 | watcher, err := fsnotify.NewWatcher() 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | defer watcher.Close() 75 | 76 | watcher.Add(Path) 77 | 78 | for { 79 | select { 80 | case ev := <-watcher.Events: 81 | if ev.Op == fsnotify.Write { 82 | log.Println("config file has been changed, attempt to reload...") 83 | Parse(false) 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "sort" 14 | "strings" 15 | "time" 16 | 17 | "github.com/GoSome/fileUpdater/pkg/process" 18 | "gopkg.in/djherbis/times.v1" 19 | ) 20 | 21 | // server configFiles struct 22 | type ServerConfigs struct { 23 | ServerHost string `json:"server_host" yaml:"server_host"` 24 | ServerPort string `json:"server_port" yaml:"server_port"` 25 | FileUpdaters []FileUpdater `json:"updaters" yaml:"updaters"` 26 | Processes []process.Process `json:"processes" yaml:"processes"` 27 | IncludeSelf bool `json:"include_self"` 28 | DisableUI bool `json:"disable_ui"` 29 | HttpData embed.FS `json:"-" yaml:"-"` 30 | } 31 | 32 | func (s *ServerConfigs) SetHttpData(data embed.FS) { 33 | s.HttpData = data 34 | } 35 | 36 | func (s ServerConfigs) GetUpdaterByName(name string) *FileUpdater { 37 | for k, v := range s.FileUpdaters { 38 | if v.Name == name { 39 | return &s.FileUpdaters[k] 40 | } 41 | } 42 | return nil 43 | } 44 | func (s ServerConfigs) RunProcess() { 45 | for _, p := range s.Processes { 46 | if p.Enable != true { 47 | continue 48 | } 49 | log.Println("????", p) 50 | p.Go() //todo there is a very interesting feature 51 | } 52 | } 53 | 54 | type FileUpdater struct { 55 | Name string `json:"name" yaml:"name"` 56 | FilePath string `json:"path" yaml:"path"` 57 | Backup bool `json:"backup" yaml:"backup"` 58 | PreHook CommandHook `json:"pre_hook" yaml:"pre_hook"` 59 | PostHook CommandHook `json:"post_hook" yaml:"post_hook"` 60 | } 61 | 62 | // should close reader 63 | func (u FileUpdater) GetFile() (reader *os.File, err error) { 64 | return os.Open(u.FilePath) 65 | } 66 | 67 | func (u FileUpdater) GetFileContent() ([]byte, error) { 68 | file, err := u.GetFile() 69 | if err != nil { 70 | return nil, err 71 | } 72 | return ioutil.ReadAll(file) 73 | } 74 | 75 | func (u FileUpdater) GetFileContentAsString() (string, error) { 76 | content, err := u.GetFileContent() 77 | if err != nil { 78 | return "", err 79 | } 80 | return string(content), nil 81 | } 82 | 83 | // when preHook Not nil should execute pre hook 84 | // when preHook executed and exit not 0 return 85 | // copy origin file for backup and update file 86 | // when file update succeeded execute the post hook 87 | // pre->write->post 88 | func (u FileUpdater) UpdateFile(date io.Reader) error { 89 | var err error 90 | 91 | // pre hook 92 | err = u.execPreHook() 93 | if err != nil { 94 | return err 95 | } 96 | 97 | // Copy File for backup 98 | // todo 99 | var bfp string 100 | if u.Backup { 101 | bfp, err = BackupFile(u.FilePath) 102 | if err != nil { 103 | log.Println("backup err: ", err) 104 | return errors.New("backup origin file failed") 105 | } 106 | } 107 | // write new content to file 108 | file, err := os.OpenFile(u.FilePath, os.O_TRUNC|os.O_RDWR|os.O_SYNC, 0644) 109 | if err != nil { 110 | return errors.New("open file failed") 111 | } 112 | _, err = io.Copy(file, date) 113 | file.Close() 114 | // when write failed,will auto restore,when restore failed,something bad happen 115 | if err != nil { 116 | // restore 117 | log.Printf("restore file! origin: %s,backup: %s", u.FilePath, bfp) 118 | err := RestoreFile(u.FilePath, bfp) 119 | if err != nil { 120 | log.Println("restore backup file failed") 121 | return errors.New("restore backup file failed,please check it out manually") 122 | } 123 | } 124 | 125 | // post hook 126 | err = u.execPostHook() 127 | if err != nil { 128 | // todo 129 | log.Println("run post hook failed ", err.Error()) 130 | return err 131 | } 132 | return nil 133 | } 134 | 135 | func (u FileUpdater) execPreHook() error { 136 | // if no command in pre Hook skip 137 | if len(u.PreHook.Commands) != 0 { 138 | if u.PreHook.Mode == "strict" { 139 | for _, c := range u.PreHook.Commands { 140 | out, err := BashExec(c) 141 | if err != nil { 142 | log.Println(out) 143 | return errors.New("pre hook execute failed") 144 | } 145 | } 146 | } else { 147 | for _, c := range u.PreHook.Commands { 148 | BashExec(c) 149 | } 150 | } 151 | } 152 | return nil 153 | } 154 | func (u FileUpdater) execPostHook() error { 155 | // do the post hook 156 | if len(u.PostHook.Commands) != 0 { 157 | if u.PostHook.Mode == "strict" { 158 | for _, c := range u.PostHook.Commands { 159 | out, err := BashExec(c) 160 | if err != nil { 161 | log.Println(out) 162 | return errors.New("pre hook execute failed") 163 | } 164 | } 165 | } else { 166 | for _, c := range u.PostHook.Commands { 167 | BashExec(c) 168 | } 169 | } 170 | } 171 | return nil 172 | } 173 | 174 | func BashExec(cmd string) (output string, err error) { 175 | var stdout, stderr bytes.Buffer 176 | command := exec.Command("bash", "-c", cmd) 177 | command.Stdout = &stdout 178 | command.Stderr = &stderr 179 | log.Printf("执行命令: %s", cmd) 180 | err = command.Run() 181 | if err != nil { 182 | output = stderr.String() 183 | log.Printf("执行命令报错: %s, stderr: \n\n%s", err.Error(), output) 184 | return output, err 185 | } 186 | return stdout.String(), nil 187 | } 188 | 189 | func BackupFile(filePath string) (newPath string, err error) { 190 | KeepBackup(filePath, 2) 191 | backupPath := genBackupFilePath(filePath) 192 | originFile, err := os.Open(filePath) 193 | defer originFile.Close() 194 | if err != nil { 195 | return "", err 196 | } 197 | backupFile, err := os.OpenFile(backupPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644) 198 | defer backupFile.Close() 199 | if err != nil { 200 | return "", err 201 | } 202 | _, err = io.Copy(backupFile, originFile) 203 | if err != nil { 204 | return "", err 205 | } 206 | return backupPath, nil 207 | } 208 | 209 | func RestoreFile(filePath, BackupPath string) error { 210 | file, err := os.OpenFile(filePath, os.O_TRUNC|os.O_RDWR|os.O_SYNC, 0644) 211 | if err != nil { 212 | return err 213 | } 214 | backcup, err := os.Open(BackupPath) 215 | if err != nil { 216 | return errors.New("open backup file failed") 217 | } 218 | _, err = io.Copy(file, backcup) 219 | return err 220 | } 221 | 222 | func KeepBackup(path string, num int) { 223 | absPath, err := filepath.Abs(path) 224 | if err != nil { 225 | log.Println("get file abs path err: ", err.Error()) 226 | return 227 | } 228 | dir := filepath.Dir(absPath) 229 | allFiles, err := filepath.Glob(dir + "/*") 230 | if err != nil { 231 | log.Println("get files err: ", err.Error()) 232 | return 233 | } 234 | var backFilesPath []string 235 | for _, f := range allFiles { 236 | if isBackupFile(f, path) { 237 | backFilesPath = append(backFilesPath, f) 238 | } 239 | } 240 | needRemoveBack := FindOldFiles(backFilesPath, len(backFilesPath)-num) 241 | log.Println("remove the old backup files: ", needRemoveBack) 242 | for _, f := range needRemoveBack { 243 | os.Remove(f) 244 | } 245 | } 246 | 247 | // return the most older files in given files 248 | func FindOldFiles(Paths []string, n int) (oldFiles []string) { 249 | fsa := make(FilesAtime, len(Paths)) 250 | for k, f := range Paths { 251 | t, err := times.Stat(f) 252 | // if get files atime failed use now instead 253 | if err != nil { 254 | fsa[k] = fileAtime{ 255 | FilePath: f, 256 | Atime: time.Now(), 257 | } 258 | continue 259 | } 260 | fsa[k] = fileAtime{ 261 | FilePath: f, 262 | Atime: t.ChangeTime(), 263 | } 264 | } 265 | // get the older 266 | sort.Reverse(fsa) 267 | if len(Paths) > n { 268 | for i, f := range fsa { 269 | if i < n { 270 | oldFiles = append(oldFiles, f.FilePath) 271 | continue 272 | } 273 | return 274 | } 275 | } 276 | for _, f := range fsa { 277 | oldFiles = append(oldFiles, f.FilePath) 278 | } 279 | return 280 | } 281 | 282 | // for sort 283 | type fileAtime struct { 284 | FilePath string 285 | Atime time.Time 286 | } 287 | type FilesAtime []fileAtime 288 | 289 | func (f FilesAtime) Len() int { 290 | return len(f) 291 | } 292 | func (f FilesAtime) Swap(i, j int) { 293 | f[i], f[j] = f[j], f[i] 294 | } 295 | func (f FilesAtime) Less(i, j int) bool { 296 | ti := f[i].Atime 297 | tj := f[j].Atime 298 | 299 | return ti.Before(tj) 300 | } 301 | 302 | // 303 | func genBackupFilePath(path string) string { 304 | absPath, err := filepath.Abs(path) 305 | if err != nil { 306 | log.Println("err: ", err.Error()) 307 | return path 308 | } 309 | dir := filepath.Dir(absPath) 310 | 311 | return dir + backupFlag(path) + time.Now().Format("2006-01-02-15:04:05") 312 | } 313 | func backupFlag(path string) string { 314 | return "/." + filepath.Base(path) + "-fub" + "." 315 | } 316 | 317 | // is f1 an backup of f2 318 | func isBackupFile(f1, f2 string) bool { 319 | flag := backupFlag(f2) 320 | res := strings.Contains(f1, flag) 321 | return res 322 | } 323 | -------------------------------------------------------------------------------- /pkg/core/core_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | @Description: just go 3 | @Author: skipper 4 | @Date: 2020/1/17 5 | @Time: 3:16 PM 6 | @ProjectName fileUpdater 7 | */ 8 | package core 9 | 10 | import ( 11 | "log" 12 | "path/filepath" 13 | "testing" 14 | ) 15 | 16 | func TestFindOldFiles(t *testing.T) { 17 | paths, err := filepath.Glob("/tmp/*") 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | log.Println(FindOldFiles(paths, 1)) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/core/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | @Description: just go 3 | @Author: skipper 4 | @Date: 2020/1/13 5 | @Time: 3:50 PM 6 | @ProjectName fileUpdater 7 | */ 8 | package core 9 | 10 | import ( 11 | "io" 12 | ) 13 | 14 | type Updater interface { 15 | GetFileContent() (io.Reader, error) 16 | } 17 | 18 | type Hook interface { 19 | Do() error 20 | } 21 | 22 | type CommandHook struct { 23 | Commands []string `json:"commands"` 24 | Mode string `json:"mode"` 25 | } 26 | 27 | func (c CommandHook) Do() error { 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/listeners/sig.go: -------------------------------------------------------------------------------- 1 | package listeners 2 | 3 | import ( 4 | "github.com/GoSome/fileUpdater/pkg/config" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func ListenSIGUSR2() { 12 | s := make(chan os.Signal, 1) 13 | signal.Notify(s, syscall.SIGUSR2) 14 | 15 | go func() { 16 | for { 17 | <-s 18 | log.Println("config file has been changed, attempt to reload...") 19 | config.Parse(false) 20 | } 21 | }() 22 | } 23 | -------------------------------------------------------------------------------- /pkg/process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | type Process struct { 10 | Command string `json:"command" yaml:"command"` 11 | AutoRestart bool `json:"auto_restart" yaml:"auto_restart"` 12 | LogPath string `json:"log_path" yaml:"log_path"` 13 | logfile *os.File 14 | Enable bool `json:"enable" yaml:"enable"` 15 | Pid int `json:"pid" yaml:"pid"` 16 | } 17 | 18 | func (p Process) Go() { 19 | go func() { 20 | err := p.Run() 21 | if err != nil { 22 | log.Println("run command ",p.Command," err: ", err.Error()) 23 | } 24 | }() 25 | } 26 | func (p *Process) Run() error { 27 | cmd := exec.Command("sh", "-c", p.Command) 28 | if p.LogPath != "" { 29 | logFile, err := os.OpenFile(p.LogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 30 | if err != nil { 31 | return err 32 | } 33 | p.logfile = logFile 34 | 35 | } 36 | if p.logfile == nil { 37 | cmd.Stdout = os.Stdout 38 | cmd.Stderr = os.Stderr 39 | } 40 | cmd.Stderr = p.logfile 41 | cmd.Stdout = p.logfile 42 | err := cmd.Start() 43 | if err != nil { 44 | return err 45 | } 46 | p.Pid = cmd.Process.Pid 47 | 48 | err = cmd.Wait() 49 | if err != nil { 50 | return err 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/server/exec.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "os/exec" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type execReq struct { 14 | Shell string `json:"shell"` 15 | } 16 | 17 | type execRes struct { 18 | ExitCode int `json:"exit_code"` 19 | Stdout string `json:"stdout"` 20 | Stderr string `json:"stderr"` 21 | } 22 | 23 | func (a *App) Exec(c *gin.Context) { 24 | var req execReq 25 | if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil { 26 | c.String(http.StatusBadRequest, "Invalid body format") 27 | } 28 | 29 | var res execRes 30 | var stdout, stderr bytes.Buffer 31 | cmd := exec.Command("bash", "-c", fmt.Sprintf("\"%s\"", req.Shell)) 32 | cmd.Stderr = &stderr 33 | cmd.Stdout = &stdout 34 | 35 | _ = cmd.Run() 36 | 37 | res.ExitCode = cmd.ProcessState.ExitCode() 38 | res.Stdout = stdout.String() 39 | res.Stderr = stderr.String() 40 | 41 | c.JSON(http.StatusOK, res) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/server/exec_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | // func TestExec(t *testing.T) { 4 | // type args struct { 5 | // body string 6 | // } 7 | // tests := []struct { 8 | // name string 9 | // args args 10 | // }{ 11 | // {"Execute pwd", args{body: `{"shell": "pwd"}`}}, 12 | // {"Execute ls", args{body: `{"shell": "ls"}`}}, 13 | // } 14 | // for _, tt := range tests { 15 | // var b bytes.Buffer 16 | // b.Write([]byte(tt.args.body)) 17 | 18 | // httpRes := httptest.NewRecorder() 19 | // c, _ := gin.CreateTestContext(httpRes) 20 | // c.Request, _ = http.NewRequest("POST", "/api/exec", &b) 21 | 22 | // t.Run(tt.name, func(t *testing.T) { 23 | // var res execRes 24 | // Exec(c) 25 | // _ = json.NewDecoder(httpRes.Body).Decode(&res) 26 | 27 | // assert.Equal(t, 0, res.ExitCode) 28 | // assert.NotEmpty(t, res.Stdout) 29 | // assert.Empty(t, res.Stderr) 30 | // }) 31 | // } 32 | // } 33 | -------------------------------------------------------------------------------- /pkg/server/getContent.go: -------------------------------------------------------------------------------- 1 | /* 2 | @Description: just go 3 | @Author: skipper 4 | @Date: 2020/1/13 5 | @Time: 5:08 PM 6 | @ProjectName fileUpdater 7 | */ 8 | package server 9 | 10 | import ( 11 | "github.com/GoSome/fileUpdater/pkg/core" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func (a *App) GetContent(c *gin.Context) { 16 | name := c.Query("name") 17 | if name == "" { 18 | // todo 19 | c.String(200, "nothing i found") 20 | return 21 | } 22 | 23 | getConfig, _ := c.Get("cfg") 24 | cfg := getConfig.(core.ServerConfigs) 25 | 26 | u := cfg.GetUpdaterByName(name) 27 | if u == nil { 28 | c.String(400, "no idea") 29 | return 30 | } 31 | r, err := u.GetFile() 32 | defer r.Close() 33 | if err != nil { 34 | c.String(400, "no idea") 35 | return 36 | } 37 | // TODO: javascript treats json file content as object incorrectly 38 | content, err := u.GetFileContentAsString() 39 | if err != nil { 40 | c.String(500, err.Error()) 41 | return 42 | } 43 | c.JSON(200, map[string]string{ 44 | "content": content, 45 | }) 46 | 47 | } 48 | 49 | type Req struct { 50 | Name string `json:"name"` 51 | } 52 | -------------------------------------------------------------------------------- /pkg/server/getUpdater.go: -------------------------------------------------------------------------------- 1 | /* 2 | @Description: just go 3 | @Author: skipper 4 | @Date: 2020/1/13 5 | @Time: 5:12 PM 6 | @ProjectName fileUpdater 7 | */ 8 | package server 9 | 10 | import ( 11 | "encoding/json" 12 | 13 | "github.com/GoSome/fileUpdater/pkg/core" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | type getUpdaterResponse struct { 18 | Updater *core.FileUpdater `json:"updater"` 19 | Content string `json:"content"` 20 | } 21 | 22 | func (a *App) GetUpdater(c *gin.Context) { 23 | name := c.Query("name") 24 | if name == "" { 25 | c.String(404, "Not Found") 26 | return 27 | } 28 | 29 | getConfig, _ := c.Get("cfg") 30 | cfg := getConfig.(core.ServerConfigs) 31 | 32 | u := cfg.GetUpdaterByName(name) 33 | if u == nil { 34 | c.String(404, "Not Found") 35 | return 36 | } 37 | 38 | content, err := u.GetFileContentAsString() 39 | if err != nil { 40 | c.String(400, err.Error()) 41 | return 42 | } 43 | 44 | response := getUpdaterResponse{ 45 | Updater: u, 46 | Content: content, 47 | } 48 | 49 | if err := json.NewEncoder(c.Writer).Encode(&response); err != nil { 50 | c.String(500, "Unknown error: %s", err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/server/getUpdaters.go: -------------------------------------------------------------------------------- 1 | /* 2 | @Description: just go 3 | @Author: skipper 4 | @Date: 2020/1/13 5 | @Time: 5:12 PM 6 | @ProjectName fileUpdater 7 | */ 8 | package server 9 | 10 | import ( 11 | "encoding/json" 12 | "log" 13 | 14 | "github.com/GoSome/fileUpdater/pkg/config" 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | func (a *App) GetUpdaters(c *gin.Context) { 19 | updates := config.Config.FileUpdaters 20 | log.Printf("updates: %v", updates) 21 | c.Header("Content-Type", "application/json") 22 | err := json.NewEncoder(c.Writer).Encode(&updates) 23 | if err != nil { 24 | log.Println("err: ", err.Error()) 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | @Description: just go 3 | @Author: skipper 4 | @Date: 2020/1/13 5 | @Time: 5:13 PM 6 | @ProjectName fileUpdater 7 | */ 8 | package server 9 | 10 | import ( 11 | "io" 12 | "io/fs" 13 | "log" 14 | "net/http" 15 | 16 | "github.com/GoSome/fileUpdater/pkg/config" 17 | "github.com/GoSome/fileUpdater/pkg/core" 18 | "github.com/gin-gonic/gin" 19 | ) 20 | 21 | func Run(cfg core.ServerConfigs) { 22 | app := App{ 23 | Options: cfg, 24 | Engine: gin.Default(), 25 | } 26 | 27 | cfg.RunProcess() 28 | app.Engine.Use(config.Inject) 29 | 30 | app.Engine.GET("/api/updaters", app.GetUpdaters) 31 | app.Engine.GET("/api/updater", app.GetUpdater) 32 | app.Engine.GET("/api/content", app.GetContent) 33 | app.Engine.POST("/api/content", app.UpdateFile) 34 | app.Engine.POST("/api/exec", app.Exec) 35 | if !cfg.DisableUI { 36 | sub, err := fs.Sub(app.Options.HttpData, "build/static") 37 | if err != nil { 38 | panic(err) 39 | } 40 | app.Engine.StaticFS("/static/", http.FS(sub)) 41 | app.Engine.GET("/", app.Index) 42 | app.Engine.NoRoute(app.Index) 43 | 44 | } 45 | log.Fatal(app.Engine.Run(cfg.ServerHost + ":" + cfg.ServerPort)) 46 | } 47 | 48 | func (a *App) Index(c *gin.Context) { 49 | f := a.Options.HttpData 50 | path := c.Request.URL.Path 51 | fileName := "build" + path 52 | if path == "/" { 53 | c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") 54 | fileName = "build/index.html" 55 | } 56 | indexFile, err := f.Open(fileName) 57 | //use frontend route 58 | if err != nil { 59 | c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") 60 | fileName = "build/index.html" 61 | indexFile, _ = f.Open(fileName) 62 | c.Status(http.StatusOK) 63 | io.Copy(c.Writer, indexFile) 64 | return 65 | } 66 | c.Status(http.StatusOK) 67 | io.Copy(c.Writer, indexFile) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/server/setup.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/GoSome/fileUpdater/pkg/core" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | type App struct { 9 | Options core.ServerConfigs 10 | Engine *gin.Engine 11 | } 12 | -------------------------------------------------------------------------------- /pkg/server/update.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "strings" 7 | 8 | "github.com/GoSome/fileUpdater/pkg/core" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type UpdateContentRequest struct { 13 | Name string `json:"name"` 14 | Content string `json:"content"` 15 | } 16 | 17 | func (a *App) UpdateFile(c *gin.Context) { 18 | var req UpdateContentRequest 19 | if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil { 20 | c.String(400, "Unprocessable Data") 21 | return 22 | } 23 | 24 | getConfig, _ := c.Get("cfg") 25 | cfg := getConfig.(core.ServerConfigs) 26 | 27 | updaters := cfg.GetUpdaterByName(req.Name) 28 | if updaters == nil { 29 | c.String(404, "Not Found") 30 | return 31 | } 32 | 33 | // TODO: get file content from FormFile 34 | f := strings.NewReader(req.Content) 35 | if err := updaters.UpdateFile(f); err != nil { 36 | //todo 37 | log.Printf("something bad when update file %s", err) 38 | c.String(400, "%s", err.Error()) 39 | return 40 | } 41 | c.String(200, "all is well") 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /test/api.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8080/api/exec 2 | Content-Type: application/json 3 | 4 | {"shell": "pwd"} 5 | 6 | ### 7 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server_host": "127.0.0.1", 3 | "server_port": "8080", 4 | "updaters": [ 5 | { 6 | "name": "test", 7 | "type": "command", 8 | "file_path": "config.json" 9 | }, 10 | { 11 | "name": "test2", 12 | "type": "command", 13 | "file_path": "/Users/skipper/home/GOGO/gomod/fileUpdater/cmd/server/main.go" 14 | }, 15 | { 16 | "name": "test3", 17 | "type": "command", 18 | "file_path": "/Users/skipper/Dev/golang/gomod/fileUpdater/test/test.json", 19 | "pre_hook": { 20 | "commands": ["ls -lha","date"] 21 | }, 22 | "post_hook": { 23 | "commands": ["pwd"] 24 | } 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /test/config.yaml: -------------------------------------------------------------------------------- 1 | server_port: "8090" 2 | server_host: "127.0.0.1" 3 | updaters: 4 | - name: test1 5 | path: /tmp/test.txt 6 | backup: false 7 | pre_hook: 8 | commands: 9 | - echo `date` > /tmp/test2.txt 10 | - name: test2 11 | path: /tmp/test2.txt 12 | processes: 13 | - command: ping baidu.com 14 | enable: true 15 | log_path: ping.log 16 | - command: ping qq.com 17 | enable: true 18 | log_path: pg.log 19 | - command: fuck 20 | enable: true 21 | -------------------------------------------------------------------------------- /test/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "what": "fuck" 3 | } -------------------------------------------------------------------------------- /ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoSome/fileUpdater/41ffed0e4c7d436c94fac30b00f6fe74e651881a/ui.png -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.9.0", 7 | "@emotion/styled": "^11.8.1", 8 | "@mui/icons-material": "^5.8.2", 9 | "@mui/material": "^5.8.2", 10 | "@reduxjs/toolkit": "^1.8.2", 11 | "@testing-library/jest-dom": "^5.16.4", 12 | "@testing-library/react": "^13.3.0", 13 | "@testing-library/user-event": "^13.5.0", 14 | "@uiw/react-textarea-code-editor": "^2.0.2", 15 | "axios": "^0.27.2", 16 | "http-proxy-middleware": "^2.0.6", 17 | "i": "^0.3.7", 18 | "npm": "^8.12.1", 19 | "prismjs": "^1.28.0", 20 | "react": "^18.1.0", 21 | "react-dom": "^18.1.0", 22 | "react-redux": "^8.0.2", 23 | "react-router-dom": "^6.3.0", 24 | "react-scripts": "5.0.1", 25 | "react-simple-code-editor": "^0.11.2", 26 | "web-vitals": "^2.1.4" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoSome/fileUpdater/41ffed0e4c7d436c94fac30b00f6fe74e651881a/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoSome/fileUpdater/41ffed0e4c7d436c94fac30b00f6fe74e651881a/ui/public/logo192.png -------------------------------------------------------------------------------- /ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoSome/fileUpdater/41ffed0e4c7d436c94fac30b00f6fe74e651881a/ui/public/logo512.png -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ui/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { store } from "./app/store"; 3 | import { Provider } from "react-redux"; 4 | import { DashboardLayout } from "./components/dashboard-layout"; 5 | import { ThemeProvider } from "@mui/material/styles"; 6 | import { theme } from "./theme"; 7 | 8 | function App() { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /ui/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /ui/src/app/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import updaterReducer from '../updaters/updaterSlice' 3 | 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | updater: updaterReducer, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /ui/src/components/dashboard-layout.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Box } from "@mui/material"; 3 | import { styled } from "@mui/material/styles"; 4 | import { DashboardNavbar } from "./dashboard-navbar"; 5 | import { DashboardSidebar } from "./dashboard-sidebar"; 6 | import { EditorMain } from "./editor/editor-layout"; 7 | 8 | const DashboardLayoutRoot = styled("div")(({ theme }) => ({ 9 | display: "flex", 10 | flex: "1 1 auto", 11 | maxWidth: "100%", 12 | paddingTop: 64, 13 | [theme.breakpoints.up("lg")]: { 14 | paddingLeft: 280, 15 | }, 16 | })); 17 | 18 | export const DashboardLayout = props => { 19 | const [isSidebarOpen, setSidebarOpen] = useState(false); 20 | 21 | return ( 22 | <> 23 | 24 | 32 | 33 | 34 | 35 | 36 | 37 | setSidebarOpen(true)} /> 38 | setSidebarOpen(false)} 40 | open={isSidebarOpen} 41 | /> 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /ui/src/components/dashboard-navbar.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import styled from "@emotion/styled"; 3 | import { AppBar, Box, IconButton, Toolbar } from "@mui/material"; 4 | import MenuIcon from "@mui/icons-material/Menu"; 5 | 6 | const DashboardNavbarRoot = styled(AppBar)(({ theme }) => ({ 7 | backgroundColor: theme.palette.background.paper, 8 | boxShadow: theme.shadows[3], 9 | })); 10 | 11 | export const DashboardNavbar = props => { 12 | const { onSidebarOpen, ...other } = props; 13 | 14 | return ( 15 | <> 16 | 27 | 35 | { 37 | onSidebarOpen(); 38 | }} 39 | sx={{ 40 | display: { 41 | xs: "inline-flex", 42 | lg: "none", 43 | }, 44 | }} 45 | > 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | DashboardNavbar.propTypes = { 57 | onSidebarOpen: PropTypes.func, 58 | }; 59 | -------------------------------------------------------------------------------- /ui/src/components/dashboard-sidebar.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | // import { useRouter } from "next/router"; 3 | import PropTypes from "prop-types"; 4 | import { Box, Divider, Drawer, Typography, useMediaQuery } from "@mui/material"; 5 | import { NavItem } from "./nav-item"; 6 | import ArticleIcon from "@mui/icons-material/Article"; 7 | import { useSelector } from "react-redux"; 8 | 9 | const axios = require("axios"); 10 | 11 | const GetAllUpdatersURl = "/api/updaters"; 12 | 13 | export const DashboardSidebar = props => { 14 | const { open, onClose } = props; 15 | // const router = useRouter(); 16 | const lgUp = useMediaQuery(theme => theme.breakpoints.up("lg"), { 17 | defaultMatches: true, 18 | noSsr: false, 19 | }); 20 | const [updaters, setUpdaters] = useState([]); 21 | const value = useSelector(state => state.updater.value); 22 | const reFlash = useSelector(state => state.updater.reFlash); 23 | 24 | useEffect(() => { 25 | axios.get(GetAllUpdatersURl).then(function (response) { 26 | setUpdaters(response.data); 27 | }); 28 | }, [reFlash]); 29 | 30 | useEffect( 31 | () => { 32 | if (!lgUp) { 33 | onClose(); 34 | } 35 | }, 36 | // eslint-disable-next-line react-hooks/exhaustive-deps 37 | [value] 38 | ); 39 | 40 | const content = ( 41 | <> 42 | 49 | 50 | 51 | FileUpdater 52 | 53 | 54 | 61 | 62 | {updaters.map(item => ( 63 | } 67 | filePath={item.path} 68 | /> 69 | ))} 70 | 71 | 72 | 73 | ); 74 | 75 | if (lgUp) { 76 | return ( 77 | 89 | {content} 90 | 91 | ); 92 | } 93 | 94 | return ( 95 | theme.zIndex.appBar + 100 }} 107 | variant="temporary" 108 | > 109 | {content} 110 | 111 | ); 112 | }; 113 | 114 | DashboardSidebar.propTypes = { 115 | onClose: PropTypes.func, 116 | open: PropTypes.bool, 117 | }; 118 | -------------------------------------------------------------------------------- /ui/src/components/editor/Editor.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEffect } from "react"; 3 | import "@uiw/react-textarea-code-editor/dist.css"; 4 | //redux 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { setContent } from "../../updaters/updaterSlice"; 7 | import CodeEditor from "@uiw/react-textarea-code-editor"; 8 | 9 | const axios = require("axios").default; 10 | 11 | function MyCodeEditor(props) { 12 | const dispatch = useDispatch(); 13 | const fileName = useSelector(state => state.updater.name); 14 | const language = useSelector(state => state.updater.language); 15 | const content = useSelector(state => state.updater.content); 16 | 17 | 18 | useEffect(() => { 19 | async function getContent() { 20 | try { 21 | const res = await axios.get("/api/content", { 22 | params: { name: fileName }, 23 | }); 24 | const { content } = await res.data; 25 | dispatch(setContent(content)); 26 | } catch (error) { 27 | console.error(error); 28 | } 29 | } 30 | getContent(); 31 | }, [fileName, dispatch]); 32 | 33 | return ( 34 |
35 | dispatch(setContent(evn.target.value))} 40 | autoFocus 41 | padding={15} 42 | disabled={props.disabled} 43 | style={{ 44 | fontSize: 16, 45 | backgroundColor: "#111827", 46 | color: "white", 47 | fontFamily: 48 | "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", 49 | }} 50 | /> 51 |
52 | ); 53 | } 54 | 55 | export default MyCodeEditor; 56 | -------------------------------------------------------------------------------- /ui/src/components/editor/editor-layout.js: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | Card, 5 | CardHeader, 6 | Divider, 7 | InputLabel, 8 | NativeSelect, 9 | Snackbar, 10 | Alert, 11 | } from "@mui/material"; 12 | 13 | import FormControl from "@mui/material/FormControl"; 14 | import MyCodeEditor from "./Editor"; 15 | import { useSelector, useDispatch } from "react-redux"; 16 | import { setLanguage, setReFlash } from "../../updaters/updaterSlice"; 17 | import { useState } from "react"; 18 | 19 | const axios = require("axios").default; 20 | 21 | const EditorLanguages = ["plaintext", "ini", "yaml", "json"]; 22 | 23 | export const EditorMain = props => { 24 | const [alertContent, setAlertContent] = useState("saved"); 25 | const [level, setLevel] = useState("success"); 26 | const [open, setOpen] = useState(false); 27 | 28 | const RaiseAlert = () => { 29 | setOpen(true); 30 | }; 31 | 32 | const handleClose = (event, reason) => { 33 | if (reason === "clickaway") { 34 | return; 35 | } 36 | setOpen(false); 37 | }; 38 | 39 | const name = useSelector(state => state.updater.name); 40 | const filePath = useSelector(state => state.updater.filePath); 41 | 42 | const content = useSelector(state => state.updater.content); 43 | 44 | const dispatch = useDispatch(); 45 | const handleSave = e => { 46 | e.preventDefault(); 47 | 48 | const j = JSON.stringify({ name: name, content: content }); 49 | axios 50 | .post("/api/content", j) 51 | .then(function (response) { 52 | if (response.status === 200) { 53 | setAlertContent("saved"); 54 | setLevel("success"); 55 | RaiseAlert(); 56 | dispatch(setReFlash()); 57 | } else { 58 | setAlertContent("save file failed! status:" + response.status); 59 | setLevel("error"); 60 | RaiseAlert(); 61 | } 62 | }) 63 | .catch(function (error) { 64 | setAlertContent("error! " + error); 65 | setLevel("error"); 66 | RaiseAlert(); 67 | }) 68 | .then(function () { 69 | // always executed 70 | }); 71 | }; 72 | 73 | return ( 74 |
75 | 76 | 86 | 87 | 94 | 95 | 96 | language 97 | 98 | { 100 | dispatch(setLanguage(evn.target.value)); 101 | }} 102 | > 103 | {EditorLanguages.map(lang => ( 104 | 107 | ))} 108 | 109 | 110 | 111 | 112 | 113 | 120 | 130 | 137 | {alertContent} 138 | 139 | 140 | 141 |
142 | ); 143 | }; 144 | -------------------------------------------------------------------------------- /ui/src/components/nav-item.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { Box, Button, ListItem } from "@mui/material"; 3 | 4 | //redux 5 | import { setName, setFilePath, increment } from "../updaters/updaterSlice"; 6 | import { useDispatch, useSelector } from "react-redux"; 7 | 8 | export const NavItem = props => { 9 | const { href, icon, title, filePath, ...others } = props; 10 | const name = useSelector(state => state.updater.name); 11 | const active = name ? title === name : false; 12 | const dispatch = useDispatch(); 13 | 14 | return ( 15 | 25 | 55 | 56 | ); 57 | }; 58 | 59 | NavItem.propTypes = { 60 | href: PropTypes.string, 61 | icon: PropTypes.node, 62 | title: PropTypes.string, 63 | }; 64 | -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /ui/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /ui/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const { createProxyMiddleware } = require('http-proxy-middleware'); 2 | 3 | module.exports = function(app) { 4 | app.use( 5 | '/api', 6 | createProxyMiddleware({ 7 | target: 'http://192.168.2.5:8090', 8 | changeOrigin: true, 9 | }) 10 | ); 11 | }; -------------------------------------------------------------------------------- /ui/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /ui/src/theme/index.js: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material'; 2 | 3 | export const theme = createTheme({ 4 | breakpoints: { 5 | values: { 6 | xs: 0, 7 | sm: 600, 8 | md: 1000, 9 | lg: 1200, 10 | xl: 1920 11 | } 12 | }, 13 | components: { 14 | MuiButton: { 15 | defaultProps: { 16 | disableElevation: true 17 | }, 18 | styleOverrides: { 19 | root: { 20 | textTransform: 'none' 21 | }, 22 | sizeSmall: { 23 | padding: '6px 16px' 24 | }, 25 | sizeMedium: { 26 | padding: '8px 20px' 27 | }, 28 | sizeLarge: { 29 | padding: '11px 24px' 30 | }, 31 | textSizeSmall: { 32 | padding: '7px 12px' 33 | }, 34 | textSizeMedium: { 35 | padding: '9px 16px' 36 | }, 37 | textSizeLarge: { 38 | padding: '12px 16px' 39 | } 40 | } 41 | }, 42 | MuiButtonBase: { 43 | defaultProps: { 44 | disableRipple: true 45 | } 46 | }, 47 | MuiCardContent: { 48 | styleOverrides: { 49 | root: { 50 | padding: '32px 24px', 51 | '&:last-child': { 52 | paddingBottom: '32px' 53 | } 54 | } 55 | } 56 | }, 57 | MuiCardHeader: { 58 | defaultProps: { 59 | titleTypographyProps: { 60 | variant: 'h6' 61 | }, 62 | subheaderTypographyProps: { 63 | variant: 'body2' 64 | } 65 | }, 66 | styleOverrides: { 67 | root: { 68 | padding: '32px 24px' 69 | } 70 | } 71 | }, 72 | MuiCssBaseline: { 73 | styleOverrides: { 74 | '*': { 75 | boxSizing: 'border-box', 76 | margin: 0, 77 | padding: 0 78 | }, 79 | html: { 80 | MozOsxFontSmoothing: 'grayscale', 81 | WebkitFontSmoothing: 'antialiased', 82 | display: 'flex', 83 | flexDirection: 'column', 84 | minHeight: '100%', 85 | width: '100%' 86 | }, 87 | body: { 88 | display: 'flex', 89 | flex: '1 1 auto', 90 | flexDirection: 'column', 91 | minHeight: '100%', 92 | width: '100%' 93 | }, 94 | '#__next': { 95 | display: 'flex', 96 | flex: '1 1 auto', 97 | flexDirection: 'column', 98 | height: '100%', 99 | width: '100%' 100 | } 101 | } 102 | }, 103 | MuiOutlinedInput: { 104 | styleOverrides: { 105 | notchedOutline: { 106 | borderColor: '#E6E8F0' 107 | } 108 | } 109 | }, 110 | MuiTableHead: { 111 | styleOverrides: { 112 | root: { 113 | backgroundColor: '#F3F4F6', 114 | '.MuiTableCell-root': { 115 | color: '#374151' 116 | }, 117 | borderBottom: 'none', 118 | '& .MuiTableCell-root': { 119 | borderBottom: 'none', 120 | fontSize: '12px', 121 | fontWeight: 600, 122 | lineHeight: 1, 123 | letterSpacing: 0.5, 124 | textTransform: 'uppercase' 125 | }, 126 | '& .MuiTableCell-paddingCheckbox': { 127 | paddingTop: 4, 128 | paddingBottom: 4 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | palette: { 135 | neutral: { 136 | 100: '#F3F4F6', 137 | 200: '#E5E7EB', 138 | 300: '#D1D5DB', 139 | 400: '#9CA3AF', 140 | 500: '#6B7280', 141 | 600: '#4B5563', 142 | 700: '#374151', 143 | 800: '#1F2937', 144 | 900: '#111827' 145 | }, 146 | action: { 147 | active: '#6B7280', 148 | focus: 'rgba(55, 65, 81, 0.12)', 149 | hover: 'rgba(55, 65, 81, 0.04)', 150 | selected: 'rgba(55, 65, 81, 0.08)', 151 | disabledBackground: 'rgba(55, 65, 81, 0.12)', 152 | disabled: 'rgba(55, 65, 81, 0.26)' 153 | }, 154 | background: { 155 | default: '#F9FAFC', 156 | paper: '#FFFFFF' 157 | }, 158 | divider: '#E6E8F0', 159 | primary: { 160 | main: '#5048E5', 161 | light: '#828DF8', 162 | dark: '#3832A0', 163 | contrastText: '#FFFFFF' 164 | }, 165 | secondary: { 166 | main: '#10B981', 167 | light: '#3FC79A', 168 | dark: '#0B815A', 169 | contrastText: '#FFFFFF' 170 | }, 171 | success: { 172 | main: '#14B8A6', 173 | light: '#43C6B7', 174 | dark: '#0E8074', 175 | contrastText: '#FFFFFF' 176 | }, 177 | info: { 178 | main: '#2196F3', 179 | light: '#64B6F7', 180 | dark: '#0B79D0', 181 | contrastText: '#FFFFFF' 182 | }, 183 | warning: { 184 | main: '#FFB020', 185 | light: '#FFBF4C', 186 | dark: '#B27B16', 187 | contrastText: '#FFFFFF' 188 | }, 189 | error: { 190 | main: '#D14343', 191 | light: '#DA6868', 192 | dark: '#922E2E', 193 | contrastText: '#FFFFFF' 194 | }, 195 | text: { 196 | primary: '#121828', 197 | secondary: '#65748B', 198 | disabled: 'rgba(55, 65, 81, 0.48)' 199 | } 200 | }, 201 | shape: { 202 | borderRadius: 8 203 | }, 204 | shadows: [ 205 | 'none', 206 | '0px 1px 1px rgba(100, 116, 139, 0.06), 0px 1px 2px rgba(100, 116, 139, 0.1)', 207 | '0px 1px 2px rgba(100, 116, 139, 0.12)', 208 | '0px 1px 4px rgba(100, 116, 139, 0.12)', 209 | '0px 1px 5px rgba(100, 116, 139, 0.12)', 210 | '0px 1px 6px rgba(100, 116, 139, 0.12)', 211 | '0px 2px 6px rgba(100, 116, 139, 0.12)', 212 | '0px 3px 6px rgba(100, 116, 139, 0.12)', 213 | '0px 2px 4px rgba(31, 41, 55, 0.06), 0px 4px 6px rgba(100, 116, 139, 0.12)', 214 | '0px 5px 12px rgba(100, 116, 139, 0.12)', 215 | '0px 5px 14px rgba(100, 116, 139, 0.12)', 216 | '0px 5px 15px rgba(100, 116, 139, 0.12)', 217 | '0px 6px 15px rgba(100, 116, 139, 0.12)', 218 | '0px 7px 15px rgba(100, 116, 139, 0.12)', 219 | '0px 8px 15px rgba(100, 116, 139, 0.12)', 220 | '0px 9px 15px rgba(100, 116, 139, 0.12)', 221 | '0px 10px 15px rgba(100, 116, 139, 0.12)', 222 | '0px 12px 22px -8px rgba(100, 116, 139, 0.25)', 223 | '0px 13px 22px -8px rgba(100, 116, 139, 0.25)', 224 | '0px 14px 24px -8px rgba(100, 116, 139, 0.25)', 225 | '0px 10px 10px rgba(31, 41, 55, 0.04), 0px 20px 25px rgba(31, 41, 55, 0.1)', 226 | '0px 25px 50px rgba(100, 116, 139, 0.25)', 227 | '0px 25px 50px rgba(100, 116, 139, 0.25)', 228 | '0px 25px 50px rgba(100, 116, 139, 0.25)', 229 | '0px 25px 50px rgba(100, 116, 139, 0.25)' 230 | ], 231 | typography: { 232 | button: { 233 | fontWeight: 600 234 | }, 235 | fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"', 236 | body1: { 237 | fontSize: '1rem', 238 | fontWeight: 400, 239 | lineHeight: 1.5 240 | }, 241 | body2: { 242 | fontSize: '0.875rem', 243 | fontWeight: 400, 244 | lineHeight: 1.57 245 | }, 246 | subtitle1: { 247 | fontSize: '1rem', 248 | fontWeight: 500, 249 | lineHeight: 1.75 250 | }, 251 | subtitle2: { 252 | fontSize: '0.875rem', 253 | fontWeight: 500, 254 | lineHeight: 1.57 255 | }, 256 | overline: { 257 | fontSize: '0.75rem', 258 | fontWeight: 600, 259 | letterSpacing: '0.5px', 260 | lineHeight: 2.5, 261 | textTransform: 'uppercase' 262 | }, 263 | caption: { 264 | fontSize: '0.75rem', 265 | fontWeight: 400, 266 | lineHeight: 1.66 267 | }, 268 | h1: { 269 | fontWeight: 700, 270 | fontSize: '3.5rem', 271 | lineHeight: 1.375 272 | }, 273 | h2: { 274 | fontWeight: 700, 275 | fontSize: '3rem', 276 | lineHeight: 1.375 277 | }, 278 | h3: { 279 | fontWeight: 700, 280 | fontSize: '2.25rem', 281 | lineHeight: 1.375 282 | }, 283 | h4: { 284 | fontWeight: 700, 285 | fontSize: '2rem', 286 | lineHeight: 1.375 287 | }, 288 | h5: { 289 | fontWeight: 600, 290 | fontSize: '1.5rem', 291 | lineHeight: 1.375 292 | }, 293 | h6: { 294 | fontWeight: 600, 295 | fontSize: '1.125rem', 296 | lineHeight: 1.375 297 | } 298 | } 299 | }); 300 | -------------------------------------------------------------------------------- /ui/src/updaters/updaterSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | value: 0, 5 | content: "", 6 | name: "", 7 | language: "plaintext", 8 | filePath: "", 9 | reFlash: 0, 10 | }; 11 | 12 | export const updaterSlice = createSlice({ 13 | name: "updater", 14 | initialState, 15 | reducers: { 16 | increment: state => { 17 | // Redux Toolkit allows us to write "mutating" logic in reducers. It 18 | // doesn't actually mutate the state because it uses the Immer library, 19 | // which detects changes to a "draft state" and produces a brand new 20 | // immutable state based off those changes 21 | state.value += 1; 22 | }, 23 | decrement: state => { 24 | state.value -= 1; 25 | }, 26 | setContent: (state, action) => { 27 | state.content = action.payload; 28 | }, 29 | setName: (state, action) => { 30 | state.name = action.payload; 31 | }, 32 | setFilePath: (state, action) => { 33 | state.filePath = action.payload; 34 | }, 35 | setLanguage: (state, action) => { 36 | state.language = action.payload; 37 | }, 38 | incrementByAmount: (state, action) => { 39 | state.value += action.payload; 40 | }, 41 | setReFlash: state => { 42 | state.reFlash += 1; 43 | }, 44 | }, 45 | }); 46 | 47 | // Action creators are generated for each case reducer function 48 | export const { 49 | increment, 50 | setLanguage, 51 | setContent, 52 | setName, 53 | setFilePath, 54 | setReFlash, 55 | } = updaterSlice.actions; 56 | 57 | export default updaterSlice.reducer; 58 | --------------------------------------------------------------------------------