├── requirements.txt ├── utils ├── clear_screen.py ├── init_dirs.py ├── convert.py ├── breadcrumb.py ├── oss.py └── process.py ├── README.md ├── config ├── config_model.py └── config_control.py ├── LICENSE ├── page ├── maunal_upload.py ├── auto_upload.py ├── init_config.py └── settings.py ├── main.py └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | oss2~=2.18.6 2 | Pillow~=11.2.1 3 | pyperclip~=1.8.2 4 | PyYAML~=6.0.2 5 | inquirerpy~=0.3.4 6 | pydantic~=1.10.22 7 | pillow_heif~=0.22.0 -------------------------------------------------------------------------------- /utils/clear_screen.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def clear_screen() -> None: 5 | """ 6 | 根据系统类型,执行对应终端清屏指令。 7 | :return: 8 | """ 9 | os.system("cls" if os.name == "nt" else "clear") 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blog-Image-Uploader 2 | 3 | 博客图片转码、重命名、上传 OSS 一网打尽。 4 | 5 | 由于我的博客使用阿里云 OSS + CDN 托管图片,每次写博客如果要上传图片,都要手动转码图片、上传 OSS、复制链接。 6 | 7 | 这操作又麻烦又浪费时间,因此随手摸了个程序解决这些问题。如果你和我有一样的困扰,可以试下我这渣作。 8 | 9 | 安装模块: `pip install -r requirements.txt` 10 | -------------------------------------------------------------------------------- /utils/init_dirs.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from config.config_control import config 4 | 5 | 6 | def init_dirs() -> bool: 7 | try: 8 | original_dir = config["upload"]["local_original_dir"] 9 | trans_dir = config["upload"]["local_trans_dir"] 10 | os.makedirs(original_dir, exist_ok=True) 11 | os.makedirs(trans_dir, exist_ok=True) 12 | return True 13 | except: 14 | return False 15 | -------------------------------------------------------------------------------- /utils/convert.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from PIL import Image 4 | from pillow_heif import register_heif_opener 5 | 6 | register_heif_opener() 7 | 8 | 9 | def image_convert(original_file: str, converted_file: str) -> None: 10 | """ 11 | 将图片转换为对应格式,格式取决于参数中的后缀名 12 | :param original_file: 原图路径 13 | :param converted_file: 转换后图片路径 14 | """ 15 | new_suffix = os.path.splitext(converted_file)[-1][1:] # 新图片后缀(不带点) 16 | img = Image.open(original_file) 17 | img.save(converted_file, new_suffix) 18 | -------------------------------------------------------------------------------- /config/config_model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class ConfigModel(BaseModel): 5 | class Auth(BaseModel): 6 | id: str = "" 7 | secret: str = "" 8 | 9 | class Bucket(BaseModel): 10 | name: str = "" 11 | endpoint: str = "" 12 | public_link: str = "" 13 | 14 | class Upload(BaseModel): 15 | bucket_dir: str = "" 16 | local_original_dir: str = "Images/Original/" 17 | local_trans_dir: str = "Images/Transcoded/" 18 | trans_format: str = "webp" 19 | use_move: bool = False 20 | 21 | class Rename(BaseModel): 22 | post_id: str = "0001" 23 | img_id: str = "01" 24 | 25 | auth: Auth = Auth() 26 | bucket: Bucket = Bucket() 27 | upload: Upload = Upload() 28 | rename: Rename = Rename() 29 | -------------------------------------------------------------------------------- /utils/breadcrumb.py: -------------------------------------------------------------------------------- 1 | from InquirerPy.utils import color_print 2 | 3 | breadcrumb_list = [] 4 | 5 | 6 | def print_breadcrumb(sep: str = ">", show_prefix: bool = True) -> None: 7 | """ 8 | 向屏幕打印面包屑导航 9 | :param sep: 分隔符(无需空格) 10 | :param show_prefix: 是否作为前缀显示 11 | :return: 12 | """ 13 | color_print([("#61AFEF", "Enter: "), ("", "确定, "), 14 | ("#61AFEF", "↑ ↓: "), ("", "选择, "), 15 | ("#61AFEF", "Ctrl+C: "), ("", "取消, "), ]) 16 | prt_list = [] 17 | first = True 18 | for bc in breadcrumb_list: 19 | if first: 20 | first = False 21 | if show_prefix: 22 | prt_list.append(("#98C379", f"{sep} ")) 23 | prt_list.append(("#E5C07B", bc)) 24 | continue 25 | prt_list.append(("#98C379", f" {sep} ")) 26 | prt_list.append(("#E5C07B", bc)) 27 | color_print(prt_list) 28 | print() 29 | 30 | 31 | def push_breadcrumb(breadcrumb: str) -> None: 32 | breadcrumb_list.append(breadcrumb) 33 | 34 | 35 | def pop_breadcrumb() -> None: 36 | breadcrumb_list.pop() 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Haotian Zou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/oss.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import oss2 4 | 5 | from config.config_control import config 6 | 7 | oss = [] 8 | 9 | 10 | def init_oss() -> bool: 11 | try: 12 | auth = oss2.Auth(config["auth"]["id"], config["auth"]["secret"]) # 初始化身份验证 13 | bucket = oss2.Bucket(auth, config["bucket"]["endpoint"], config["bucket"]["name"]) # 初始化Bucket 14 | except: 15 | return False 16 | oss.append(auth) 17 | oss.append(bucket) 18 | return True 19 | 20 | 21 | def test_bucket() -> bool: 22 | """ 23 | OSS测试,将会进行一次上传、下载操作,操作后会删除测试文件 24 | :return: 25 | """ 26 | auth, bucket = oss 27 | try: 28 | bucket.put_object("BUCKET_TEST_FILE", b"Test File") 29 | test_object = bucket.get_object("BUCKET_TEST_FILE") 30 | bucket.delete_object("BUCKET_TEST_FILE") 31 | except: 32 | return False 33 | return test_object.read() == b"Test File" 34 | 35 | 36 | def upload_file(local_file: str) -> str: 37 | """ 38 | OSS上传文件 39 | :param local_file: 本地文件的路径 40 | :return: 上传对象的OSS链接 41 | """ 42 | auth, bucket = oss 43 | file_name = os.path.basename(local_file) 44 | bucket.put_object_from_file( 45 | key=os.path.join(config["upload"]["bucket_dir"], file_name), 46 | filename=local_file, 47 | headers={"Content-Type": f"image/{config['upload']['trans_format']}"} 48 | ) 49 | return config["bucket"]["public_link"] + "/" + config["upload"]["bucket_dir"] + file_name 50 | -------------------------------------------------------------------------------- /config/config_control.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Union 3 | 4 | import pydantic.error_wrappers 5 | import yaml 6 | 7 | from config.config_model import ConfigModel 8 | from page.init_config import init_config_page 9 | 10 | 11 | def load_config() -> Union[None, dict]: 12 | if not os.path.exists("config.yaml"): 13 | save_config(init_config_page()) 14 | try: 15 | with open("config.yaml", "r") as config_file: 16 | _config = yaml.safe_load(config_file) 17 | except: 18 | return None 19 | try: 20 | validated_config = ConfigModel(**_config).dict() 21 | return validated_config 22 | except pydantic.error_wrappers.ValidationError as e: 23 | return None 24 | 25 | 26 | def save_config(_config: dict) -> None: 27 | try: 28 | with open("config.yaml", "w") as config_file: 29 | yaml.safe_dump(_config, config_file, sort_keys=False) 30 | except: 31 | print("配置文件保存失败") 32 | 33 | 34 | def update_config(edited_config: dict) -> None: 35 | global config 36 | save_config(edited_config) 37 | config.update(**edited_config) 38 | 39 | 40 | def increase_id() -> None: 41 | global config 42 | img_id = int(config["rename"]["img_id"]) + 1 43 | config["rename"]["img_id"] = "{:02d}".format(img_id) 44 | save_config(config) 45 | 46 | 47 | def init_config() -> bool: 48 | cfg = load_config() 49 | if cfg is None: 50 | return False 51 | config.update(**cfg) 52 | return True 53 | 54 | 55 | config: dict = {} 56 | -------------------------------------------------------------------------------- /page/maunal_upload.py: -------------------------------------------------------------------------------- 1 | import pyperclip 2 | from InquirerPy import inquirer 3 | from InquirerPy.utils import color_print 4 | from InquirerPy.validator import PathValidator 5 | 6 | from config.config_control import config 7 | from utils.breadcrumb import print_breadcrumb 8 | from utils.clear_screen import clear_screen 9 | from utils.process import process 10 | 11 | 12 | def manual_upload() -> None: 13 | while True: 14 | clear_screen() 15 | print_breadcrumb() 16 | 17 | # 获取上传后的文件名 18 | try: 19 | dest_name_without_ext = inquirer.text( 20 | message="输入目的文件名(不包含后缀):", 21 | validate=lambda result: len(result) > 0, 22 | invalid_message="文件名不可为空", 23 | ).execute() 24 | except KeyboardInterrupt: 25 | return 26 | 27 | dest_bucket_name = config["bucket"]["name"] 28 | dest_bucket_dir = config["upload"]["bucket_dir"] 29 | dest_name = dest_name_without_ext + "." + config["upload"]["trans_format"] 30 | 31 | # 获取本地原图路径 32 | try: 33 | src_path = inquirer.filepath( 34 | message="请输入文件路径:", 35 | validate=PathValidator(is_file=True, message="文件路径错误"), 36 | ).execute() 37 | except KeyboardInterrupt: 38 | return 39 | 40 | print(f"本地路径: {src_path}") 41 | print(f"目的路径: oss://{dest_bucket_name}/{dest_bucket_dir + dest_name}") 42 | 43 | public_link = process(src_path, dest_name_without_ext) 44 | if len(public_link): 45 | color_print([("green", "[上传成功] "), ("orange", "图片链接: "), ("blue", public_link)]) 46 | pyperclip.copy(public_link) 47 | color_print([("orange", "(图片链接已复制)")]) 48 | else: 49 | color_print([("red", "[上传失败] "), ("orange", "图片上传失败")]) 50 | 51 | input("按回车继续") 52 | -------------------------------------------------------------------------------- /page/auto_upload.py: -------------------------------------------------------------------------------- 1 | import pyperclip 2 | from InquirerPy import inquirer 3 | from InquirerPy.utils import color_print 4 | from InquirerPy.validator import PathValidator 5 | 6 | from config.config_control import config, increase_id 7 | from utils.breadcrumb import print_breadcrumb, push_breadcrumb, pop_breadcrumb 8 | from utils.clear_screen import clear_screen 9 | from utils.process import process 10 | 11 | 12 | def auto_upload() -> None: 13 | while True: 14 | clear_screen() 15 | print_breadcrumb() 16 | 17 | dest_bucket_name = config["bucket"]["name"] 18 | dest_bucket_dir = config["upload"]["bucket_dir"] 19 | dest_name_without_ext = config["rename"]["post_id"] + "-" + config["rename"]["img_id"] 20 | dest_name = dest_name_without_ext + "." + config["upload"]["trans_format"] 21 | print(f"目的位置: oss://{dest_bucket_name}/{dest_bucket_dir + dest_name}") 22 | 23 | # 获取本地原图路径 24 | try: 25 | src_path = inquirer.filepath( 26 | message="请输入文件路径:", 27 | validate=PathValidator(is_file=True, message="文件路径错误"), 28 | ).execute() 29 | except KeyboardInterrupt: 30 | return 31 | 32 | clear_screen() 33 | push_breadcrumb("开始上传") 34 | print_breadcrumb() 35 | pop_breadcrumb() 36 | 37 | print(f"本地路径: {src_path}") 38 | print(f"目的路径: oss://{dest_bucket_name}/{dest_bucket_dir + dest_name}") 39 | 40 | public_link = process(src_path, dest_name_without_ext) 41 | if len(public_link): 42 | increase_id() 43 | color_print([("green", "[上传成功] "), ("orange", "图片链接: "), ("blue", public_link)]) 44 | pyperclip.copy(public_link) 45 | color_print([("orange", "(图片链接已复制)")]) 46 | else: 47 | color_print([("red", "[上传失败] "), ("orange", "图片上传失败")]) 48 | 49 | input("按回车继续") 50 | -------------------------------------------------------------------------------- /page/init_config.py: -------------------------------------------------------------------------------- 1 | from InquirerPy import inquirer 2 | 3 | from config.config_model import ConfigModel 4 | 5 | 6 | def init_config_page() -> dict: 7 | new_config = ConfigModel().dict() 8 | try: 9 | print("\n> 配置文件初始化") 10 | #################################### 11 | print("> [1] 账号设置") 12 | key_id = inquirer.text( 13 | message="Access Key ID:", 14 | validate=lambda result: len(result) > 0, 15 | invalid_message="ID不可为空", 16 | ).execute() 17 | 18 | key_secret = inquirer.secret( 19 | message="Access Key Secret:", 20 | validate=lambda result: len(result) > 0, 21 | invalid_message="Secret不可为空", 22 | ).execute() 23 | new_config["auth"]["id"] = key_id 24 | new_config["auth"]["secret"] = key_secret 25 | #################################### 26 | print("> [2] 储存桶设置") 27 | bucket_name = inquirer.text( 28 | message="储存桶名称:", 29 | validate=lambda result: len(result) > 0, 30 | invalid_message="名称不可为空", 31 | ).execute() 32 | new_config["bucket"]["name"] = bucket_name 33 | 34 | bucket_endpoint = inquirer.text( 35 | message="Endpoint:", 36 | validate=lambda result: len(result) > 0, 37 | invalid_message="Endpoint不可为空", 38 | ).execute() 39 | new_config["bucket"]["endpoint"] = bucket_endpoint 40 | 41 | bucket_public_link = inquirer.text( 42 | message="储存桶公网链接(如果没有请保持默认):", 43 | default=bucket_name + "." + bucket_endpoint, 44 | validate=lambda result: len(result) > 0, 45 | invalid_message="公网链接不可为空", 46 | ).execute() 47 | bucket_public_link = bucket_public_link.removesuffix("/") 48 | new_config["bucket"]["public_link"] = bucket_public_link 49 | #################################### 50 | print("> 设置完成,更多设置请前往菜单") 51 | input("按回车继续") 52 | return new_config 53 | except KeyboardInterrupt: 54 | exit(0) 55 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from InquirerPy import inquirer 2 | from InquirerPy.base.control import Choice 3 | from InquirerPy.utils import color_print, patched_print 4 | 5 | from config.config_control import init_config 6 | from page.auto_upload import auto_upload 7 | from page.maunal_upload import manual_upload 8 | from page.settings import settings 9 | from utils.breadcrumb import print_breadcrumb, push_breadcrumb, pop_breadcrumb 10 | from utils.clear_screen import clear_screen 11 | from utils.init_dirs import init_dirs 12 | from utils.oss import test_bucket, init_oss 13 | 14 | 15 | def init(): 16 | try: 17 | flag = True 18 | print("读取配置文件...", end=" ") 19 | flag &= init_config() 20 | print("完成" if flag else "错误") 21 | if not flag: 22 | raise 23 | print("初始化图片目录...", end=" ") 24 | flag &= init_dirs() 25 | print("完成" if flag else "错误") 26 | if not flag: 27 | raise 28 | print("初始化OSS...", end=" ") 29 | flag &= init_oss() 30 | print("完成" if flag else "错误") 31 | if not flag: 32 | raise 33 | print("测试OSS...", end=" ") 34 | flag &= test_bucket() 35 | print("完成" if flag else "错误") 36 | if not flag: 37 | raise 38 | except: 39 | color_print([("red", "[!] 程序初始化出现错误,请检查配置")]) 40 | input("按任意键继续...") 41 | 42 | 43 | def main(): 44 | push_breadcrumb("首页") 45 | while True: 46 | clear_screen() 47 | print_breadcrumb() 48 | try: 49 | action = inquirer.select( 50 | message="请选择操作:", 51 | choices=[ 52 | Choice(value=0, name="自动模式"), 53 | Choice(value=1, name="手动模式"), 54 | Choice(value=2, name="设置"), 55 | Choice(value=-1, name="退出"), 56 | ], 57 | default=0, 58 | ).execute() 59 | except KeyboardInterrupt: 60 | exit(0) 61 | if action == 0: 62 | push_breadcrumb("自动模式") 63 | auto_upload() 64 | pop_breadcrumb() 65 | elif action == 1: 66 | push_breadcrumb("手动模式") 67 | manual_upload() 68 | pop_breadcrumb() 69 | elif action == 2: 70 | push_breadcrumb("设置") 71 | settings() 72 | pop_breadcrumb() 73 | else: 74 | exit(0) 75 | 76 | 77 | if __name__ == "__main__": 78 | init() 79 | main() 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | Images/Original/* 3 | Images/Transcoded/* 4 | run.bat 5 | config.yaml 6 | 7 | ### Python template 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | -------------------------------------------------------------------------------- /utils/process.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from config.config_control import config 5 | from utils.convert import image_convert 6 | from utils.oss import upload_file 7 | 8 | 9 | def check_source_dir(source_dir: str) -> str: 10 | """ 11 | 预处理并检查目录 12 | :param source_dir: 目录 13 | :return: 目录,若空则异常 14 | """ 15 | source_dir = source_dir.strip() # 去除首尾多余空格 16 | source_dir = source_dir.replace("\\", "/") # 替换\为/ 17 | source_dir = source_dir.strip("\'").strip("\"") # 删除多余引号 18 | if not os.path.exists(source_dir): 19 | return "" 20 | return source_dir 21 | 22 | 23 | def move_and_rename(source_dir: str, dest_name_without_ext: str) -> str: 24 | """ 25 | 将原图储存并重命名 26 | :param source_dir: 源文件位置 27 | :param dest_name_without_ext: 目的文件名(不含后缀) 28 | :return: 目的文件,若空则异常 29 | """ 30 | original_ext = os.path.splitext(source_dir)[-1] # 图片后缀名 31 | renamed_dir = os.path.join(config["upload"]["local_original_dir"], dest_name_without_ext + original_ext) # 新图片路径 32 | try: 33 | if config["upload"]["use_move"]: 34 | shutil.move(source_dir, renamed_dir) # 移动文件 35 | else: 36 | shutil.copy(source_dir, renamed_dir) # 复制文件 37 | return renamed_dir 38 | except: 39 | return "" 40 | 41 | 42 | def format_convert(dest_name_without_ext: str, renamed_dir: str) -> str: 43 | """ 44 | 图片转码 45 | :param dest_name_without_ext: 目的文件名(不含后缀) 46 | :param renamed_dir: 目的文件路径 47 | :return: 转码文件,若空则异常 48 | """ 49 | converted_dir = os.path.join( 50 | config["upload"]["local_trans_dir"], 51 | dest_name_without_ext + "." + config["upload"]["trans_format"] 52 | ) # 转码后图片路径 53 | try: 54 | image_convert(renamed_dir, converted_dir) 55 | return converted_dir 56 | except: 57 | return "" 58 | 59 | 60 | def upload_oss(converted_dir: str) -> str: 61 | """ 62 | 将转码文件上传OSS 63 | :param converted_dir: 转码文件 64 | :return: 文件外链,若空则异常 65 | """ 66 | try: 67 | link = upload_file(converted_dir) 68 | return link 69 | except: 70 | return "" 71 | 72 | 73 | def process(source_dir: str, dest_name_without_ext: str) -> str: 74 | """ 75 | 完整的一次操作 76 | :param source_dir: 源文件位置 77 | :param dest_name_without_ext: 目的文件名(不含后缀) 78 | :return: 文件外链,若空则异常 79 | """ 80 | # 预处理并检查目录 81 | print("检查本地文件目录...", end=" ") 82 | source_dir = check_source_dir(source_dir) 83 | if len(source_dir) == 0: 84 | print("错误") 85 | return "" 86 | print("完成") 87 | 88 | # 储存并重命名 89 | print("储存并重命名原图...", end=" ") 90 | renamed_dir = move_and_rename(source_dir, dest_name_without_ext) 91 | if len(renamed_dir) == 0: 92 | print("错误") 93 | return "" 94 | print("完成") 95 | 96 | # 图片转码 97 | print("图片转码...", end=" ") 98 | converted_dir = format_convert(dest_name_without_ext, renamed_dir) 99 | if len(converted_dir) == 0: 100 | print("错误") 101 | return "" 102 | print("完成") 103 | 104 | # 图片上传 105 | print("上传OSS...", end=" ") 106 | public_link = upload_oss(converted_dir) 107 | if len(public_link) == 0: 108 | print("错误") 109 | return "" 110 | print("完成") 111 | 112 | return public_link 113 | -------------------------------------------------------------------------------- /page/settings.py: -------------------------------------------------------------------------------- 1 | import oss2 2 | from InquirerPy import inquirer 3 | from InquirerPy.base.control import Choice 4 | from InquirerPy.separator import Separator 5 | from InquirerPy.validator import PathValidator 6 | 7 | from config.config_control import config, update_config 8 | from utils.breadcrumb import print_breadcrumb, push_breadcrumb, pop_breadcrumb 9 | from utils.clear_screen import clear_screen 10 | 11 | 12 | def settings() -> None: 13 | while True: 14 | clear_screen() 15 | print_breadcrumb() 16 | edited_config = config 17 | try: 18 | action = inquirer.select( 19 | message="选择设置项目:", 20 | choices=[ 21 | Choice(value=0, name="账号设置"), 22 | Choice(value=1, name="储存桶设置"), 23 | Choice(value=2, name="上传设置"), 24 | Choice(value=3, name="命名设置"), 25 | Separator(), 26 | Choice(value=-2, name="返回"), 27 | Choice(value=-1, name="保存并返回"), 28 | ], 29 | default=None, 30 | ).execute() 31 | except KeyboardInterrupt: 32 | return 33 | if action == -2: 34 | return 35 | elif action == -1: 36 | update_config(edited_config) 37 | return 38 | elif action == 0: 39 | push_breadcrumb("账号设置") 40 | edited_config = auth_settings(edited_config) 41 | pop_breadcrumb() 42 | elif action == 1: 43 | push_breadcrumb("储存桶设置") 44 | edited_config = bucket_settings(edited_config) 45 | pop_breadcrumb() 46 | elif action == 2: 47 | push_breadcrumb("上传设置") 48 | edited_config = upload_settings(edited_config) 49 | pop_breadcrumb() 50 | elif action == 3: 51 | push_breadcrumb("命名设置") 52 | edited_config = raname_settings(edited_config) 53 | pop_breadcrumb() 54 | 55 | 56 | def auth_settings(edited_config: dict) -> dict: 57 | while True: 58 | clear_screen() 59 | print_breadcrumb() 60 | try: 61 | action = inquirer.select( 62 | message="选择设置项目:", 63 | choices=[ 64 | Choice(value=0, name="Access Key ID"), 65 | Choice(value=1, name="Access Key Secret"), 66 | Separator(), 67 | Choice(value=-1, name="返回"), 68 | ], 69 | ).execute() 70 | except KeyboardInterrupt: 71 | return edited_config 72 | if action == -1: 73 | return edited_config 74 | elif action == 0: 75 | clear_screen() 76 | push_breadcrumb("Access Key ID") 77 | print_breadcrumb() 78 | pop_breadcrumb() 79 | try: 80 | key_id = inquirer.text( 81 | message="Access Key ID:", 82 | validate=lambda result: len(result) > 0, 83 | invalid_message="ID不可为空", 84 | default=edited_config["auth"]["id"] 85 | ).execute() 86 | except KeyboardInterrupt: 87 | continue 88 | edited_config["auth"]["id"] = key_id 89 | elif action == 1: 90 | clear_screen() 91 | push_breadcrumb("Access Key Secret") 92 | print_breadcrumb() 93 | pop_breadcrumb() 94 | try: 95 | key_secret = inquirer.secret( 96 | message="Access Key Secret:", 97 | validate=lambda result: len(result) > 0, 98 | invalid_message="Secret不可为空", 99 | default=edited_config["auth"]["secret"] 100 | ).execute() 101 | edited_config["auth"]["secret"] = key_secret 102 | except KeyboardInterrupt: 103 | continue 104 | 105 | 106 | def bucket_settings(edited_config: dict) -> dict: 107 | while True: 108 | clear_screen() 109 | print_breadcrumb() 110 | try: 111 | action = inquirer.select( 112 | message="选择设置项目:", 113 | choices=[ 114 | Choice(value=0, name=f"储存桶名称: {edited_config['bucket']['name']}"), 115 | Choice(value=1, name=f"Endpoint: {edited_config['bucket']['endpoint']}"), 116 | Choice(value=2, name=f"公网链接: {edited_config['bucket']['public_link']}"), 117 | Separator(), 118 | Choice(value=-1, name="返回"), 119 | ], 120 | ).execute() 121 | except KeyboardInterrupt: 122 | return edited_config 123 | if action == -1: 124 | return edited_config 125 | elif action == 0: 126 | clear_screen() 127 | push_breadcrumb("储存桶名称") 128 | print_breadcrumb() 129 | pop_breadcrumb() 130 | try: 131 | bucket_name = inquirer.text( 132 | message="储存桶名称:", 133 | validate=lambda result: len(result) > 0, 134 | invalid_message="名称不可为空", 135 | default=edited_config["bucket"]["name"] 136 | ).execute() 137 | except KeyboardInterrupt: 138 | continue 139 | edited_config["bucket"]["name"] = bucket_name 140 | elif action == 1: 141 | clear_screen() 142 | push_breadcrumb("Endpoint") 143 | print_breadcrumb() 144 | pop_breadcrumb() 145 | try: 146 | bucket_endpoint = inquirer.text( 147 | message="Endpoint:", 148 | validate=lambda result: len(result) > 0, 149 | invalid_message="Endpoint不可为空", 150 | default=edited_config["bucket"]["endpoint"] 151 | ).execute() 152 | edited_config["bucket"]["endpoint"] = bucket_endpoint 153 | except KeyboardInterrupt: 154 | continue 155 | elif action == 2: 156 | clear_screen() 157 | push_breadcrumb("公网链接") 158 | print_breadcrumb() 159 | pop_breadcrumb() 160 | try: 161 | bucket_public_link = inquirer.text( 162 | message="储存桶公网链接(如果没有请保持默认):", 163 | default=edited_config["bucket"]["public_link"], 164 | validate=lambda result: len(result) > 0, 165 | invalid_message="公网链接不可为空", 166 | ).execute() 167 | except KeyboardInterrupt: 168 | continue 169 | bucket_public_link.removesuffix("/") 170 | edited_config["bucket"]["public_link"] = bucket_public_link 171 | try: 172 | oss2.Bucket( 173 | oss2.Auth( 174 | edited_config["auth"]["key"], 175 | edited_config["auth"]["secret"] 176 | ), 177 | edited_config["bucket"]["endpoint"], 178 | edited_config["bucket"]["name"] 179 | ) 180 | except oss2.exceptions.ClientError: 181 | print("OSS测试失败,请检查配置是否正确") 182 | 183 | 184 | def upload_settings(edited_config: dict) -> dict: 185 | while True: 186 | clear_screen() 187 | print_breadcrumb() 188 | try: 189 | action = inquirer.select( 190 | message="选择设置项目:", 191 | choices=[ 192 | Choice(value=0, name=f"远程上传路径: {edited_config['upload']['bucket_dir']}"), 193 | Choice(value=1, name=f"本地原图路径: {edited_config['upload']['local_original_dir']}"), 194 | Choice(value=2, name=f"本地转码路径: {edited_config['upload']['local_trans_dir']}"), 195 | Choice(value=3, name=f"转码格式: {edited_config['upload']['trans_format']}"), 196 | Choice(value=4, name=f"移动图片: {'是' if edited_config['upload']['use_move'] else '否'}"), 197 | Separator(), 198 | Choice(value=-1, name="返回"), 199 | ], 200 | ).execute() 201 | except KeyboardInterrupt: 202 | return edited_config 203 | if action == -1: 204 | return edited_config 205 | elif action == 0: 206 | clear_screen() 207 | push_breadcrumb("远程上传路径") 208 | print_breadcrumb() 209 | pop_breadcrumb() 210 | try: 211 | bucket_dir: str = inquirer.text( 212 | message="远程上传路径:", 213 | default=edited_config['upload']['bucket_dir'], 214 | ).execute() 215 | except KeyboardInterrupt: 216 | continue 217 | if bucket_dir.startswith("/"): 218 | bucket_dir = bucket_dir.removeprefix("/") 219 | edited_config['upload']['bucket_dir'] = bucket_dir 220 | elif action == 1: 221 | clear_screen() 222 | push_breadcrumb("本地原图路径") 223 | print_breadcrumb() 224 | pop_breadcrumb() 225 | try: 226 | local_original_dir = inquirer.filepath( 227 | message="本地原图路径:", 228 | validate=PathValidator(is_dir=True, message="路径不合法"), 229 | default=edited_config['upload']['local_original_dir'], 230 | only_directories=True 231 | ).execute() 232 | except KeyboardInterrupt: 233 | continue 234 | local_original_dir: str = local_original_dir.strip().replace("\\", "/") 235 | if not local_original_dir.endswith("/"): 236 | local_original_dir += "/" 237 | edited_config['upload']['local_original_dir'] = local_original_dir 238 | elif action == 2: 239 | clear_screen() 240 | push_breadcrumb("本地转码路径") 241 | print_breadcrumb() 242 | pop_breadcrumb() 243 | try: 244 | local_trans_dir = inquirer.filepath( 245 | message="本地转码路径:", 246 | validate=PathValidator(is_dir=True, message="路径不合法"), 247 | default=edited_config['upload']['local_trans_dir'], 248 | only_directories=True 249 | ).execute() 250 | except KeyboardInterrupt: 251 | continue 252 | local_trans_dir: str = local_trans_dir.strip().replace("\\", "/") 253 | if not local_trans_dir.endswith("/"): 254 | local_trans_dir += "/" 255 | edited_config['upload']['local_trans_dir'] = local_trans_dir 256 | elif action == 3: 257 | clear_screen() 258 | push_breadcrumb("转码格式") 259 | print_breadcrumb() 260 | pop_breadcrumb() 261 | try: 262 | trans_format: str = inquirer.text( 263 | message="转码格式:", 264 | default=edited_config['upload']['trans_format'], 265 | validate=lambda result: len(result) > 0, 266 | invalid_message="格式不可为空", 267 | ).execute() 268 | except KeyboardInterrupt: 269 | continue 270 | edited_config['upload']['trans_format'] = trans_format 271 | elif action == 4: 272 | clear_screen() 273 | push_breadcrumb("移动图片") 274 | print_breadcrumb() 275 | pop_breadcrumb() 276 | try: 277 | use_move: bool = inquirer.select( 278 | message="使用移动代替复制:", 279 | choices=[ 280 | Choice(value=True, name="是"), 281 | Choice(value=False, name="否"), 282 | ], 283 | default=edited_config['upload']['use_move'], 284 | ).execute() 285 | except KeyboardInterrupt: 286 | continue 287 | edited_config['upload']['use_move'] = use_move 288 | 289 | 290 | def raname_settings(edited_config: dict) -> dict: 291 | while True: 292 | clear_screen() 293 | print_breadcrumb() 294 | try: 295 | action = inquirer.select( 296 | message="选择设置项目:", 297 | choices=[ 298 | Choice(value=0, name=f"文章ID: {edited_config['rename']['post_id']}"), 299 | Choice(value=1, name=f"图片ID: {edited_config['rename']['img_id']}"), 300 | Separator(), 301 | Choice(value=-1, name="返回"), 302 | ], 303 | ).execute() 304 | except KeyboardInterrupt: 305 | return edited_config 306 | if action == -1: 307 | return edited_config 308 | elif action == 0: 309 | clear_screen() 310 | push_breadcrumb("文章ID") 311 | print_breadcrumb() 312 | pop_breadcrumb() 313 | try: 314 | post_id = inquirer.text( 315 | message="文章ID:", 316 | validate=lambda result: len(result) > 0, 317 | invalid_message="ID不可为空", 318 | default=edited_config["rename"]["post_id"] 319 | ).execute() 320 | except KeyboardInterrupt: 321 | continue 322 | edited_config["rename"]["post_id"] = post_id 323 | elif action == 1: 324 | clear_screen() 325 | push_breadcrumb("图片ID") 326 | print_breadcrumb() 327 | pop_breadcrumb() 328 | try: 329 | img_id = inquirer.text( 330 | message="图片ID:", 331 | validate=lambda result: len(result) > 0, 332 | invalid_message="ID不可为空", 333 | default=edited_config["rename"]["img_id"] 334 | ).execute() 335 | except KeyboardInterrupt: 336 | continue 337 | edited_config["rename"]["img_id"] = img_id 338 | --------------------------------------------------------------------------------