123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055 |
- // SiYuan - Refactor your thinking
- // Copyright (c) 2020-present, b3log.org
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- package model
- import (
- "bytes"
- "errors"
- "fmt"
- "os"
- "path"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "sync"
- "time"
- "unicode/utf8"
- "github.com/88250/gulu"
- "github.com/88250/lute"
- "github.com/88250/lute/ast"
- "github.com/88250/lute/html"
- "github.com/88250/lute/parse"
- util2 "github.com/88250/lute/util"
- "github.com/dustin/go-humanize"
- "github.com/facette/natsort"
- "github.com/siyuan-note/filelock"
- "github.com/siyuan-note/logging"
- "github.com/siyuan-note/riff"
- "github.com/siyuan-note/siyuan/kernel/av"
- "github.com/siyuan-note/siyuan/kernel/cache"
- "github.com/siyuan-note/siyuan/kernel/conf"
- "github.com/siyuan-note/siyuan/kernel/filesys"
- "github.com/siyuan-note/siyuan/kernel/search"
- "github.com/siyuan-note/siyuan/kernel/sql"
- "github.com/siyuan-note/siyuan/kernel/task"
- "github.com/siyuan-note/siyuan/kernel/treenode"
- "github.com/siyuan-note/siyuan/kernel/util"
- )
- type File struct {
- Path string `json:"path"`
- Name string `json:"name"` // 标题,即 ial["title"]
- Icon string `json:"icon"`
- Name1 string `json:"name1"` // 命名,即 ial["name"]
- Alias string `json:"alias"`
- Memo string `json:"memo"`
- Bookmark string `json:"bookmark"`
- ID string `json:"id"`
- Count int `json:"count"`
- Size uint64 `json:"size"`
- HSize string `json:"hSize"`
- Mtime int64 `json:"mtime"`
- CTime int64 `json:"ctime"`
- HMtime string `json:"hMtime"`
- HCtime string `json:"hCtime"`
- Sort int `json:"sort"`
- SubFileCount int `json:"subFileCount"`
- Hidden bool `json:"hidden"`
- NewFlashcardCount int `json:"newFlashcardCount"`
- DueFlashcardCount int `json:"dueFlashcardCount"`
- FlashcardCount int `json:"flashcardCount"`
- }
- func (box *Box) docFromFileInfo(fileInfo *FileInfo, ial map[string]string) (ret *File) {
- ret = &File{}
- ret.Path = fileInfo.path
- ret.Size = uint64(fileInfo.size)
- ret.Name = ial["title"] + ".sy"
- ret.Icon = ial["icon"]
- ret.ID = ial["id"]
- ret.Name1 = ial["name"]
- ret.Alias = ial["alias"]
- ret.Memo = ial["memo"]
- ret.Bookmark = ial["bookmark"]
- t, _ := time.ParseInLocation("20060102150405", ret.ID[:14], time.Local)
- ret.CTime = t.Unix()
- ret.HCtime = t.Format("2006-01-02 15:04:05")
- ret.HSize = humanize.Bytes(ret.Size)
- mTime := t
- if updated := ial["updated"]; "" != updated {
- if updatedTime, err := time.ParseInLocation("20060102150405", updated, time.Local); nil == err {
- mTime = updatedTime
- }
- }
- ret.Mtime = mTime.Unix()
- ret.HMtime = util.HumanizeTime(mTime, Conf.Lang)
- return
- }
- func (box *Box) docIAL(p string) (ret map[string]string) {
- name := strings.ToLower(filepath.Base(p))
- if !strings.HasSuffix(name, ".sy") {
- return nil
- }
- ret = cache.GetDocIAL(p)
- if nil != ret {
- return ret
- }
- filePath := filepath.Join(util.DataDir, box.ID, p)
- data, err := filelock.ReadFile(filePath)
- if util.IsCorruptedSYData(data) {
- box.moveCorruptedData(filePath)
- return nil
- }
- if nil != err {
- logging.LogErrorf("read file [%s] failed: %s", p, err)
- return nil
- }
- ret = filesys.ReadDocIAL(data)
- if 1 > len(ret) {
- logging.LogWarnf("tree [%s] is corrupted", filePath)
- box.moveCorruptedData(filePath)
- return nil
- }
- cache.PutDocIAL(p, ret)
- return ret
- }
- func (box *Box) moveCorruptedData(filePath string) {
- base := filepath.Base(filePath)
- to := filepath.Join(util.WorkspaceDir, "corrupted", time.Now().Format("2006-01-02-150405"), box.ID, base)
- if copyErr := filelock.Copy(filePath, to); nil != copyErr {
- logging.LogErrorf("copy corrupted data file [%s] failed: %s", filePath, copyErr)
- return
- }
- if removeErr := filelock.Remove(filePath); nil != removeErr {
- logging.LogErrorf("remove corrupted data file [%s] failed: %s", filePath, removeErr)
- return
- }
- logging.LogWarnf("moved corrupted data file [%s] to [%s]", filePath, to)
- }
- func SearchDocsByKeyword(keyword string, flashcard bool) (ret []map[string]string) {
- ret = []map[string]string{}
- var deck *riff.Deck
- var deckBlockIDs []string
- if flashcard {
- deck = Decks[builtinDeckID]
- if nil == deck {
- return
- }
- deckBlockIDs = deck.GetBlockIDs()
- }
- openedBoxes := Conf.GetOpenedBoxes()
- boxes := map[string]*Box{}
- for _, box := range openedBoxes {
- boxes[box.ID] = box
- }
- var rootBlocks []*sql.Block
- if "" != keyword {
- for _, box := range boxes {
- if strings.Contains(box.Name, keyword) {
- if flashcard {
- newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs)
- if 0 < flashcardCount {
- ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)})
- }
- } else {
- ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon})
- }
- }
- }
- condition := "hpath LIKE '%" + keyword + "%'"
- if "" != keyword {
- namCondition := Conf.Search.NAMFilter(keyword)
- if "" != namCondition {
- condition += " " + namCondition
- }
- }
- rootBlocks = sql.QueryRootBlockByCondition(condition)
- } else {
- for _, box := range boxes {
- if flashcard {
- newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs)
- if 0 < flashcardCount {
- ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)})
- }
- } else {
- ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon})
- }
- }
- }
- for _, rootBlock := range rootBlocks {
- b := boxes[rootBlock.Box]
- if nil == b {
- continue
- }
- hPath := b.Name + rootBlock.HPath
- if flashcard {
- newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootBlock.ID, deck, deckBlockIDs)
- if 0 < flashcardCount {
- ret = append(ret, map[string]string{"path": rootBlock.Path, "hPath": hPath, "box": rootBlock.Box, "boxIcon": b.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)})
- }
- } else {
- ret = append(ret, map[string]string{"path": rootBlock.Path, "hPath": hPath, "box": rootBlock.Box, "boxIcon": b.Icon})
- }
- }
- sort.Slice(ret, func(i, j int) bool {
- return ret[i]["hPath"] < ret[j]["hPath"]
- })
- return
- }
- type FileInfo struct {
- path string
- name string
- size int64
- isdir bool
- }
- func ListDocTree(boxID, listPath string, sortMode int, flashcard, showHidden bool, maxListCount int) (ret []*File, totals int, err error) {
- //os.MkdirAll("pprof", 0755)
- //cpuProfile, _ := os.Create("pprof/cpu_profile_list_doc_tree")
- //pprof.StartCPUProfile(cpuProfile)
- //defer pprof.StopCPUProfile()
- ret = []*File{}
- var deck *riff.Deck
- var deckBlockIDs []string
- if flashcard {
- deck = Decks[builtinDeckID]
- if nil == deck {
- return
- }
- deckBlockIDs = deck.GetBlockIDs()
- }
- box := Conf.Box(boxID)
- if nil == box {
- return nil, 0, errors.New(Conf.Language(0))
- }
- boxConf := box.GetConf()
- if util.SortModeUnassigned == sortMode {
- sortMode = Conf.FileTree.Sort
- if util.SortModeFileTree != boxConf.SortMode {
- sortMode = boxConf.SortMode
- }
- }
- var files []*FileInfo
- start := time.Now()
- files, totals, err = box.Ls(listPath)
- if nil != err {
- return
- }
- elapsed := time.Now().Sub(start).Milliseconds()
- if 100 < elapsed {
- logging.LogWarnf("ls elapsed [%dms]", elapsed)
- }
- start = time.Now()
- boxLocalPath := filepath.Join(util.DataDir, box.ID)
- var docs []*File
- for _, file := range files {
- if file.isdir {
- if !ast.IsNodeIDPattern(file.name) {
- continue
- }
- parentDocPath := strings.TrimSuffix(file.path, "/") + ".sy"
- parentDocFile := box.Stat(parentDocPath)
- if nil == parentDocFile {
- continue
- }
- if ial := box.docIAL(parentDocPath); nil != ial {
- if !showHidden && "true" == ial["custom-hidden"] {
- continue
- }
- doc := box.docFromFileInfo(parentDocFile, ial)
- subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, file.path))
- if nil == err {
- for _, subFile := range subFiles {
- subDocFilePath := path.Join(file.path, subFile.Name())
- if subIAL := box.docIAL(subDocFilePath); "true" == subIAL["custom-hidden"] {
- continue
- }
- if strings.HasSuffix(subFile.Name(), ".sy") {
- doc.SubFileCount++
- }
- }
- }
- if flashcard {
- rootID := strings.TrimSuffix(filepath.Base(parentDocPath), ".sy")
- newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs)
- if 0 < flashcardCount {
- doc.NewFlashcardCount = newFlashcardCount
- doc.DueFlashcardCount = dueFlashcardCount
- doc.FlashcardCount = flashcardCount
- docs = append(docs, doc)
- }
- } else {
- docs = append(docs, doc)
- }
- }
- continue
- }
- subFolder := filepath.Join(boxLocalPath, strings.TrimSuffix(file.path, ".sy"))
- if gulu.File.IsDir(subFolder) {
- continue
- }
- if ial := box.docIAL(file.path); nil != ial {
- if !showHidden && "true" == ial["custom-hidden"] {
- continue
- }
- doc := box.docFromFileInfo(file, ial)
- if flashcard {
- rootID := strings.TrimSuffix(filepath.Base(file.path), ".sy")
- newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs)
- if 0 < flashcardCount {
- doc.NewFlashcardCount = newFlashcardCount
- doc.DueFlashcardCount = dueFlashcardCount
- doc.FlashcardCount = flashcardCount
- docs = append(docs, doc)
- }
- } else {
- docs = append(docs, doc)
- }
- }
- }
- elapsed = time.Now().Sub(start).Milliseconds()
- if 500 < elapsed {
- logging.LogWarnf("build docs [%d] elapsed [%dms]", len(docs), elapsed)
- }
- start = time.Now()
- refCount := sql.QueryRootBlockRefCount()
- for _, doc := range docs {
- if count := refCount[doc.ID]; 0 < count {
- doc.Count = count
- }
- }
- elapsed = time.Now().Sub(start).Milliseconds()
- if 500 < elapsed {
- logging.LogWarnf("query root block ref count elapsed [%dms]", elapsed)
- }
- start = time.Now()
- switch sortMode {
- case util.SortModeNameASC:
- sort.Slice(docs, func(i, j int) bool {
- return util.PinYinCompare(util.RemoveEmojiInvisible(docs[i].Name), util.RemoveEmojiInvisible(docs[j].Name))
- })
- case util.SortModeNameDESC:
- sort.Slice(docs, func(i, j int) bool {
- return util.PinYinCompare(util.RemoveEmojiInvisible(docs[j].Name), util.RemoveEmojiInvisible(docs[i].Name))
- })
- case util.SortModeUpdatedASC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime < docs[j].Mtime })
- case util.SortModeUpdatedDESC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime > docs[j].Mtime })
- case util.SortModeAlphanumASC:
- sort.Slice(docs, func(i, j int) bool {
- return natsort.Compare(util.RemoveEmojiInvisible(docs[i].Name), util.RemoveEmojiInvisible(docs[j].Name))
- })
- case util.SortModeAlphanumDESC:
- sort.Slice(docs, func(i, j int) bool {
- return natsort.Compare(util.RemoveEmojiInvisible(docs[j].Name), util.RemoveEmojiInvisible(docs[i].Name))
- })
- case util.SortModeCustom:
- fileTreeFiles := docs
- box.fillSort(&fileTreeFiles)
- sort.Slice(fileTreeFiles, func(i, j int) bool {
- if fileTreeFiles[i].Sort == fileTreeFiles[j].Sort {
- return util.TimeFromID(fileTreeFiles[i].ID) > util.TimeFromID(fileTreeFiles[j].ID)
- }
- return fileTreeFiles[i].Sort < fileTreeFiles[j].Sort
- })
- ret = append(ret, fileTreeFiles...)
- totals = len(ret)
- if maxListCount < len(ret) {
- ret = ret[:maxListCount]
- }
- ret = ret[:]
- return
- case util.SortModeRefCountASC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].Count < docs[j].Count })
- case util.SortModeRefCountDESC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].Count > docs[j].Count })
- case util.SortModeCreatedASC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].CTime < docs[j].CTime })
- case util.SortModeCreatedDESC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].CTime > docs[j].CTime })
- case util.SortModeSizeASC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].Size < docs[j].Size })
- case util.SortModeSizeDESC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].Size > docs[j].Size })
- case util.SortModeSubDocCountASC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount < docs[j].SubFileCount })
- case util.SortModeSubDocCountDESC:
- sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount > docs[j].SubFileCount })
- }
- if util.SortModeCustom != sortMode {
- ret = append(ret, docs...)
- }
- totals = len(ret)
- if maxListCount < len(ret) {
- ret = ret[:maxListCount]
- }
- ret = ret[:]
- elapsed = time.Now().Sub(start).Milliseconds()
- if 200 < elapsed {
- logging.LogInfof("sort docs elapsed [%dms]", elapsed)
- }
- return
- }
- func ContentStat(content string) (ret *util.BlockStatResult) {
- luteEngine := util.NewLute()
- return contentStat(content, luteEngine)
- }
- func contentStat(content string, luteEngine *lute.Lute) (ret *util.BlockStatResult) {
- tree := luteEngine.BlockDOM2Tree(content)
- runeCnt, wordCnt, linkCnt, imgCnt, refCnt := tree.Root.Stat()
- return &util.BlockStatResult{
- RuneCount: runeCnt,
- WordCount: wordCnt,
- LinkCount: linkCnt,
- ImageCount: imgCnt,
- RefCount: refCnt,
- }
- }
- func BlocksWordCount(ids []string) (ret *util.BlockStatResult) {
- ret = &util.BlockStatResult{}
- trees := map[string]*parse.Tree{} // 缓存
- luteEngine := util.NewLute()
- for _, id := range ids {
- bt := treenode.GetBlockTree(id)
- if nil == bt {
- continue
- }
- tree := trees[bt.RootID]
- if nil == tree {
- tree, _ = filesys.LoadTree(bt.BoxID, bt.Path, luteEngine)
- if nil == tree {
- continue
- }
- trees[bt.RootID] = tree
- }
- node := treenode.GetNodeInTree(tree, id)
- if nil == node {
- continue
- }
- runeCnt, wordCnt, linkCnt, imgCnt, refCnt := node.Stat()
- ret.RuneCount += runeCnt
- ret.WordCount += wordCnt
- ret.LinkCount += linkCnt
- ret.ImageCount += imgCnt
- ret.RefCount += refCnt
- }
- return
- }
- func StatTree(id string) (ret *util.BlockStatResult) {
- WaitForWritingFiles()
- tree, _ := LoadTreeByBlockID(id)
- if nil == tree {
- return
- }
- var databaseBlockNodes []*ast.Node
- ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering || ast.NodeAttributeView != n.Type {
- return ast.WalkContinue
- }
- databaseBlockNodes = append(databaseBlockNodes, n)
- return ast.WalkContinue
- })
- luteEngine := util.NewLute()
- var dbRuneCnt, dbWordCnt, dbLinkCnt, dbImgCnt, dbRefCnt int
- for _, n := range databaseBlockNodes {
- if "" == n.AttributeViewID {
- continue
- }
- attrView, _ := av.ParseAttributeView(n.AttributeViewID)
- if nil == attrView {
- continue
- }
- content := bytes.Buffer{}
- for _, kValues := range attrView.KeyValues {
- for _, v := range kValues.Values {
- switch kValues.Key.Type {
- case av.KeyTypeURL:
- if v.IsEmpty() {
- continue
- }
- dbLinkCnt++
- content.WriteString(v.URL.Content)
- case av.KeyTypeMAsset:
- if v.IsEmpty() {
- continue
- }
- for _, asset := range v.MAsset {
- if av.AssetTypeImage == asset.Type {
- dbImgCnt++
- }
- }
- case av.KeyTypeBlock:
- if v.IsEmpty() {
- continue
- }
- if !v.IsDetached {
- dbRefCnt++
- }
- content.WriteString(v.Block.Content)
- case av.KeyTypeText:
- if v.IsEmpty() {
- continue
- }
- content.WriteString(v.Text.Content)
- case av.KeyTypeNumber:
- if v.IsEmpty() {
- continue
- }
- v.Number.FormatNumber()
- content.WriteString(v.Number.FormattedContent)
- case av.KeyTypeEmail:
- if v.IsEmpty() {
- continue
- }
- content.WriteString(v.Email.Content)
- case av.KeyTypePhone:
- if v.IsEmpty() {
- continue
- }
- content.WriteString(v.Phone.Content)
- }
- }
- }
- dbStat := contentStat(content.String(), luteEngine)
- dbRuneCnt += dbStat.RuneCount
- dbWordCnt += dbStat.WordCount
- }
- runeCnt, wordCnt, linkCnt, imgCnt, refCnt := tree.Root.Stat()
- runeCnt += dbRuneCnt
- wordCnt += dbWordCnt
- linkCnt += dbLinkCnt
- imgCnt += dbImgCnt
- refCnt += dbRefCnt
- return &util.BlockStatResult{
- RuneCount: runeCnt,
- WordCount: wordCnt,
- LinkCount: linkCnt,
- ImageCount: imgCnt,
- RefCount: refCnt,
- }
- }
- func GetDoc(startID, endID, id string, index int, query string, queryTypes map[string]bool, queryMethod, mode int, size int, isBacklink bool) (blockCount int, dom, parentID, parent2ID, rootID, typ string, eof, scroll bool, boxID, docPath string, isBacklinkExpand bool, err error) {
- //os.MkdirAll("pprof", 0755)
- //cpuProfile, _ := os.Create("pprof/GetDoc")
- //pprof.StartCPUProfile(cpuProfile)
- //defer pprof.StopCPUProfile()
- WaitForWritingFiles() // 写入数据时阻塞,避免获取到的数据不一致
- inputIndex := index
- tree, err := LoadTreeByBlockID(id)
- if nil != err {
- if ErrBlockNotFound == err {
- if 0 == mode {
- err = ErrTreeNotFound // 初始化打开文档时如果找不到则关闭编辑器
- }
- }
- return
- }
- if nil == tree {
- err = ErrBlockNotFound
- return
- }
- luteEngine := NewLute()
- node := treenode.GetNodeInTree(tree, id)
- if nil == node {
- // Unable to open the doc when the block pointed by the scroll position does not exist https://github.com/siyuan-note/siyuan/issues/9030
- node = treenode.GetNodeInTree(tree, tree.Root.ID)
- if nil == node {
- err = ErrBlockNotFound
- return
- }
- }
- if isBacklink { // 引用计数浮窗请求,需要按照反链逻辑组装 https://github.com/siyuan-note/siyuan/issues/6853
- if ast.NodeParagraph == node.Type {
- if nil != node.Parent && ast.NodeListItem == node.Parent.Type {
- node = node.Parent
- }
- }
- }
- located := false
- isDoc := ast.NodeDocument == node.Type
- isHeading := ast.NodeHeading == node.Type
- boxID = node.Box
- docPath = node.Path
- if isDoc {
- if 4 == mode { // 加载文档末尾
- node = node.LastChild
- located = true
- // 重新计算 index
- ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering {
- return ast.WalkContinue
- }
- index++
- return ast.WalkContinue
- })
- } else {
- node = node.FirstChild
- }
- typ = ast.NodeDocument.String()
- idx := 0
- if 0 < index {
- ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering || !n.IsChildBlockOf(tree.Root, 1) {
- return ast.WalkContinue
- }
- idx++
- if index == idx {
- node = n.DocChild()
- if "1" == node.IALAttr("heading-fold") {
- // 加载到折叠标题下方块的话需要回溯到上方标题块
- for h := node.Previous; nil != h; h = h.Previous {
- if "1" == h.IALAttr("fold") {
- node = h
- break
- }
- }
- }
- located = true
- return ast.WalkStop
- }
- return ast.WalkContinue
- })
- }
- } else {
- if 0 == index && 0 != mode {
- // 非文档且没有指定 index 时需要计算 index
- ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering {
- return ast.WalkContinue
- }
- index++
- if id == n.ID {
- node = n.DocChild()
- located = true
- return ast.WalkStop
- }
- return ast.WalkContinue
- })
- }
- }
- if 1 < index && !located {
- count := 0
- ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering {
- return ast.WalkContinue
- }
- count++
- if index == count {
- node = n.DocChild()
- return ast.WalkStop
- }
- return ast.WalkContinue
- })
- }
- blockCount = tree.DocBlockCount()
- if ast.NodeDocument == node.Type {
- parentID = node.ID
- parent2ID = parentID
- } else {
- parentID = node.Parent.ID
- parent2ID = parentID
- tmp := node
- if ast.NodeListItem == node.Type {
- // 列表项聚焦返回和面包屑保持一致 https://github.com/siyuan-note/siyuan/issues/4914
- tmp = node.Parent
- }
- if headingParent := treenode.HeadingParent(tmp); nil != headingParent {
- parent2ID = headingParent.ID
- }
- }
- rootID = tree.Root.ID
- if !isDoc {
- typ = node.Type.String()
- }
- // 判断是否需要显示动态加载滚动条 https://github.com/siyuan-note/siyuan/issues/7693
- childCount := 0
- ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering {
- return ast.WalkContinue
- }
- if 1 > childCount {
- childCount = 1
- } else {
- childCount += treenode.CountBlockNodes(n)
- }
- if childCount > Conf.Editor.DynamicLoadBlocks && blockCount > conf.MinDynamicLoadBlocks {
- scroll = true
- return ast.WalkStop
- }
- return ast.WalkContinue
- })
- var nodes []*ast.Node
- if isBacklink {
- // 引用计数浮窗请求,需要按照反链逻辑组装 https://github.com/siyuan-note/siyuan/issues/6853
- nodes, isBacklinkExpand = getBacklinkRenderNodes(node)
- } else {
- // 如果同时存在 startID 和 endID,并且是动态加载的情况,则只加载 startID 和 endID 之间的块 [startID, endID]
- if "" != startID && "" != endID && scroll {
- nodes, eof = loadNodesByStartEnd(tree, startID, endID)
- if 1 > len(nodes) {
- // 按 mode 加载兜底
- nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading)
- } else {
- // 文档块没有指定 index 时需要计算 index,否则初次打开文档时 node-index 会为 0,导致首次 Ctrl+Home 无法回到顶部
- ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering {
- return ast.WalkContinue
- }
- index++
- if nodes[0].ID == n.ID {
- return ast.WalkStop
- }
- return ast.WalkContinue
- })
- }
- } else {
- nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading)
- }
- }
- refCount := sql.QueryRootChildrenRefCount(rootID)
- virtualBlockRefKeywords := getBlockVirtualRefKeywords(tree.Root)
- subTree := &parse.Tree{ID: rootID, Root: &ast.Node{Type: ast.NodeDocument}, Marks: tree.Marks}
- var keywords []string
- if "" != query && (0 == queryMethod || 1 == queryMethod) { // 只有关键字搜索和查询语法搜索才支持高亮
- if 0 == queryMethod {
- query = stringQuery(query)
- }
- typeFilter := buildTypeFilter(queryTypes)
- keywords = highlightByQuery(query, typeFilter, rootID)
- }
- for _, n := range nodes {
- var unlinks []*ast.Node
- ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering {
- return ast.WalkContinue
- }
- if "1" == n.IALAttr("heading-fold") {
- // 折叠标题下被引用的块无法悬浮查看
- // The referenced block under the folded heading cannot be hovered to view https://github.com/siyuan-note/siyuan/issues/9582
- if (0 != mode && id != n.ID) || isDoc {
- unlinks = append(unlinks, n)
- return ast.WalkContinue
- }
- }
- if avs := n.IALAttr(av.NodeAttrNameAvs); "" != avs {
- // 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545
- avNames := getAvNames(n.IALAttr(av.NodeAttrNameAvs))
- if "" != avNames {
- n.SetIALAttr(av.NodeAttrViewNames, avNames)
- }
- }
- if "" != n.ID {
- // 填充块引计数
- if cnt := refCount[n.ID]; 0 < cnt {
- n.SetIALAttr("refcount", strconv.Itoa(cnt))
- }
- }
- if 0 < len(keywords) {
- hitBlock := false
- for p := n.Parent; nil != p; p = p.Parent {
- if p.ID == id {
- hitBlock = true
- break
- }
- }
- if hitBlock {
- if ast.NodeCodeBlockCode == n.Type && !treenode.IsChartCodeBlockCode(n) {
- // 支持代码块搜索定位 https://github.com/siyuan-note/siyuan/issues/5520
- code := string(n.Tokens)
- markedCode := search.EncloseHighlighting(code, keywords, search.SearchMarkLeft, search.SearchMarkRight, Conf.Search.CaseSensitive, false)
- if code != markedCode {
- n.Tokens = gulu.Str.ToBytes(markedCode)
- return ast.WalkContinue
- }
- } else if markReplaceSpan(n, &unlinks, keywords, search.MarkDataType, luteEngine) {
- return ast.WalkContinue
- }
- }
- }
- if processVirtualRef(n, &unlinks, virtualBlockRefKeywords, refCount, luteEngine) {
- return ast.WalkContinue
- }
- return ast.WalkContinue
- })
- for _, unlink := range unlinks {
- unlink.Unlink()
- }
- subTree.Root.AppendChild(n)
- }
- luteEngine.RenderOptions.NodeIndexStart = index
- dom = luteEngine.Tree2BlockDOM(subTree, luteEngine.RenderOptions)
- SetRecentDocByTree(tree)
- return
- }
- func loadNodesByStartEnd(tree *parse.Tree, startID, endID string) (nodes []*ast.Node, eof bool) {
- node := treenode.GetNodeInTree(tree, startID)
- if nil == node {
- return
- }
- nodes = append(nodes, node)
- for n := node.Next; nil != n; n = n.Next {
- if treenode.IsInFoldedHeading(n, nil) {
- continue
- }
- nodes = append(nodes, n)
- if n.ID == endID {
- if next := n.Next; nil == next {
- eof = true
- } else {
- eof = util2.IsDocIAL(n.Tokens) || util2.IsDocIAL(next.Tokens)
- }
- break
- }
- }
- return
- }
- func loadNodesByMode(node *ast.Node, inputIndex, mode, size int, isDoc, isHeading bool) (nodes []*ast.Node, eof bool) {
- if 2 == mode /* 向下 */ {
- next := node.Next
- if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") {
- // 标题展开时进行动态加载导致重复内容 https://github.com/siyuan-note/siyuan/issues/4671
- // 这里要考虑折叠标题是最后一个块的情况
- if children := treenode.HeadingChildren(node); 0 < len(children) {
- next = children[len(children)-1].Next
- }
- }
- if nil == next {
- eof = true
- } else {
- eof = util2.IsDocIAL(node.Tokens) || util2.IsDocIAL(next.Tokens)
- }
- }
- count := 0
- switch mode {
- case 0: // 仅加载当前 ID
- nodes = append(nodes, node)
- if isDoc {
- for n := node.Next; nil != n; n = n.Next {
- if treenode.IsInFoldedHeading(n, nil) {
- continue
- }
- nodes = append(nodes, n)
- if 1 > count {
- count++
- } else {
- count += treenode.CountBlockNodes(n)
- }
- if size < count {
- break
- }
- }
- } else if isHeading {
- level := node.HeadingLevel
- for n := node.Next; nil != n; n = n.Next {
- if treenode.IsInFoldedHeading(n, node) {
- // 大纲点击折叠标题跳转聚焦 https://github.com/siyuan-note/siyuan/issues/4920
- // 多级标题折叠后上级块引浮窗中未折叠 https://github.com/siyuan-note/siyuan/issues/4997
- continue
- }
- if ast.NodeHeading == n.Type {
- if n.HeadingLevel <= level {
- break
- }
- }
- nodes = append(nodes, n)
- count++
- if size < count {
- break
- }
- }
- }
- case 4: // Ctrl+End 跳转到末尾后向上加载
- for n := node; nil != n; n = n.Previous {
- if treenode.IsInFoldedHeading(n, nil) {
- continue
- }
- nodes = append([]*ast.Node{n}, nodes...)
- if 1 > count {
- count++
- } else {
- count += treenode.CountBlockNodes(n)
- }
- if size < count {
- break
- }
- }
- eof = true
- case 1: // 向上加载
- for n := node.Previous; /* 从上一个节点开始加载 */ nil != n; n = n.Previous {
- if treenode.IsInFoldedHeading(n, nil) {
- continue
- }
- nodes = append([]*ast.Node{n}, nodes...)
- if 1 > count {
- count++
- } else {
- count += treenode.CountBlockNodes(n)
- }
- if size < count {
- break
- }
- }
- eof = nil == node.Previous
- case 2: // 向下加载
- for n := node.Next; /* 从下一个节点开始加载 */ nil != n; n = n.Next {
- if treenode.IsInFoldedHeading(n, node) {
- continue
- }
- nodes = append(nodes, n)
- if 1 > count {
- count++
- } else {
- count += treenode.CountBlockNodes(n)
- }
- if size < count {
- break
- }
- }
- case 3: // 上下都加载
- for n := node; nil != n; n = n.Previous {
- if treenode.IsInFoldedHeading(n, nil) {
- continue
- }
- nodes = append([]*ast.Node{n}, nodes...)
- if 1 > count {
- count++
- } else {
- count += treenode.CountBlockNodes(n)
- }
- if 0 < inputIndex {
- if 1 < count {
- break // 滑块指示器加载
- }
- } else {
- if size < count {
- break
- }
- }
- }
- if size/2 < count {
- size = size / 2
- } else {
- size = size - count
- }
- count = 0
- for n := node.Next; nil != n; n = n.Next {
- if treenode.IsInFoldedHeading(n, nil) {
- continue
- }
- nodes = append(nodes, n)
- if 1 > count {
- count++
- } else {
- count += treenode.CountBlockNodes(n)
- }
- if 0 < inputIndex {
- if size < count {
- break
- }
- } else {
- if size < count {
- break
- }
- }
- }
- }
- return
- }
- func writeTreeUpsertQueue(tree *parse.Tree) (err error) {
- if err = filesys.WriteTree(tree); nil != err {
- return
- }
- sql.UpsertTreeQueue(tree)
- return
- }
- func writeTreeIndexQueue(tree *parse.Tree) (err error) {
- if err = filesys.WriteTree(tree); nil != err {
- return
- }
- sql.IndexTreeQueue(tree)
- return
- }
- func indexWriteTreeIndexQueue(tree *parse.Tree) (err error) {
- treenode.IndexBlockTree(tree)
- return writeTreeIndexQueue(tree)
- }
- func indexWriteTreeUpsertQueue(tree *parse.Tree) (err error) {
- treenode.IndexBlockTree(tree)
- return writeTreeUpsertQueue(tree)
- }
- func renameWriteJSONQueue(tree *parse.Tree) (err error) {
- if err = filesys.WriteTree(tree); nil != err {
- return
- }
- sql.RenameTreeQueue(tree)
- treenode.IndexBlockTree(tree)
- return
- }
- func DuplicateDoc(tree *parse.Tree) {
- msgId := util.PushMsg(Conf.Language(116), 30000)
- defer util.PushClearMsg(msgId)
- resetTree(tree, "Duplicated")
- createTreeTx(tree)
- WaitForWritingFiles()
- return
- }
- func createTreeTx(tree *parse.Tree) {
- transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}}
- PerformTransactions(&[]*Transaction{transaction})
- }
- var createDocLock = sync.Mutex{}
- func CreateDocByMd(boxID, p, title, md string, sorts []string) (tree *parse.Tree, err error) {
- createDocLock.Lock()
- defer createDocLock.Unlock()
- box := Conf.Box(boxID)
- if nil == box {
- err = errors.New(Conf.Language(0))
- return
- }
- luteEngine := util.NewLute()
- dom := luteEngine.Md2BlockDOM(md, false)
- tree, err = createDoc(box.ID, p, title, dom)
- if nil != err {
- return
- }
- WaitForWritingFiles()
- ChangeFileTreeSort(box.ID, sorts)
- return
- }
- func CreateWithMarkdown(boxID, hPath, md, parentID, id string) (retID string, err error) {
- createDocLock.Lock()
- defer createDocLock.Unlock()
- box := Conf.Box(boxID)
- if nil == box {
- err = errors.New(Conf.Language(0))
- return
- }
- WaitForWritingFiles()
- luteEngine := util.NewLute()
- dom := luteEngine.Md2BlockDOM(md, false)
- retID, err = createDocsByHPath(box.ID, hPath, dom, parentID, id)
- WaitForWritingFiles()
- return
- }
- func CreateDailyNote(boxID string) (p string, existed bool, err error) {
- createDocLock.Lock()
- defer createDocLock.Unlock()
- box := Conf.Box(boxID)
- if nil == box {
- err = ErrBoxNotFound
- return
- }
- boxConf := box.GetConf()
- if "" == boxConf.DailyNoteSavePath || "/" == boxConf.DailyNoteSavePath {
- err = errors.New(Conf.Language(49))
- return
- }
- hPath, err := RenderGoTemplate(boxConf.DailyNoteSavePath)
- if nil != err {
- return
- }
- WaitForWritingFiles()
- existRoot := treenode.GetBlockTreeRootByHPath(box.ID, hPath)
- if nil != existRoot {
- existed = true
- p = existRoot.Path
- tree, loadErr := LoadTreeByBlockID(existRoot.RootID)
- if nil != loadErr {
- logging.LogWarnf("load tree by block id [%s] failed: %v", existRoot.RootID, loadErr)
- return
- }
- p = tree.Path
- date := time.Now().Format("20060102")
- if tree.Root.IALAttr("custom-dailynote-"+date) == "" {
- tree.Root.SetIALAttr("custom-dailynote-"+date, date)
- if err = indexWriteTreeUpsertQueue(tree); nil != err {
- return
- }
- }
- return
- }
- id, err := createDocsByHPath(box.ID, hPath, "", "", "")
- if nil != err {
- return
- }
- var templateTree *parse.Tree
- var templateDom string
- if "" != boxConf.DailyNoteTemplatePath {
- tplPath := filepath.Join(util.DataDir, "templates", boxConf.DailyNoteTemplatePath)
- if !filelock.IsExist(tplPath) {
- logging.LogWarnf("not found daily note template [%s]", tplPath)
- } else {
- var renderErr error
- templateTree, templateDom, renderErr = RenderTemplate(tplPath, id, false)
- if nil != renderErr {
- logging.LogWarnf("render daily note template [%s] failed: %s", boxConf.DailyNoteTemplatePath, err)
- }
- }
- }
- if "" != templateDom {
- var tree *parse.Tree
- tree, err = LoadTreeByBlockID(id)
- if nil == err {
- tree.Root.FirstChild.Unlink()
- luteEngine := util.NewLute()
- newTree := luteEngine.BlockDOM2Tree(templateDom)
- var children []*ast.Node
- for c := newTree.Root.FirstChild; nil != c; c = c.Next {
- children = append(children, c)
- }
- for _, c := range children {
- tree.Root.AppendChild(c)
- }
- // Creating a dailynote template supports doc attributes https://github.com/siyuan-note/siyuan/issues/10698
- templateIALs := parse.IAL2Map(templateTree.Root.KramdownIAL)
- for k, v := range templateIALs {
- if "name" == k || "alias" == k || "bookmark" == k || "memo" == k || strings.HasPrefix(k, "custom-") {
- tree.Root.SetIALAttr(k, v)
- }
- }
- tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
- if err = indexWriteTreeUpsertQueue(tree); nil != err {
- return
- }
- }
- }
- IncSync()
- WaitForWritingFiles()
- tree, err := LoadTreeByBlockID(id)
- if nil != err {
- logging.LogErrorf("load tree by block id [%s] failed: %v", id, err)
- return
- }
- p = tree.Path
- date := time.Now().Format("20060102")
- tree.Root.SetIALAttr("custom-dailynote-"+date, date)
- if err = indexWriteTreeUpsertQueue(tree); nil != err {
- return
- }
- return
- }
- func GetHPathByPath(boxID, p string) (hPath string, err error) {
- if "/" == p {
- hPath = "/"
- return
- }
- luteEngine := util.NewLute()
- tree, err := filesys.LoadTree(boxID, p, luteEngine)
- if nil != err {
- return
- }
- hPath = tree.HPath
- return
- }
- func GetHPathsByPaths(paths []string) (hPaths []string, err error) {
- pathsBoxes := getBoxesByPaths(paths)
- for p, box := range pathsBoxes {
- if nil == box {
- logging.LogWarnf("box not found by path [%s]", p)
- continue
- }
- bt := treenode.GetBlockTreeByPath(p)
- if nil == bt {
- logging.LogWarnf("block tree not found by path [%s]", p)
- continue
- }
- hpath := html.UnescapeString(bt.HPath)
- hPaths = append(hPaths, box.Name+hpath)
- }
- return
- }
- func GetHPathByID(id string) (hPath string, err error) {
- tree, err := LoadTreeByBlockID(id)
- if nil != err {
- return
- }
- hPath = tree.HPath
- return
- }
- func GetFullHPathByID(id string) (hPath string, err error) {
- tree, err := LoadTreeByBlockID(id)
- if nil != err {
- return
- }
- box := Conf.Box(tree.Box)
- var boxName string
- if nil != box {
- boxName = box.Name
- }
- hPath = boxName + tree.HPath
- return
- }
- func GetIDsByHPath(hpath, boxID string) (ret []string, err error) {
- ret = []string{}
- roots := treenode.GetBlockTreeRootsByHPath(boxID, hpath)
- if 1 > len(roots) {
- return
- }
- for _, root := range roots {
- ret = append(ret, root.ID)
- }
- ret = gulu.Str.RemoveDuplicatedElem(ret)
- if 1 > len(ret) {
- ret = []string{}
- }
- return
- }
- func MoveDocs(fromPaths []string, toBoxID, toPath string, callback interface{}) (err error) {
- toBox := Conf.Box(toBoxID)
- if nil == toBox {
- err = errors.New(Conf.Language(0))
- return
- }
- fromPaths = util.FilterMoveDocFromPaths(fromPaths, toPath)
- if 1 > len(fromPaths) {
- return
- }
- pathsBoxes := getBoxesByPaths(fromPaths)
- if 1 == len(fromPaths) {
- // 移动到自己的父文档下的情况相当于不移动,直接返回
- if fromBox := pathsBoxes[fromPaths[0]]; nil != fromBox && fromBox.ID == toBoxID {
- parentDir := path.Dir(fromPaths[0])
- if ("/" == toPath && "/" == parentDir) || (parentDir+".sy" == toPath) {
- return
- }
- }
- }
- // 检查路径深度是否超过限制
- for fromPath, fromBox := range pathsBoxes {
- childDepth := util.GetChildDocDepth(filepath.Join(util.DataDir, fromBox.ID, fromPath))
- if depth := strings.Count(toPath, "/") + childDepth; 6 < depth && !Conf.FileTree.AllowCreateDeeper {
- err = errors.New(Conf.Language(118))
- return
- }
- }
- // A progress layer appears when moving more than 64 documents at once https://github.com/siyuan-note/siyuan/issues/9356
- subDocsCount := 0
- for fromPath, fromBox := range pathsBoxes {
- subDocsCount += countSubDocs(fromBox.ID, fromPath)
- }
- needShowProgress := 64 < subDocsCount
- if needShowProgress {
- defer util.PushClearProgress()
- }
- WaitForWritingFiles()
- luteEngine := util.NewLute()
- count := 0
- for fromPath, fromBox := range pathsBoxes {
- count++
- if needShowProgress {
- util.PushEndlessProgress(fmt.Sprintf(Conf.Language(70), fmt.Sprintf("%d/%d", count, len(fromPaths))))
- }
- _, err = moveDoc(fromBox, fromPath, toBox, toPath, luteEngine, callback)
- if nil != err {
- return
- }
- }
- cache.ClearDocsIAL()
- IncSync()
- return
- }
- func countSubDocs(box, p string) (ret int) {
- p = strings.TrimSuffix(p, ".sy")
- _ = filepath.Walk(filepath.Join(util.DataDir, box, p), func(path string, info os.FileInfo, err error) error {
- if nil != err {
- return err
- }
- if info.IsDir() {
- return nil
- }
- if strings.HasSuffix(path, ".sy") {
- ret++
- }
- return nil
- })
- return
- }
- func moveDoc(fromBox *Box, fromPath string, toBox *Box, toPath string, luteEngine *lute.Lute, callback interface{}) (newPath string, err error) {
- isSameBox := fromBox.ID == toBox.ID
- if isSameBox {
- if !fromBox.Exist(toPath) {
- err = ErrBlockNotFound
- return
- }
- } else {
- if !toBox.Exist(toPath) {
- err = ErrBlockNotFound
- return
- }
- }
- tree, err := filesys.LoadTree(fromBox.ID, fromPath, luteEngine)
- if nil != err {
- err = ErrBlockNotFound
- return
- }
- moveToRoot := "/" == toPath
- toBlockID := tree.ID
- fromFolder := path.Join(path.Dir(fromPath), tree.ID)
- toFolder := "/"
- if !moveToRoot {
- var toTree *parse.Tree
- if isSameBox {
- toTree, err = filesys.LoadTree(fromBox.ID, toPath, luteEngine)
- } else {
- toTree, err = filesys.LoadTree(toBox.ID, toPath, luteEngine)
- }
- if nil != err {
- err = ErrBlockNotFound
- return
- }
- toBlockID = toTree.ID
- toFolder = path.Join(path.Dir(toPath), toBlockID)
- }
- if isSameBox {
- if err = fromBox.MkdirAll(toFolder); nil != err {
- return
- }
- } else {
- if err = toBox.MkdirAll(toFolder); nil != err {
- return
- }
- }
- if fromBox.Exist(fromFolder) {
- // 移动子文档文件夹
- newFolder := path.Join(toFolder, tree.ID)
- if isSameBox {
- if err = fromBox.Move(fromFolder, newFolder); nil != err {
- return
- }
- } else {
- absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromFolder)
- absToPath := filepath.Join(util.DataDir, toBox.ID, newFolder)
- if filelock.IsExist(absToPath) {
- filelock.Remove(absToPath)
- }
- if err = filelock.Rename(absFromPath, absToPath); nil != err {
- msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err)
- logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err)
- err = errors.New(msg)
- return
- }
- }
- }
- newPath = path.Join(toFolder, tree.ID+".sy")
- if isSameBox {
- if err = fromBox.Move(fromPath, newPath); nil != err {
- return
- }
- tree, err = filesys.LoadTree(fromBox.ID, newPath, luteEngine)
- if nil != err {
- return
- }
- moveTree(tree)
- } else {
- absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromPath)
- absToPath := filepath.Join(util.DataDir, toBox.ID, newPath)
- if err = filelock.Rename(absFromPath, absToPath); nil != err {
- msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err)
- logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err)
- err = errors.New(msg)
- return
- }
- tree, err = filesys.LoadTree(toBox.ID, newPath, luteEngine)
- if nil != err {
- return
- }
- moveTree(tree)
- moveSorts(tree.ID, fromBox.ID, toBox.ID)
- }
- evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast)
- evt.Data = map[string]interface{}{
- "fromNotebook": fromBox.ID,
- "fromPath": fromPath,
- "toNotebook": toBox.ID,
- "toPath": toPath,
- "newPath": newPath,
- }
- evt.Callback = callback
- util.PushEvent(evt)
- return
- }
- func RemoveDoc(boxID, p string) {
- box := Conf.Box(boxID)
- if nil == box {
- return
- }
- WaitForWritingFiles()
- luteEngine := util.NewLute()
- removeDoc(box, p, luteEngine)
- IncSync()
- return
- }
- func RemoveDocs(paths []string) {
- util.PushEndlessProgress(Conf.Language(116))
- defer util.PushClearProgress()
- paths = util.FilterSelfChildDocs(paths)
- pathsBoxes := getBoxesByPaths(paths)
- WaitForWritingFiles()
- luteEngine := util.NewLute()
- for p, box := range pathsBoxes {
- removeDoc(box, p, luteEngine)
- }
- return
- }
- func removeDoc(box *Box, p string, luteEngine *lute.Lute) {
- tree, _ := filesys.LoadTree(box.ID, p, luteEngine)
- if nil == tree {
- return
- }
- historyDir, err := GetHistoryDir(HistoryOpDelete)
- if nil != err {
- logging.LogErrorf("get history dir failed: %s", err)
- return
- }
- historyPath := filepath.Join(historyDir, box.ID, p)
- absPath := filepath.Join(util.DataDir, box.ID, p)
- if err = filelock.Copy(absPath, historyPath); nil != err {
- logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absPath, historyPath, err)
- return
- }
- // 关联的属性视图也要复制到历史中 https://github.com/siyuan-note/siyuan/issues/9567
- avNodes := tree.Root.ChildrenByType(ast.NodeAttributeView)
- for _, avNode := range avNodes {
- srcAvPath := filepath.Join(util.DataDir, "storage", "av", avNode.AttributeViewID+".json")
- destAvPath := filepath.Join(historyDir, "storage", "av", avNode.AttributeViewID+".json")
- if copyErr := filelock.Copy(srcAvPath, destAvPath); nil != copyErr {
- logging.LogErrorf("copy av [%s] failed: %s", srcAvPath, copyErr)
- }
- }
- copyDocAssetsToDataAssets(box.ID, p)
- removeIDs := treenode.RootChildIDs(tree.ID)
- dir := path.Dir(p)
- childrenDir := path.Join(dir, tree.ID)
- existChildren := box.Exist(childrenDir)
- if existChildren {
- absChildrenDir := filepath.Join(util.DataDir, tree.Box, childrenDir)
- historyPath = filepath.Join(historyDir, tree.Box, childrenDir)
- if err = filelock.Copy(absChildrenDir, historyPath); nil != err {
- logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absChildrenDir, historyPath, err)
- return
- }
- }
- indexHistoryDir(filepath.Base(historyDir), util.NewLute())
- if existChildren {
- if err = box.Remove(childrenDir); nil != err {
- logging.LogErrorf("remove children dir [%s%s] failed: %s", box.ID, childrenDir, err)
- return
- }
- logging.LogInfof("removed children dir [%s%s]", box.ID, childrenDir)
- }
- if err = box.Remove(p); nil != err {
- logging.LogErrorf("remove [%s%s] failed: %s", box.ID, p, err)
- return
- }
- logging.LogInfof("removed doc [%s%s]", box.ID, p)
- box.removeSort(removeIDs)
- RemoveRecentDoc(removeIDs)
- if "/" != dir {
- others, err := os.ReadDir(filepath.Join(util.DataDir, box.ID, dir))
- if nil == err && 1 > len(others) {
- box.Remove(dir)
- }
- }
- evt := util.NewCmdResult("removeDoc", 0, util.PushModeBroadcast)
- evt.Data = map[string]interface{}{
- "ids": removeIDs,
- }
- util.PushEvent(evt)
- task.AppendTask(task.DatabaseIndex, removeDoc0, box, p, childrenDir)
- }
- func removeDoc0(box *Box, p, childrenDir string) {
- treenode.RemoveBlockTreesByPathPrefix(childrenDir)
- sql.RemoveTreePathQueue(box.ID, childrenDir)
- cache.RemoveDocIAL(p)
- return
- }
- func RenameDoc(boxID, p, title string) (err error) {
- box := Conf.Box(boxID)
- if nil == box {
- err = errors.New(Conf.Language(0))
- return
- }
- WaitForWritingFiles()
- luteEngine := util.NewLute()
- tree, err := filesys.LoadTree(box.ID, p, luteEngine)
- if nil != err {
- return
- }
- title = gulu.Str.RemoveInvisible(title)
- if 512 < utf8.RuneCountInString(title) {
- // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299
- return errors.New(Conf.Language(106))
- }
- oldTitle := tree.Root.IALAttr("title")
- if oldTitle == title {
- return
- }
- if "" == title {
- title = Conf.language(105)
- }
- title = strings.ReplaceAll(title, "/", "")
- tree.HPath = path.Join(path.Dir(tree.HPath), title)
- tree.Root.SetIALAttr("title", title)
- tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
- if err = renameWriteJSONQueue(tree); nil != err {
- return
- }
- refText := getNodeRefText(tree.Root)
- evt := util.NewCmdResult("rename", 0, util.PushModeBroadcast)
- evt.Data = map[string]interface{}{
- "box": boxID,
- "id": tree.Root.ID,
- "path": p,
- "title": title,
- "refText": refText,
- }
- util.PushEvent(evt)
- box.renameSubTrees(tree)
- updateRefTextRenameDoc(tree)
- IncSync()
- return
- }
- func createDoc(boxID, p, title, dom string) (tree *parse.Tree, err error) {
- title = gulu.Str.RemoveInvisible(title)
- if 512 < utf8.RuneCountInString(title) {
- // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299
- err = errors.New(Conf.Language(106))
- return
- }
- title = strings.ReplaceAll(title, "/", "")
- title = strings.TrimSpace(title)
- if "" == title {
- title = Conf.Language(105)
- }
- baseName := strings.TrimSpace(path.Base(p))
- if "" == strings.TrimSuffix(baseName, ".sy") {
- err = errors.New(Conf.Language(16))
- return
- }
- if strings.HasPrefix(baseName, ".") {
- err = errors.New(Conf.Language(13))
- return
- }
- box := Conf.Box(boxID)
- if nil == box {
- err = errors.New(Conf.Language(0))
- return
- }
- id := strings.TrimSuffix(path.Base(p), ".sy")
- var hPath string
- folder := path.Dir(p)
- if "/" != folder {
- parentID := path.Base(folder)
- parentTree, loadErr := LoadTreeByBlockID(parentID)
- if nil != loadErr {
- logging.LogErrorf("get parent tree [%s] failed", parentID)
- err = ErrBlockNotFound
- return
- }
- hPath = path.Join(parentTree.HPath, title)
- } else {
- hPath = "/" + title
- }
- if depth := strings.Count(p, "/"); 7 < depth && !Conf.FileTree.AllowCreateDeeper {
- err = errors.New(Conf.Language(118))
- return
- }
- if !box.Exist(folder) {
- if err = box.MkdirAll(folder); nil != err {
- return
- }
- }
- if box.Exist(p) {
- err = errors.New(Conf.Language(1))
- return
- }
- luteEngine := util.NewLute()
- tree = luteEngine.BlockDOM2Tree(dom)
- tree.Box = boxID
- tree.Path = p
- tree.HPath = hPath
- tree.ID = id
- tree.Root.ID = id
- tree.Root.Spec = "1"
- updated := util.TimeFromID(id)
- tree.Root.KramdownIAL = [][]string{{"id", id}, {"title", html.EscapeAttrVal(title)}, {"updated", updated}}
- if nil == tree.Root.FirstChild {
- tree.Root.AppendChild(treenode.NewParagraph())
- }
- // 如果段落块中仅包含一个 mp3/mp4 超链接,则将其转换为音视频块
- // Convert mp3 and mp4 hyperlinks to audio and video when moving cloud inbox to docs https://github.com/siyuan-note/siyuan/issues/9778
- var unlinks []*ast.Node
- ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
- if !entering {
- return ast.WalkContinue
- }
- if ast.NodeParagraph == n.Type {
- link := n.FirstChild
- if nil != link && link.IsTextMarkType("a") {
- if strings.HasSuffix(link.TextMarkAHref, ".mp3") {
- unlinks = append(unlinks, n)
- audio := &ast.Node{ID: n.ID, Type: ast.NodeAudio, Tokens: []byte("<audio controls=\"controls\" src=\"" + link.TextMarkAHref + "\" data-src=\"" + link.TextMarkAHref + "\"></audio>")}
- audio.SetIALAttr("id", n.ID)
- audio.SetIALAttr("updated", util.TimeFromID(n.ID))
- n.InsertBefore(audio)
- } else if strings.HasSuffix(link.TextMarkAHref, ".mp4") {
- unlinks = append(unlinks, n)
- video := &ast.Node{ID: n.ID, Type: ast.NodeVideo, Tokens: []byte("<video controls=\"controls\" src=\"" + link.TextMarkAHref + "\" data-src=\"" + link.TextMarkAHref + "\"></video>")}
- video.SetIALAttr("id", n.ID)
- video.SetIALAttr("updated", util.TimeFromID(n.ID))
- n.InsertBefore(video)
- }
- }
- }
- return ast.WalkContinue
- })
- for _, unlink := range unlinks {
- unlink.Unlink()
- }
- transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}}
- PerformTransactions(&[]*Transaction{transaction})
- WaitForWritingFiles()
- return
- }
- func moveSorts(rootID, fromBox, toBox string) {
- root := treenode.GetBlockTree(rootID)
- if nil == root {
- return
- }
- fromRootSorts := map[string]int{}
- ids := treenode.RootChildIDs(rootID)
- fromConfPath := filepath.Join(util.DataDir, fromBox, ".siyuan", "sort.json")
- fromFullSortIDs := map[string]int{}
- if filelock.IsExist(fromConfPath) {
- data, err := filelock.ReadFile(fromConfPath)
- if nil != err {
- logging.LogErrorf("read sort conf failed: %s", err)
- return
- }
- if err = gulu.JSON.UnmarshalJSON(data, &fromFullSortIDs); nil != err {
- logging.LogErrorf("unmarshal sort conf failed: %s", err)
- }
- }
- for _, id := range ids {
- fromRootSorts[id] = fromFullSortIDs[id]
- }
- toConfPath := filepath.Join(util.DataDir, toBox, ".siyuan", "sort.json")
- toFullSortIDs := map[string]int{}
- if filelock.IsExist(toConfPath) {
- data, err := filelock.ReadFile(toConfPath)
- if nil != err {
- logging.LogErrorf("read sort conf failed: %s", err)
- return
- }
- if err = gulu.JSON.UnmarshalJSON(data, &toFullSortIDs); nil != err {
- logging.LogErrorf("unmarshal sort conf failed: %s", err)
- return
- }
- }
- for id, sortVal := range fromRootSorts {
- toFullSortIDs[id] = sortVal
- }
- data, err := gulu.JSON.MarshalJSON(toFullSortIDs)
- if nil != err {
- logging.LogErrorf("marshal sort conf failed: %s", err)
- return
- }
- if err = filelock.WriteFile(toConfPath, data); nil != err {
- logging.LogErrorf("write sort conf failed: %s", err)
- return
- }
- }
- func ChangeFileTreeSort(boxID string, paths []string) {
- if 1 > len(paths) {
- return
- }
- WaitForWritingFiles()
- box := Conf.Box(boxID)
- sortIDs := map[string]int{}
- max := 0
- for i, p := range paths {
- id := strings.TrimSuffix(path.Base(p), ".sy")
- sortIDs[id] = i + 1
- if i == len(paths)-1 {
- max = i + 2
- }
- }
- p := paths[0]
- parentPath := path.Dir(p)
- absParentPath := filepath.Join(util.DataDir, boxID, parentPath)
- files, err := os.ReadDir(absParentPath)
- if nil != err {
- logging.LogErrorf("read dir [%s] failed: %s", absParentPath, err)
- }
- sortFolderIDs := map[string]int{}
- for _, f := range files {
- if !strings.HasSuffix(f.Name(), ".sy") {
- continue
- }
- id := strings.TrimSuffix(f.Name(), ".sy")
- val := sortIDs[id]
- if 0 == val {
- val = max
- max++
- }
- sortFolderIDs[id] = val
- }
- confDir := filepath.Join(util.DataDir, box.ID, ".siyuan")
- if err = os.MkdirAll(confDir, 0755); nil != err {
- logging.LogErrorf("create conf dir failed: %s", err)
- return
- }
- confPath := filepath.Join(confDir, "sort.json")
- fullSortIDs := map[string]int{}
- var data []byte
- if filelock.IsExist(confPath) {
- data, err = filelock.ReadFile(confPath)
- if nil != err {
- logging.LogErrorf("read sort conf failed: %s", err)
- return
- }
- if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err {
- logging.LogErrorf("unmarshal sort conf failed: %s", err)
- }
- }
- for sortID, sortVal := range sortFolderIDs {
- fullSortIDs[sortID] = sortVal
- }
- data, err = gulu.JSON.MarshalJSON(fullSortIDs)
- if nil != err {
- logging.LogErrorf("marshal sort conf failed: %s", err)
- return
- }
- if err = filelock.WriteFile(confPath, data); nil != err {
- logging.LogErrorf("write sort conf failed: %s", err)
- return
- }
- IncSync()
- }
- func (box *Box) fillSort(files *[]*File) {
- confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json")
- if !filelock.IsExist(confPath) {
- return
- }
- data, err := filelock.ReadFile(confPath)
- if nil != err {
- logging.LogErrorf("read sort conf failed: %s", err)
- return
- }
- fullSortIDs := map[string]int{}
- if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err {
- logging.LogErrorf("unmarshal sort conf failed: %s", err)
- return
- }
- for _, f := range *files {
- id := strings.TrimSuffix(f.ID, ".sy")
- f.Sort = fullSortIDs[id]
- }
- }
- func (box *Box) removeSort(ids []string) {
- confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json")
- if !filelock.IsExist(confPath) {
- return
- }
- data, err := filelock.ReadFile(confPath)
- if nil != err {
- logging.LogErrorf("read sort conf failed: %s", err)
- return
- }
- fullSortIDs := map[string]int{}
- if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err {
- logging.LogErrorf("unmarshal sort conf failed: %s", err)
- return
- }
- for _, toRemove := range ids {
- delete(fullSortIDs, toRemove)
- }
- data, err = gulu.JSON.MarshalJSON(fullSortIDs)
- if nil != err {
- logging.LogErrorf("marshal sort conf failed: %s", err)
- return
- }
- if err = filelock.WriteFile(confPath, data); nil != err {
- logging.LogErrorf("write sort conf failed: %s", err)
- return
- }
- }
- func (box *Box) addMinSort(parentPath, id string) {
- docs, _, err := ListDocTree(box.ID, parentPath, util.SortModeUnassigned, false, false, 1)
- if nil != err {
- logging.LogErrorf("list doc tree failed: %s", err)
- return
- }
- sortVal := 0
- if 0 < len(docs) {
- sortVal = docs[0].Sort - 1
- }
- confDir := filepath.Join(util.DataDir, box.ID, ".siyuan")
- if err = os.MkdirAll(confDir, 0755); nil != err {
- logging.LogErrorf("create conf dir failed: %s", err)
- return
- }
- confPath := filepath.Join(confDir, "sort.json")
- fullSortIDs := map[string]int{}
- var data []byte
- if filelock.IsExist(confPath) {
- data, err = filelock.ReadFile(confPath)
- if nil != err {
- logging.LogErrorf("read sort conf failed: %s", err)
- return
- }
- if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); nil != err {
- logging.LogErrorf("unmarshal sort conf failed: %s", err)
- }
- }
- fullSortIDs[id] = sortVal
- data, err = gulu.JSON.MarshalJSON(fullSortIDs)
- if nil != err {
- logging.LogErrorf("marshal sort conf failed: %s", err)
- return
- }
- if err = filelock.WriteFile(confPath, data); nil != err {
- logging.LogErrorf("write sort conf failed: %s", err)
- return
- }
- }
|