diff --git a/app/appearance/langs/en_US.json b/app/appearance/langs/en_US.json index ac1f404ab..43988e20a 100644 --- a/app/appearance/langs/en_US.json +++ b/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]", diff --git a/app/appearance/langs/es_ES.json b/app/appearance/langs/es_ES.json index ef3d6ea9a..7add10a26 100644 --- a/app/appearance/langs/es_ES.json +++ b/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]", diff --git a/app/appearance/langs/fr_FR.json b/app/appearance/langs/fr_FR.json index 9277f209e..699f5d457 100644 --- a/app/appearance/langs/fr_FR.json +++ b/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]", diff --git a/app/appearance/langs/zh_CHT.json b/app/appearance/langs/zh_CHT.json index 3f09953fe..0b75f3e30 100644 --- a/app/appearance/langs/zh_CHT.json +++ b/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": "自動同步失敗次數過多,請嘗試手動觸發同步,如果還有問題請反饋", diff --git a/app/appearance/langs/zh_CN.json b/app/appearance/langs/zh_CN.json index 10b645f60..72ad87541 100644 --- a/app/appearance/langs/zh_CN.json +++ b/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": "请在 [设置 - 启用云端同步] 中开启云端同步", diff --git a/kernel/api/format.go b/kernel/api/format.go index e234a4938..dcc54c449 100644 --- a/kernel/api/format.go +++ b/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) diff --git a/kernel/api/router.go b/kernel/api/router.go index d6a2cf2a4..4ee8ae1ac 100644 --- a/kernel/api/router.go +++ b/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) diff --git a/kernel/model/assets.go b/kernel/model/assets.go index 89ebf1810..8dee9e5fc 100644 --- a/kernel/model/assets.go +++ b/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{}