A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at upstream/main 359 lines 8.9 kB view raw
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}