Browse Source

:art: Automatically download network assets when the cloud inbox is moved to docs https://github.com/siyuan-note/siyuan/issues/9775

Daniel 1 year ago
parent
commit
a8625ca27d

+ 2 - 2
app/appearance/langs/en_US.json

@@ -1235,9 +1235,9 @@
     "116": "Processing, please wait...",
     "117": "[%s] is not a valid Pandoc executable",
     "118": "The current settings do not allow the creation of sub-documents under a document 7 levels deep",
-    "119": "Downloading web image [%s]",
+    "119": "Downloading network file [%s]",
     "120": "Download complete, [%d] files total",
-    "121": "There is no network image in this document",
+    "121": "There is no network file in this document",
     "122": "This function needs to be configured on the SiYuan desktop. If you have already configured it, please refresh it in the top account settings",
     "123": "The synchronization function can only be activated after adding/selecting the cloud synchronization directory",
     "124": "Please enable cloud sync in [Settings - Enable Cloud Sync]",

+ 2 - 2
app/appearance/langs/es_ES.json

@@ -1235,9 +1235,9 @@
     "116": "Procesando, por favor espere...",
     "117": "[%s] no es un ejecutable válido de Pandoc",
     "118": "La configuración actual no permite la creación de subdocumentos bajo un documento de 7 niveles de profundidad",
-    "119": "Descargando imagen web [%s]",
+    "119": "Descargando archivo de red [%s]",
     "120": "Descarga completa, [%d] archivos en total",
-    "121": "No hay ninguna imagen de red en este documento",
+    "121": "No hay ningún archivo de red en este documento",
     "122": "Esta funci\u00f3n debe configurarse en el escritorio de SiYuan. Si ya la configur\u00f3, actualice en la configuraci\u00f3n de la cuenta superior",
     "123": "La función de sincronización solo puede activarse después de añadir/seleccionar el directorio de sincronización en la nube",
     "124": "Por favor, active la sincronización en la nube en [Configuración - Activar sincronización en la nube]",

+ 2 - 2
app/appearance/langs/fr_FR.json

@@ -1235,9 +1235,9 @@
     "116": "Traitement en cours, veuillez patienter...",
     "117": "[%s] n'est pas un exécutable Pandoc valide",
     "118": "Les paramètres actuels ne permettent pas la création de sous-documents sous un document de 7 niveaux de profondeur",
-    "119": "Téléchargement de l'image Web [%s]",
+    "119": "Téléchargement du fichier réseau [%s]",
     "120": "Téléchargement terminé, [%d] fichiers au total",
-    "121": "Il n'y a pas d'image réseau dans ce document",
+    "121": "Il n'y a aucun fichier réseau dans ce document",
     "122": "Cette fonction doit être configurée sur le bureau SiYuan. Si vous l'avez déjà configurée, veuillez l'actualiser dans les paramètres supérieurs du compte",
     "123": "Ajouter/sélectionner un répertoire de synchronisation Cloud avant d'activer la synchronisation",
     "124": "Veuillez activer la synchronisation cloud dans [Paramètres - Activer la synchronisation cloud]",

+ 2 - 2
app/appearance/langs/zh_CHT.json

@@ -1235,10 +1235,10 @@
     "116": "正在處理中,請稍等...",
     "117": "[%s] 不是有效的 Pandoc 可執行文件",
     "118": "當前設置不允許在 7 層深度的文檔下建立子文檔",
-    "119": "正在下載網絡圖片 [%s]",
+    "119": "正在下載網路檔案 [%s]",
     "120": "下載完畢,一共 [%d] 個文件",
     "122": "該功能需在思源桌面端進行配置。如果你已經配置,請在頂部賬號設置中進行重新整理",
-    "121": "該文檔中不存在網絡圖片",
+    "121": "該文件中不存在網路檔案",
     "123": "新增/選擇雲端同步目錄後才能啟用同步功能",
     "124": "請在 [設置 - 啟用雲端同步] 中開啟雲端同步",
     "125": "自動同步失敗次數過多,請嘗試手動觸發同步,如果還有問題請<a href=\"https://ld246.com/article/1649901726096\" target=\"_blank\">反饋</a>",

+ 2 - 2
app/appearance/langs/zh_CN.json

@@ -1235,9 +1235,9 @@
     "116": "正在处理中,请稍等...",
     "117": "[%s] 不是有效的 Pandoc 可执行文件",
     "118": "当前设置不允许在 7 层深度的文档下创建子文档",
-    "119": "正在下载网络图片 [%s]",
+    "119": "正在下载网络文件 [%s]",
     "120": "下载完毕,一共 [%d] 个文件",
-    "121": "该文档中不存在网络图片",
+    "121": "该文档中不存在网络文件",
     "122": "该功能需在思源桌面端进行配置。如果你已经配置,请在顶部账号设置中进行刷新",
     "123": "添加/选择云端同步目录后才能启用同步功能",
     "124": "请在 [设置 - 启用云端同步] 中开启云端同步",

