Skip to content

Commit 275cf86

Browse files
committed
完成markdown导出功能
1 parent 24c2603 commit 275cf86

File tree

6 files changed

+166
-137
lines changed

6 files changed

+166
-137
lines changed

controllers/BookController.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,3 +1289,29 @@ func (this *BookController) Comment() {
12891289
}
12901290
this.JsonResult(1, "书籍不存在")
12911291
}
1292+
1293+
// ExportMarkdown 将书籍导出为markdown
1294+
// 注意:系统管理员和书籍参与者有权限导出
1295+
func (this *BookController) Export2Markdown() {
1296+
identify := this.GetString("identify")
1297+
if this.Member.MemberId == 0 {
1298+
this.JsonResult(1, "请先登录")
1299+
}
1300+
if !this.Member.IsAdministrator() {
1301+
if _, err := models.NewBookResult().FindByIdentify(identify, this.Member.MemberId); err != nil {
1302+
this.JsonResult(1, "无操作权限")
1303+
}
1304+
}
1305+
path, err := models.NewBook().Export2Markdown(identify)
1306+
if err != nil {
1307+
this.JsonResult(1, err.Error())
1308+
}
1309+
defer func() {
1310+
os.Remove(path)
1311+
}()
1312+
attchmentName := filepath.Base(path)
1313+
if book, _ := models.NewBook().FindByIdentify(identify, "book_name", "book_id"); book != nil && book.BookId > 0 {
1314+
attchmentName = book.BookName + ".zip"
1315+
}
1316+
this.Ctx.Output.Download(strings.TrimLeft(path, "./"), attchmentName)
1317+
}

controllers/StaticController.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (this *StaticController) APP() {
3333
}
3434

3535
func (this *StaticController) Test() {
36-
beego.Error(store.ModelStoreLocal.CopyDir("/uploads/projects/0b92b4bc102f4644a6192c7e5f9d5953", "/uploads/projects/0b92b4bc102f4644a6192c7e5f9d595366666--"))
36+
// fmt.Println(models.NewBook().Export2Markdown("4b98937398e612778711608a3d09a9c4"))
3737
this.Ctx.WriteString("this is a test")
3838
}
3939

models/book.go

