A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
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
17package bazaar
18
19import (
20 "os"
21 "path/filepath"
22 "runtime"
23 "sort"
24 "strings"
25
26 "github.com/88250/go-humanize"
27 "github.com/siyuan-note/logging"
28 "github.com/siyuan-note/siyuan/kernel/util"
29)
30
31type Plugin struct {
32 *Package
33 Enabled bool `json:"enabled"`
34}
35
36// Plugins 返回集市插件列表
37func Plugins(frontend string) (plugins []*Plugin) {
38 plugins = []*Plugin{}
39 result := getStageAndBazaar("plugins")
40
41 if !result.Online {
42 return
43 }
44 if result.StageErr != nil {
45 return
46 }
47 if 1 > len(result.BazaarIndex) {
48 return
49 }
50
51 for _, repo := range result.StageIndex.Repos {
52 if nil == repo.Package {
53 continue
54 }
55 plugin := buildPluginFromStageRepo(repo, frontend, result.BazaarIndex)
56 if nil != plugin {
57 plugins = append(plugins, plugin)
58 }
59 }
60
61 sort.Slice(plugins, func(i, j int) bool { return plugins[i].Updated > plugins[j].Updated })
62 return
63}
64
65// buildPluginFromStageRepo 使用 stage 内嵌的 package 构建 *Plugin,不发起 HTTP 请求。
66func buildPluginFromStageRepo(repo *StageRepo, frontend string, bazaarIndex map[string]*bazaarPackage) *Plugin {
67 pkg := *repo.Package
68 pkg.URL = strings.TrimSuffix(pkg.URL, "/")
69 repoURLHash := strings.Split(repo.URL, "@")
70 if 2 != len(repoURLHash) {
71 return nil
72 }
73 pkg.RepoURL = "https://github.com/" + repoURLHash[0]
74 pkg.RepoHash = repoURLHash[1]
75 pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim"
76 pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232"
77 pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png"
78 pkg.Updated = repo.Updated
79 pkg.Stars = repo.Stars
80 pkg.OpenIssues = repo.OpenIssues
81 pkg.Size = repo.Size
82 pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2)
83 pkg.InstallSize = repo.InstallSize
84 pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2)
85 pkg.HUpdated = formatUpdated(pkg.Updated)
86 pkg.PreferredFunding = getPreferredFunding(pkg.Funding)
87 pkg.PreferredName = GetPreferredName(&pkg)
88 pkg.PreferredDesc = getPreferredDesc(pkg.Description)
89 pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg)
90 pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg)
91 pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion
92 pkg.Incompatible = isIncompatiblePlugin(&Plugin{Package: &pkg}, frontend)
93 if bp := bazaarIndex[repoURLHash[0]]; nil != bp {
94 pkg.Downloads = bp.Downloads
95 }
96 packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize)
97 return &Plugin{Package: &pkg}
98}
99
100func ParseInstalledPlugin(name, frontend string) (found bool, displayName string, incompatible, disabledInPublish, disallowInstall bool) {
101 pluginsPath := filepath.Join(util.DataDir, "plugins")
102 if !util.IsPathRegularDirOrSymlinkDir(pluginsPath) {
103 return
104 }
105
106 pluginDirs, err := os.ReadDir(pluginsPath)
107 if err != nil {
108 logging.LogWarnf("read plugins folder failed: %s", err)
109 return
110 }
111
112 for _, pluginDir := range pluginDirs {
113 if !util.IsDirRegularOrSymlink(pluginDir) {
114 continue
115 }
116 dirName := pluginDir.Name()
117 if name != dirName {
118 continue
119 }
120
121 plugin, parseErr := PluginJSON(dirName)
122 if nil != parseErr || nil == plugin {
123 return
124 }
125
126 found = true
127 displayName = GetPreferredName(plugin.Package)
128 incompatible = isIncompatiblePlugin(plugin, frontend)
129 disabledInPublish = plugin.DisabledInPublish
130 disallowInstall = disallowInstallBazaarPackage(plugin.Package)
131 }
132 return
133}
134
135func InstalledPlugins(frontend string) (ret []*Plugin) {
136 ret = []*Plugin{}
137
138 pluginsPath := filepath.Join(util.DataDir, "plugins")
139 if !util.IsPathRegularDirOrSymlinkDir(pluginsPath) {
140 return
141 }
142
143 pluginDirs, err := os.ReadDir(pluginsPath)
144 if err != nil {
145 logging.LogWarnf("read plugins folder failed: %s", err)
146 return
147 }
148
149 bazaarPlugins := Plugins(frontend)
150
151 for _, pluginDir := range pluginDirs {
152 if !util.IsDirRegularOrSymlink(pluginDir) {
153 continue
154 }
155 dirName := pluginDir.Name()
156
157 plugin, parseErr := PluginJSON(dirName)
158 if nil != parseErr || nil == plugin {
159 continue
160 }
161
162 plugin.RepoURL = plugin.URL
163 plugin.DisallowInstall = disallowInstallBazaarPackage(plugin.Package)
164 if bazaarPkg := getBazaarPlugin(plugin.Name, bazaarPlugins); nil != bazaarPkg {
165 plugin.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package)
166 plugin.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion
167 plugin.RepoURL = bazaarPkg.RepoURL
168 }
169
170 installPath := filepath.Join(util.DataDir, "plugins", dirName)
171 plugin.Installed = true
172 plugin.PreviewURL = "/plugins/" + dirName + "/preview.png"
173 plugin.PreviewURLThumb = "/plugins/" + dirName + "/preview.png"
174 plugin.IconURL = "/plugins/" + dirName + "/icon.png"
175 plugin.PreferredFunding = getPreferredFunding(plugin.Funding)
176 plugin.PreferredName = GetPreferredName(plugin.Package)
177 plugin.PreferredDesc = getPreferredDesc(plugin.Description)
178 info, statErr := os.Stat(filepath.Join(installPath, "plugin.json"))
179 if nil != statErr {
180 logging.LogWarnf("stat install plugin.json failed: %s", statErr)
181 continue
182 }
183 plugin.HInstallDate = info.ModTime().Format("2006-01-02")
184 if installSize, ok := packageInstallSizeCache.Get(plugin.RepoURL); ok {
185 plugin.InstallSize = installSize.(int64)
186 } else {
187 is, _ := util.SizeOfDirectory(installPath)
188 plugin.InstallSize = is
189 packageInstallSizeCache.SetDefault(plugin.RepoURL, is)
190 }
191 plugin.HInstallSize = humanize.BytesCustomCeil(uint64(plugin.InstallSize), 2)
192 plugin.PreferredReadme = loadInstalledReadme(installPath, "/plugins/"+dirName+"/", plugin.Readme)
193 plugin.Outdated = isOutdatedPlugin(plugin, bazaarPlugins)
194 plugin.Incompatible = isIncompatiblePlugin(plugin, frontend)
195 ret = append(ret, plugin)
196 }
197 return
198}
199
200func getBazaarPlugin(name string, plugins []*Plugin) *Plugin {
201 for _, p := range plugins {
202 if p.Name == name {
203 return p
204 }
205 }
206 return nil
207}
208
209func InstallPlugin(repoURL, repoHash, installPath string, systemID string) error {
210 repoURLHash := repoURL + "@" + repoHash
211 data, err := downloadPackage(repoURLHash, true, systemID)
212 if err != nil {
213 return err
214 }
215 return installPackage(data, installPath, repoURLHash)
216}
217
218func UninstallPlugin(installPath string) error {
219 return uninstallPackage(installPath)
220}
221
222func isIncompatiblePlugin(plugin *Plugin, currentFrontend string) bool {
223 if 1 > len(plugin.Backends) {
224 return false
225 }
226
227 currentBackend := getCurrentBackend()
228 backendOk := false
229 for _, backend := range plugin.Backends {
230 if backend == currentBackend || "all" == backend {
231 backendOk = true
232 break
233 }
234 }
235
236 frontendOk := false
237 for _, frontend := range plugin.Frontends {
238 if frontend == currentFrontend || "all" == frontend {
239 frontendOk = true
240 break
241 }
242 }
243 return !backendOk || !frontendOk
244}
245
246func getCurrentBackend() string {
247 switch util.Container {
248 case util.ContainerDocker:
249 return "docker"
250 case util.ContainerIOS:
251 return "ios"
252 case util.ContainerAndroid:
253 return "android"
254 case util.ContainerHarmony:
255 return "harmony"
256 default:
257 return runtime.GOOS
258 }
259}