bcmd 0.0.65__tar.gz → 0.6.25__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.
Files changed (57) hide show
  1. bcmd-0.6.25/MANIFEST.in +2 -0
  2. bcmd-0.6.25/PKG-INFO +24 -0
  3. bcmd-0.0.65/bcmd/main.py → bcmd-0.6.25/bcmd/__init__.py +1 -1
  4. bcmd-0.6.25/bcmd/common/func.py +19 -0
  5. bcmd-0.6.25/bcmd/common/secret.py +69 -0
  6. bcmd-0.6.25/bcmd/resources/project/main.py +1 -0
  7. {bcmd-0.0.65 → bcmd-0.6.25}/bcmd/tasks/__init__.py +9 -5
  8. {bcmd-0.0.65 → bcmd-0.6.25}/bcmd/tasks/bin.py +24 -28
  9. bcmd-0.6.25/bcmd/tasks/code.py +132 -0
  10. {bcmd-0.0.65 → bcmd-0.6.25}/bcmd/tasks/crypto.py +64 -11
  11. bcmd-0.6.25/bcmd/tasks/docs.py +186 -0
  12. bcmd-0.6.25/bcmd/tasks/download.py +74 -0
  13. bcmd-0.6.25/bcmd/tasks/image.py +369 -0
  14. bcmd-0.6.25/bcmd/tasks/lib.py +93 -0
  15. {bcmd-0.0.65 → bcmd-0.6.25}/bcmd/tasks/math.py +2 -49
  16. bcmd-0.6.25/bcmd/tasks/mirror.py +45 -0
  17. bcmd-0.6.25/bcmd/tasks/pdf.py +43 -0
  18. bcmd-0.6.25/bcmd/tasks/project.py +71 -0
  19. bcmd-0.6.25/bcmd/tasks/proxy.py +62 -0
  20. {bcmd-0.0.65 → bcmd-0.6.25}/bcmd/tasks/time.py +12 -12
  21. bcmd-0.6.25/bcmd/tasks/upgrade.py +21 -0
  22. bcmd-0.6.25/bcmd/tasks/wasabi.py +94 -0
  23. bcmd-0.6.25/bcmd/tasks/web.py +27 -0
  24. bcmd-0.6.25/bcmd.egg-info/PKG-INFO +24 -0
  25. bcmd-0.6.25/bcmd.egg-info/SOURCES.txt +62 -0
  26. bcmd-0.6.25/bcmd.egg-info/entry_points.txt +2 -0
  27. bcmd-0.6.25/bcmd.egg-info/requires.txt +17 -0
  28. bcmd-0.6.25/bcmd.egg-info/top_level.txt +3 -0
  29. bcmd-0.6.25/pyproject.toml +46 -0
  30. bcmd-0.6.25/test/__init__.py +0 -0
  31. bcmd-0.6.25/test/conftest.py +17 -0
  32. bcmd-0.6.25/test/test_pdf.py +47 -0
  33. bcmd-0.6.25/test/test_proxy.py +8 -0
  34. bcmd-0.6.25/test/test_wasabi.py +28 -0
  35. bcmd-0.0.65/MANIFEST.in +0 -1
  36. bcmd-0.0.65/PKG-INFO +0 -10
  37. bcmd-0.0.65/bcmd/common/password.py +0 -34
  38. bcmd-0.0.65/bcmd/tasks/download.py +0 -39
  39. bcmd-0.0.65/bcmd/tasks/jwt.py +0 -87
  40. bcmd-0.0.65/bcmd/tasks/lib.py +0 -112
  41. bcmd-0.0.65/bcmd/tasks/mirror.py +0 -55
  42. bcmd-0.0.65/bcmd/tasks/proxy.py +0 -27
  43. bcmd-0.0.65/bcmd/tasks/task.py +0 -144
  44. bcmd-0.0.65/bcmd/tasks/temp.py +0 -31
  45. bcmd-0.0.65/bcmd/tasks/venv.py +0 -118
  46. bcmd-0.0.65/bcmd.egg-info/PKG-INFO +0 -10
  47. bcmd-0.0.65/bcmd.egg-info/SOURCES.txt +0 -27
  48. bcmd-0.0.65/bcmd.egg-info/entry_points.txt +0 -2
  49. bcmd-0.0.65/bcmd.egg-info/requires.txt +0 -2
  50. bcmd-0.0.65/bcmd.egg-info/top_level.txt +0 -1
  51. bcmd-0.0.65/pyproject.toml +0 -20
  52. {bcmd-0.0.65 → bcmd-0.6.25}/README.md +0 -0
  53. {bcmd-0.0.65/bcmd → bcmd-0.6.25/bcmd/common}/__init__.py +0 -0
  54. {bcmd-0.0.65 → bcmd-0.6.25}/bcmd/tasks/json.py +0 -0
  55. {bcmd-0.0.65/bcmd/common → bcmd-0.6.25/bcmd/utils}/__init__.py +0 -0
  56. {bcmd-0.0.65 → bcmd-0.6.25}/bcmd.egg-info/dependency_links.txt +0 -0
  57. {bcmd-0.0.65 → bcmd-0.6.25}/setup.cfg +0 -0
