package.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. // SiYuan - Refactor your thinking
  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. "fmt"
  21. "os"
  22. "path/filepath"
  23. "strings"
  24. "sync"
  25. "time"
  26. "github.com/88250/gulu"
  27. "github.com/88250/lute"
  28. "github.com/araddon/dateparse"
  29. "github.com/imroc/req/v3"
  30. gcache "github.com/patrickmn/go-cache"
  31. "github.com/siyuan-note/filelock"
  32. "github.com/siyuan-note/httpclient"
  33. "github.com/siyuan-note/logging"
  34. "github.com/siyuan-note/siyuan/kernel/util"
  35. "golang.org/x/mod/semver"
  36. textUnicode "golang.org/x/text/encoding/unicode"
  37. "golang.org/x/text/transform"
  38. )
  39. type DisplayName struct {
  40. Default string `json:"default"`
  41. ZhCN string `json:"zh_CN"`
  42. EnUS string `json:"en_US"`
  43. ZhCHT string `json:"zh_CHT"`
  44. }
  45. type Description struct {
  46. Default string `json:"default"`
  47. ZhCN string `json:"zh_CN"`
  48. EnUS string `json:"en_US"`
  49. ZhCHT string `json:"zh_CHT"`
  50. }
  51. type Readme struct {
  52. Default string `json:"default"`
  53. ZhCN string `json:"zh_CN"`
  54. EnUS string `json:"en_US"`
  55. ZhCHT string `json:"zh_CHT"`
  56. }
  57. type Funding struct {
  58. OpenCollective string `json:"openCollective"`
  59. Patreon string `json:"patreon"`
  60. GitHub string `json:"github"`
  61. Custom []string `json:"custom"`
  62. }
  63. type Package struct {
  64. Author string `json:"author"`
  65. URL string `json:"url"`
  66. Version string `json:"version"`
  67. MinAppVersion string `json:"minAppVersion"`
  68. Backends []string `json:"backends"`
  69. Frontends []string `json:"frontends"`
  70. DisplayName *DisplayName `json:"displayName"`
  71. Description *Description `json:"description"`
  72. Readme *Readme `json:"readme"`
  73. Funding *Funding `json:"funding"`
  74. Keywords []string `json:"keywords"`
  75. PreferredFunding string `json:"preferredFunding"`
  76. PreferredName string `json:"preferredName"`
  77. PreferredDesc string `json:"preferredDesc"`
  78. PreferredReadme string `json:"preferredReadme"`
  79. Name string `json:"name"`
  80. RepoURL string `json:"repoURL"`
  81. RepoHash string `json:"repoHash"`
  82. PreviewURL string `json:"previewURL"`
  83. PreviewURLThumb string `json:"previewURLThumb"`
  84. IconURL string `json:"iconURL"`
  85. Installed bool `json:"installed"`
  86. Outdated bool `json:"outdated"`
  87. Current bool `json:"current"`
  88. Updated string `json:"updated"`
  89. Stars int `json:"stars"`
  90. OpenIssues int `json:"openIssues"`
  91. Size int64 `json:"size"`
  92. HSize string `json:"hSize"`
  93. InstallSize int64 `json:"installSize"`
  94. HInstallSize string `json:"hInstallSize"`
  95. HInstallDate string `json:"hInstallDate"`
  96. HUpdated string `json:"hUpdated"`
  97. Downloads int `json:"downloads"`
  98. Incompatible bool `json:"incompatible"`
  99. }
  100. type StagePackage struct {
  101. Author string `json:"author"`
  102. URL string `json:"url"`
  103. Version string `json:"version"`
  104. Description *Description `json:"description"`
  105. Readme *Readme `json:"readme"`
  106. I18N []string `json:"i18n"`
  107. Funding *Funding `json:"funding"`
  108. }
  109. type StageRepo struct {
  110. URL string `json:"url"`
  111. Updated string `json:"updated"`
  112. Stars int `json:"stars"`
  113. OpenIssues int `json:"openIssues"`
  114. Size int64 `json:"size"`
  115. InstallSize int64 `json:"installSize"`
  116. Package *StagePackage `json:"package"`
  117. }
  118. type StageIndex struct {
  119. Repos []*StageRepo `json:"repos"`
  120. }
  121. func getPreferredReadme(readme *Readme) string {
  122. if nil == readme {
  123. return "README.md"
  124. }
  125. ret := readme.Default
  126. switch util.Lang {
  127. case "zh_CN":
  128. if "" != readme.ZhCN {
  129. ret = readme.ZhCN
  130. }
  131. case "zh_CHT":
  132. if "" != readme.ZhCHT {
  133. ret = readme.ZhCHT
  134. } else if "" != readme.ZhCN {
  135. ret = readme.ZhCN
  136. }
  137. case "en_US":
  138. if "" != readme.EnUS {
  139. ret = readme.EnUS
  140. }
  141. default:
  142. if "" != readme.EnUS {
  143. ret = readme.EnUS
  144. }
  145. }
  146. return ret
  147. }
  148. func GetPreferredName(pkg *Package) string {
  149. if nil == pkg.DisplayName {
  150. return pkg.Name
  151. }
  152. ret := pkg.DisplayName.Default
  153. switch util.Lang {
  154. case "zh_CN":
  155. if "" != pkg.DisplayName.ZhCN {
  156. ret = pkg.DisplayName.ZhCN
  157. }
  158. case "zh_CHT":
  159. if "" != pkg.DisplayName.ZhCHT {
  160. ret = pkg.DisplayName.ZhCHT
  161. } else if "" != pkg.DisplayName.ZhCN {
  162. ret = pkg.DisplayName.ZhCN
  163. }
  164. case "en_US":
  165. if "" != pkg.DisplayName.EnUS {
  166. ret = pkg.DisplayName.EnUS
  167. }
  168. default:
  169. if "" != pkg.DisplayName.EnUS {
  170. ret = pkg.DisplayName.EnUS
  171. }
  172. }
  173. return ret
  174. }
  175. func getPreferredDesc(desc *Description) string {
  176. if nil == desc {
  177. return ""
  178. }
  179. ret := desc.Default
  180. switch util.Lang {
  181. case "zh_CN":
  182. if "" != desc.ZhCN {
  183. ret = desc.ZhCN
  184. }
  185. case "zh_CHT":
  186. if "" != desc.ZhCHT {
  187. ret = desc.ZhCHT
  188. } else if "" != desc.ZhCN {
  189. ret = desc.ZhCN
  190. }
  191. case "en_US":
  192. if "" != desc.EnUS {
  193. ret = desc.EnUS
  194. }
  195. default:
  196. if "" != desc.EnUS {
  197. ret = desc.EnUS
  198. }
  199. }
  200. return ret
  201. }
  202. func getPreferredFunding(funding *Funding) string {
  203. if nil == funding {
  204. return ""
  205. }
  206. if "" != funding.OpenCollective {
  207. return "https://opencollective.com/" + funding.OpenCollective
  208. }
  209. if "" != funding.Patreon {
  210. return "https://www.patreon.com/" + funding.Patreon
  211. }
  212. if "" != funding.GitHub {
  213. return "https://github.com/sponsors/" + funding.GitHub
  214. }
  215. if 0 < len(funding.Custom) {
  216. return funding.Custom[0]
  217. }
  218. return ""
  219. }
  220. func PluginJSON(pluginDirName string) (ret *Plugin, err error) {
  221. p := filepath.Join(util.DataDir, "plugins", pluginDirName, "plugin.json")
  222. if !filelock.IsExist(p) {
  223. err = os.ErrNotExist
  224. return
  225. }
  226. data, err := filelock.ReadFile(p)
  227. if nil != err {
  228. logging.LogErrorf("read plugin.json [%s] failed: %s", p, err)
  229. return
  230. }
  231. if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
  232. logging.LogErrorf("parse plugin.json [%s] failed: %s", p, err)
  233. return
  234. }
  235. ret.URL = strings.TrimSuffix(ret.URL, "/")
  236. return
  237. }
  238. func WidgetJSON(widgetDirName string) (ret *Widget, err error) {
  239. p := filepath.Join(util.DataDir, "widgets", widgetDirName, "widget.json")
  240. if !filelock.IsExist(p) {
  241. err = os.ErrNotExist
  242. return
  243. }
  244. data, err := filelock.ReadFile(p)
  245. if nil != err {
  246. logging.LogErrorf("read widget.json [%s] failed: %s", p, err)
  247. return
  248. }
  249. if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
  250. logging.LogErrorf("parse widget.json [%s] failed: %s", p, err)
  251. return
  252. }
  253. ret.URL = strings.TrimSuffix(ret.URL, "/")
  254. return
  255. }
  256. func IconJSON(iconDirName string) (ret *Icon, err error) {
  257. p := filepath.Join(util.IconsPath, iconDirName, "icon.json")
  258. if !gulu.File.IsExist(p) {
  259. err = os.ErrNotExist
  260. return
  261. }
  262. data, err := os.ReadFile(p)
  263. if nil != err {
  264. logging.LogErrorf("read icon.json [%s] failed: %s", p, err)
  265. return
  266. }
  267. if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
  268. logging.LogErrorf("parse icon.json [%s] failed: %s", p, err)
  269. return
  270. }
  271. ret.URL = strings.TrimSuffix(ret.URL, "/")
  272. return
  273. }
  274. func TemplateJSON(templateDirName string) (ret *Template, err error) {
  275. p := filepath.Join(util.DataDir, "templates", templateDirName, "template.json")
  276. if !filelock.IsExist(p) {
  277. err = os.ErrNotExist
  278. return
  279. }
  280. data, err := filelock.ReadFile(p)
  281. if nil != err {
  282. logging.LogErrorf("read template.json [%s] failed: %s", p, err)
  283. return
  284. }
  285. if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
  286. logging.LogErrorf("parse template.json [%s] failed: %s", p, err)
  287. return
  288. }
  289. ret.URL = strings.TrimSuffix(ret.URL, "/")
  290. return
  291. }
  292. func ThemeJSON(themeDirName string) (ret *Theme, err error) {
  293. p := filepath.Join(util.ThemesPath, themeDirName, "theme.json")
  294. if !gulu.File.IsExist(p) {
  295. err = os.ErrNotExist
  296. return
  297. }
  298. data, err := os.ReadFile(p)
  299. if nil != err {
  300. logging.LogErrorf("read theme.json [%s] failed: %s", p, err)
  301. return
  302. }
  303. ret = &Theme{}
  304. if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
  305. logging.LogErrorf("parse theme.json [%s] failed: %s", p, err)
  306. return
  307. }
  308. ret.URL = strings.TrimSuffix(ret.URL, "/")
  309. return
  310. }
  311. var cachedStageIndex = map[string]*StageIndex{}
  312. var stageIndexCacheTime int64
  313. var stageIndexLock = sync.Mutex{}
  314. func getStageIndex(pkgType string) (ret *StageIndex, err error) {
  315. rhyRet, err := util.GetRhyResult(false)
  316. if nil != err {
  317. return
  318. }
  319. stageIndexLock.Lock()
  320. defer stageIndexLock.Unlock()
  321. now := time.Now().Unix()
  322. if 3600 >= now-stageIndexCacheTime && nil != cachedStageIndex[pkgType] {
  323. ret = cachedStageIndex[pkgType]
  324. return
  325. }
  326. bazaarHash := rhyRet["bazaar"].(string)
  327. ret = &StageIndex{}
  328. request := httpclient.NewBrowserRequest()
  329. u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/" + pkgType + ".json"
  330. resp, reqErr := request.SetSuccessResult(ret).Get(u)
  331. if nil != reqErr {
  332. logging.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
  333. return
  334. }
  335. if 200 != resp.StatusCode {
  336. logging.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
  337. return
  338. }
  339. stageIndexCacheTime = now
  340. cachedStageIndex[pkgType] = ret
  341. return
  342. }
  343. func isOutdatedTheme(theme *Theme, bazaarThemes []*Theme) bool {
  344. if !strings.HasPrefix(theme.URL, "https://github.com/") {
  345. return false
  346. }
  347. repo := strings.TrimPrefix(theme.URL, "https://github.com/")
  348. parts := strings.Split(repo, "/")
  349. if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
  350. return false
  351. }
  352. for _, pkg := range bazaarThemes {
  353. if theme.URL == pkg.URL && theme.Name == pkg.Name && theme.Author == pkg.Author && 0 > semver.Compare("v"+theme.Version, "v"+pkg.Version) {
  354. theme.RepoHash = pkg.RepoHash
  355. return true
  356. }
  357. }
  358. return false
  359. }
  360. func isOutdatedIcon(icon *Icon, bazaarIcons []*Icon) bool {
  361. if !strings.HasPrefix(icon.URL, "https://github.com/") {
  362. return false
  363. }
  364. repo := strings.TrimPrefix(icon.URL, "https://github.com/")
  365. parts := strings.Split(repo, "/")
  366. if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
  367. return false
  368. }
  369. for _, pkg := range bazaarIcons {
  370. if icon.URL == pkg.URL && icon.Name == pkg.Name && icon.Author == pkg.Author && 0 > semver.Compare("v"+icon.Version, "v"+pkg.Version) {
  371. icon.RepoHash = pkg.RepoHash
  372. return true
  373. }
  374. }
  375. return false
  376. }
  377. func isOutdatedPlugin(plugin *Plugin, bazaarPlugins []*Plugin) bool {
  378. if !strings.HasPrefix(plugin.URL, "https://github.com/") {
  379. return false
  380. }
  381. repo := strings.TrimPrefix(plugin.URL, "https://github.com/")
  382. parts := strings.Split(repo, "/")
  383. if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
  384. return false
  385. }
  386. for _, pkg := range bazaarPlugins {
  387. if plugin.URL == pkg.URL && plugin.Name == pkg.Name && plugin.Author == pkg.Author && 0 > semver.Compare("v"+plugin.Version, "v"+pkg.Version) {
  388. plugin.RepoHash = pkg.RepoHash
  389. return true
  390. }
  391. }
  392. return false
  393. }
  394. func isOutdatedWidget(widget *Widget, bazaarWidgets []*Widget) bool {
  395. if !strings.HasPrefix(widget.URL, "https://github.com/") {
  396. return false
  397. }
  398. repo := strings.TrimPrefix(widget.URL, "https://github.com/")
  399. parts := strings.Split(repo, "/")
  400. if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
  401. return false
  402. }
  403. for _, pkg := range bazaarWidgets {
  404. if widget.URL == pkg.URL && widget.Name == pkg.Name && widget.Author == pkg.Author && 0 > semver.Compare("v"+widget.Version, "v"+pkg.Version) {
  405. widget.RepoHash = pkg.RepoHash
  406. return true
  407. }
  408. }
  409. return false
  410. }
  411. func isOutdatedTemplate(template *Template, bazaarTemplates []*Template) bool {
  412. if !strings.HasPrefix(template.URL, "https://github.com/") {
  413. return false
  414. }
  415. repo := strings.TrimPrefix(template.URL, "https://github.com/")
  416. parts := strings.Split(repo, "/")
  417. if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
  418. return false
  419. }
  420. for _, pkg := range bazaarTemplates {
  421. if template.URL == pkg.URL && template.Name == pkg.Name && template.Author == pkg.Author && 0 > semver.Compare("v"+template.Version, "v"+pkg.Version) {
  422. template.RepoHash = pkg.RepoHash
  423. return true
  424. }
  425. }
  426. return false
  427. }
  428. func GetPackageREADME(repoURL, repoHash, packageType string) (ret string) {
  429. repoURLHash := repoURL + "@" + repoHash
  430. stageIndex := cachedStageIndex[packageType]
  431. if nil == stageIndex {
  432. return
  433. }
  434. url := strings.TrimPrefix(repoURLHash, "https://github.com/")
  435. var repo *StageRepo
  436. for _, r := range stageIndex.Repos {
  437. if r.URL == url {
  438. repo = r
  439. break
  440. }
  441. }
  442. if nil == repo {
  443. return
  444. }
  445. readme := getPreferredReadme(repo.Package.Readme)
  446. data, err := downloadPackage(repoURLHash+"/"+readme, false, "")
  447. if nil != err {
  448. ret = fmt.Sprintf("Load bazaar package's README.md(%s) failed: %s", readme, err.Error())
  449. if readme == repo.Package.Readme.Default || "" == strings.TrimSpace(repo.Package.Readme.Default) {
  450. return
  451. }
  452. readme = repo.Package.Readme.Default
  453. data, err = downloadPackage(repoURLHash+"/"+readme, false, "")
  454. if nil != err {
  455. ret += fmt.Sprintf("<br>Load bazaar package's README.md(%s) failed: %s", readme, err.Error())
  456. return
  457. }
  458. }
  459. if 2 < len(data) {
  460. if 255 == data[0] && 254 == data[1] {
  461. data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.LittleEndian, textUnicode.ExpectBOM).NewDecoder(), data)
  462. } else if 254 == data[0] && 255 == data[1] {
  463. data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.ExpectBOM).NewDecoder(), data)
  464. }
  465. }
  466. ret, err = renderREADME(repoURL, data)
  467. return
  468. }
  469. func renderREADME(repoURL string, mdData []byte) (ret string, err error) {
  470. luteEngine := lute.New()
  471. luteEngine.SetSoftBreak2HardBreak(false)
  472. luteEngine.SetCodeSyntaxHighlight(false)
  473. linkBase := "https://cdn.jsdelivr.net/gh/" + strings.TrimPrefix(repoURL, "https://github.com/")
  474. luteEngine.SetLinkBase(linkBase)
  475. ret = luteEngine.Md2HTML(string(mdData))
  476. ret = util.LinkTarget(ret, linkBase)
  477. return
  478. }
  479. var (
  480. packageLocks = map[string]*sync.Mutex{}
  481. packageLocksLock = sync.Mutex{}
  482. )
  483. func downloadPackage(repoURLHash string, pushProgress bool, systemID string) (data []byte, err error) {
  484. packageLocksLock.Lock()
  485. defer packageLocksLock.Unlock()
  486. // repoURLHash: https://github.com/88250/Comfortably-Numb@6286912c381ef3f83e455d06ba4d369c498238dc
  487. repoURL := repoURLHash[:strings.LastIndex(repoURLHash, "@")]
  488. lock, ok := packageLocks[repoURLHash]
  489. if !ok {
  490. lock = &sync.Mutex{}
  491. packageLocks[repoURLHash] = lock
  492. }
  493. lock.Lock()
  494. defer lock.Unlock()
  495. repoURLHash = strings.TrimPrefix(repoURLHash, "https://github.com/")
  496. u := util.BazaarOSSServer + "/package/" + repoURLHash
  497. buf := &bytes.Buffer{}
  498. resp, err := httpclient.NewCloudFileRequest2m().SetOutput(buf).SetDownloadCallback(func(info req.DownloadInfo) {
  499. if pushProgress {
  500. progress := float32(info.DownloadedSize) / float32(info.Response.ContentLength)
  501. //logging.LogDebugf("downloading bazaar package [%f]", progress)
  502. util.PushDownloadProgress(repoURL, progress)
  503. }
  504. }).Get(u)
  505. if nil != err {
  506. logging.LogErrorf("get bazaar package [%s] failed: %s", u, err)
  507. return nil, errors.New("get bazaar package failed, please check your network")
  508. }
  509. if 200 != resp.StatusCode {
  510. logging.LogErrorf("get bazaar package [%s] failed: %d", u, resp.StatusCode)
  511. return nil, errors.New("get bazaar package failed: " + resp.Status)
  512. }
  513. data = buf.Bytes()
  514. go incPackageDownloads(repoURLHash, systemID)
  515. return
  516. }
  517. func incPackageDownloads(repoURLHash, systemID string) {
  518. if strings.Contains(repoURLHash, ".md") || "" == systemID {
  519. return
  520. }
  521. repo := strings.Split(repoURLHash, "@")[0]
  522. u := util.GetCloudServer() + "/apis/siyuan/bazaar/addBazaarPackageDownloadCount"
  523. httpclient.NewCloudRequest30s().SetBody(
  524. map[string]interface{}{
  525. "systemID": systemID,
  526. "repo": repo,
  527. }).Post(u)
  528. }
  529. func uninstallPackage(installPath string) (err error) {
  530. if err = os.RemoveAll(installPath); nil != err {
  531. logging.LogErrorf("remove [%s] failed: %s", installPath, err)
  532. return fmt.Errorf("remove community package [%s] failed", filepath.Base(installPath))
  533. }
  534. packageCache.Flush()
  535. return
  536. }
  537. func installPackage(data []byte, installPath, repoURLHash string) (err error) {
  538. err = installPackage0(data, installPath)
  539. if nil != err {
  540. return
  541. }
  542. packageCache.Delete(strings.TrimPrefix(repoURLHash, "https://github.com/"))
  543. return
  544. }
  545. func installPackage0(data []byte, installPath string) (err error) {
  546. tmpPackage := filepath.Join(util.TempDir, "bazaar", "package")
  547. if err = os.MkdirAll(tmpPackage, 0755); nil != err {
  548. return
  549. }
  550. name := gulu.Rand.String(7)
  551. tmp := filepath.Join(tmpPackage, name+".zip")
  552. if err = os.WriteFile(tmp, data, 0644); nil != err {
  553. return
  554. }
  555. unzipPath := filepath.Join(tmpPackage, name)
  556. if err = gulu.Zip.Unzip(tmp, unzipPath); nil != err {
  557. logging.LogErrorf("write file [%s] failed: %s", installPath, err)
  558. return
  559. }
  560. dirs, err := os.ReadDir(unzipPath)
  561. if nil != err {
  562. return
  563. }
  564. srcPath := unzipPath
  565. if 1 == len(dirs) && dirs[0].IsDir() {
  566. srcPath = filepath.Join(unzipPath, dirs[0].Name())
  567. }
  568. if err = filelock.Copy(srcPath, installPath); nil != err {
  569. return
  570. }
  571. return
  572. }
  573. func formatUpdated(updated string) (ret string) {
  574. t, e := dateparse.ParseIn(updated, time.Now().Location())
  575. if nil == e {
  576. ret = t.Format("2006-01-02")
  577. } else {
  578. if strings.Contains(updated, "T") {
  579. ret = updated[:strings.Index(updated, "T")]
  580. } else {
  581. ret = strings.ReplaceAll(strings.ReplaceAll(updated, "T", ""), "Z", "")
  582. }
  583. }
  584. return
  585. }
  586. type bazaarPackage struct {
  587. Name string `json:"name"`
  588. Downloads int `json:"downloads"`
  589. }
  590. var cachedBazaarIndex = map[string]*bazaarPackage{}
  591. var bazaarIndexCacheTime int64
  592. var bazaarIndexLock = sync.Mutex{}
  593. func getBazaarIndex() map[string]*bazaarPackage {
  594. bazaarIndexLock.Lock()
  595. defer bazaarIndexLock.Unlock()
  596. now := time.Now().Unix()
  597. if 3600 >= now-bazaarIndexCacheTime {
  598. return cachedBazaarIndex
  599. }
  600. request := httpclient.NewBrowserRequest()
  601. u := util.BazaarStatServer + "/bazaar/index.json"
  602. resp, reqErr := request.SetSuccessResult(&cachedBazaarIndex).Get(u)
  603. if nil != reqErr {
  604. logging.LogErrorf("get bazaar index [%s] failed: %s", u, reqErr)
  605. return cachedBazaarIndex
  606. }
  607. if 200 != resp.StatusCode {
  608. logging.LogErrorf("get bazaar index [%s] failed: %d", u, resp.StatusCode)
  609. return cachedBazaarIndex
  610. }
  611. bazaarIndexCacheTime = now
  612. return cachedBazaarIndex
  613. }
  614. // defaultMinAppVersion 如果集市包中缺失 minAppVersion 项,则使用该值作为最低支持的版本号,小于该版本号时不显示集市包
  615. // Add marketplace package config item `minAppVersion` https://github.com/siyuan-note/siyuan/issues/8330
  616. const defaultMinAppVersion = "2.9.0"
  617. func disallowDisplayBazaarPackage(pkg *Package) bool {
  618. if "" == pkg.MinAppVersion { // TODO: 目前暂时放过所有不带 minAppVersion 的集市包,后续版本会使用 defaultMinAppVersion
  619. return false
  620. }
  621. if 0 < semver.Compare("v"+pkg.MinAppVersion, "v"+util.Ver) {
  622. return true
  623. }
  624. return false
  625. }
  626. var packageCache = gcache.New(6*time.Hour, 30*time.Minute) // [repoURL]*Package
  627. var packageInstallSizeCache = gcache.New(48*time.Hour, 6*time.Hour) // [repoURL]*int64