Lines changed: 109 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/TruthHun/BookStack/models/store"
2121
"github.com/TruthHun/BookStack/utils"
2222
"github.com/TruthHun/BookStack/utils/html2md"
23+
"github.com/TruthHun/gotil/filetil"
2324
"github.com/astaxie/beego"
2425
"github.com/astaxie/beego/logs"
2526
"github.com/astaxie/beego/orm"
@@ -814,190 +815,165 @@ func (m *Book) Copy(sourceBookIdentify string) (err error) {
814815
// Export2Markdown 将书籍导出markdown
815816
func (m *Book) Export2Markdown(identify string) (path string, err error) {
816817
var (
817-
book *Book
818-
baseDir = "uploads/export"
819-
files []string
820-
dirMap = make(map[string]bool)
818+
book *Book
819+
exportDir = fmt.Sprintf("uploads/export/%v", identify)
820+
attachPrefix string
821+
exportAttachPrefix = "../attachments/"
822+
docs []Document
823+
ds []DocumentStore
824+
docIds []interface{}
825+
docMap = make(map[int]Document)
826+
o = orm.NewOrm()
827+
isOSSProject = utils.StoreType == utils.StoreOss
828+
cover = ""
829+
replaces []string
821830
)
822831

832+
path = fmt.Sprintf("uploads/export/%v.zip", identify)
823833
if book, err = m.FindByIdentify(identify); err != nil {
824834
beego.Error(err)
825835
return
826836
}
827837

828-
path = fmt.Sprintf(baseDir+"/%v.zip", identify)
829-
830-
// one := NewDocument()
831-
// orm.NewOrm().QueryTable("md_documents").Filter("book_id", book.BookId).OrderBy("-ModifyTime").One(one, "ModifyTime")
832-
// if info, errInfo := os.Stat(path); errInfo == nil {
833-
// if info.ModTime().Unix() > one.ModifyTime.Unix() { //没有更新,则直接下载缓存文档
834-
// return
835-
// }
836-
// }
837-
838-
dir := fmt.Sprintf(baseDir+"/%v", identify)
839-
os.MkdirAll(dir, os.ModePerm)
838+
os.MkdirAll(filepath.Join(exportDir, "docs"), os.ModePerm)
839+
// 最后删除导出目录
840+
defer func() {
841+
os.RemoveAll(exportDir)
842+
}()
840843

841-
// cover
842-
if book.Cover != "" {
843-
file := filepath.Join(dir, "cover"+filepath.Ext(book.Cover))
844-
utils.CopyFile(file, strings.TrimLeft(book.Cover, "./"))
845-
files = append(files, file)
844+
attachPrefix = fmt.Sprintf("/uploads/projects/%s/", identify)
845+
if isOSSProject {
846+
attachPrefix = fmt.Sprintf("/projects/%s/", identify)
846847
}
847848

848-
// chapter,并修正章节链接
849-
var (
850-
docs []Document
851-
links = make(map[string]string)
852-
replaces []string
853-
linkFmt = "/docs/%v/%v"
854-
)
855-
orm.NewOrm().QueryTable(NewDocument()).Filter("book_id", book.BookId).Limit(10000).All(&docs)
856-
857-
// 查找需要替换的链接
858-
for _, item := range docs {
859-
filename := strconv.Itoa(item.DocumentId) + ".md"
860-
links[strconv.Itoa(item.DocumentId)] = filename
861-
links[fmt.Sprintf(linkFmt, identify, item.DocumentId)] = filename
862-
if item.Identify != "" {
863-
filename = strings.TrimSuffix(item.Identify, ".md") + ".md"
864-
links[fmt.Sprintf(linkFmt, identify, item.Identify)] = filename
865-
links[item.Identify] = filename
866-
}
849+
o.QueryTable(NewDocument()).Filter("book_id", book.BookId).Limit(100000).All(&docs, "document_id", "document_name", "identify")
850+
if len(docs) == 0 {
851+
err = errors.New("找不到书籍章节")
852+
return
867853
}
868854

869-
linkMDPrefix := "]("
870-
for old, new := range links {
871-
replaces = append(replaces, linkMDPrefix+old, linkMDPrefix+new)
855+
ext := ".md"
856+
replaces = append(replaces, attachPrefix, exportAttachPrefix)
857+
for _, doc := range docs {
858+
docIds = append(docIds, doc.DocumentId)
859+
identify := doc.Identify
860+
id := strconv.Itoa(doc.DocumentId)
861+
if filepath.Ext(doc.Identify) == "" {
862+
doc.Identify = doc.Identify + ext
863+
}
864+
docMap[doc.DocumentId] = doc
865+
replaces = append(replaces,
866+
"]($"+identify, "]("+doc.Identify,
867+
"href=\"$"+identify, "href=\""+doc.Identify,
868+
"]($"+id, "]("+doc.Identify,
869+
"href=\"$"+id, "href=\""+doc.Identify,
870+
)
872871
}
873872

874-
replaces = append(replaces, "[TOC]", "") // 从markdown中移除TOC
873+
replaces = append(replaces, "]($", "](", "href=\"$", "href=\"")
875874

875+
// 图片等文件附件链接、URL链接等替换
876876
replacer := strings.NewReplacer(replaces...)
877-
gitbookReplacer := strings.NewReplacer(" ", "-", "_", "", "&", "", ".", "")
878-
879-
for _, item := range docs {
880-
filename := strconv.Itoa(item.DocumentId) + ".md"
881-
if item.Identify != "" {
882-
filename = strings.TrimSuffix(item.Identify, ".md") + ".md"
883-
}
884877

885-
if strings.ToLower(filename) == "summary.md" {
878+
o.QueryTable(NewDocumentStore()).Filter("document_id__in", docIds...).Limit(100000).All(&ds)
879+
for _, item := range ds {
880+
doc, ok := docMap[item.DocumentId]
881+
if !ok {
886882
continue
887883
}
888884

889885
// 基本的链接替换
890886
md := replacer.Replace(item.Markdown)
891-
892-
file := filepath.Join(dir, filename)
893-
isDir := false
894-
// 基础图片链接替换
895-
if doc, _ := goquery.NewDocumentFromReader(strings.NewReader(item.Release)); doc != nil {
896-
txt := strings.TrimSpace(doc.Find("body").Text())
897-
if txt == "" || txt == "[TOC]" {
898-
isDir = true
899-
} else {
900-
doc.Find("img").Each(func(idx int, sel *goquery.Selection) {
901-
if src, ok := sel.Attr("src"); ok {
902-
srcName := strings.TrimLeft(src, "./")
903-
imgFile := filepath.Join(dir, srcName)
904-
utils.CopyFile(imgFile, srcName)
905-
md = strings.ReplaceAll(md, linkMDPrefix+src, linkMDPrefix+srcName)
906-
files = append(files, imgFile)
907-
}
908-
})
909-
doc.Find("a").Each(func(idx int, sel *goquery.Selection) {
910-
if href, ok := sel.Attr("href"); ok {
911-
newHref := href
912-
if !strings.Contains(href, ".md") {
913-
if strings.Contains(href, "#") {
914-
newHref = strings.ReplaceAll(href, "#", ".html#")
915-
} else {
916-
newHref = href + ".html"
917-
}
918-
}
919-
if slice := strings.Split(newHref, "#"); len(slice) > 1 {
920-
newHref = slice[0] + "#" + gitbookReplacer.Replace(strings.Join(slice[1:], "#"))
921-
}
922-
md = strings.ReplaceAll(md, "]("+href, "]("+newHref)
923-
}
924-
})
925-
}
926-
}
927-
928-
md = strings.ReplaceAll(md, "](/docs/", "](../")
929-
930-
if isDir {
931-
dirMap[filename] = true
932-
dirMap[strconv.Itoa(item.DocumentId)+".md"] = true
933-
} else {
934-
ioutil.WriteFile(file, []byte(md), os.ModePerm)
935-
files = append(files, file)
936-
}
887+
file := filepath.Join(exportDir, "docs", doc.Identify)
888+
ioutil.WriteFile(file, []byte(md), os.ModePerm)
937889
}
938890

939-
// 生成新的 SUMMARY 文档
891+
// SUMMARY 文档内容
940892
docModel := NewDocument()
941893
cont, _ := docModel.CreateDocumentTreeForHtml(book.BookId, 0)
942-
prefix := "/docs/" + book.Identify + "/"
943-
if doc, _ := goquery.NewDocumentFromReader(strings.NewReader(cont)); doc != nil {
944-
doc.Find("a").Each(func(idx int, sel *goquery.Selection) {
894+
// 把最后没有 .md 结尾的链接替换为 .md 结尾
895+
if gq, _ := goquery.NewDocumentFromReader(strings.NewReader(cont)); gq != nil {
896+
gq.Find("a").Each(func(i int, sel *goquery.Selection) {
945897
if href, ok := sel.Attr("href"); ok {
946-
href = strings.TrimPrefix(href, prefix)
947-
if !strings.Contains(href, ".md") {
948-
if strings.Contains(href, "#") {
949-
href = strings.Replace(href, "#", ".md#", 1)
950-
} else {
951-
href = href + ".md"
952-
}
953-
}
954-
title := strings.ToLower(strings.TrimSpace(sel.Text()))
955-
if slice := strings.Split(href, "#"); len(slice) > 0 {
956-
href = slice[0] + "#" + gitbookReplacer.Replace(title)
957-
}
958-
sel.SetAttr("href", href)
959-
if _, ok := dirMap[strings.Split(href, "#")[0]]; ok {
960-
sel.BeforeHtml(title)
961-
sel.Remove()
898+
if strings.ToLower(filepath.Ext(href)) != ".md" {
899+
href = href + ".md"
900+
sel.SetAttr("href", href)
962901
}
963902
}
964903
})
965-
cont, _ = doc.Html()
904+
cont, _ = gq.Html()
966905
}
967-
968906
md := html2md.Convert(cont)
907+
md = strings.ReplaceAll(md, fmt.Sprintf("](/read/%s/", identify), "](docs/")
969908
md = fmt.Sprintf("- [%v](README.md)\n", book.BookName) + md
970-
971-
// 删除和覆盖已存在的summary文件
972-
os.Remove(filepath.Join(dir, "summary.md"))
973-
summaryFile := filepath.Join(dir, "SUMMARY.md")
909+
summaryFile := filepath.Join(exportDir, "SUMMARY.md")
974910
ioutil.WriteFile(summaryFile, []byte(md), os.ModePerm)
975911

912+
// 书籍封面处理
913+
if book.Cover != "" {
914+
cover = fmt.Sprintf("![封面](attachments/%s)", strings.TrimPrefix(strings.TrimLeft(strings.ReplaceAll(book.Cover, "\\", "/"), "./"), strings.TrimLeft(attachPrefix, "./")))
915+
}
916+
976917
// README
977-
md = fmt.Sprintf("# %+v\n\n", book.BookName) + md
978-
readmeFile := filepath.Join(dir, "README.md")
918+
md = fmt.Sprintf("# %s\n\n%s\n\n%s\n\n\n\n## 目录\n\n%s", book.BookName, cover, book.Description, md)
919+
readmeFile := filepath.Join(exportDir, "README.md")
979920
ioutil.WriteFile(readmeFile, []byte(md), os.ModePerm)
980921

981-
files = append(files, summaryFile, readmeFile)
922+
dst := filepath.Join(exportDir, "attachments")
923+
src := fmt.Sprintf("projects/%s", identify)
924+
if !isOSSProject {
925+
src = "uploads/" + src
926+
}
927+
928+
if errDown := m.down2local(src, dst); errDown != nil {
929+
beego.Error("down2local error:", errDown.Error())
930+
}
931+
932+
err = m.zip(exportDir, path)
933+
if err != nil {
934+
beego.Error("压缩失败:", err.Error())
935+
}
936+
return
937+
}
938+
939+
// 把书籍相关附件下载到本地
940+
func (m *Book) down2local(srcDir, desDir string) (err error) {
941+
if utils.StoreType == utils.StoreLocal {
942+
return store.ModelStoreLocal.CopyDir(srcDir, desDir)
943+
}
944+
return store.ModelStoreOss.Down2local(srcDir, desDir)
945+
}
982946

947+
func (m *Book) zip(dir, zipFile string) (err error) {
983948
// zip 压缩
984949

985950
var (
986951
d *os.File
987952
fw io.Writer
988953
fcont []byte
954+
fl []filetil.FileList
989955
)
990-
os.Remove(path)
991-
d, err = os.Create(path)
956+
957+
if fl, err = filetil.ScanFiles(dir); err != nil {
958+
return
959+
}
960+
961+
os.Remove(zipFile)
962+
d, err = os.Create(zipFile)
992963
if err != nil {
993964
beego.Error(err)
994965
return
995966
}
996967
defer d.Close()
997968
zipWriter := zip.NewWriter(d)
998969
defer zipWriter.Close()
999-
for _, file := range files {
1000-
info, errInfo := os.Stat(file)
970+
971+
for _, file := range fl {
972+
if file.IsDir {
973+
continue
974+
}
975+
976+
info, errInfo := os.Stat(file.Path)
1001977
if errInfo != nil {
1002978
beego.Error(errInfo)
1003979
continue
@@ -1010,15 +986,15 @@ func (m *Book) Export2Markdown(identify string) (path string, err error) {
1010986
}
1011987

1012988
header.Method = zip.Deflate
1013-
header.Name = strings.TrimLeft(strings.TrimPrefix(strings.ReplaceAll(file, "\\", "/"), dir), "./")
989+
header.Name = strings.TrimLeft(strings.TrimPrefix(strings.ReplaceAll(file.Path, "\\", "/"), dir), "./")
1014990

1015991
fw, err = zipWriter.CreateHeader(header)
1016992
if err != nil {
1017993
beego.Error(err)
1018994
return
1019995
}
1020996

1021-
fcont, err = ioutil.ReadFile(file)
997+
fcont, err = ioutil.ReadFile(file.Path)
1022998
if err != nil {
1023999
return
10241000
}
@@ -1027,6 +1003,5 @@ func (m *Book) Export2Markdown(identify string) (path string, err error) {
10271003
return
10281004
}
10291005
}
1030-
os.RemoveAll(dir)
10311006
return
10321007
}

models/store/oss.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,26 @@ func (o *Oss) CopyDir(sourceDir, targetDir string) (err error) {
275275
}
276276
return
277277
}
278+
279+
// Down2local 下载文件夹下的文件到本地
280+
func (o *Oss) Down2local(sourceDir, targetDir string) (err error) {
281+
var (
282+
objects []string
283+
bucket, _ = o.GetBucket()
284+
)
285+
286+
sourceDir = strings.TrimLeft(sourceDir, "./")
287+
targetDir = strings.TrimLeft(targetDir, "./")
288+
objects, err = o.ListObjects(sourceDir)
289+
if err != nil {
290+
return
291+
}
292+
for _, obj := range objects {
293+
targetFile := strings.ReplaceAll(filepath.Join(targetDir, strings.TrimPrefix(obj, sourceDir)), "\\", "/")
294+
os.MkdirAll(filepath.Dir(targetFile), os.ModePerm)
295+
if err = bucket.GetObjectToFile(obj, targetFile); err != nil {
296+
beego.Error("download:", obj, "==>", targetFile, err.Error())
297+
}
298+
}
299+
return
300+
}

0 commit comments

Comments
 (0)