增加清理附件按钮,对悬空无引用的图片/附件进行清理 (#918)

* add attach_clean & change_theme

* add attach_clean & change_theme

* add

* add readme

---------

Co-authored-by: root <root@DESKTOP-L84EQPB.localdomain>
pull/926/head
Sharklet 2023-12-19 13:19:31 +08:00 committed by GitHub
parent 0dbb5d7967
commit 71b8e528ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 200 additions and 18 deletions

2
.gitignore vendored
View File

@ -24,7 +24,7 @@ _testmain.go
mindoc
mindoc_linux_amd64
mindoc_linux_musl_amd64
database
database/mindoc.db
*.test
*.prof
.idea

View File

@ -268,3 +268,59 @@ docker run -p 8181:8181 --name mindoc -e DB_ADAPTER=mysql -e MYSQL_PORT_3306_TCP
一个不纯粹的PHPer一个不自由的 gopher 。
# 部署补充
- 若内网部署draw.io无法使用外网则需要用tomcat运行war包https://github.com/jgraph/drawio 从release下载之后修改markdown.js的TODO行对应的链接即可
- 为了护眼简单增加了编辑界面的主题切换见editormd.js和markdown_edit_template.tpl
- (需重新编译项)为了对已删除文档/文档引用图片删除文字后,对悬空无引用的图片/附件进行清理,增加了清理接口,需重新编译
- 编译后除二进制文件外还需更新三个文件: conf/lang/en-us.ini,zh-cn.ini; attach_list.tpl
- 若不想重新编译也可通过database/clean.py手动执行对无引用图片/附件的文件清理和数据库记录双向清理。
- 若采用nginx二级部署以yourpath/为例,需修改
- conf/app.conf修改`baseurl="/yourpath"`
- static/js/kancloud.js文件中`url: "/comment/xxxxx` => `url: "/yourpath" + "/comment/xxxxx`, 共两处
- nginx端口代理示例:
```
增加
location /yourpath/ {
rewrite ^/yourpath/(.*) /$1 break;
proxy_pass http://127.0.0.1:8181;
}
```
注意使用的是127.0.0.1根据自身选择替换如果nginx是docker部署则还需要在docker中托管运行mindoc具体参考如下配置:
- docker-compose代理示例(docker-nginx代理运行mindoc)
```
version: '3'
services:
mynginx:
image: nginx:latest
ports:
- "8880:80"
command:
- bash
- -c
- |
service nginx start
cd /src/mindoc/ && ./mindoc
volumes:
- ..:/src
- ./nginx:/etc/nginx/conf.d
```
目录结构
```
onefolder
|
- docker
|
- docker-compose.yml
- nginx
|
- mynginx.conf
- mindoc
|
- database/
- conf/
- ...
```

View File

@ -54,6 +54,7 @@ yes = yes
no = no
read = Read
generate = Generate
clean = Clean
[init]
default_proj_name = MinDoc Demo Project

View File

@ -54,6 +54,7 @@ yes = 是
no =
read = 阅读
generate = 生成
clean = 清理
[init]
default_proj_name = MinDoc演示项目

View File

@ -634,6 +634,41 @@ func (c *ManagerController) AttachList() {
c.Data["Lists"] = attachList
}
//附件清理.
func (c *ManagerController) AttachClean() {
c.Prepare()
attachList, _, err := models.NewAttachment().FindToPager(0, 0)
if err != nil {
c.Abort("500")
}
for _, item := range attachList {
p := filepath.Join(conf.WorkingDirectory, item.FilePath)
item.IsExist = filetil.FileExists(p)
if item.IsExist {
// 判断
searchList, err := models.NewDocumentSearchResult().SearchAllDocument(item.HttpPath)
if err != nil {
c.Abort("500")
} else if len(searchList) == 0 {
logs.Info("delete file:", item.FilePath)
item.FilePath = p
if err := item.Delete(); err != nil {
logs.Error("AttachDelete => ", err)
c.JsonResult(6002, err.Error())
break
}
}
}
}
c.JsonResult(0, "ok")
}
//附件详情.
func (c *ManagerController) AttachDetailed() {
c.Prepare()

34
database/clean.py 100644
View File

@ -0,0 +1,34 @@
import sqlite3
import os, glob
conn = sqlite3.connect("mindoc.db")
cur = conn.cursor() #通过建立数据库游标对象,准备读写操作
cmd = """
SELECT
att.http_path
FROM
md_attachment AS att
WHERE (att.document_id != 0 OR (NOT EXISTS( SELECT 1 FROM md_documents WHERE markdown LIKE ("%" || att.http_path || "%"))))
AND (att.document_id = 0 OR (NOT EXISTS( SELECT 1 FROM md_documents WHERE att.document_id = document_id )))
"""
cur.execute(cmd)
file_list = cur.fetchall()
for file_item in file_list:
item_path = file_item[0]
# 1. 删除os文件
if os.path.exists(os.path.join("..", item_path[1:])):
os.remove(os.path.join("..", item_path[1:]))
# 2. 查询os是否删除成功成功则删除附件记录
if not os.path.exists(os.path.join("..", item_path[1:])):
cmd = """
delete
from md_attachment
WHERE http_path = '{}'
""".format(item_path)
cur.execute(cmd)
conn.commit() #保存提交,确保数据保存成功
conn.close() #关闭与数据库的连接

View File

@ -102,11 +102,15 @@ func (m *Attachment) FindToPager(pageIndex, pageSize int) (attachList []*Attachm
return nil, 0, err
}
totalCount = int(total)
offset := (pageIndex - 1) * pageSize
var list []*Attachment
_, err = o.QueryTable(m.TableNameWithPrefix()).OrderBy("-attachment_id").Offset(offset).Limit(pageSize).All(&list)
offset := (pageIndex - 1) * pageSize
if pageSize == 0 {
_, err = o.QueryTable(m.TableNameWithPrefix()).OrderBy("-attachment_id").Offset(offset).Limit(pageSize).All(&list)
} else {
_, err = o.QueryTable(m.TableNameWithPrefix()).OrderBy("-attachment_id").All(&list)
}
if err != nil {
if err == orm.ErrNoRows {

View File

@ -309,3 +309,23 @@ func (m *DocumentSearchResult) SearchDocument(keyword string, bookId int) (docs
return
}
// 所有项目搜索.
func (m *DocumentSearchResult) SearchAllDocument(keyword string) (docs []*DocumentSearchResult, err error) {
o := orm.NewOrm()
sql := "SELECT * FROM md_documents WHERE (document_name LIKE ? OR `release` LIKE ?) "
keyword = "%" + keyword + "%"
_need_escape := need_escape(keyword)
escape_sql := func(sql string) string {
if _need_escape {
return escape_re.ReplaceAllString(sql, escape_replace)
}
return sql
}
_, err = o.Raw(escape_sql(sql), keyword, keyword).QueryRows(&docs)
return
}

View File

@ -154,6 +154,7 @@ func init() {
web.Router("/manager/books/open", &controllers.ManagerController{}, "post:PrivatelyOwned")
web.Router("/manager/attach/list", &controllers.ManagerController{}, "*:AttachList")
web.Router("/manager/attach/clean", &controllers.ManagerController{}, "post:AttachClean")
web.Router("/manager/attach/detailed/:id", &controllers.ManagerController{}, "*:AttachDetailed")
web.Router("/manager/attach/delete", &controllers.ManagerController{}, "post:AttachDelete")
web.Router("/manager/label/list", &controllers.ManagerController{}, "get:LabelList")

View File

@ -71,7 +71,7 @@
"list-ul", "list-ol", "hr", "|",
"link", "reference-link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "emoji", "html-entities", "pagebreak", "|",
"goto-line", "watch", "preview", "fullscreen", "clear", "search", "|",
"help", "info"
"help", "changetheme", "info"
],
simple : [
"undo", "redo", "|",
@ -79,12 +79,12 @@
"h1", "h2", "h3", "h4", "h5", "h6", "|",
"list-ul", "list-ol", "hr", "|",
"watch", "preview", "fullscreen", "|",
"help", "info"
"help", "changetheme", "info"
],
mini : [
"undo", "redo", "|",
"watch", "preview", "|",
"help", "info"
"help", "changetheme", "info"
]
};
@ -94,8 +94,8 @@
name : "", // Form element name
value : "", // value for CodeMirror, if mode not gfm/markdown
theme : "", // Editor.md self themes, before v1.5.0 is CodeMirror theme, default empty
editorTheme : "default", // Editor area, this is CodeMirror theme at v1.5.0
previewTheme : "", // Preview area theme, default empty
editorTheme : "pastel-on-dark", //"default", // Editor area, this is CodeMirror theme at v1.5.0
previewTheme : "dark", //"", // Preview area theme, default empty
markdown : "", // Markdown source code
appendMarkdown : "", // if in init textarea value not empty, append markdown to textarea
width : "100%",
@ -225,6 +225,7 @@
fullscreen : "fa-arrows-alt",
clear : "fa-eraser",
help : "fa-question-circle",
changetheme : "fa-info-circle",
info : "fa-info-circle"
},
toolbarIconTexts : {},
@ -271,6 +272,7 @@
clear : "清空",
search : "搜索",
help : "使用帮助",
changetheme : "切换编辑主题",
info : "关于" + editormd.title
},
buttons : {
@ -322,7 +324,10 @@
},
help : {
title : "使用帮助"
}
},
changetheme : {
title : "切换编辑主题"
},
}
}
};
@ -3385,6 +3390,11 @@
this.executePlugin("helpDialog", "help-dialog/help-dialog");
},
changetheme : function() {
this.setEditorTheme((this.settings.editorTheme=="default")?"pastel-on-dark":"default");
this.setPreviewTheme((this.settings.previewTheme=="")?"dark":"");
},
info : function() {
this.showInfoDialog();
}

