mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-12-16 22:50:13 +01:00
286 lines
7.7 KiB
Go
286 lines
7.7 KiB
Go
// 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
|
||
}
|