@@ -0,0 +1,2 @@
1
+ recursive-include bcmd/resources *
2
+ prune test
bcmd-0.6.25/PKG-INFO ADDED
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: bcmd
3
+ Version: 0.6.25
4
+ Summary: Commands for Beni
5
+ Author-email: Beni Mang <benimang@126.com>
6
+ Maintainer-email: Beni Mang <benimang@126.com>
7
+ Keywords: benimang,beni,bcmd
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: aioconsole>=0.8.1
10
+ Requires-Dist: async-lru>=2.0.5
11
+ Requires-Dist: benimang==0.8.10
12
+ Requires-Dist: cryptography>=45.0.4
13
+ Requires-Dist: nest-asyncio>=1.6.0
14
+ Requires-Dist: paramiko>=3.5.1
15
+ Requires-Dist: pillow>=11.2.1
16
+ Requires-Dist: prettytable>=3.16.0
17
+ Requires-Dist: pymupdf>=1.26.1
18
+ Requires-Dist: qiniu>=7.16.0
19
+ Requires-Dist: typer>=0.16.0
20
+ Provides-Extra: full
21
+ Requires-Dist: img2pdf>=0.6.1; extra == "full"
22
+ Requires-Dist: pytest>=8.4.0; extra == "full"
23
+ Requires-Dist: pytest-asyncio>=1.0.0; extra == "full"
24
+ Requires-Dist: pytest-order>=1.3.0; extra == "full"
@@ -6,5 +6,5 @@ from .tasks import *
6
6
 
7
7
 
8
8
  def run():
9
- btask.options.lock = ''
9
+ btask.options.lock = 0 # 允许多开
10
10
  asyncio.run(btask.main())
@@ -0,0 +1,19 @@
1
+ import importlib.resources
2
+ from contextlib import contextmanager
3
+ from pathlib import Path
4
+
5
+ from beni import btask
6
+
7
+
8
+ def checkFileOrNotExists(file: Path):
9
+ btask.assertTrue(file.is_file() or not file.exists(), f'必须是文件 {file}')
10
+
11
+
12
+ def checkPathOrNotExists(folder: Path):
13
+ btask.assertTrue(folder.is_dir() or not folder.exists(), f'必须是目录 {folder}')
14
+
15
+
16
+ @contextmanager
17
+ def useResources(name: str):
18
+ with importlib.resources.path('bcmd.resources', name) as target:
19
+ yield target
@@ -0,0 +1,69 @@
1
+ import tkinter as tk
2
+ from tkinter import messagebox
3
+ from typing import Any, TypedDict
4
+
5
+ from async_lru import alru_cache
6
+ from beni import bcrypto, btask
7
+ from beni.bform import BForm
8
+
9
+
10
+ class PypiSecret(TypedDict):
11
+ username: str
12
+ password: str
13
+
14
+
15
+ @alru_cache
16
+ async def getPypi(value: str = '') -> PypiSecret:
17
+ return _getData(
18
+ '请输入 PYPI 密钥信息',
19
+ value or 'QbuF2mV/lqovtF5dskZGD7qHknYbNuF2QseWRtWxLZTPrC/jL1tcxV8JEKaRjLsu46PxJZ7zepJwggnUTIWnEAoV5VtgP2/hbuzxxHha8817kR5c65H9fXm8eOal7DYXsUoGPQMnm59UWNXUKjmIaP4sn9nySFlRYqa8sEZSbYQ4N0NL35Dpj1e3wyQxJ+7h2jwKAz50Hh8G4yAM3/js9+NUe4ymts+UXcwsP3ADIBMkzjnFc0lEYg2d+fw0A74XWCvoZPoGqHZR/THUOVNAYxoGgDzP4SPIk1XsmtpxvfO/DpJd/Cg/0fB3MYagGKI1+m6Bxqhvd1I/lf0YbM5y4E4=',
20
+ )
21
+
22
+
23
+ class QiniuSecret(TypedDict):
24
+ bucket: str
25
+ baseUrl: str
26
+ ak: str
27
+ sk: str
28
+
29
+
30
+ @alru_cache
31
+ async def getQiniu(value: str = '') -> QiniuSecret:
32
+ return _getData(
33
+ '请输入 七牛云 密钥信息',
34
+ value or 'vNroFKeKklrdcJ89suFm+iyuJsq/cyUB5+QWoeeiMc/J0oSLF9cg5rqbK1IRxF0cCQ8KmkQQhdVa+PI6kuTBhoSH6IviVTylzAOrJywEccz9jWkJkW28Y9Vo4ePZmfWf/j7wdxNB144z234KD8IxJn4lR2A0L9JN5kk1o1/hpcydXL74FNtt03lYL/E3WVcvpUfw37mri2HMYOfUw81dRwW35/hMuQjtq1BBrKrIsSKTHH44tROMcgyvt+Qy292AtDBcsYiZxBKhQtBFPMq/vUs=',
35
+ )
36
+
37
+
38
+ def _getData(title: str, content: str) -> Any:
39
+
40
+ class PasswordForm(BForm):
41
+ result: dict[str, Any] = {}
42
+
43
+ def __init__(self):
44
+ super().__init__(title=title)
45
+ self.passwordVar = tk.StringVar(value='')
46
+ ary = content.split(' ')
47
+ if len(ary) > 1:
48
+ self.addLabel('提示', ary[0])
49
+ self.addEntry('密码', self.passwordVar, width=30, password=True, focus=True, command=self.onBtn)
50
+ self.addBtn('确定', self.onBtn)
51
+
52
+ def onBtn(self):
53
+ try:
54
+ self.result.update(
55
+ bcrypto.decryptJson(content, self.passwordVar.get())
56
+ )
57
+ self.destroy()
58
+ except:
59
+ messagebox.showerror('密码错误', '密码错误,请重新输入')
60
+
61
+ def getResult(self):
62
+ return self.result
63
+
64
+ result = PasswordForm().run()
65
+
66
+ if result is None:
67
+ btask.abort('用户取消操作')
68
+ else:
69
+ return result
@@ -0,0 +1 @@
1
+ print('OK')
@@ -1,14 +1,18 @@
1
1
  # type: ignore
2
2
  from . import bin
3
+ from . import code
4
+ from . import crypto
5
+ from . import docs
3
6
  from . import download
7
+ from . import image
4
8
  from . import json
5
- from . import jwt
6
9
  from . import lib
7
10
  from . import math
8
11
  from . import mirror
12
+ from . import pdf
13
+ from . import project
9
14
  from . import proxy
10
- from . import crypto
11
- from . import task
12
- from . import temp
13
15
  from . import time
14
- from . import venv
16
+ from . import upgrade
17
+ from . import wasabi
18
+ from . import web
@@ -1,16 +1,15 @@
1
- import os
2
1
  from datetime import datetime
3
2
  from pathlib import Path
4
3
  from typing import Final
5
4
 
6
5
  import typer
7
6
  from beni import bcolor, bfile, bpath, btask, bzip
8
- from beni.bfunc import syncCall
7
+ from beni.bfunc import syncCall, textToAry
9
8
  from beni.bqiniu import QiniuBucket
10
9
  from beni.btype import Null
11
10
  from prettytable import PrettyTable
12
11
 
13
- from bcmd.common import password
12
+ from ..common import secret
14
13
 
15
14
  app: Final = btask.newSubApp('bin 工具')
16
15
 
