├── .github └── workflows │ ├── docker-publish.yml │ └── python-package.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.en.md ├── README.md ├── app.py ├── conf.yaml.default ├── install.sh ├── requirements.txt └── templates └── index.html /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | push: 10 | # Publish semver tags as releases. 11 | tags: [ '**' ] 12 | 13 | env: 14 | # Use docker.io for Docker Hub if empty 15 | REGISTRY: 'docker.io' 16 | # github.repository as / 17 | IMAGE_NAME: ${{ github.repository }} 18 | 19 | 20 | jobs: 21 | build: 22 | 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: read 26 | packages: write 27 | # This is used to complete the identity challenge 28 | # with sigstore/fulcio when running outside of PRs. 29 | id-token: write 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | # Install the cosign tool except on PR 36 | # https://github.com/sigstore/cosign-installer 37 | - name: Install cosign 38 | uses: sigstore/cosign-installer@v3.6.0 39 | with: 40 | cosign-release: 'v2.4.0' 41 | 42 | # Set up BuildKit Docker container builder to be able to build 43 | # multi-platform images and export cache 44 | # https://github.com/docker/setup-buildx-action 45 | - name: Set up Docker Buildx 46 | uses: docker/setup-buildx-action@v3 47 | 48 | # Login against a Docker registry except on PR 49 | # https://github.com/docker/login-action 50 | - name: Log into registry ${{ env.REGISTRY }} 51 | uses: docker/login-action@v3 52 | with: 53 | registry: ${{ env.REGISTRY }} 54 | username: ${{ secrets.DOCKERHUB_USERNAME }} 55 | password: ${{ secrets.DOCKERHUB_TOKEN }} 56 | 57 | # Extract metadata (tags, labels) for Docker 58 | # https://github.com/docker/metadata-action 59 | - name: Extract Docker metadata 60 | id: meta 61 | uses: docker/metadata-action@v5 62 | with: 63 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 64 | 65 | # Build and push Docker image with Buildx (don't push on PR) 66 | # https://github.com/docker/build-push-action 67 | - name: Build and push Docker image 68 | id: build-and-push 69 | uses: docker/build-push-action@v6 70 | with: 71 | context: . 72 | platforms: linux/amd64,linux/arm64 73 | push: true 74 | tags: ${{ steps.meta.outputs.tags }} 75 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | tags: [ '**' ] 6 | 7 | jobs: 8 | Linux-build-amd64: 9 | name: Build Linux Amd64 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: actions/setup-python@v5 16 | with: 17 | # 必须加'' 18 | python-version: '3.10' 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install wheel pyinstaller 24 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 25 | 26 | - name: Pyinstaller 27 | run: | 28 | pyinstaller --onefile --add-data "conf.yaml.default:." --add-data "templates:templates" --name vpspeek app.py 29 | 30 | - name: Verify generated file 31 | run: | 32 | ls -l dist/ 33 | 34 | - name: Upload Linux File 35 | uses: actions/upload-artifact@v3 36 | with: 37 | path: dist/vpspeek 38 | 39 | Create-release: 40 | permissions: write-all 41 | runs-on: ubuntu-latest 42 | needs: [ Linux-build-amd64 ] 43 | steps: 44 | - name: Download Linux File 45 | uses: actions/download-artifact@v3 46 | with: 47 | path: dist/ 48 | 49 | - name: Move downloaded file 50 | run: | 51 | mv dist/artifact/* dist/ 52 | 53 | - name: Verify file after move 54 | run: | 55 | ls dist/ 56 | 57 | - name: Create Release 58 | id: create_release 59 | uses: actions/create-release@v1 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }} 62 | with: 63 | tag_name: ${{ github.ref }} 64 | release_name: ${{ github.ref }} 65 | draft: false 66 | prerelease: false 67 | 68 | - name: Upload Release Asset 69 | uses: actions/upload-release-asset@v1 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }} 72 | with: 73 | upload_url: ${{ steps.create_release.outputs.upload_url }} 74 | asset_path: dist/vpspeek 75 | asset_name: vpspeek 76 | asset_content_type: application/octet-stream -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | 164 | # vpspeek 165 | data.json 166 | conf.yaml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build Stage 2 | FROM python:3.9-alpine AS build 3 | 4 | ENV TZ=Asia/Shanghai 5 | 6 | # Install system dependencies needed to compile Python packages 7 | RUN apk --no-cache add \ 8 | gcc \ 9 | musl-dev \ 10 | libffi-dev \ 11 | openssl-dev \ 12 | python3-dev \ 13 | make 14 | 15 | # Set the working directory 16 | WORKDIR /app 17 | 18 | # Copy the requirements.txt into the build stage 19 | COPY requirements.txt requirements.txt 20 | 21 | # Install Python dependencies in a temporary directory 22 | RUN pip install --no-cache-dir --target=/install -r requirements.txt 23 | 24 | # Stage 2: Final Image 25 | FROM python:3.9-alpine 26 | 27 | ENV TZ=Asia/Shanghai 28 | 29 | # Set the working directory 30 | WORKDIR /app 31 | 32 | # Install runtime dependencies only 33 | RUN apk --no-cache add curl 34 | 35 | # Copy the installed dependencies from the build stage 36 | COPY --from=build /install /usr/local/lib/python3.9/site-packages 37 | 38 | # Copy the rest of the application code 39 | COPY templates ./templates 40 | COPY app.py . 41 | COPY conf.yaml.default . 42 | 43 | # Define the default command to run the app 44 | CMD ["python", "app.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 vvnocode 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 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | ## Features 2 | 3 | - Supports both automatic and manual speed testing. 4 | - Can be configured to randomly select the next execution time within a specific time range, making the timing of tasks 5 | less predictable. 6 | - Allows querying and sorting of speed test results. 7 | - Supports different operation modes to prevent attacks. 8 | - One-click installation and upgrade are supported. 9 | - User timezone configuration is supported, making it more user-friendly for VPS deployments in different time zones. 10 | The default timezone is Asia/Shanghai. 11 | 12 | ### Interface 13 | 14 | Access via `ip:5000`. 15 | 16 | ![](https://s1.locimg.com/2024/09/18/bdb8e17c0bcd7.png) 17 | 18 | ### Resource Usage 19 | 20 | - Binary file size: 9M, with around 41M of memory usage during runtime. 21 | ![](https://s1.locimg.com/2024/09/18/ab84785aeb29f.png) 22 | 23 | - Docker image size: around 27M, with around 25M of memory usage during runtime. 24 | ![](https://s1.locimg.com/2024/09/16/b050a4d1e0127.png) 25 | 26 | ## Usage 27 | 28 | ### Installation 29 | 30 | - Install on a VPS to test international network speeds. 31 | - Install on NAS to test download speeds from a specified speed test address (configured in `conf.yaml`). 32 | 33 | #### Script Installation 34 | 35 | This script installs the precompiled binary file. It will automatically generate a `conf.yaml` configuration file and a 36 | `data.json` file for storing data. 37 | If you modify the `conf.yaml` file, you need to restart the service with `systemctl restart vpspeek`. 38 | 39 | ```shell 40 | curl -L https://raw.githubusercontent.com/vvnocode/vpspeek/master/install.sh -o vpspeek.sh && chmod +x vpspeek.sh && sudo ./vpspeek.sh 41 | ``` 42 | 43 | #### Using Docker Command Line 44 | 45 | ```shell 46 | docker run --name vpspeek -p 5000:5000 vvnocode/vpspeek:latest 47 | ``` 48 | 49 | Mapping file. 50 | 51 | ```shell 52 | docker run --name vpspeek -p 5000:5000 -v /mnt/user/appdata/vpspeek/vvnode/data.json:/app/data.json -v /mnt/user/appdata/vpspeek/vvnode/conf.yaml:/app/conf.yaml vvnocode/vpspeek:latest 53 | ``` 54 | 55 | #### Using Docker Compose 56 | 57 | ```yaml 58 | services: 59 | vpspeek: 60 | image: vvnocode/vpspeek:latest 61 | container_name: vpspeek 62 | ports: 63 | - "5000:5000" 64 | volumes: 65 | - /mnt/user/appdata/vpspeek/vvnode/data.json:/app/data.json 66 | - /mnt/user/appdata/vpspeek/vvnode/conf.yaml:/app/conf.yaml 67 | restart: unless-stopped 68 | ``` 69 | 70 | #### Notes 71 | 72 | If you are using Docker with file mapping, ensure that the mapped host files exist: 73 | 74 | 1. Create the `conf.yaml` file (it can be empty). 75 | 2. Create the `data.json` file (it can be empty). 76 | 77 | ### Configuration 78 | 79 | - The default configuration works out of the box. If needed, modify the `conf.yaml` file. 80 | - You can configure the speed test address; the default is Cloudflare's test server. 81 | - You can configure different modes in `conf.yaml`. The default mode is `default`. 82 | - `full`: Full functionality, with no security checks on API requests (password login will be added in the future). 83 | - `default`: No security checks, but only query operations are allowed. Manual speed testing is disabled. 84 | - `safe`: API requests require a validation key in the header, which is defined in the `conf.yaml`. 85 | - Under the default settings, the maximum daily download amount is calculated as `60 / max_interval * 24 * 100M`. For 86 | example, when `max_interval = 60`, the maximum daily download is 2400M. 87 | - Similarly, under extreme conditions, the maximum daily download is `60 / min_interval * 24 * 100M`. For example, when 88 | `min_interval = 30`, the maximum daily download is 4800M. 89 | - If configured appropriately, the tasks will be distributed across 24 hours and won't put too much pressure on the 90 | server. 91 | 92 | ## Development 93 | 94 | The speed test is done by using the `curl` command to download files of a specified size. 95 | 96 | ```shell 97 | curl -o /dev/null -s -w "%{size_download} %{time_total} %{speed_download}\n" 'https://speed.cloudflare.com/__down?during=download&bytes=104857600' 98 | ``` 99 | 100 | ### Development Environment 101 | 102 | - Python 3.9 103 | - PyCharm 104 | 105 | ### Build 106 | 107 | #### Building Binary 108 | 109 | Packaging command: 110 | 111 | ```shell 112 | pyinstaller --onefile --add-data "conf.yaml.default:." --add-data "templates:templates" --name vpspeek app.py 113 | ``` 114 | 115 | #### Building Docker Image 116 | 117 | ```shell 118 | # Building for amd64 and arm64 platforms separately 119 | # For linux/amd64: 120 | docker build --platform linux/amd64 -t vvnocode/vpspeek:0.3 . 121 | # Tag the image: 122 | docker tag vvnocode/vpspeek:0.3 vvnocode/vpspeek:latest 123 | # Push to registry: 124 | docker push vvnocode/vpspeek:0.3 125 | docker push vvnocode/vpspeek:latest 126 | 127 | # For linux/arm64: 128 | docker build --platform linux/arm64 -t vvnocode/vpspeek:0.3 . 129 | # Repeat tagging and pushing steps as above 130 | 131 | # Build for both amd64 and arm64 simultaneously (if supported by your system): 132 | docker buildx create --use 133 | docker buildx build --platform linux/amd64,linux/arm64 -t vvnocode/vpspeek:0.1 --load . 134 | # Tag the image: 135 | docker tag vvnocode/vpspeek:0.1 vvnocode/vpspeek:latest 136 | # Push to registry: 137 | docker push vvnocode/vpspeek:0.1 138 | docker push vvnocode/vpspeek:latest 139 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](README.en.md) 2 | 3 | ## 功能介绍 4 | 5 | - 可以自动执行测速,也可以手动执行。 6 | - 可以配置在一个时间范围内随机选择下次执行时间,这样定时任务的特征会小一点。 7 | - 可以对测速结果查询、排序。 8 | - 可以配置运行模式,防止被人攻击 9 | - 支持一键安装&升级。 10 | - 支持配置用户时区,让部署在不同时区vps上的显示更友好。默认用户时区Asia/Shanghai。 11 | - 支持配置自动更新 12 | 13 | ### 页面展示 14 | 15 | 输入ip:5000访问 16 | 17 | ![](https://s1.locimg.com/2024/09/18/bdb8e17c0bcd7.png) 18 | 19 | ### 运行资源占用 20 | 21 | - 二进制文件大小9M,运行时内存占用41M左右 22 | ![](https://s1.locimg.com/2024/09/18/ab84785aeb29f.png) 23 | 24 | - docker镜像大小在27M左右,运行时占用系统内存25M左右。 25 | ![](https://s1.locimg.com/2024/09/16/b050a4d1e0127.png) 26 | 27 | ## 安装 28 | 29 | - 安装到vps上,可以测试国际网络速度。 30 | - 安装到nas上,可以测试nas从测速地址(可在conf.yaml配置)下载文件的速度。 31 | 32 | ### 使用脚本安装 33 | 34 | 此脚本会安装编译好的二进制文件。运行会自动生成conf.yam配置文件和数据存储data.json文件。 35 | 如果更改配置文件conf.yaml,需要运行`systemctl restart vpspeek`重启服务。 36 | 37 | ```shell 38 | curl -L https://raw.githubusercontent.com/vvnocode/vpspeek/master/install.sh -o vpspeek.sh && chmod +x vpspeek.sh && sudo ./vpspeek.sh 39 | ``` 40 | 41 | #### 自动更新 42 | 43 | 1. 设置 EDITOR 环境变量为 vim 44 | 45 | `export EDITOR=vim` 46 | 47 | 2. 打开 crontab 编辑器 48 | 49 | `crontab -e` 50 | 51 | 如果提示没有vim,可以安装一个。 52 | 53 | `apt update && apt install vim` 54 | 55 | 3. 在打开的 vim 编辑器中添加以下行。表示每天6点执行更新。 56 | 57 | `0 6 * * * curl -L https://raw.githubusercontent.com/vvnocode/vpspeek/master/install.sh -o vpspeek.sh && chmod +x vpspeek.sh && sudo ./vpspeek.sh` 58 | 59 | 4. 保存并退出 vim: 60 | 61 | 按 Esc 进入命令模式。 输入 :wq 并回车。 62 | 63 | 5. 您可以使用以下命令来检查 crontab 定时任务是否正确配置: 64 | 65 | `crontab -l` 66 | 67 | ### 使用docker命令行 68 | 69 | ```shell 70 | docker run --name vpspeek -p 5000:5000 vvnocode/vpspeek:latest 71 | ``` 72 | 73 | 映射文件 74 | 75 | ```shell 76 | docker run --name vpspeek -p 5000:5000 -v /mnt/user/appdata/vpspeek/vvnode/data.json:/app/data.json -v /mnt/user/appdata/vpspeek/vvnode/conf.yaml:/app/conf.yaml vvnocode/vpspeek:latest 77 | ``` 78 | 79 | #### 自动更新 80 | 81 | 使用 containrrr/watchtower 实现自动更新 82 | 83 | ### 使用docker-compose 84 | 85 | ```yaml 86 | services: 87 | vpspeek: 88 | image: vvnocode/vpspeek:latest 89 | container_name: vpspeek 90 | ports: 91 | - "5000:5000" 92 | volumes: 93 | - /mnt/user/appdata/vpspeek/vvnode/data.json:/app/data.json 94 | - /mnt/user/appdata/vpspeek/vvnode/conf.yaml:/app/conf.yaml 95 | restart: unless-stopped 96 | ``` 97 | 98 | #### 自动更新 99 | 100 | 使用 containrrr/watchtower 实现自动更新 101 | 102 | ### 注意 103 | 104 | 使用docker安装且需要映射文件的,需要确保映射的宿主机文件存在。 105 | 106 | 1. 需要创建conf.yaml,可以是空文件。 107 | 2. 需要创建data.json,可以是空文件。 108 | 109 | ## 配置 110 | 111 | - 默认配置即可使用,如需修改,请修改conf.yaml。 112 | - 可以自行配置测速地址,默认是cloudflare的测速地址。 113 | - 可以对模式进行配置。conf.yaml中,mode默认是default。 114 | - full:完整的功能,调用接口无安全验证,后续会改为使用密码登录。 115 | - default:无安全验证,但是只能查询,关闭手动测速接口。 116 | - safe:接口需要在header增加校验,参数为conf.yaml的key。 117 | - 默认配置在极限情况下,每天测速下载最少为60/max_interval\*24\*100M,当max_interval=60时,每日下载量最多2400M数据。 118 | - 默认配置在极限情况下,每天测速下载最少为60/min_interval\*24\*100M,当min_interval=30时,每日下载量最多4800M数据。 119 | - 如果配置合理,任务分散到24小时执行,并不会对服务器造成过大压力。 120 | 121 | ## 开发 122 | 123 | 基于curl命令下载指定大小的文件达到测速目的。 124 | 125 | ```shell 126 | # 原命令 127 | curl -o /dev/null 'https://speed.cloudflare.com/__down?during=download&bytes=104857600' 128 | # 调整下 129 | curl -o /dev/null -s -w "%{size_download} %{time_total} %{speed_download}\n" 'https://speed.cloudflare.com/__down?during=download&bytes=104857600' 130 | ``` 131 | 132 | ### 开发环境 133 | 134 | - python 3.10 135 | - PyCharm 136 | 137 | ### 构建 138 | 139 | #### 使用GitHub Actions自动构建 140 | 141 | - [x] 打tag后自动构建docker并推送到dockerhub。 142 | - [x] 打tag后自动构建二进制可执行文件并发布到release页面。 143 | 144 | #### 构建二进制文件 145 | 146 | 打包命令 147 | 148 | ```shell 149 | pyinstaller --onefile --add-data "conf.yaml.default:." --add-data "templates:templates" --name vpspeek app.py 150 | ``` 151 | 152 | #### 构建docker镜像 153 | 154 | ```shell 155 | #分别构建amd64、arm64 156 | #linux/amd64 157 | docker build --platform linux/amd64 -t vvnocode/vpspeek:0.3 . 158 | #tag 159 | docker tag vvnocode/vpspeek:0.3 vvnocode/vpspeek:latest 160 | #推送 161 | docker push vvnocode/vpspeek:0.3 162 | docker push vvnocode/vpspeek:latest 163 | 164 | #linux/arm64 165 | docker build --platform linux/arm64 -t vvnocode/vpspeek:0.3 . 166 | #重复上面操作tag、push 167 | ``` -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | import subprocess 5 | import random 6 | import datetime 7 | import json 8 | import threading 9 | import uuid # 用于生成随机key 10 | import pytz # 用于时区转换 11 | from flask import Flask, jsonify, request, render_template, abort 12 | 13 | from ruamel.yaml import YAML 14 | from apscheduler.schedulers.background import BackgroundScheduler 15 | 16 | app = Flask(__name__) 17 | 18 | # 初始化 YAML 解析器 19 | yaml = YAML() 20 | yaml.preserve_quotes = True # 保留引号 21 | 22 | 23 | # 配置文件路径处理 24 | def resource_path(relative_path, external=False): 25 | """ 获取资源文件路径,兼容开发和打包后的情况 """ 26 | if external: 27 | # 外部资源文件的路径(与可执行文件同目录) 28 | return os.path.join(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.abspath('.'), 29 | relative_path) 30 | else: 31 | # 内部资源文件路径(如 conf.yaml.default) 32 | try: 33 | base_path = sys._MEIPASS 34 | except AttributeError: 35 | base_path = os.path.abspath(os.path.dirname(__file__)) 36 | return os.path.join(base_path, relative_path) 37 | 38 | 39 | # 检查并加载/更新配置文件 40 | def load_or_create_config(): 41 | # 外部配置文件路径 42 | config_file = resource_path('conf.yaml', external=True) 43 | # 默认配置文件路径 (打包后存在于 MEIPASS 临时目录中) 44 | default_config_file = resource_path('conf.yaml.default') 45 | 46 | # 如果默认配置文件不存在,抛出异常 47 | if not os.path.exists(default_config_file): 48 | raise FileNotFoundError(f"默认配置文件 '{default_config_file}' 不存在!") 49 | 50 | # 如果 conf.yaml 不存在,复制 conf.yaml.default 51 | if not os.path.exists(config_file): 52 | print(f"'{config_file}' 不存在,复制默认配置文件...") 53 | shutil.copy(default_config_file, config_file) 54 | 55 | # 读取默认配置 56 | with open(default_config_file, 'r') as default_file: 57 | default_conf = yaml.load(default_file) 58 | 59 | # 读取用户配置(如果存在) 60 | with open(config_file, 'r') as user_file: 61 | user_conf = yaml.load(user_file) 62 | 63 | merged_conf = merge_dicts(default_conf, user_conf) 64 | 65 | # 如果 conf.yaml 中没有 key 或 key 为空,生成一个随机的 key 66 | if 'key' not in merged_conf or not merged_conf['key']: 67 | merged_conf['key'] = str(uuid.uuid4()) 68 | print(f"生成新的API key: {merged_conf['key']}") 69 | 70 | # 如果没有模式配置,默认为 'default' 71 | if 'mode' not in merged_conf or not merged_conf['mode']: 72 | merged_conf['mode'] = 'default' 73 | print(f"设置模式为默认模式: {merged_conf['mode']}") 74 | 75 | # 保存合并后的配置到 conf.yaml 76 | with open(config_file, 'w') as config_file_out: 77 | yaml.dump(merged_conf, config_file_out) 78 | 79 | return merged_conf 80 | 81 | 82 | # 加载数据 83 | def load_data(): 84 | data_file = resource_path('data.json', external=True) # 确保 data.json 位于外部目录 85 | if not os.path.exists(data_file): 86 | data = {"results": [], "next_run": None} 87 | save_data(data) 88 | else: 89 | try: 90 | with open(data_file, 'r') as file: 91 | data = json.load(file) 92 | except json.JSONDecodeError: 93 | data = {"results": [], "next_run": None} 94 | save_data(data) 95 | return data 96 | 97 | 98 | # 保存数据 99 | def save_data(data): 100 | data_file = resource_path('data.json', external=True) # 确保保存到外部路径 101 | with open(data_file, 'w') as fileIO: 102 | json.dump(data, fileIO, indent=4) 103 | 104 | 105 | # 以默认配置为基础,用用户配置更新字段 106 | def merge_dicts(defaults, overrides): 107 | for key, value in overrides.items(): 108 | if isinstance(value, dict) and key in defaults: 109 | merge_dicts(defaults[key], value) 110 | else: 111 | defaults[key] = value 112 | return defaults 113 | 114 | 115 | # 测速函数 116 | def speed_test(triggered_by="auto"): 117 | # 获取用户的时区设置并进行时间转换 118 | timestamp_utc = datetime.datetime.now(pytz.utc) 119 | timestamp_user = convert_time_to_user_timezone(timestamp_utc).strftime('%Y-%m-%d %H:%M:%S') 120 | 121 | result = subprocess.run( 122 | ["curl", "-o", "/dev/null", "-s", "-w", "%{size_download} %{time_total} %{speed_download}\n", 123 | conf['speedtest_url']], 124 | capture_output=True, 125 | text=True 126 | ) 127 | 128 | if result.returncode != 0: 129 | print("Speed test failed.") 130 | return 131 | 132 | size_download, time_total, speed_download = result.stdout.strip().split() 133 | size_download = float(size_download) / (1024 * 1024) # 转换为 MB 134 | time_total = float(time_total) # 下载总用时 135 | speed_download = (float(speed_download) * 8) / (1024 * 1024) # 转换为 Mbps 136 | 137 | new_result = { 138 | "timestamp": timestamp_user, 139 | "file_size_MB": round(size_download, 2), 140 | "time_seconds": round(time_total, 2), 141 | "speed_Mbps": round(speed_download, 2), 142 | "triggered_by": triggered_by 143 | } 144 | 145 | data = load_data() 146 | data["results"].append(new_result) 147 | set_next_run(data) 148 | save_data(data) 149 | 150 | 151 | # 设置下一次运行时间 152 | def set_next_run(data): 153 | next_run_interval = random.randint(conf['min_interval'], conf['max_interval']) * 60 154 | next_run = convert_time_to_user_timezone() + datetime.timedelta(seconds=next_run_interval) 155 | data["next_run"] = next_run.strftime('%Y-%m-%d %H:%M:%S') 156 | 157 | 158 | # 检查是否应该运行测速 159 | def check_run(): 160 | data = load_data() 161 | if data["next_run"]: 162 | next_run_user_time = datetime.datetime.strptime(data["next_run"], '%Y-%m-%d %H:%M:%S') 163 | # 将 next_run 转换为有时区信息的 datetime 对象 164 | next_run_next_run_user_time_zone = user_timezone().localize(next_run_user_time) 165 | if convert_time_to_user_timezone() >= next_run_next_run_user_time_zone: 166 | speed_test() 167 | else: 168 | speed_test() 169 | 170 | 171 | # 校验API请求的key 172 | def check_api_key(): 173 | api_key = request.headers.get('X-API-Key') 174 | if conf.get('mode') == 'safe' and api_key != conf['key']: 175 | abort(401, description="Unauthorized: Invalid API Key") 176 | 177 | 178 | def user_timezone(): 179 | return pytz.timezone(conf.get('user_timezone', 'Asia/Shanghai')) 180 | 181 | 182 | # 转换为用户时区 183 | def convert_time_to_user_timezone(time=None): 184 | if time is None: 185 | time = datetime.datetime.now(pytz.utc) # 使用当前的 UTC 时间 186 | return time.astimezone(user_timezone()) # 转换为用户时区的时间 187 | 188 | 189 | # Flask 路由 190 | @app.route('/') 191 | def home(): 192 | # 校验key 193 | check_api_key() 194 | 195 | return render_template('index.html') 196 | 197 | 198 | @app.route('/config', methods=['GET']) 199 | def get_config(): 200 | # 校验key 201 | check_api_key() 202 | 203 | # 只返回 min_interval、max_interval 和 vps_name 三个字段 204 | return jsonify({ 205 | 'min_interval': conf.get('min_interval'), 206 | 'max_interval': conf.get('max_interval'), 207 | 'vps_name': conf.get('vps_name'), 208 | 'user_timezone': conf.get('user_timezone') 209 | }) 210 | 211 | 212 | @app.route('/data', methods=['GET']) 213 | def get_data(): 214 | # 校验key 215 | check_api_key() 216 | 217 | sort_key = request.args.get('sort_by', 'timestamp') 218 | sort_order = request.args.get('sort_order', 'desc') 219 | 220 | data = load_data() 221 | # 按请求的字段排序结果 222 | results = sorted(data['results'], key=lambda x: x.get(sort_key, 'timestamp'), reverse=sort_order == 'desc') 223 | 224 | # 更新 data['results'],并保存 225 | data['results'] = results 226 | save_data(data) 227 | 228 | # 返回完整的 data 229 | return jsonify(data) 230 | 231 | 232 | @app.route('/trigger_speed_test', methods=['GET']) 233 | def trigger_speed_test(): 234 | # 根据模式决定是否允许手动触发测速 235 | if conf.get('mode') == 'default': 236 | return jsonify({"message": "Manual speed tests are disabled in default mode"}), 403 237 | 238 | # 校验key 239 | check_api_key() 240 | 241 | speed_test(triggered_by="manual") 242 | return jsonify({"message": "Speed test initiated"}), 202 243 | 244 | 245 | # 定时任务 246 | def run_scheduler(): 247 | scheduler = BackgroundScheduler() 248 | scheduler.add_job(check_run, 'interval', seconds=10) 249 | scheduler.start() 250 | 251 | 252 | if __name__ == '__main__': 253 | # 加载配置 254 | conf = load_or_create_config() 255 | 256 | # 运行测速守护进程 257 | threading.Thread(target=run_scheduler).start() 258 | 259 | # 启动Flask 260 | app.run(host='0.0.0.0', debug=False, port=conf['port']) 261 | -------------------------------------------------------------------------------- /conf.yaml.default: -------------------------------------------------------------------------------- 1 | # 服务端口 2 | port: 5000 3 | speedtest_url: https://speed.cloudflare.com/__down?during=download&bytes=104857600 4 | # 下次执行最快分钟 5 | min_interval: 30 6 | # 下次执行最慢分钟 7 | max_interval: 60 8 | # vps 名称 9 | vps_name: 我的xx 10 | # 模式 11 | # full:完整的功能,调用接口无安全验证,后续会改为使用密码登录;default:无安全验证,但是只能查询,关闭手动测速接口;safe:接口需要在header增加校验,参数为conf.yaml的key; 12 | mode: 13 | # 调用接口需要的key 14 | key: 15 | # 用户时区 16 | user_timezone: Asia/Shanghai -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define the download URL and file names 4 | DOWNLOAD_URL="https://github.com/vvnocode/vpspeek/releases/latest/download/vpspeek" 5 | DOWNLOAD_FILE="download_vpspeek" 6 | TARGET_FILE="vpspeek" 7 | SERVICE_NAME="vpspeek" 8 | 9 | # Number of retries 10 | RETRIES=3 11 | 12 | # Download the file with retries 13 | attempt=0 14 | while [ $attempt -lt $RETRIES ]; do 15 | echo "Attempting to download file... (Attempt $((attempt + 1))/$RETRIES)" 16 | wget -O $DOWNLOAD_FILE $DOWNLOAD_URL 17 | 18 | if [ $? -eq 0 ]; then 19 | echo "Download successful." 20 | break 21 | else 22 | echo "Download failed." 23 | attempt=$((attempt + 1)) 24 | if [ $attempt -eq $RETRIES ]; then 25 | echo "Download failed after $RETRIES attempts. Exiting." 26 | exit 1 27 | fi 28 | fi 29 | done 30 | 31 | # Check if the service exists and stop it if running 32 | if systemctl list-units --type=service | grep -q "$SERVICE_NAME"; then 33 | echo "Stopping $SERVICE_NAME service..." 34 | systemctl stop $SERVICE_NAME 35 | fi 36 | 37 | # Remove old vpspeek file if it exists 38 | if [ -f "$TARGET_FILE" ]; then 39 | echo "Removing old $TARGET_FILE file..." 40 | rm -f $TARGET_FILE 41 | fi 42 | 43 | # Rename downloaded file to vpspeek and make it executable 44 | mv $DOWNLOAD_FILE $TARGET_FILE 45 | chmod +x $TARGET_FILE 46 | echo "Renamed $DOWNLOAD_FILE to $TARGET_FILE and made it executable." 47 | 48 | # Create a systemd service file if it doesn't exist 49 | SERVICE_PATH="/etc/systemd/system/$SERVICE_NAME.service" 50 | if [ ! -f "$SERVICE_PATH" ]; then 51 | echo "Creating systemd service file for $SERVICE_NAME..." 52 | cat < $SERVICE_PATH 53 | [Unit] 54 | Description=VPSPeek Service 55 | After=network.target 56 | 57 | [Service] 58 | ExecStart=$(pwd)/$TARGET_FILE 59 | Restart=always 60 | User=root 61 | 62 | [Install] 63 | WantedBy=multi-user.target 64 | EOL 65 | 66 | # Reload systemd, enable service, and start it 67 | systemctl daemon-reload 68 | systemctl enable $SERVICE_NAME 69 | fi 70 | 71 | # Start the service 72 | echo "Starting $SERVICE_NAME service..." 73 | systemctl start $SERVICE_NAME 74 | 75 | echo "$SERVICE_NAME installed and started successfully." 76 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | APScheduler~=3.10.4 2 | flask~=3.0.3 3 | ruamel.yaml~=0.18.6 4 | ruamel.yaml.clib~=0.2.8 5 | pytz~=2024.2 -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Speed Test Dashboard 7 | 8 | 16 | 17 | 18 | 19 | 22 |
23 |

速度测试

24 | 用户时区: 25 | 26 |
27 | 下次执行时间在配置的时间间隔区间 28 | 29 | 30 |
31 |
32 | 33 |
34 |

测速历史记录

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
测速时间消耗流量(M)测速耗时(秒)速度 (Mbps)触发
49 |
50 | 51 | 137 | 138 | 139 | --------------------------------------------------------------------------------