pyutilscripts 0.9.0__tar.gz → 0.10.0__tar.gz
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.
- pyutilscripts-0.10.0/.editorconfig +13 -0
- pyutilscripts-0.10.0/.github/workflows/python-publish.yml +70 -0
- pyutilscripts-0.10.0/.gitignore +5 -0
- pyutilscripts-0.10.0/Changelog.md +60 -0
- {pyutilscripts-0.9.0/pyutilscripts.egg-info → pyutilscripts-0.10.0}/PKG-INFO +11 -10
- pyutilscripts-0.10.0/TODO.md +28 -0
- pyutilscripts-0.10.0/makefile +28 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/pyproject.toml +9 -6
- pyutilscripts-0.10.0/pytest.ini +4 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/pyutilscripts/__init__.py +1 -1
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/pyutilscripts/fcopy.py +17 -14
- pyutilscripts-0.10.0/pyutilscripts/ntraffic.py +269 -0
- pyutilscripts-0.10.0/pyutilscripts/ntunnel.py +173 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/pyutilscripts/utils/__init__.py +7 -5
- pyutilscripts-0.9.0/PKG-INFO +0 -82
- pyutilscripts-0.9.0/pyutilscripts.egg-info/SOURCES.txt +0 -19
- pyutilscripts-0.9.0/pyutilscripts.egg-info/dependency_links.txt +0 -1
- pyutilscripts-0.9.0/pyutilscripts.egg-info/entry_points.txt +0 -5
- pyutilscripts-0.9.0/pyutilscripts.egg-info/requires.txt +0 -7
- pyutilscripts-0.9.0/pyutilscripts.egg-info/top_level.txt +0 -1
- pyutilscripts-0.9.0/setup.cfg +0 -4
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/LICENSE +0 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/README.md +0 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/pyutilscripts/forward_tcp.py +0 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/pyutilscripts/httpd.py +0 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/pyutilscripts/prunedirs.py +0 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/pyutilscripts/toast.py +0 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/tests/test_action_parser.py +0 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/tests/test_fcopy.py +0 -0
- {pyutilscripts-0.9.0 → pyutilscripts-0.10.0}/tests/test_fcopy_cli.py +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Editor configuration, see https://editorconfig.org
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
charset = utf-8
|
|
6
|
+
indent_style = space
|
|
7
|
+
indent_size = 4
|
|
8
|
+
trim_trailing_whitespace = true
|
|
9
|
+
insert_final_newline = true
|
|
10
|
+
|
|
11
|
+
# 下面只覆盖不一样的配置,其他全部继承 [*]
|
|
12
|
+
[*.md]
|
|
13
|
+
indent_size = 2
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release-build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.x"
|
|
28
|
+
|
|
29
|
+
- name: Build release distributions
|
|
30
|
+
run: |
|
|
31
|
+
# NOTE: put your own distribution build steps here.
|
|
32
|
+
python -m pip install build
|
|
33
|
+
python -m build
|
|
34
|
+
|
|
35
|
+
- name: Upload distributions
|
|
36
|
+
uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: release-dists
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
pypi-publish:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
needs:
|
|
44
|
+
- release-build
|
|
45
|
+
permissions:
|
|
46
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
47
|
+
id-token: write
|
|
48
|
+
|
|
49
|
+
# Dedicated environments with protections for publishing are strongly recommended.
|
|
50
|
+
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
|
|
51
|
+
environment:
|
|
52
|
+
name: pypi
|
|
53
|
+
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
|
54
|
+
# url: https://pypi.org/p/YOURPROJECT
|
|
55
|
+
#
|
|
56
|
+
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
|
|
57
|
+
# ALTERNATIVE: exactly, uncomment the following line instead:
|
|
58
|
+
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
- name: Retrieve release distributions
|
|
62
|
+
uses: actions/download-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: release-dists
|
|
65
|
+
path: dist/
|
|
66
|
+
|
|
67
|
+
- name: Publish release distributions to PyPI
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
with:
|
|
70
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.10.0
|
|
4
|
+
|
|
5
|
+
- fcopy 优化 交互模式的行动清单, 将说明注释放在文档尾部
|
|
6
|
+
- ntunnel 添加 一个简单的网络工具, 基于 TUN 设备的 UPD/TCP 隧道,用于向远端转发 IP报文
|
|
7
|
+
|
|
8
|
+
## v0.9.0
|
|
9
|
+
|
|
10
|
+
- httpd 添加 http.server 限速版的文件服务器
|
|
11
|
+
|
|
12
|
+
## v0.8.0
|
|
13
|
+
|
|
14
|
+
- toast 添加 windows toast 通知支持
|
|
15
|
+
|
|
16
|
+
## v0.7.0
|
|
17
|
+
|
|
18
|
+
- fcopy 调整了运行时与verbose模式的输出, 提升用户体验
|
|
19
|
+
- fcopy 调整了默认编辑器优先级, 优先使用 Nano, 可通过 `export EDITOR=vim` 覆盖
|
|
20
|
+
- fcopy 调整了行动清单的输出与注释,增强可读性
|
|
21
|
+
- fcopy 修复一些bugs: --filter 为None导致的异常, Windows下因为BOM 文件头不能解析的问题
|
|
22
|
+
|
|
23
|
+
## v0.6.0
|
|
24
|
+
|
|
25
|
+
- fcopy 重构了行动列表与执行输出的内容, 使宏观行为更加清晰
|
|
26
|
+
- fcopy 调整了过滤逻辑, --filter 选项去除默认值, 输入文件无效视为致命错误
|
|
27
|
+
|
|
28
|
+
## v0.5.0
|
|
29
|
+
|
|
30
|
+
- fcopy 添加空目录支持
|
|
31
|
+
- fcopy 添加文件统计信息
|
|
32
|
+
|
|
33
|
+
## v0.4.0
|
|
34
|
+
|
|
35
|
+
- fcopy 添加严格模式, 将警告视为错误
|
|
36
|
+
- fcopy 优化交互内容, 增强用户体验
|
|
37
|
+
|
|
38
|
+
## v0.3.1
|
|
39
|
+
|
|
40
|
+
- fcopy 交互模式: 行动列表支持行尾注释
|
|
41
|
+
- fcopy 交互模式: 添加文件属性注释, 以便了解文件为何会被忽略
|
|
42
|
+
|
|
43
|
+
## v0.3.0
|
|
44
|
+
|
|
45
|
+
- fcopy 添加文件清单生成模式
|
|
46
|
+
- fcopy 添加单元测试覆盖
|
|
47
|
+
- fcopy 修复拷贝逻辑Bug
|
|
48
|
+
|
|
49
|
+
## v0.2.0
|
|
50
|
+
|
|
51
|
+
- fcopy 添加更新、覆盖、重命名模式
|
|
52
|
+
- fcopy 支持交互模式(生成行动列表,在用户编辑或确认后,才开始执行)
|
|
53
|
+
- fcopy 添加过滤模式
|
|
54
|
+
|
|
55
|
+
## v0.1.0
|
|
56
|
+
|
|
57
|
+
- Initial release.
|
|
58
|
+
- fcopy.py
|
|
59
|
+
- prunedirs.py
|
|
60
|
+
- forward-tcp.py
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyutilscripts
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: PyUtilScripts 是一个基于 Python 的通用小工具集合,目标是提供编写通用任务的辅助工具。
|
|
5
|
-
Author-email: Zero Kwok <zero.kwok@foxmail.com>
|
|
6
|
-
License: MIT License
|
|
7
5
|
Project-URL: Homepage, https://github.com/ZeroKwok/pyutilscripts
|
|
8
6
|
Project-URL: Issues, https://github.com/ZeroKwok/pyutilscripts/issues
|
|
7
|
+
Author-email: Zero Kwok <zero.kwok@foxmail.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
License-File: LICENSE
|
|
9
10
|
Keywords: tools
|
|
10
11
|
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: Topic :: Software Development :: Build Tools
|
|
12
12
|
Classifier: Programming Language :: Python
|
|
13
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.7
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.8
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.9
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
21
|
Requires-Python: >=3.7
|
|
21
|
-
Description-Content-Type: text/markdown
|
|
22
|
-
License-File: LICENSE
|
|
23
22
|
Requires-Dist: click
|
|
24
23
|
Requires-Dist: natsort
|
|
24
|
+
Requires-Dist: pytun-pmd3
|
|
25
25
|
Requires-Dist: termcolor
|
|
26
26
|
Provides-Extra: dev
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist: pytest
|
|
29
|
-
|
|
27
|
+
Requires-Dist: debugpy; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
30
31
|
|
|
31
32
|
# **PyUtilScripts**
|
|
32
33
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
`PyUtilScripts` 是一个基于 Python 的通用小工具集合,目标是提供便捷的日常任务自动化/辅助工具,帮助开发者、运维人员或普通用户提升效率。
|
|
4
|
+
|
|
5
|
+
## 近期
|
|
6
|
+
|
|
7
|
+
- [x] fcopy --actions 清单细化行动模式
|
|
8
|
+
- [x] c 拷贝, 即目标不存在
|
|
9
|
+
- [x] s 跳过, 即目标存在, 深/浅对比无差异, 视为文件无更新
|
|
10
|
+
- [x] i 忽略, 即目标存在, 元数据对比不一致, 但深对比无差异, 忽略
|
|
11
|
+
- [x] u 更新, 即目标存在, 元数据对比不一致, 但源文件更新鲜
|
|
12
|
+
- [x] o 覆盖, 即目标存在, 无条件拷贝并覆盖(--mode overwrite)
|
|
13
|
+
- [x] r 重命名, 即目标存在, 拷贝并递增文件名(--mode rename)
|
|
14
|
+
- [x] m 创建目录, 源文件是目录, 且目标不存在 (确保空目录被创建)
|
|
15
|
+
- [x] fcopy --actions 清单 action 为 u 时, 添加两个文件的修改时间注释
|
|
16
|
+
- [x] fcopy --filter 添加正则匹配过滤
|
|
17
|
+
- [x] fcopy --actions 清单支持行尾注释
|
|
18
|
+
- [x] fcopy 增加严格模式, 避免在拷贝时源文件不存在, 导致结果不如预期
|
|
19
|
+
- [x] fcopy --actions 清单添加原/目标目录, 与必要的注释
|
|
20
|
+
- [x] fcopy 支持空目录
|
|
21
|
+
- [x] fcopy --filter 过滤掉的文件, 应该出现在清单文件中, f
|
|
22
|
+
- [x] fcopy -i 交互模式下, 源目录中不存在的文件, 应该出现在清单文件中, m
|
|
23
|
+
- [x] fcopy 因拷贝创建的目录是冗余的, 因此只有空目录才体现在清单中, e
|
|
24
|
+
- [x] fcopy -i 交互模式下, 拷贝时不应出现: 过滤/缺失/忽略的文件警告, 仅在非交互模式下显示
|
|
25
|
+
- [x] fcopy -i 交互模式下, 如果存被过滤的项, 则显示过滤文件名, 便于调试
|
|
26
|
+
- [x] fcopy 优化 交互模式的行动清单, 将说明注释放在文档尾部
|
|
27
|
+
- [ ] fcopy 支持 将目标目录中, 未在 --list 清单中指定的文件, 移动至回收站(待定)
|
|
28
|
+
- [ ] fcopy 增加 忽略警告模式
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.PHONY: build upload test clean
|
|
2
|
+
|
|
3
|
+
build: clean
|
|
4
|
+
@echo "Building..."
|
|
5
|
+
python -m build
|
|
6
|
+
|
|
7
|
+
upload:
|
|
8
|
+
@echo "Uploading..."
|
|
9
|
+
twine upload --repository testpypi dist/*
|
|
10
|
+
twine upload dist/*
|
|
11
|
+
|
|
12
|
+
setup:
|
|
13
|
+
@echo "Setting up..."
|
|
14
|
+
python -m pip install .
|
|
15
|
+
|
|
16
|
+
test: clean
|
|
17
|
+
@echo "Testing..."
|
|
18
|
+
python -m pip install -e .[dev]
|
|
19
|
+
pytest tests
|
|
20
|
+
|
|
21
|
+
venv:
|
|
22
|
+
@echo "Setting up virtual environment..."
|
|
23
|
+
python -m venv venv
|
|
24
|
+
source venv/bin/activate
|
|
25
|
+
pip install -e .[dev]
|
|
26
|
+
|
|
27
|
+
clean:
|
|
28
|
+
rm -fr dist/*
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["
|
|
3
|
-
build-backend = "
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[tool.hatch.version]
|
|
6
|
+
path = "pyutilscripts/__init__.py"
|
|
4
7
|
|
|
5
8
|
[project]
|
|
6
9
|
name = "pyutilscripts"
|
|
@@ -28,17 +31,16 @@ dependencies = [
|
|
|
28
31
|
"click",
|
|
29
32
|
"natsort",
|
|
30
33
|
"termcolor",
|
|
34
|
+
"pytun_pmd3",
|
|
31
35
|
]
|
|
32
36
|
|
|
33
37
|
[project.optional-dependencies]
|
|
34
38
|
dev = [
|
|
39
|
+
"debugpy",
|
|
35
40
|
"pytest",
|
|
36
41
|
"pytest-cov",
|
|
37
42
|
]
|
|
38
43
|
|
|
39
|
-
[tool.setuptools.dynamic]
|
|
40
|
-
version = {attr = "pyutilscripts.projectVersion"}
|
|
41
|
-
|
|
42
44
|
[project.urls]
|
|
43
45
|
Homepage = "https://github.com/ZeroKwok/pyutilscripts"
|
|
44
46
|
Issues = "https://github.com/ZeroKwok/pyutilscripts/issues"
|
|
@@ -47,4 +49,5 @@ Issues = "https://github.com/ZeroKwok/pyutilscripts/issues"
|
|
|
47
49
|
"fcopy" = "pyutilscripts.fcopy:main"
|
|
48
50
|
"prunedirs" = "pyutilscripts.prunedirs:main"
|
|
49
51
|
"forward.tcp" = "pyutilscripts.forward_tcp:main"
|
|
50
|
-
"httpd" = "pyutilscripts.httpd:main"
|
|
52
|
+
"httpd" = "pyutilscripts.httpd:main"
|
|
53
|
+
"ntunnel" = "pyutilscripts.ntunnel:main"
|
|
@@ -37,8 +37,16 @@ ListFileHead = """# File list generated by fcopy on {Date}
|
|
|
37
37
|
# Count : {Count}
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
ActionFileHead = """
|
|
40
|
+
ActionFileHead = """
|
|
41
|
+
# fcopy action plan — edit to change copying actions (see end of this file for details)
|
|
41
42
|
#
|
|
43
|
+
# Source Directory: {Source}
|
|
44
|
+
# Target Directory: {Target}
|
|
45
|
+
# Blacklist : {Filter}
|
|
46
|
+
# Action Count : {Count}
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
ActionFileTail = """
|
|
42
50
|
# Actions:
|
|
43
51
|
# c - Copy : Target doesn't exist
|
|
44
52
|
# u - Update : Target exists with metadata mismatch, source is newer
|
|
@@ -56,11 +64,6 @@ ActionFileHead = """# Action plan for file copying (edit this file to change act
|
|
|
56
64
|
# r file3.txt -> file(3).txt Copy and Rename to file(3).txt
|
|
57
65
|
# i file2.txt # file:=, meta:> Files are the same and source file is newer
|
|
58
66
|
# i file2.txt # file:≠, meta:< Files are different and source is older
|
|
59
|
-
#
|
|
60
|
-
# Source Directory: {Source}
|
|
61
|
-
# Target Directory: {Target}
|
|
62
|
-
# Blacklist : {Filter}
|
|
63
|
-
# Action Count : {Count}
|
|
64
67
|
"""
|
|
65
68
|
|
|
66
69
|
ActionPriority = ["c", "u", "o", "r", "e", "m", "i", "f", "s"]
|
|
@@ -449,7 +452,7 @@ def line_append_space(line, align=16, minLength=32):
|
|
|
449
452
|
return line + max(n - l, 1) * " "
|
|
450
453
|
|
|
451
454
|
|
|
452
|
-
def join_actions(actions: list[Action], head: str, args):
|
|
455
|
+
def join_actions(actions: list[Action], head: str, tail: str, args):
|
|
453
456
|
def comment1(prefix, same, cmp):
|
|
454
457
|
return f'{prefix} file:{["≠", "="][same]}, meta:{["<", ">"][cmp == 1]}'
|
|
455
458
|
def comment2(prefix, stat):
|
|
@@ -489,10 +492,10 @@ def join_actions(actions: list[Action], head: str, args):
|
|
|
489
492
|
body = "\n".join(lines)
|
|
490
493
|
body = body.format(**counter)
|
|
491
494
|
|
|
492
|
-
return head.
|
|
495
|
+
return head.strip() + "\n" + body + "\n" + tail.strip()
|
|
493
496
|
|
|
494
497
|
|
|
495
|
-
def print_actions(actions: list, head: str, args):
|
|
498
|
+
def print_actions(actions: list, head: str, tail: str, args):
|
|
496
499
|
output(2, f"\nThe following actions will be performed:\n", "yellow")
|
|
497
500
|
colors = {
|
|
498
501
|
"#": "dark_grey",
|
|
@@ -500,7 +503,7 @@ def print_actions(actions: list, head: str, args):
|
|
|
500
503
|
}
|
|
501
504
|
colors.update(ActionColors)
|
|
502
505
|
|
|
503
|
-
lines = join_actions(actions, head, args)
|
|
506
|
+
lines = join_actions(actions, head, tail, args)
|
|
504
507
|
for line in lines.splitlines():
|
|
505
508
|
if not line:
|
|
506
509
|
output(2)
|
|
@@ -523,12 +526,12 @@ def get_available_editor(defaults=["nano", "vim", "vi", "notepad"]):
|
|
|
523
526
|
return None
|
|
524
527
|
|
|
525
528
|
|
|
526
|
-
def edit_actions(actions: list, head: str, args) -> list:
|
|
529
|
+
def edit_actions(actions: list, head: str, tail: str, args) -> list:
|
|
527
530
|
"""
|
|
528
531
|
使用 click.edit() 启动编辑器让用户编辑行动计划。
|
|
529
532
|
返回: None 用户取消编辑或没保存
|
|
530
533
|
"""
|
|
531
|
-
content = join_actions(actions, head, args)
|
|
534
|
+
content = join_actions(actions, head, tail, args)
|
|
532
535
|
|
|
533
536
|
# 打开编辑器让用户编辑内容
|
|
534
537
|
edited = click.edit(
|
|
@@ -555,9 +558,9 @@ def copy_files(args):
|
|
|
555
558
|
return 1
|
|
556
559
|
|
|
557
560
|
if args.interactive:
|
|
558
|
-
actions = edit_actions(actions, ActionFileHead, args)
|
|
561
|
+
actions = edit_actions(actions, ActionFileHead, ActionFileTail, args)
|
|
559
562
|
elif args.dry_run or args.verbose > 1:
|
|
560
|
-
print_actions(actions, ActionFileHead, args)
|
|
563
|
+
print_actions(actions, ActionFileHead, ActionFileTail, args)
|
|
561
564
|
output(2)
|
|
562
565
|
|
|
563
566
|
copied, skipped, missing, filtered = 0, 0, 0, 0
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#! python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# This file is part of the PyUtilScripts project.
|
|
5
|
+
# Copyright (c) 2020-2026 zero <zero.kwok@foxmail.com>
|
|
6
|
+
#
|
|
7
|
+
# For the full copyright and license information, please view the LICENSE
|
|
8
|
+
# file that was distributed with this source code.
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
import struct
|
|
12
|
+
import socket
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
import contextlib
|
|
16
|
+
|
|
17
|
+
prefix = b'\xef\x5a'
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
class AnyEndpoint:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.sock = None
|
|
23
|
+
self.peers = None
|
|
24
|
+
|
|
25
|
+
self.tx_bytes = 0 # 发送字节数
|
|
26
|
+
self.rx_bytes = 0 # 接收字节数
|
|
27
|
+
self.tx_packets = 0 # 发送包数
|
|
28
|
+
self.rx_packets = 0 # 接收包数
|
|
29
|
+
self.stats_time = time.time()
|
|
30
|
+
self.stats_lock = threading.Lock()
|
|
31
|
+
|
|
32
|
+
def add_tx(self, tx_bytes=0, tx_packets=1):
|
|
33
|
+
with self.stats_lock:
|
|
34
|
+
self.tx_bytes += tx_bytes
|
|
35
|
+
self.tx_packets += tx_packets
|
|
36
|
+
|
|
37
|
+
def add_rx(self, rx_bytes=0, rx_packets=1):
|
|
38
|
+
with self.stats_lock:
|
|
39
|
+
self.rx_bytes += rx_bytes
|
|
40
|
+
self.rx_packets += rx_packets
|
|
41
|
+
|
|
42
|
+
def stats(self, reset_timer:bool=True):
|
|
43
|
+
with self.stats_lock:
|
|
44
|
+
elapsed = time.time() - self.stats_time
|
|
45
|
+
if reset_timer:
|
|
46
|
+
self.stats_time = time.time()
|
|
47
|
+
tx_rate = self.tx_bytes / elapsed if elapsed > 0 else 0
|
|
48
|
+
rx_rate = self.rx_bytes / elapsed if elapsed > 0 else 0
|
|
49
|
+
return (elapsed, (self.tx_packets, self.tx_bytes, tx_rate), (self.rx_packets, self.rx_bytes, rx_rate))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class UDPEndpoint(AnyEndpoint):
|
|
53
|
+
def __init__(self, addr, peers = None):
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.type = 'UDP'
|
|
56
|
+
self.addr = addr
|
|
57
|
+
self.peers = peers
|
|
58
|
+
self.closed = True
|
|
59
|
+
|
|
60
|
+
def listen(self):
|
|
61
|
+
self.close()
|
|
62
|
+
try:
|
|
63
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
64
|
+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
65
|
+
self.sock.bind(self.addr)
|
|
66
|
+
self.addr = self.sock.getsockname()
|
|
67
|
+
self.closed = False
|
|
68
|
+
except OSError as e:
|
|
69
|
+
logger.error(f"UDPEndpoint listen failed on {self.addr}: {e}")
|
|
70
|
+
self.close()
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def establish(self) -> bool:
|
|
74
|
+
if self.closed or not self.sock:
|
|
75
|
+
logger.error("UDPEndpoint establish: socket not ready")
|
|
76
|
+
return False
|
|
77
|
+
self.send_packet(prefix + b'HELLO') # 握手, 目的是传送地址
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
def close(self):
|
|
81
|
+
if self.sock:
|
|
82
|
+
self.closed = True
|
|
83
|
+
with contextlib.suppress(Exception):
|
|
84
|
+
self.sock.close()
|
|
85
|
+
self.sock = None
|
|
86
|
+
|
|
87
|
+
def release(self):
|
|
88
|
+
self.close()
|
|
89
|
+
|
|
90
|
+
def send_packet(self, data):
|
|
91
|
+
if not self.sock:
|
|
92
|
+
logger.error("UDPEndpoint send: socket not created")
|
|
93
|
+
return False
|
|
94
|
+
if self.peers is None:
|
|
95
|
+
logger.debug("UDPEndpoint send: no peers address")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
self.sock.sendto(data, self.peers)
|
|
100
|
+
self.add_tx(len(data))
|
|
101
|
+
return True
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"UDPEndpoint send failed to {self.peers}: {e}")
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def recv_packet(self):
|
|
107
|
+
if not self.sock or self.closed:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
data, addr = self.sock.recvfrom(65535)
|
|
112
|
+
self.add_rx(len(data))
|
|
113
|
+
|
|
114
|
+
if data.startswith(prefix):
|
|
115
|
+
if data[2:] == b'HELLO':
|
|
116
|
+
self.peers = addr
|
|
117
|
+
logger.debug(f"UDPEndpoint recv HELLO from {addr}")
|
|
118
|
+
return data
|
|
119
|
+
except socket.timeout:
|
|
120
|
+
return None
|
|
121
|
+
except ConnectionResetError as e: # 对端未监听端口
|
|
122
|
+
return None
|
|
123
|
+
except OSError as e:
|
|
124
|
+
if not self.closed:
|
|
125
|
+
logger.error(f"UDPEndpoint recv error: {e}")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TCPEndpoint(AnyEndpoint):
|
|
131
|
+
def __init__(self, addr, peers = None, timeout=5):
|
|
132
|
+
super().__init__()
|
|
133
|
+
self.type = 'TCP'
|
|
134
|
+
self.addr = addr
|
|
135
|
+
self.peers = peers
|
|
136
|
+
self.timeout = timeout
|
|
137
|
+
self.connected = False
|
|
138
|
+
self.listen_sock = None
|
|
139
|
+
|
|
140
|
+
def listen(self):
|
|
141
|
+
if self.peers: # client mode not need to listen
|
|
142
|
+
return True
|
|
143
|
+
if self.listen_sock: # already listening
|
|
144
|
+
return True
|
|
145
|
+
if self.connected or not self.addr:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
150
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
151
|
+
sock.settimeout(self.timeout)
|
|
152
|
+
sock.bind(self.addr)
|
|
153
|
+
sock.listen(1)
|
|
154
|
+
self.listen_sock = sock
|
|
155
|
+
return True
|
|
156
|
+
except OSError as e:
|
|
157
|
+
logger.error(f"TCP listen failed on {self.addr}: {str(e)}", exc_info=True)
|
|
158
|
+
if 'sock' in locals() and sock:
|
|
159
|
+
sock.close()
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def connect(self):
|
|
163
|
+
logger.debug(f"TCP connect to {self.addr} ...")
|
|
164
|
+
if self.connected:
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
self.close()
|
|
169
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
170
|
+
self.sock.settimeout(self.timeout)
|
|
171
|
+
self.sock.connect(self.peers)
|
|
172
|
+
self.connected = True
|
|
173
|
+
logger.info(f"[+] TCP connected to {self.peers}")
|
|
174
|
+
return True
|
|
175
|
+
except OSError as e:
|
|
176
|
+
logger.error(f"TCPEndpoint connect failed to {self.peers}: {e}")
|
|
177
|
+
if self.sock:
|
|
178
|
+
self.close()
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
def accept(self):
|
|
182
|
+
if not self.listen_sock:
|
|
183
|
+
logger.error("TCPEndpoint accept: no listen socket")
|
|
184
|
+
return False
|
|
185
|
+
if self.connected:
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
self.close()
|
|
190
|
+
self.sock, self.peers = self.listen_sock.accept()
|
|
191
|
+
self.sock.settimeout(self.timeout)
|
|
192
|
+
self.connected = True
|
|
193
|
+
logger.debug(f"TCPEndpoint accept: connected to {self.peers}")
|
|
194
|
+
return True
|
|
195
|
+
except socket.timeout:
|
|
196
|
+
return False
|
|
197
|
+
except OSError as e:
|
|
198
|
+
if self.listen_sock:
|
|
199
|
+
logger.error(f"TCPEndpoint accept error: {e}")
|
|
200
|
+
if self.sock:
|
|
201
|
+
self.close()
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
def establish(self) -> bool:
|
|
205
|
+
if not self.connected:
|
|
206
|
+
if self.listen_sock:
|
|
207
|
+
return self.accept()
|
|
208
|
+
else:
|
|
209
|
+
return self.connect()
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
def close(self):
|
|
213
|
+
self.connected = False
|
|
214
|
+
if self.sock:
|
|
215
|
+
with contextlib.suppress(Exception):
|
|
216
|
+
self.sock.close()
|
|
217
|
+
self.sock = None
|
|
218
|
+
|
|
219
|
+
def release(self):
|
|
220
|
+
self.close()
|
|
221
|
+
if self.listen_sock:
|
|
222
|
+
with contextlib.suppress(Exception):
|
|
223
|
+
self.listen_sock.close()
|
|
224
|
+
self.listen_sock = None
|
|
225
|
+
|
|
226
|
+
def send_packet(self, data):
|
|
227
|
+
if not self.establish():
|
|
228
|
+
logger.debug("TCPEndpoint send packet: failed to establish connection")
|
|
229
|
+
return False
|
|
230
|
+
try:
|
|
231
|
+
self.sock.sendall(struct.pack('!I', len(data)) + data)
|
|
232
|
+
self.add_tx(len(data))
|
|
233
|
+
return True
|
|
234
|
+
except OSError as e:
|
|
235
|
+
logger.error(f"TCPEndpoint send failed to {self.peers}: {e}")
|
|
236
|
+
self.close()
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
def recv_packet(self):
|
|
240
|
+
if not self.establish():
|
|
241
|
+
logger.debug("TCPEndpoint recv packet: failed to establish connection")
|
|
242
|
+
return None
|
|
243
|
+
try:
|
|
244
|
+
length = self._recv_exact(4)
|
|
245
|
+
if not length:
|
|
246
|
+
return None
|
|
247
|
+
length = struct.unpack('!I', length)[0]
|
|
248
|
+
data = self._recv_exact(length)
|
|
249
|
+
self.add_rx(len(data))
|
|
250
|
+
return data
|
|
251
|
+
except OSError as e:
|
|
252
|
+
logger.error(f"TCPEndpoint recv failed from {self.peers}: {e}")
|
|
253
|
+
self.close()
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
def _recv_exact(self, size):
|
|
257
|
+
data = b''
|
|
258
|
+
while len(data) < size:
|
|
259
|
+
try:
|
|
260
|
+
chunk = self.sock.recv(size - len(data))
|
|
261
|
+
if not chunk: # EOF
|
|
262
|
+
self.close()
|
|
263
|
+
return None
|
|
264
|
+
data += chunk
|
|
265
|
+
except socket.timeout:
|
|
266
|
+
logger.warning(f"TCPEndpoint recv timeout, peer={self.peers}")
|
|
267
|
+
self.close()
|
|
268
|
+
return None
|
|
269
|
+
return data
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#! python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# This file is part of the PyUtilScripts project.
|
|
5
|
+
# Copyright (c) 2020-2026 zero <zero.kwok@foxmail.com>
|
|
6
|
+
#
|
|
7
|
+
# For the full copyright and license information, please view the LICENSE
|
|
8
|
+
# file that was distributed with this source code.
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import logging
|
|
13
|
+
import argparse
|
|
14
|
+
import traceback
|
|
15
|
+
import threading
|
|
16
|
+
import contextlib
|
|
17
|
+
|
|
18
|
+
from .utils import *
|
|
19
|
+
from .ntraffic import UDPEndpoint, TCPEndpoint, AnyEndpoint
|
|
20
|
+
|
|
21
|
+
running = True
|
|
22
|
+
|
|
23
|
+
def create_tun(name:str, addr: tuple, mtu=1400):
|
|
24
|
+
try:
|
|
25
|
+
import pytun_pmd3 as pytun
|
|
26
|
+
if sys.platform == 'win32':
|
|
27
|
+
tun = pytun.TunTapDevice(name=name)
|
|
28
|
+
else:
|
|
29
|
+
tun = pytun.TunTapDevice(name=name, flags=pytun.linux.IFF_NO_PI | pytun.linux.IFF_TUN)
|
|
30
|
+
tun.addr = addr
|
|
31
|
+
tun.mtu = mtu
|
|
32
|
+
tun.up()
|
|
33
|
+
return tun
|
|
34
|
+
except ImportError:
|
|
35
|
+
print('[!] pytun module not found. Please install "pytun-pmd3" using pip.')
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"[!] Error creating TUN device: {e}")
|
|
38
|
+
exit(1)
|
|
39
|
+
|
|
40
|
+
def forward_tun_to_peers(tun, endpoint:AnyEndpoint, args:dict):
|
|
41
|
+
global running
|
|
42
|
+
while running:
|
|
43
|
+
try:
|
|
44
|
+
packet = tun.read(tun.mtu)
|
|
45
|
+
if packet:
|
|
46
|
+
args.debug and print_packet(packet, prefix='[TUN -> PEER]')
|
|
47
|
+
endpoint.send_packet(packet)
|
|
48
|
+
except TimeoutError:
|
|
49
|
+
continue
|
|
50
|
+
except Exception as e:
|
|
51
|
+
if running:
|
|
52
|
+
print(f"[!] Error in [TUN -> PEER]: {traceback.format_exc()}")
|
|
53
|
+
|
|
54
|
+
def forward_peers_to_tun(tun, endpoint:AnyEndpoint, args:dict):
|
|
55
|
+
global running
|
|
56
|
+
while running:
|
|
57
|
+
try:
|
|
58
|
+
data = endpoint.recv_packet()
|
|
59
|
+
if data:
|
|
60
|
+
args.debug and print_packet(data, prefix='[TUN <- PEER]')
|
|
61
|
+
with contextlib.suppress(OSError): # 忽略无效数据包引发的无效参数异常
|
|
62
|
+
tun.write(data)
|
|
63
|
+
else:
|
|
64
|
+
time.sleep(1)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"[!] Error in [TUN <- PEER]: {traceback.format_exc()}")
|
|
67
|
+
|
|
68
|
+
def print_packet(packet, prefix=''):
|
|
69
|
+
src = dst = "N/A"
|
|
70
|
+
version = packet[0] >> 4
|
|
71
|
+
if version == 4:
|
|
72
|
+
src, dst = packet[12:16], packet[16:20]
|
|
73
|
+
src, dst = ".".join(map(str, src)), ".".join(map(str, dst))
|
|
74
|
+
elif version == 6:
|
|
75
|
+
src, dst = packet[8:24], packet[24:40]
|
|
76
|
+
src, dst = src.hex(':'), dst.hex(':')
|
|
77
|
+
print(f"{prefix}: {src} -> {dst} (len={len(packet)})")
|
|
78
|
+
|
|
79
|
+
def report_stats(args, endpoint: AnyEndpoint):
|
|
80
|
+
while True:
|
|
81
|
+
time.sleep(args.stats_interval)
|
|
82
|
+
(elapsed, (tx_packets, tx_bytes, tx_rate), (rx_packets, rx_bytes, rx_rate)) = endpoint.stats()
|
|
83
|
+
tx = f"{tx_packets} pkts {format_bytes(tx_bytes, precision='.2f')} {format_bytes(tx_rate, precision=' 7.1f', postfix='/s')}"
|
|
84
|
+
rx = f"{rx_packets} pkts {format_bytes(rx_bytes, precision='.2f')} {format_bytes(rx_rate, precision=' 7.1f', postfix='/s')}"
|
|
85
|
+
print(f"[*] {elapsed:06.1f}s TX [{tx}] - RX [{rx}]")
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
parser = argparse.ArgumentParser(description="Create a simple TUN to forward IP packets to remote peers")
|
|
89
|
+
parser.add_argument("--name", default="tun0", help="Interface name (default: tun0)")
|
|
90
|
+
parser.add_argument("--addr", default="fd00::1", help="IPv6 Address (default: fd00::1)")
|
|
91
|
+
parser.add_argument("--protocol", default="udp", help="Transport protocol (udp/tcp)")
|
|
92
|
+
parser.add_argument("--remote", type=str, default=None, help="Forward packets to remote endpoint")
|
|
93
|
+
parser.add_argument("--listen", type=str, default='0.0.0.0:8001', help="Listen for incoming packets (default: 0.0.0.0:8001)")
|
|
94
|
+
parser.add_argument("--stats-interval", type=int, default=5, help="Traffic report interval in seconds (default: 5)")
|
|
95
|
+
parser.add_argument("--no-stats", action="store_true", help="Disable traffic statistics reporting")
|
|
96
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
|
97
|
+
args = parser.parse_args()
|
|
98
|
+
|
|
99
|
+
if args.debug:
|
|
100
|
+
print(f"[*] Debug: Starting with arguments: {args}")
|
|
101
|
+
input("Press Enter to continue...")
|
|
102
|
+
logging.root.addHandler(logging.StreamHandler())
|
|
103
|
+
logging.root.setLevel(logging.DEBUG)
|
|
104
|
+
logging.debug("[*] Debug: Logging enabled")
|
|
105
|
+
|
|
106
|
+
def parse_addr(addr, default=None):
|
|
107
|
+
if addr is None:
|
|
108
|
+
return None
|
|
109
|
+
host, port = addr.split(':')
|
|
110
|
+
host = host if host else default[0]
|
|
111
|
+
port = port if port else default[1]
|
|
112
|
+
return (host, int(port))
|
|
113
|
+
|
|
114
|
+
args.remote = parse_addr(args.remote, default=('127.0.0.1', 0))
|
|
115
|
+
args.listen = parse_addr(args.listen, default=('0.0.0.0', 0))
|
|
116
|
+
|
|
117
|
+
if args.protocol.upper() != 'UDP':
|
|
118
|
+
Endpoint = TCPEndpoint
|
|
119
|
+
else:
|
|
120
|
+
Endpoint = UDPEndpoint
|
|
121
|
+
|
|
122
|
+
endpoint = Endpoint(addr=args.listen, peers=args.remote)
|
|
123
|
+
endpoint.listen()
|
|
124
|
+
endpoint.establish()
|
|
125
|
+
print(f"[+] {endpoint.type} socket listen to {endpoint.addr}")
|
|
126
|
+
|
|
127
|
+
tun = create_tun(args.name, args.addr)
|
|
128
|
+
print(f"[+] Interface {tun.name} is UP")
|
|
129
|
+
print(f"[+] IP Address: {args.addr}")
|
|
130
|
+
|
|
131
|
+
# 启动转发线程
|
|
132
|
+
t1 = threading.Thread(target=forward_tun_to_peers, args=(tun, endpoint, args))
|
|
133
|
+
t1.daemon = True
|
|
134
|
+
t1.start()
|
|
135
|
+
|
|
136
|
+
t2 = threading.Thread(target=forward_peers_to_tun, args=(tun, endpoint, args))
|
|
137
|
+
t2.daemon = True
|
|
138
|
+
t2.start()
|
|
139
|
+
|
|
140
|
+
# 启动统计报告线程(如果不禁用)
|
|
141
|
+
if not args.no_stats:
|
|
142
|
+
t_stats = threading.Thread(target=report_stats, args=(args, endpoint))
|
|
143
|
+
t_stats.daemon = True
|
|
144
|
+
t_stats.start()
|
|
145
|
+
print(f"[+] Traffic statistics reporting every {args.stats_interval}s")
|
|
146
|
+
else:
|
|
147
|
+
print("[*] Traffic statistics reporting disabled")
|
|
148
|
+
|
|
149
|
+
print(f"[*] Tunnel is running. Press Ctrl+C to exit.")
|
|
150
|
+
try:
|
|
151
|
+
while True:
|
|
152
|
+
time.sleep(1)
|
|
153
|
+
except KeyboardInterrupt:
|
|
154
|
+
print("\n[!] Shutting down...")
|
|
155
|
+
finally:
|
|
156
|
+
# 最终报告
|
|
157
|
+
if not args.no_stats:
|
|
158
|
+
print("\n[!] Final Traffic Summary:")
|
|
159
|
+
(_, (tx_packets, tx_bytes, _), (rx_packets, rx_bytes, _)) = endpoint.stats()
|
|
160
|
+
print(f" Total TX: {format_bytes(tx_bytes)} ({tx_packets} packets)")
|
|
161
|
+
print(f" Total RX: {format_bytes(rx_bytes)} ({rx_packets} packets)")
|
|
162
|
+
print(f" Total: {format_bytes(tx_bytes + rx_bytes)}")
|
|
163
|
+
|
|
164
|
+
global running
|
|
165
|
+
running = False
|
|
166
|
+
tun.down()
|
|
167
|
+
tun.close()
|
|
168
|
+
endpoint.release()
|
|
169
|
+
print("[+] Cleanup complete.")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
main()
|
|
@@ -2,14 +2,16 @@ import math
|
|
|
2
2
|
import time
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def format_bytes(
|
|
6
|
-
""
|
|
5
|
+
def format_bytes(
|
|
6
|
+
size_bytes: int, precision: str = ".2f", postfix: str = "", base: int = 1024
|
|
7
|
+
) -> str:
|
|
8
|
+
"""将字节数格式化为人类可读形式"""
|
|
7
9
|
if size_bytes == 0:
|
|
8
10
|
return "0B"
|
|
9
11
|
units = ("B", "KB", "MB", "GB", "TB", "PB")
|
|
10
|
-
i = int(math.floor(math.log(size_bytes,
|
|
11
|
-
size = size_bytes / (
|
|
12
|
-
return f"{size
|
|
12
|
+
i = int(math.floor(math.log(size_bytes, base)))
|
|
13
|
+
size = size_bytes / (base**i)
|
|
14
|
+
return f"{size:{precision}}{units[i]}"
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
def format_ftime(seconds: float, format: str = "%Y-%m-%d %H:%M"):
|
pyutilscripts-0.9.0/PKG-INFO
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pyutilscripts
|
|
3
|
-
Version: 0.9.0
|
|
4
|
-
Summary: PyUtilScripts 是一个基于 Python 的通用小工具集合,目标是提供编写通用任务的辅助工具。
|
|
5
|
-
Author-email: Zero Kwok <zero.kwok@foxmail.com>
|
|
6
|
-
License: MIT License
|
|
7
|
-
Project-URL: Homepage, https://github.com/ZeroKwok/pyutilscripts
|
|
8
|
-
Project-URL: Issues, https://github.com/ZeroKwok/pyutilscripts/issues
|
|
9
|
-
Keywords: tools
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: Topic :: Software Development :: Build Tools
|
|
12
|
-
Classifier: Programming Language :: Python
|
|
13
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Requires-Python: >=3.7
|
|
21
|
-
Description-Content-Type: text/markdown
|
|
22
|
-
License-File: LICENSE
|
|
23
|
-
Requires-Dist: click
|
|
24
|
-
Requires-Dist: natsort
|
|
25
|
-
Requires-Dist: termcolor
|
|
26
|
-
Provides-Extra: dev
|
|
27
|
-
Requires-Dist: pytest; extra == "dev"
|
|
28
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
29
|
-
Dynamic: license-file
|
|
30
|
-
|
|
31
|
-
# **PyUtilScripts**
|
|
32
|
-
|
|
33
|
-
`PyUtilScripts` 是一个基于 Python 的通用小工具集合,目标是提供编写通用任务的辅助工具。
|
|
34
|
-
|
|
35
|
-
## 📦 安装
|
|
36
|
-
|
|
37
|
-
### 通过 pip 安装
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
pip install pyutilscripts
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### 从源码安装
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
git clone https://github.com/ZeroKwok/PyUtilScripts.git
|
|
47
|
-
cd PyUtilScripts
|
|
48
|
-
pip install .
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## 📝 使用说明
|
|
54
|
-
|
|
55
|
-
- **fcopy**
|
|
56
|
-
- 基于清单文件的复制工具
|
|
57
|
-
- 特点
|
|
58
|
-
- 支持 更新、覆盖写、重命名模式
|
|
59
|
-
- 支持 交互模式,精准把控拷贝细节(拷贝前生成行动列表,在用户编辑或确认后,才具体执行行动列表中记录的动作)
|
|
60
|
-
- 支持 过滤模式,忽略某些文件或目录
|
|
61
|
-
- 示例:
|
|
62
|
-
- 按文件清单拷贝指定目录下的文件
|
|
63
|
-
- 更新模式 `fcopy -l /path/to/list.txt -s /path/to/src -t /path/to/dest`
|
|
64
|
-
- 覆盖模式 `fcopy -l /path/to/list.txt -s /path/to/src -t /path/to/dest -m o`
|
|
65
|
-
- 重命名模式 `fcopy -l /path/to/list.txt -s /path/to/src -t /path/to/dest -m r`
|
|
66
|
-
- 通过指定目录下的文件生成文件清单
|
|
67
|
-
- `fcopy -l /path/to/list.txt -s /path/to/src --update-list`
|
|
68
|
-
- 交互模式下拷贝指定目录的文件
|
|
69
|
-
- `fcopy -l /path/to/list.txt -s /path/to/src -t /path/to/dest -i`
|
|
70
|
-
- 概念
|
|
71
|
-
- 文件清单(fcopy.list)决定要拷贝的文件
|
|
72
|
-
- 行动清单决定拷贝行为(交互模式下通过编辑器呈现)
|
|
73
|
-
|
|
74
|
-
- **prunedirs**
|
|
75
|
-
- 递归删除空目录
|
|
76
|
-
- 示例:
|
|
77
|
-
- `prunedirs /path/to/dir`
|
|
78
|
-
|
|
79
|
-
- **forward.tcp**
|
|
80
|
-
- TCP 端口转发工具
|
|
81
|
-
- 示例:
|
|
82
|
-
- `forward.tcp -s 0.0.0.0:8081 -d 127.0.0.1:1081`
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.md
|
|
3
|
-
pyproject.toml
|
|
4
|
-
pyutilscripts/__init__.py
|
|
5
|
-
pyutilscripts/fcopy.py
|
|
6
|
-
pyutilscripts/forward_tcp.py
|
|
7
|
-
pyutilscripts/httpd.py
|
|
8
|
-
pyutilscripts/prunedirs.py
|
|
9
|
-
pyutilscripts/toast.py
|
|
10
|
-
pyutilscripts.egg-info/PKG-INFO
|
|
11
|
-
pyutilscripts.egg-info/SOURCES.txt
|
|
12
|
-
pyutilscripts.egg-info/dependency_links.txt
|
|
13
|
-
pyutilscripts.egg-info/entry_points.txt
|
|
14
|
-
pyutilscripts.egg-info/requires.txt
|
|
15
|
-
pyutilscripts.egg-info/top_level.txt
|
|
16
|
-
pyutilscripts/utils/__init__.py
|
|
17
|
-
tests/test_action_parser.py
|
|
18
|
-
tests/test_fcopy.py
|
|
19
|
-
tests/test_fcopy_cli.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pyutilscripts
|
pyutilscripts-0.9.0/setup.cfg
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|