@@ -20,41 +19,39 @@ _PREFIX = 'bin/'
20
19
  @app.command()
21
20
  @syncCall
22
21
  async def download(
23
- names: str = typer.Argument(None, help="如果有多个使用,分割"),
22
+ names: list[str] = typer.Argument(None, help="支持多个"),
24
23
  file: Path = typer.Option(None, '--file', '-f', help="文件形式指定参数,行为单位"),
25
- output: Path = typer.Option(None, '--output', '-o', help="本地保存路径"),
24
+ output: Path = typer.Option(Path.cwd(), '--output', '-o', help="本地保存路径"),
25
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
26
26
  ):
27
27
  '从七牛云下载执行文件'
28
28
  bucket: QiniuBucket = Null
29
- if not output:
30
- output = Path(os.curdir)
31
- output = output.resolve()
32
- targetList: list[str] = []
33
- if names:
34
- targetList.extend(names.split(','))
35
29
  if file:
36
30
  content = await bfile.readText(Path(file))
37
- targetList.extend(content.split('\n'))
38
- targetList = [x.strip() for x in targetList]
39
- targetList = [x for x in targetList if x]
40
- for target in targetList:
31
+ names.extend(
32
+ textToAry(content)
33
+ )
34
+ for target in names:
41
35
  binFile = output / target
42
36
  if binFile.exists():
43
37
  bcolor.printYellow(f'已存在 {binFile}')
44
38
  else:
45
39
  key = f'bin/{target}.zip'
46
- bucket = bucket or await _getBucket()
40
+ bucket = bucket or await _getBucket(secretValue)
47
41
  await bucket.downloadPrivateFileUnzip(key, output)
48
42
  bcolor.printGreen(f'added {binFile}')
49
43
 
50
44
 
51
45
  @app.command('list')
52
46
  @syncCall
53
- async def getList():
47
+ async def getList(
48
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
49
+ ):
54
50
  '列出可下载的文件'
55
- bucket = await _getBucket()
51
+ bucket = await _getBucket(secretValue)
56
52
  datas = (await bucket.getFileList(_PREFIX, limit=1000))[0]
57
53
  datas = [x for x in datas if x.key != _PREFIX and x.key.endswith('.zip')]
54
+ datas.sort(key=lambda x: x.time, reverse=True)
58
55
  table = PrettyTable()
