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 model
18
19import (
20 "errors"
21 "fmt"
22 "os"
23 "path/filepath"
24 "strings"
25 "sync"
26 "time"
27 "unicode/utf8"
28
29 "github.com/88250/gulu"
30 "github.com/88250/lute/ast"
31 "github.com/siyuan-note/filelock"
32 "github.com/siyuan-note/logging"
33 "github.com/siyuan-note/siyuan/kernel/task"
34 "github.com/siyuan-note/siyuan/kernel/util"
35)
36
37func CreateBox(name string) (id string, err error) {
38 name = util.RemoveInvalid(name)
39 if 512 < utf8.RuneCountInString(name) {
40 // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299
41 err = errors.New(Conf.Language(106))
42 return
43 }
44 if "" == name {
45 name = Conf.language(105)
46 }
47
48 FlushTxQueue()
49
50 createDocLock.Lock()
51 defer createDocLock.Unlock()
52
53 boxes, _ := ListNotebooks()
54 for i, b := range boxes {
55 c := b.GetConf()
56 c.Sort = i + 1
57 b.SaveConf(c)
58 }
59
60 id = ast.NewNodeID()
61 boxLocalPath := filepath.Join(util.DataDir, id)
62 err = os.MkdirAll(boxLocalPath, 0755)
63 if err != nil {
64 return
65 }
66
67 box := &Box{ID: id, Name: name}
68 boxConf := box.GetConf()
69 boxConf.Name = name
70 box.SaveConf(boxConf)
71 IncSync()
72 logging.LogInfof("created box [%s]", id)
73 return
74}
75
76func RenameBox(boxID, name string) (err error) {
77 box := Conf.Box(boxID)
78 if nil == box {
79 return errors.New(Conf.Language(0))
80 }
81
82 if 512 < utf8.RuneCountInString(name) {
83 // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299
84 err = errors.New(Conf.Language(106))
85 return
86 }
87
88 if "" == name {
89 name = Conf.language(105)
90 }
91
92 boxConf := box.GetConf()
93 boxConf.Name = name
94 box.Name = name
95 box.SaveConf(boxConf)
96 IncSync()
97 logging.LogInfof("renamed box [%s] to [%s]", boxID, name)
98 return
99}
100
101var boxLock = sync.Map{}
102
103func RemoveBox(boxID string) (err error) {
104 if _, ok := boxLock.Load(boxID); ok {
105 err = fmt.Errorf(Conf.language(239))
106 return
107 }
108
109 boxLock.Store(boxID, true)
110 defer boxLock.Delete(boxID)
111
112 if util.IsReservedFilename(boxID) {
113 return errors.New(fmt.Sprintf("can not remove [%s] caused by it is a reserved file", boxID))
114 }
115
116 FlushTxQueue()
117 isUserGuide := IsUserGuide(boxID)
118 createDocLock.Lock()
119 defer createDocLock.Unlock()
120
121 localPath := filepath.Join(util.DataDir, boxID)
122 if !filelock.IsExist(localPath) {
123 return
124 }
125 if !gulu.File.IsDir(localPath) {
126 return errors.New(fmt.Sprintf("can not remove [%s] caused by it is not a dir", boxID))
127 }
128
129 if !isUserGuide {
130 var historyDir string
131 historyDir, err = GetHistoryDir(HistoryOpDelete)
132 if err != nil {
133 logging.LogErrorf("get history dir failed: %s", err)
134 return
135 }
136 p := strings.TrimPrefix(localPath, util.DataDir)
137 historyPath := filepath.Join(historyDir, p)
138 if err = filelock.Copy(localPath, historyPath); err != nil {
139 logging.LogErrorf("gen sync history failed: %s", err)
140 return
141 }
142
143 copyBoxAssetsToDataAssets(boxID)
144 }
145
146 unmount0(boxID)
147 if err = filelock.Remove(localPath); err != nil {
148 return
149 }
150 IncSync()
151
152 logging.LogInfof("removed box [%s]", boxID)
153 return
154}
155
156func Unmount(boxID string) {
157 FlushTxQueue()
158
159 unmount0(boxID)
160 evt := util.NewCmdResult("unmount", 0, util.PushModeBroadcast)
161 evt.Data = map[string]interface{}{
162 "box": boxID,
163 }
164 util.PushEvent(evt)
165}
166
167func unmount0(boxID string) {
168 box := Conf.Box(boxID)
169 if nil == box {
170 return
171 }
172
173 boxConf := box.GetConf()
174 boxConf.Closed = true
175 box.SaveConf(boxConf)
176 box.Unindex()
177}
178
179func Mount(boxID string) (alreadyMount bool, err error) {
180 if _, ok := boxLock.Load(boxID); ok {
181 err = fmt.Errorf(Conf.language(239))
182 return
183 }
184
185 boxLock.Store(boxID, true)
186 defer boxLock.Delete(boxID)
187
188 FlushTxQueue()
189 isUserGuide := IsUserGuide(boxID)
190
191 localPath := filepath.Join(util.DataDir, boxID)
192 var reMountGuide bool
193 if isUserGuide {
194 // 重新挂载帮助文档
195
196 guideBox := Conf.Box(boxID)
197 if nil != guideBox {
198 unmount0(guideBox.ID)
199 reMountGuide = true
200 }
201
202 if err = filelock.Remove(localPath); err != nil {
203 return
204 }
205
206 boxes, _ := ListNotebooks()
207 var sort int
208 if len(boxes) > 0 {
209 sort = boxes[0].Sort - 1
210 }
211
212 p := filepath.Join(util.WorkingDir, "guide", boxID)
213 if err = filelock.Copy(p, localPath); err != nil {
214 return
215 }
216
217 avDirPath := filepath.Join(util.WorkingDir, "guide", boxID, "storage", "av")
218 if filelock.IsExist(avDirPath) {
219 if err = filelock.Copy(avDirPath, filepath.Join(util.DataDir, "storage", "av")); err != nil {
220 return
221 }
222 }
223
224 if box := Conf.Box(boxID); nil != box {
225 boxConf := box.GetConf()
226 boxConf.Closed = true
227 boxConf.Sort = sort
228 box.SaveConf(boxConf)
229 }
230
231 if Conf.OpenHelp {
232 Conf.OpenHelp = false
233 Conf.Save()
234 }
235
236 task.AppendAsyncTaskWithDelay(task.PushMsg, 3*time.Second, util.PushErrMsg, Conf.Language(52), 7000)
237 go func() {
238 // 每次打开帮助文档时自动检查版本更新并提醒 https://github.com/siyuan-note/siyuan/issues/5057
239 time.Sleep(time.Second * 10)
240 CheckUpdate(true)
241 }()
242 }
243
244 if !gulu.File.IsDir(localPath) {
245 return false, errors.New("can not open file, just support open folder only")
246 }
247
248 for _, box := range Conf.GetOpenedBoxes() {
249 if box.ID == boxID {
250 return true, nil
251 }
252 }
253
254 box := &Box{ID: boxID}
255 boxConf := box.GetConf()
256 boxConf.Closed = false
257 box.SaveConf(boxConf)
258
259 box.Index()
260 // 缓存根一级的文档树展开
261 ListDocTree(box.ID, "/", util.SortModeUnassigned, false, false, Conf.FileTree.MaxListCount)
262 util.ClearPushProgress(100)
263
264 if reMountGuide {
265 return true, nil
266 }
267 return false, nil
268}
269
270func IsUserGuide(boxID string) bool {
271 return "20210808180117-czj9bvb" == boxID || "20210808180117-6v0mkxr" == boxID || "20211226090932-5lcq56f" == boxID || "20240530133126-axarxgx" == boxID
272}