commit 86266646533e2345ff214884c8d3103d085159c8 Author: youmetme <321640253@qq.com> Date: Thu Oct 3 18:23:21 2024 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a592909 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ + +# 千手 +一款计划备份工具 + + diff --git a/api/api.py b/api/api.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/api.py @@ -0,0 +1 @@ + diff --git a/app.py b/app.py new file mode 100644 index 0000000..ef0fa1c --- /dev/null +++ b/app.py @@ -0,0 +1,12 @@ +from flask import Flask + +app = Flask(__name__, static_folder="./web/dist/", static_url_path="") + + +@app.route("/") +def main(): + return app.send_static_file("index.html") + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/objects/backup_object.py b/core/objects/backup_object.py new file mode 100644 index 0000000..e322e05 --- /dev/null +++ b/core/objects/backup_object.py @@ -0,0 +1,67 @@ +""" +******************************************** +* @Date: 2024 09 27 +* @Description: BackupObject备份对象 +******************************************** +""" + +import os +from typing import List, Union +from .tree_object import TreeObject +from .blob_object import BlobObject + + +class BackupObject(ObjectBase): + """ + 备份对象,负责管理存储对象(tree、blob)对象、备份信息、恢复等操作 + """ + + backup_name: str + backup_time_lately: str + backup_time_create: str + backup_size: int + backup_version_number: int + backup_tree: List[Union[TreeObject, BlobObject]] + new_backup_flag: bool + + def __init__(self, backup_name: str, backup_base_path: str): + self.backup_name = backup_name + + backup_path = os.path.join(backup_base_path, backup_name) + if not os.path.exists(backup_path): + self.new_backup_flag = True + else: + self.new_backup_flag = False + pass #TODO 读取备份信息 + + + def createNewBackup(self, backup_dirs: List[str]): + """ + 创建新的备份 + + :return + """ + for backup_dir in backup_dirs: + if os.path.isdir(backup_dir): + self.backup_tree.append(TreeObject(backup_dir)) + else: + self.backup_tree.append(BlobObject(backup_dir)) + + def backup(self): + pass + + def recover(self, recover_path: str): + """ + 恢复到指定备份 + + :return + """ + pass + + def getBackupTree(self): + """ + 获取备份树 + + :return + """ + pass diff --git a/core/objects/blob_object.py b/core/objects/blob_object.py new file mode 100644 index 0000000..7deb68e --- /dev/null +++ b/core/objects/blob_object.py @@ -0,0 +1,70 @@ +""" +******************************************** +* @Date: 2024 09 27 +* @Description: BlobObject存储对象 +******************************************** +""" + +import hashlib +import os +import zlib + + +class BlobObject: + object_id: str + __buff_size: int = 8192 + + def __init__(self, file_path: str) -> None: + self.file_path = file_path + self.object_id = self.contentSha1() + + def writeBlob(self, base_path) -> None: + Folder = base_path + "/" + self.__getFolderName() + if not os.path.exists(Folder): + os.makedirs(Folder) + self.__compressFile( + self.file_path, + base_path + "/" + self.__getFolderName() + "/" + self.__getFileName(), + ) + + def __compressFile(self, file_path, save_path) -> None: + compresser = zlib.compressobj(9) + write_file = open(save_path, "wb") + with open(file_path, "rb") as f: + while True: + data = f.read(self.__buff_size) + if not data: + break + compressedData = compresser.compress(data) + write_file.write(compressedData) + write_file.write(compresser.flush()) + write_file.close() + + def __getFolderName(self) -> str: + return self.object_id[:2] + + def __getFileName(self) -> str: + return self.object_id[2:] + + def getBlobAbsPath(self) -> str: + """ + 获取blob的绝对路径,此相对路径是基于存储路径的,不是相对当前文件的路径 + + :return 相对路径,格式为xx/xxxx... + """ + return self.__getFolderName() + "/" + self.__getFileName() + + def contentSha1(self) -> str: + """ + 计算文件内容的sha1值 + + :return sha1的hex值 + """ + with open(self.file_path, "rb") as f: + sha1 = hashlib.sha1() + while True: + data = f.read(self.__buff_size) + if not data: + break + sha1.update(data) + return sha1.hexdigest() diff --git a/core/objects/tree_object.py b/core/objects/tree_object.py new file mode 100644 index 0000000..cc40aa4 --- /dev/null +++ b/core/objects/tree_object.py @@ -0,0 +1,31 @@ +""" +******************************************** +* @Date: 2024 09 27 +* @Description: BackObject树对象,标记多个存储存储对象和树对象,为备份对象提供目标 +******************************************** +""" + +from typing import Dict, List +from .blob_object import BlobObject +import os + + +class TreeObject: + __children: List[BlobObject] = [] # 备份树节点列表,节点为BlobObject备份存储对象 + __file_map: Dict[str, str] = {} # 备份文件路径到blob对象的映射 + + def __init__(self, tree_path: str): + self.tree_path = tree_path + self.__loadChildren() + + def __loadChildren(self): + for root, dirs, files in os.walk(self.tree_path): + for name in files: + blob_path = os.path.join(root, name) + blob = BlobObject(blob_path) + self.__children.append(blob) + self.__file_map[blob_path] = blob.object_id + + def writeBlobs(self, base_path: str): + for child in self.__children: + child.writeBlob(base_path) diff --git a/core/plan/Plan.py b/core/plan/Plan.py new file mode 100644 index 0000000..9338e2c --- /dev/null +++ b/core/plan/Plan.py @@ -0,0 +1,8 @@ +class Plan: + __priority_factor: float # 优先级因子 + + def __init__(self): + pass + + def getPriorityFactor(self) -> float: + return self.__priority_factor diff --git a/core/plan/PlanMinHeap.py b/core/plan/PlanMinHeap.py new file mode 100644 index 0000000..4ba93e4 --- /dev/null +++ b/core/plan/PlanMinHeap.py @@ -0,0 +1,15 @@ +from typing import List +from .Plan import Plan + + +class PlanPriorityMinHeap: + """ + 计划优先小顶堆,按照优先级存储多个计划 + """ + + plans: List[Plan] + def __init__(self): + self.plans = [] + + def addPlan(self, plan: Plan): + pass diff --git a/core/plan/__init__.py b/core/plan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/target/__init__.py b/core/target/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/target/targetBase.py b/core/target/targetBase.py new file mode 100644 index 0000000..c68f1ea --- /dev/null +++ b/core/target/targetBase.py @@ -0,0 +1,25 @@ +""" +******************************************** +* @Date: 2024 09 27 +* @Description: TargetBase存储后端的抽象类 +******************************************** +""" + +from abc import ABC, abstractmethod +from core.objects.back_object import BackObject + + +class TargetBase(ABC): + @abstractmethod + def test(self) -> bool: + """ + 测试目标连通性 + + :param + :return + """ + pass + + @abstractmethod + def back(self, back_object: BackObject): + pass diff --git a/core/timer/__init__.py b/core/timer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/docs/开发指南.md b/docs/开发指南.md new file mode 100644 index 0000000..e69de29 diff --git a/makefile b/makefile new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0634ffb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "thousandhands" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "flask>=3.0.3", + "pytest>=8.3.3", +] + diff --git a/script/build.sh b/script/build.sh new file mode 100644 index 0000000..e69de29 diff --git a/test.py b/test.py new file mode 100644 index 0000000..5681f79 --- /dev/null +++ b/test.py @@ -0,0 +1,4 @@ +import pytest + +if __name__ == "__main__": + pytest.main(["tests/", "-v"]) diff --git a/tests/core/objects/test_back_object.py b/tests/core/objects/test_back_object.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/objects/test_blob_object.py b/tests/core/objects/test_blob_object.py new file mode 100644 index 0000000..61ee7a9 --- /dev/null +++ b/tests/core/objects/test_blob_object.py @@ -0,0 +1,17 @@ +from core.objects.blob_object import BlobObject +import hashlib +import tempfile + + +class Test_Blob: + + def test_newBlob(self): + newBlob = BlobObject("README.md") + assert ( + newBlob.object_id + == hashlib.sha1(open("README.md", "rb").read()).hexdigest() + ) + def test_writeBlob(self): + newBlob = BlobObject("README.md") + with tempfile.TemporaryDirectory() as tmpdirname: + newBlob.writeBlob(tmpdirname) \ No newline at end of file diff --git a/tests/core/objects/test_tree_object.py b/tests/core/objects/test_tree_object.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/log.py b/utils/log.py new file mode 100644 index 0000000..4392010 --- /dev/null +++ b/utils/log.py @@ -0,0 +1,6 @@ +import logging + + +class Logger: + def __init__(self, name): + self.logger = logging.getLogger(name) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..6a74674 --- /dev/null +++ b/uv.lock @@ -0,0 +1,156 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "blinker" +version = "1.8.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/57/a6a1721eff09598fb01f3c7cda070c1b6a0f12d63c83236edf79a440abcc/blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83", size = 23161 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", size = 9456 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "flask" +version = "3.0.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/e1/d104c83026f8d35dfd2c261df7d64738341067526406b40190bc063e829a/flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842", size = 676315 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", size = 101735 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "thousandhands" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "flask", specifier = ">=3.0.3" }, + { name = "pytest", specifier = ">=8.3.3" }, +] + +[[package]] +name = "werkzeug" +version = "3.0.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/e2/6dbcaab07560909ff8f654d3a2e5a60552d937c909455211b1b36d7101dc/werkzeug-3.0.4.tar.gz", hash = "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306", size = 803966 } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl", hash = "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", size = 227554 }, +] diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..8ee54e8 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/web/.prettierrc.js b/web/.prettierrc.js new file mode 100644 index 0000000..299a8f7 --- /dev/null +++ b/web/.prettierrc.js @@ -0,0 +1,22 @@ +module.exports = { + printWidth: 200, //单行长度 + tabWidth: 4, //缩进长度 + useTabs: false, //使用空格代替tab缩进 + semi: false, //句末使用分号 + singleQuote: true, //使用单引号 + quoteProps: 'as-needed', //仅在必需时为对象的key添加引号 + jsxSingleQuote: true, // jsx中使用单引号 + trailingComma: 'all', //多行时尽可能打印尾随逗号 + bracketSpacing: true, //在对象前后添加空格-eg: { foo: bar } + jsxBracketSameLine: true, //多属性html标签的‘>’折行放置 + arrowParens: 'always', //单参数箭头函数参数周围使用圆括号-eg: (x) => x + requirePragma: false, //无需顶部注释即可格式化 + insertPragma: false, //在已被preitter格式化的文件顶部加上标注 + proseWrap: 'preserve', //不知道怎么翻译 + htmlWhitespaceSensitivity: 'ignore', //对HTML全局空白不敏感 + vueIndentScriptAndStyle: false, //不对vue中的script及style标签缩进 + endOfLine: 'lf', //结束行形式 + embeddedLanguageFormatting: 'auto', //对引用代码进行格式化 + }; + + \ No newline at end of file diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..13a91a0 --- /dev/null +++ b/web/README.md @@ -0,0 +1,33 @@ +# web + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +bun install +``` + +### Compile and Hot-Reload for Development + +```sh +bun dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +bun build +``` diff --git a/web/bun.lockb b/web/bun.lockb new file mode 100755 index 0000000..a4e760b Binary files /dev/null and b/web/bun.lockb differ diff --git a/web/env.d.ts b/web/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..a888544 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..cbbdff1 --- /dev/null +++ b/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build --force" + }, + "dependencies": { + "vue": "^3.4.29" + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.14.5", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/tsconfig": "^0.5.1", + "element-plus": "^2.8.4", + "npm-run-all2": "^6.2.0", + "typescript": "~5.4.0", + "unplugin-auto-import": "^0.18.3", + "unplugin-vue-components": "^0.27.4", + "vite": "^5.3.1", + "vue-tsc": "^2.0.21" + } +} diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/web/public/favicon.ico differ diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..36272c3 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/web/src/assets/base.css b/web/src/assets/base.css new file mode 100644 index 0000000..8816868 --- /dev/null +++ b/web/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/web/src/assets/logo.svg b/web/src/assets/logo.svg new file mode 100644 index 0000000..7565660 --- /dev/null +++ b/web/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/web/src/assets/main.css b/web/src/assets/main.css new file mode 100644 index 0000000..36fb845 --- /dev/null +++ b/web/src/assets/main.css @@ -0,0 +1,35 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..0ac3a5f --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,6 @@ +import './assets/main.css' + +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..e14c754 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..66b5e57 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..f094063 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..5c45e1d --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,16 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +})