InPanel 1.2.1__py3-none-any.whl

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 (198) hide show
  1. inpanel/__init__.py +29 -0
  2. inpanel/acme.py +289 -0
  3. inpanel/aliyuncs.py +269 -0
  4. inpanel/app.py +514 -0
  5. inpanel/apt.py +223 -0
  6. inpanel/base.py +200 -0
  7. inpanel/config.py +139 -0
  8. inpanel/data/config.ini +29 -0
  9. inpanel/data/history +5 -0
  10. inpanel/data/runlogs.ini +12 -0
  11. inpanel/data/update_info.ini +4 -0
  12. inpanel/dnf.py +225 -0
  13. inpanel/lib/__init__.py +9 -0
  14. inpanel/lib/async_process.py +98 -0
  15. inpanel/lib/filelock.py +82 -0
  16. inpanel/lib/magic.py +233 -0
  17. inpanel/lib/pxssh.py +310 -0
  18. inpanel/lib/pyDes.py +867 -0
  19. inpanel/mod/__init__.py +0 -0
  20. inpanel/mod/apt.py +316 -0
  21. inpanel/mod/certificate.py +268 -0
  22. inpanel/mod/config.py +372 -0
  23. inpanel/mod/cron.py +315 -0
  24. inpanel/mod/disk.py +322 -0
  25. inpanel/mod/dnf.py +239 -0
  26. inpanel/mod/file.py +722 -0
  27. inpanel/mod/firewall.py +199 -0
  28. inpanel/mod/firewall_base.py +137 -0
  29. inpanel/mod/firewall_firewalld.py +170 -0
  30. inpanel/mod/firewall_iptables.py +176 -0
  31. inpanel/mod/firewall_macos.py +213 -0
  32. inpanel/mod/firewall_ufw.py +128 -0
  33. inpanel/mod/ftp.py +231 -0
  34. inpanel/mod/httpd.py +1205 -0
  35. inpanel/mod/lighttpd.py +26 -0
  36. inpanel/mod/login.py +98 -0
  37. inpanel/mod/mysql.py +577 -0
  38. inpanel/mod/named.py +26 -0
  39. inpanel/mod/nginx.py +2327 -0
  40. inpanel/mod/package.py +33 -0
  41. inpanel/mod/package_apt.py +62 -0
  42. inpanel/mod/package_base.py +77 -0
  43. inpanel/mod/package_dnf.py +62 -0
  44. inpanel/mod/package_map.py +366 -0
  45. inpanel/mod/package_yum.py +64 -0
  46. inpanel/mod/php.py +289 -0
  47. inpanel/mod/process.py +291 -0
  48. inpanel/mod/proftpd.py +26 -0
  49. inpanel/mod/pureftpd.py +26 -0
  50. inpanel/mod/query.py +108 -0
  51. inpanel/mod/server.py +1319 -0
  52. inpanel/mod/service.py +262 -0
  53. inpanel/mod/setting.py +228 -0
  54. inpanel/mod/shell.py +78 -0
  55. inpanel/mod/ssh.py +280 -0
  56. inpanel/mod/system.py +220 -0
  57. inpanel/mod/task.py +1337 -0
  58. inpanel/mod/ufw.py +14 -0
  59. inpanel/mod/user.py +306 -0
  60. inpanel/mod/vsftpd.py +67 -0
  61. inpanel/mod/yum.py +243 -0
  62. inpanel/plugins/__init__.py +10 -0
  63. inpanel/plugins/__pycache__/__init__.cpython-313.pyc +0 -0
  64. inpanel/plugins/acme/static/index.html +10 -0
  65. inpanel/plugins/acme/static/index.js +13 -0
  66. inpanel/plugins/shadowsocks-libev/__pycache__/info.cpython-313.pyc +0 -0
  67. inpanel/plugins/shadowsocks-libev/info.py +73 -0
  68. inpanel/plugins/shadowsocks-libev/static/index.html +70 -0
  69. inpanel/plugins/shadowsocks-libev/static/index.js +18 -0
  70. inpanel/public/css/global.css +138 -0
  71. inpanel/public/favicon.ico +0 -0
  72. inpanel/public/images/alipay.jpg +0 -0
  73. inpanel/public/images/loading.gif +0 -0
  74. inpanel/public/images/weixin.jpg +0 -0
  75. inpanel/public/index.html +94 -0
  76. inpanel/public/js/controllers/controllers.js +566 -0
  77. inpanel/public/js/controllers/database.js +590 -0
  78. inpanel/public/js/controllers/ecs.js +624 -0
  79. inpanel/public/js/controllers/file.js +940 -0
  80. inpanel/public/js/controllers/plugins.js +105 -0
  81. inpanel/public/js/controllers/service.js +1460 -0
  82. inpanel/public/js/controllers/site.js +1241 -0
  83. inpanel/public/js/controllers/utils.js +2319 -0
  84. inpanel/public/js/core.js +221 -0
  85. inpanel/public/js/directives.js +784 -0
  86. inpanel/public/js/filters.js +395 -0
  87. inpanel/public/js/services.js +257 -0
  88. inpanel/public/lib/angular/1.0.2/angular-resource.min.js +10 -0
  89. inpanel/public/lib/angular/1.0.2/angular.min.js +158 -0
  90. inpanel/public/lib/bootstrap/css/bootstrap-theme.css +587 -0
  91. inpanel/public/lib/bootstrap/css/bootstrap-theme.css.map +1 -0
  92. inpanel/public/lib/bootstrap/css/bootstrap-theme.min.css +6 -0
  93. inpanel/public/lib/bootstrap/css/bootstrap-theme.min.css.map +1 -0
  94. inpanel/public/lib/bootstrap/css/bootstrap.css +6757 -0
  95. inpanel/public/lib/bootstrap/css/bootstrap.css.map +1 -0
  96. inpanel/public/lib/bootstrap/css/bootstrap.min.css +6 -0
  97. inpanel/public/lib/bootstrap/css/bootstrap.min.css.map +1 -0
  98. inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  99. inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  100. inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  101. inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  102. inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  103. inpanel/public/lib/bootstrap/img/glyphicons-halflings-white.png +0 -0
  104. inpanel/public/lib/bootstrap/img/glyphicons-halflings.png +0 -0
  105. inpanel/public/lib/bootstrap/js/bootstrap.js +2377 -0
  106. inpanel/public/lib/bootstrap/js/bootstrap.min.js +7 -0
  107. inpanel/public/lib/bootstrap/js/npm.js +13 -0
  108. inpanel/public/lib/codemirror/codemirror-compressed.js +1 -0
  109. inpanel/public/lib/codemirror/codemirror.css +173 -0
  110. inpanel/public/lib/jquery/jquery-1.12.4.min.js +5 -0
  111. inpanel/public/lib/md5/md5.js +379 -0
  112. inpanel/public/lib/md5/md5.min.js +1 -0
  113. inpanel/public/partials/database/index.html +172 -0
  114. inpanel/public/partials/database/mysql/dbedit.html +462 -0
  115. inpanel/public/partials/database/mysql/dbnew.html +348 -0
  116. inpanel/public/partials/database/mysql/useredit.html +310 -0
  117. inpanel/public/partials/database/mysql/usernew.html +97 -0
  118. inpanel/public/partials/directives/message.html +20 -0
  119. inpanel/public/partials/directives/navbar.html +25 -0
  120. inpanel/public/partials/directives/selector.html +57 -0
  121. inpanel/public/partials/directives/srvbase.html +72 -0
  122. inpanel/public/partials/directives/srvext.html +34 -0
  123. inpanel/public/partials/directives/srvfile.html +23 -0
  124. inpanel/public/partials/directives/srvinstall.html +34 -0
  125. inpanel/public/partials/directives/srvlog.html +23 -0
  126. inpanel/public/partials/directives/srvminiop.html +28 -0
  127. inpanel/public/partials/directives/srvuninstall.html +38 -0
  128. inpanel/public/partials/directives/srvupdate.html +30 -0
  129. inpanel/public/partials/ecs/account.html +156 -0
  130. inpanel/public/partials/ecs/ecs.html +198 -0
  131. inpanel/public/partials/ecs/index.html +40 -0
  132. inpanel/public/partials/ecs/setings.html +459 -0
  133. inpanel/public/partials/file/file.html +501 -0
  134. inpanel/public/partials/file/trash.html +95 -0
  135. inpanel/public/partials/ftp.html +36 -0
  136. inpanel/public/partials/log.html +83 -0
  137. inpanel/public/partials/login.html +79 -0
  138. inpanel/public/partials/logout.html +22 -0
  139. inpanel/public/partials/main.html +534 -0
  140. inpanel/public/partials/plugins/index.html +55 -0
  141. inpanel/public/partials/plugins/plugins.html +13 -0
  142. inpanel/public/partials/secure.html +13 -0
  143. inpanel/public/partials/service/apache.html +147 -0
  144. inpanel/public/partials/service/cron.html +71 -0
  145. inpanel/public/partials/service/index.html +293 -0
  146. inpanel/public/partials/service/iptables.html +40 -0
  147. inpanel/public/partials/service/lighttpd.html +63 -0
  148. inpanel/public/partials/service/mariadb.html +113 -0
  149. inpanel/public/partials/service/memcache.html +36 -0
  150. inpanel/public/partials/service/minio.html +44 -0
  151. inpanel/public/partials/service/mongodb.html +44 -0
  152. inpanel/public/partials/service/mysql.html +113 -0
  153. inpanel/public/partials/service/named.html +61 -0
  154. inpanel/public/partials/service/nginx.html +296 -0
  155. inpanel/public/partials/service/ntp.html +40 -0
  156. inpanel/public/partials/service/php.html +250 -0
  157. inpanel/public/partials/service/proftpd.html +59 -0
  158. inpanel/public/partials/service/pureftpd.html +59 -0
  159. inpanel/public/partials/service/redis.html +40 -0
  160. inpanel/public/partials/service/samba.html +58 -0
  161. inpanel/public/partials/service/sendmail.html +40 -0
  162. inpanel/public/partials/service/ssh.html +186 -0
  163. inpanel/public/partials/service/tomcat.html +65 -0
  164. inpanel/public/partials/service/vsftpd.html +195 -0
  165. inpanel/public/partials/setting.html +299 -0
  166. inpanel/public/partials/site/apache/site.html +277 -0
  167. inpanel/public/partials/site/index.html +344 -0
  168. inpanel/public/partials/site/nginx/site.html +558 -0
  169. inpanel/public/partials/sorry.html +19 -0
  170. inpanel/public/partials/storage/autofm.html +103 -0
  171. inpanel/public/partials/storage/backup.html +42 -0
  172. inpanel/public/partials/storage/index.html +360 -0
  173. inpanel/public/partials/storage/migrate.html +93 -0
  174. inpanel/public/partials/storage/movedata.html +98 -0
  175. inpanel/public/partials/storage/remote.html +77 -0
  176. inpanel/public/partials/utils/cron.html +486 -0
  177. inpanel/public/partials/utils/firewall.html +266 -0
  178. inpanel/public/partials/utils/index.html +193 -0
  179. inpanel/public/partials/utils/network.html +118 -0
  180. inpanel/public/partials/utils/process.html +239 -0
  181. inpanel/public/partials/utils/repository.html +373 -0
  182. inpanel/public/partials/utils/shell.html +28 -0
  183. inpanel/public/partials/utils/ssl.html +319 -0
  184. inpanel/public/partials/utils/time.html +109 -0
  185. inpanel/public/partials/utils/user.html +359 -0
  186. inpanel/public/robots.txt +2 -0
  187. inpanel/remote.py +109 -0
  188. inpanel/templates/file/preview.html +34 -0
  189. inpanel/templates/index.html +93 -0
  190. inpanel/utils.py +282 -0
  191. inpanel/web.py +1811 -0
  192. inpanel/yum.py +756 -0
  193. inpanel-1.2.1.dist-info/METADATA +399 -0
  194. inpanel-1.2.1.dist-info/RECORD +198 -0
  195. inpanel-1.2.1.dist-info/WHEEL +5 -0
  196. inpanel-1.2.1.dist-info/entry_points.txt +2 -0
  197. inpanel-1.2.1.dist-info/licenses/LICENSE +30 -0
  198. inpanel-1.2.1.dist-info/top_level.txt +1 -0
