mindoc/models/book_result.go

621 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package models
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"encoding/base64"
"github.com/PuerkitoBio/goquery"
"github.com/astaxie/beego"
"github.com/astaxie/beego/logs"
"github.com/astaxie/beego/orm"
"github.com/lifei6671/mindoc/conf"
"github.com/lifei6671/mindoc/converter"
"github.com/lifei6671/mindoc/utils/filetil"
"github.com/lifei6671/mindoc/utils/ziptil"
"gopkg.in/russross/blackfriday.v2"
"regexp"
"github.com/lifei6671/mindoc/utils/cryptil"
"github.com/lifei6671/mindoc/utils/requests"
)
type BookResult struct {
BookId int `json:"book_id"`
BookName string `json:"book_name"`
Identify string `json:"identify"`
OrderIndex int `json:"order_index"`
Description string `json:"description"`
Publisher string `json:"publisher"`
PrivatelyOwned int `json:"privately_owned"`
PrivateToken string `json:"private_token"`
DocCount int `json:"doc_count"`
CommentStatus string `json:"comment_status"`
CommentCount int `json:"comment_count"`
CreateTime time.Time `json:"create_time"`
CreateName string `json:"create_name"`
RealName string `json:"real_name"`
ModifyTime time.Time `json:"modify_time"`
Cover string `json:"cover"`
Theme string `json:"theme"`
Label string `json:"label"`
MemberId int `json:"member_id"`
Editor string `json:"editor"`
AutoRelease bool `json:"auto_release"`
HistoryCount int `json:"history_count"`
RelationshipId int `json:"relationship_id"`
RoleId int `json:"role_id"`
RoleName string `json:"role_name"`
Status int `json:"status"`
IsEnableShare bool `json:"is_enable_share"`
IsUseFirstDocument bool `json:"is_use_first_document"`
LastModifyText string `json:"last_modify_text"`
IsDisplayComment bool `json:"is_display_comment"`
IsDownload bool `json:"is_download"`
IsLock bool `json:"is_lock"`
}
func NewBookResult() *BookResult {
return &BookResult{}
}
// 根据项目标识查询项目以及指定用户权限的信息.
func (m *BookResult) FindByIdentify(identify string, memberId int) (*BookResult, error) {
if identify == "" || memberId <= 0 {
return m, ErrInvalidParameter
}
o := orm.NewOrm()
book := NewBook()
err := o.QueryTable(book.TableNameWithPrefix()).Filter("identify", identify).One(book)
if err != nil {
return m, err
}
relationship := NewRelationship()
err = o.QueryTable(relationship.TableNameWithPrefix()).Filter("book_id", book.BookId).Filter("member_id", memberId).One(relationship)
if err != nil {
return m, err
}
var relationship2 Relationship
err = o.QueryTable(relationship.TableNameWithPrefix()).Filter("book_id", book.BookId).Filter("role_id", 0).One(&relationship2)
if err != nil {
logs.Error("根据项目标识查询项目以及指定用户权限的信息 => ", err)
return m, ErrPermissionDenied
}
member, err := NewMember().Find(relationship2.MemberId)
if err != nil {
return m, err
}
m = NewBookResult().ToBookResult(*book)
m.CreateName = member.Account
if member.RealName != "" {
m.RealName = member.RealName
}
m.MemberId = relationship.MemberId
m.RoleId = relationship.RoleId
m.RelationshipId = relationship.RelationshipId
if m.RoleId == conf.BookFounder {
m.RoleName = "创始人"
} else if m.RoleId == conf.BookAdmin {
m.RoleName = "管理员"
} else if m.RoleId == conf.BookEditor {
m.RoleName = "编辑者"
} else if m.RoleId == conf.BookObserver {
m.RoleName = "观察者"
}
doc := NewDocument()
err = o.QueryTable(doc.TableNameWithPrefix()).Filter("book_id", book.BookId).OrderBy("modify_time").One(doc)
if err == nil {
member2 := NewMember()
member2.Find(doc.ModifyAt)
m.LastModifyText = member2.Account + " 于 " + doc.ModifyTime.Local().Format("2006-01-02 15:04:05")
}
return m, nil
}
func (m *BookResult) FindToPager(pageIndex, pageSize int) (books []*BookResult, totalCount int, err error) {
o := orm.NewOrm()
count, err := o.QueryTable(NewBook().TableNameWithPrefix()).Count()
if err != nil {
return
}
totalCount = int(count)
sql := `SELECT
book.*,rel.relationship_id,rel.role_id,m.account AS create_name,m.real_name
FROM md_books AS book
LEFT JOIN md_relationship AS rel ON rel.book_id = book.book_id AND rel.role_id = 0
LEFT JOIN md_members AS m ON rel.member_id = m.member_id
ORDER BY book.order_index DESC ,book.book_id DESC LIMIT ?,?`
offset := (pageIndex - 1) * pageSize
_, err = o.Raw(sql, offset, pageSize).QueryRows(&books)
return
}
//实体转换
func (m *BookResult) ToBookResult(book Book) *BookResult {
m.BookId = book.BookId
m.BookName = book.BookName
m.Identify = book.Identify
m.OrderIndex = book.OrderIndex
m.Description = strings.Replace(book.Description, "\r\n", "<br/>", -1)
m.PrivatelyOwned = book.PrivatelyOwned
m.PrivateToken = book.PrivateToken
m.DocCount = book.DocCount
m.CommentStatus = book.CommentStatus
m.CommentCount = book.CommentCount
m.CreateTime = book.CreateTime
m.ModifyTime = book.ModifyTime
m.Cover = book.Cover
m.Label = book.Label
m.Status = book.Status
m.Editor = book.Editor
m.Theme = book.Theme
m.AutoRelease = book.AutoRelease == 1
m.IsEnableShare = book.IsEnableShare == 0
m.Publisher = book.Publisher
m.HistoryCount = book.HistoryCount
m.IsDownload = book.IsDownload == 0
m.IsLock = book.IsLock == 1
m.IsUseFirstDocument = book.IsUseFirstDocument == 1
if book.Theme == "" {
m.Theme = "default"
}
if book.Editor == "" {
m.Editor = "markdown"
}
doc := NewDocument()
o := orm.NewOrm()
err := o.QueryTable(doc.TableNameWithPrefix()).Filter("book_id", book.BookId).OrderBy("modify_time").One(doc)
if err == nil {
member2 := NewMember()
member2.Find(doc.ModifyAt)
m.LastModifyText = member2.Account + " 于 " + doc.ModifyTime.Local().Format("2006-01-02 15:04:05")
}
return m
}
//导出PDF、word等格式
func (m *BookResult) Converter(sessionId string) (ConvertBookResult, error) {
convertBookResult := ConvertBookResult{}
outputPath := filepath.Join(conf.WorkingDirectory, "uploads", "books", strconv.Itoa(m.BookId))
viewPath := beego.BConfig.WebConfig.ViewsPath
pdfpath := filepath.Join(outputPath, "book.pdf")
epubpath := filepath.Join(outputPath, "book.epub")
mobipath := filepath.Join(outputPath, "book.mobi")
docxpath := filepath.Join(outputPath, "book.docx")
//先将转换的文件储存到临时目录
tempOutputPath := filepath.Join(os.TempDir(), sessionId, m.Identify,"source") //filepath.Abs(filepath.Join("cache", sessionId))
if err := os.MkdirAll(outputPath, 0766); err != nil {
beego.Error("创建目录失败 => ",outputPath,err)
}
if err := os.MkdirAll(tempOutputPath, 0766);err != nil {
beego.Error("创建目录失败 => ",tempOutputPath,err)
}
defer os.RemoveAll(strings.TrimSuffix(tempOutputPath,"source"))
if filetil.FileExists(pdfpath) && filetil.FileExists(epubpath) && filetil.FileExists(mobipath) && filetil.FileExists(docxpath) {
convertBookResult.EpubPath = epubpath
convertBookResult.MobiPath = mobipath
convertBookResult.PDFPath = pdfpath
convertBookResult.WordPath = docxpath
return convertBookResult, nil
}
docs, err := NewDocument().FindListByBookId(m.BookId)
if err != nil {
return convertBookResult, err
}
tocList := make([]converter.Toc, 0)
for _, item := range docs {
if item.ParentId == 0 {
toc := converter.Toc{
Id: item.DocumentId,
Link: strconv.Itoa(item.DocumentId) + ".html",
Pid: item.ParentId,
Title: item.DocumentName,
}
tocList = append(tocList, toc)
}
}
for _, item := range docs {
if item.ParentId != 0 {
toc := converter.Toc{
Id: item.DocumentId,
Link: strconv.Itoa(item.DocumentId) + ".html",
Pid: item.ParentId,
Title: item.DocumentName,
}
tocList = append(tocList, toc)
}
}
ebookConfig := converter.Config{
Charset: "utf-8",
Cover: m.Cover,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
Description: string(blackfriday.Run([]byte(m.Description))),
Footer: "<p style='color:#8E8E8E;font-size:12px;'>本文档使用 <a href='https://www.iminho.me' style='text-decoration:none;color:#1abc9c;font-weight:bold;'>MinDoc</a> 构建 <span style='float:right'>- _PAGENUM_ -</span></p>",
Header: "<p style='color:#8E8E8E;font-size:12px;'>_SECTION_</p>",
Identifier: "",
Language: "zh-CN",
Creator: m.CreateName,
Publisher: m.Publisher,
Contributor: m.Publisher,
Title: m.BookName,
Format: []string{"epub", "mobi", "pdf", "docx"},
FontSize: "14",
PaperSize: "a4",
MarginLeft: "72",
MarginRight: "72",
MarginTop: "72",
MarginBottom: "72",
Toc: tocList,
More: []string{},
}
if m.Publisher != "" {
ebookConfig.Footer = "<p style='color:#8E8E8E;font-size:12px;'>本文档由 <span style='text-decoration:none;color:#1abc9c;font-weight:bold;'>" + m.Publisher + "</span> 生成<span style='float:right'>- _PAGENUM_ -</span></p>"
}
if m.RealName != "" {
ebookConfig.Creator = m.RealName
}
if tempOutputPath, err = filepath.Abs(tempOutputPath); err != nil {
beego.Error("导出目录配置错误:" + err.Error())
return convertBookResult, err
}
for _, item := range docs {
name := strconv.Itoa(item.DocumentId)
fpath := filepath.Join(tempOutputPath, name+".html")
f, err := os.OpenFile(fpath, os.O_CREATE|os.O_RDWR, 0777)
if err != nil {
return convertBookResult, err
}
var buf bytes.Buffer
if err := beego.ExecuteViewPathTemplate(&buf, "document/export.tpl", viewPath, map[string]interface{}{"Model": m, "Lists": item, "BaseUrl": conf.BaseUrl}); err != nil {
return convertBookResult, err
}
html := buf.String()
if err != nil {
f.Close()
return convertBookResult, err
}
bufio := bytes.NewReader(buf.Bytes())
doc, err := goquery.NewDocumentFromReader(bufio)
doc.Find("img").Each(func(i int, contentSelection *goquery.Selection) {
if src, ok := contentSelection.Attr("src"); ok && strings.HasPrefix(src, "/") {
//contentSelection.SetAttr("src", baseUrl + src)
spath := filepath.Join(conf.WorkingDirectory, src)
if ff, e := ioutil.ReadFile(spath); e == nil {
encodeString := base64.StdEncoding.EncodeToString(ff)
src = "data:image/" + filepath.Ext(src) + ";base64," + encodeString
contentSelection.SetAttr("src", src)
}
}
})
html, err = doc.Html()
if err != nil {
f.Close()
return convertBookResult, err
}
f.WriteString(html)
f.Close()
}
if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "css", "kancloud.css"), filepath.Join(tempOutputPath, "styles", "css", "kancloud.css")); err != nil {
beego.Error("复制CSS样式出错 => static/css/kancloud.css",err)
}
if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "css", "export.css"), filepath.Join(tempOutputPath, "styles", "css", "export.css"));err != nil {
beego.Error("复制CSS样式出错 => static/css/export.css",err)
}
if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "editor.md", "css", "editormd.preview.css"), filepath.Join(tempOutputPath, "styles", "editor.md", "css", "editormd.preview.css"));err != nil {
beego.Error("复制CSS样式出错 => static/editor.md/css/editormd.preview.css",err)
}
if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "prettify", "themes", "prettify.css"), filepath.Join(tempOutputPath, "styles", "prettify", "themes", "prettify.css")); err != nil {
beego.Error("复制CSS样式出错 => static/prettify/themes/prettify.css",err)
}
if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "css", "markdown.preview.css"), filepath.Join(tempOutputPath, "styles", "css", "markdown.preview.css"));err != nil {
beego.Error("复制CSS样式出错 => static/css/markdown.preview.css",err)
}
if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "highlight", "styles", "vs.css"), filepath.Join(tempOutputPath, "styles", "highlight", "styles", "vs.css")); err != nil {
beego.Error("复制CSS样式出错 => static/highlight/styles/vs.css",err)
}
if err := filetil.CopyFile(filepath.Join(conf.WorkingDirectory, "static", "katex", "katex.min.css"), filepath.Join(tempOutputPath, "styles", "katex", "katex.min.css")); err != nil {
beego.Error("复制CSS样式出错 => static/katex/katex.min.css",err)
}
eBookConverter := &converter.Converter{
BasePath: tempOutputPath,
OutputPath: filepath.Join(strings.TrimSuffix(tempOutputPath, "source"),"output"),
Config: ebookConfig,
Debug: true,
}
os.MkdirAll(eBookConverter.OutputPath,0766)
if err := eBookConverter.Convert(); err != nil {
beego.Error("转换文件错误:" + m.BookName + " => " + err.Error())
return convertBookResult, err
}
beego.Info("文档转换完成:" + m.BookName)
if err := filetil.CopyFile(filepath.Join(eBookConverter.OutputPath,"output", "book.mobi"),mobipath,);err != nil {
beego.Error("复制文档失败 => ",filepath.Join(eBookConverter.OutputPath,"output", "book.mobi"),err)
}
if err := filetil.CopyFile(filepath.Join(eBookConverter.OutputPath,"output", "book.pdf"),pdfpath);err != nil {
beego.Error("复制文档失败 => ",filepath.Join(eBookConverter.OutputPath,"output", "book.pdf"),err)
}
if err := filetil.CopyFile(filepath.Join(eBookConverter.OutputPath,"output", "book.epub"),epubpath); err != nil{
beego.Error("复制文档失败 => ",filepath.Join(eBookConverter.OutputPath,"output", "book.epub"),err)
}
if err := filetil.CopyFile(filepath.Join(eBookConverter.OutputPath,"output", "book.docx"),docxpath); err != nil {
beego.Error("复制文档失败 => ",filepath.Join(eBookConverter.OutputPath,"output", "book.docx"),err)
}
convertBookResult.MobiPath = mobipath
convertBookResult.PDFPath = pdfpath
convertBookResult.EpubPath = epubpath
convertBookResult.WordPath = docxpath
return convertBookResult, nil
}
//导出Markdown原始文件
func (m *BookResult) ExportMarkdown(sessionId string) (string, error) {
outputPath := filepath.Join(conf.WorkingDirectory, "uploads", "books", strconv.Itoa(m.BookId), "book.zip")
os.MkdirAll(filepath.Dir(outputPath), 0644)
tempOutputPath := filepath.Join(os.TempDir(), sessionId, "markdown")
defer os.RemoveAll(tempOutputPath)
bookUrl := conf.URLFor("DocumentController.Index",":key" , m.Identify) + "/"
err := exportMarkdown(tempOutputPath, 0, m.BookId,tempOutputPath,bookUrl)
if err != nil {
return "", err
}
if err := ziptil.Compress(outputPath, tempOutputPath); err != nil {
beego.Error("导出Markdown失败=>", err)
return "", err
}
return outputPath, nil
}
//递归导出Markdown文档
func exportMarkdown(p string, parentId int, bookId int,baseDir string,bookUrl string) error {
o := orm.NewOrm()
var docs []*Document
_, err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("book_id", bookId).Filter("parent_id", parentId).All(&docs)
if err != nil {
beego.Error("导出Markdown失败=>", err)
return err
}
for _, doc := range docs {
//获取当前文档的子文档数量如果数量不为0则将当前文档命名为READMD.md并设置成目录。
subDocCount, err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("parent_id", doc.DocumentId).Count()
if err != nil {
beego.Error("导出Markdown失败=>", err)
return err
}
var docPath string
if subDocCount > 0 {
if doc.Identify != "" {
docPath = filepath.Join(p, doc.Identify, "README.md")
} else {
docPath = filepath.Join(p, strconv.Itoa(doc.DocumentId), "README.md")
}
} else {
if doc.Identify != "" {
if strings.HasSuffix(doc.Identify,".md") || strings.HasSuffix(doc.Identify,".markdown") {
docPath = filepath.Join(p, doc.Identify)
}else {
docPath = filepath.Join(p, doc.Identify+".md")
}
} else {
docPath = filepath.Join(p, strings.TrimSpace(doc.DocumentName)+".md")
}
}
dirPath := filepath.Dir(docPath)
os.MkdirAll(dirPath, 0766)
markdown := doc.Markdown
//如果当前文档不为空
if strings.TrimSpace(doc.Markdown) != "" {
re := regexp.MustCompile(`!\[(.*?)\]\((.*?)\)`)
//处理文档中图片
markdown = re.ReplaceAllStringFunc(doc.Markdown, func(image string) string {
images := re.FindAllSubmatch([]byte(image), -1)
if len(images) <= 0 || len(images[0]) < 3 {
return image
}
originalImageUrl := string(images[0][2])
imageUrl := strings.Replace(string(originalImageUrl), "\\", "/", -1)
//如果是本地路径,则需要将图片复制到项目目录
if strings.HasPrefix(imageUrl, "http://") || strings.HasPrefix(imageUrl, "https://") {
imageExt := cryptil.Md5Crypt(imageUrl) + filepath.Ext(imageUrl)
dstFile := filepath.Join(baseDir, "uploads", time.Now().Format("200601"), imageExt)
if err := requests.DownloadAndSaveFile(imageUrl, dstFile); err == nil {
imageUrl = strings.TrimPrefix(strings.Replace(dstFile, "\\", "/", -1), strings.Replace(baseDir, "\\", "/", -1))
if !strings.HasPrefix(imageUrl, "/") && !strings.HasPrefix(imageUrl, "\\") {
imageUrl = "/" + imageUrl
}
}
} else if strings.HasPrefix(imageUrl, "/") {
filetil.CopyFile(filepath.Join(conf.WorkingDirectory, imageUrl), filepath.Join(baseDir, imageUrl))
}
imageUrl = strings.Replace(strings.TrimSuffix(image, originalImageUrl+")")+imageUrl+")", "\\", "/", -1)
return imageUrl
})
linkRe := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`)
markdown = linkRe.ReplaceAllStringFunc(markdown, func(link string) string {
links := linkRe.FindAllStringSubmatch(link, -1)
if len(links) > 0 && len(links[0]) >= 3 {
originalLink := links[0][2]
//如果当前链接位于当前项目内
if strings.HasPrefix(originalLink,bookUrl) {
docIdentify := strings.TrimSpace(strings.TrimPrefix(originalLink, bookUrl))
tempDoc := NewDocument()
if id,err := strconv.Atoi(docIdentify);err == nil && id > 0 {
err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("document_id",id).One(tempDoc,"identify","parent_id","document_id")
if err != nil {
beego.Error(err)
return link
}
}else{
err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("identify",docIdentify).One(tempDoc,"identify","parent_id","document_id")
if err != nil {
beego.Error(err)
return link
}
}
tempLink := recursiveJoinDocumentIdentify(tempDoc.ParentId,"") + strings.TrimPrefix(originalLink, bookUrl)
if !strings.HasSuffix(tempLink,".md") && !strings.HasSuffix(doc.Identify,".markdown") {
tempLink = tempLink + ".md"
}
relative := strings.TrimPrefix(strings.Replace(p,"\\","/",-1),strings.Replace(baseDir,"\\","/",-1))
repeat := 0
if relative != "" {
relative = strings.TrimSuffix(strings.TrimPrefix(relative,"/"),"/")
repeat = strings.Count(relative,"/") + 1
}
beego.Info(repeat,"|",relative,"|",p,"|",baseDir)
tempLink = strings.Repeat("../",repeat) + tempLink
link = strings.TrimSuffix(link, originalLink+")") + tempLink + ")"
}
}
return link
})
}else{
markdown = "# " + doc.DocumentName + "\n"
}
if err := ioutil.WriteFile(docPath, []byte(markdown), 0644); err != nil {
beego.Error("导出Markdown失败=>", err)
return err
}
if subDocCount > 0 {
if err = exportMarkdown(dirPath, doc.DocumentId, bookId,baseDir,bookUrl); err != nil {
return err
}
}
}
return nil
}
func recursiveJoinDocumentIdentify(parentDocId int,identify string) string {
o := orm.NewOrm()
doc := NewDocument()
err := o.QueryTable(NewDocument().TableNameWithPrefix()).Filter("document_id",parentDocId).One(doc,"identify","parent_id","document_id")
if err != nil {
beego.Error(err)
return identify
}
if doc.Identify == "" {
identify = strconv.Itoa(doc.DocumentId) + "/" + identify
}else{
identify = doc.Identify + "/" + identify
}
if doc.ParentId > 0 {
identify = recursiveJoinDocumentIdentify(doc.ParentId,identify)
}
return identify
}
//查询项目的第一篇文档
func (m *BookResult) FindFirstDocumentByBookId(bookId int) (*Document, error) {
o := orm.NewOrm()
doc := NewDocument()
err := o.QueryTable(doc.TableNameWithPrefix()).Filter("book_id", bookId).Filter("parent_id", 0).OrderBy("order_sort").One(doc)
return doc, err
}