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.
- inpanel/__init__.py +29 -0
- inpanel/acme.py +289 -0
- inpanel/aliyuncs.py +269 -0
- inpanel/app.py +514 -0
- inpanel/apt.py +223 -0
- inpanel/base.py +200 -0
- inpanel/config.py +139 -0
- inpanel/data/config.ini +29 -0
- inpanel/data/history +5 -0
- inpanel/data/runlogs.ini +12 -0
- inpanel/data/update_info.ini +4 -0
- inpanel/dnf.py +225 -0
- inpanel/lib/__init__.py +9 -0
- inpanel/lib/async_process.py +98 -0
- inpanel/lib/filelock.py +82 -0
- inpanel/lib/magic.py +233 -0
- inpanel/lib/pxssh.py +310 -0
- inpanel/lib/pyDes.py +867 -0
- inpanel/mod/__init__.py +0 -0
- inpanel/mod/apt.py +316 -0
- inpanel/mod/certificate.py +268 -0
- inpanel/mod/config.py +372 -0
- inpanel/mod/cron.py +315 -0
- inpanel/mod/disk.py +322 -0
- inpanel/mod/dnf.py +239 -0
- inpanel/mod/file.py +722 -0
- inpanel/mod/firewall.py +199 -0
- inpanel/mod/firewall_base.py +137 -0
- inpanel/mod/firewall_firewalld.py +170 -0
- inpanel/mod/firewall_iptables.py +176 -0
- inpanel/mod/firewall_macos.py +213 -0
- inpanel/mod/firewall_ufw.py +128 -0
- inpanel/mod/ftp.py +231 -0
- inpanel/mod/httpd.py +1205 -0
- inpanel/mod/lighttpd.py +26 -0
- inpanel/mod/login.py +98 -0
- inpanel/mod/mysql.py +577 -0
- inpanel/mod/named.py +26 -0
- inpanel/mod/nginx.py +2327 -0
- inpanel/mod/package.py +33 -0
- inpanel/mod/package_apt.py +62 -0
- inpanel/mod/package_base.py +77 -0
- inpanel/mod/package_dnf.py +62 -0
- inpanel/mod/package_map.py +366 -0
- inpanel/mod/package_yum.py +64 -0
- inpanel/mod/php.py +289 -0
- inpanel/mod/process.py +291 -0
- inpanel/mod/proftpd.py +26 -0
- inpanel/mod/pureftpd.py +26 -0
- inpanel/mod/query.py +108 -0
- inpanel/mod/server.py +1319 -0
- inpanel/mod/service.py +262 -0
- inpanel/mod/setting.py +228 -0
- inpanel/mod/shell.py +78 -0
- inpanel/mod/ssh.py +280 -0
- inpanel/mod/system.py +220 -0
- inpanel/mod/task.py +1337 -0
- inpanel/mod/ufw.py +14 -0
- inpanel/mod/user.py +306 -0
- inpanel/mod/vsftpd.py +67 -0
- inpanel/mod/yum.py +243 -0
- inpanel/plugins/__init__.py +10 -0
- inpanel/plugins/__pycache__/__init__.cpython-313.pyc +0 -0
- inpanel/plugins/acme/static/index.html +10 -0
- inpanel/plugins/acme/static/index.js +13 -0
- inpanel/plugins/shadowsocks-libev/__pycache__/info.cpython-313.pyc +0 -0
- inpanel/plugins/shadowsocks-libev/info.py +73 -0
- inpanel/plugins/shadowsocks-libev/static/index.html +70 -0
- inpanel/plugins/shadowsocks-libev/static/index.js +18 -0
- inpanel/public/css/global.css +138 -0
- inpanel/public/favicon.ico +0 -0
- inpanel/public/images/alipay.jpg +0 -0
- inpanel/public/images/loading.gif +0 -0
- inpanel/public/images/weixin.jpg +0 -0
- inpanel/public/index.html +94 -0
- inpanel/public/js/controllers/controllers.js +566 -0
- inpanel/public/js/controllers/database.js +590 -0
- inpanel/public/js/controllers/ecs.js +624 -0
- inpanel/public/js/controllers/file.js +940 -0
- inpanel/public/js/controllers/plugins.js +105 -0
- inpanel/public/js/controllers/service.js +1460 -0
- inpanel/public/js/controllers/site.js +1241 -0
- inpanel/public/js/controllers/utils.js +2319 -0
- inpanel/public/js/core.js +221 -0
- inpanel/public/js/directives.js +784 -0
- inpanel/public/js/filters.js +395 -0
- inpanel/public/js/services.js +257 -0
- inpanel/public/lib/angular/1.0.2/angular-resource.min.js +10 -0
- inpanel/public/lib/angular/1.0.2/angular.min.js +158 -0
- inpanel/public/lib/bootstrap/css/bootstrap-theme.css +587 -0
- inpanel/public/lib/bootstrap/css/bootstrap-theme.css.map +1 -0
- inpanel/public/lib/bootstrap/css/bootstrap-theme.min.css +6 -0
- inpanel/public/lib/bootstrap/css/bootstrap-theme.min.css.map +1 -0
- inpanel/public/lib/bootstrap/css/bootstrap.css +6757 -0
- inpanel/public/lib/bootstrap/css/bootstrap.css.map +1 -0
- inpanel/public/lib/bootstrap/css/bootstrap.min.css +6 -0
- inpanel/public/lib/bootstrap/css/bootstrap.min.css.map +1 -0
- inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
- inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
- inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
- inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
- inpanel/public/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
- inpanel/public/lib/bootstrap/img/glyphicons-halflings-white.png +0 -0
- inpanel/public/lib/bootstrap/img/glyphicons-halflings.png +0 -0
- inpanel/public/lib/bootstrap/js/bootstrap.js +2377 -0
- inpanel/public/lib/bootstrap/js/bootstrap.min.js +7 -0
- inpanel/public/lib/bootstrap/js/npm.js +13 -0
- inpanel/public/lib/codemirror/codemirror-compressed.js +1 -0
- inpanel/public/lib/codemirror/codemirror.css +173 -0
- inpanel/public/lib/jquery/jquery-1.12.4.min.js +5 -0
- inpanel/public/lib/md5/md5.js +379 -0
- inpanel/public/lib/md5/md5.min.js +1 -0
- inpanel/public/partials/database/index.html +172 -0
- inpanel/public/partials/database/mysql/dbedit.html +462 -0
- inpanel/public/partials/database/mysql/dbnew.html +348 -0
- inpanel/public/partials/database/mysql/useredit.html +310 -0
- inpanel/public/partials/database/mysql/usernew.html +97 -0
- inpanel/public/partials/directives/message.html +20 -0
- inpanel/public/partials/directives/navbar.html +25 -0
- inpanel/public/partials/directives/selector.html +57 -0
- inpanel/public/partials/directives/srvbase.html +72 -0
- inpanel/public/partials/directives/srvext.html +34 -0
- inpanel/public/partials/directives/srvfile.html +23 -0
- inpanel/public/partials/directives/srvinstall.html +34 -0
- inpanel/public/partials/directives/srvlog.html +23 -0
- inpanel/public/partials/directives/srvminiop.html +28 -0
- inpanel/public/partials/directives/srvuninstall.html +38 -0
- inpanel/public/partials/directives/srvupdate.html +30 -0
- inpanel/public/partials/ecs/account.html +156 -0
- inpanel/public/partials/ecs/ecs.html +198 -0
- inpanel/public/partials/ecs/index.html +40 -0
- inpanel/public/partials/ecs/setings.html +459 -0
- inpanel/public/partials/file/file.html +501 -0
- inpanel/public/partials/file/trash.html +95 -0
- inpanel/public/partials/ftp.html +36 -0
- inpanel/public/partials/log.html +83 -0
- inpanel/public/partials/login.html +79 -0
- inpanel/public/partials/logout.html +22 -0
- inpanel/public/partials/main.html +534 -0
- inpanel/public/partials/plugins/index.html +55 -0
- inpanel/public/partials/plugins/plugins.html +13 -0
- inpanel/public/partials/secure.html +13 -0
- inpanel/public/partials/service/apache.html +147 -0
- inpanel/public/partials/service/cron.html +71 -0
- inpanel/public/partials/service/index.html +293 -0
- inpanel/public/partials/service/iptables.html +40 -0
- inpanel/public/partials/service/lighttpd.html +63 -0
- inpanel/public/partials/service/mariadb.html +113 -0
- inpanel/public/partials/service/memcache.html +36 -0
- inpanel/public/partials/service/minio.html +44 -0
- inpanel/public/partials/service/mongodb.html +44 -0
- inpanel/public/partials/service/mysql.html +113 -0
- inpanel/public/partials/service/named.html +61 -0
- inpanel/public/partials/service/nginx.html +296 -0
- inpanel/public/partials/service/ntp.html +40 -0
- inpanel/public/partials/service/php.html +250 -0
- inpanel/public/partials/service/proftpd.html +59 -0
- inpanel/public/partials/service/pureftpd.html +59 -0
- inpanel/public/partials/service/redis.html +40 -0
- inpanel/public/partials/service/samba.html +58 -0
- inpanel/public/partials/service/sendmail.html +40 -0
- inpanel/public/partials/service/ssh.html +186 -0
- inpanel/public/partials/service/tomcat.html +65 -0
- inpanel/public/partials/service/vsftpd.html +195 -0
- inpanel/public/partials/setting.html +299 -0
- inpanel/public/partials/site/apache/site.html +277 -0
- inpanel/public/partials/site/index.html +344 -0
- inpanel/public/partials/site/nginx/site.html +558 -0
- inpanel/public/partials/sorry.html +19 -0
- inpanel/public/partials/storage/autofm.html +103 -0
- inpanel/public/partials/storage/backup.html +42 -0
- inpanel/public/partials/storage/index.html +360 -0
- inpanel/public/partials/storage/migrate.html +93 -0
- inpanel/public/partials/storage/movedata.html +98 -0
- inpanel/public/partials/storage/remote.html +77 -0
- inpanel/public/partials/utils/cron.html +486 -0
- inpanel/public/partials/utils/firewall.html +266 -0
- inpanel/public/partials/utils/index.html +193 -0
- inpanel/public/partials/utils/network.html +118 -0
- inpanel/public/partials/utils/process.html +239 -0
- inpanel/public/partials/utils/repository.html +373 -0
- inpanel/public/partials/utils/shell.html +28 -0
- inpanel/public/partials/utils/ssl.html +319 -0
- inpanel/public/partials/utils/time.html +109 -0
- inpanel/public/partials/utils/user.html +359 -0
- inpanel/public/robots.txt +2 -0
- inpanel/remote.py +109 -0
- inpanel/templates/file/preview.html +34 -0
- inpanel/templates/index.html +93 -0
- inpanel/utils.py +282 -0
- inpanel/web.py +1811 -0
- inpanel/yum.py +756 -0
- inpanel-1.2.1.dist-info/METADATA +399 -0
- inpanel-1.2.1.dist-info/RECORD +198 -0
- inpanel-1.2.1.dist-info/WHEEL +5 -0
- inpanel-1.2.1.dist-info/entry_points.txt +2 -0
- inpanel-1.2.1.dist-info/licenses/LICENSE +30 -0
- 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))
|