@tachybase/plugin-database-clean 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +134 -0
  2. package/README.zh-CN.md +136 -0
  3. package/client.d.ts +2 -0
  4. package/client.js +1 -0
  5. package/dist/client/index.d.ts +1 -0
  6. package/dist/client/index.js +1 -0
  7. package/dist/client/locale.d.ts +7 -0
  8. package/dist/client/pages/TableDetail.d.ts +1 -0
  9. package/dist/client/pages/TableList.d.ts +1 -0
  10. package/dist/client/plugin.d.ts +7 -0
  11. package/dist/externalVersion.js +11 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +39 -0
  14. package/dist/locale/en-US.json +65 -0
  15. package/dist/locale/zh-CN.json +65 -0
  16. package/dist/node_modules/archiver/LICENSE +22 -0
  17. package/dist/node_modules/archiver/index.js +73 -0
  18. package/dist/node_modules/archiver/lib/core.js +974 -0
  19. package/dist/node_modules/archiver/lib/error.js +40 -0
  20. package/dist/node_modules/archiver/lib/plugins/json.js +110 -0
  21. package/dist/node_modules/archiver/lib/plugins/tar.js +167 -0
  22. package/dist/node_modules/archiver/lib/plugins/zip.js +120 -0
  23. package/dist/node_modules/archiver/package.json +1 -0
  24. package/dist/server/adapters/base-adapter.d.ts +63 -0
  25. package/dist/server/adapters/base-adapter.js +31 -0
  26. package/dist/server/adapters/index.d.ts +36 -0
  27. package/dist/server/adapters/index.js +86 -0
  28. package/dist/server/adapters/mysql-adapter.d.ts +13 -0
  29. package/dist/server/adapters/mysql-adapter.js +118 -0
  30. package/dist/server/adapters/postgres-adapter.d.ts +13 -0
  31. package/dist/server/adapters/postgres-adapter.js +115 -0
  32. package/dist/server/adapters/sqlite-adapter.d.ts +13 -0
  33. package/dist/server/adapters/sqlite-adapter.js +115 -0
  34. package/dist/server/collections/.gitkeep +0 -0
  35. package/dist/server/constants.d.ts +4 -0
  36. package/dist/server/constants.js +38 -0
  37. package/dist/server/index.d.ts +1 -0
  38. package/dist/server/index.js +33 -0
  39. package/dist/server/plugin.d.ts +11 -0
  40. package/dist/server/plugin.js +61 -0
  41. package/dist/server/resourcers/database-clean.d.ts +31 -0
  42. package/dist/server/resourcers/database-clean.js +303 -0
  43. package/dist/server/services/database-service.d.ts +46 -0
  44. package/dist/server/services/database-service.js +138 -0
  45. package/dist/server/services/filtered-backup-service.d.ts +40 -0
  46. package/dist/server/services/filtered-backup-service.js +269 -0
  47. package/dist/server/utils/lock.d.ts +23 -0
  48. package/dist/server/utils/lock.js +62 -0
  49. package/dist/server/utils.d.ts +2 -0
  50. package/dist/server/utils.js +43 -0
  51. package/package.json +26 -0
  52. package/server.d.ts +2 -0
  53. package/server.js +1 -0
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # @tachybase/plugin-database-clean
2
+
3
+ Database Clean Plugin - View table usage and clean database tables
4
+
5
+ ## Features
6
+
7
+ - 📊 **Table Overview**: View whitelisted table usage including size, row count, creation time, update time
8
+ - 🔍 **Data Filtering**: Filter by createdAt/updatedAt time ranges or ID range
9
+ - 💾 **Data Backup**: Backup filtered data before cleaning (`.tbdump` format, compatible with module-backup)
10
+ - 🗑️ **Data Cleanup**: Safe physical deletion with filtered data cleanup
11
+ - 📦 **Batch Cleaning**: Support batch cleaning for large datasets (split by count or batch size, up to 1000 batches)
12
+ - 🔄 **Space Release**: Optional VACUUM FULL to release disk space after cleaning
13
+ - 🔒 **Security Control**: Whitelist mechanism, only allows operations on specified tables
14
+ - 🏗️ **Database Adapter**: Extensible adapter architecture supporting PostgreSQL, MySQL, and SQLite
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pnpm pm add @tachybase/plugin-database-clean
20
+ pnpm pm enable @tachybase/plugin-database-clean
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Configure Whitelist
26
+
27
+ Configure whitelist tables in `src/server/constants.ts`:
28
+
29
+ ```typescript
30
+ export const WHITELIST_TABLES = [
31
+ 'users',
32
+ 'orders',
33
+ 'logs',
34
+ ];
35
+ ```
36
+
37
+ ### Permission Configuration
38
+
39
+ The plugin automatically registers ACL snippet: `pm.database-clean.*`
40
+
41
+ Configure corresponding permissions in role permissions to use.
42
+
43
+ ## UI Workflow
44
+
45
+ 1. **Table List Page**: View all whitelisted tables with their size, row count, and time info
46
+ 2. **Table Detail Page**:
47
+ - View table data with pagination
48
+ - Filter by time range (createdAt/updatedAt) or ID range
49
+ - Click "Clean" button to start the cleaning workflow
50
+ 3. **Cleaning Workflow**:
51
+ - Step 1: Choose to backup first or clean directly
52
+ - Step 2: If backup, optionally download the backup file
53
+ - Step 3: Configure batch settings (no batch / split into N batches / N records per batch)
54
+ - Step 4: Choose to release disk space (VACUUM FULL) or clean only
55
+ - During cleaning: Button shows progress like "(1/100) Cleaning..."
56
+
57
+ ## API
58
+
59
+ ### Get Table List
60
+
61
+ ```
62
+ GET /databaseClean:list
63
+ ```
64
+
65
+ ### Get Table Info
66
+
67
+ ```
68
+ GET /databaseClean:get?filterByTk=tableName
69
+ ```
70
+
71
+ Returns: Table info including `hasCreatedAt`, `hasUpdatedAt`, `minId`, `maxId`
72
+
73
+ ### Get Table Data
74
+
75
+ ```
76
+ GET /databaseClean:data?filterByTk=tableName&page=1&pageSize=20&filter=...
77
+ ```
78
+
79
+ Returns: Paginated data with `filteredMinId`, `filteredMaxId` for batch cleaning support
80
+
81
+ ### Backup Data
82
+
83
+ ```
84
+ POST /databaseClean:backup
85
+ {
86
+ "collectionName": "users",
87
+ "filter": {
88
+ "createdAt": {
89
+ "$gte": "2024-01-01T00:00:00Z",
90
+ "$lte": "2024-12-31T23:59:59Z"
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### Clean Data
97
+
98
+ ```
99
+ POST /databaseClean:clean
100
+ {
101
+ "collectionName": "users",
102
+ "filter": {
103
+ "createdAt": {
104
+ "$gte": "2024-01-01T00:00:00Z",
105
+ "$lte": "2024-12-31T23:59:59Z"
106
+ }
107
+ },
108
+ "vacuumFull": true // Optional: Execute VACUUM FULL after cleaning (release disk space)
109
+ }
110
+ ```
111
+
112
+ ### Download Backup File
113
+
114
+ ```
115
+ GET /databaseClean:download?filterByTk=db-clean_users_20240101_120000.tbdump
116
+ ```
117
+
118
+ ## Backup File Format
119
+
120
+ Backup files use `.tbdump` format (compatible with `module-backup`):
121
+ - Filename: `db-clean_{tableName}_{filterRange}_{timestamp}_{random}.tbdump`
122
+ - Contains: JSON-formatted data with meta information
123
+ - Can be restored using standard backup restore procedures
124
+
125
+ ## Notes
126
+
127
+ - Supports PostgreSQL, MySQL, and SQLite databases
128
+ - Only allows operations on whitelisted tables
129
+ - Backup is optional before cleanup (recommended but not required)
130
+ - Cleanup operations are physical deletions, please use with caution
131
+ - **VACUUM FULL** (PostgreSQL): Locks the table during execution, may take a long time for large tables
132
+ - **OPTIMIZE TABLE** (MySQL): Reclaims space and defragments the table
133
+ - **VACUUM** (SQLite): Rebuilds the entire database file
134
+ - **Batch Cleaning**: For large datasets (>50,000 records), it's recommended to use batch cleaning to avoid long-running transactions
@@ -0,0 +1,136 @@
1
+ # @tachybase/plugin-database-clean
2
+
3
+ 数据库清理插件 - 用于查看数据表占用和清理数据表
4
+
5
+ ## 功能特性
6
+
7
+ - 📊 **表概览**:查看白名单数据表的占用大小、数据条数、创建时间、更新时间
8
+ - 🔍 **数据筛选**:支持按 createdAt/updatedAt 时间范围或 ID 范围筛选
9
+ - 💾 **数据备份**:清理前备份筛选数据(`.tbdump` 格式,兼容 module-backup)
10
+ - 🗑️ **数据清理**:安全的物理删除操作,支持筛选数据清理
11
+ - 📦 **分批清理**:支持大数据量分批清理(按批数或每批条数分割,最多 1000 批)
12
+ - 🔄 **空间释放**:清理后可选执行 VACUUM FULL 释放磁盘空间
13
+ - 🔒 **安全控制**:白名单机制,只允许操作指定的表
14
+ - 🏗️ **数据库适配器**:可扩展的适配器架构,支持 PostgreSQL、MySQL 和 SQLite
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ pnpm pm add @tachybase/plugin-database-clean
20
+ pnpm pm enable @tachybase/plugin-database-clean
21
+ ```
22
+
23
+ ## 使用
24
+
25
+ ### 配置白名单
26
+
27
+ 在 `src/server/constants.ts` 中配置白名单表:
28
+
29
+ ```typescript
30
+ export const WHITELIST_TABLES = [
31
+ 'users',
32
+ 'orders',
33
+ 'logs',
34
+ ];
35
+ ```
36
+
37
+ ### 权限配置
38
+
39
+ 插件会自动注册 ACL snippet:`pm.database-clean.*`
40
+
41
+ 在角色权限中配置相应权限即可使用。
42
+
43
+ ## 界面操作流程
44
+
45
+ 1. **表列表页面**:查看所有白名单表的大小、数据条数和时间信息
46
+ 2. **表详情页面**:
47
+ - 分页查看表数据
48
+ - 按时间范围(createdAt/updatedAt)或 ID 范围筛选
49
+ - 点击"清理"按钮开始清理流程
50
+ 3. **清理流程**:
51
+ - 第一步:选择先备份还是直接清理
52
+ - 第二步:如果备份,可选择下载备份文件
53
+ - 第三步:配置分批设置(不分批 / 分为 N 批 / 每批 N 条)
54
+ - 第四步:选择是否释放磁盘空间(VACUUM FULL)
55
+ - 清理过程中:按钮显示进度,如 "(1/100) 清理中..."
56
+
57
+ ## API
58
+
59
+ ### 获取表列表
60
+
61
+ ```
62
+ GET /databaseClean:list
63
+ ```
64
+
65
+ ### 获取表信息
66
+
67
+ ```
68
+ GET /databaseClean:get?filterByTk=表名
69
+ ```
70
+
71
+ 返回:表信息,包含 `hasCreatedAt`、`hasUpdatedAt`、`minId`、`maxId`
72
+
73
+ ### 获取表数据
74
+
75
+ ```
76
+ GET /databaseClean:data?filterByTk=表名&page=1&pageSize=20&filter=...
77
+ ```
78
+
79
+ 返回:分页数据,包含 `filteredMinId`、`filteredMaxId` 用于分批清理
80
+
81
+ ### 备份数据
82
+
83
+ ```
84
+ POST /databaseClean:backup
85
+ {
86
+ "collectionName": "users",
87
+ "filter": {
88
+ "createdAt": {
89
+ "$gte": "2024-01-01T00:00:00Z",
90
+ "$lte": "2024-12-31T23:59:59Z"
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### 清理数据
97
+
98
+ ```
99
+ POST /databaseClean:clean
100
+ {
101
+ "collectionName": "users",
102
+ "filter": {
103
+ "createdAt": {
104
+ "$gte": "2024-01-01T00:00:00Z",
105
+ "$lte": "2024-12-31T23:59:59Z"
106
+ }
107
+ },
108
+ "vacuumFull": true // 可选:清理后执行 VACUUM FULL(释放磁盘空间)
109
+ }
110
+ ```
111
+
112
+ ### 下载备份文件
113
+
114
+ ```
115
+ GET /databaseClean:download?filterByTk=db-clean_users_20240101_120000.tbdump
116
+ ```
117
+
118
+ ## 备份文件格式
119
+
120
+ 备份文件使用 `.tbdump` 格式(兼容 `module-backup`):
121
+ - 文件名:`db-clean_{表名}_{筛选范围}_{时间戳}_{随机数}.tbdump`
122
+ - 内容:JSON 格式的数据及元信息
123
+ - 可通过标准备份恢复流程进行恢复
124
+
125
+ ## 注意事项
126
+
127
+ - 支持 PostgreSQL、MySQL 和 SQLite 数据库
128
+ - 只允许操作白名单中的表
129
+ - 清理前备份是可选的(建议备份但不强制)
130
+ - 清理操作是物理删除,请谨慎操作
131
+ - **VACUUM FULL**(PostgreSQL):执行时会锁定数据表,对于大表可能需要较长时间
132
+ - **OPTIMIZE TABLE**(MySQL):回收空间并整理表碎片
133
+ - **VACUUM**(SQLite):重建整个数据库文件
134
+ - **分批清理**:对于大数据量(超过 5 万条),建议使用分批清理以避免长时间事务
135
+ - **索引维护**:所有数据库在执行 DELETE 操作时会自动维护索引,无需手动重建
136
+
package/client.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './dist/client';
2
+ export { default } from './dist/client';
package/client.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/client/index.js');
@@ -0,0 +1 @@
1
+ export { default } from './plugin';
@@ -0,0 +1 @@
1
+ (function(p,u){typeof exports=="object"&&typeof module!="undefined"?u(exports,require("@tachybase/client"),require("react/jsx-runtime"),require("react"),require("@ant-design/icons"),require("antd"),require("dayjs"),require("react-router-dom")):typeof define=="function"&&define.amd?define(["exports","@tachybase/client","react/jsx-runtime","react","@ant-design/icons","antd","dayjs","react-router-dom"],u):(p=typeof globalThis!="undefined"?globalThis:p||self,u(p["@tachybase/plugin-database-clean"]={},p["@tachybase/client"],p.jsxRuntime,p.react,p["@ant-design/icons"],p.antd,p.dayjs,p["react-router-dom"]))})(this,function(p,u,e,r,I,a,P,j){"use strict";var ut=Object.defineProperty,ht=Object.defineProperties;var pt=Object.getOwnPropertyDescriptors;var Ee=Object.getOwnPropertySymbols;var ft=Object.prototype.hasOwnProperty,gt=Object.prototype.propertyIsEnumerable;var Ne=(p,u,e)=>u in p?ut(p,u,{enumerable:!0,configurable:!0,writable:!0,value:e}):p[u]=e,v=(p,u)=>{for(var e in u||(u={}))ft.call(u,e)&&Ne(p,e,u[e]);if(Ee)for(var e of Ee(u))gt.call(u,e)&&Ne(p,e,u[e]);return p},A=(p,u)=>ht(p,pt(u));var T=(p,u,e)=>new Promise((r,I)=>{var a=M=>{try{j(e.next(M))}catch(_){I(_)}},P=M=>{try{j(e.throw(M))}catch(_){I(_)}},j=M=>M.done?r(M.value):Promise.resolve(M.value).then(a,P);j((e=e.apply(p,u)).next())});const M="database-clean";function _(){const{i18n:t}=u.useApp();return{t:(w,f={})=>t.t(w,v({ns:[M,"client"],nsMode:"fallback"},f))}}var ne=typeof globalThis!="undefined"?globalThis:typeof window!="undefined"?window:typeof global!="undefined"?global:typeof self!="undefined"?self:{},oe={exports:{}},je=oe.exports,ye;function $e(){return ye||(ye=1,function(t,y){(function(w,f){f()})(je,function(){function w(o,i){return typeof i=="undefined"?i={autoBom:!1}:typeof i!="object"&&(console.warn("Deprecated: Expected third argument to be a object"),i={autoBom:!i}),i.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(o.type)?new Blob(["\uFEFF",o],{type:o.type}):o}function f(o,i,C){var c=new XMLHttpRequest;c.open("GET",o),c.responseType="blob",c.onload=function(){L(c.response,i,C)},c.onerror=function(){console.error("could not download file")},c.send()}function Y(o){var i=new XMLHttpRequest;i.open("HEAD",o,!1);try{i.send()}catch(C){}return 200<=i.status&&299>=i.status}function b(o){try{o.dispatchEvent(new MouseEvent("click"))}catch(C){var i=document.createEvent("MouseEvents");i.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),o.dispatchEvent(i)}}var g=typeof window=="object"&&window.window===window?window:typeof self=="object"&&self.self===self?self:typeof ne=="object"&&ne.global===ne?ne:void 0,W=g.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),L=g.saveAs||(typeof window!="object"||window!==g?function(){}:"download"in HTMLAnchorElement.prototype&&!W?function(o,i,C){var c=g.URL||g.webkitURL,k=document.createElement("a");i=i||o.name||"download",k.download=i,k.rel="noopener",typeof o=="string"?(k.href=o,k.origin===location.origin?b(k):Y(k.href)?f(o,i,C):b(k,k.target="_blank")):(k.href=c.createObjectURL(o),setTimeout(function(){c.revokeObjectURL(k.href)},4e4),setTimeout(function(){b(k)},0))}:"msSaveOrOpenBlob"in navigator?function(o,i,C){if(i=i||o.name||"download",typeof o!="string")navigator.msSaveOrOpenBlob(w(o,C),i);else if(Y(o))f(o,i,C);else{var c=document.createElement("a");c.href=o,c.target="_blank",setTimeout(function(){b(c)})}}:function(o,i,C,c){if(c=c||open("","_blank"),c&&(c.document.title=c.document.body.innerText="downloading..."),typeof o=="string")return f(o,i,C);var k=o.type==="application/octet-stream",K=/constructor/i.test(g.HTMLElement)||g.safari,h=/CriOS\/[\d]+/.test(navigator.userAgent);if((h||k&&K||W)&&typeof FileReader!="undefined"){var D=new FileReader;D.onloadend=function(){var B=D.result;B=h?B:B.replace(/^data:[^;]*;/,"data:attachment/file;"),c?c.location.href=B:location=B,c=null},D.readAsDataURL(o)}else{var O=g.URL||g.webkitURL,$=O.createObjectURL(o);c?c.location=$:location.href=$,c=null,setTimeout(function(){O.revokeObjectURL($)},4e4)}});g.saveAs=L.saveAs=L,t.exports=L})}(oe)),oe.exports}var Ve=$e();function J(t){const y={};if(t.createdAt&&t.createdAt[0]&&t.createdAt[1]){const w=P(t.createdAt[0]),f=P(t.createdAt[1]);w.isValid()&&f.isValid()&&(y.createdAt={$gte:w.startOf("day").toISOString(),$lte:f.endOf("day").toISOString()})}if(t.updatedAt&&t.updatedAt[0]&&t.updatedAt[1]){const w=P(t.updatedAt[0]),f=P(t.updatedAt[1]);w.isValid()&&f.isValid()&&(y.updatedAt={$gte:w.startOf("day").toISOString(),$lte:f.endOf("day").toISOString()})}return t.idRange&&(t.idRange[0]!==null||t.idRange[1]!==null)&&(y.id={},t.idRange[0]!==null&&(y.id.$gte=t.idRange[0]),t.idRange[1]!==null&&(y.id.$lte=t.idRange[1])),y}const qe=()=>{var Te,Be,me,Me,xe,Ie,Le,De,Oe,ze;const{t}=_(),y=u.useAPIClient(),w=j.useNavigate(),f=j.useLocation(),b=j.useParams().tableName||f.pathname.split("/").pop(),{message:g,modal:W,notification:L}=a.App.useApp(),[o,i]=r.useState(null),[C,c]=r.useState([]),[k,K]=r.useState(!1),[h,D]=r.useState(!1),[O,$]=r.useState(!1),[B,be]=r.useState(!1),[de,yt]=r.useState(!1),[Se,ue]=r.useState(null),[Ge,Q]=r.useState(!1),[He,Z]=r.useState(!1),[Xe,G]=r.useState(!1),[Je,le]=r.useState(!1),[Ye,he]=r.useState(null),[E,Qe]=r.useState({count:0,minId:null,maxId:null}),[V,we]=r.useState("none"),[se,Ze]=r.useState(10),[re,Re]=r.useState(1e4),[z,R]=r.useState(null),[ee,pe]=r.useState({current:1,pageSize:20,total:0}),[x,te]=r.useState({}),d=h||O||B||de,ae=r.useMemo(()=>y.resource("databaseClean"),[y]);r.useEffect(()=>{b&&Ce()},[b]),r.useEffect(()=>{b&&o&&ke()},[b,o,ee.current,ee.pageSize,x]);const Ce=()=>T(null,null,function*(){var n;if(b)try{const s=yield ae.get({filterByTk:b});i((n=s.data)==null?void 0:n.data)}catch(s){g.error(s.message||t("Failed to load table info")),w("/_admin/system-services/database-clean")}}),ke=()=>T(null,null,function*(){var n,s;if(b){K(!0);try{const l=J(x),m=yield ae.data({filterByTk:b,page:ee.current,pageSize:ee.pageSize,filter:Object.keys(l).length>0?l:void 0}),{data:q,meta:S}=m.data;c(Array.isArray(q)?q:[]),pe(H=>A(v({},H),{total:(S==null?void 0:S.count)||0})),Qe({count:(S==null?void 0:S.count)||0,minId:(n=S==null?void 0:S.filteredMinId)!=null?n:null,maxId:(s=S==null?void 0:S.filteredMaxId)!=null?s:null})}catch(l){g.error(l.message||t("Failed to load table data"))}finally{K(!1)}}}),ve=()=>T(null,null,function*(){yield Promise.all([Ce(),ke()])}),et=()=>T(null,null,function*(){var n,s;if(b){D(!0);try{const l=J(x),q=(s=(n=(yield ae.backup({values:{collectionName:b,filter:Object.keys(l).length>0?l:void 0}})).data)==null?void 0:n.data)==null?void 0:s.fileName;return ue(q),g.success(t("Backup Success")),q}catch(l){return g.error(l.message||t("Backup Failed")),null}finally{D(!1)}}}),tt=n=>T(null,null,function*(){const s=n||Se;if(s){$(!0);try{const l=yield y.request({url:"databaseClean:download",method:"get",params:{filterByTk:s},responseType:"blob"}),m=new Blob([l.data]);return Ve.saveAs(m,s),g.success(t("Download")+" "+t("Success")),!0}catch(l){return g.error(l.message||t("Download")+" "+t("Failed")),!1}finally{$(!1)}}}),at=()=>{Q(!0)},nt=()=>T(null,null,function*(){const n=yield et();n&&(ue(n),he(J(x)),Q(!1),Z(!0))}),ot=()=>T(null,null,function*(){(yield tt(Se))&&(Z(!1),G(!0))}),lt=()=>{Z(!1),G(!0)},st=()=>T(null,null,function*(){he(J(x)),Q(!1),G(!0)}),rt=()=>{G(!1),le(!0)},Ae=n=>T(null,null,function*(){le(!1),yield it(n)}),it=(n=!1)=>T(null,null,function*(){var s,l,m,q;if(b){be(!0),R(null);try{const S=Ye||J(x);let H=0;const{minId:ie,maxId:fe,count:Fe}=E;let U=[];if(V!=="none"&&ie!==null&&fe!==null&&Fe>0){const F=fe-ie+1;let N;V==="count"?N=se:N=Math.ceil(Fe/re),N=Math.max(1,N);const ce=Math.ceil(F/N);for(let X=0;X<N;X++){const Pe=ie+X*ce,ge=Math.min(ie+(X+1)*ce-1,fe);U.push({minId:Pe,maxId:ge})}}if(U.length>1){R({current:0,total:U.length,deletedCount:0});for(let F=0;F<U.length;F++){const N=U[F],ce=A(v({},S),{id:A(v({},S.id),{$gte:N.minId,$lte:N.maxId})}),X=F===U.length-1,ge=((l=(s=(yield ae.clean({values:{collectionName:b,filter:ce,vacuumFull:X&&n}})).data)==null?void 0:s.data)==null?void 0:l.deletedCount)||0;H+=ge,R({current:F+1,total:U.length,deletedCount:H})}}else H=((q=(m=(yield ae.clean({values:{collectionName:b,filter:Object.keys(S).length>0?S:void 0,vacuumFull:n}})).data)==null?void 0:m.data)==null?void 0:q.deletedCount)||0;g.success(t("Clean Success")+` (${H} ${t("records deleted")})`),n&&g.success(t("Space released successfully")),ue(null),he(null),R(null),we("none"),te({}),pe(F=>A(v({},F),{current:1})),yield ve()}catch(S){g.error(S.message||t("Clean Failed"))}finally{be(!1),R(null)}}}),ct=n=>{pe(s=>A(v({},s),{current:n.current,pageSize:n.pageSize}))},dt=r.useMemo(()=>{if(!C.length)return[];const n=C[0];return Object.keys(n).map(s=>({title:s,dataIndex:s,key:s,render:l=>l instanceof Date||typeof l=="string"&&l.match(/^\d{4}-\d{2}-\d{2}/)?e.jsx(u.DatePicker.ReadPretty,{value:P(l),showTime:!0}):typeof l=="object"&&l!==null?e.jsx("pre",{style:{margin:0},children:JSON.stringify(l,null,2)}):String(l)}))},[C]);return o?e.jsx("div",{children:e.jsx(a.Card,{bordered:!1,children:e.jsxs(a.Space,{direction:"vertical",style:{width:"100%"},size:"large",children:[e.jsxs("div",{children:[e.jsx("h2",{children:o.name}),e.jsxs(a.Space,{children:[e.jsxs("span",{children:[t("Origin"),": ",o.origin]}),e.jsx("span",{children:"|"}),e.jsxs("span",{children:[t("Size"),": ",Ue(o.size)]}),e.jsx("span",{children:"|"}),e.jsxs("span",{children:[t("Row Count"),": ",o.rowCount.toLocaleString()]})]})]}),e.jsx(a.Card,{size:"small",title:t("Filter"),children:e.jsxs(a.Space,{direction:"vertical",style:{width:"100%"},children:[o.hasCreatedAt&&e.jsxs("div",{children:[e.jsxs("label",{children:[t("Created At Range"),": "]}),e.jsx(u.DatePicker.RangePicker,{value:x.createdAt,onChange:n=>{te(s=>A(v({},s),{createdAt:n}))},showTime:!0,style:{width:400}})]}),o.hasUpdatedAt&&e.jsxs("div",{children:[e.jsxs("label",{children:[t("Updated At Range"),": "]}),e.jsx(u.DatePicker.RangePicker,{value:x.updatedAt,onChange:n=>{te(s=>A(v({},s),{updatedAt:n}))},showTime:!0,style:{width:400}})]}),e.jsxs("div",{children:[e.jsxs("label",{children:[t("ID Range"),": "]}),e.jsxs(a.Space,{children:[e.jsx(a.InputNumber,{placeholder:t("Min ID"),value:(Te=x.idRange)==null?void 0:Te[0],onChange:n=>{te(s=>{var m;const l=(m=s.idRange)==null?void 0:m[1];return n!==null&&l!==null&&n>l?A(v({},s),{idRange:[n,n]}):A(v({},s),{idRange:[n,l!=null?l:null]})})},style:{width:150},min:(Be=o.minId)!=null?Be:0,max:(xe=(Me=(me=x.idRange)==null?void 0:me[1])!=null?Me:o.maxId)!=null?xe:void 0}),e.jsx("span",{children:"-"}),e.jsx(a.InputNumber,{placeholder:t("Max ID"),value:(Ie=x.idRange)==null?void 0:Ie[1],onChange:n=>{te(s=>{var m;const l=(m=s.idRange)==null?void 0:m[0];return n!==null&&l!==null&&n<l?A(v({},s),{idRange:[n,n]}):A(v({},s),{idRange:[l!=null?l:null,n]})})},style:{width:150},min:(Oe=(De=(Le=x.idRange)==null?void 0:Le[0])!=null?De:o.minId)!=null?Oe:0,max:(ze=o.maxId)!=null?ze:void 0})]})]}),!o.hasCreatedAt&&!o.hasUpdatedAt&&e.jsx(a.Alert,{message:t("No time fields, use ID range filter"),type:"info",showIcon:!0})]})}),e.jsxs(a.Space,{children:[e.jsx(a.Button,{onClick:ve,icon:e.jsx(I.ReloadOutlined,{}),disabled:d,children:t("Refresh")}),e.jsx(a.Button,{danger:!0,icon:e.jsx(I.DeleteOutlined,{}),loading:B,disabled:d,onClick:at,children:B?z?`(${z.current}/${z.total}) ${t("Cleaning...")}`:t("Cleaning..."):t("Clean")})]}),e.jsx(a.Modal,{title:e.jsxs(a.Space,{children:[e.jsx(I.WarningOutlined,{style:{color:"#faad14"}}),t("Confirm Clean")]}),open:Ge,onCancel:()=>!d&&Q(!1),closable:!d,maskClosable:!d,keyboard:!d,footer:null,width:500,children:e.jsxs(a.Space,{direction:"vertical",style:{width:"100%"},size:"middle",children:[e.jsx(a.Alert,{message:t("This action cannot be undone"),description:t("Are you sure you want to clean the filtered data?"),type:"warning",showIcon:!0}),e.jsx(a.Typography.Text,{type:"secondary",children:t("It is recommended to backup before cleaning. You can also clean directly without backup.")}),e.jsxs(a.Space,{style:{width:"100%",justifyContent:"flex-end"},children:[e.jsx(a.Button,{onClick:()=>Q(!1),disabled:d,children:t("Cancel")}),e.jsx(a.Button,{icon:e.jsx(I.SaveOutlined,{}),loading:h,disabled:d,onClick:nt,children:t("Backup then Clean")}),e.jsx(a.Button,{danger:!0,icon:e.jsx(I.DeleteOutlined,{}),disabled:d,onClick:st,children:t("Clean directly")})]})]})}),e.jsx(a.Modal,{title:t("Backup Complete"),open:He,onCancel:()=>!d&&Z(!1),closable:!d,maskClosable:!d,keyboard:!d,footer:null,width:450,children:e.jsxs(a.Space,{direction:"vertical",style:{width:"100%"},size:"middle",children:[e.jsx(a.Typography.Text,{children:t("Do you want to download the backup file before cleaning?")}),e.jsxs(a.Space,{style:{width:"100%",justifyContent:"flex-end"},children:[e.jsx(a.Button,{onClick:()=>Z(!1),disabled:d,children:t("Cancel")}),e.jsx(a.Button,{onClick:lt,disabled:d,children:t("Skip download")}),e.jsx(a.Button,{type:"primary",icon:e.jsx(I.DownloadOutlined,{}),loading:O,disabled:d&&!O,onClick:ot,children:t("Download and continue")})]})]})}),e.jsx(a.Modal,{title:t("Batch Settings"),open:Xe,onCancel:()=>!d&&G(!1),closable:!d,maskClosable:!d,keyboard:!d,footer:null,width:550,children:e.jsxs(a.Space,{direction:"vertical",style:{width:"100%"},size:"middle",children:[e.jsxs(a.Typography.Text,{children:[t("Total records to clean"),": ",e.jsx("strong",{children:E.count.toLocaleString()})]}),e.jsx(a.Radio.Group,{value:V,onChange:n=>we(n.target.value),style:{width:"100%"},children:e.jsxs(a.Space,{direction:"vertical",style:{width:"100%"},children:[e.jsx(a.Radio,{value:"none",children:t("No batching")}),e.jsx(a.Radio,{value:"count",children:e.jsxs(a.Space,{children:[t("Split into"),e.jsx(a.InputNumber,{min:2,max:1e3,value:se,onChange:n=>n&&Ze(n),changeOnWheel:!0,disabled:V!=="count",style:{width:80}}),t("batches")]})}),e.jsx(a.Radio,{value:"size",children:e.jsxs(a.Space,{children:[t("Each batch"),e.jsx(a.InputNumber,{min:1e3,max:1e6,step:1e3,value:re,onChange:n=>n&&Re(n),changeOnWheel:!0,disabled:V!=="size",style:{width:100}}),t("records")]})})]})}),e.jsx(a.Alert,{type:"info",showIcon:!0,message:V==="none"?E.count>5e4?t("Will clean {{count}} records at once. Large data volume may take a long time.",{count:E.count.toLocaleString()}):t("Will clean {{count}} records at once.",{count:E.count.toLocaleString()}):V==="count"?t("Will clean {{count}} records in {{batches}} batches, {{perBatch}} records per batch.",{count:E.count.toLocaleString(),batches:se,perBatch:Math.ceil(E.count/se).toLocaleString()}):t("Will clean {{count}} records in {{batches}} batches, {{perBatch}} records per batch.",{count:E.count.toLocaleString(),batches:Math.ceil(E.count/re),perBatch:re.toLocaleString()})}),e.jsxs(a.Space,{style:{width:"100%",justifyContent:"flex-end"},children:[e.jsx(a.Button,{onClick:()=>G(!1),disabled:d,children:t("Cancel")}),e.jsx(a.Button,{type:"primary",onClick:rt,disabled:d,children:t("Next")})]})]})}),e.jsx(a.Modal,{title:e.jsxs(a.Space,{children:[e.jsx(I.WarningOutlined,{style:{color:"#faad14"}}),t("Release disk space")]}),open:Je,onCancel:()=>!d&&le(!1),closable:!d,maskClosable:!d,keyboard:!d,footer:null,width:550,children:e.jsxs(a.Space,{direction:"vertical",style:{width:"100%"},size:"middle",children:[z&&e.jsxs("div",{children:[e.jsxs(a.Typography.Text,{children:[t("Cleaning progress"),": ",z.current," / ",z.total," ",t("batches")]}),e.jsx(a.Progress,{percent:Math.round(z.current/z.total*100),status:"active"}),e.jsxs(a.Typography.Text,{type:"secondary",children:[t("Deleted"),": ",z.deletedCount.toLocaleString()," ",t("records")]})]}),!z&&e.jsxs(e.Fragment,{children:[e.jsx(a.Typography.Text,{children:t("Do you want to release disk space after cleaning?")}),e.jsx(a.Alert,{message:t("Release space warning"),description:e.jsxs(e.Fragment,{children:[t("Releasing space will lock the table and may take a long time for large tables. Other operations on this table will be blocked during the process."),V!=="none"&&e.jsxs(e.Fragment,{children:[e.jsx("br",{}),e.jsx("br",{}),t("Space will be released only after the last batch is completed.")]})]}),type:"warning",showIcon:!0}),e.jsxs(a.Space,{style:{width:"100%",justifyContent:"flex-end"},children:[e.jsx(a.Button,{onClick:()=>le(!1),disabled:d,children:t("Cancel")}),e.jsx(a.Button,{onClick:()=>Ae(!1),loading:B,disabled:d&&!B,children:t("Clean only")}),e.jsx(a.Button,{danger:!0,onClick:()=>Ae(!0),loading:B||de,disabled:d&&!(B||de),children:t("Clean and release space")})]})]})]})}),e.jsx(a.Table,{dataSource:C,loading:k,columns:dt,rowKey:(n,s)=>n.id||s,pagination:A(v({},ee),{showSizeChanger:!0,showTotal:n=>t("Total: {{total}}",{total:n})}),onChange:ct,scroll:{x:"max-content"}})]})})}):e.jsx("div",{style:{textAlign:"center",padding:50},children:e.jsx(a.Spin,{size:"large"})})};function Ue(t){if(t===0)return"0 B";const y=1024,w=["B","KB","MB","GB","TB"],f=Math.floor(Math.log(t)/Math.log(y));return Math.round(t/Math.pow(y,f)*100)/100+" "+w[f]}function _e(t){if(t===0)return"0 B";const y=1024,w=["B","KB","MB","GB","TB"],f=Math.floor(Math.log(t)/Math.log(y));return Math.round(t/Math.pow(y,f)*100)/100+" "+w[f]}const We=()=>{const{t}=_(),y=u.useAPIClient(),w=j.useNavigate(),{message:f}=a.App.useApp(),[Y,b]=r.useState([]),[g,W]=r.useState(!1),[L,o]=r.useState({current:1,pageSize:20,total:0}),i=r.useMemo(()=>y.resource("databaseClean"),[y]),C=r.useCallback(()=>T(null,null,function*(){yield c()}),[]);r.useEffect(()=>{c()},[L.current,L.pageSize]);const c=()=>T(null,null,function*(){W(!0);try{const h=yield i.list({params:{page:L.current,pageSize:L.pageSize}}),{data:D,meta:O}=h.data;b(Array.isArray(D)?D:[]),o($=>A(v({},$),{total:(O==null?void 0:O.count)||0}))}catch(h){f.error(h.message||t("Failed to load table list"))}finally{W(!1)}}),k=h=>{o(D=>A(v({},D),{current:h.current,pageSize:h.pageSize}))},K=[{title:t("Table Name"),dataIndex:"name",key:"name",render:h=>e.jsx("a",{onClick:()=>{w(`/_admin/system-services/database-clean/${h}`)},children:h})},{title:t("Origin"),dataIndex:"origin",key:"origin"},{title:t("Size"),dataIndex:"size",key:"size",render:h=>_e(h)},{title:t("Row Count"),dataIndex:"rowCount",key:"rowCount",render:h=>h.toLocaleString()},{title:t("Created At"),dataIndex:"createdAt",key:"createdAt",render:h=>h?e.jsx(u.DatePicker.ReadPretty,{value:P(h),showTime:!0}):"-"},{title:t("Updated At"),dataIndex:"updatedAt",key:"updatedAt",render:h=>h?e.jsx(u.DatePicker.ReadPretty,{value:P(h),showTime:!0}):"-"}];return e.jsx("div",{children:e.jsxs(a.Card,{bordered:!1,children:[e.jsx(a.Space,{style:{float:"right",marginBottom:16},children:e.jsx(a.Button,{onClick:C,icon:e.jsx(I.ReloadOutlined,{}),children:t("Refresh")})}),e.jsx(a.Table,{dataSource:Y,loading:g,columns:K,rowKey:"name",pagination:A(v({},L),{showSizeChanger:!0,showTotal:h=>t("Total: {{total}}",{total:h})}),onChange:k})]})})};class Ke extends u.Plugin{afterAdd(){return T(this,null,function*(){})}beforeLoad(){return T(this,null,function*(){})}load(){return T(this,null,function*(){this.app.systemSettingsManager.add("system-services."+M,{title:this.t("Database Clean"),icon:"DatabaseOutlined",Component:We,aclSnippet:"pm.database-clean.*",sort:-50}),this.app.systemSettingsManager.add("system-services."+M+"/:tableName",{title:this.t("Table Detail"),Component:qe,groupKey:"system-services."+M,aclSnippet:"pm.database-clean.*"})})}}p.default=Ke,Object.defineProperties(p,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
@@ -0,0 +1,7 @@
1
+ export declare const NAMESPACE = "database-clean";
2
+ export declare function lang(key: string): string;
3
+ export declare function generateNTemplate(key: string): string;
4
+ export declare function useTranslation(): {
5
+ t: (key: string, props?: {}) => string;
6
+ };
7
+ export declare const tval: (key: string) => string;
@@ -0,0 +1 @@
1
+ export declare const TableDetail: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare const TableList: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { Plugin } from '@tachybase/client';
2
+ declare class PluginPluginDatabaseClean extends Plugin {
3
+ afterAdd(): Promise<void>;
4
+ beforeLoad(): Promise<void>;
5
+ load(): Promise<void>;
6
+ }
7
+ export default PluginPluginDatabaseClean;
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ "@tachybase/client": "1.6.1",
3
+ "@tego/server": "1.6.1",
4
+ "lodash": "4.17.21",
5
+ "react": "18.3.1",
6
+ "@ant-design/icons": "5.6.1",
7
+ "antd": "5.22.5",
8
+ "dayjs": "1.11.13",
9
+ "react-router-dom": "6.28.1",
10
+ "sequelize": "6.37.5"
11
+ };
@@ -0,0 +1,2 @@
1
+ export * from './server';
2
+ export { default } from './server';
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var index_exports = {};
30
+ __export(index_exports, {
31
+ default: () => import_server.default
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+ __reExport(index_exports, require("./server"), module.exports);
35
+ var import_server = __toESM(require("./server"));
36
+ // Annotate the CommonJS export names for ESM import in node:
37
+ 0 && (module.exports = {
38
+ ...require("./server")
39
+ });
@@ -0,0 +1,65 @@
1
+ {
2
+ "Are you sure you want to clean the filtered data?": "Are you sure you want to clean the filtered data?",
3
+ "Backup Complete": "Backup Complete",
4
+ "Backup Failed": "Backup Failed",
5
+ "Backup Success": "Backup Success",
6
+ "Backup then Clean": "Backup then Clean",
7
+ "Batch Settings": "Batch Settings",
8
+ "Cancel": "Cancel",
9
+ "Clean": "Clean",
10
+ "Clean Failed": "Clean Failed",
11
+ "Clean Success": "Clean Success",
12
+ "Clean and release space": "Clean and release space",
13
+ "Clean directly": "Clean directly",
14
+ "Clean only": "Clean only",
15
+ "Cleaning progress": "Cleaning progress",
16
+ "Cleaning...": "Cleaning...",
17
+ "Confirm Clean": "Confirm Clean",
18
+ "Created At": "Created At",
19
+ "Created At Range": "Created At Range",
20
+ "Database Clean": "Database Clean",
21
+ "Deleted": "Deleted",
22
+ "Do you want to download the backup file before cleaning?": "Do you want to download the backup file before cleaning?",
23
+ "Do you want to release disk space after cleaning?": "Do you want to release disk space after cleaning?",
24
+ "Download": "Download",
25
+ "Download and continue": "Download and continue",
26
+ "Each batch": "Each batch",
27
+ "Failed": "Failed",
28
+ "Failed to load table data": "Failed to load table data",
29
+ "Failed to load table info": "Failed to load table info",
30
+ "Failed to load table list": "Failed to load table list",
31
+ "Failed to release space": "Failed to release space",
32
+ "Filter": "Filter",
33
+ "ID Range": "ID Range",
34
+ "It is recommended to backup before cleaning. You can also clean directly without backup.": "It is recommended to backup before cleaning. You can also clean directly without backup.",
35
+ "Max ID": "Max ID",
36
+ "Min ID": "Min ID",
37
+ "Next": "Next",
38
+ "No batching": "No batching",
39
+ "No time fields, use ID range filter": "This table has no time fields, you can use ID range filter",
40
+ "Origin": "Origin",
41
+ "Refresh": "Refresh",
42
+ "Release disk space": "Release disk space",
43
+ "Release space warning": "Release Space Warning",
44
+ "Releasing space will lock the table and may take a long time for large tables. Other operations on this table will be blocked during the process.": "Releasing space will lock the table and may take a long time for large tables. Other operations on this table will be blocked during the process.",
45
+ "Row Count": "Row Count",
46
+ "Size": "Size",
47
+ "Skip download": "Skip download",
48
+ "Space released successfully": "Space released successfully",
49
+ "Space will be released only after the last batch is completed.": "Space will be released only after the last batch is completed.",
50
+ "Split into": "Split into",
51
+ "Success": "Success",
52
+ "Table Detail": "Table Detail",
53
+ "Table Name": "Table Name",
54
+ "This action cannot be undone": "This action cannot be undone",
55
+ "Total records to clean": "Total records to clean",
56
+ "Total: {{total}}": "Total: {{total}}",
57
+ "Updated At": "Updated At",
58
+ "Updated At Range": "Updated At Range",
59
+ "Will clean {{count}} records at once.": "Will clean {{count}} records at once.",
60
+ "Will clean {{count}} records at once. Large data volume may take a long time.": "Will clean {{count}} records at once. Large data volume may take a long time.",
61
+ "Will clean {{count}} records in {{batches}} batches, {{perBatch}} records per batch.": "Will clean {{count}} records in {{batches}} batches, {{perBatch}} records per batch.",
62
+ "batches": "batches",
63
+ "records": "records",
64
+ "records deleted": "records deleted"
65
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "Are you sure you want to clean the filtered data?": "确定要清理筛选后的数据吗?",
3
+ "Backup Complete": "备份完成",
4
+ "Backup Failed": "备份失败",
5
+ "Backup Success": "备份成功",
6
+ "Backup then Clean": "备份后清理",
7
+ "Batch Settings": "分批设置",
8
+ "Cancel": "取消",
9
+ "Clean": "清理",
10
+ "Clean Failed": "清理失败",
11
+ "Clean Success": "清理成功",
12
+ "Clean and release space": "清理并释放空间",
13
+ "Clean directly": "直接清理",
14
+ "Clean only": "仅清理",
15
+ "Cleaning progress": "清理进度",
16
+ "Cleaning...": "清理中...",
17
+ "Confirm Clean": "确认清理",
18
+ "Created At": "创建时间",
19
+ "Created At Range": "创建时间范围",
20
+ "Database Clean": "数据库清理",
21
+ "Deleted": "已删除",
22
+ "Do you want to download the backup file before cleaning?": "是否在清理前下载备份文件?",
23
+ "Do you want to release disk space after cleaning?": "是否在清理后释放磁盘空间?",
24
+ "Download": "下载",
25
+ "Download and continue": "下载并继续",
26
+ "Each batch": "每批",
27
+ "Failed": "失败",
28
+ "Failed to load table data": "加载表数据失败",
29
+ "Failed to load table info": "加载表信息失败",
30
+ "Failed to load table list": "加载表列表失败",
31
+ "Failed to release space": "释放空间失败",
32
+ "Filter": "筛选",
33
+ "ID Range": "ID 范围",
34
+ "It is recommended to backup before cleaning. You can also clean directly without backup.": "建议在清理前先备份数据。您也可以选择不备份直接清理。",
35
+ "Max ID": "最大 ID",
36
+ "Min ID": "最小 ID",
37
+ "Next": "下一步",
38
+ "No batching": "不分批",
39
+ "No time fields, use ID range filter": "该表没有时间字段,可以使用 ID 范围筛选",
40
+ "Origin": "来源",
41
+ "Refresh": "刷新",
42
+ "Release disk space": "释放磁盘空间",
43
+ "Release space warning": "释放空间警告",
44
+ "Releasing space will lock the table and may take a long time for large tables. Other operations on this table will be blocked during the process.": "释放空间会锁定数据表,对于大表可能需要较长时间。在此过程中,该表的其他操作将被阻塞。",
45
+ "Row Count": "数据条数",
46
+ "Size": "占用大小",
47
+ "Skip download": "跳过下载",
48
+ "Space released successfully": "空间释放成功",
49
+ "Space will be released only after the last batch is completed.": "空间将仅在最后一批清理完成后释放。",
50
+ "Split into": "分为",
51
+ "Success": "成功",
52
+ "Table Detail": "表详情",
53
+ "Table Name": "表名",
54
+ "This action cannot be undone": "此操作不可恢复",
55
+ "Total records to clean": "待清理数据总数",
56
+ "Total: {{total}}": "共 {{total}} 条",
57
+ "Updated At": "最近更新时间",
58
+ "Updated At Range": "更新时间范围",
59
+ "Will clean {{count}} records at once.": "将一次性清理 {{count}} 条数据。",
60
+ "Will clean {{count}} records at once. Large data volume may take a long time.": "将一次性清理 {{count}} 条数据,数据量较大可能需要较长时间。",
61
+ "Will clean {{count}} records in {{batches}} batches, {{perBatch}} records per batch.": "将清理 {{count}} 条数据,分 {{batches}} 批进行,每批 {{perBatch}} 条。",
62
+ "batches": "批",
63
+ "records": "条",
64
+ "records deleted": "条记录已删除"
65
+ }
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012-2014 Chris Talkington, contributors.
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.