+ 19 - 0
kernel/api/format.go

@@ -25,6 +25,25 @@ import (
 	"github.com/siyuan-note/siyuan/kernel/util"
 )
 
+func netAssets2LocalAssets(c *gin.Context) {
+	ret := gulu.Ret.NewResult()
+	defer c.JSON(http.StatusOK, ret)
+
+	arg, ok := util.JsonArg(c, ret)
+	if !ok {
+		return
+	}
+
+	id := arg["id"].(string)
+	err := model.NetAssets2LocalAssets(id)
+	if nil != err {
+		ret.Code = -1
+		ret.Msg = err.Error()
+		ret.Data = map[string]interface{}{"closeTimeout": 5000}
+		return
+	}
+}
+
 func netImg2LocalAssets(c *gin.Context) {
 	ret := gulu.Ret.NewResult()
 	defer c.JSON(http.StatusOK, ret)

+ 1 - 0
kernel/api/router.go

@@ -116,6 +116,7 @@ func ServeAPI(ginServer *gin.Engine) {
 
 	ginServer.Handle("POST", "/api/format/autoSpace", model.CheckAuth, model.CheckReadonly, autoSpace)
 	ginServer.Handle("POST", "/api/format/netImg2LocalAssets", model.CheckAuth, model.CheckReadonly, netImg2LocalAssets)
+	ginServer.Handle("POST", "/api/format/netAssets2LocalAssets", model.CheckAuth, model.CheckReadonly, netAssets2LocalAssets)
 
 	ginServer.Handle("POST", "/api/history/getNotebookHistory", model.CheckAuth, getNotebookHistory)
 	ginServer.Handle("POST", "/api/history/rollbackNotebookHistory", model.CheckAuth, model.CheckReadonly, rollbackNotebookHistory)

+ 158 - 0
kernel/model/assets.go

@@ -220,6 +220,164 @@ func NetImg2LocalAssets(rootID, originalURL string) (err error) {
 	return
 }
 
+func NetAssets2LocalAssets(rootID 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().
+		SetProxy(httpclient.ProxyFromEnvironment)
+
+	ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
+		if !entering || (ast.NodeLinkDest != n.Type && !n.IsTextMarkType("a")) {
+			return ast.WalkContinue
+		}
+
+		var dest []byte
+		if ast.NodeLinkDest == n.Type {
+			dest = n.Tokens
+		} else {
+			dest = []byte(n.TextMarkAHref)
+		}
+
+		if util.IsAssetLinkDest(dest) {
+			return ast.WalkContinue
+		}
+
+		if bytes.HasPrefix(bytes.ToLower(dest), []byte("file://")) { // 处理本地文件链接
+			u := string(dest)[7:]
+			if !gulu.File.IsExist(u) || gulu.File.IsDir(u) {
+				return ast.WalkContinue
+			}
+
+			name := filepath.Base(u)
+			name = util.FilterFileName(name)
+			name = util.TruncateLenFileName(name)
+			name = "network-asset-" + 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.WalkContinue
+			}
+
+			if ast.NodeLinkDest == n.Type {
+				n.Tokens = []byte("assets/" + name)
+			} else {
+				n.TextMarkAHref = "assets/" + name
+			}
+			files++
+			return ast.WalkContinue
+		}
+
+		if bytes.HasPrefix(bytes.ToLower(dest), []byte("https://")) || bytes.HasPrefix(bytes.ToLower(dest), []byte("http://")) {
+			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)
+			resp, reqErr := request.Get(u)
+			if nil != reqErr {
+				logging.LogErrorf("download network asset [%s] failed: %s", u, reqErr)
+				return ast.WalkContinue
+			}
+			if 200 != resp.StatusCode {
+				logging.LogErrorf("download network asset [%s] failed: %d", u, resp.StatusCode)
+				return ast.WalkContinue
+			}
+
+			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
+			}
+
+			data, repErr := resp.ToBytes()
+			if nil != repErr {
+				logging.LogErrorf("download network asset [%s] failed: %s", u, repErr)
+				return ast.WalkContinue
+			}
+			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.FilterFileName(name)
+			name = util.TruncateLenFileName(name)
+			name = "network-asset-" + name + "-" + ast.NewNodeID() + ext
+			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
+			}
+
+			if ast.NodeLinkDest == n.Type {
+				n.Tokens = []byte("assets/" + name)
+			} else {
+				n.TextMarkAHref = "assets/" + name
+			}
+			files++
+		}
+		return ast.WalkContinue
+	})
+
+	if 0 < files {
+		util.PushUpdateMsg(msgId, Conf.Language(113), 7000)
+		if err = writeJSONQueue(tree); nil != err {
+			return
+		}
+		util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(120), files), 5000)
+	}
+	return
+}
+
 func SearchAssetsByName(keyword string, exts []string) (ret []*cache.Asset) {
 	ret = []*cache.Asset{}