59
56
  table.add_column(
60
57
  bcolor.yellow('文件名称'),
@@ -74,9 +71,10 @@ async def getList():
74
71
  async def upload(
75
72
  file: Path = typer.Argument(..., help="本地文件路径"),
76
73
  force: bool = typer.Option(False, '--force', '-f', help="强制覆盖"),
74
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
77
75
  ):
78
76
  '上传'
79
- bucket = await _getBucket()
77
+ bucket = await _getBucket(secretValue)
80
78
  key = f'{_PREFIX}{file.name}.zip'
81
79
  if not force:
82
80
  if await bucket.getFileStatus(key):
@@ -91,17 +89,15 @@ async def upload(
91
89
  @syncCall
92
90
  async def remove(
93
91
  key: str = typer.Argument(..., help="云端文件key"),
92
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
94
93
  ):
95
- bucket = await _getBucket()
94
+ bucket = await _getBucket(secretValue)
96
95
  await bucket.deleteFiles(f'{_PREFIX}{key}.zip')
97
96
  bcolor.printGreen('OK')
98
97
 
99
98
 
100
- async def _getBucket():
101
- ak, sk = await password.getQiniu()
102
- return QiniuBucket(
103
- 'pytask',
104
- 'http://qiniu-cdn.pytask.com',
105
- ak,
106
- sk,
107
- )
99
+ # ------------------------------------------------------------------------------------
100
+
101
+
102
+ async def _getBucket(secretValue: str):
103
+ return QiniuBucket(**await secret.getQiniu())
@@ -0,0 +1,132 @@
1
+ import asyncio
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Final
5
+
6
+ import typer
7
+ from beni import bfile, bpath, btask
8
+ from beni.bcolor import printGreen, printMagenta, printYellow
9
+ from beni.bfunc import syncCall
10
+
11
+ app: Final = btask.newSubApp('code 工具')
12
+
13
+
14
+ @app.command()
15
+ @syncCall
16
+ async def tidy_tasks(
17
+ tasks_path: Path = typer.Argument(Path.cwd(), help="tasks 路径"),
18
+ ):
19
+ '整理 task 项目中的 tasks/__init__.py'
20
+
21
+ initFile = tasks_path / '__init__.py'
22
+ btask.assertTrue(initFile.is_file(), '文件不存在', initFile)
23
+ files = bpath.listFile(tasks_path)
24
+ files = [x for x in files if not x.name.startswith('_')]
25
+ contents = [f'from . import {x.stem}' for x in files]
26
+ contents.insert(0, '# type: ignore')
27
+ contents.append('')
28
+ content = '\n'.join(contents)
29
+ oldContent = await bfile.readText(initFile)
30
+ if oldContent != content:
31
+ await bfile.writeText(
32
+ initFile,
33
+ content,
34
+ )
35
+ printYellow(initFile)
36
+ printMagenta(content)
37
+ printGreen('OK')
38
+
39
+
40
+ @app.command()
41
+ @syncCall
42
+ async def tidy_modules(
43
+ modules_path: Path = typer.Argument(Path.cwd(), help="modules_path 路径"),
44
+ ):
45
+ '整理 fastapi 项目中的 modules/__init__.py'
46
+
47
+ importContents: list[str] = []
48
+ managerContents: list[str] = []
49
+
50
+ xxdict: dict[str, set[Path]] = {}
51
+ for file in sorted(modules_path.glob('**/*Manager.py')):
52
+ if file.parent == modules_path:
53
+ subName = '.'
54
+ elif file.parent.parent == modules_path:
55
+ subName = f'.{file.parent.stem}'
56
+ else:
57
+ continue
58
+ xxdict.setdefault(subName, set()).add(file)
59
+ for subName in sorted(xxdict.keys()):
60
+ files = sorted(xxdict[subName])
61
+ importContents.append(f'from {subName} import {", ".join([x.stem for x in files])}')
62
+ managerContents.extend([f' {x.stem},' for x in sorted([y for x in xxdict.values() for y in x])])
63
+
64
+ managerContents = [x for x in managerContents if x]
65
+ contents = [
66
+ '\n'.join(importContents),
67
+ 'managers = [\n' + '\n'.join(managerContents) + '\n]',
68
+ ]
69
+ content = '\n\n'.join(contents) + '\n'
70
+ file = modules_path / '__init__.py'
71
+ printYellow(str(file))
72
+ printMagenta(content)
73
+ await bfile.writeText(file, content)
74
+ printGreen('OK')
75
+
76
+
77
+ @app.command()
78
+ @syncCall
79
+ async def gen_init_py(
80
+ workspace_path: Path = typer.Argument(Path.cwd(), help='workspace 路径'),
81
+ ):
82
+ '递归生成 __init__.py 文件'
83
+
84
+ ignoreSubDirs = [
85
+ '.git',
86
+ 'venv',
87
+ 'node_modules',
88
+ '.pytest_cache',
89
+ '__pycache__',
90
+ '.vscode',
91
+ ]
92
+ folderList = bpath.listDir(workspace_path, True)
93
+ # 剔除子目录是这些的文件 .git venv ...
94
+ folderList = [x for x in folderList if not any([y in x.parts for y in ignoreSubDirs])]
95
+ for folder in folderList:
96
+ pyInitFile = folder / '__init__.py'
97
+ if not pyInitFile.exists():
98
+ printYellow(pyInitFile)
99
+ await bfile.writeText(pyInitFile, '')
100
+ printGreen('OK')
101
+
102
+
103
+ @app.command()
104
+ @syncCall
105
+ async def to_lf(
106
+ path: Path = typer.Option(None, '--path', help='指定目录或具体图片文件,默认当前目录'),
107
+ ):
108
+ '将所有文件转换为 LF 格式'
109
+ ignoreSubDirs = [
110
+ '.git',
111
+ 'venv',
112
+ 'node_modules',
113
+ '.pytest_cache',
114
+ '__pycache__',
115
+ ]
116
+ path = path or Path(os.getcwd())
117
+ files = bpath.listFile(path, True)
118
+ # 剔除子目录是这些的文件 .git venv ...
119
+ files = [x for x in files if not any([y in x.parts for y in ignoreSubDirs])]
120
+
121
+ async def convertFile(file: Path):
122
+ try:
123
+ content = await bfile.readText(file)
124
+ if '\r\n' in content:
125
+ content = content.replace('\r\n', '\n')
126
+ await bfile.writeText(file, content)
127
+ printYellow(file)
128
+ except:
129
+ pass
130
+
131
+ await asyncio.gather(*[convertFile(file) for file in files])
132
+ printGreen('OK')
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import getpass
2
3
  import json
3
4
  from pathlib import Path
@@ -5,8 +6,9 @@ from typing import Final
5
6
 
6
7
  import pyperclip
7
8
  import typer
8
- from beni import bcolor, bcrypto, btask
9
+ from beni import bcolor, bcrypto, bfile, bpath, btask
9
10
  from beni.bfunc import syncCall
11
+ from beni.binput import genPassword
10
12
  from rich.console import Console
11
13
 
12
14
  app: Final = btask.newSubApp('加密(v2)')
@@ -19,7 +21,7 @@ async def encrypt_text():
19
21
  content = pyperclip.paste()
20
22
  assert content, '剪贴板内容不能为空'
21
23
  bcolor.printGreen(content)
22
- password = _genPassword()
24
+ password = genPassword()
23
25
  result = bcrypto.encryptText(content, password)
24
26
  pyperclip.copy(result)
25
27
  print('密文已复制到剪贴板')
@@ -55,7 +57,7 @@ async def encrypt_json():
55
57
  except:
56
58
  return btask.abort('错误:剪贴板内容必须是JSON格式', content)
57
59
  Console().print_json(data=data, indent=4, ensure_ascii=False, sort_keys=True)
58
- password = _genPassword()
60
+ password = genPassword()
59
61
  result = bcrypto.encryptJson(data, password)
60
62
  pyperclip.copy(result)
61
63
  print('密文已复制到剪贴板')
@@ -87,7 +89,7 @@ async def encrypt_file(
87
89
  ):
88
90
  '加密文件(文件路径使用剪贴板内容)'
89
91
  assert file.is_file(), '文件不存在'
90
- password = _genPassword()
92
+ password = genPassword()
91
93
  await bcrypto.encryptFile(file, password)
92
94
  bcolor.printGreen('OK')
93
95
 
@@ -104,10 +106,61 @@ async def decrypt_file(
104
106
  bcolor.printGreen('OK')
105
107
 
106
108
 
107
- def _genPassword():
108
- password = ''
109
- while not password:
110
- password = getpass.getpass('输入密码:')
111
- while password != getpass.getpass('再次密码:'):
112
- pass
113
- return password
109
+ @app.command()
110
+ @syncCall
111
+ async def encrypt_path(
112
+ path: Path = typer.Argument(..., help='指定需要加密的目录'),
113
+ ):
114
+ '加密目录下所有的文件(目录路径使用剪贴板内容)'
115
+ path = path or Path(os.getcwd())
116
+ assert path.is_dir(), '目录不存在'
117
+ password = genPassword()
118
+ errorSet: set[str] = set()
119
+
120
+ async def handleFile(file: Path):
121
+ try:
122
+ content = await bfile.readText(file)
123
+ assert not content.startswith(bcrypto.FLAG), '文件已加密'
124
+ content = bcrypto.encryptText(content, password)
125
+ await bfile.writeText(file, content)
126
+ except:
127
+ errorSet.add(str(file))
128
+
129
+ fileList = sorted(bpath.listFile(path, True))
130
+ await asyncio.gather(*[handleFile(f) for f in fileList])
131
+
132
+ for file in fileList:
133
+ if str(file) in errorSet:
134
+ bcolor.printRed(file)
135
+ else:
136
+ print(file)
137
+
138
+
139
+ @app.command()
140
+ @syncCall
141
+ async def decrypt_path(
142
+ path: Path = typer.Argument(..., help='指定需要解密的目录'),
143
+ ):
144
+ '解密目录下所有的文件(目录路径使用剪贴板内容)'
145
+ path = path or Path(os.getcwd())
146
+ assert path.is_dir(), '目录不存在'
147
+ password = getpass.getpass('输入密码:')
148
+ errorSet: set[str] = set()
149
+
150
+ async def handleFile(file: Path):
151
+ try:
152
+ content = await bfile.readText(file)
153
+ assert content.startswith(bcrypto.FLAG), '文件未加密'
154
+ content = bcrypto.decryptText(content, password)
155
+ await bfile.writeText(file, content)
156
+ except:
157
+ errorSet.add(str(file))
158
+
159
+ fileList = sorted(bpath.listFile(path, True))
160
+ await asyncio.gather(*[handleFile(f) for f in fileList])
161
+
162
+ for file in fileList:
163
+ if str(file) in errorSet:
164
+ bcolor.printRed(file)
165
+ else:
166
+ print(file)
@@ -0,0 +1,186 @@
1
+ import datetime
2
+ import json
3
+ import pickle
4
+ import tkinter as tk
5
+ from contextlib import asynccontextmanager
6
+ from pathlib import Path
7
+ from tkinter import messagebox
8
+ from typing import Any, Final
9
+ from uuid import uuid4
10
+
11
+ import paramiko
12
+ from beni import bcolor, bcrypto, bfile, bpath, brun, btask, bzip
13
+ from beni.bform import BForm
14
+ from beni.bfunc import syncCall
15
+ from typer import Argument, Option
16
+
17
+ app: Final = btask.newSubApp('Vitepress 网站相关')
18
+ conf: dict[str, Any] = {}
19
+ isUpload: bool = False
20
+ password: str = ''
21
+ now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
22
+
23
+ projectPath = bpath.get('')
24
+ docsPath = bpath.get('')
25
+ vitepressDistPath = bpath.get('')
26
+ distPath = bpath.get('')
27
+ deployPath = bpath.get('')
28
+ zipFile = bpath.get('')
29
+
30
+
31
+ @app.command()
32
+ @syncCall
33
+ async def build(
34
+ path: Path = Argument(Path.cwd(), help='workspace 路径'),
35
+ build_cmd: str = Option('docs:build', help='vitepress 构建命令'),
36
+ ):
37
+ '发布'
38
+ with bpath.useTempPath(True) as tempPath:
39
+ await init(path)
40
+ await userInput()
41
+ await vitepressBuild(tempPath / 'site', build_cmd)
42
+ await makeDeploy(tempPath / 'deploy')
43
+ for file in bpath.listPath(tempPath):
44
+ await bzip.sevenZip(zipFile, file)
45
+ if isUpload:
46
+ await upload()
47
+ bcolor.printGreen('OK')
48
+
49
+
50
+ async def init(path: Path):
51
+ global projectPath, docsPath, vitepressDistPath, distPath, deployPath, zipFile
52
+
53
+ # 初始化目录路径
54
+ projectPath = path
55
+ docsPath = projectPath / 'docs'
56
+ vitepressDistPath = docsPath / '.vitepress/dist'
57
+ distPath = projectPath / 'dist'
58
+ deployPath = projectPath / 'deploy'
59
+
60
+ # 清空打包用到的目录
61
+ bpath.remove(distPath)
62
+ bpath.make(distPath)
63
+ bpath.remove(vitepressDistPath)
64
+
65
+ # 更新配置
66
+ projectTomlFile = projectPath / 'project.toml'
67
+ if not projectTomlFile.exists():
68
+ btask.abort('部署文件不存在', projectTomlFile)
69
+ conf.update(await bfile.readToml(projectTomlFile))
70
+
71
+ # 整理特殊的字段
72
+ zipFile = distPath / f'{conf['domain']}_{now}.7z'
73
+ conf['upload_file_name'] = zipFile.name
74
+ conf['temp_path'] += f'/{uuid4()}'
75
+
76
+
77
+ async def userInput():
78
+ global isUpload, password
79
+
80
+ class BuildForm(BForm):
81
+ def __init__(self):
82
+ super().__init__(title='发布网站')
83
+ self.varIsUpload = tk.BooleanVar(value=True)
84
+ self.varPassword = tk.StringVar(value='')
85
+ self.addLabel('网站', conf['domain'])
86
+ self.addCheckBox('上传服务器', '', self.varIsUpload)
87
+ self.addEntry('请输入密码', self.varPassword, width=20, command=self.onBtn, password=True, focus=True)
88
+ self.addBtn('确定', self.onBtn)
89
+
90
+ def onBtn(self):
91
+ password = self.varPassword.get()
92
+ try:
93
+ bcrypto.decryptJson(conf['server_info'], password)
94
+ self.destroy()
95
+ except:
96
+ messagebox.showerror('密码错误', '密码错误,请重新输入')
97
+
98
+ def getResult(self):
99
+ return self.varIsUpload.get(), self.varPassword.get()
100
+
101
+ result = BuildForm().run()
102
+ if not result:
103
+ btask.abort('用户取消操作')
104
+ isUpload, password = result
105
+
106
+ # 将里面加密的内容解密
107
+ for k, v in conf.items():
108
+ if str(v).startswith(bcrypto.FLAG):
109
+ conf[k] = bcrypto.decryptText(v, password)
110
+
111
+
112
+ async def vitepressBuild(outputPath: Path, build_cmd: str):
113
+ with bpath.changePath(projectPath):
114
+ await brun.run('pnpm install')
115
+ await brun.run(f'pnpm {build_cmd}')
116
+ bpath.copy(vitepressDistPath, outputPath)
117
+
118
+
119
+ async def makeDeploy(outputPath: Path):
120
+ bpath.copy(deployPath, outputPath)
121
+
122
+ # 删除配置里面加密和删除非字符串的配置,剩下的内容用于替换文件名和文件内容
123
+ dataDict: dict[str, Any] = pickle.loads(pickle.dumps(conf))
124
+ for k in list(dataDict.keys()):
125
+ v = dataDict[k]
126
+ if not isinstance(v, str):
127
+ del dataDict[k]
128
+
129
+ # 文件名以及内容调整
130
+ fileList = bpath.listFile(outputPath, True)
131
+ for file in fileList:
132
+
133
+ # 替换文件名
134
+ toFile = str(file)
135
+ for k, v in dataDict.items():
136
+ toFile = toFile.replace(f'{{{k}}}', str(v))
137
+ if toFile != str(file):
138
+ file.rename(toFile)
139
+ file = bpath.get(toFile)
140
+
141
+ # 替换文件内容
142
+ content = await bfile.readText(file)
143
+ oldContent = content
144
+ if content.startswith(bcrypto.FLAG):
145
+ content = bcrypto.decryptText(content, password)
146
+ for k, v in dataDict.items():
147
+ content = content.replace(f'{{{k}}}', str(v))
148
+ if oldContent != content:
149
+ await bfile.writeText(file, content)
150
+
151
+ # 将 Scrips 里面的文件都抽离到 dist 目录
152
+ scriptsPath = outputPath / 'Scripts'
153
+ for file in bpath.listFile(scriptsPath):
154
+ bpath.move(file, distPath / f'{conf['domain']}_{now}_{file.stem}{file.suffix}')
155
+ bpath.remove(scriptsPath)
156
+
157
+
158
+ @asynccontextmanager
159
+ async def sshClient():
160
+ client = paramiko.SSHClient()
161
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
162
+ with bpath.useTempFile() as tempFile:
163
+ await bfile.writeText(tempFile, conf['server_key'])
164
+ client.connect(
165
+ **json.loads(conf['server_info']),
166
+ key_filename=str(tempFile),
167
+ )
168
+ yield client
169
+ client.close()
170
+
171
+
172
+ async def upload():
173
+ async with sshClient() as client:
174
+
175
+ def executeCmd(cmd: str):
176
+ _, stdout, _ = client.exec_command(f"bash -lc '{cmd}'")
177
+ print(stdout.read().decode())
178
+
179
+ sftp = client.open_sftp()
180
+ for file in bpath.listFile(distPath):
181
+ executeCmd(f'mkdir -p {conf['temp_path']}')
182
+ sftp.put(str(file), f'{conf['temp_path']}/{file.name}')
183
+ shFile = list(distPath.glob('*.sh'))[0]
184
+ executeCmd(f'sh {conf['temp_path']}/{shFile.name}')
185
+ executeCmd(f'rm -rf {conf['temp_path']}')
186
+ sftp.close()