inpanel/mod/file.py ADDED
@@ -0,0 +1,722 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (c) 2017-2026 Jackson Dou
5
+ # All rights reserved.
6
+ #
7
+ # InPanel is distributed under the terms of the (new) BSD License.
8
+ # The full license can be found in 'LICENSE'.
9
+ '''Module for file Management'''
10
+
11
+ import os
12
+ import shelve
13
+ import stat
14
+ from grp import getgrgid, getgrnam
15
+ from mimetypes import guess_type
16
+ from pathlib import Path
17
+ from pwd import getpwnam, getpwuid
18
+ from time import time
19
+ from uuid import uuid4
20
+ from ..base import kernel_name
21
+
22
+ try:
23
+ import imghdr
24
+ except ImportError:
25
+ class imghdr:
26
+ @staticmethod
27
+ def what(filepath):
28
+ try:
29
+ from PIL import Image
30
+ with Image.open(filepath) as img:
31
+ return img.format.lower()
32
+ except ImportError:
33
+ pass
34
+ return None
35
+
36
+ from . import server
37
+ from ..utils import b2h, ftime
38
+
39
+ charsets = ('utf-8', 'gb2312', 'gbk', 'gb18030', 'big5', 'euc-jp', 'euc-kr',
40
+ 'iso-8859-2', 'shift_jis')
41
+
42
+
43
+ def web_handler(context):
44
+ '''handler for web server'''
45
+ action = context.get_argument('action', '')
46
+
47
+ if action == 'last':
48
+ lastdir = context.runlogs.get('file', 'lastdir')
49
+ lastfile = context.runlogs.get('file', 'lastfile')
50
+ context.write({'code': 0, 'msg': '', 'data': {'lastdir': lastdir, 'lastfile': lastfile}})
51
+
52
+ elif action == 'history':
53
+ from ..base import history_path
54
+ from pathlib import Path
55
+ paths = []
56
+ if Path(history_path).exists():
57
+ with open(history_path, 'r', encoding='utf-8') as f:
58
+ paths = [line.strip() for line in f.readlines() if line.strip()]
59
+ context.write({'code': 0, 'msg': '', 'data': paths})
60
+
61
+ elif action == 'add_history':
62
+ path = context.get_argument('path', '')
63
+ if not path:
64
+ context.write({'code': -1, 'msg': '路径不能为空!'})
65
+ return
66
+
67
+ from ..base import history_path
68
+ from pathlib import Path
69
+
70
+ paths = []
71
+ if Path(history_path).exists():
72
+ with open(history_path, 'r', encoding='utf-8') as f:
73
+ paths = [line.strip() for line in f.readlines() if line.strip()]
74
+
75
+ if path in paths:
76
+ paths.remove(path)
77
+ paths.insert(0, path)
78
+
79
+ if len(paths) > 30:
80
+ paths = paths[:30]
81
+
82
+ Path(history_path).parent.mkdir(parents=True, exist_ok=True)
83
+ with open(history_path, 'w', encoding='utf-8') as f:
84
+ f.write('\n'.join(paths))
85
+
86
+ context.write({'code': 0, 'msg': '', 'data': paths})
87
+
88
+ elif action == 'listdir':
89
+ path = context.get_argument('path', '')
90
+ showhidden = context.get_argument('showhidden', 'off')
91
+ remember = context.get_argument('remember', 'on')
92
+ onlydir = context.get_argument('onlydir', 'off')
93
+ items = listdir(path, showhidden=='on', onlydir=='on')
94
+ if items is False:
95
+ context.write({'code': -1, 'msg': f'目录 {path} 不存在!'})
96
+ else:
97
+ if remember == 'on':
98
+ context.runlogs.set('file', 'lastdir', path)
99
+ context.write({'code': 0, 'msg': '成功获取文件列表!', 'data': items})
100
+
101
+ elif action == 'getitem':
102
+ path = context.get_argument('path', '')
103
+ item = getitem(path)
104
+ if item is False:
105
+ context.write({'code': -1, 'msg': f'{path} 不存在!'})
106
+ else:
107
+ context.write({'code': 0, 'msg': f'成功获取 {path} 的信息!', 'data': item})
108
+
109
+ elif action == 'fread':
110
+ path = context.get_argument('path', '')
111
+ remember = context.get_argument('remember', 'on')
112
+ size = fsize(path)
113
+ if size is None:
114
+ context.write({'code': -1, 'msg': f'文件 {path} 不存在!'})
115
+ elif size > 1024*1024*2: # support 1MB of file at max
116
+ context.write({'code': -1, 'msg': f'读取 {path} 失败!不允许在线编辑超过2MB的文件!'})
117
+ # elif not mod_file.istext(path):
118
+ # context.write({'code': -1, 'msg': f'读取 {path} 失败!无法识别文件类型 !'})
119
+ else:
120
+ if remember == 'on':
121
+ context.runlogs.set('file', 'lastfile', path)
122
+ charset, content = decode(path)
123
+ if not charset:
124
+ context.write({'code': -1, 'msg': '不可识别的文件编码 !'})
125
+ return
126
+ data = {
127
+ 'filename': str(Path(path).name),
128
+ 'filepath': path,
129
+ 'mimetype': mimetype(path),
130
+ 'charset': charset,
131
+ 'content': content,
132
+ }
133
+ context.write({'code': 0, 'msg': '成功读取文件内容 !', 'data': data})
134
+
135
+ elif action == 'fclose':
136
+ context.runlogs.set('file', 'lastfile', '')
137
+ context.write({'code': 0, 'msg': ''})
138
+
139
+ elif action == 'fwrite':
140
+ path = context.get_argument('path', '')
141
+ charset = context.get_argument('charset', '')
142
+ content = context.get_argument('content', '')
143
+
144
+ if context.config.get('runtime', 'mode') == 'demo':
145
+ if not path.startswith('/var/www'):
146
+ context.write({'code': -1, 'msg': '演示模式不允许修改除 /var/www 以外的目录!'})
147
+ return
148
+
149
+ if not charset in charsets:
150
+ context.write({'code': -1, 'msg': '不可识别的文件编码!'})
151
+ return
152
+ content = encode(content, charset)
153
+ if not content:
154
+ context.write({'code': -1, 'msg': '文件编码转换出错,保存失败!'})
155
+ return
156
+ if fsave(path, content):
157
+ context.write({'code': 0, 'msg': '文件保存成功!'})
158
+ else:
159
+ context.write({'code': -1, 'msg': '文件保存失败!'})
160
+
161
+ elif action == 'createfolder':
162
+ path = context.get_argument('path', '')
163
+ name = context.get_argument('name', '')
164
+
165
+ if context.config.get('runtime', 'mode') == 'demo':
166
+ if not path.startswith('/var/www') and not path.startswith(context.settings['package_path']):
167
+ context.write({'code': -1, 'msg': '演示模式不允许修改除 /var/www 以外的目录!'})
168
+ return
169
+
170
+ if dadd(path, name):
171
+ context.write({'code': 0, 'msg': '文件夹创建成功!'})
172
+ else:
173
+ context.write({'code': -1, 'msg': '文件夹创建失败!'})
174
+
175
+ elif action == 'createfile':
176
+ path = context.get_argument('path', '')
177
+ name = context.get_argument('name', '')
178
+
179
+ if context.config.get('runtime', 'mode') == 'demo':
180
+ if not path.startswith('/var/www'):
181
+ context.write({'code': -1, 'msg': '演示模式不允许修改除 /var/www 以外的目录!'})
182
+ return
183
+
184
+ if fadd(path, name):
185
+ context.write({'code': 0, 'msg': '文件创建成功!'})
186
+ else:
187
+ context.write({'code': -1, 'msg': '文件创建失败!'})
188
+
189
+ elif action == 'rename':
190
+ path = context.get_argument('path', '')
191
+ name = context.get_argument('name', '')
192
+
193
+ if context.config.get('runtime', 'mode') == 'demo':
194
+ if not path.startswith('/var/www'):
195
+ context.write({'code': -1, 'msg': '演示模式不允许修改除 /var/www 以外的目录!'})
196
+ return
197
+
198
+ if rename(path, name):
199
+ context.write({'code': 0, 'msg': '重命名成功!'})
200
+ else:
201
+ context.write({'code': -1, 'msg': '重命名失败!'})
202
+
203
+ elif action == 'exist':
204
+ path = context.get_argument('path', '')
205
+ name = context.get_argument('name', '')
206
+ context.write({'code': 0, 'msg': '', 'data': str(Path(path) / name)})
207
+
208
+ elif action == 'link':
209
+ srcpath = context.get_argument('srcpath', '')
210
+ despath = context.get_argument('despath', '')
211
+
212
+ if context.config.get('runtime', 'mode') == 'demo':
213
+ if not despath.startswith('/var/www') and not despath.startswith(context.settings['package_path']):
214
+ context.write({'code': -1, 'msg': '演示模式不允许在除 /var/www 以外的目录下创建链接!'})
215
+ return
216
+
217
+ if link(srcpath, despath):
218
+ context.write({'code': 0, 'msg': f'链接 {despath} 创建成功 !'})
219
+ else:
220
+ context.write({'code': -1, 'msg': f'链接 {despath} 创建失败 !'})
221
+
222
+ elif action == 'delete':
223
+ paths = context.get_argument('paths', '')
224
+ paths = paths.split(',')
225
+
226
+ if context.config.get('runtime', 'mode') == 'demo':
227
+ for path in paths:
228
+ if not path.startswith('/var/www') and not path.startswith(context.settings['package_path']):
229
+ context.write({'code': -1, 'msg': '演示模式不允许在除 /var/www 以外的目录执行删除操作!'})
230
+ return
231
+
232
+ if len(paths) == 1:
233
+ path = paths[0]
234
+ if delete(path):
235
+ context.write({'code': 0, 'msg': f'已将 {path} 移入回收站 !'})
236
+ else:
237
+ context.write({'code': -1, 'msg': f'将 {path} 移入回收站失败 !'})
238
+ else:
239
+ for path in paths:
240
+ if not delete(path):
241
+ context.write({'code': -1, 'msg': f'将 {path} 移入回收站失败 !'})
242
+ return
243
+ context.write({'code': 0, 'msg': '批量移入回收站成功!'})
244
+
245
+ elif action == 'tlist':
246
+ context.write({'code': 0, 'msg': '', 'data': tlist()})
247
+
248
+ elif action == 'trashs':
249
+ context.write({'code': 0, 'msg': '', 'data': trashs()})
250
+
251
+ elif action == 'titem':
252
+ mount = context.get_argument('mount', '')
253
+ uuid = context.get_argument('uuid', '')
254
+ info = titem(mount, uuid)
255
+ if info:
256
+ context.write({'code': 0, 'msg': '', 'data': info})
257
+ else:
258
+ context.write({'code': -1, 'msg': '获取项目信息失败!'})
259
+
260
+ elif action == 'trestore':
261
+ mount = context.get_argument('mount', '')
262
+ uuid = context.get_argument('uuid', '')
263
+ info = titem(mount, uuid)
264
+ if info and trestore(mount, uuid):
265
+ context.write({'code': 0, 'msg': f'已还原 {info["name"]} 到 {info["path"]} !'})
266
+ else:
267
+ context.write({'code': -1, 'msg': '还原失败!'})
268
+
269
+ elif action == 'tdelete':
270
+ mount = context.get_argument('mount', '')
271
+ uuid = context.get_argument('uuid', '')
272
+ info = titem(mount, uuid)
273
+ if info and tdelete(mount, uuid):
274
+ context.write({'code': 0, 'msg': f'已删除 {info["name"]} !'})
275
+ else:
276
+ context.write({'code': -1, 'msg': '删除失败!'})
277
+
278
+
279
+ def listdir(path, showdotfiles=False, onlydir=None):
280
+ '''list folders (and files)'''
281
+ path = str(Path(path))
282
+ if not Path(path).exists() or not Path(path).is_dir():
283
+ return False
284
+ items = sorted(os.listdir(path))
285
+ if not showdotfiles:
286
+ items = [item for item in items if not item.startswith('.')]
287
+ for i, item in enumerate(items):
288
+ items[i] = getitem(str(Path(path) / item))
289
+ # let folders list before files
290
+ rt = []
291
+ for i in range(len(items) - 1, -1, -1):
292
+ if items[i]['isdir'] or items[i]['islnk'] and not items[i][
293
+ 'link_broken'] and items[i]['link_isdir']:
294
+ rt.insert(0, items.pop(i))
295
+ # check if only list directories
296
+ if not onlydir:
297
+ rt.extend(items)
298
+ return rt
299
+
300
+
301
+ def listfile(directory):
302
+ '''only list files of directory'''
303
+ d = str(Path(directory))
304
+ if not Path(d).exists() or not Path(d).is_dir():
305
+ return None
306
+ items = sorted(os.listdir(d))
307
+ return items if len(items) > 0 else []
308
+
309
+
310
+ def getitem(path):
311
+ '''get file stat'''
312
+ if not Path(path).exists() and not Path(path).is_symlink():
313
+ return False
314
+ name = Path(path).name
315
+ basepath = str(Path(path).parent)
316
+ l_stat = os.lstat(path)
317
+ mode = l_stat.st_mode
318
+ try:
319
+ uname = getpwuid(l_stat.st_uid).pw_name
320
+ except:
321
+ uname = ''
322
+ try:
323
+ gname = getgrgid(l_stat.st_gid).gr_name
324
+ except:
325
+ gname = ''
326
+ item = {
327
+ 'name': name,
328
+ 'isdir': stat.S_ISDIR(mode),
329
+ 'ischr': stat.S_ISCHR(mode),
330
+ 'isblk': stat.S_ISBLK(mode),
331
+ 'isreg': stat.S_ISREG(mode),
332
+ 'isfifo': stat.S_ISFIFO(mode),
333
+ 'islnk': stat.S_ISLNK(mode),
334
+ 'issock': stat.S_ISSOCK(mode),
335
+ 'perms': oct(l_stat.st_mode)[-3:], # '0100777' 最后三位
336
+ 'mode': mode,
337
+ 'filemode': stat.filemode(mode),
338
+ 'uid': l_stat.st_uid,
339
+ 'gid': l_stat.st_gid,
340
+ 'uname': uname,
341
+ 'gname': gname,
342
+ 'inode': l_stat.st_ino,
343
+ 'dev': l_stat.st_dev,
344
+ 'size': b2h(l_stat.st_size),
345
+ 'atime': ftime(l_stat.st_atime),
346
+ 'mtime': ftime(l_stat.st_mtime),
347
+ 'ctime': ftime(l_stat.st_ctime),
348
+ }
349
+ if not item['isdir']:
350
+ item['is_image'] = is_image(path)
351
+ if item['islnk']:
352
+ linkfile = os.readlink(path)
353
+ item['linkto'] = linkfile
354
+ if not linkfile.startswith('/'):
355
+ linkfile = str(Path(basepath) / linkfile)
356
+ try:
357
+ mode = os.stat(linkfile).st_mode
358
+ item['link_isdir'] = stat.S_ISDIR(mode)
359
+ item['link_isreg'] = stat.S_ISREG(mode)
360
+ item['link_broken'] = False
361
+ except:
362
+ item['link_broken'] = True
363
+ return item
364
+
365
+
366
+ def rename(oldpath, newname):
367
+ # path = str(Path(oldpath)
368
+ if not Path(oldpath).exists():
369
+ return False
370
+ try:
371
+ basepath = str(Path(oldpath).parent)
372
+ newpath = str(Path(basepath) / newname)
373
+ os.rename(oldpath, newpath)
374
+ return True
375
+ except:
376
+ return False
377
+
378
+
379
+ def link(srcpath, despath):
380
+ try:
381
+ os.symlink(srcpath, despath)
382
+ return True
383
+ except:
384
+ return False
385
+
386
+
387
+ def dadd(path, name):
388
+ path = str(Path(path))
389
+ if not Path(path).exists() or not Path(path).is_dir():
390
+ return False
391
+ dpath = str(Path(path) / name)
392
+ if Path(dpath).exists():
393
+ return False
394
+ try:
395
+ Path(dpath).mkdir(parents=True, exist_ok=True)
396
+ return True
397
+ except:
398
+ return False
399
+
400
+
401
+ def istext(filepath):
402
+ mime = guess_type(filepath)[0]
403
+ print('mime', mime)
404
+ if mime is not None:
405
+ return mime.startswith('text/') or mime.endswith(
406
+ '/xml') or mime.endswith('json') or mime in (
407
+ 'application/javascript', 'application/vnd.apple.mpegurl',
408
+ 'application/x-x509-ca-cert', '.conf')
409
+ if mime is None:
410
+ suffix = Path(filepath).suffix
411
+ print('suffix', suffix)
412
+ return suffix in ('.txt', '.ini', '.js', '.mjs', '.json', '.m3u',
413
+ '.m3u8', '.tcl', '.eml', '.mht', '.mhtml', '.key')
414
+ return False
415
+
416
+ def is_image(filepath):
417
+ if not Path(filepath).exists():
418
+ return False
419
+ if Path(filepath).is_dir():
420
+ return False
421
+ suffix = imghdr.what(filepath)
422
+ return suffix in ('rgb', 'gif', 'jpg', 'jpeg', 'png', 'bmp', 'webp')
423
+
424
+
425
+ def mimetype(filepath):
426
+ if not Path(filepath).exists():
427
+ return False
428
+ if Path(filepath).is_symlink():
429
+ linkfile = os.readlink(filepath)
430
+ if linkfile.startswith('/'):
431
+ filepath = linkfile
432
+ else:
433
+ basepath = str(Path(filepath).parent)
434
+ filepath = str(Path(basepath) / linkfile)
435
+ if not Path(filepath).exists():
436
+ return False
437
+ # mime = magic.from_file(filepath, mime=True)
438
+ # # sometimes it still return like "text/plain; charset=us-ascii"
439
+ # if ';' in mime:
440
+ # mime = mime.split(';', 1)[0]
441
+ # if mime == 'text/plain':
442
+ tmime = guess_type(filepath)[0]
443
+ if tmime:
444
+ return tmime
445
+ # return mime
446
+
447
+
448
+ def fsize(filepath):
449
+ if not Path(filepath).is_file():
450
+ return None
451
+ return os.lstat(filepath).st_size
452
+
453
+
454
+ def fadd(path, name):
455
+ path = str(Path(path))
456
+ if not Path(path).exists() or not Path(path).is_dir():
457
+ return False
458
+ fpath = str(Path(path) / name)
459
+ if Path(fpath).exists():
460
+ return False
461
+ try:
462
+ with open(fpath, 'w', encoding='utf-8'):
463
+ pass
464
+ return True
465
+ except:
466
+ return False
467
+
468
+
469
+ def fsave(path, content, bakup=True):
470
+ if not Path(path).exists():
471
+ return False
472
+ try:
473
+ if bakup:
474
+ dname = str(Path(path).parent)
475
+ filename = '.%s.bak' % Path(path).name
476
+ os.rename(path, str(Path(dname) / filename))
477
+ with open(path, 'wb') as f:
478
+ f.write(content)
479
+ return True
480
+ except:
481
+ return False
482
+
483
+
484
+ def decode(filepath):
485
+ """Detect charset of content and decode it.
486
+ """
487
+ with open(filepath, 'rb') as file:
488
+ content = file.read()
489
+ for charset in charsets:
490
+ try:
491
+ return (charset, content.decode(charset))
492
+ except:
493
+ continue
494
+ return (None, content)
495
+
496
+
497
+ def encode(content, charset):
498
+ """Encode content using specified charset.
499
+ """
500
+ try:
501
+ return content.encode(charset)
502
+ except:
503
+ return False
504
+
505
+
506
+ def delete(path):
507
+ '''Move files to the Recycle Bin'''
508
+ if not Path(path).exists():
509
+ return False
510
+ path = str(Path(path))
511
+ mounts = _getmounts()
512
+ if kernel_name == 'Darwin':
513
+ trashpath = str(Path.home() / '.deleted_files')
514
+ else:
515
+ mount = ''
516
+ for m in mounts:
517
+ if path.startswith(m):
518
+ mount = m
519
+ break
520
+ if not mount:
521
+ return False
522
+ trashpath = str(Path(mount) / '.deleted_files')
523
+ _inittrash(mounts)
524
+ try:
525
+ uuid = str(uuid4())
526
+ filename = Path(path).name
527
+ db = shelve.open(str(Path(trashpath) / '.fileinfo'), 'c')
528
+ db[uuid] = '\t'.join([filename, path, str(int(time()))])
529
+
530
+ os.rename(path, str(Path(trashpath) / uuid))
531
+ # deal with the .filename.bak
532
+ dname = str(Path(path).parent)
533
+ bakfilepath = str(Path(dname) / ('.%s.bak' % filename))
534
+ if Path(bakfilepath).exists():
535
+ return delete(bakfilepath)
536
+ return True
537
+ except:
538
+ return False
539
+ finally:
540
+ db.close()
541
+
542
+
543
+ def _getmounts():
544
+ if kernel_name == 'Darwin':
545
+ return [str(Path.home())]
546
+ else:
547
+ mounts = server.ServerInfo.mounts()
548
+ mounts = [mount['path'] for mount in mounts]
549
+ # let the longest path at the first
550
+ # mounts.sort(lambda x, y: cmp(len(y), len(x)))
551
+ return sorted(mounts, key=lambda x: len(x), reverse=False)
552
+ # return mounts
553
+
554
+
555
+ def _inittrash(mounts=None):
556
+ # initialize the trash
557
+ if not mounts:
558
+ mounts = _getmounts()
559
+ for mount in mounts:
560
+ trashpath = str(Path(mount) / '.deleted_files')
561
+ if not Path(trashpath).exists():
562
+ Path(trashpath).mkdir(parents=True, exist_ok=True)
563
+ metafile = str(Path(trashpath) / '.fileinfo')
564
+ shelve.open(metafile, 'c').close()
565
+
566
+
567
+ def trashs():
568
+ """Return trash path list.
569
+ """
570
+ mounts = _getmounts()
571
+ return [str(Path(mount) / '.deleted_files') for mount in mounts]
572
+
573
+
574
+ def tlist():
575
+ mounts = _getmounts()
576
+ _inittrash(mounts)
577
+ # gather informations in each mount point's trash
578
+ items = []
579
+ for mount in mounts:
580
+ trashfile = str(Path(mount) / '.deleted_files' / '.fileinfo')
581
+ with shelve.open(trashfile, 'c') as db:
582
+ for uuid, info in db.items():
583
+ fields = info.split('\t')
584
+ item = {
585
+ 'uuid': uuid,
586
+ 'name': fields[0],
587
+ 'path': fields[1],
588
+ 'time': ftime(float(fields[2])),
589
+ 'mount': mount
590
+ }
591
+ filepath = str(Path(mount) / '.deleted_files' / uuid)
592
+ if Path(filepath).exists():
593
+ mode = os.stat(filepath).st_mode
594
+ item['isdir'] = stat.S_ISDIR(mode)
595
+ item['isreg'] = stat.S_ISREG(mode)
596
+ item['islnk'] = stat.S_ISLNK(mode)
597
+ items.append(item)
598
+ # items.sort(lambda x, y: cmp(y['time'], x['time']))
599
+ return items
600
+
601
+
602
+ def titem(mount, uuid):
603
+ # _inittrash()
604
+ try:
605
+ trashpath = str(Path(mount) / '.deleted_files')
606
+ db = shelve.open(str(Path(trashpath) / '.fileinfo'), 'c')
607
+ info = db[uuid]
608
+ db.close()
609
+ fields = info.split('\t')
610
+ info = {
611
+ 'uuid': uuid,
612
+ 'name': fields[0],
613
+ 'path': fields[1],
614
+ 'time': ftime(float(fields[2])),
615
+ 'mount': mount
616
+ }
617
+ info['originpath'] = str(Path(trashpath) / uuid)
618
+ return info
619
+ except:
620
+ return False
621
+
622
+
623
+ def trestore(mount, uuid):
624
+ # _inittrash()
625
+ try:
626
+ info = titem(mount, uuid)
627
+ trashpath = str(Path(mount) / '.deleted_files')
628
+ os.rename(str(Path(trashpath) / uuid), info['path'])
629
+ db = shelve.open(str(Path(trashpath) / '.fileinfo'), 'c')
630
+ del db[uuid]
631
+ db.close()
632
+ return True
633
+ except:
634
+ return False
635
+
636
+
637
+ def tdelete(mount, uuid):
638
+ # the real file or directory should be deleted external
639
+ # _inittrash()
640
+ try:
641
+ db = shelve.open(str(Path(mount) / '.deleted_files' / '.fileinfo'), 'c')
642
+ del db[uuid]
643
+ db.close()
644
+ return True
645
+ except:
646
+ return False
647
+
648
+
649
+ def chown(path, user, group, recursively=False):
650
+ if not Path(path).exists():
651
+ return False
652
+ try:
653
+ userid = groupid = -1
654
+ if user:
655
+ userid = getpwnam(user).pw_uid
656
+ if group:
657
+ groupid = getgrnam(group).gr_gid
658
+ if Path(path).exists() and recursively:
659
+ for root, dirs, files in os.walk(path):
660
+ for momo in dirs:
661
+ tpath = str(Path(root) / momo)
662
+ if not Path(tpath).exists():
663
+ continue # maybe broken link
664
+ os.chown(tpath, userid, groupid)
665
+ for momo in files:
666
+ tpath = str(Path(root) / momo)
667
+ if not Path(tpath).exists():
668
+ continue
669
+ os.chown(tpath, userid, groupid)
670
+ os.chown(path, userid, groupid)
671
+ except:
672
+ return False
673
+ return True
674
+
675
+
676
+ def chmod(path, perms, recursively=False):
677
+ if not Path(path).exists():
678
+ return False
679
+ try:
680
+ if Path(path).exists() and recursively:
681
+ for root, dirs, files in os.walk(path):
682
+ for momo in dirs:
683
+ tpath = str(Path(root) / momo)
684
+ if not Path(tpath).exists():
685
+ continue # maybe broken link
686
+ os.chmod(tpath, perms)
687
+ for momo in files:
688
+ tpath = str(Path(root) / momo)
689
+ if not Path(tpath).exists():
690
+ continue
691
+ os.chmod(tpath, perms)
692
+ os.chmod(path, perms)
693
+ except:
694
+ return False
695
+ return True
696
+
697
+
698
+ if __name__ == '__main__':
699
+ print('* List directory of /Users:')
700
+ path = '/Users'
701
+ items = listdir(path)
702
+ if items is not False:
703
+ for item in items:
704
+ print(' name: %s' % item['name'])
705
+ print(' isdir: %s' % str(item['isdir']))
706
+ # print(' isreg: %s' % str(item['isreg']))
707
+ # print(' islnk: %s' % str(item['islnk']))
708
+ # print(' perms: %s' % str(item['perms']))
709
+ # print(' uname: %s' % item['uname'])
710
+ # print(' gname: %s' % item['gname'])
711
+ # print(' size: %s' % item['size'])
712
+ # print(' atime: %s' % item['atime'])
713
+ # print(' mtime: %s' % item['mtime'])
714
+ # print(' ctime: %s' % item['ctime'])
715
+ f = str(Path(path) / item['name'])
716
+ # print(f)
717
+ # # if mime == 'text/plain':
718
+ # t = guess_type(f)[0]
719
+ # print(t)
720
+ # print(t.startswith('text'))
721
+ print(' istext: %s' % str(istext(f)))
722
+ # print(' mimetype: %s' % mimetype(f))