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 @@
+///