package.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. // SiYuan - Build Your Eternal Digital Garden
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package bazaar
  17. import (
  18. "bytes"
  19. "errors"
  20. "os"
  21. "path/filepath"
  22. "strings"
  23. "sync"
  24. "time"
  25. "github.com/88250/gulu"
  26. "github.com/88250/lute"
  27. "github.com/PuerkitoBio/goquery"
  28. "github.com/araddon/dateparse"
  29. "github.com/imroc/req/v3"
  30. "github.com/siyuan-note/httpclient"
  31. "github.com/siyuan-note/logging"
  32. "github.com/siyuan-note/siyuan/kernel/util"
  33. textUnicode "golang.org/x/text/encoding/unicode"
  34. "golang.org/x/text/transform"
  35. )
  36. func GetPackageREADME(repoURL, repoHash string, systemID string) (ret string) {
  37. repoURLHash := repoURL + "@" + repoHash
  38. data, err := downloadPackage(repoURLHash+"/README.md", false, systemID)
  39. if nil != err {
  40. ret = "Load bazaar package's README.md failed: " + err.Error()
  41. return
  42. }
  43. if 2 < len(data) {
  44. if 255 == data[0] && 254 == data[1] {
  45. data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.LittleEndian, textUnicode.ExpectBOM).NewDecoder(), data)
  46. } else if 254 == data[1] && 255 == data[0] {
  47. data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.ExpectBOM).NewDecoder(), data)
  48. }
  49. }
  50. luteEngine := lute.New()
  51. luteEngine.SetSoftBreak2HardBreak(false)
  52. luteEngine.SetCodeSyntaxHighlight(false)
  53. linkBase := repoURL + "/blob/main/"
  54. luteEngine.SetLinkBase(linkBase)
  55. ret = luteEngine.Md2HTML(string(data))
  56. doc, err := goquery.NewDocumentFromReader(strings.NewReader(ret))
  57. if nil != err {
  58. logging.LogErrorf("parse HTML failed: %s", err)
  59. return ret
  60. }
  61. doc.Find("a").Each(func(i int, selection *goquery.Selection) {
  62. if href, ok := selection.Attr("href"); ok && util.IsRelativePath(href) {
  63. selection.SetAttr("href", linkBase+href)
  64. }
  65. })
  66. ret, _ = doc.Find("body").Html()
  67. return
  68. }
  69. func downloadPackage(repoURLHash string, pushProgress bool, systemID string) (data []byte, err error) {
  70. // repoURLHash: https://github.com/88250/Comfortably-Numb@6286912c381ef3f83e455d06ba4d369c498238dc
  71. pushID := repoURLHash[:strings.LastIndex(repoURLHash, "@")]
  72. repoURLHash = strings.TrimPrefix(repoURLHash, "https://github.com/")
  73. u := util.BazaarOSSServer + "/package/" + repoURLHash
  74. buf := &bytes.Buffer{}
  75. resp, err := httpclient.NewBrowserDownloadRequest().SetOutput(buf).SetDownloadCallback(func(info req.DownloadInfo) {
  76. if pushProgress {
  77. util.PushDownloadProgress(pushID, float32(info.DownloadedSize)/float32(info.Response.ContentLength))
  78. }
  79. }).Get(u)
  80. if nil != err {
  81. u = util.BazaarOSSServer + "/package/" + repoURLHash
  82. resp, err = httpclient.NewBrowserDownloadRequest().SetOutput(buf).SetDownloadCallback(func(info req.DownloadInfo) {
  83. if pushProgress {
  84. util.PushDownloadProgress(pushID, float32(info.DownloadedSize)/float32(info.Response.ContentLength))
  85. }
  86. }).Get(u)
  87. if nil != err {
  88. logging.LogErrorf("get bazaar package [%s] failed: %s", u, err)
  89. return nil, errors.New("get bazaar package failed")
  90. }
  91. }
  92. if 200 != resp.StatusCode {
  93. logging.LogErrorf("get bazaar package [%s] failed: %d", u, resp.StatusCode)
  94. return nil, errors.New("get bazaar package failed")
  95. }
  96. data = buf.Bytes()
  97. go incPackageDownloads(repoURLHash, systemID)
  98. return
  99. }
  100. func incPackageDownloads(repoURLHash, systemID string) {
  101. if strings.Contains(repoURLHash, ".md") {
  102. return
  103. }
  104. repo := strings.Split(repoURLHash, "@")[0]
  105. u := util.AliyunServer + "/apis/siyuan/bazaar/addBazaarPackageDownloadCount"
  106. httpclient.NewCloudRequest().SetBody(
  107. map[string]interface{}{
  108. "systemID": systemID,
  109. "repo": repo,
  110. }).Post(u)
  111. }
  112. func installPackage(data []byte, installPath string) (err error) {
  113. dir := filepath.Join(util.TempDir, "bazaar", "package")
  114. if err = os.MkdirAll(dir, 0755); nil != err {
  115. return
  116. }
  117. name := gulu.Rand.String(7)
  118. tmp := filepath.Join(dir, name+".zip")
  119. if err = os.WriteFile(tmp, data, 0644); nil != err {
  120. return
  121. }
  122. unzipPath := filepath.Join(dir, name)
  123. if err = gulu.Zip.Unzip(tmp, unzipPath); nil != err {
  124. logging.LogErrorf("write file [%s] failed: %s", installPath, err)
  125. err = errors.New("write file failed")
  126. return
  127. }
  128. dirs, err := os.ReadDir(unzipPath)
  129. if nil != err {
  130. return
  131. }
  132. for _, d := range dirs {
  133. if d.IsDir() && strings.Contains(d.Name(), "-") {
  134. dir = d.Name()
  135. break
  136. }
  137. }
  138. srcPath := filepath.Join(unzipPath, dir)
  139. if err = gulu.File.Copy(srcPath, installPath); nil != err {
  140. return
  141. }
  142. return
  143. }
  144. func formatUpdated(updated string) (ret string) {
  145. t, e := dateparse.ParseIn(updated, time.Now().Location())
  146. if nil == e {
  147. ret = t.Format("2006-01-02")
  148. } else {
  149. if strings.Contains(updated, "T") {
  150. ret = updated[:strings.Index(updated, "T")]
  151. } else {
  152. ret = strings.ReplaceAll(strings.ReplaceAll(updated, "T", ""), "Z", "")
  153. }
  154. }
  155. return
  156. }
  157. type bazaarPackage struct {
  158. Name string `json:"name"`
  159. Downloads int `json:"downloads"`
  160. }
  161. var cachedBazaarIndex = map[string]*bazaarPackage{}
  162. var bazaarIndexCacheTime int64
  163. var bazaarIndexLock = sync.Mutex{}
  164. func getBazaarIndex() map[string]*bazaarPackage {
  165. bazaarIndexLock.Lock()
  166. defer bazaarIndexLock.Unlock()
  167. now := time.Now().Unix()
  168. if 3600 >= now-bazaarIndexCacheTime {
  169. return cachedBazaarIndex
  170. }
  171. request := httpclient.NewBrowserRequest()
  172. u := util.BazaarStatServer + "/bazaar/index.json"
  173. resp, reqErr := request.SetResult(&cachedBazaarIndex).Get(u)
  174. if nil != reqErr {
  175. logging.LogErrorf("get bazaar index [%s] failed: %s", u, reqErr)
  176. return cachedBazaarIndex
  177. }
  178. if 200 != resp.StatusCode {
  179. logging.LogErrorf("get bazaar index [%s] failed: %d", u, resp.StatusCode)
  180. return cachedBazaarIndex
  181. }
  182. bazaarIndexCacheTime = now
  183. return cachedBazaarIndex
  184. }