♻️ Supports converting network assets in the database to local https://github.com/siyuan-note/siyuan/issues/12096
This commit is contained in:
parent
2a7702a78b
commit
df8d70995c
2 changed files with 78 additions and 231 deletions
|
@ -35,7 +35,7 @@ func netAssets2LocalAssets(c *gin.Context) {
|
|||
}
|
||||
|
||||
id := arg["id"].(string)
|
||||
err := model.NetAssets2LocalAssets(id)
|
||||
err := model.NetAssets2LocalAssets(id, false, "")
|
||||
if nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
|
@ -58,7 +58,7 @@ func netImg2LocalAssets(c *gin.Context) {
|
|||
if urlArg := arg["url"]; nil != urlArg {
|
||||
url = urlArg.(string)
|
||||
}
|
||||
err := model.NetImg2LocalAssets(id, url)
|
||||
err := model.NetAssets2LocalAssets(id, true, url)
|
||||
if nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
|
|
|
@ -75,182 +75,7 @@ func DocImageAssets(rootID string) (ret []string, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func NetImg2LocalAssets(rootID, originalURL string) (err error) {
|
||||
tree, err := LoadTreeByBlockID(rootID)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
var files int
|
||||
msgId := gulu.Rand.String(7)
|
||||
|
||||
docDirLocalPath := filepath.Join(util.DataDir, tree.Box, path.Dir(tree.Path))
|
||||
assetsDirPath := getAssetsDir(filepath.Join(util.DataDir, tree.Box), docDirLocalPath)
|
||||
if !gulu.File.IsExist(assetsDirPath) {
|
||||
if err = os.MkdirAll(assetsDirPath, 0755); nil != err {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
browserClient := req.C().
|
||||
SetUserAgent(util.UserAgent).
|
||||
SetTimeout(30 * time.Second).
|
||||
EnableInsecureSkipVerify(). // HTTPS certificate is no longer verified when `Convert network images to local images` https://github.com/siyuan-note/siyuan/issues/9080
|
||||
SetProxy(httpclient.ProxyFromEnvironment)
|
||||
|
||||
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
||||
if !entering {
|
||||
return ast.WalkContinue
|
||||
}
|
||||
if ast.NodeImage == n.Type {
|
||||
linkDest := n.ChildByType(ast.NodeLinkDest)
|
||||
linkText := n.ChildByType(ast.NodeLinkText)
|
||||
if nil == linkText {
|
||||
linkText = &ast.Node{Type: ast.NodeLinkText, Tokens: []byte("image")}
|
||||
if openBracket := n.ChildByType(ast.NodeOpenBracket); nil != openBracket {
|
||||
openBracket.InsertAfter(linkText)
|
||||
}
|
||||
}
|
||||
|
||||
dest := linkDest.Tokens
|
||||
if util.IsAssetLinkDest(dest) {
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(bytes.ToLower(dest), []byte("file://")) {
|
||||
// `网络图片转换为本地图片` 支持处理 `file://` 本地路径图片 https://github.com/siyuan-note/siyuan/issues/6546
|
||||
|
||||
u := string(dest)[7:]
|
||||
unescaped, _ := url.PathUnescape(u)
|
||||
if unescaped != u {
|
||||
// `Convert network images/assets to local` supports URL-encoded local file names https://github.com/siyuan-note/siyuan/issues/9929
|
||||
u = unescaped
|
||||
}
|
||||
if strings.Contains(u, ":") {
|
||||
u = strings.TrimPrefix(u, "/")
|
||||
}
|
||||
|
||||
if !gulu.File.IsExist(u) || gulu.File.IsDir(u) {
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
|
||||
name := filepath.Base(u)
|
||||
name = util.FilterUploadFileName(name)
|
||||
name = util.TruncateLenFileName(name)
|
||||
if 1 > len(bytes.TrimSpace(linkText.Tokens)) {
|
||||
linkText.Tokens = []byte(name)
|
||||
}
|
||||
name = "net-img-" + name
|
||||
name = util.AssetName(name)
|
||||
writePath := filepath.Join(assetsDirPath, name)
|
||||
if err = filelock.Copy(u, writePath); nil != err {
|
||||
logging.LogErrorf("copy [%s] to [%s] failed: %s", u, writePath, err)
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
|
||||
linkDest.Tokens = []byte("assets/" + name)
|
||||
files++
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(bytes.ToLower(dest), []byte("https://")) || bytes.HasPrefix(bytes.ToLower(dest), []byte("http://")) || bytes.HasPrefix(dest, []byte("//")) {
|
||||
if bytes.HasPrefix(dest, []byte("//")) {
|
||||
// `Convert network images to local` supports `//` https://github.com/siyuan-note/siyuan/issues/10598
|
||||
dest = append([]byte("https:"), dest...)
|
||||
}
|
||||
|
||||
u := string(dest)
|
||||
if strings.Contains(u, "qpic.cn") {
|
||||
// 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/5052
|
||||
if strings.Contains(u, "http://") {
|
||||
u = strings.Replace(u, "http://", "https://", 1)
|
||||
}
|
||||
|
||||
// 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/6431
|
||||
// 下面这部分需要注释掉,否则会导致响应 400
|
||||
//if strings.HasSuffix(u, "/0") {
|
||||
// u = strings.Replace(u, "/0", "/640", 1)
|
||||
//} else if strings.Contains(u, "/0?") {
|
||||
// u = strings.Replace(u, "/0?", "/640?", 1)
|
||||
//}
|
||||
}
|
||||
util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(119), u), 15000)
|
||||
request := browserClient.R()
|
||||
request.SetRetryCount(1).SetRetryFixedInterval(3 * time.Second)
|
||||
if "" != originalURL {
|
||||
request.SetHeader("Referer", originalURL) // 改进浏览器剪藏扩展转换本地图片成功率 https://github.com/siyuan-note/siyuan/issues/7464
|
||||
}
|
||||
resp, reqErr := request.Get(u)
|
||||
if nil != reqErr {
|
||||
logging.LogErrorf("download net img [%s] failed: %s", u, reqErr)
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
logging.LogErrorf("download net img [%s] failed: %d", u, resp.StatusCode)
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
data, repErr := resp.ToBytes()
|
||||
if nil != repErr {
|
||||
logging.LogErrorf("download net img [%s] failed: %s", u, repErr)
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
var name string
|
||||
if strings.Contains(u, "?") {
|
||||
name = u[:strings.Index(u, "?")]
|
||||
name = path.Base(name)
|
||||
} else {
|
||||
name = path.Base(u)
|
||||
}
|
||||
if strings.Contains(name, "#") {
|
||||
name = name[:strings.Index(name, "#")]
|
||||
}
|
||||
name, _ = url.PathUnescape(name)
|
||||
ext := path.Ext(name)
|
||||
if "" == ext {
|
||||
if mtype := mimetype.Detect(data); nil != mtype {
|
||||
ext = mtype.Extension()
|
||||
}
|
||||
}
|
||||
if "" == ext {
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
exts, _ := mime.ExtensionsByType(contentType)
|
||||
if 0 < len(exts) {
|
||||
ext = exts[0]
|
||||
}
|
||||
}
|
||||
name = strings.TrimSuffix(name, ext)
|
||||
name = util.FilterUploadFileName(name)
|
||||
name = util.TruncateLenFileName(name)
|
||||
if 1 > len(bytes.TrimSpace(linkText.Tokens)) {
|
||||
linkText.Tokens = []byte(name)
|
||||
}
|
||||
name = "net-img-" + name + "-" + ast.NewNodeID() + ext
|
||||
writePath := filepath.Join(assetsDirPath, name)
|
||||
if err = filelock.WriteFile(writePath, data); nil != err {
|
||||
logging.LogErrorf("write downloaded net img [%s] to local assets [%s] failed: %s", u, writePath, err)
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
|
||||
linkDest.Tokens = []byte("assets/" + name)
|
||||
files++
|
||||
}
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
return ast.WalkContinue
|
||||
})
|
||||
if 0 < files {
|
||||
util.PushUpdateMsg(msgId, Conf.Language(113), 7000)
|
||||
if err = writeTreeUpsertQueue(tree); nil != err {
|
||||
return
|
||||
}
|
||||
util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(120), files), 5000)
|
||||
} else {
|
||||
util.PushUpdateMsg(msgId, Conf.Language(121), 3000)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NetAssets2LocalAssets(rootID string) (err error) {
|
||||
func NetAssets2LocalAssets(rootID string, onlyImg bool, originalURL string) (err error) {
|
||||
tree, err := LoadTreeByBlockID(rootID)
|
||||
if nil != err {
|
||||
return
|
||||
|
@ -273,32 +98,15 @@ func NetAssets2LocalAssets(rootID string) (err error) {
|
|||
EnableInsecureSkipVerify().
|
||||
SetProxy(httpclient.ProxyFromEnvironment)
|
||||
|
||||
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
||||
if !entering || (ast.NodeLinkDest != n.Type && !n.IsTextMarkType("a") && ast.NodeAudio != n.Type && ast.NodeVideo != n.Type) {
|
||||
return ast.WalkContinue
|
||||
destNodes := getRemoteAssetsLinkDestsInTree(tree, onlyImg)
|
||||
for _, destNode := range destNodes {
|
||||
dest := getRemoteAssetsLinkDest(destNode, onlyImg)
|
||||
if "" == dest {
|
||||
continue
|
||||
}
|
||||
|
||||
var dest []byte
|
||||
if ast.NodeLinkDest == n.Type {
|
||||
dest = n.Tokens
|
||||
} else if n.IsTextMarkType("a") {
|
||||
dest = []byte(n.TextMarkAHref)
|
||||
} else if ast.NodeAudio == n.Type || ast.NodeVideo == n.Type {
|
||||
if srcIndex := bytes.Index(n.Tokens, []byte("src=\"")); 0 < srcIndex {
|
||||
src := n.Tokens[srcIndex+len("src=\""):]
|
||||
if srcIndex = bytes.Index(src, []byte("\"")); 0 < srcIndex {
|
||||
src = src[:bytes.Index(src, []byte("\""))]
|
||||
dest = bytes.TrimSpace(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if util.IsAssetLinkDest(dest) {
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(bytes.ToLower(dest), []byte("file://")) { // 处理本地文件链接
|
||||
u := string(dest)[7:]
|
||||
if strings.HasPrefix(strings.ToLower(dest), "file://") { // 处理本地文件链接
|
||||
u := dest[7:]
|
||||
unescaped, _ := url.PathUnescape(u)
|
||||
if unescaped != u {
|
||||
// `Convert network images/assets to local` supports URL-encoded local file names https://github.com/siyuan-note/siyuan/issues/9929
|
||||
|
@ -309,7 +117,7 @@ func NetAssets2LocalAssets(rootID string) (err error) {
|
|||
}
|
||||
|
||||
if !gulu.File.IsExist(u) || gulu.File.IsDir(u) {
|
||||
return ast.WalkContinue
|
||||
continue
|
||||
}
|
||||
|
||||
name := filepath.Base(u)
|
||||
|
@ -320,27 +128,21 @@ func NetAssets2LocalAssets(rootID string) (err error) {
|
|||
writePath := filepath.Join(assetsDirPath, name)
|
||||
if err = filelock.Copy(u, writePath); nil != err {
|
||||
logging.LogErrorf("copy [%s] to [%s] failed: %s", u, writePath, err)
|
||||
return ast.WalkContinue
|
||||
continue
|
||||
}
|
||||
|
||||
if ast.NodeLinkDest == n.Type {
|
||||
n.Tokens = []byte("assets/" + name)
|
||||
} else if n.IsTextMarkType("a") {
|
||||
n.TextMarkAHref = "assets/" + name
|
||||
} else if ast.NodeAudio == n.Type || ast.NodeVideo == n.Type {
|
||||
n.Tokens = bytes.ReplaceAll(n.Tokens, dest, []byte("assets/"+name))
|
||||
}
|
||||
setAssetsLinkDest(destNode, dest, "assets/"+name)
|
||||
files++
|
||||
return ast.WalkContinue
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(bytes.ToLower(dest), []byte("https://")) || bytes.HasPrefix(bytes.ToLower(dest), []byte("http://")) || bytes.HasPrefix(dest, []byte("//")) {
|
||||
if bytes.HasPrefix(dest, []byte("//")) {
|
||||
if strings.HasPrefix(strings.ToLower(dest), "https://") || strings.HasPrefix(strings.ToLower(dest), "http://") || strings.HasPrefix(dest, "//") {
|
||||
if strings.HasPrefix(dest, "//") {
|
||||
// `Convert network images to local` supports `//` https://github.com/siyuan-note/siyuan/issues/10598
|
||||
dest = append([]byte("https:"), dest...)
|
||||
dest = "https:" + dest
|
||||
}
|
||||
|
||||
u := string(dest)
|
||||
u := dest
|
||||
if strings.Contains(u, "qpic.cn") {
|
||||
// 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/5052
|
||||
if strings.Contains(u, "http://") {
|
||||
|
@ -358,30 +160,33 @@ func NetAssets2LocalAssets(rootID string) (err error) {
|
|||
util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(119), u), 15000)
|
||||
request := browserClient.R()
|
||||
request.SetRetryCount(1).SetRetryFixedInterval(3 * time.Second)
|
||||
if "" != originalURL {
|
||||
request.SetHeader("Referer", originalURL) // 改进浏览器剪藏扩展转换本地图片成功率 https://github.com/siyuan-note/siyuan/issues/7464
|
||||
}
|
||||
resp, reqErr := request.Get(u)
|
||||
if strings.Contains(strings.ToLower(resp.GetContentType()), "text/html") {
|
||||
// 忽略超链接网页 `Convert network assets to local` no longer process webpage https://github.com/siyuan-note/siyuan/issues/9965
|
||||
return ast.WalkContinue
|
||||
continue
|
||||
}
|
||||
|
||||
if nil != reqErr {
|
||||
logging.LogErrorf("download network asset [%s] failed: %s", u, reqErr)
|
||||
return ast.WalkContinue
|
||||
continue
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
logging.LogErrorf("download network asset [%s] failed: %d", u, resp.StatusCode)
|
||||
return ast.WalkContinue
|
||||
continue
|
||||
}
|
||||
|
||||
if 1024*1024*96 < resp.ContentLength {
|
||||
logging.LogWarnf("network asset [%s]' size [%s] is large then [96 MB], ignore it", u, humanize.IBytes(uint64(resp.ContentLength)))
|
||||
return ast.WalkContinue
|
||||
continue
|
||||
}
|
||||
|
||||
data, repErr := resp.ToBytes()
|
||||
if nil != repErr {
|
||||
logging.LogErrorf("download network asset [%s] failed: %s", u, repErr)
|
||||
return ast.WalkContinue
|
||||
continue
|
||||
}
|
||||
var name string
|
||||
if strings.Contains(u, "?") {
|
||||
|
@ -414,20 +219,14 @@ func NetAssets2LocalAssets(rootID string) (err error) {
|
|||
writePath := filepath.Join(assetsDirPath, name)
|
||||
if err = filelock.WriteFile(writePath, data); nil != err {
|
||||
logging.LogErrorf("write downloaded network asset [%s] to local asset [%s] failed: %s", u, writePath, err)
|
||||
return ast.WalkContinue
|
||||
continue
|
||||
}
|
||||
|
||||
if ast.NodeLinkDest == n.Type {
|
||||
n.Tokens = []byte("assets/" + name)
|
||||
} else if n.IsTextMarkType("a") {
|
||||
n.TextMarkAHref = "assets/" + name
|
||||
} else if ast.NodeAudio == n.Type || ast.NodeVideo == n.Type {
|
||||
n.Tokens = bytes.ReplaceAll(n.Tokens, dest, []byte("assets/"+name))
|
||||
}
|
||||
setAssetsLinkDest(destNode, dest, "assets/"+name)
|
||||
files++
|
||||
continue
|
||||
}
|
||||
return ast.WalkContinue
|
||||
})
|
||||
}
|
||||
|
||||
if 0 < files {
|
||||
util.PushUpdateMsg(msgId, Conf.Language(113), 7000)
|
||||
|
@ -1242,6 +1041,54 @@ func assetsLinkDestsInNode(node *ast.Node) (ret []string) {
|
|||
return
|
||||
}
|
||||
|
||||
func setAssetsLinkDest(node *ast.Node, oldDest, dest string) {
|
||||
if ast.NodeLinkDest == node.Type {
|
||||
node.Tokens = bytes.ReplaceAll(node.Tokens, []byte(oldDest), []byte(dest))
|
||||
} else if node.IsTextMarkType("a") {
|
||||
node.TextMarkAHref = strings.ReplaceAll(node.TextMarkAHref, oldDest, dest)
|
||||
} else if ast.NodeAudio == node.Type || ast.NodeVideo == node.Type {
|
||||
node.Tokens = bytes.ReplaceAll(node.Tokens, []byte(oldDest), []byte(dest))
|
||||
}
|
||||
}
|
||||
|
||||
func getRemoteAssetsLinkDest(node *ast.Node, onlyImg bool) (ret string) {
|
||||
if onlyImg {
|
||||
if ast.NodeLinkDest == node.Type && node.ParentIs(ast.NodeImage) {
|
||||
ret = string(node.Tokens)
|
||||
}
|
||||
} else {
|
||||
if ast.NodeLinkDest == node.Type {
|
||||
ret = string(node.Tokens)
|
||||
} else if node.IsTextMarkType("a") {
|
||||
ret = node.TextMarkAHref
|
||||
} else if ast.NodeAudio == node.Type || ast.NodeVideo == node.Type {
|
||||
ret = treenode.GetNodeSrcTokens(node)
|
||||
}
|
||||
}
|
||||
|
||||
if util.IsAssetLinkDest([]byte(ret)) {
|
||||
ret = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getRemoteAssetsLinkDestsInTree(tree *parse.Tree, onlyImg bool) (nodes []*ast.Node) {
|
||||
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
|
||||
if !entering {
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
dest := getRemoteAssetsLinkDest(n, onlyImg)
|
||||
if "" == dest {
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
nodes = append(nodes, n)
|
||||
return ast.WalkContinue
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// allAssetAbsPaths 返回 asset 相对路径(assets/xxx)到绝对路径(F:\SiYuan\data\assets\xxx)的映射。
|
||||
func allAssetAbsPaths() (assetsAbsPathMap map[string]string, err error) {
|
||||
notebooks, err := ListNotebooks()
|
||||
|
|
Loading…
Add table
Reference in a new issue