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 api
18
19import (
20 "fmt"
21 "net/http"
22 "os"
23 "path/filepath"
24 "sort"
25 "strings"
26 "time"
27 "unicode/utf8"
28
29 "github.com/88250/gulu"
30 "github.com/gin-gonic/gin"
31 "github.com/siyuan-note/logging"
32 "github.com/siyuan-note/siyuan/kernel/model"
33 "github.com/siyuan-note/siyuan/kernel/util"
34)
35
36func checkWorkspaceDir(c *gin.Context) {
37 ret := gulu.Ret.NewResult()
38 defer c.JSON(http.StatusOK, ret)
39
40 arg, ok := util.JsonArg(c, ret)
41 if !ok {
42 return
43 }
44
45 path := arg["path"].(string)
46 // 检查路径是否是分区根路径
47 if util.IsPartitionRootPath(path) {
48 ret.Code = -1
49 ret.Msg = model.Conf.Language(273)
50 ret.Data = map[string]interface{}{"closeTimeout": 7000}
51 return
52 }
53
54 // 检查路径是否包含其他文件
55 if !util.IsWorkspaceDir(path) {
56 entries, err := os.ReadDir(path)
57 if err != nil {
58 ret.Code = -1
59 ret.Msg = fmt.Sprintf("read dir [%s] failed: %s", path, err)
60 return
61 }
62 if 0 < len(entries) {
63 ret.Code = -1
64 ret.Msg = model.Conf.Language(274)
65 ret.Data = map[string]interface{}{"closeTimeout": 7000}
66 return
67 }
68 }
69
70 if isInvalidWorkspacePath(path) {
71 ret.Code = -1
72 ret.Msg = "This workspace name is not allowed, please use another name"
73 return
74 }
75
76 if !gulu.File.IsExist(path) {
77 ret.Code = -1
78 ret.Msg = "This workspace does not exist"
79 return
80 }
81
82 ret.Data = map[string]interface{}{
83 "isWorkspace": util.IsWorkspaceDir(path),
84 }
85}
86
87func createWorkspaceDir(c *gin.Context) {
88 ret := gulu.Ret.NewResult()
89 defer c.JSON(http.StatusOK, ret)
90
91 arg, ok := util.JsonArg(c, ret)
92 if !ok {
93 return
94 }
95
96 absPath := arg["path"].(string)
97 absPath = util.RemoveInvalid(absPath)
98 absPath = strings.TrimSpace(absPath)
99 if isInvalidWorkspacePath(absPath) {
100 ret.Code = -1
101 ret.Msg = "This workspace name is not allowed, please use another name"
102 return
103 }
104
105 if !gulu.File.IsExist(absPath) {
106 if err := os.MkdirAll(absPath, 0755); err != nil {
107 ret.Code = -1
108 ret.Msg = fmt.Sprintf("create workspace dir [%s] failed: %s", absPath, err)
109 return
110 }
111 }
112
113 workspacePaths, err := util.ReadWorkspacePaths()
114 if err != nil {
115 ret.Code = -1
116 ret.Msg = err.Error()
117 return
118 }
119
120 workspacePaths = append(workspacePaths, absPath)
121
122 if err = util.WriteWorkspacePaths(workspacePaths); err != nil {
123 ret.Code = -1
124 ret.Msg = err.Error()
125 return
126 }
127}
128
129func removeWorkspaceDir(c *gin.Context) {
130 ret := gulu.Ret.NewResult()
131 defer c.JSON(http.StatusOK, ret)
132
133 arg, ok := util.JsonArg(c, ret)
134 if !ok {
135 return
136 }
137
138 path := arg["path"].(string)
139
140 if util.IsWorkspaceLocked(path) || util.WorkspaceDir == path {
141 msg := "Cannot remove current workspace"
142 ret.Code = -1
143 ret.Msg = msg
144 ret.Data = map[string]interface{}{"closeTimeout": 3000}
145 return
146 }
147
148 workspacePaths, err := util.ReadWorkspacePaths()
149 if err != nil {
150 ret.Code = -1
151 ret.Msg = err.Error()
152 return
153 }
154
155 workspacePaths = gulu.Str.RemoveElem(workspacePaths, path)
156
157 if err = util.WriteWorkspacePaths(workspacePaths); err != nil {
158 ret.Code = -1
159 ret.Msg = err.Error()
160 return
161 }
162}
163
164func removeWorkspaceDirPhysically(c *gin.Context) {
165 ret := gulu.Ret.NewResult()
166 defer c.JSON(http.StatusOK, ret)
167
168 arg, ok := util.JsonArg(c, ret)
169 if !ok {
170 return
171 }
172
173 path := arg["path"].(string)
174 if gulu.File.IsDir(path) {
175 err := os.RemoveAll(path)
176 if err != nil {
177 ret.Code = -1
178 ret.Msg = err.Error()
179 return
180 }
181 }
182
183 logging.LogInfof("removed workspace [%s] physically", path)
184 if util.WorkspaceDir == path {
185 os.Exit(logging.ExitCodeOk)
186 }
187}
188
189type Workspace struct {
190 Path string `json:"path"`
191 Closed bool `json:"closed"`
192}
193
194func getMobileWorkspaces(c *gin.Context) {
195 ret := gulu.Ret.NewResult()
196 defer c.JSON(http.StatusOK, ret)
197
198 if util.ContainerIOS != util.Container && util.ContainerAndroid != util.Container && util.ContainerHarmony != util.Container {
199 return
200 }
201
202 root := filepath.Dir(util.WorkspaceDir)
203 dirs, err := os.ReadDir(root)
204 if err != nil {
205 logging.LogErrorf("read dir [%s] failed: %s", root, err)
206 ret.Code = -1
207 ret.Msg = err.Error()
208 return
209 }
210
211 ret.Data = []string{}
212 var paths []string
213 for _, dir := range dirs {
214 if dir.IsDir() {
215 absPath := filepath.Join(root, dir.Name())
216 if isInvalidWorkspacePath(absPath) {
217 continue
218 }
219
220 paths = append(paths, absPath)
221 }
222 }
223 ret.Data = paths
224}
225
226func getWorkspaces(c *gin.Context) {
227 ret := gulu.Ret.NewResult()
228 defer c.JSON(http.StatusOK, ret)
229
230 workspacePaths, err := util.ReadWorkspacePaths()
231 if err != nil {
232 ret.Code = -1
233 ret.Msg = err.Error()
234 return
235 }
236
237 if role := model.GetGinContextRole(c); !model.IsValidRole(role, []model.Role{
238 model.RoleAdministrator,
239 }) {
240 ret.Data = []*Workspace{}
241 return
242 }
243
244 var workspaces, openedWorkspaces, closedWorkspaces []*Workspace
245 for _, p := range workspacePaths {
246 closed := !util.IsWorkspaceLocked(p)
247 if closed {
248 closedWorkspaces = append(closedWorkspaces, &Workspace{Path: p, Closed: closed})
249 } else {
250 openedWorkspaces = append(openedWorkspaces, &Workspace{Path: p, Closed: closed})
251 }
252 }
253 sort.Slice(openedWorkspaces, func(i, j int) bool {
254 return util.NaturalCompare(filepath.Base(openedWorkspaces[i].Path), filepath.Base(openedWorkspaces[j].Path))
255 })
256 sort.Slice(closedWorkspaces, func(i, j int) bool {
257 return util.NaturalCompare(filepath.Base(closedWorkspaces[i].Path), filepath.Base(closedWorkspaces[j].Path))
258 })
259 workspaces = append(workspaces, openedWorkspaces...)
260 workspaces = append(workspaces, closedWorkspaces...)
261 ret.Data = workspaces
262}
263
264func setWorkspaceDir(c *gin.Context) {
265 ret := gulu.Ret.NewResult()
266 defer c.JSON(http.StatusOK, ret)
267
268 arg, ok := util.JsonArg(c, ret)
269 if !ok {
270 return
271 }
272
273 path := arg["path"].(string)
274 if util.WorkspaceDir == path {
275 ret.Code = -1
276 ret.Msg = model.Conf.Language(78)
277 ret.Data = map[string]interface{}{"closeTimeout": 3000}
278 return
279 }
280
281 if util.IsCloudDrivePath(path) {
282 ret.Code = -1
283 ret.Msg = model.Conf.Language(196)
284 ret.Data = map[string]interface{}{"closeTimeout": 7000}
285 return
286 }
287
288 if gulu.OS.IsWindows() {
289 // 改进判断工作空间路径实现 https://github.com/siyuan-note/siyuan/issues/7569
290 installDirLower := strings.ToLower(filepath.Dir(util.WorkingDir))
291 pathLower := strings.ToLower(path)
292 if strings.HasPrefix(pathLower, installDirLower) && (util.IsSubPath(installDirLower, pathLower) || filepath.Clean(installDirLower) == filepath.Clean(pathLower)) {
293 ret.Code = -1
294 ret.Msg = model.Conf.Language(98)
295 ret.Data = map[string]interface{}{"closeTimeout": 5000}
296 return
297 }
298 }
299
300 // 检查路径是否在已有的工作空间路径中
301 pathIsWorkspace := util.IsWorkspaceDir(path)
302 if !pathIsWorkspace {
303 for p := filepath.Dir(path); !util.IsPartitionRootPath(p); p = filepath.Dir(p) {
304 if util.IsWorkspaceDir(p) {
305 ret.Code = -1
306 ret.Msg = fmt.Sprintf(model.Conf.Language(256), path, p)
307 ret.Data = map[string]interface{}{"closeTimeout": 7000}
308 return
309 }
310 }
311 }
312
313 workspacePaths, err := util.ReadWorkspacePaths()
314 if err != nil {
315 ret.Code = -1
316 ret.Msg = err.Error()
317 return
318 }
319
320 workspacePaths = append(workspacePaths, path)
321 workspacePaths = gulu.Str.RemoveDuplicatedElem(workspacePaths)
322 workspacePaths = gulu.Str.RemoveElem(workspacePaths, path)
323 workspacePaths = append(workspacePaths, path) // 切换的工作空间固定放在最后一个
324
325 if err = util.WriteWorkspacePaths(workspacePaths); err != nil {
326 ret.Code = -1
327 ret.Msg = err.Error()
328 return
329 }
330
331 if util.ContainerAndroid == util.Container || util.ContainerIOS == util.Container || util.ContainerHarmony == util.Container {
332 util.PushMsg(model.Conf.Language(42), 1000*15)
333 time.Sleep(time.Second * 1)
334 model.Close(false, false, 1)
335 time.Sleep(time.Second * 1)
336 }
337}
338
339func isInvalidWorkspacePath(absPath string) bool {
340 if "" == absPath {
341 return true
342 }
343 name := filepath.Base(absPath)
344 if "" == name {
345 return true
346 }
347 if strings.HasPrefix(name, ".") {
348 return true
349 }
350 if !gulu.File.IsValidFilename(name) {
351 return true
352 }
353 if 32 < utf8.RuneCountInString(name) {
354 // Adjust workspace name length limit to 32 runes https://github.com/siyuan-note/siyuan/issues/9440
355 return true
356 }
357 toLower := strings.ToLower(name)
358 return gulu.Str.Contains(toLower, []string{"conf", "home", "data", "temp"})
359}