View File

@ -72,7 +72,7 @@ $(function () {
drawio.show = function () {
const drawUrl = 'https://embed.diagrams.net/?embed=1&libraries=1&proto=json&spin=1&saveAndExit=1&noSaveBtn=1&noExitBtn=0';
const drawUrl = 'https://embed.diagrams.net/?embed=1&libraries=1&proto=json&spin=1&saveAndExit=1&noSaveBtn=1&noExitBtn=0'; // TODO: with Tomcat & https://github.com/jgraph/drawio
this.div = document.createElement('div');
this.div.id = 'diagram';
this.gXml = '';

View File

@ -80,12 +80,6 @@
<div class="editormd-group">
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.h1"}}"><i class="fa editormd-bold first" name="h1" unselectable="on">H1</i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.h2"}}"><i class="fa editormd-bold item" name="h2" unselectable="on">H2</i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.h3"}}"><i class="fa editormd-bold item" name="h3" unselectable="on">H3</i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.h4"}}"><i class="fa editormd-bold item" name="h4" unselectable="on">H4</i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.h5"}}"><i class="fa editormd-bold item" name="h5" unselectable="on">H5</i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.h6"}}"><i class="fa editormd-bold last" name="h6" unselectable="on">H6</i></a>
</div>
<div class="editormd-group">
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.unorder_list"}}"><i class="fa fa-list-ul first" name="list-ul" unselectable="on"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.order_list"}}"><i class="fa fa-list-ol item" name="list-ol" unselectable="on"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.hline"}}"><i class="fa fa-minus last" name="hr" unselectable="on"></i></a>
@ -111,10 +105,12 @@
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.modify_history"}}"><i class="fa fa-history item" name="history" aria-hidden="true"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.sidebar"}}"><i class="fa fa-columns item" aria-hidden="true" name="sidebar"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.help"}}"><i class="fa fa-question-circle-o last" aria-hidden="true" name="help"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.changetheme"}}"><i class="fa fa-paint-brush item" aria-hidden="true" name="changetheme"></i></a>
</div>
<div class="editormd-group pull-right">
<a target="_blank" href="{{urlfor "DocumentController.Read" ":key" .Model.Identify ":id" ""}}" data-toggle="tooltip" data-title="{{i18n .Lang "blog.preview"}}"><i class="fa fa-external-link" name="preview-open" aria-hidden="true"></i></a>
<!--<a target="_blank" href="{{urlfor "DocumentController.Read" ":key" .Model.Identify ":id" ""}}" data-toggle="tooltip" data-title="{{i18n .Lang "blog.preview"}}"><i class="fa fa-external-link" name="preview-open" aria-hidden="true"></i></a>-->
<a href="{{urlfor "DocumentController.Read" ":key" .Model.Identify ":id" ""}}" data-toggle="tooltip" data-title="{{i18n .Lang "blog.preview"}}"><i class="fa fa-external-link" name="preview-open" aria-hidden="true"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.publish"}}"><i class="fa fa-cloud-upload" name="release" aria-hidden="true"></i></a>
</div>

View File

@ -27,8 +27,9 @@
{{template "manager/widgets.tpl" .}}
<div class="page-right">
<div class="m-box">
<div class="box-head">
<div class="box-head" id="attachAll">
<strong class="box-title">{{i18n .Lang "mgr.attachment_mgr"}}</strong>
<button type="button" data-method="clean" class="btn btn-danger btn-sm" data-loading-text="{{i18n $.Lang "message.processing"}}">{{i18n $.Lang "common.clean"}}</button>
</div>
</div>
<div class="box-body">
@ -104,6 +105,29 @@
}
});
});
$("#attachAll").on("click","button[data-method='clean']",function () {
var $this = $(this);
$(this).button("loading");
$.ajax({
url : "{{urlfor "ManagerController.AttachClean"}}",
type : "post",
dataType : "json",
success : function (res) {
if(res.errcode === 0){
alert("done");
}else {
layer.msg(res.message);
}
},
error : function () {
layer.msg({{i18n .Lang "message.system_error"}});
},
complete : function () {
$this.button("reset");
}
});
});
});
</script>
</body>