siyuan/kernel/util/pandoc.go

286 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 util
import (
"bytes"
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/88250/gulu"
"github.com/siyuan-note/eventbus"
"github.com/siyuan-note/logging"
)
var ErrPandocNotFound = errors.New("not found executable pandoc")
func ConvertPandoc(dir string, args ...string) (path string, err error) {
if "" == PandocBinPath || ContainerStd != Container {
err = ErrPandocNotFound
return
}
pandoc := exec.Command(PandocBinPath, args...)
gulu.CmdAttr(pandoc)
path = filepath.Join("temp", "convert", "pandoc", dir)
absPath := filepath.Join(WorkspaceDir, path)
if err = os.MkdirAll(absPath, 0755); err != nil {
logging.LogErrorf("mkdir [%s] failed: [%s]", absPath, err)
return
}
pandoc.Dir = absPath
output, err := pandoc.CombinedOutput()
if err != nil {
err = errors.Join(err, errors.New(string(output)))
logging.LogErrorf("pandoc convert output failed: %s", err)
return
}
path = "/" + filepath.ToSlash(path)
return
}
func Pandoc(from, to, o, content string) (err error) {
if "" == from || "" == to || "md" == to {
if err = gulu.File.WriteFileSafer(o, []byte(content), 0644); err != nil {
logging.LogErrorf("write export markdown file [%s] failed: %s", o, err)
}
return
}
if "" == PandocBinPath || ContainerStd != Container {
err = ErrPandocNotFound
return
}
dir := filepath.Join(WorkspaceDir, "temp", "convert", "pandoc", gulu.Rand.String(7))
if err = os.MkdirAll(dir, 0755); err != nil {
logging.LogErrorf("mkdir [%s] failed: [%s]", dir, err)
return
}
tmpPath := filepath.Join(dir, gulu.Rand.String(7))
if err = os.WriteFile(tmpPath, []byte(content), 0644); err != nil {
logging.LogErrorf("write file failed: [%s]", err)
return
}
args := []string{
tmpPath,
"--from", from,
"--to", to,
"--resource-path", filepath.Dir(o),
"-s",
"-o", o,
}
pandoc := exec.Command(PandocBinPath, args...)
gulu.CmdAttr(pandoc)
output, err := pandoc.CombinedOutput()
if err != nil {
logging.LogErrorf("pandoc convert output [%s], error [%s]", string(output), err)
return
}
return
}
var (
PandocBinPath string // Pandoc 可执行文件路径
)
func InitPandoc() {
if ContainerStd != Container {
return
}
pandocDir := filepath.Join(TempDir, "pandoc")
if confPath := filepath.Join(ConfDir, "conf.json"); gulu.File.IsExist(confPath) {
// Workspace built-in Pandoc is no longer initialized after customizing Pandoc path https://github.com/siyuan-note/siyuan/issues/8377
if data, err := os.ReadFile(confPath); err == nil {
conf := map[string]interface{}{}
if err = gulu.JSON.UnmarshalJSON(data, &conf); err == nil && nil != conf["export"] {
export := conf["export"].(map[string]interface{})
if customPandocBinPath := export["pandocBin"].(string); !strings.HasPrefix(customPandocBinPath, pandocDir) {
if pandocVer := getPandocVer(customPandocBinPath); "" != pandocVer {
PandocBinPath = customPandocBinPath
logging.LogInfof("custom pandoc [ver=%s, bin=%s]", pandocVer, PandocBinPath)
return
}
}
}
}
}
defer eventbus.Publish(EvtConfPandocInitialized)
if gulu.OS.IsWindows() {
if "amd64" == runtime.GOARCH {
PandocBinPath = filepath.Join(pandocDir, "bin", "pandoc.exe")
}
} else if gulu.OS.IsDarwin() {
PandocBinPath = filepath.Join(pandocDir, "bin", "pandoc")
} else if gulu.OS.IsLinux() {
if "amd64" == runtime.GOARCH {
PandocBinPath = filepath.Join(pandocDir, "bin", "pandoc")
}
}
pandocVer := getPandocVer(PandocBinPath)
if "" != pandocVer {
logging.LogInfof("built-in pandoc [ver=%s, bin=%s]", pandocVer, PandocBinPath)
return
}
pandocZip := filepath.Join(WorkingDir, "pandoc.zip")
if "dev" == Mode || !gulu.File.IsExist(pandocZip) {
if gulu.OS.IsWindows() {
if "amd64" == runtime.GOARCH {
pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-windows-amd64.zip")
}
} else if gulu.OS.IsDarwin() {
if "amd64" == runtime.GOARCH {
pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-darwin-amd64.zip")
} else if "arm64" == runtime.GOARCH {
pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-darwin-arm64.zip")
}
} else if gulu.OS.IsLinux() {
if "amd64" == runtime.GOARCH {
pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-linux-amd64.zip")
} else if "arm64" == runtime.GOARCH {
pandocZip = filepath.Join(WorkingDir, "pandoc/pandoc-linux-arm64.zip")
}
}
}
if !gulu.File.IsExist(pandocZip) {
PandocBinPath = ""
logging.LogErrorf("pandoc zip [%s] not found", pandocZip)
return
}
if err := gulu.Zip.Unzip(pandocZip, pandocDir); err != nil {
logging.LogErrorf("unzip pandoc failed: %s", err)
return
}
if gulu.OS.IsDarwin() || gulu.OS.IsLinux() {
exec.Command("chmod", "+x", PandocBinPath).CombinedOutput()
}
pandocVer = getPandocVer(PandocBinPath)
logging.LogInfof("initialized built-in pandoc [ver=%s, bin=%s]", pandocVer, PandocBinPath)
}
func getPandocVer(binPath string) (ret string) {
if "" == binPath {
return
}
cmd := exec.Command(binPath, "--version")
gulu.CmdAttr(cmd)
data, err := cmd.CombinedOutput()
if err == nil && strings.HasPrefix(string(data), "pandoc") {
parts := bytes.Split(data, []byte("\n"))
if 0 < len(parts) {
ret = strings.TrimPrefix(string(parts[0]), "pandoc")
ret = strings.ReplaceAll(ret, ".exe", "")
ret = strings.TrimSpace(ret)
}
return
}
return
}
func IsValidPandocBin(binPath string) bool {
if "" == binPath {
return false
}
// 解析符号链接
if real, err := filepath.EvalSymlinks(binPath); err == nil {
binPath = real
}
// 文件信息检查
fi, err := os.Stat(binPath)
if err != nil || fi.IsDir() || !fi.Mode().IsRegular() {
return false
}
// 在 Unix 上要求拥有可执行权限
if !gulu.OS.IsWindows() {
if fi.Mode().Perm()&0111 == 0 {
return false
}
}
// 读取文件头判断是否为二进制并排除脚本(#!
f, err := os.Open(binPath)
if err != nil {
return false
}
defer f.Close()
header := make([]byte, 16)
n, _ := f.Read(header)
header = header[:n]
// 拒绝以 shebang 开头的脚本
if bytes.HasPrefix(header, []byte("#!")) {
return false
}
isBin := false
// 常见二进制魔数ELF, PE("MZ"), Mach-O/FAT
if len(header) >= 4 {
switch {
case bytes.Equal(header[:4], []byte{0x7f, 'E', 'L', 'F'}):
isBin = true // ELF
case bytes.Equal(header[:4], []byte{0xfe, 0xed, 0xfa, 0xce}):
isBin = true // Mach-O
case bytes.Equal(header[:4], []byte{0xce, 0xfa, 0xed, 0xfe}):
isBin = true // Mach-O (swapped)
case bytes.Equal(header[:4], []byte{0xca, 0xfe, 0xba, 0xbe}):
isBin = true // FAT
}
}
// PE only needs first 2 bytes "MZ"
if !isBin && len(header) >= 2 && bytes.Equal(header[:2], []byte{'M', 'Z'}) {
isBin = true
}
// Windows 上允许 .exe 文件(作为补充判断)
if !isBin && gulu.OS.IsWindows() {
ext := strings.ToLower(filepath.Ext(binPath))
if ext == ".exe" {
isBin = true
}
}
if !isBin {
return false
}
cmd := exec.Command(binPath, "--version")
gulu.CmdAttr(cmd)
data, err := cmd.CombinedOutput()
if err == nil && strings.HasPrefix(string(data), "pandoc") {
return true
}
return false
}