├── .github └── workflows │ └── release.yml ├── .gitignore ├── Makefile ├── README.md ├── go.mod ├── go.sum └── yuque.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 5 | name: Build release 6 | jobs: 7 | build: 8 | name: Build And Upload Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Set up Go 1.13 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: 1.13 15 | 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Create Release ${{ github.ref }} 20 | id: create_release 21 | uses: actions/create-release@v1.0.0 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | tag_name: ${{ github.ref }} 26 | release_name: Release ${{ github.ref }} 27 | draft: false 28 | prerelease: false 29 | 30 | - name: Build All Release 31 | run: | 32 | make clean 33 | make vet 34 | make build_all 35 | - name: Upload file to release 36 | uses: labulaka521/upload-release-action@v1-release 37 | with: 38 | repo_token: ${{ secrets.GITHUB_TOKEN }} 39 | file: build/* 40 | tag: ${{ github.ref }} 41 | overwrite: true 42 | file_glob: true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build_dir=build 2 | appname := yuquesync 3 | 4 | sources := $(wildcard *.go) 5 | 6 | build = GOOS=$(1) GOARCH=$(2) go build -o ${build_dir}/$(appname)-$(1)-$(2) 7 | md5 = md5sum ${build_dir}/$(appname)-$(1)-$(2) > ${build_dir}/$(appname)-$(1)-$(2)_checksum.txt 8 | tar = tar -cvzf ${build_dir}/$(appname)-$(1)-$(2).tar.gz -C ${build_dir} $(appname)-$(1)-$(2) $(appname)-$(1)-$(2)_checksum.txt 9 | delete = rm -rf ${build_dir}/$(appname)-$(1)-$(2) ${build_dir}/$(appname)-$(1)-$(2)_checksum.txt 10 | ALL_LINUX = linux-amd64 \ 11 | linux-386 \ 12 | linux-arm \ 13 | linux-arm64 14 | 15 | ALL = $(ALL_LINUX) \ 16 | darwin-amd64 \ 17 | windows-amd64 18 | 19 | build_linux: $(ALL_LINUX:%=build/%) 20 | 21 | build_all: $(ALL:%=build/%) 22 | 23 | build/%: 24 | $(call build,$(firstword $(subst -, , $*)),$(word 2, $(subst -, ,$*))) 25 | $(call md5,$(firstword $(subst -, , $*)),$(word 2, $(subst -, ,$*))) 26 | $(call tar,$(firstword $(subst -, , $*)),$(word 2, $(subst -, ,$*))) 27 | $(call delete,$(firstword $(subst -, , $*)),$(word 2, $(subst -, ,$*))) 28 | clean: 29 | rm -rf ${build_dir}/* 30 | 31 | vet: 32 | go vet yuque.go -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 将语雀指定知识库的文章同步到Hugo中 2 | 3 | 在hugo的配置文件中加入下面的配置,然后下载yuque_sync到hugo的源代码目录,在hugo命令之前执行yuque_sync就可以将文章同步过去, 4 | 点击进入知识库后查看类似这样的URL https://www.yuque.com/[user]/[kb] 5 | 其中`user`和`kb`分别对应下面配置的对应项,`token`是如果知识库是私密的就需要token,否则可以留空白 6 | ```toml 7 | [yuque-sync] 8 | # 用户名 9 | user = "" 10 | # 知识库路径 11 | kb = "" 12 | # 私密仓库需要token 13 | token = "" 14 | # api 15 | api = "https://www.yuque.com/api/v2" 16 | # port 17 | port = 8081 18 | # sync after command 19 | aftercmd = "" 20 | # markdown文件目录文件路径为content/${article} 21 | article = "post" 22 | ``` 23 | 请在`hugo博客主目录`运行,同步后的文章会存储在`content/post`,如果你在文章中使用了语雀来插入图片,那么图片会被下载到本地的`content/images/`目录下,并且同步的文档里的图片链接会被替换为`/image/imagepath.png`,请将`hugo`命令加入环境变量中 24 | web 25 | 语雀还可以设置webhook,所以可以结合travis来触发自动拉取最新的文章后自动构建博客 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module yuquesync 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/creack/pty v1.1.9 // indirect 8 | github.com/kr/pty v1.1.8 // indirect 9 | github.com/rogpeppe/go-internal v1.5.0 // indirect 10 | github.com/stretchr/objx v0.2.0 // indirect 11 | github.com/wujiyu115/yuqueg v0.0.0-20191029130010-467e2b959872 12 | go.uber.org/zap v1.12.0 // indirect 13 | golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf // indirect 14 | golang.org/x/mod v0.1.0 // indirect 15 | golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 // indirect 16 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 17 | golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect 18 | golang.org/x/text v0.3.2 // indirect 19 | golang.org/x/tools v0.0.0-20191031144223-d9fd88a569ec // indirect 20 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect 21 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 22 | gopkg.in/yaml.v2 v2.2.4 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 8 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 10 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 11 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 16 | github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 19 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 20 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 21 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 22 | github.com/wujiyu115/yuqueg v0.0.0-20191029130010-467e2b959872 h1:YRsfjjX+uYn6FzDuEFJCskTME3zshGAR7ViQITJeEjA= 23 | github.com/wujiyu115/yuqueg v0.0.0-20191029130010-467e2b959872/go.mod h1:5MVx9onoToEJUhb2vzzY1fiHhCSFyqZ1T2PHOmlLbgQ= 24 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 25 | go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= 26 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 27 | go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 28 | go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= 29 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 30 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 31 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 32 | go.uber.org/zap v1.12.0 h1:dySoUQPFBGj6xwjmBzageVL8jGi8uxc6bEmJQjA06bw= 33 | go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 34 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 35 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 36 | golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 37 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 38 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 39 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 40 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 44 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 45 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 46 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 47 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 50 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 51 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 52 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 53 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 54 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 55 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 56 | golang.org/x/tools v0.0.0-20191031144223-d9fd88a569ec/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 57 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 58 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 62 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 63 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 64 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 65 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 66 | -------------------------------------------------------------------------------- /yuque.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "regexp" 15 | "strings" 16 | "time" 17 | 18 | "github.com/BurntSushi/toml" 19 | ) 20 | 21 | var ( 22 | yq yuquesync 23 | ) 24 | 25 | const ( 26 | hugocfg = "config.toml" 27 | mainpath = "content" 28 | imagepath = "images" 29 | hugocmd = "hugo" 30 | ) 31 | 32 | // Docs docs 33 | type Docs struct { 34 | Data []data `json:"data"` 35 | } 36 | 37 | type data struct { 38 | ID int `json:"id"` 39 | Body string `json:"body,omitempty"` 40 | PublishedAt string `json:"published_at"` 41 | Title string `json:"title"` 42 | } 43 | 44 | // DocsContent docs content 45 | type DocsContent struct { 46 | Data data `json:"data"` 47 | } 48 | 49 | // ReqGet request 50 | func ReqGet(uri string) ([]byte, error) { 51 | url := yq.YuQue.API + uri 52 | log.Println("Start Req Url: ", url) 53 | r, err := http.NewRequest(http.MethodGet, url, nil) 54 | if err != nil { 55 | return nil, err 56 | } 57 | r.Header.Add("Content-Type", "application/json") 58 | r.Header.Add("User-Agent", "blog") 59 | if yq.YuQue.Token != "" { 60 | r.Header.Add("X-Auth-Token", yq.YuQue.Token) 61 | } 62 | client := http.Client{Timeout: 60 * time.Second} 63 | 64 | resp, err := client.Do(r) 65 | if err != nil { 66 | return nil, err 67 | } 68 | defer resp.Body.Close() 69 | if resp.StatusCode != 200 { 70 | return nil, fmt.Errorf("Err Status Code: %d", resp.StatusCode) 71 | } 72 | 73 | body, err := ioutil.ReadAll(resp.Body) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return body, nil 78 | } 79 | 80 | // 获取所有的文章ID 81 | func getAllDocs(user string, path string) (*Docs, error) { 82 | alldocsid := Docs{} 83 | uri := fmt.Sprintf("/repos/%s/%s/docs/", user, path) 84 | 85 | body, err := ReqGet(uri) 86 | if err != nil { 87 | return nil, err 88 | } 89 | err = json.Unmarshal(body, &alldocsid) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return &alldocsid, nil 94 | } 95 | 96 | func getDocsDetail(user, kb string, id int) (*DocsContent, error) { 97 | if user == "" || kb == "" { 98 | return nil, errors.New("Please input user and docs path") 99 | } 100 | uri := fmt.Sprintf("/repos/%s/%s/docs/%d?raw=1", user, kb, id) 101 | docscontent := DocsContent{} 102 | 103 | body, err := ReqGet(uri) 104 | if err != nil { 105 | return nil, err 106 | } 107 | err = json.Unmarshal(body, &docscontent) 108 | if err != nil { 109 | return nil, err 110 | } 111 | return &docscontent, nil 112 | } 113 | 114 | func downloadDocs(storepath string, docs *DocsContent) error { 115 | err := replaceimageurl(docs) 116 | if err != nil { 117 | log.Printf("replaceimageurl failed: %+v\n", err) 118 | return err 119 | } 120 | var buf bytes.Buffer 121 | if !strings.HasPrefix(docs.Data.Body, "---") { 122 | buf.WriteString("---\n") 123 | buf.WriteString(fmt.Sprintf("title: \"%s\"\n", docs.Data.Title)) 124 | buf.WriteString(fmt.Sprintf("date: %s\n", docs.Data.PublishedAt)) 125 | buf.WriteString("draft: false\n") 126 | buf.WriteString("---\n") 127 | buf.WriteString("\n") 128 | buf.WriteString("\n") 129 | } 130 | 131 | buf.WriteString(docs.Data.Body) 132 | 133 | file, err := os.Create(storepath) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | _, err = file.Write(buf.Bytes()) 139 | if err != nil { 140 | return err 141 | } 142 | return file.Sync() 143 | } 144 | 145 | // 替换语雀的照片 146 | func replaceimageurl(docs *DocsContent) error { 147 | replregex, err := regexp.Compile(`\((https://cdn\.nlark\.com/yuque.*?\))`) 148 | if err != nil { 149 | return err 150 | } 151 | imagenumber := 1 152 | for { 153 | url := replregex.FindAllString(docs.Data.Body, 1) 154 | if len(url) == 0 || len(url) != 1 { 155 | break 156 | } 157 | 158 | imageallpath := fmt.Sprintf("%s/%s/%d_%d.png", mainpath, imagepath, docs.Data.ID, imagenumber) 159 | log.Printf("Start Replace %s image\n", imageallpath) 160 | err := downimage(imageallpath, url[0]) 161 | if err != nil { 162 | log.Printf("downimage failed url: %s err: %+v\n", url[0], err) 163 | continue 164 | } 165 | docs.Data.Body = strings.Replace(docs.Data.Body, url[0], fmt.Sprintf("(/%s/%d_%d.png)", imagepath, docs.Data.ID, imagenumber), 1) 166 | imagenumber++ 167 | } 168 | log.Println("Replace All Image success") 169 | return nil 170 | } 171 | 172 | // down picture to disk 173 | func downimage(picpath, url string) error { 174 | url = strings.TrimLeft(url, "(") 175 | url = strings.TrimRight(url, ")") 176 | resp, err := http.Get(url) 177 | if err != nil { 178 | return err 179 | } 180 | defer resp.Body.Close() 181 | file, err := os.Create(picpath) 182 | if err != nil { 183 | return err 184 | } 185 | defer file.Close() 186 | _, err = io.Copy(file, resp.Body) 187 | if err != nil { 188 | return err 189 | } 190 | return nil 191 | } 192 | 193 | func mkdirpath(dir string) error { 194 | 195 | if _, err := os.Stat(dir); os.IsNotExist(err) { 196 | err = os.MkdirAll(dir, 0755) 197 | if err != nil { 198 | log.Printf("MkdirAll dir %s failed: %+v\n", dir, err) 199 | return err 200 | } 201 | } 202 | return nil 203 | } 204 | 205 | func httpwebhook() { 206 | http.HandleFunc("/yuque", func(w http.ResponseWriter, r *http.Request) { 207 | body, err := ioutil.ReadAll(r.Body) 208 | if err != nil { 209 | log.Printf("ReadAll body: %+v\n", err) 210 | w.WriteHeader(500) 211 | return 212 | } 213 | docscontent := DocsContent{} 214 | err = json.Unmarshal(body, &docscontent) 215 | if err != nil { 216 | log.Printf("Unmarshal body: %+v\n", err) 217 | w.WriteHeader(500) 218 | return 219 | } 220 | downfilepath := fmt.Sprintf("%s/%s/%d.md", mainpath, yq.YuQue.Article, docscontent.Data.ID) 221 | err = downloadDocs(downfilepath, &docscontent) 222 | if err != nil { 223 | log.Printf("Download Title %s Err: %v", docscontent.Data.Title, err) 224 | w.WriteHeader(500) 225 | return 226 | } 227 | runhugocmd() 228 | }) 229 | addr := fmt.Sprintf(":%d", yq.YuQue.Port) 230 | log.Printf("Start Webhooks API %s/yuque", addr) 231 | log.Fatal(http.ListenAndServe(addr, nil)) 232 | } 233 | 234 | func runhugocmd() { 235 | cmds := []string{hugocmd} 236 | if yq.YuQue.AfterCmd != "" { 237 | cmds = append(cmds, yq.YuQue.AfterCmd) 238 | } 239 | for _, cmd := range cmds { 240 | if cmd == "" { 241 | continue 242 | } 243 | cmd := exec.Command("bash", "-c", cmd) 244 | err := cmd.Run() 245 | if err != nil { 246 | log.Printf("run hugo command failed: %+v\n", err) 247 | } 248 | } 249 | 250 | } 251 | 252 | type yuque struct { 253 | User string `toml:"user"` 254 | Kb string `toml:"kb"` 255 | Token string `toml:"token"` 256 | API string `toml:"api"` 257 | Port int `toml:"port"` 258 | Article string `toml:"article"` 259 | AfterCmd string `toml:"aftercmd"` 260 | } 261 | 262 | type yuquesync struct { 263 | YuQue yuque `toml:"yuque-sync"` 264 | } 265 | 266 | func main() { 267 | _, err := toml.DecodeFile(hugocfg, &yq) 268 | if err != nil { 269 | log.Fatal(err) 270 | } 271 | 272 | if yq.YuQue.User == "" || yq.YuQue.Kb == "" { 273 | log.Fatalln("Please input user and docs path") 274 | } 275 | err = mkdirpath(fmt.Sprintf("%s/%s", mainpath, imagepath)) 276 | if err != nil { 277 | log.Fatalln(err) 278 | } 279 | err = mkdirpath(fmt.Sprintf("%s/%s", mainpath, yq.YuQue.Article)) 280 | if err != nil { 281 | log.Fatalln(err) 282 | } 283 | 284 | docs, err := getAllDocs(yq.YuQue.User, yq.YuQue.Kb) 285 | if err != nil { 286 | log.Fatal("Get All Docs Id Failed: ", err) 287 | } 288 | log.Println("Total Get Article", len(docs.Data)) 289 | 290 | for _, docs := range docs.Data { 291 | time.Sleep(time.Millisecond * 300) 292 | res, err := getDocsDetail(yq.YuQue.User, yq.YuQue.Kb, docs.ID) 293 | if err != nil { 294 | log.Println("Get Docs content Err", err.Error()) 295 | continue 296 | } 297 | downfilepath := fmt.Sprintf("%s/%s/%d.md", mainpath, yq.YuQue.Article, docs.ID) 298 | err = downloadDocs(downfilepath, res) 299 | if err != nil { 300 | log.Printf("Download Title %s Err: %v", res.Data.Title, err) 301 | } 302 | 303 | } 304 | 305 | runhugocmd() 306 | httpwebhook() 307 | } 308 | --------------------------------------------------------------------------------