mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-14 07:26:59 +08:00
Compare commits
1 Commits
1.23.0-bet
...
uptime-imp
Author | SHA1 | Date | |
---|---|---|---|
|
fecee1e649 |
@@ -1,28 +0,0 @@
|
|||||||
# Codespaces
|
|
||||||
|
|
||||||
You can modifiy Uptime Kuma in your browser without setting up a local development.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
1. Click `Code` -> `Create codespace on master`
|
|
||||||
2. Wait a few minutes until you see there are two exposed ports
|
|
||||||
3. Go to the `3000` url, see if it is working
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Frontend
|
|
||||||
|
|
||||||
Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded.
|
|
||||||
You don't need to restart the frontend, unless you try to add a new frontend dependency.
|
|
||||||
|
|
||||||
## Backend
|
|
||||||
|
|
||||||
The backend does not automatically hot-reload.
|
|
||||||
You will need to restart the backend after changing something using these steps:
|
|
||||||
|
|
||||||
1. Click `Terminal`
|
|
||||||
2. Click `Codespaces: server-dev` in the right panel
|
|
||||||
3. Press `Ctrl + C` to stop the server
|
|
||||||
4. Press `Up` to run `npm run start-server-dev`
|
|
||||||
|
|
||||||

|
|
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm",
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
|
||||||
},
|
|
||||||
"updateContentCommand": "npm ci",
|
|
||||||
"postCreateCommand": "",
|
|
||||||
"postAttachCommand": {
|
|
||||||
"frontend-dev": "npm run start-frontend-devcontainer",
|
|
||||||
"server-dev": "npm run start-server-dev",
|
|
||||||
"open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME"
|
|
||||||
},
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
|
||||||
"streetsidesoftware.code-spell-checker",
|
|
||||||
"dbaeumer.vscode-eslint"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"forwardPorts": [3000, 3001]
|
|
||||||
}
|
|
@@ -18,7 +18,6 @@ README.md
|
|||||||
.vscode
|
.vscode
|
||||||
.eslint*
|
.eslint*
|
||||||
.stylelint*
|
.stylelint*
|
||||||
/.devcontainer
|
|
||||||
/.github
|
/.github
|
||||||
yarn.lock
|
yarn.lock
|
||||||
app.json
|
app.json
|
||||||
@@ -34,7 +33,6 @@ tsconfig.json
|
|||||||
/ecosystem.config.js
|
/ecosystem.config.js
|
||||||
/extra/healthcheck.exe
|
/extra/healthcheck.exe
|
||||||
/extra/healthcheck
|
/extra/healthcheck
|
||||||
extra/exe-builder
|
|
||||||
|
|
||||||
|
|
||||||
### .gitignore content (commented rules are duplicated)
|
### .gitignore content (commented rules are duplicated)
|
||||||
@@ -50,4 +48,6 @@ dist-ssr
|
|||||||
#!/data/.gitkeep
|
#!/data/.gitkeep
|
||||||
#.vscode
|
#.vscode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### End of .gitignore content
|
### End of .gitignore content
|
||||||
|
10
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
10
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
@@ -26,12 +26,6 @@ body:
|
|||||||
label: "📝 Describe your problem"
|
label: "📝 Describe your problem"
|
||||||
description: "Please walk us through it step by step."
|
description: "Please walk us through it step by step."
|
||||||
placeholder: "Describe what are you asking for..."
|
placeholder: "Describe what are you asking for..."
|
||||||
- type: textarea
|
|
||||||
id: error-msg
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
attributes:
|
|
||||||
label: "📝 Error Message(s) or Log"
|
|
||||||
- type: input
|
- type: input
|
||||||
id: uptime-kuma-version
|
id: uptime-kuma-version
|
||||||
attributes:
|
attributes:
|
||||||
@@ -44,7 +38,7 @@ body:
|
|||||||
id: operating-system
|
id: operating-system
|
||||||
attributes:
|
attributes:
|
||||||
label: "💻 Operating System and Arch"
|
label: "💻 Operating System and Arch"
|
||||||
description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
|
description: "Which OS is your server/device running on?"
|
||||||
placeholder: "Ex. Ubuntu 20.04 x86"
|
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -52,7 +46,7 @@ body:
|
|||||||
id: browser-vendor
|
id: browser-vendor
|
||||||
attributes:
|
attributes:
|
||||||
label: "🌐 Browser"
|
label: "🌐 Browser"
|
||||||
description: "Which browser are you running on? (For Replit, please do not report this bug)"
|
description: "Which browser are you running on?"
|
||||||
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -61,8 +61,8 @@ body:
|
|||||||
id: operating-system
|
id: operating-system
|
||||||
attributes:
|
attributes:
|
||||||
label: "💻 Operating System and Arch"
|
label: "💻 Operating System and Arch"
|
||||||
description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
|
description: "Which OS is your server/device running on?"
|
||||||
placeholder: "Ex. Ubuntu 20.04 x64 "
|
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
19
.github/ISSUE_TEMPLATE/security.md
vendored
19
.github/ISSUE_TEMPLATE/security.md
vendored
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
name: "Security Issue"
|
|
||||||
about: "Just for alerting @louislam, do not provide any details here"
|
|
||||||
title: "Security Issue"
|
|
||||||
ref: "main"
|
|
||||||
labels:
|
|
||||||
|
|
||||||
- security
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
DO NOT PROVIDE ANY DETAILS HERE. Please privately report to https://github.com/louislam/uptime-kuma/security/advisories/new.
|
|
||||||
|
|
||||||
|
|
||||||
Why need this issue? It is because GitHub Advisory do not send a notification to @louislam, it is a workaround to do so.
|
|
||||||
|
|
||||||
Your GitHub Advisory URL:
|
|
||||||
|
|
1
.github/config/exclude.txt
vendored
1
.github/config/exclude.txt
vendored
@@ -1 +0,0 @@
|
|||||||
# This is a .gitignore style file for 'GrantBirki/json-yaml-validate' Action workflow
|
|
34
.github/workflows/auto-test.yml
vendored
34
.github/workflows/auto-test.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
name: Auto Test
|
name: Auto Test
|
||||||
@@ -21,8 +21,8 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
node: [ 14, 20 ]
|
node: [ 14, 16, 18, 19 ]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
- run: npm install npm@latest -g
|
cache: 'npm'
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm test
|
||||||
@@ -41,29 +41,6 @@ jobs:
|
|||||||
HEADLESS_TEST: 1
|
HEADLESS_TEST: 1
|
||||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||||
|
|
||||||
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
|
|
||||||
armv7-simple-test:
|
|
||||||
needs: [ check-linters ]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ ARMv7 ]
|
|
||||||
node: [ 14.21.3, 20.5.0 ]
|
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node }}
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node }}
|
|
||||||
- run: npm install npm@latest -g
|
|
||||||
- run: npm ci --production
|
|
||||||
|
|
||||||
check-linters:
|
check-linters:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -75,6 +52,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
cache: 'npm'
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
|
|
||||||
@@ -89,6 +67,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
cache: 'npm'
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run cy:test
|
- run: npm run cy:test
|
||||||
@@ -104,6 +83,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
cache: 'npm'
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run cy:run:unit
|
- run: npm run cy:run:unit
|
||||||
|
26
.github/workflows/json-yaml-validate.yml
vendored
26
.github/workflows/json-yaml-validate.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: json-yaml-validate
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write # enable write permissions for pull request comments
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
json-yaml-validate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: json-yaml-validate
|
|
||||||
id: json-yaml-validate
|
|
||||||
uses: GrantBirki/json-yaml-validate@v1.3.0
|
|
||||||
with:
|
|
||||||
comment: "true" # enable comment mode
|
|
||||||
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
|
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -20,9 +20,3 @@ cypress/screenshots
|
|||||||
/extra/healthcheck.exe
|
/extra/healthcheck.exe
|
||||||
/extra/healthcheck
|
/extra/healthcheck
|
||||||
/extra/healthcheck-armv7
|
/extra/healthcheck-armv7
|
||||||
|
|
||||||
extra/exe-builder/bin
|
|
||||||
extra/exe-builder/obj
|
|
||||||
|
|
||||||
.vs
|
|
||||||
.vscode
|
|
||||||
|
@@ -10,7 +10,5 @@
|
|||||||
"color-function-notation": "legacy",
|
"color-function-notation": "legacy",
|
||||||
"shorthand-property-no-redundant-values": null,
|
"shorthand-property-no-redundant-values": null,
|
||||||
"color-hex-length": null,
|
"color-hex-length": null,
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
|
||||||
"at-rule-no-unknown": null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,30 +34,30 @@ Yes or no, it depends on what you will try to do. Since I don't want to waste yo
|
|||||||
|
|
||||||
Here are some references:
|
Here are some references:
|
||||||
|
|
||||||
### ✅ Usually accepted:
|
✅ Usually Accept:
|
||||||
- Bug fix
|
- Bug fix
|
||||||
- Security fix
|
- Security fix
|
||||||
- Adding notification providers
|
- Adding notification providers
|
||||||
- Adding new language files (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
|
- Adding new language files (You should go to https://weblate.kuma.pet for existing languages)
|
||||||
- Adding new language keys: `$t("...")`
|
- Adding new language keys: `$t("...")`
|
||||||
|
|
||||||
### ⚠️ Discussion required:
|
⚠️ Discussion First
|
||||||
- Large pull requests
|
- Large pull requests
|
||||||
- New features
|
- New features
|
||||||
|
|
||||||
### ❌ Won't be merged:
|
❌ Won't Merge
|
||||||
- A dedicated pr for translating existing languages (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
|
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet)
|
||||||
- Do not pass the auto test
|
- Do not pass auto test
|
||||||
- Any breaking changes
|
- Any breaking changes
|
||||||
- Duplicated pull requests
|
- Duplicated pull request
|
||||||
- Buggy
|
- Buggy
|
||||||
- UI/UX is not close to Uptime Kuma
|
- UI/UX is not close to Uptime Kuma
|
||||||
- Modifications or deletions of existing logic without a valid reason.
|
- Existing logic is completely modified or deleted for no reason
|
||||||
- Adding functions that is completely out of scope
|
- A function that is completely out of scope
|
||||||
- Converting existing code into other programming languages
|
- Convert existing code into other programming languages
|
||||||
- Unnecessarily large code changes that are hard to review and cause conflicts with other PRs.
|
- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
|
||||||
|
|
||||||
The above cases may not cover all possible situations.
|
The above cases cannot cover all situations.
|
||||||
|
|
||||||
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
|
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
|
||||||
|
|
||||||
@@ -106,11 +106,11 @@ I personally do not like something that requires so many configurations before y
|
|||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
- [`Node.js`](https://nodejs.org/) >= 14
|
- Node.js >= 14
|
||||||
- [`npm`](https://www.npmjs.com/) >= 8.5
|
- NPM >= 8.5
|
||||||
- [`git`](https://git-scm.com/)
|
- Git
|
||||||
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
|
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||||
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
|
- A SQLite GUI tool (SQLite Expert Personal is suggested)
|
||||||
|
|
||||||
## Install Dependencies for Development
|
## Install Dependencies for Development
|
||||||
|
|
||||||
@@ -218,17 +218,7 @@ If for maybe security reasons, a library must be updated. Then you must need to
|
|||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are ommited, they can not be translated).
|
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
**Don't include any other languages in your inital Pull-Request** (even if this is your mother tounge), to avoid merge-conflicts between weblate and `master`.
|
|
||||||
The translations can then (after merging a PR into `master`) be translated by awesome people donating their language-skills.
|
|
||||||
|
|
||||||
If you want to help by translating Uptime Kuma into your language, please visit the [instructions on how to translate using weblate](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
|
||||||
|
|
||||||
## Spelling & Grammar
|
|
||||||
|
|
||||||
Feel free to correct the grammar in the documentation or code.
|
|
||||||
My mother language is not english and my grammar is not that great.
|
|
||||||
|
|
||||||
## Wiki
|
## Wiki
|
||||||
|
|
||||||
@@ -245,13 +235,12 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
|||||||
|
|
||||||
1. Draft a release note
|
1. Draft a release note
|
||||||
2. Make sure the repo is cleared
|
2. Make sure the repo is cleared
|
||||||
3. If the healthcheck is updated, remember to re-compile it: `npm run build-docker-builder-go`
|
|
||||||
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
|
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||||
4. Wait until the `Press any key to continue`
|
4. Wait until the `Press any key to continue`
|
||||||
5. `git push`
|
5. `git push`
|
||||||
6. Publish the release note as 1.X.X
|
6. Publish the release note as 1.X.X
|
||||||
7. Press any key to continue
|
7. Press any key to continue
|
||||||
8. Deploy to the demo server: `npm run deploy-demo-server`
|
8. SSH to demo site server and update to 1.X.X
|
||||||
|
|
||||||
Checking:
|
Checking:
|
||||||
|
|
||||||
|
43
README.md
43
README.md
@@ -1,16 +1,16 @@
|
|||||||
<div align="center" width="100%">
|
|
||||||
<img src="./public/icon.svg" width="128" alt="" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
# Uptime Kuma
|
# Uptime Kuma
|
||||||
|
|
||||||
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
|
||||||
|
|
||||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
||||||
[](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
|
[](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
|
||||||
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
|
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<div align="center" width="100%">
|
||||||
|
<img src="./public/icon.svg" width="128" alt="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
||||||
|
|
||||||
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
|
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
|
||||||
|
|
||||||
## 🥔 Live Demo
|
## 🥔 Live Demo
|
||||||
@@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
|
|||||||
|
|
||||||
## ⭐ Features
|
## ⭐ Features
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
||||||
* Fancy, Reactive, Fast UI/UX
|
* Fancy, Reactive, Fast UI/UX
|
||||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
|
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
|
||||||
* 20 second intervals
|
* 20 second intervals
|
||||||
@@ -49,13 +49,8 @@ Uptime Kuma is now running on http://localhost:3001
|
|||||||
|
|
||||||
### 💪🏻 Non-Docker
|
### 💪🏻 Non-Docker
|
||||||
|
|
||||||
Requirements:
|
Required Tools:
|
||||||
- Platform
|
- [Node.js](https://nodejs.org/en/download/) >= 14
|
||||||
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
|
|
||||||
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
|
|
||||||
- ❌ Replit / Heroku
|
|
||||||
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 / 20.4
|
|
||||||
- [npm](https://docs.npmjs.com/cli/) >= 7
|
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
||||||
|
|
||||||
@@ -91,10 +86,6 @@ pm2 monit
|
|||||||
pm2 save && pm2 startup
|
pm2 save && pm2 startup
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows Portable (x64)
|
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
|
|
||||||
|
|
||||||
### Advanced Installation
|
### Advanced Installation
|
||||||
|
|
||||||
If you need more options or need to browse via a reverse proxy, please read:
|
If you need more options or need to browse via a reverse proxy, please read:
|
||||||
@@ -152,18 +143,17 @@ Telegram Notification Sample:
|
|||||||
|
|
||||||
If you love this project, please consider giving me a ⭐.
|
If you love this project, please consider giving me a ⭐.
|
||||||
|
|
||||||
## 🗣️ Discussion / Ask for Help
|
## 🗣️ Discussion
|
||||||
|
|
||||||
⚠️ For any general or technical questions, please don't send me an email, as I am unable to provide support in that manner. I will not response if you asked such questions.
|
### Issues Page
|
||||||
|
|
||||||
I recommend using Google, GitHub Issues, or Uptime Kuma's Subreddit for finding answers to your question. If you cannot find the information you need, feel free to ask:
|
You can discuss or ask for help in [issues](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
- [GitHub Issues](https://github.com/louislam/uptime-kuma/issues)
|
### Subreddit
|
||||||
- [Subreddit r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
|
||||||
|
|
||||||
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
|
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
|
||||||
You can mention me if you ask a question on Reddit.
|
You can mention me if you ask a question on Reddit.
|
||||||
|
[r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
@@ -184,10 +174,7 @@ If you want to report a bug or request a new feature, feel free to open a [new i
|
|||||||
### Translations
|
### Translations
|
||||||
If you want to translate Uptime Kuma into your language, please visit [Weblate Readme](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
If you want to translate Uptime Kuma into your language, please visit [Weblate Readme](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
||||||
|
|
||||||
## Spelling & Grammar
|
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
|
||||||
|
|
||||||
Feel free to correct the grammar in the documentation or code.
|
|
||||||
My mother language is not english and my grammar is not that great.
|
|
||||||
|
|
||||||
### Create Pull Requests
|
### Create Pull Requests
|
||||||
If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||||
|
@@ -2,15 +2,10 @@
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
1. Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
|
Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
|
||||||
1. Please also create a empty security issues for alerting me, as GitHub Advisory do not send a notification, I probably will miss without this. https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md
|
|
||||||
|
|
||||||
Do not use the public issue tracker or discuss it in the public as it will cause more damage.
|
Do not use the public issue tracker or discuss it in the public as it will cause more damage.
|
||||||
|
|
||||||
## Do you accept other 3rd-party bug bounty platforms?
|
|
||||||
|
|
||||||
At this moment, I DO NOT accept other bug bounty platforms, because I am not familiar with these platforms and someone have tried to send a phishing link to me by this already. To minimize my own risk, please report through GitHub Advisories only. I will ignore all 3rd-party bug bounty platforms emails.
|
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
### Uptime Kuma Versions
|
### Uptime Kuma Versions
|
||||||
|
@@ -4,4 +4,8 @@ if (process.env.TEST_FRONTEND) {
|
|||||||
config.presets = [ "@babel/preset-env" ];
|
config.presets = [ "@babel/preset-env" ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.TEST_BACKEND) {
|
||||||
|
config.plugins = [ "babel-plugin-rewire" ];
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
@@ -3,7 +3,6 @@ import vue from "@vitejs/plugin-vue";
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import visualizer from "rollup-plugin-visualizer";
|
import visualizer from "rollup-plugin-visualizer";
|
||||||
import viteCompression from "vite-plugin-compression";
|
import viteCompression from "vite-plugin-compression";
|
||||||
import commonjs from "vite-plugin-commonjs";
|
|
||||||
|
|
||||||
const postCssScss = require("postcss-scss");
|
const postCssScss = require("postcss-scss");
|
||||||
const postcssRTLCSS = require("postcss-rtlcss");
|
const postcssRTLCSS = require("postcss-rtlcss");
|
||||||
@@ -17,12 +16,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||||
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
|
|
||||||
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
|
|
||||||
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
commonjs(),
|
|
||||||
vue(),
|
vue(),
|
||||||
legacy({
|
legacy({
|
||||||
targets: [ "since 2015" ],
|
targets: [ "since 2015" ],
|
||||||
@@ -47,9 +42,6 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
commonjsOptions: {
|
|
||||||
include: [ /.js$/ ],
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE status_page
|
|
||||||
ADD show_certificate_expiry BOOLEAN default 0 NOT NULL;
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -1,7 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD description TEXT default null;
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -1,7 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD gamedig_given_port_only BOOLEAN default 1 not null;
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -1,7 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD invert_keyword BOOLEAN default 0 not null;
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -1,6 +0,0 @@
|
|||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
COMMIT
|
|
@@ -1,6 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD timeout DOUBLE default 0 not null;
|
|
||||||
COMMIT;
|
|
@@ -1,10 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD json_path TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD expected_value VARCHAR(255);
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -1,22 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD kafka_producer_topic VARCHAR(255);
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD kafka_producer_brokers TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD kafka_producer_ssl INTEGER;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD kafka_producer_allow_auto_topic_creation VARCHAR(255);
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD kafka_producer_sasl_options TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD kafka_producer_message TEXT;
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -1,13 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
CREATE TABLE [api_key] (
|
|
||||||
[id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
[key] VARCHAR(255) NOT NULL,
|
|
||||||
[name] VARCHAR(255) NOT NULL,
|
|
||||||
[user_id] INTEGER NOT NULL,
|
|
||||||
[created_date] DATETIME DEFAULT (DATETIME('now')) NOT NULL,
|
|
||||||
[active] BOOLEAN DEFAULT 1 NOT NULL,
|
|
||||||
[expires] DATETIME DEFAULT NULL,
|
|
||||||
CONSTRAINT FK_user FOREIGN KEY ([user_id]) REFERENCES [user]([id]) ON DELETE CASCADE ON UPDATE CASCADE
|
|
||||||
);
|
|
||||||
COMMIT;
|
|
@@ -1,12 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor ADD http_body_encoding VARCHAR(25);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
UPDATE monitor SET http_body_encoding = 'json' WHERE (type = 'http' or type = 'keyword') AND http_body_encoding IS NULL;
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -1,11 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
DROP TABLE maintenance_timeslot;
|
|
||||||
|
|
||||||
-- 999 characters. https://stackoverflow.com/questions/46134830/maximum-length-for-cron-job
|
|
||||||
ALTER TABLE maintenance ADD cron TEXT;
|
|
||||||
ALTER TABLE maintenance ADD timezone VARCHAR(255);
|
|
||||||
ALTER TABLE maintenance ADD duration INTEGER;
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -1,19 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD oauth_client_id TEXT default null;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD oauth_client_secret TEXT default null;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD oauth_token_url TEXT default null;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD oauth_scopes TEXT default null;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD oauth_auth_method TEXT default null;
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -1,13 +0,0 @@
|
|||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD tls_ca TEXT default null;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD tls_cert TEXT default null;
|
|
||||||
|
|
||||||
ALTER TABLE monitor
|
|
||||||
ADD tls_key TEXT default null;
|
|
||||||
|
|
||||||
COMMIT;
|
|
@@ -4,5 +4,5 @@ WORKDIR /app
|
|||||||
|
|
||||||
# Install apprise, iputils for non-root ping, setpriv
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib git && \
|
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib git && \
|
||||||
pip3 --no-cache-dir install apprise==1.4.0 && \
|
pip3 --no-cache-dir install apprise==1.2.1 && \
|
||||||
rm -rf /root/.cache
|
rm -rf /root/.cache
|
||||||
|
@@ -5,44 +5,24 @@ ARG TARGETPLATFORM
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
|
# Install Curl
|
||||||
# python3* = apprise's dependencies
|
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||||
# sqlite3 = for debugging
|
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
||||||
# iputils-ping = for ping
|
RUN apt update && \
|
||||||
# util-linux = for setpriv (Should be dropped in 2.0.0?)
|
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||||
# dumb-init = avoid zombie processes (#480)
|
sqlite3 iputils-ping util-linux dumb-init git && \
|
||||||
# curl = for debugging
|
pip3 --no-cache-dir install apprise==1.2.1 && \
|
||||||
# ca-certificates = keep the cert up-to-date
|
|
||||||
# sudo = for start service nscd with non-root user
|
|
||||||
# nscd = for better DNS caching
|
|
||||||
# (pip) apprise = for notifications
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get --yes --no-install-recommends install \
|
|
||||||
python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
|
||||||
sqlite3 \
|
|
||||||
iputils-ping \
|
|
||||||
util-linux \
|
|
||||||
dumb-init \
|
|
||||||
curl \
|
|
||||||
ca-certificates \
|
|
||||||
sudo \
|
|
||||||
nscd && \
|
|
||||||
pip3 --no-cache-dir install apprise==1.4.5 && \
|
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
apt --yes autoremove
|
apt --yes autoremove
|
||||||
|
|
||||||
# Install cloudflared
|
# Install cloudflared
|
||||||
RUN set -eux && \
|
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
||||||
mkdir -p --mode=0755 /usr/share/keyrings && \
|
COPY extra/download-cloudflared.js ./extra/download-cloudflared.js
|
||||||
curl --fail --show-error --silent --location --insecure https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
|
RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
|
||||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared buster main' | tee /etc/apt/sources.list.d/cloudflared.list && \
|
dpkg --add-architecture arm && \
|
||||||
apt-get update && \
|
apt update && \
|
||||||
apt-get install --yes --no-install-recommends cloudflared && \
|
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
||||||
cloudflared version && \
|
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
rm -f cloudflared.deb && \
|
||||||
apt --yes autoremove
|
apt --yes autoremove
|
||||||
|
|
||||||
# For nscd
|
|
||||||
COPY ./docker/etc/nscd.conf /etc/nscd.conf
|
|
||||||
COPY ./docker/etc/sudoers /etc/sudoers
|
|
||||||
|
|
||||||
|
@@ -26,8 +26,6 @@ RUN chmod +x /app/extra/entrypoint.sh
|
|||||||
FROM louislam/uptime-kuma:base-debian AS release
|
FROM louislam/uptime-kuma:base-debian AS release
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV UPTIME_KUMA_IS_CONTAINER=1
|
|
||||||
|
|
||||||
# Copy app files from build layer
|
# Copy app files from build layer
|
||||||
COPY --from=build /app /app
|
COPY --from=build /app /app
|
||||||
|
|
||||||
@@ -72,7 +70,8 @@ RUN git clone https://github.com/louislam/uptime-kuma.git .
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
EXPOSE 3000 3001
|
EXPOSE 3000 3001
|
||||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
|
VOLUME ["/app/data"]
|
||||||
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||||
CMD ["npm", "run", "start-pr-test"]
|
CMD ["npm", "run", "start-pr-test"]
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
|
@@ -1,90 +0,0 @@
|
|||||||
#
|
|
||||||
# /etc/nscd.conf
|
|
||||||
#
|
|
||||||
# An example Name Service Cache config file. This file is needed by nscd.
|
|
||||||
#
|
|
||||||
# Legal entries are:
|
|
||||||
#
|
|
||||||
# logfile <file>
|
|
||||||
# debug-level <level>
|
|
||||||
# threads <initial #threads to use>
|
|
||||||
# max-threads <maximum #threads to use>
|
|
||||||
# server-user <user to run server as instead of root>
|
|
||||||
# server-user is ignored if nscd is started with -S parameters
|
|
||||||
# stat-user <user who is allowed to request statistics>
|
|
||||||
# reload-count unlimited|<number>
|
|
||||||
# paranoia <yes|no>
|
|
||||||
# restart-interval <time in seconds>
|
|
||||||
#
|
|
||||||
# enable-cache <service> <yes|no>
|
|
||||||
# positive-time-to-live <service> <time in seconds>
|
|
||||||
# negative-time-to-live <service> <time in seconds>
|
|
||||||
# suggested-size <service> <prime number>
|
|
||||||
# check-files <service> <yes|no>
|
|
||||||
# persistent <service> <yes|no>
|
|
||||||
# shared <service> <yes|no>
|
|
||||||
# max-db-size <service> <number bytes>
|
|
||||||
# auto-propagate <service> <yes|no>
|
|
||||||
#
|
|
||||||
# Currently supported cache names (services): passwd, group, hosts, services
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
# logfile /var/log/nscd.log
|
|
||||||
# threads 4
|
|
||||||
# max-threads 32
|
|
||||||
# server-user node
|
|
||||||
# stat-user somebody
|
|
||||||
debug-level 0
|
|
||||||
# reload-count 5
|
|
||||||
paranoia no
|
|
||||||
# restart-interval 3600
|
|
||||||
|
|
||||||
enable-cache passwd no
|
|
||||||
positive-time-to-live passwd 600
|
|
||||||
negative-time-to-live passwd 20
|
|
||||||
suggested-size passwd 211
|
|
||||||
check-files passwd yes
|
|
||||||
persistent passwd yes
|
|
||||||
shared passwd yes
|
|
||||||
max-db-size passwd 33554432
|
|
||||||
auto-propagate passwd yes
|
|
||||||
|
|
||||||
enable-cache group no
|
|
||||||
positive-time-to-live group 3600
|
|
||||||
negative-time-to-live group 60
|
|
||||||
suggested-size group 211
|
|
||||||
check-files group yes
|
|
||||||
persistent group yes
|
|
||||||
shared group yes
|
|
||||||
max-db-size group 33554432
|
|
||||||
auto-propagate group yes
|
|
||||||
|
|
||||||
enable-cache hosts yes
|
|
||||||
positive-time-to-live hosts 3600
|
|
||||||
negative-time-to-live hosts 20
|
|
||||||
suggested-size hosts 211
|
|
||||||
check-files hosts yes
|
|
||||||
persistent hosts yes
|
|
||||||
# Set shared to "no" to display stats in `nscd -g`
|
|
||||||
# Read more: https://stackoverflow.com/questions/40429245/nscdcentos7curl-0-dns-cache-hit-rate
|
|
||||||
shared hosts no
|
|
||||||
max-db-size hosts 33554432
|
|
||||||
|
|
||||||
enable-cache services no
|
|
||||||
positive-time-to-live services 28800
|
|
||||||
negative-time-to-live services 20
|
|
||||||
suggested-size services 211
|
|
||||||
check-files services yes
|
|
||||||
persistent services yes
|
|
||||||
shared services yes
|
|
||||||
max-db-size services 33554432
|
|
||||||
|
|
||||||
enable-cache netgroup no
|
|
||||||
positive-time-to-live netgroup 28800
|
|
||||||
negative-time-to-live netgroup 20
|
|
||||||
suggested-size netgroup 211
|
|
||||||
check-files netgroup yes
|
|
||||||
persistent netgroup yes
|
|
||||||
shared netgroup yes
|
|
||||||
max-db-size netgroup 33554432
|
|
@@ -1,31 +0,0 @@
|
|||||||
#
|
|
||||||
# This file MUST be edited with the 'visudo' command as root.
|
|
||||||
#
|
|
||||||
# Please consider adding local content in /etc/sudoers.d/ instead of
|
|
||||||
# directly modifying this file.
|
|
||||||
#
|
|
||||||
# See the man page for details on how to write a sudoers file.
|
|
||||||
#
|
|
||||||
Defaults env_reset
|
|
||||||
Defaults mail_badpass
|
|
||||||
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
||||||
|
|
||||||
# Host alias specification
|
|
||||||
|
|
||||||
# User alias specification
|
|
||||||
|
|
||||||
# Cmnd alias specification
|
|
||||||
|
|
||||||
# User privilege specification
|
|
||||||
root ALL=(ALL:ALL) ALL
|
|
||||||
|
|
||||||
# Allow members of group sudo to execute any command
|
|
||||||
%sudo ALL=(ALL:ALL) ALL
|
|
||||||
|
|
||||||
# See sudoers(5) for more information on "#include" directives:
|
|
||||||
|
|
||||||
#includedir /etc/sudoers.d
|
|
||||||
|
|
||||||
# Allow `node` to control service (mainly for nscd)
|
|
||||||
node ALL=(root) NOPASSWD: /usr/sbin/nscdservice
|
|
||||||
node ALL=(root) NOPASSWD: /usr/sbin/service
|
|
@@ -22,8 +22,7 @@ if (! exists) {
|
|||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
// Also update package-lock.json
|
// Also update package-lock.json
|
||||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
childProcess.spawnSync("npm", [ "install" ]);
|
||||||
childProcess.spawnSync(npm, [ "install" ]);
|
|
||||||
|
|
||||||
commit(version);
|
commit(version);
|
||||||
tag(version);
|
tag(version);
|
||||||
|
@@ -1,60 +0,0 @@
|
|||||||
require("dotenv").config();
|
|
||||||
const { NodeSSH } = require("node-ssh");
|
|
||||||
const readline = require("readline");
|
|
||||||
const rl = readline.createInterface({ input: process.stdin,
|
|
||||||
output: process.stdout });
|
|
||||||
const prompt = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
console.log("SSH to demo server");
|
|
||||||
const ssh = new NodeSSH();
|
|
||||||
await ssh.connect({
|
|
||||||
host: process.env.UPTIME_KUMA_DEMO_HOST,
|
|
||||||
port: process.env.UPTIME_KUMA_DEMO_PORT,
|
|
||||||
username: process.env.UPTIME_KUMA_DEMO_USERNAME,
|
|
||||||
privateKeyPath: process.env.UPTIME_KUMA_DEMO_PRIVATE_KEY_PATH
|
|
||||||
});
|
|
||||||
|
|
||||||
let cwd = process.env.UPTIME_KUMA_DEMO_CWD;
|
|
||||||
let result;
|
|
||||||
|
|
||||||
const version = await prompt("Enter Version: ");
|
|
||||||
|
|
||||||
result = await ssh.execCommand("git fetch --all", {
|
|
||||||
cwd,
|
|
||||||
});
|
|
||||||
console.log(result.stdout + result.stderr);
|
|
||||||
|
|
||||||
await prompt("Press any key to continue...");
|
|
||||||
|
|
||||||
result = await ssh.execCommand(`git checkout ${version} --force`, {
|
|
||||||
cwd,
|
|
||||||
});
|
|
||||||
console.log(result.stdout + result.stderr);
|
|
||||||
|
|
||||||
result = await ssh.execCommand("npm run download-dist", {
|
|
||||||
cwd,
|
|
||||||
});
|
|
||||||
console.log(result.stdout + result.stderr);
|
|
||||||
|
|
||||||
result = await ssh.execCommand("npm install --production", {
|
|
||||||
cwd,
|
|
||||||
});
|
|
||||||
console.log(result.stdout + result.stderr);
|
|
||||||
|
|
||||||
/*
|
|
||||||
result = await ssh.execCommand("pm2 restart 1", {
|
|
||||||
cwd,
|
|
||||||
});
|
|
||||||
console.log(result.stdout + result.stderr);*/
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
} finally {
|
|
||||||
rl.close();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// When done reading prompt, exit program
|
|
||||||
rl.on("close", () => process.exit(0));
|
|
48
extra/download-cloudflared.js
Normal file
48
extra/download-cloudflared.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//
|
||||||
|
|
||||||
|
const http = require("https"); // or 'https' for https:// URLs
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const platform = process.argv[2];
|
||||||
|
|
||||||
|
if (!platform) {
|
||||||
|
console.error("No platform??");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let arch = null;
|
||||||
|
|
||||||
|
if (platform === "linux/amd64") {
|
||||||
|
arch = "amd64";
|
||||||
|
} else if (platform === "linux/arm64") {
|
||||||
|
arch = "arm64";
|
||||||
|
} else if (platform === "linux/arm/v7") {
|
||||||
|
arch = "arm";
|
||||||
|
} else {
|
||||||
|
console.error("Invalid platform?? " + platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fs.createWriteStream("cloudflared.deb");
|
||||||
|
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download specified file
|
||||||
|
* @param {string} url URL to request
|
||||||
|
*/
|
||||||
|
function get(url) {
|
||||||
|
http.get(url, function (res) {
|
||||||
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
console.log("Redirect to " + res.headers.location);
|
||||||
|
get(res.headers.location);
|
||||||
|
} else if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
res.pipe(file);
|
||||||
|
|
||||||
|
res.on("end", function () {
|
||||||
|
console.log("Downloaded");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(res.statusCode);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -47,7 +47,6 @@ function download(url) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("Done");
|
console.log("Done");
|
||||||
process.exit(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tarStream.on("error", () => {
|
tarStream.on("error", () => {
|
||||||
|
1
extra/exe-builder/.gitignore
vendored
1
extra/exe-builder/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
packages/
|
|
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<configuration>
|
|
||||||
<startup>
|
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
|
||||||
</startup>
|
|
||||||
|
|
||||||
<runtime>
|
|
||||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Reflection" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.1" newVersion="4.1.1.1" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Runtime.InteropServices" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
</assemblyBinding>
|
|
||||||
</runtime>
|
|
||||||
</configuration>
|
|
84
extra/exe-builder/DownloadForm.Designer.cs
generated
84
extra/exe-builder/DownloadForm.Designer.cs
generated
@@ -1,84 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace UptimeKuma {
|
|
||||||
partial class DownloadForm {
|
|
||||||
/// <summary>
|
|
||||||
/// Required designer variable.
|
|
||||||
/// </summary>
|
|
||||||
private IContainer components = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clean up any resources being used.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
|
||||||
protected override void Dispose(bool disposing) {
|
|
||||||
if (disposing && (components != null)) {
|
|
||||||
components.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Windows Form Designer generated code
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Required method for Designer support - do not modify
|
|
||||||
/// the contents of this method with the code editor.
|
|
||||||
/// </summary>
|
|
||||||
private void InitializeComponent() {
|
|
||||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DownloadForm));
|
|
||||||
this.progressBar = new System.Windows.Forms.ProgressBar();
|
|
||||||
this.label = new System.Windows.Forms.Label();
|
|
||||||
this.labelData = new System.Windows.Forms.Label();
|
|
||||||
this.SuspendLayout();
|
|
||||||
//
|
|
||||||
// progressBar
|
|
||||||
//
|
|
||||||
this.progressBar.Location = new System.Drawing.Point(12, 12);
|
|
||||||
this.progressBar.Name = "progressBar";
|
|
||||||
this.progressBar.Size = new System.Drawing.Size(472, 41);
|
|
||||||
this.progressBar.TabIndex = 0;
|
|
||||||
//
|
|
||||||
// label
|
|
||||||
//
|
|
||||||
this.label.Location = new System.Drawing.Point(12, 59);
|
|
||||||
this.label.Name = "label";
|
|
||||||
this.label.Size = new System.Drawing.Size(472, 23);
|
|
||||||
this.label.TabIndex = 1;
|
|
||||||
this.label.Text = "Preparing...";
|
|
||||||
//
|
|
||||||
// labelData
|
|
||||||
//
|
|
||||||
this.labelData.Location = new System.Drawing.Point(12, 82);
|
|
||||||
this.labelData.Name = "labelData";
|
|
||||||
this.labelData.Size = new System.Drawing.Size(472, 23);
|
|
||||||
this.labelData.TabIndex = 2;
|
|
||||||
//
|
|
||||||
// DownloadForm
|
|
||||||
//
|
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
|
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
|
||||||
this.ClientSize = new System.Drawing.Size(496, 117);
|
|
||||||
this.Controls.Add(this.labelData);
|
|
||||||
this.Controls.Add(this.label);
|
|
||||||
this.Controls.Add(this.progressBar);
|
|
||||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
|
||||||
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
|
|
||||||
this.MaximizeBox = false;
|
|
||||||
this.Name = "DownloadForm";
|
|
||||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
|
||||||
this.Text = "Uptime Kuma";
|
|
||||||
this.Load += new System.EventHandler(this.DownloadForm_Load);
|
|
||||||
this.ResumeLayout(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private System.Windows.Forms.Label labelData;
|
|
||||||
|
|
||||||
private System.Windows.Forms.Label label;
|
|
||||||
|
|
||||||
private System.Windows.Forms.ProgressBar progressBar;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,204 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace UptimeKuma {
|
|
||||||
public partial class DownloadForm : Form {
|
|
||||||
private readonly Queue<DownloadItem> downloadQueue = new();
|
|
||||||
private readonly WebClient webClient = new();
|
|
||||||
private DownloadItem currentDownloadItem;
|
|
||||||
|
|
||||||
public DownloadForm() {
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DownloadForm_Load(object sender, EventArgs e) {
|
|
||||||
webClient.DownloadProgressChanged += DownloadProgressChanged;
|
|
||||||
webClient.DownloadFileCompleted += DownloadFileCompleted;
|
|
||||||
|
|
||||||
label.Text = "Reading latest version...";
|
|
||||||
|
|
||||||
// Read json from https://uptime.kuma.pet/version
|
|
||||||
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
|
|
||||||
var versionObj = JsonConvert.DeserializeObject<Version>(versionJson);
|
|
||||||
|
|
||||||
var nodeVersion = versionObj.nodejs;
|
|
||||||
var uptimeKumaVersion = versionObj.latest;
|
|
||||||
var hasUpdateFile = File.Exists("update");
|
|
||||||
|
|
||||||
if (!Directory.Exists("node")) {
|
|
||||||
downloadQueue.Enqueue(new DownloadItem {
|
|
||||||
URL = $"https://nodejs.org/dist/v{nodeVersion}/node-v{nodeVersion}-win-x64.zip",
|
|
||||||
Filename = "node.zip",
|
|
||||||
TargetFolder = "node"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists("core") || hasUpdateFile) {
|
|
||||||
|
|
||||||
// It is update, rename the core folder to core.old
|
|
||||||
if (Directory.Exists("core")) {
|
|
||||||
// Remove the old core.old folder
|
|
||||||
if (Directory.Exists("core.old")) {
|
|
||||||
Directory.Delete("core.old", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.Move("core", "core.old");
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadQueue.Enqueue(new DownloadItem {
|
|
||||||
URL = $"https://github.com/louislam/uptime-kuma/archive/refs/tags/{uptimeKumaVersion}.zip",
|
|
||||||
Filename = "core.zip",
|
|
||||||
TargetFolder = "core"
|
|
||||||
});
|
|
||||||
|
|
||||||
File.WriteAllText("version.json", versionJson);
|
|
||||||
|
|
||||||
// Delete the update file
|
|
||||||
if (hasUpdateFile) {
|
|
||||||
File.Delete("update");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadNextFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadNextFile() {
|
|
||||||
if (downloadQueue.Count > 0) {
|
|
||||||
var item = downloadQueue.Dequeue();
|
|
||||||
|
|
||||||
currentDownloadItem = item;
|
|
||||||
|
|
||||||
// Download if the zip file is not existing
|
|
||||||
if (!File.Exists(item.Filename)) {
|
|
||||||
label.Text = item.URL;
|
|
||||||
webClient.DownloadFileAsync(new Uri(item.URL), item.Filename);
|
|
||||||
} else {
|
|
||||||
progressBar.Value = 100;
|
|
||||||
label.Text = "Use local " + item.Filename;
|
|
||||||
DownloadFileCompleted(null, null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
npmSetup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void npmSetup() {
|
|
||||||
labelData.Text = "";
|
|
||||||
|
|
||||||
var npm = "..\\node\\npm.cmd";
|
|
||||||
var cmd = $"{npm} ci --production & {npm} run download-dist & exit";
|
|
||||||
|
|
||||||
var startInfo = new ProcessStartInfo {
|
|
||||||
FileName = "cmd.exe",
|
|
||||||
Arguments = $"/k \"{cmd}\"",
|
|
||||||
RedirectStandardOutput = false,
|
|
||||||
RedirectStandardError = false,
|
|
||||||
RedirectStandardInput = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = false,
|
|
||||||
WorkingDirectory = "core"
|
|
||||||
};
|
|
||||||
|
|
||||||
var process = new Process();
|
|
||||||
process.StartInfo = startInfo;
|
|
||||||
process.EnableRaisingEvents = true;
|
|
||||||
process.Exited += (_, e) => {
|
|
||||||
progressBar.Value = 100;
|
|
||||||
|
|
||||||
if (process.ExitCode == 0) {
|
|
||||||
Task.Delay(2000).ContinueWith(_ => {
|
|
||||||
Application.Restart();
|
|
||||||
});
|
|
||||||
label.Text = "Done";
|
|
||||||
} else {
|
|
||||||
label.Text = "Failed, exit code: " + process.ExitCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
process.Start();
|
|
||||||
label.Text = "Installing dependencies and download dist files";
|
|
||||||
progressBar.Value = 50;
|
|
||||||
process.WaitForExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) {
|
|
||||||
progressBar.Value = e.ProgressPercentage;
|
|
||||||
var total = e.TotalBytesToReceive / 1024;
|
|
||||||
var current = e.BytesReceived / 1024;
|
|
||||||
|
|
||||||
if (total > 0) {
|
|
||||||
labelData.Text = $"{current}KB/{total}KB";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadFileCompleted(object sender, AsyncCompletedEventArgs e) {
|
|
||||||
Extract(currentDownloadItem);
|
|
||||||
DownloadNextFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Extract(DownloadItem item) {
|
|
||||||
if (Directory.Exists(item.TargetFolder)) {
|
|
||||||
var dir = new DirectoryInfo(item.TargetFolder);
|
|
||||||
dir.Delete(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists("temp")) {
|
|
||||||
var dir = new DirectoryInfo("temp");
|
|
||||||
dir.Delete(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
labelData.Text = $"Extracting {item.Filename}...";
|
|
||||||
|
|
||||||
ZipFile.ExtractToDirectory(item.Filename, "temp");
|
|
||||||
|
|
||||||
string[] dirList;
|
|
||||||
|
|
||||||
// Move to the correct level
|
|
||||||
dirList = Directory.GetDirectories("temp");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (dirList.Length > 0) {
|
|
||||||
var dir = dirList[0];
|
|
||||||
|
|
||||||
// As sometime ExtractToDirectory is still locking the directory, loop until ok
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
Directory.Move(dir, item.TargetFolder);
|
|
||||||
break;
|
|
||||||
} catch (Exception exception) {
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
MessageBox.Show("Unexcepted Error: Cannot move extracted files, folder not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
labelData.Text = $"Extracted";
|
|
||||||
|
|
||||||
if (Directory.Exists("temp")) {
|
|
||||||
var dir = new DirectoryInfo("temp");
|
|
||||||
dir.Delete(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
File.Delete(item.Filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DownloadItem {
|
|
||||||
public string URL { get; set; }
|
|
||||||
public string Filename { get; set; }
|
|
||||||
public string TargetFolder { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,377 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
|
||||||
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>
|
|
||||||
AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAA
|
|
||||||
AABgAAAAAQAgAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAA////BPT09Bfu7u4e8fHxJPPz8yv19fUy9fX1M/Pz8yvx8fEk9vb2HPPz8xXMzMwFAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//
|
|
||||||
/wHv7+8f7u7uPPPz81Tx8fFs8fHxgPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGB8fHxcfHx8V3x8fFI9PT0MOvr6w0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AADy8vIU8fHxS/Dw8Hbx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fFr9PT0R/Dw8CIAAAABAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAA8vLyFPHx8Vnx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fFs9fX1Mb+/vwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAICAgALy8vI88fHxfvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy8nby8vI8gICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAzMzMBfHx8Vrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyYf///wwAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8vLyYPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8W/z8/MWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+9R8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLw8PB26urqDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLy8ijx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu7w7Ifj79ud2u7PtNLrw83P677dzeu85c3r
|
|
||||||
u+rM67rwzOu68c7rverQ68Dj0uvD3NbuyM3b7c+64u7apujv5ZPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxXgAAAAEAAAAAAAAAAAAAAAAAAAAA4+PjCfDw
|
|
||||||
8Hfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLd7tSmzeu92MbqsvvG6bH/xumy/8fq
|
|
||||||
s//H6rP/yOq0/8jqtf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//Q7MDx1u7Kz9/t
|
|
||||||
163s8OuJ8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu/v7y8AAAAAAAAAAAAA
|
|
||||||
AAAAAAAA7u7uPfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC5PDdl8jqtuTE6a7/xOmv/8Xp
|
|
||||||
sP/G6bH/xumx/8bpsv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zr
|
|
||||||
u//N67v/zey8/87svf/P67742e3Mx+jv5ZLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw
|
|
||||||
8HWAgIACAAAAAAAAAACqqqoD8vLyc/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLf7degxOiu+cPo
|
|
||||||
rf/D6a7/xOmu/8Xpr//F6bD/xumx/8bpsf/G6bL/x+qz/8fqs//I6rT/yOq1/8nqtv/J6rb/yuu3/8rr
|
|
||||||
uP/L67j/y+u5/8zruv/M67v/zeu7/83svP/O7L3/zuy9/87svfzc7tK28fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fEkAAAAAAAAAADz8/Mq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgunv
|
|
||||||
5o3D6a/0wuis/8Lorf/D6K3/xOmu/8Tprv/F6a//xemw/8bpsf/G6bH/xumy/8fqs//H6rP/yOq0/8jq
|
|
||||||
tf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/87svf/O7L3/3e/TtPHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAADy8vJM8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgszqutDB6Kv/weir/8LorP/D6K3/w+it/8Tprv/E6a7/xemv/8XpsP/G6bH/xumx/8bp
|
|
||||||
sv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zru//N67v/zey8/87s
|
|
||||||
vf/O7L3/zuy++u3w6Yzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJ1AAAAAAAAAADx8fFr8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC6O/kjsDoqvzA6Kr/weir/8Loq//C6Kz/w+it/8Porf/E6a7/xOmu/8Xp
|
|
||||||
r//F6bD/xumx/8bpsf/G6bL/x+qz/8fqtP/I6rT/yOq1/8nqtv/J6rb/yuu3/8rruP/L67n/y+u5/8zr
|
|
||||||
uv/M67v/zeu7/83svP/O7L3/zuy9/93u07Xx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC////Bv//
|
|
||||||
/wfx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1ezJsr/nqf/A56n/weiq/8Hoq//C6Kv/wuis/8Po
|
|
||||||
rf/D6K3/xOmu/8Pprv+856T/uOed/7bmmv+05Zf/teWZ/7jnnf+86KP/wOio/8fqs//J6rb/yeq2/8rr
|
|
||||||
t//K67j/y+u5/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/9buyNLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8vLyE/Ly8hPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCy+q6zr/nqP/A56n/wOep/8Ho
|
|
||||||
qv/B6Kv/wuir/8LorP+u5Y//neF2/5bgav+V4Gr/luBr/5fhbP+Y4W7/meFv/5rhcf+b4nL/nOJ0/53i
|
|
||||||
dv+j5H//reaM/7nnnf/E6q//y+y4/8vruf/L67n/zOu6/8zru//N67v/zey8/9Lsxd/x8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC7+/vIPb29hzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/n
|
|
||||||
qP+/56j/wOep/8Dnqf/B6Kr/weir/7nmn/+R32T/kt9l/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nh
|
|
||||||
b/+a4XH/m+Jy/5zidP+d4nX/nuN3/5/jeP+f4nn/weqq/8rruP/L67n/y+u5/8zruv/M67v/zeu7/9Ls
|
|
||||||
w+Lx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwI/Hx8SXx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGCxeix5L/nqP+/56j/v+eo/8Dnqf/A56n/weiq/7Pllv+Q3mP/kd9k/5LfZf+T32f/lOBo/5Xg
|
|
||||||
av+W4Gv/l+Ft/5jhbv+Z4W//muFx/5vicv+c4nT/neJ1/57jd/+f43j/xOmu/8rrt//K67j/y+u5/8vr
|
|
||||||
uf/M67r/zOu7/9Tsxtfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC9PT0GO/v7yDx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGCx+m037/nqP+/56j/v+eo/7/nqP/A56n/wOip/7TmmP+P3mH/kN5j/5Hf
|
|
||||||
ZP+S32b/k99n/5TgaP+V4Gr/luBr/5fhbf+Y4W7/meFw/5rhcf+b4nL/nOJ0/53idf+h5Hz/yuu2/8nq
|
|
||||||
t//K67f/yuu4/8vruf/L67n/zOu6/9ftysrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7e3tDvT0
|
|
||||||
9Bfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCyOq117/nqP+/56j/v+eo/7/nqP+/56j/wOep/7vn
|
|
||||||
of+O3mD/j95h/5DeY/+R32T/kt9m/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nhcP+a4nH/m+Jy/5zi
|
|
||||||
dP+r5Yr/yOq1/8nqtv/J6rf/yuu3/8rruP/L67n/y+u5/9zu1LHx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLz8/OA////A+7u7g/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCz+q+xb/nqP+/56j/v+eo/7/n
|
|
||||||
qP+/56j/v+eo/8Dnqf+S4Gb/jt5g/4/eYf+Q3mP/kd9k/5LfZv+T32f/lOBo/5Xgav+W4Gv/l+Ft/5jh
|
|
||||||
bv+Z4XD/muJx/5vic/+4553/yOq0/8jqtf/J6rb/yeq3/8rrt//K67j/y+u5/+bw4Zfx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fFrAAAAAP///wHz8/N88fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1+zMrr/n
|
|
||||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+f4Xn/jd5f/47eYP+P3mH/kN5j/5HfZP+S32b/k99n/5Tg
|
|
||||||
af+V4Gr/luBr/5fhbf+Y4W7/meFw/5vic//F6rD/x+q0/8jqtP/I6rX/yeq2/8nqt//K67f/zOu88u/x
|
|
||||||
74Px8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLv7+9QAAAAAAAAAADw8PBm8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC5e7gk7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//jN1d/43eX/+O3mD/j95h/5De
|
|
||||||
Y/+R32T/kt9m/5PfZ/+U4Gn/leBq/5bga/+X4W3/mOFu/6rliP/G6rL/x+qz/8fqtP/I6rT/yOq1/8nq
|
|
||||||
tv/J6rf/1OzGy/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YL19fUzAAAAAAAAAADy8vJO8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgsPoru2/56j/v+eo/7/nqP+/56j/v+eo/7/nqP++6Kf/j95i/4zd
|
|
||||||
Xf+N3l//jt5g/4/eYv+Q3mP/kd9k/5LfZv+T32f/lOBp/5Xgav+W4Gz/l+Ft/7voov/G6bL/xuqy/8fq
|
|
||||||
s//H6rT/yOq1/8jqtf/J6rb/4e/Zo/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PARAAAAAAAA
|
|
||||||
AADu7u4u8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgszpvMm/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
|
||||||
qP+/56j/q+SL/4vdXP+M3V3/jd5f/47eYP+P3mL/kN9j/5HfZP+S32b/k99n/5Tgaf+V4Gr/qOOH/8Xp
|
|
||||||
sP/G6bH/xumy/8bqsv/H6rP/x+q0/8jqtf/K67jy8PHwhPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8WoAAAAAAAAAAAAAAADo6OgL8fHxgfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguDv2J2/56j/v+eo/7/n
|
|
||||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/6Xjgv+L3Vz/jN1d/43eX/+O3mD/j95i/5DfY/+R32T/kt9m/5Pf
|
|
||||||
Z/+k44D/xOmu/8XpsP/F6bD/xumx/8bpsv/G6rL/x+qz/8fqtP/W7cnB8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvPz80AAAAAAAAAAAAAAAAAAAAAA8PDwZ/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLD6K/rv+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//kt5n/4zdXf+N3l//jt5g/4/e
|
|
||||||
Yv+Q32P/luFs/67kj//D6K3/xOmu/8Tpr//F6bD/xemw/8bpsf/G6bL/xuqy/8fqtP7o7+WR8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xYAAAAAAAAAAAAAAAAAAAAA8vLyPPHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLV7ci0v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOio/7Xl
|
|
||||||
mv+u5I7/rOSM/67kj/+35pz/wumr/8Lorf/D6K3/w+it/8Tprv/E6a//xemw/8XpsP/G6bH/xumy/9Ds
|
|
||||||
wNPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyZQAAAAAAAAAAAAAAAAAAAAAAAAAA////DPHx
|
|
||||||
8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
|
||||||
qP+/56j/v+eo/7/nqP+/56j/wOep/8Doqv/B6Kr/weir/8LorP/C6K3/w+it/8Porv/E6a7/xOmv/8Xp
|
|
||||||
sP/F6bD/yOq18uvw6Yvx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7+/vMQAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAPHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6O/ij8LorPG/56j/v+eo/7/n
|
|
||||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weiq/8Hoq//C6Kz/wuit/8Po
|
|
||||||
rf/D6K7/xOmu/8Tpr//F6bH74u/anvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PB6////BQAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPPz8yrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguHu
|
|
||||||
2pnB56v2v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/wOiq/8Ho
|
|
||||||
q//B6Kv/wuis/8Lorf/D6K3/w+mu/8Tprv3b7dKq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fFJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHy8vJf8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLi7tyXwumt8L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
|
||||||
qP+/56j/wOep/8Doqv/B6Kv/weir/8LorP/C6K3/xOiv+d7u1aTx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvLy8nb///8KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+8Q8/Pze/Hx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6/Dpiszqu82/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
|
||||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weir/8Hoq//H6bTj5e7elfHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8yoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAA9fX1MvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLe7tShx+mz3r/n
|
|
||||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/xumy5drtz6rv8e+D8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyTgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPHx8Unx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgubv45DU68e2y+q6z8XoseTD6a7uweir9MPpru7F6bHly+q50tLsxLrl796U8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJh////AwAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wHx8fFZ8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8Wzf398IAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8D8/PzVfHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwZujo
|
|
||||||
6AsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAA////AfHx8Ujx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fFa////BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/Mp8vLydvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8/PzfPHx8TcAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CvLy8lDz8/N/8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvPz84Hx8fFa8PDwEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AADw8PAR8vLyTvHx8X3x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fF/8/PzVvT09BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wXz8/Mq8/PzU/Hx8XDx8fGB8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLy8vJz8fHxWO/v7y////8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8G7e3tHfLy
|
|
||||||
8ifu7u4u8PDwNPT09C/y8vIo7+/vH+Pj4wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAP///////wAA////////AAD///////8AAP//gAf//wAA//gAAD//AAD/wAAAB/8AAP+A
|
|
||||||
AAAB/wAA/gAAAAB/AAD8AAAAAD8AAPgAAAAAHwAA8AAAAAAPAADwAAAAAAcAAOAAAAAABwAA4AAAAAAD
|
|
||||||
AADAAAAAAAMAAMAAAAAAAwAAwAAAAAABAACAAAAAAAEAAIAAAAAAAQAAgAAAAAABAACAAAAAAAEAAIAA
|
|
||||||
AAAAAQAAgAAAAAABAACAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAAABwAAwAAAAAAH
|
|
||||||
AADgAAAAAAcAAOAAAAAADwAA4AAAAAAPAADwAAAAAB8AAPAAAAAAHwAA+AAAAAA/AAD8AAAAAD8AAPwA
|
|
||||||
AAAAfwAA/gAAAAD/AAD/AAAAAf8AAP+AAAAD/wAA/8AAAAf/AAD/8AAAH/8AAP/8AAA//wAA//8AAf//
|
|
||||||
AAD//+AP//8AAP///////wAA////////AAD///////8AACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAgICAAu/v7xD09PQX7u7uHvDw8CP29vYb8vLyFOrq6gwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICA
|
|
||||||
gALy8vIm7+/vT/Pz82fz8/N98fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw8Hrw8PBm7+/vUPT0
|
|
||||||
9C3o6OgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOPj
|
|
||||||
4wnz8/NC8vLydPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YHy8vJj8/PzKoCAgAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AADx8fEl8vLydfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxcfHx8SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAA9PT0LfHx8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8/PzgPLy8j0AAAABAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAO3t7Rzx8fGA8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLr8OmM5O7emeTv
|
|
||||||
3Z7h79mj5fDem+nv45Tu8u6H8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy
|
|
||||||
8joAAAAAAAAAAAAAAAD///8E8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC7vDshtns0K7N67zayeq288fq
|
|
||||||
s//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/P7L7w0+zF29vv0Lrn8OKX8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8/PzfvPz8xUAAAAAAAAAAPX19TLx8fGC8fHxgvHx8YLx8fGC8fHxgt3u1KXF6rHzxOmv/8Xp
|
|
||||||
sP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/M67v/zey8/87svf/S7MPj4u7Zp/Hx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8/PzVQAAAAAAAAAA8fHxavHx8YLx8fGC8fHxgvHx8YLf7defwuis/cPo
|
|
||||||
rf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruv/M67v/zey8/87s
|
|
||||||
vf/N67z/3e7SufHx8YLx8fGC8fHxgvHx8YLz8/N8////Bf///w3x8fGC8fHxgvHx8YLx8fGC8fHxgsXp
|
|
||||||
sOnB6Kv/wuis/8Porf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vr
|
|
||||||
uv/M67v/zey8/87svf/O67z96/Hoj/Hx8YLx8fGC8fHxgvHx8YLy8vIm8/PzK/Hx8YLx8fGC8fHxgvHx
|
|
||||||
8YLg79icwOep/8Hoqv/B6Kv/wuis/8Porf/E6a7/wuit/73opP+76KL/u+eh/77opv/D6a3/yeu1/8nq
|
|
||||||
tv/K67f/y+u5/8zruv/M67v/zey8/87svf/d7tSz8fHxgvHx8YLx8fGC8fHxgvHx8Tby8vI68fHxgvHx
|
|
||||||
8YLx8fGC8fHxgtTrxre/56j/wOep/8Hoqv/B6Kv/uOad/53idv+V4Gn/leBq/5fhbP+Y4W//muFx/5vi
|
|
||||||
c/+e4Xb/puWD/7PmlP/D6a3/y+u5/8zruv/M67v/zey8/9rtzsHx8fGC8fHxgvHx8YLx8fGC8/PzQfPz
|
|
||||||
80Lx8fGC8fHxgvHx8YLx8fGC0OvAwr/nqP+/56j/wOep/8Hoqv+o44b/kd9k/5LfZv+U4Gj/leBq/5fh
|
|
||||||
bf+Y4W//muFx/5vic/+d4nX/n+N3/7fnm//K67j/y+u5/8zruv/M67v/2u3QvPHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLy8vI98/PzP/Hx8YLx8fGC8fHxgvHx8YLQ6sK/v+eo/7/nqP+/56j/wOep/6jjhv+P3mL/kd9k/5Lf
|
|
||||||
Zv+U4Gj/leBr/5fhbf+Y4W//muFx/5zic/+d4nX/v+mm/8nqt//K67j/y+u5/8zruv/f79au8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvX19TLx8fE38fHxgvHx8YLx8fGC8fHxgtTrybO/56j/v+eo/7/nqP+/56j/sOSS/47e
|
|
||||||
YP+P3mL/kd9k/5LfZv+U4Gj/leBr/5fhbf+Z4W//muJx/5/jd//H6bP/yeq2/8nqt//K67j/y+u5/+nv
|
|
||||||
45Tx8fGC8fHxgvHx8YLx8fGC7+/vIPHx8SXx8fGC8fHxgvHx8YLx8fGC4e/Zm7/nqP+/56j/v+eo/7/n
|
|
||||||
qP+956X/jt5h/47eYP+P3mL/kd9k/5LfZv+U4Gn/luBr/5fhbf+Z4W//q+aK/8fqs//I6rT/yeq2/8nq
|
|
||||||
t//N7Lvw8fHxgvHx8YLx8fGC8fHxgvPz84D///8G6+vrDfHx8YLx8fGC8fHxgvHx8YLv8e+Dweis87/n
|
|
||||||
qP+/56j/v+eo/7/nqP+d4XX/jN1e/47eYP+P3mL/kd9k/5PfZ/+U4Gn/luBr/5fhbf+86KP/xuqy/8fq
|
|
||||||
s//I6rX/yeq2/9Tsx8nx8fGC8fHxgvHx8YLx8fGC8PDwaAAAAAAAAAAA8fHxbPHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLM6rrMv+eo/7/nqP+/56j/v+eo/7blmv+N3V//jN1e/47eYP+Q3mL/kd9k/5PfZ/+U4Gn/qeSH/8Xp
|
|
||||||
sP/G6bH/xuqy/8fqs//I6rX/5fDem/Hx8YLx8fGC8fHxgvHx8YLz8/M/AAAAAAAAAADz8/NB8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgt3s06O/56j/v+eo/7/nqP+/56j/v+eo/7Xmmf+U32n/jN1e/47eYP+Q3mL/k99o/6zk
|
|
||||||
i//D6a7/xemv/8XpsP/G6bH/xuqy/8vqu+jx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xUAAAAAAAAAAPT0
|
|
||||||
9Bfx8fGC8fHxgvHx8YLx8fGC8fHvg8Tpsee/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+35pz/suWV/7Xm
|
|
||||||
mf/A6Kj/wuit/8Porf/E6a7/xemv/8XpsP/G6bH/3e3UqvHx8YLx8fGC8fHxgvHx8YLw8PBmAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAPHx8W7x8fGC8fHxgvHx8YLx8fGC4u7cmMHnqvm/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
|
||||||
qP+/56j/wOep/8Hoqv/C6Kz/wuit/8Porf/E6a7/xemv/9Hrwszx8fGC8fHxgvHx8YLx8fGC8fHxgvX1
|
|
||||||
9TEAAAAAAAAAAAAAAAAAAAAA7u7uO/Hx8YLx8fGC8fHxgvHx8YLx8fGC3e7SpMHoqfq/56j/v+eo/7/n
|
|
||||||
qP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz/wuit/8Porf/O67zV8PHwhPHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLy8vJ2////BQAAAAAAAAAAAAAAAAAAAACqqqoD8PDwafHx8YLx8fGC8fHxgvHx8YLx8fGC4O/YnMTo
|
|
||||||
ruy/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz90uvEwe/x74Px8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvPz8ykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/MW8fHxfPHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8PLuhdXtyLXF6bHlv+eo/7/nqP+/56j/v+eo/7/nqP/B6Kv0zeq8zOXv4JTx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADy8vIm8fHxgPHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLs8OmJ4e/Zm93u06Pf7def5+/hkvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxXf///wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AADy8vIo8/PzffHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8VnMzMwFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAD29vYb8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz83/v7+9BgICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8/PzQPLy8nnx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz84Hx8fFc9PT0GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////B/X19TLx8fFc8PDwevHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgPHx8Wv09PRE9PT0FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAA7+/vEPb29hvw8PAj7+/vH/T09Be/v78EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////8B///wAA//wAAD/wAAAP4AAAB+AA
|
|
||||||
AAfAAAADwAAAA4AAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAADwAAAA8AAAAPAAAAH4AAAB+AA
|
|
||||||
AA/wAAAP+AAAH/gAAD/+AAB//wAB///AA///+B////////////8oAAAAEAAAACAAAAABACAAAAAAAAAE
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CfDw8BH///8GAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAAu7u7i7x8fFe8PDwevHx8YLx8fGC8fHxgvDw
|
|
||||||
8Hvx8fFs7+/vT/Dw8CMAAAABAAAAAAAAAAAAAAAA5ubmCvLy8l/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
|
||||||
8YLx8fGC8fHxgvHx8YLx8fGC8/PzZu7u7g8AAAAAAAAAAPHx8V3x8fGC8fHxgunv5o7Z7c200+vFytTs
|
|
||||||
xc7W7cnH2+7QueLu2qbu8OyH8fHxgvHx8YLx8fFu////BfHx8STx8fGC8fHxgtrtzq3D6a/8xemw/8bp
|
|
||||||
sv/I6rT/yeq2/8vruP/M67v/z+u++Nzu0bjx8fGC8fHxgu/v7zDx8fFI8fHxguzw6ojC56z3wuis/8Tp
|
|
||||||
rv/E6q3/weiq/8fqsv/J6rb/y+u5/8zru//N67z/6/HpjfHx8YLy8vJN8fHxXPHx8YLg79icv+eo/8Ho
|
|
||||||
qv+k4n//lOBo/5fhbf+a4XH/n+J5/7Pmlv/L67n/zOu7/+Xw353x8fGC8fHxXvHx8Vrx8fGC4O3Zm7/n
|
|
||||||
qP+/56j/nuF3/5HfZP+U4Gj/l+Ft/5ricf+x5pL/yeq3/8vruf/r8emN8fHxgu/v70/x8fFK8fHxguzw
|
|
||||||
6ojA6Kn8v+eo/6njiP+O3mD/kd9k/5Tgaf+X4W3/vuim/8jqtP/N67zr8fHxgvHx8YLy8vI68/PzK/Hx
|
|
||||||
8YLx8fGCx+m03L/nqP++6Kb/meBw/47eYP+S32X/q+SL/8XpsP/G6rL/1+zLvvHx8YLz8/OB8PDwEdXV
|
|
||||||
1Qbx8fF98fHxgt/t1Z/A56j9v+eo/7/nqP+656H/vuim/8Lorf/E6a7/yOq18Ovw6Yvx8fGC8vLyYwAA
|
|
||||||
AAAAAAAA8fHxR/Hx8YLx8fGC2O3NrMDnqfq/56j/v+eo/7/nqP/B6Kv/xumy7OTu3Zfx8fGC8/PzgfLy
|
|
||||||
8icAAAAAAAAAAP///wPz8/Nm8fHxgvHx8YLo7+SO0+zFuczquszM6bzJ1+zMru7w7Ibx8fGC8fHxgvHx
|
|
||||||
8UcAAAAAAAAAAAAAAAAAAAAA4+PjCfHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgfPz
|
|
||||||
80D///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8/PzK/Ly8mDz8/N+8fHxgvHx8YLy8vJ68vLyUezs
|
|
||||||
7BsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAevr6w3j4+MJAAAAAAAA
|
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD8fwAA4AcAAMADAACAAQAAgAEAAIABAACAAQAAgAEAAIAB
|
|
||||||
AADAAwAAwAMAAOAHAADwDwAA/n8AAP//AAA=
|
|
||||||
</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
@@ -1,3 +0,0 @@
|
|||||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
|
||||||
<Costura DisableCompression='true' IncludeDebugSymbols='false' />
|
|
||||||
</Weavers>
|
|
@@ -1,141 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
||||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
|
||||||
<xs:element name="Weavers">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:all>
|
|
||||||
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:all>
|
|
||||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:element>
|
|
||||||
</xs:all>
|
|
||||||
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="DisableCompression" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="DisableCleanup" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="ExcludeAssemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="IncludeAssemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with |.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with |.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="PreloadOrder" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
</xs:all>
|
|
||||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
</xs:schema>
|
|
@@ -1,243 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using UptimeKuma.Properties;
|
|
||||||
|
|
||||||
namespace UptimeKuma {
|
|
||||||
static class Program {
|
|
||||||
/// <summary>
|
|
||||||
/// The main entry point for the application.
|
|
||||||
/// </summary>
|
|
||||||
[STAThread]
|
|
||||||
static void Main(string[] args) {
|
|
||||||
var cwd = Path.GetDirectoryName(Application.ExecutablePath);
|
|
||||||
|
|
||||||
if (cwd != null) {
|
|
||||||
Environment.CurrentDirectory = cwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
Application.EnableVisualStyles();
|
|
||||||
Application.SetCompatibleTextRenderingDefault(false);
|
|
||||||
Application.Run(new UptimeKumaApplicationContext());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UptimeKumaApplicationContext : ApplicationContext
|
|
||||||
{
|
|
||||||
private static Mutex mutex = null;
|
|
||||||
|
|
||||||
const string appName = "Uptime Kuma";
|
|
||||||
|
|
||||||
private NotifyIcon trayIcon;
|
|
||||||
private Process process;
|
|
||||||
|
|
||||||
private MenuItem statusMenuItem;
|
|
||||||
private MenuItem runWhenStarts;
|
|
||||||
private MenuItem openMenuItem;
|
|
||||||
|
|
||||||
private RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
|
|
||||||
|
|
||||||
|
|
||||||
public UptimeKumaApplicationContext() {
|
|
||||||
|
|
||||||
// Single instance only
|
|
||||||
bool createdNew;
|
|
||||||
mutex = new Mutex(true, appName, out createdNew);
|
|
||||||
if (!createdNew) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startingText = "Starting server...";
|
|
||||||
trayIcon = new NotifyIcon();
|
|
||||||
trayIcon.Text = startingText;
|
|
||||||
|
|
||||||
runWhenStarts = new MenuItem("Run when system starts", RunWhenStarts);
|
|
||||||
runWhenStarts.Checked = registryKey.GetValue(appName) != null;
|
|
||||||
|
|
||||||
statusMenuItem = new MenuItem(startingText);
|
|
||||||
statusMenuItem.Enabled = false;
|
|
||||||
|
|
||||||
openMenuItem = new MenuItem("Open", Open);
|
|
||||||
openMenuItem.Enabled = false;
|
|
||||||
|
|
||||||
trayIcon.Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location);
|
|
||||||
trayIcon.ContextMenu = new ContextMenu(new MenuItem[] {
|
|
||||||
statusMenuItem,
|
|
||||||
openMenuItem,
|
|
||||||
//new("Debug Console", DebugConsole),
|
|
||||||
runWhenStarts,
|
|
||||||
new("Check for Update...", CheckForUpdate),
|
|
||||||
new("Visit GitHub...", VisitGitHub),
|
|
||||||
new("About", About),
|
|
||||||
new("Exit", Exit),
|
|
||||||
});
|
|
||||||
|
|
||||||
trayIcon.MouseDoubleClick += new MouseEventHandler(Open);
|
|
||||||
trayIcon.Visible = true;
|
|
||||||
|
|
||||||
var hasUpdateFile = File.Exists("update");
|
|
||||||
|
|
||||||
if (!hasUpdateFile && Directory.Exists("core") && Directory.Exists("node") && Directory.Exists("core/node_modules") && Directory.Exists("core/dist")) {
|
|
||||||
// Go go go
|
|
||||||
StartProcess();
|
|
||||||
} else {
|
|
||||||
DownloadFiles();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DownloadFiles() {
|
|
||||||
var form = new DownloadForm();
|
|
||||||
form.Closed += Exit;
|
|
||||||
form.Show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RunWhenStarts(object sender, EventArgs e) {
|
|
||||||
if (registryKey == null) {
|
|
||||||
MessageBox.Show("Error: Unable to set startup registry key.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runWhenStarts.Checked) {
|
|
||||||
registryKey.DeleteValue(appName, false);
|
|
||||||
runWhenStarts.Checked = false;
|
|
||||||
} else {
|
|
||||||
registryKey.SetValue(appName, Application.ExecutablePath);
|
|
||||||
runWhenStarts.Checked = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void StartProcess() {
|
|
||||||
var startInfo = new ProcessStartInfo {
|
|
||||||
FileName = "node/node.exe",
|
|
||||||
Arguments = "server/server.js --data-dir=\"../data/\"",
|
|
||||||
RedirectStandardOutput = false,
|
|
||||||
RedirectStandardError = false,
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
WorkingDirectory = "core"
|
|
||||||
};
|
|
||||||
|
|
||||||
process = new Process();
|
|
||||||
process.StartInfo = startInfo;
|
|
||||||
process.EnableRaisingEvents = true;
|
|
||||||
process.Exited += ProcessExited;
|
|
||||||
|
|
||||||
try {
|
|
||||||
process.Start();
|
|
||||||
//Open(null, null);
|
|
||||||
|
|
||||||
// Async task to check if the server is ready
|
|
||||||
Task.Run(() => {
|
|
||||||
var runningText = "Server is running";
|
|
||||||
using TcpClient tcpClient = new TcpClient();
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
tcpClient.Connect("127.0.0.1", 3001);
|
|
||||||
statusMenuItem.Text = runningText;
|
|
||||||
openMenuItem.Enabled = true;
|
|
||||||
trayIcon.Text = runningText;
|
|
||||||
break;
|
|
||||||
} catch (Exception) {
|
|
||||||
System.Threading.Thread.Sleep(2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
MessageBox.Show("Startup failed: " + e.Message, "Uptime Kuma Error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void StopProcess() {
|
|
||||||
process?.Kill();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Open(object sender, EventArgs e) {
|
|
||||||
Process.Start("http://localhost:3001");
|
|
||||||
}
|
|
||||||
|
|
||||||
void DebugConsole(object sender, EventArgs e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void CheckForUpdate(object sender, EventArgs e) {
|
|
||||||
var needUpdate = false;
|
|
||||||
|
|
||||||
// Check version.json exists
|
|
||||||
if (File.Exists("version.json")) {
|
|
||||||
// Load version.json and compare with the latest version from GitHub
|
|
||||||
var currentVersionObj = JsonConvert.DeserializeObject<Version>(File.ReadAllText("version.json"));
|
|
||||||
|
|
||||||
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
|
|
||||||
var latestVersionObj = JsonConvert.DeserializeObject<Version>(versionJson);
|
|
||||||
|
|
||||||
// Compare version, if the latest version is newer, then update
|
|
||||||
if (new System.Version(latestVersionObj.latest).CompareTo(new System.Version(currentVersionObj.latest)) > 0) {
|
|
||||||
var result = MessageBox.Show("A new version is available. Do you want to update?", "Update", MessageBoxButtons.YesNo);
|
|
||||||
if (result == DialogResult.Yes) {
|
|
||||||
// Create a empty file `update`, so the app will download the core files again at startup
|
|
||||||
File.Create("update").Close();
|
|
||||||
|
|
||||||
trayIcon.Visible = false;
|
|
||||||
process?.Kill();
|
|
||||||
|
|
||||||
// Restart the app, it will download the core files again at startup
|
|
||||||
Application.Restart();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MessageBox.Show("You are using the latest version.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void VisitGitHub(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
Process.Start("https://github.com/louislam/uptime-kuma");
|
|
||||||
}
|
|
||||||
|
|
||||||
void About(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
MessageBox.Show("Uptime Kuma Windows Runtime v1.0.0" + Environment.NewLine + "© 2023 Louis Lam", "Info");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Exit(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
// Hide tray icon, otherwise it will remain shown until user mouses over it
|
|
||||||
trayIcon.Visible = false;
|
|
||||||
process?.Kill();
|
|
||||||
Application.Exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProcessExited(object sender, EventArgs e) {
|
|
||||||
|
|
||||||
if (process.ExitCode != 0) {
|
|
||||||
var line = "";
|
|
||||||
while (!process.StandardOutput.EndOfStream)
|
|
||||||
{
|
|
||||||
line += process.StandardOutput.ReadLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageBox.Show("Uptime Kuma exited unexpectedly. Exit code: " + process.ExitCode + " " + line);
|
|
||||||
}
|
|
||||||
|
|
||||||
trayIcon.Visible = false;
|
|
||||||
Application.Exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,36 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
|
||||||
// set of attributes. Change these attribute values to modify the information
|
|
||||||
// associated with an assembly.
|
|
||||||
[assembly: AssemblyTitle("Uptime Kuma")]
|
|
||||||
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
|
|
||||||
[assembly: AssemblyConfiguration("")]
|
|
||||||
[assembly: AssemblyCompany("Uptime Kuma")]
|
|
||||||
[assembly: AssemblyProduct("Uptime Kuma")]
|
|
||||||
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
|
|
||||||
[assembly: AssemblyTrademark("")]
|
|
||||||
[assembly: AssemblyCulture("")]
|
|
||||||
|
|
||||||
// Setting ComVisible to false makes the types in this assembly not visible
|
|
||||||
// to COM components. If you need to access a type in this assembly from
|
|
||||||
// COM, set the ComVisible attribute to true on that type.
|
|
||||||
[assembly: ComVisible(false)]
|
|
||||||
|
|
||||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
|
||||||
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
|
|
||||||
|
|
||||||
// Version information for an assembly consists of the following four values:
|
|
||||||
//
|
|
||||||
// Major Version
|
|
||||||
// Minor Version
|
|
||||||
// Build Number
|
|
||||||
// Revision
|
|
||||||
//
|
|
||||||
// You can specify all the values or you can default the Build and Revision Numbers
|
|
||||||
// by using the '*' as shown below:
|
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
|
||||||
[assembly: AssemblyVersion("1.0.1.0")]
|
|
||||||
[assembly: AssemblyFileVersion("1.0.1.0")]
|
|
62
extra/exe-builder/Properties/Resources.Designer.cs
generated
62
extra/exe-builder/Properties/Resources.Designer.cs
generated
@@ -1,62 +0,0 @@
|
|||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
// Runtime Version:4.0.30319.42000
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
namespace UptimeKuma.Properties {
|
|
||||||
/// <summary>
|
|
||||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
|
||||||
/// </summary>
|
|
||||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
|
||||||
// class via a tool like ResGen or Visual Studio.
|
|
||||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
|
||||||
// with the /str option, or rebuild your VS project.
|
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder",
|
|
||||||
"4.0.0.0")]
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
|
||||||
internal class Resources {
|
|
||||||
private static global::System.Resources.ResourceManager resourceMan;
|
|
||||||
|
|
||||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
|
||||||
|
|
||||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance",
|
|
||||||
"CA1811:AvoidUncalledPrivateCode")]
|
|
||||||
internal Resources() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the cached ResourceManager instance used by this class.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
|
|
||||||
.Advanced)]
|
|
||||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
|
||||||
get {
|
|
||||||
if ((resourceMan == null)) {
|
|
||||||
global::System.Resources.ResourceManager temp =
|
|
||||||
new global::System.Resources.ResourceManager("UptimeKuma.Properties.Resources",
|
|
||||||
typeof(Resources).Assembly);
|
|
||||||
resourceMan = temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resourceMan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Overrides the current thread's CurrentUICulture property for all
|
|
||||||
/// resource lookups using this strongly typed resource class.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
|
|
||||||
.Advanced)]
|
|
||||||
internal static global::System.Globalization.CultureInfo Culture {
|
|
||||||
get { return resourceCulture; }
|
|
||||||
set { resourceCulture = value; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,117 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
</root>
|
|
23
extra/exe-builder/Properties/Settings.Designer.cs
generated
23
extra/exe-builder/Properties/Settings.Designer.cs
generated
@@ -1,23 +0,0 @@
|
|||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
// Runtime Version:4.0.30319.42000
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
namespace UptimeKuma.Properties {
|
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(
|
|
||||||
"Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
|
|
||||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
|
||||||
private static Settings defaultInstance =
|
|
||||||
((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
|
||||||
|
|
||||||
public static Settings Default {
|
|
||||||
get { return defaultInstance; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
|
|
||||||
<Profiles>
|
|
||||||
<Profile Name="(Default)" />
|
|
||||||
</Profiles>
|
|
||||||
<Settings />
|
|
||||||
</SettingsFile>
|
|
@@ -1,212 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<Import Project="packages\Costura.Fody.5.7.0\build\Costura.Fody.props" Condition="Exists('packages\Costura.Fody.5.7.0\build\Costura.Fody.props')" />
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
|
||||||
<PropertyGroup>
|
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
|
||||||
<ProjectGuid>{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}</ProjectGuid>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<RootNamespace>UptimeKuma</RootNamespace>
|
|
||||||
<AssemblyName>uptime-kuma</AssemblyName>
|
|
||||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
|
||||||
<Deterministic>true</Deterministic>
|
|
||||||
<ApplicationIcon>..\..\public\favicon.ico</ApplicationIcon>
|
|
||||||
<LangVersion>9</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>full</DebugType>
|
|
||||||
<Optimize>false</Optimize>
|
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<PostBuildEvent>COPY "$(SolutionDir)bin\Debug\uptime-kuma.exe" "%UserProfile%\Desktop\uptime-kuma-win64\"</PostBuildEvent>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="Costura, Version=5.7.0.0, Culture=neutral, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\Costura.Fody.5.7.0\lib\netstandard1.0\Costura.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Microsoft.Win32.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="mscorlib" />
|
|
||||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System" />
|
|
||||||
<Reference Include="System.AppContext, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.AppContext.4.3.0\lib\net463\System.AppContext.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.ComponentModel.Composition" />
|
|
||||||
<Reference Include="System.Console, Version=4.0.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Console.4.3.1\lib\net46\System.Console.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="System.Diagnostics.DiagnosticSource, Version=7.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Diagnostics.DiagnosticSource.7.0.1\lib\net462\System.Diagnostics.DiagnosticSource.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Diagnostics.Tracing, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Globalization.Calendars, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.IO, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.IO.4.3.0\lib\net462\System.IO.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.IO.Compression, Version=4.1.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.IO.Compression.FileSystem" />
|
|
||||||
<Reference Include="System.IO.Compression.ZipFile, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.IO.FileSystem, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.IO.FileSystem.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Linq, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Linq.4.3.0\lib\net463\System.Linq.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Linq.Expressions, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Linq.Expressions.4.3.0\lib\net463\System.Linq.Expressions.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Net.Http, Version=4.1.1.3, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Net.Sockets, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Numerics" />
|
|
||||||
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Reflection, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Runtime, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Runtime.Extensions, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Runtime.Extensions.4.3.1\lib\net462\System.Runtime.Extensions.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Runtime.InteropServices, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Runtime.InteropServices.4.3.0\lib\net463\System.Runtime.InteropServices.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Security.Cryptography.Algorithms, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Security.Cryptography.Encoding, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Security.Cryptography.Primitives, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Security.Cryptography.X509Certificates, Version=4.1.1.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Text.RegularExpressions, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Text.RegularExpressions.4.3.1\lib\net463\System.Text.RegularExpressions.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Xml.Linq" />
|
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
|
||||||
<Reference Include="Microsoft.CSharp" />
|
|
||||||
<Reference Include="System.Data" />
|
|
||||||
<Reference Include="System.Deployment" />
|
|
||||||
<Reference Include="System.Drawing" />
|
|
||||||
<Reference Include="System.Windows.Forms" />
|
|
||||||
<Reference Include="System.Xml" />
|
|
||||||
<Reference Include="System.Xml.ReaderWriter, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="DownloadForm.cs">
|
|
||||||
<SubType>Form</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="DownloadForm.Designer.cs">
|
|
||||||
<DependentUpon>DownloadForm.cs</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Program.cs" />
|
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
|
||||||
<Compile Include="Version.cs" />
|
|
||||||
<EmbeddedResource Include="DownloadForm.resx">
|
|
||||||
<DependentUpon>DownloadForm.cs</DependentUpon>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<EmbeddedResource Include="Properties\Resources.resx">
|
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<Compile Include="Properties\Resources.Designer.cs">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<None Include="..\..\public\favicon.ico">
|
|
||||||
<Link>favicon.ico</Link>
|
|
||||||
</None>
|
|
||||||
<None Include="packages.config" />
|
|
||||||
<None Include="Properties\Settings.settings">
|
|
||||||
<Generator>SettingsSingleFileGenerator</Generator>
|
|
||||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
|
||||||
</None>
|
|
||||||
<Compile Include="Properties\Settings.Designer.cs">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Settings.settings</DependentUpon>
|
|
||||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="App.config" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include=".gitignore" />
|
|
||||||
<Content Include="app.manifest" />
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
|
||||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
|
||||||
<PropertyGroup>
|
|
||||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.</ErrorText>
|
|
||||||
</PropertyGroup>
|
|
||||||
<Error Condition="!Exists('packages\Costura.Fody.5.7.0\build\Costura.Fody.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Costura.Fody.5.7.0\build\Costura.Fody.props'))" />
|
|
||||||
<Error Condition="!Exists('packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Costura.Fody.5.7.0\build\Costura.Fody.targets'))" />
|
|
||||||
<Error Condition="!Exists('packages\Fody.6.6.4\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Fody.6.6.4\build\Fody.targets'))" />
|
|
||||||
<Error Condition="!Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets'))" />
|
|
||||||
</Target>
|
|
||||||
<Import Project="packages\Costura.Fody.5.7.0\build\Costura.Fody.targets" Condition="Exists('packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" />
|
|
||||||
<Import Project="packages\Fody.6.6.4\build\Fody.targets" Condition="Exists('packages\Fody.6.6.4\build\Fody.targets')" />
|
|
||||||
<Import Project="packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets" Condition="Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" />
|
|
||||||
</Project>
|
|
@@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UptimeKuma", "UptimeKuma.csproj", "{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
@@ -1,3 +0,0 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=UptimeKuma_002FProperties_002FResources/@EntryIndexedValue">True</s:Boolean>
|
|
||||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean></wpf:ResourceDictionary>
|
|
@@ -1,9 +0,0 @@
|
|||||||
namespace UptimeKuma {
|
|
||||||
public class Version {
|
|
||||||
public string latest { get; set; }
|
|
||||||
public string slow { get; set; }
|
|
||||||
public string beta { get; set; }
|
|
||||||
public string nodejs { get; set; }
|
|
||||||
public string exe { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<asmv3:application>
|
|
||||||
<asmv3:windowsSettings>
|
|
||||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
|
||||||
</asmv3:windowsSettings>
|
|
||||||
</asmv3:application>
|
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
|
||||||
<security>
|
|
||||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<!-- UAC Manifest Options
|
|
||||||
If you want to change the Windows User Account Control level replace the
|
|
||||||
requestedExecutionLevel node with one of the following.
|
|
||||||
|
|
||||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
|
||||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
|
||||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
|
||||||
|
|
||||||
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
|
||||||
Remove this element if your application requires this virtualization for backwards
|
|
||||||
compatibility.
|
|
||||||
-->
|
|
||||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
|
||||||
</requestedPrivileges>
|
|
||||||
</security>
|
|
||||||
</trustInfo>
|
|
||||||
</assembly>
|
|
@@ -1,56 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<packages>
|
|
||||||
<package id="Costura.Fody" version="5.7.0" targetFramework="net472" developmentDependency="true" />
|
|
||||||
<package id="Fody" version="6.6.4" targetFramework="net472" developmentDependency="true" />
|
|
||||||
<package id="Microsoft.NETCore.Platforms" version="7.0.0" targetFramework="net472" />
|
|
||||||
<package id="Microsoft.Win32.Primitives" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="NETStandard.Library" version="2.0.3" targetFramework="net472" />
|
|
||||||
<package id="Newtonsoft.Json" version="13.0.2" targetFramework="net472" />
|
|
||||||
<package id="System.AppContext" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Console" version="4.3.1" targetFramework="net472" />
|
|
||||||
<package id="System.Diagnostics.DiagnosticSource" version="7.0.1" targetFramework="net472" />
|
|
||||||
<package id="System.Net.Http" version="4.3.4" targetFramework="net472" />
|
|
||||||
<package id="System.Runtime.Extensions" version="4.3.1" targetFramework="net472" />
|
|
||||||
<package id="System.Security.Cryptography.Algorithms" version="4.3.1" targetFramework="net472" />
|
|
||||||
<package id="System.Security.Cryptography.X509Certificates" version="4.3.2" targetFramework="net472" />
|
|
||||||
<package id="System.Text.RegularExpressions" version="4.3.1" targetFramework="net472" />
|
|
||||||
<package id="System.Xml.ReaderWriter" version="4.3.1" targetFramework="net472" />
|
|
||||||
<package id="System.Memory" version="4.5.5" targetFramework="net472" />
|
|
||||||
<package id="System.Net.Primitives" version="4.3.1" targetFramework="net472" />
|
|
||||||
<package id="System.Runtime" version="4.3.1" targetFramework="net472" />
|
|
||||||
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
|
|
||||||
<package id="System.Collections" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Collections.Concurrent" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Diagnostics.Debug" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Diagnostics.Tools" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Diagnostics.Tracing" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Globalization" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Globalization.Calendars" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.IO" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.IO.Compression" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.IO.Compression.ZipFile" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.IO.FileSystem" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.IO.FileSystem.Primitives" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Linq" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Linq.Expressions" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Net.Sockets" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
|
|
||||||
<package id="System.ObjectModel" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Reflection" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Reflection.Extensions" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Reflection.Primitives" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Resources.ResourceManager" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net472" />
|
|
||||||
<package id="System.Runtime.Handles" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Runtime.InteropServices" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Runtime.Numerics" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Security.Cryptography.Encoding" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Text.Encoding" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Text.Encoding.Extensions" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Threading" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Threading.Tasks" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Threading.Timer" version="4.3.0" targetFramework="net472" />
|
|
||||||
<package id="System.Xml.XDocument" version="4.3.0" targetFramework="net472" />
|
|
||||||
</packages>
|
|
@@ -1,8 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* ⚠️ ⚠️ ⚠️ ⚠️ Due to the weird issue in Portainer that the healthcheck script is still pointing to this script for unknown reason.
|
|
||||||
* IT CANNOT BE DROPPED, even though it looks like it is not used.
|
|
||||||
* See more: https://github.com/louislam/uptime-kuma/issues/2774#issuecomment-1429092359
|
|
||||||
*
|
|
||||||
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
|
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
|
||||||
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
*/
|
*/
|
||||||
@@ -23,17 +19,17 @@ if (sslKey && sslCert) {
|
|||||||
|
|
||||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||||
// Dual-stack support for (::)
|
// Dual-stack support for (::)
|
||||||
let hostname = process.env.UPTIME_KUMA_HOST;
|
let hostname = process.env.UPTIME_KUMA_SERVICE_HOST || process.env.UPTIME_KUMA_HOST || "::";
|
||||||
|
|
||||||
// Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD
|
// Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD
|
||||||
if (!hostname && !FBSD) {
|
if (!hostname && !FBSD) {
|
||||||
hostname = process.env.HOST;
|
hostname = process.env.HOST;
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
|
const port = parseInt(process.env.UPTIME_KUMA_SERVICE_PORT || process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
host: hostname || "127.0.0.1",
|
host: hostname,
|
||||||
port: port,
|
port: port,
|
||||||
timeout: 28 * 1000,
|
timeout: 28 * 1000,
|
||||||
};
|
};
|
||||||
|
@@ -5,15 +5,15 @@
|
|||||||
|
|
||||||
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||||
println("=====================");
|
println("=====================");
|
||||||
println("Uptime Kuma Install Script");
|
println("Uptime Kuma Installer");
|
||||||
println("=====================");
|
println("=====================");
|
||||||
println("Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8");
|
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
|
||||||
println("---------------------------------------");
|
println("---------------------------------------");
|
||||||
println("This script is designed for Linux and basic usage.");
|
println("This script is designed for Linux and basic usage.");
|
||||||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
||||||
println("---------------------------------------");
|
println("---------------------------------------");
|
||||||
println("");
|
println("");
|
||||||
println("Local - Install Uptime Kuma on your current machine with git, Node.js and pm2");
|
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
|
||||||
println("Docker - Install Uptime Kuma Docker container");
|
println("Docker - Install Uptime Kuma Docker container");
|
||||||
println("");
|
println("");
|
||||||
|
|
||||||
@@ -29,10 +29,14 @@ function checkNode() {
|
|||||||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
||||||
println("Node Version: " ++ nodeVersion);
|
println("Node Version: " ++ nodeVersion);
|
||||||
|
|
||||||
if (nodeVersion <= "12") {
|
if (nodeVersion < "12") {
|
||||||
println("Error: Required Node.js 14");
|
println("Error: Required Node.js 14");
|
||||||
call("exit", "1");
|
call("exit", "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nodeVersion == "12") {
|
||||||
|
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deb() {
|
function deb() {
|
||||||
@@ -56,8 +60,8 @@ function deb() {
|
|||||||
bash("apt --yes install curl");
|
bash("apt --yes install curl");
|
||||||
}
|
}
|
||||||
|
|
||||||
println("Installing Node.js 16");
|
println("Installing Node.js 14");
|
||||||
bash("curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt");
|
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
|
||||||
bash("apt --yes install nodejs");
|
bash("apt --yes install nodejs");
|
||||||
bash("node -v");
|
bash("node -v");
|
||||||
|
|
||||||
@@ -87,10 +91,6 @@ if (type == "local") {
|
|||||||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
||||||
if (os == "Ubuntu") {
|
if (os == "Ubuntu") {
|
||||||
distribution = "ubuntu";
|
distribution = "ubuntu";
|
||||||
|
|
||||||
// Get ubuntu version
|
|
||||||
bash(". /etc/lsb-release");
|
|
||||||
version = DISTRIB_RELEASE;
|
|
||||||
}
|
}
|
||||||
if (os == "Debian") {
|
if (os == "Debian") {
|
||||||
distribution = "debian";
|
distribution = "debian";
|
||||||
@@ -101,7 +101,6 @@ if (type == "local") {
|
|||||||
|
|
||||||
println("Your OS: " ++ os);
|
println("Your OS: " ++ os);
|
||||||
println("Distribution: " ++ distribution);
|
println("Distribution: " ++ distribution);
|
||||||
println("Version: " ++ version);
|
|
||||||
println("Arch: " ++ arch);
|
println("Arch: " ++ arch);
|
||||||
|
|
||||||
if ("$3" != "") {
|
if ("$3" != "") {
|
||||||
@@ -132,32 +131,15 @@ if (type == "local") {
|
|||||||
checkNode();
|
checkNode();
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
bash("dnfCheck=$(dnf --version)");
|
|
||||||
|
|
||||||
// Use yum
|
|
||||||
if (dnfCheck == "") {
|
|
||||||
bash("curlCheck=$(curl --version)");
|
bash("curlCheck=$(curl --version)");
|
||||||
if (curlCheck == "") {
|
if (curlCheck == "") {
|
||||||
println("Installing Curl");
|
println("Installing Curl");
|
||||||
bash("yum -y -q install curl");
|
bash("yum -y -q install curl");
|
||||||
}
|
}
|
||||||
|
|
||||||
println("Installing Node.js 16");
|
println("Installing Node.js 14");
|
||||||
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
|
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
|
||||||
bash("yum install -y -q nodejs");
|
bash("yum install -y -q nodejs");
|
||||||
} else {
|
|
||||||
bash("curlCheck=$(curl --version)");
|
|
||||||
if (curlCheck == "") {
|
|
||||||
println("Installing Curl");
|
|
||||||
bash("dnf -y install curl");
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Installing Node.js 16");
|
|
||||||
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
|
|
||||||
bash("dnf install -y nodejs");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bash("node -v");
|
bash("node -v");
|
||||||
|
|
||||||
bash("nodeCheckAgain=$(node -v)");
|
bash("nodeCheckAgain=$(node -v)");
|
||||||
@@ -211,14 +193,6 @@ if (type == "local") {
|
|||||||
bash("pm2 startup");
|
bash("pm2 startup");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Check again
|
|
||||||
bash("check=$(pm2 --version)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Error: pm2 is not found!");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("mkdir -p $installPath");
|
bash("mkdir -p $installPath");
|
||||||
bash("cd $installPath");
|
bash("cd $installPath");
|
||||||
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
// Read the file from private/sort-contributors.txt
|
|
||||||
const file = fs.readFileSync("private/sort-contributors.txt", "utf8");
|
|
||||||
|
|
||||||
// Convert to an array of lines
|
|
||||||
let lines = file.split("\n");
|
|
||||||
|
|
||||||
// Remove empty lines
|
|
||||||
lines = lines.filter((line) => line !== "");
|
|
||||||
|
|
||||||
// Remove duplicates
|
|
||||||
lines = [ ...new Set(lines) ];
|
|
||||||
|
|
||||||
// Remove @weblate and @UptimeKumaBot
|
|
||||||
lines = lines.filter((line) => line !== "@weblate" && line !== "@UptimeKumaBot" && line !== "@louislam");
|
|
||||||
|
|
||||||
// Sort the lines
|
|
||||||
lines = lines.sort();
|
|
||||||
|
|
||||||
// Output the lines, concat with " "
|
|
||||||
console.log(lines.join(" "));
|
|
@@ -1,9 +0,0 @@
|
|||||||
// Check if docker is running
|
|
||||||
const { exec } = require("child_process");
|
|
||||||
|
|
||||||
exec("docker ps", (err, stdout, stderr) => {
|
|
||||||
if (err) {
|
|
||||||
console.error("Docker is not running. Please start docker and try again.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
@@ -26,8 +26,7 @@ if (! exists) {
|
|||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
// Also update package-lock.json
|
// Also update package-lock.json
|
||||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
childProcess.spawnSync("npm", [ "install" ]);
|
||||||
childProcess.spawnSync(npm, [ "install" ]);
|
|
||||||
|
|
||||||
commit(newVersion);
|
commit(newVersion);
|
||||||
tag(newVersion);
|
tag(newVersion);
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
42
install.sh
42
install.sh
@@ -3,15 +3,15 @@
|
|||||||
# The command is working on Windows PowerShell and Docker for Windows only.
|
# The command is working on Windows PowerShell and Docker for Windows only.
|
||||||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||||
"echo" "-e" "====================="
|
"echo" "-e" "====================="
|
||||||
"echo" "-e" "Uptime Kuma Install Script"
|
"echo" "-e" "Uptime Kuma Installer"
|
||||||
"echo" "-e" "====================="
|
"echo" "-e" "====================="
|
||||||
"echo" "-e" "Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8"
|
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
|
||||||
"echo" "-e" "---------------------------------------"
|
"echo" "-e" "---------------------------------------"
|
||||||
"echo" "-e" "This script is designed for Linux and basic usage."
|
"echo" "-e" "This script is designed for Linux and basic usage."
|
||||||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
||||||
"echo" "-e" "---------------------------------------"
|
"echo" "-e" "---------------------------------------"
|
||||||
"echo" "-e" ""
|
"echo" "-e" ""
|
||||||
"echo" "-e" "Local - Install Uptime Kuma on your current machine with git, Node.js and pm2"
|
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
|
||||||
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
||||||
"echo" "-e" ""
|
"echo" "-e" ""
|
||||||
if [ "$1" != "" ]; then
|
if [ "$1" != "" ]; then
|
||||||
@@ -25,9 +25,12 @@ function checkNode {
|
|||||||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
||||||
"echo" "-e" "Node Version: ""$nodeVersion"
|
"echo" "-e" "Node Version: ""$nodeVersion"
|
||||||
_0="12"
|
_0="12"
|
||||||
if [ $(($nodeVersion <= $_0)) == 1 ]; then
|
if [ $(($nodeVersion < $_0)) == 1 ]; then
|
||||||
"echo" "-e" "Error: Required Node.js 14"
|
"echo" "-e" "Error: Required Node.js 14"
|
||||||
"exit" "1"
|
"exit" "1"
|
||||||
|
fi
|
||||||
|
if [ "$nodeVersion" == "12" ]; then
|
||||||
|
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
function deb {
|
function deb {
|
||||||
@@ -47,8 +50,8 @@ fi
|
|||||||
"echo" "-e" "Installing Curl"
|
"echo" "-e" "Installing Curl"
|
||||||
apt --yes install curl
|
apt --yes install curl
|
||||||
fi
|
fi
|
||||||
"echo" "-e" "Installing Node.js 16"
|
"echo" "-e" "Installing Node.js 14"
|
||||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt
|
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
|
||||||
apt --yes install nodejs
|
apt --yes install nodejs
|
||||||
node -v
|
node -v
|
||||||
nodeCheckAgain=$(node -v)
|
nodeCheckAgain=$(node -v)
|
||||||
@@ -73,9 +76,6 @@ if [ "$type" == "local" ]; then
|
|||||||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
||||||
if [ "$os" == "Ubuntu" ]; then
|
if [ "$os" == "Ubuntu" ]; then
|
||||||
distribution="ubuntu"
|
distribution="ubuntu"
|
||||||
# Get ubuntu version
|
|
||||||
. /etc/lsb-release
|
|
||||||
version="$DISTRIB_RELEASE"
|
|
||||||
fi
|
fi
|
||||||
if [ "$os" == "Debian" ]; then
|
if [ "$os" == "Debian" ]; then
|
||||||
distribution="debian"
|
distribution="debian"
|
||||||
@@ -85,7 +85,6 @@ fi
|
|||||||
arch=$(uname -i)
|
arch=$(uname -i)
|
||||||
"echo" "-e" "Your OS: ""$os"
|
"echo" "-e" "Your OS: ""$os"
|
||||||
"echo" "-e" "Distribution: ""$distribution"
|
"echo" "-e" "Distribution: ""$distribution"
|
||||||
"echo" "-e" "Version: ""$version"
|
|
||||||
"echo" "-e" "Arch: ""$arch"
|
"echo" "-e" "Arch: ""$arch"
|
||||||
if [ "$3" != "" ]; then
|
if [ "$3" != "" ]; then
|
||||||
port="$3"
|
port="$3"
|
||||||
@@ -109,27 +108,14 @@ fi
|
|||||||
if [ "$nodeCheck" != "" ]; then
|
if [ "$nodeCheck" != "" ]; then
|
||||||
"checkNode"
|
"checkNode"
|
||||||
else
|
else
|
||||||
dnfCheck=$(dnf --version)
|
|
||||||
# Use yum
|
|
||||||
if [ "$dnfCheck" == "" ]; then
|
|
||||||
curlCheck=$(curl --version)
|
curlCheck=$(curl --version)
|
||||||
if [ "$curlCheck" == "" ]; then
|
if [ "$curlCheck" == "" ]; then
|
||||||
"echo" "-e" "Installing Curl"
|
"echo" "-e" "Installing Curl"
|
||||||
yum -y -q install curl
|
yum -y -q install curl
|
||||||
fi
|
fi
|
||||||
"echo" "-e" "Installing Node.js 16"
|
"echo" "-e" "Installing Node.js 14"
|
||||||
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
|
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
|
||||||
yum install -y -q nodejs
|
yum install -y -q nodejs
|
||||||
else
|
|
||||||
curlCheck=$(curl --version)
|
|
||||||
if [ "$curlCheck" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Curl"
|
|
||||||
dnf -y install curl
|
|
||||||
fi
|
|
||||||
"echo" "-e" "Installing Node.js 16"
|
|
||||||
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
|
|
||||||
dnf install -y nodejs
|
|
||||||
fi
|
|
||||||
node -v
|
node -v
|
||||||
nodeCheckAgain=$(node -v)
|
nodeCheckAgain=$(node -v)
|
||||||
if [ "$nodeCheckAgain" == "" ]; then
|
if [ "$nodeCheckAgain" == "" ]; then
|
||||||
@@ -175,12 +161,6 @@ fi
|
|||||||
"echo" "-e" "Installing PM2"
|
"echo" "-e" "Installing PM2"
|
||||||
npm install pm2 -g && pm2 install pm2-logrotate
|
npm install pm2 -g && pm2 install pm2-logrotate
|
||||||
pm2 startup
|
pm2 startup
|
||||||
fi
|
|
||||||
# Check again
|
|
||||||
check=$(pm2 --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Error: pm2 is not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
mkdir -p $installPath
|
mkdir -p $installPath
|
||||||
cd $installPath
|
cd $installPath
|
||||||
|
24480
package-lock.json
generated
24480
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.23.0-beta.1",
|
"version": "1.20.0-beta.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "14 || 16 || 18 || >= 20.4.0"
|
"node": "14.* || >=16.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-legacy": "npm install",
|
"install-legacy": "npm install",
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
"lint": "npm run lint:js && npm run lint:style",
|
"lint": "npm run lint:js && npm run lint:style",
|
||||||
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
||||||
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
||||||
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
|
|
||||||
"start": "npm run start-server",
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
@@ -35,31 +34,28 @@
|
|||||||
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
|
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
|
||||||
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
||||||
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||||
"build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.22.1 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.19.6 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
"remove-2fa": "node extra/remove-2fa.js",
|
"remove-2fa": "node extra/remove-2fa.js",
|
||||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
||||||
"test-install-script-rockylinux": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/rockylinux.dockerfile .",
|
|
||||||
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
||||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||||
"test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile .",
|
|
||||||
"test-install-script-debian-buster": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian-buster.dockerfile .",
|
|
||||||
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
||||||
"test-install-script-ubuntu1804": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1804.dockerfile .",
|
|
||||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
||||||
|
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||||
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
||||||
"ncu-patch": "npm-check-updates -u -t patch",
|
"ncu-patch": "npm-check-updates -u -t patch",
|
||||||
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||||
"release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||||
"git-remove-tag": "git tag -d",
|
"git-remove-tag": "git tag -d",
|
||||||
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
||||||
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
||||||
@@ -67,19 +63,18 @@
|
|||||||
"cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
|
"cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
|
||||||
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js",
|
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js",
|
||||||
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
||||||
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
|
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go"
|
||||||
"deploy-demo-server": "node extra/deploy-demo-server.js",
|
|
||||||
"sort-contributors": "node extra/sort-contributors.js"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "~1.7.3",
|
"@grpc/grpc-js": "~1.7.3",
|
||||||
"@louislam/ping": "~0.4.4-mod.1",
|
"@louislam/ping": "~0.4.2-mod.1",
|
||||||
"@louislam/sqlite3": "15.1.6",
|
"@louislam/sqlite3": "15.1.2",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.27.0",
|
"axios": "~0.27.0",
|
||||||
"axios-ntlm": "1.3.0",
|
"axios-ntlm": "1.3.0",
|
||||||
"badge-maker": "~3.3.1",
|
"badge-maker": "~3.3.1",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
|
"bree": "~7.1.5",
|
||||||
"cacheable-lookup": "~6.0.4",
|
"cacheable-lookup": "~6.0.4",
|
||||||
"chardet": "~1.4.0",
|
"chardet": "~1.4.0",
|
||||||
"check-password-strength": "^2.0.5",
|
"check-password-strength": "^2.0.5",
|
||||||
@@ -88,59 +83,47 @@
|
|||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
"compare-versions": "~3.6.0",
|
"compare-versions": "~3.6.0",
|
||||||
"compression": "~1.7.4",
|
"compression": "~1.7.4",
|
||||||
"croner": "~6.0.5",
|
|
||||||
"dayjs": "~1.11.5",
|
"dayjs": "~1.11.5",
|
||||||
"dotenv": "~16.0.3",
|
"dotenv": "~16.0.3",
|
||||||
"express": "~4.17.3",
|
"express": "~4.17.3",
|
||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"gamedig": "~4.0.5",
|
"gamedig": "^4.0.5",
|
||||||
"http-graceful-shutdown": "~3.1.7",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"http-proxy-agent": "~5.0.0",
|
"http-proxy-agent": "~5.0.0",
|
||||||
"https-proxy-agent": "~5.0.1",
|
"https-proxy-agent": "~5.0.1",
|
||||||
"iconv-lite": "~0.6.3",
|
"iconv-lite": "~0.6.3",
|
||||||
"isomorphic-ws": "^5.0.0",
|
|
||||||
"jsesc": "~3.0.2",
|
"jsesc": "~3.0.2",
|
||||||
"jsonata": "^2.0.3",
|
|
||||||
"jsonwebtoken": "~9.0.0",
|
"jsonwebtoken": "~9.0.0",
|
||||||
"jwt-decode": "~3.1.2",
|
"jwt-decode": "~3.1.2",
|
||||||
"kafkajs": "^2.2.4",
|
|
||||||
"limiter": "~2.1.0",
|
"limiter": "~2.1.0",
|
||||||
"liquidjs": "^10.7.0",
|
"mongodb": "~4.13.0",
|
||||||
"mongodb": "~4.14.0",
|
|
||||||
"mqtt": "~4.3.7",
|
"mqtt": "~4.3.7",
|
||||||
"mssql": "~8.1.4",
|
"mssql": "~8.1.4",
|
||||||
"mysql2": "~2.3.3",
|
"mysql2": "~2.3.3",
|
||||||
"nanoid": "~3.3.4",
|
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
"node-radius-client": "~1.0.0",
|
"node-radius-client": "~1.0.0",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"nostr-tools": "^1.13.1",
|
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"openid-client": "^5.4.2",
|
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"pg": "~8.8.0",
|
"pg": "~8.8.0",
|
||||||
"pg-connection-string": "~2.5.0",
|
"pg-connection-string": "~2.5.0",
|
||||||
"playwright-core": "~1.35.1",
|
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
"protobufjs": "~7.2.4",
|
"protobufjs": "~7.1.1",
|
||||||
"qs": "~6.10.4",
|
"redbean-node": "~0.2.0",
|
||||||
"redbean-node": "~0.3.0",
|
|
||||||
"redis": "~4.5.1",
|
"redis": "~4.5.1",
|
||||||
"semver": "~7.5.4",
|
"socket.io": "~4.5.3",
|
||||||
"socket.io": "~4.6.1",
|
"socket.io-client": "~4.5.3",
|
||||||
"socket.io-client": "~4.6.1",
|
|
||||||
"socks-proxy-agent": "6.1.1",
|
"socks-proxy-agent": "6.1.1",
|
||||||
"tar": "~6.1.11",
|
"tar": "~6.1.11",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2",
|
"thirty-two": "~1.0.2"
|
||||||
"ws": "^8.13.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "~5.0.1",
|
"@actions/github": "~5.0.1",
|
||||||
"@babel/eslint-parser": "^7.22.7",
|
"@babel/eslint-parser": "~7.17.0",
|
||||||
"@babel/preset-env": "^7.15.8",
|
"@babel/preset-env": "^7.15.8",
|
||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
@@ -148,28 +131,27 @@
|
|||||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"@types/bootstrap": "~5.1.9",
|
"@types/bootstrap": "~5.1.9",
|
||||||
"@vitejs/plugin-legacy": "~4.1.0",
|
"@vitejs/plugin-legacy": "~2.1.0",
|
||||||
"@vitejs/plugin-vue": "~4.2.3",
|
"@vitejs/plugin-vue": "~3.1.0",
|
||||||
"@vue/compiler-sfc": "~3.3.4",
|
"@vue/compiler-sfc": "~3.2.36",
|
||||||
"@vuepic/vue-datepicker": "~3.4.8",
|
"@vuepic/vue-datepicker": "~3.4.8",
|
||||||
"aedes": "^0.46.3",
|
"aedes": "^0.46.3",
|
||||||
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
"chart.js": "~4.2.1",
|
"chart.js": "~3.6.2",
|
||||||
"chartjs-adapter-dayjs-4": "~1.0.4",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
"concurrently": "^7.1.0",
|
"concurrently": "^7.1.0",
|
||||||
"core-js": "~3.26.1",
|
"core-js": "~3.26.1",
|
||||||
"cronstrue": "~2.24.0",
|
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"cypress": "^12.17.0",
|
"cypress": "^10.1.0",
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
"dns2": "~2.0.1",
|
"dns2": "~2.0.1",
|
||||||
"dompurify": "~2.4.3",
|
"dompurify": "~2.4.3",
|
||||||
"eslint": "~8.14.0",
|
"eslint": "~8.14.0",
|
||||||
"eslint-plugin-vue": "~8.7.1",
|
"eslint-plugin-vue": "~8.7.1",
|
||||||
"favico.js": "~0.3.10",
|
"favico.js": "~0.3.10",
|
||||||
"jest": "~29.6.1",
|
|
||||||
"marked": "~4.2.5",
|
"marked": "~4.2.5",
|
||||||
"node-ssh": "~13.1.0",
|
"jest": "~27.2.5",
|
||||||
"postcss-html": "~1.5.0",
|
"postcss-html": "~1.5.0",
|
||||||
"postcss-rtlcss": "~3.7.2",
|
"postcss-rtlcss": "~3.7.2",
|
||||||
"postcss-scss": "~4.0.4",
|
"postcss-scss": "~4.0.4",
|
||||||
@@ -177,17 +159,16 @@
|
|||||||
"qrcode": "~1.5.0",
|
"qrcode": "~1.5.0",
|
||||||
"rollup-plugin-visualizer": "^5.6.0",
|
"rollup-plugin-visualizer": "^5.6.0",
|
||||||
"sass": "~1.42.1",
|
"sass": "~1.42.1",
|
||||||
"stylelint": "^15.10.1",
|
"stylelint": "~14.7.1",
|
||||||
"stylelint-config-standard": "~25.0.0",
|
"stylelint-config-standard": "~25.0.0",
|
||||||
"terser": "~5.15.0",
|
"terser": "~5.15.0",
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
"typescript": "~4.4.4",
|
"typescript": "~4.4.4",
|
||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
"vite": "~4.4.1",
|
"vite": "~3.1.0",
|
||||||
"vite-plugin-commonjs": "^0.8.0",
|
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vue": "~3.3.4",
|
"vue": "next",
|
||||||
"vue-chartjs": "~5.2.0",
|
"vue-chart-3": "3.0.9",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
"vue-contenteditable": "~3.0.4",
|
"vue-contenteditable": "~3.0.4",
|
||||||
"vue-i18n": "~9.2.2",
|
"vue-i18n": "~9.2.2",
|
||||||
@@ -198,7 +179,6 @@
|
|||||||
"vue-router": "~4.0.14",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0",
|
"vuedraggable": "~4.1.0",
|
||||||
"wait-on": "^6.0.1",
|
"wait-on": "^6.0.1"
|
||||||
"whatwg-url": "~12.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
100
server/auth.js
100
server/auth.js
@@ -2,10 +2,7 @@ const basicAuth = require("express-basic-auth");
|
|||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { setting } = require("./util-server");
|
const { setting } = require("./util-server");
|
||||||
const { log } = require("../src/util");
|
const { loginRateLimiter } = require("./rate-limiter");
|
||||||
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
|
|
||||||
const { Settings } = require("./settings");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login to web app
|
* Login to web app
|
||||||
@@ -37,36 +34,8 @@ exports.login = async function (username, password) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a provided API key
|
* Callback for myAuthorizer
|
||||||
* @param {string} key API key to verify
|
* @callback myAuthorizerCB
|
||||||
*/
|
|
||||||
async function verifyAPIKey(key) {
|
|
||||||
if (typeof key !== "string") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// uk prefix + key ID is before _
|
|
||||||
let index = key.substring(2, key.indexOf("_"));
|
|
||||||
let clear = key.substring(key.indexOf("_") + 1, key.length);
|
|
||||||
|
|
||||||
let hash = await R.findOne("api_key", " id=? ", [ index ]);
|
|
||||||
|
|
||||||
if (hash === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let current = dayjs();
|
|
||||||
let expiry = dayjs(hash.expires);
|
|
||||||
if (expiry.diff(current) < 0 || !hash.active) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash && passwordHash.verify(clear, hash.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback for basic auth authorizers
|
|
||||||
* @callback authCallback
|
|
||||||
* @param {any} err Any error encountered
|
* @param {any} err Any error encountered
|
||||||
* @param {boolean} authorized Is the client authorized?
|
* @param {boolean} authorized Is the client authorized?
|
||||||
*/
|
*/
|
||||||
@@ -75,35 +44,9 @@ async function verifyAPIKey(key) {
|
|||||||
* Custom authorizer for express-basic-auth
|
* Custom authorizer for express-basic-auth
|
||||||
* @param {string} username
|
* @param {string} username
|
||||||
* @param {string} password
|
* @param {string} password
|
||||||
* @param {authCallback} callback
|
* @param {myAuthorizerCB} callback
|
||||||
*/
|
*/
|
||||||
function apiAuthorizer(username, password, callback) {
|
function myAuthorizer(username, password, callback) {
|
||||||
// API Rate Limit
|
|
||||||
apiRateLimiter.pass(null, 0).then((pass) => {
|
|
||||||
if (pass) {
|
|
||||||
verifyAPIKey(password).then((valid) => {
|
|
||||||
if (!valid) {
|
|
||||||
log.warn("api-auth", "Failed API auth attempt: invalid API Key");
|
|
||||||
}
|
|
||||||
callback(null, valid);
|
|
||||||
// Only allow a set number of api requests per minute
|
|
||||||
// (currently set to 60)
|
|
||||||
apiRateLimiter.removeTokens(1);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
log.warn("api-auth", "Failed API auth attempt: rate limit exceeded");
|
|
||||||
callback(null, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom authorizer for express-basic-auth
|
|
||||||
* @param {string} username
|
|
||||||
* @param {string} password
|
|
||||||
* @param {authCallback} callback
|
|
||||||
*/
|
|
||||||
function userAuthorizer(username, password, callback) {
|
|
||||||
// Login Rate Limit
|
// Login Rate Limit
|
||||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||||
if (pass) {
|
if (pass) {
|
||||||
@@ -111,12 +54,10 @@ function userAuthorizer(username, password, callback) {
|
|||||||
callback(null, user != null);
|
callback(null, user != null);
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
|
|
||||||
loginRateLimiter.removeTokens(1);
|
loginRateLimiter.removeTokens(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.warn("basic-auth", "Failed basic auth attempt: rate limit exceeded");
|
|
||||||
callback(null, false);
|
callback(null, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -130,7 +71,7 @@ function userAuthorizer(username, password, callback) {
|
|||||||
*/
|
*/
|
||||||
exports.basicAuth = async function (req, res, next) {
|
exports.basicAuth = async function (req, res, next) {
|
||||||
const middleware = basicAuth({
|
const middleware = basicAuth({
|
||||||
authorizer: userAuthorizer,
|
authorizer: myAuthorizer,
|
||||||
authorizeAsync: true,
|
authorizeAsync: true,
|
||||||
challenge: true,
|
challenge: true,
|
||||||
});
|
});
|
||||||
@@ -143,32 +84,3 @@ exports.basicAuth = async function (req, res, next) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Use use API Key if API keys enabled, else use basic auth
|
|
||||||
* @param {express.Request} req Express request object
|
|
||||||
* @param {express.Response} res Express response object
|
|
||||||
* @param {express.NextFunction} next
|
|
||||||
*/
|
|
||||||
exports.apiAuth = async function (req, res, next) {
|
|
||||||
if (!await Settings.get("disableAuth")) {
|
|
||||||
let usingAPIKeys = await Settings.get("apiKeysEnabled");
|
|
||||||
let middleware;
|
|
||||||
if (usingAPIKeys) {
|
|
||||||
middleware = basicAuth({
|
|
||||||
authorizer: apiAuthorizer,
|
|
||||||
authorizeAsync: true,
|
|
||||||
challenge: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
middleware = basicAuth({
|
|
||||||
authorizer: userAuthorizer,
|
|
||||||
authorizeAsync: true,
|
|
||||||
challenge: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
middleware(req, res, next);
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@@ -1,33 +1,27 @@
|
|||||||
const { setSetting, setting } = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const compareVersions = require("compare-versions");
|
const compareVersions = require("compare-versions");
|
||||||
const { log } = require("../src/util");
|
|
||||||
|
|
||||||
exports.version = require("../package.json").version;
|
exports.version = require("../package.json").version;
|
||||||
exports.latestVersion = null;
|
exports.latestVersion = null;
|
||||||
|
|
||||||
// How much time in ms to wait between update checks
|
|
||||||
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
|
|
||||||
const UPDATE_CHECKER_LATEST_VERSION_URL = "https://uptime.kuma.pet/version";
|
|
||||||
|
|
||||||
let interval;
|
let interval;
|
||||||
|
|
||||||
|
/** Start 48 hour check interval */
|
||||||
exports.startInterval = () => {
|
exports.startInterval = () => {
|
||||||
let check = async () => {
|
let check = async () => {
|
||||||
if (await setting("checkUpdate") === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("update-checker", "Retrieving latest versions");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(UPDATE_CHECKER_LATEST_VERSION_URL);
|
const res = await axios.get("https://uptime.kuma.pet/version");
|
||||||
|
|
||||||
// For debug
|
// For debug
|
||||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||||
res.data.slow = "1000.0.0";
|
res.data.slow = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await setting("checkUpdate") === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let checkBeta = await setting("checkBeta");
|
let checkBeta = await setting("checkBeta");
|
||||||
|
|
||||||
if (checkBeta && res.data.beta) {
|
if (checkBeta && res.data.beta) {
|
||||||
@@ -41,14 +35,12 @@ exports.startInterval = () => {
|
|||||||
exports.latestVersion = res.data.slow;
|
exports.latestVersion = res.data.slow;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (_) {
|
} catch (_) { }
|
||||||
log.info("update-checker", "Failed to check for new versions");
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
check();
|
check();
|
||||||
interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
|
interval = setInterval(check, 3600 * 1000 * 48);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -113,49 +113,15 @@ async function sendProxyList(socket) {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit API key list to client
|
|
||||||
* @param {Socket} socket Socket.io socket instance
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function sendAPIKeyList(socket) {
|
|
||||||
const timeLogger = new TimeLogger();
|
|
||||||
|
|
||||||
let result = [];
|
|
||||||
const list = await R.find(
|
|
||||||
"api_key",
|
|
||||||
"user_id=?",
|
|
||||||
[ socket.userID ],
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let bean of list) {
|
|
||||||
result.push(bean.toPublicJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
io.to(socket.userID).emit("apiKeyList", result);
|
|
||||||
timeLogger.print("Sent API Key List");
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the version information to the client.
|
* Emits the version information to the client.
|
||||||
* @param {Socket} socket Socket.io socket instance
|
* @param {Socket} socket Socket.io socket instance
|
||||||
* @param {boolean} hideVersion
|
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function sendInfo(socket, hideVersion = false) {
|
async function sendInfo(socket) {
|
||||||
let version;
|
|
||||||
let latestVersion;
|
|
||||||
|
|
||||||
if (!hideVersion) {
|
|
||||||
version = checkVersion.version;
|
|
||||||
latestVersion = checkVersion.latestVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.emit("info", {
|
socket.emit("info", {
|
||||||
version,
|
version: checkVersion.version,
|
||||||
latestVersion,
|
latestVersion: checkVersion.latestVersion,
|
||||||
primaryBaseURL: await setting("primaryBaseURL"),
|
primaryBaseURL: await setting("primaryBaseURL"),
|
||||||
serverTimezone: await server.getTimezone(),
|
serverTimezone: await server.getTimezone(),
|
||||||
serverTimezoneOffset: server.getTimezoneOffset(),
|
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||||
@@ -191,7 +157,6 @@ module.exports = {
|
|||||||
sendImportantHeartbeatList,
|
sendImportantHeartbeatList,
|
||||||
sendHeartbeatList,
|
sendHeartbeatList,
|
||||||
sendProxyList,
|
sendProxyList,
|
||||||
sendAPIKeyList,
|
|
||||||
sendInfo,
|
sendInfo,
|
||||||
sendDockerHostList
|
sendDockerHostList
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
// Interop with browser
|
const args = require("args-parser")(process.argv);
|
||||||
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
|
|
||||||
const demoMode = args["demo"] || false;
|
const demoMode = args["demo"] || false;
|
||||||
|
|
||||||
const badgeConstants = {
|
const badgeConstants = {
|
||||||
|
@@ -2,8 +2,9 @@ const fs = require("fs");
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { setSetting, setting } = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
const { log, sleep } = require("../src/util");
|
const { log, sleep } = require("../src/util");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
const knex = require("knex");
|
const knex = require("knex");
|
||||||
const path = require("path");
|
const { PluginsManager } = require("./plugins-manager");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database & App Data Folder
|
* Database & App Data Folder
|
||||||
@@ -22,17 +23,18 @@ class Database {
|
|||||||
*/
|
*/
|
||||||
static uploadDir;
|
static uploadDir;
|
||||||
|
|
||||||
static screenshotDir;
|
|
||||||
|
|
||||||
static path;
|
static path;
|
||||||
|
|
||||||
static dockerTLSDir;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
static patched = false;
|
static patched = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For Backup only
|
||||||
|
*/
|
||||||
|
static backupPath = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add patch filename in key
|
* Add patch filename in key
|
||||||
* Values:
|
* Values:
|
||||||
@@ -68,19 +70,6 @@ class Database {
|
|||||||
"patch-maintenance-table2.sql": true,
|
"patch-maintenance-table2.sql": true,
|
||||||
"patch-add-gamedig-monitor.sql": true,
|
"patch-add-gamedig-monitor.sql": true,
|
||||||
"patch-add-google-analytics-status-page-tag.sql": true,
|
"patch-add-google-analytics-status-page-tag.sql": true,
|
||||||
"patch-http-body-encoding.sql": true,
|
|
||||||
"patch-add-description-monitor.sql": true,
|
|
||||||
"patch-api-key-table.sql": true,
|
|
||||||
"patch-monitor-tls.sql": true,
|
|
||||||
"patch-maintenance-cron.sql": true,
|
|
||||||
"patch-add-parent-monitor.sql": true,
|
|
||||||
"patch-add-invert-keyword.sql": true,
|
|
||||||
"patch-added-json-query.sql": true,
|
|
||||||
"patch-added-kafka-producer.sql": true,
|
|
||||||
"patch-add-certificate-expiry-status-page.sql": true,
|
|
||||||
"patch-monitor-oauth-cc.sql": true,
|
|
||||||
"patch-add-timeout-monitor.sql": true,
|
|
||||||
"patch-add-gamedig-given-port.sql": true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,28 +88,23 @@ class Database {
|
|||||||
// Data Directory (must be end with "/")
|
// Data Directory (must be end with "/")
|
||||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||||
|
|
||||||
Database.path = path.join(Database.dataDir, "kuma.db");
|
// Plugin feature is working only if the dataDir = "./data";
|
||||||
|
if (Database.dataDir !== "./data/") {
|
||||||
|
log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/");
|
||||||
|
PluginsManager.disable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Database.path = Database.dataDir + "kuma.db";
|
||||||
if (! fs.existsSync(Database.dataDir)) {
|
if (! fs.existsSync(Database.dataDir)) {
|
||||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
Database.uploadDir = path.join(Database.dataDir, "upload/");
|
Database.uploadDir = Database.dataDir + "upload/";
|
||||||
|
|
||||||
if (! fs.existsSync(Database.uploadDir)) {
|
if (! fs.existsSync(Database.uploadDir)) {
|
||||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create screenshot dir
|
|
||||||
Database.screenshotDir = path.join(Database.dataDir, "screenshots/");
|
|
||||||
if (! fs.existsSync(Database.screenshotDir)) {
|
|
||||||
fs.mkdirSync(Database.screenshotDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
Database.dockerTLSDir = path.join(Database.dataDir, "docker-tls/");
|
|
||||||
if (! fs.existsSync(Database.dockerTLSDir)) {
|
|
||||||
fs.mkdirSync(Database.dockerTLSDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("db", `Data Dir: ${Database.dataDir}`);
|
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,12 +161,12 @@ class Database {
|
|||||||
await R.exec("PRAGMA journal_mode = WAL");
|
await R.exec("PRAGMA journal_mode = WAL");
|
||||||
}
|
}
|
||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||||
|
|
||||||
// This ensures that an operating system crash or power failure will not corrupt the database.
|
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||||
// FULL synchronous is very safe, but it is also slower.
|
// FULL synchronous is very safe, but it is also slower.
|
||||||
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||||
await R.exec("PRAGMA synchronous = NORMAL");
|
await R.exec("PRAGMA synchronous = FULL");
|
||||||
|
|
||||||
if (!noLog) {
|
if (!noLog) {
|
||||||
log.info("db", "SQLite config:");
|
log.info("db", "SQLite config:");
|
||||||
@@ -210,7 +194,15 @@ class Database {
|
|||||||
} else {
|
} else {
|
||||||
log.info("db", "Database patch is needed");
|
log.info("db", "Database patch is needed");
|
||||||
|
|
||||||
// Try catch anything here
|
try {
|
||||||
|
this.backup(version);
|
||||||
|
} catch (e) {
|
||||||
|
log.error("db", e);
|
||||||
|
log.error("db", "Unable to create a backup before patching the database. Please make sure you have enough space and permission.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try catch anything here, if gone wrong, restore the backup
|
||||||
try {
|
try {
|
||||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||||
const sqlFile = `./db/patch${i}.sql`;
|
const sqlFile = `./db/patch${i}.sql`;
|
||||||
@@ -226,6 +218,7 @@ class Database {
|
|||||||
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
||||||
log.error("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
log.error("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
|
this.restore();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +260,8 @@ class Database {
|
|||||||
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
||||||
log.error("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
log.error("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
|
this.restore();
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +363,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.backup(dayjs().format("YYYYMMDDHHmmss"));
|
||||||
|
|
||||||
log.info("db", sqlFilename + " is patching");
|
log.info("db", sqlFilename + " is patching");
|
||||||
this.patched = true;
|
this.patched = true;
|
||||||
await this.importSQLFile("./db/" + sqlFilename);
|
await this.importSQLFile("./db/" + sqlFilename);
|
||||||
@@ -433,9 +430,6 @@ class Database {
|
|||||||
|
|
||||||
log.info("db", "Closing the database");
|
log.info("db", "Closing the database");
|
||||||
|
|
||||||
// Flush WAL to main database
|
|
||||||
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Database.noReject = true;
|
Database.noReject = true;
|
||||||
await R.close();
|
await R.close();
|
||||||
@@ -452,6 +446,90 @@ class Database {
|
|||||||
process.removeListener("unhandledRejection", listener);
|
process.removeListener("unhandledRejection", listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One backup one time in this process.
|
||||||
|
* Reset this.backupPath if you want to backup again
|
||||||
|
* @param {string} version Version code of backup
|
||||||
|
*/
|
||||||
|
static backup(version) {
|
||||||
|
if (! this.backupPath) {
|
||||||
|
log.info("db", "Backing up the database");
|
||||||
|
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||||
|
fs.copyFileSync(Database.path, this.backupPath);
|
||||||
|
|
||||||
|
const shmPath = Database.path + "-shm";
|
||||||
|
if (fs.existsSync(shmPath)) {
|
||||||
|
this.backupShmPath = shmPath + ".bak" + version;
|
||||||
|
fs.copyFileSync(shmPath, this.backupShmPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const walPath = Database.path + "-wal";
|
||||||
|
if (fs.existsSync(walPath)) {
|
||||||
|
this.backupWalPath = walPath + ".bak" + version;
|
||||||
|
fs.copyFileSync(walPath, this.backupWalPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double confirm if all files actually backup
|
||||||
|
if (!fs.existsSync(this.backupPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(shmPath)) {
|
||||||
|
if (!fs.existsSync(this.backupShmPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupShmPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(walPath)) {
|
||||||
|
if (!fs.existsSync(this.backupWalPath)) {
|
||||||
|
throw new Error("Backup failed! " + this.backupWalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restore from most recent backup */
|
||||||
|
static restore() {
|
||||||
|
if (this.backupPath) {
|
||||||
|
log.error("db", "Patching the database failed!!! Restoring the backup");
|
||||||
|
|
||||||
|
const shmPath = Database.path + "-shm";
|
||||||
|
const walPath = Database.path + "-wal";
|
||||||
|
|
||||||
|
// Delete patch failed db
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(Database.path)) {
|
||||||
|
fs.unlinkSync(Database.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(shmPath)) {
|
||||||
|
fs.unlinkSync(shmPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(walPath)) {
|
||||||
|
fs.unlinkSync(walPath);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error("db", "Restore failed; you may need to restore the backup manually");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore backup
|
||||||
|
fs.copyFileSync(this.backupPath, Database.path);
|
||||||
|
|
||||||
|
if (this.backupShmPath) {
|
||||||
|
fs.copyFileSync(this.backupShmPath, shmPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.backupWalPath) {
|
||||||
|
fs.copyFileSync(this.backupWalPath, walPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.info("db", "Nothing to restore");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Get the size of the database */
|
/** Get the size of the database */
|
||||||
static getSize() {
|
static getSize() {
|
||||||
log.debug("db", "Database.getSize()");
|
log.debug("db", "Database.getSize()");
|
||||||
|
@@ -2,16 +2,8 @@ const axios = require("axios");
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const version = require("../package.json").version;
|
const version = require("../package.json").version;
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const Database = require("./database");
|
|
||||||
|
|
||||||
class DockerHost {
|
class DockerHost {
|
||||||
|
|
||||||
static CertificateFileNameCA = "ca.pem";
|
|
||||||
static CertificateFileNameCert = "cert.pem";
|
|
||||||
static CertificateFileNameKey = "key.pem";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a docker host
|
* Save a docker host
|
||||||
* @param {Object} dockerHost Docker host to save
|
* @param {Object} dockerHost Docker host to save
|
||||||
@@ -74,6 +66,10 @@ class DockerHost {
|
|||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"User-Agent": "Uptime-Kuma/" + version
|
"User-Agent": "Uptime-Kuma/" + version
|
||||||
},
|
},
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dockerHost.dockerType === "socket") {
|
if (dockerHost.dockerType === "socket") {
|
||||||
@@ -81,7 +77,6 @@ class DockerHost {
|
|||||||
} else if (dockerHost.dockerType === "tcp") {
|
} else if (dockerHost.dockerType === "tcp") {
|
||||||
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
||||||
}
|
}
|
||||||
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
|
||||||
|
|
||||||
let res = await axios.request(options);
|
let res = await axios.request(options);
|
||||||
|
|
||||||
@@ -116,53 +111,6 @@ class DockerHost {
|
|||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns HTTPS agent options with client side TLS parameters if certificate files
|
|
||||||
* for the given host are available under a predefined directory path.
|
|
||||||
*
|
|
||||||
* The base path where certificates are looked for can be set with the
|
|
||||||
* 'DOCKER_TLS_DIR_PATH' environmental variable or defaults to 'data/docker-tls/'.
|
|
||||||
*
|
|
||||||
* If a directory in this path exists with a name matching the FQDN of the docker host
|
|
||||||
* (e.g. the FQDN of 'https://example.com:2376' is 'example.com' so the directory
|
|
||||||
* 'data/docker-tls/example.com/' would be searched for certificate files),
|
|
||||||
* then 'ca.pem', 'key.pem' and 'cert.pem' files are included in the agent options.
|
|
||||||
* File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
|
|
||||||
*
|
|
||||||
* @param {String} dockerType i.e. "tcp" or "socket"
|
|
||||||
* @param {String} url The docker host URL rewritten to https://
|
|
||||||
* @return {Object}
|
|
||||||
* */
|
|
||||||
static getHttpsAgentOptions(dockerType, url) {
|
|
||||||
let baseOptions = {
|
|
||||||
maxCachedSessions: 0,
|
|
||||||
rejectUnauthorized: true
|
|
||||||
};
|
|
||||||
let certOptions = {};
|
|
||||||
|
|
||||||
let dirName = (new URL(url)).hostname;
|
|
||||||
|
|
||||||
let caPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCA);
|
|
||||||
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
|
|
||||||
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
|
|
||||||
|
|
||||||
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
|
|
||||||
let ca = fs.readFileSync(caPath);
|
|
||||||
let key = fs.readFileSync(keyPath);
|
|
||||||
let cert = fs.readFileSync(certPath);
|
|
||||||
certOptions = {
|
|
||||||
ca,
|
|
||||||
key,
|
|
||||||
cert
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...baseOptions,
|
|
||||||
...certOptions
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
24
server/git.js
Normal file
24
server/git.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
|
class Git {
|
||||||
|
|
||||||
|
static clone(repoURL, cwd, targetDir = ".") {
|
||||||
|
let result = childProcess.spawnSync("git", [
|
||||||
|
"clone",
|
||||||
|
repoURL,
|
||||||
|
targetDir,
|
||||||
|
], {
|
||||||
|
cwd: cwd,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(result.stderr.toString("utf-8"));
|
||||||
|
} else {
|
||||||
|
return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Git,
|
||||||
|
};
|
@@ -1,51 +1,41 @@
|
|||||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
const path = require("path");
|
||||||
const { clearOldData } = require("./jobs/clear-old-data");
|
const Bree = require("bree");
|
||||||
const { incrementalVacuum } = require("./jobs/incremental-vacuum");
|
const { SHARE_ENV } = require("worker_threads");
|
||||||
const Cron = require("croner");
|
const { log } = require("../src/util");
|
||||||
|
let bree;
|
||||||
const jobs = [
|
const jobs = [
|
||||||
{
|
{
|
||||||
name: "clear-old-data",
|
name: "clear-old-data",
|
||||||
interval: "14 03 * * *",
|
interval: "at 03:14",
|
||||||
jobFunc: clearOldData,
|
|
||||||
croner: null,
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "incremental-vacuum",
|
|
||||||
interval: "*/5 * * * *",
|
|
||||||
jobFunc: incrementalVacuum,
|
|
||||||
croner: null,
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize background jobs
|
* Initialize background jobs
|
||||||
* @returns {Promise<void>}
|
* @param {Object} args Arguments to pass to workers
|
||||||
|
* @returns {Bree}
|
||||||
*/
|
*/
|
||||||
const initBackgroundJobs = async function () {
|
const initBackgroundJobs = function (args) {
|
||||||
const timezone = await UptimeKumaServer.getInstance().getTimezone();
|
bree = new Bree({
|
||||||
|
root: path.resolve("server", "jobs"),
|
||||||
for (const job of jobs) {
|
jobs,
|
||||||
const cornerJob = new Cron(
|
worker: {
|
||||||
job.interval,
|
env: SHARE_ENV,
|
||||||
{
|
workerData: args,
|
||||||
name: job.name,
|
|
||||||
timezone,
|
|
||||||
},
|
},
|
||||||
job.jobFunc,
|
workerMessageHandler: (message) => {
|
||||||
);
|
log.info("jobs", message);
|
||||||
job.croner = cornerJob;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bree.start();
|
||||||
|
return bree;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Stop all background jobs if running */
|
/** Stop all background jobs if running */
|
||||||
const stopBackgroundJobs = function () {
|
const stopBackgroundJobs = function () {
|
||||||
for (const job of jobs) {
|
if (bree) {
|
||||||
if (job.croner) {
|
bree.stop();
|
||||||
job.croner.stop();
|
|
||||||
job.croner = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,15 +1,12 @@
|
|||||||
|
const { log, exit, connectDb } = require("./util-worker");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { log } = require("../../src/util");
|
|
||||||
const { setSetting, setting } = require("../util-server");
|
const { setSetting, setting } = require("../util-server");
|
||||||
|
|
||||||
const DEFAULT_KEEP_PERIOD = 180;
|
const DEFAULT_KEEP_PERIOD = 180;
|
||||||
|
|
||||||
/**
|
(async () => {
|
||||||
* Clears old data from the heartbeat table of the database.
|
await connectDb();
|
||||||
* @return {Promise<void>} A promise that resolves when the data has been cleared.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const clearOldData = async () => {
|
|
||||||
let period = await setting("keepDataPeriodDays");
|
let period = await setting("keepDataPeriodDays");
|
||||||
|
|
||||||
// Set Default Period
|
// Set Default Period
|
||||||
@@ -23,30 +20,26 @@ const clearOldData = async () => {
|
|||||||
try {
|
try {
|
||||||
parsedPeriod = parseInt(period);
|
parsedPeriod = parseInt(period);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
log.warn("clearOldData", "Failed to parse setting, resetting to default..");
|
log("Failed to parse setting, resetting to default..");
|
||||||
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||||
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedPeriod < 1) {
|
if (parsedPeriod < 1) {
|
||||||
log.info("clearOldData", `Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
log(`Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
|
log(`Clearing Data older than ${parsedPeriod} days...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await R.exec(
|
await R.exec(
|
||||||
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||||
[ parsedPeriod ]
|
[ parsedPeriod ]
|
||||||
);
|
);
|
||||||
|
|
||||||
await R.exec("PRAGMA optimize;");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
log(`Failed to clear old data: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
exit();
|
||||||
clearOldData,
|
})();
|
||||||
};
|
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
const { R } = require("redbean-node");
|
|
||||||
const { log } = require("../../src/util");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run incremental_vacuum and checkpoint the WAL.
|
|
||||||
* @return {Promise<void>} A promise that resolves when the process is finished.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const incrementalVacuum = async () => {
|
|
||||||
try {
|
|
||||||
log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)...");
|
|
||||||
await R.exec("PRAGMA incremental_vacuum(200)");
|
|
||||||
await R.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
|
||||||
} catch (e) {
|
|
||||||
log.error("incrementalVacuum", `Failed: ${e.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
incrementalVacuum,
|
|
||||||
};
|
|
50
server/jobs/util-worker.js
Normal file
50
server/jobs/util-worker.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const { parentPort, workerData } = require("worker_threads");
|
||||||
|
const Database = require("../database");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message to parent process for logging
|
||||||
|
* since worker_thread does not have access to stdout, this is used
|
||||||
|
* instead of console.log()
|
||||||
|
* @param {any} any The message to log
|
||||||
|
*/
|
||||||
|
const log = function (any) {
|
||||||
|
if (parentPort) {
|
||||||
|
parentPort.postMessage(any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exit the worker process
|
||||||
|
* @param {number} error The status code to exit
|
||||||
|
*/
|
||||||
|
const exit = function (error) {
|
||||||
|
if (error && error !== 0) {
|
||||||
|
process.exit(error);
|
||||||
|
} else {
|
||||||
|
if (parentPort) {
|
||||||
|
parentPort.postMessage("done");
|
||||||
|
} else {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Connects to the database */
|
||||||
|
const connectDb = async function () {
|
||||||
|
const dbPath = path.join(
|
||||||
|
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||||
|
);
|
||||||
|
|
||||||
|
Database.init({
|
||||||
|
"data-dir": dbPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Database.connect();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
log,
|
||||||
|
exit,
|
||||||
|
connectDb,
|
||||||
|
};
|
@@ -1,76 +0,0 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
|
||||||
const { R } = require("redbean-node");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
|
|
||||||
class APIKey extends BeanModel {
|
|
||||||
/**
|
|
||||||
* Get the current status of this API key
|
|
||||||
* @returns {string} active, inactive or expired
|
|
||||||
*/
|
|
||||||
getStatus() {
|
|
||||||
let current = dayjs();
|
|
||||||
let expiry = dayjs(this.expires);
|
|
||||||
if (expiry.diff(current) < 0) {
|
|
||||||
return "expired";
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.active ? "active" : "inactive";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object that ready to parse to JSON
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
key: this.key,
|
|
||||||
name: this.name,
|
|
||||||
userID: this.user_id,
|
|
||||||
createdDate: this.created_date,
|
|
||||||
active: this.active,
|
|
||||||
expires: this.expires,
|
|
||||||
status: this.getStatus(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object that ready to parse to JSON with sensitive fields
|
|
||||||
* removed
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
toPublicJSON() {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
userID: this.user_id,
|
|
||||||
createdDate: this.created_date,
|
|
||||||
active: this.active,
|
|
||||||
expires: this.expires,
|
|
||||||
status: this.getStatus(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new API Key and store it in the database
|
|
||||||
* @param {Object} key Object sent by client
|
|
||||||
* @param {int} userID ID of socket user
|
|
||||||
* @returns {Promise<bean>}
|
|
||||||
*/
|
|
||||||
static async save(key, userID) {
|
|
||||||
let bean;
|
|
||||||
bean = R.dispense("api_key");
|
|
||||||
|
|
||||||
bean.key = key.key;
|
|
||||||
bean.name = key.name;
|
|
||||||
bean.user_id = userID;
|
|
||||||
bean.active = key.active;
|
|
||||||
bean.expires = key.expires;
|
|
||||||
|
|
||||||
await R.store(bean);
|
|
||||||
|
|
||||||
return bean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = APIKey;
|
|
@@ -9,12 +9,12 @@ class Group extends BeanModel {
|
|||||||
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
async toPublicJSON(showTags = false) {
|
||||||
let monitorBeanList = await this.getMonitorList();
|
let monitorBeanList = await this.getMonitorList();
|
||||||
let monitorList = [];
|
let monitorList = [];
|
||||||
|
|
||||||
for (let bean of monitorBeanList) {
|
for (let bean of monitorBeanList) {
|
||||||
monitorList.push(await bean.toPublicJSON(showTags, certExpiry));
|
monitorList.push(await bean.toPublicJSON(showTags));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -1,10 +1,8 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { parseTimeObject, parseTimeFromTimeObject, log } = require("../../src/util");
|
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
|
||||||
|
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const Cron = require("croner");
|
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
|
||||||
const apicache = require("../modules/apicache");
|
|
||||||
|
|
||||||
class Maintenance extends BeanModel {
|
class Maintenance extends BeanModel {
|
||||||
|
|
||||||
@@ -17,19 +15,16 @@ class Maintenance extends BeanModel {
|
|||||||
|
|
||||||
let dateRange = [];
|
let dateRange = [];
|
||||||
if (this.start_date) {
|
if (this.start_date) {
|
||||||
dateRange.push(this.start_date);
|
dateRange.push(utcToLocal(this.start_date));
|
||||||
} else {
|
|
||||||
dateRange.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.end_date) {
|
if (this.end_date) {
|
||||||
dateRange.push(this.end_date);
|
dateRange.push(utcToLocal(this.end_date));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeRange = [];
|
let timeRange = [];
|
||||||
let startTime = parseTimeObject(this.start_time);
|
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
|
||||||
timeRange.push(startTime);
|
timeRange.push(startTime);
|
||||||
let endTime = parseTimeObject(this.end_time);
|
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
|
||||||
timeRange.push(endTime);
|
timeRange.push(endTime);
|
||||||
|
|
||||||
let obj = {
|
let obj = {
|
||||||
@@ -44,44 +39,12 @@ class Maintenance extends BeanModel {
|
|||||||
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
||||||
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
||||||
timeslotList: [],
|
timeslotList: [],
|
||||||
cron: this.cron,
|
|
||||||
duration: this.duration,
|
|
||||||
durationMinutes: parseInt(this.duration / 60),
|
|
||||||
timezone: await this.getTimezone(), // Only valid timezone
|
|
||||||
timezoneOption: this.timezone, // Mainly for dropdown menu, because there is a option "SAME_AS_SERVER"
|
|
||||||
timezoneOffset: await this.getTimezoneOffset(),
|
|
||||||
status: await this.getStatus(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.strategy === "manual") {
|
const timeslotList = await this.getTimeslotList();
|
||||||
// Do nothing, no timeslots
|
|
||||||
} else if (this.strategy === "single") {
|
|
||||||
obj.timeslotList.push({
|
|
||||||
startDate: this.start_date,
|
|
||||||
endDate: this.end_date,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Should be cron or recurring here
|
|
||||||
if (this.beanMeta.job) {
|
|
||||||
let runningTimeslot = this.getRunningTimeslot();
|
|
||||||
|
|
||||||
if (runningTimeslot) {
|
for (let timeslot of timeslotList) {
|
||||||
obj.timeslotList.push(runningTimeslot);
|
obj.timeslotList.push(await timeslot.toPublicJSON());
|
||||||
}
|
|
||||||
|
|
||||||
let nextRunDate = this.beanMeta.job.nextRun();
|
|
||||||
if (nextRunDate) {
|
|
||||||
let startDateDayjs = dayjs(nextRunDate);
|
|
||||||
|
|
||||||
let startDate = startDateDayjs.toISOString();
|
|
||||||
let endDate = startDateDayjs.add(this.duration, "second").toISOString();
|
|
||||||
|
|
||||||
obj.timeslotList.push({
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(obj.weekdays)) {
|
if (!Array.isArray(obj.weekdays)) {
|
||||||
@@ -92,9 +55,54 @@ class Maintenance extends BeanModel {
|
|||||||
obj.daysOfMonth = [];
|
obj.daysOfMonth = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Maintenance Status
|
||||||
|
if (!obj.active) {
|
||||||
|
obj.status = "inactive";
|
||||||
|
} else if (obj.strategy === "manual") {
|
||||||
|
obj.status = "under-maintenance";
|
||||||
|
} else if (obj.timeslotList.length > 0) {
|
||||||
|
let currentTimestamp = dayjs().unix();
|
||||||
|
|
||||||
|
for (let timeslot of obj.timeslotList) {
|
||||||
|
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
|
||||||
|
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
|
||||||
|
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
|
||||||
|
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
|
||||||
|
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
|
||||||
|
|
||||||
|
obj.status = "under-maintenance";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!obj.status) {
|
||||||
|
obj.status = "scheduled";
|
||||||
|
}
|
||||||
|
} else if (obj.timeslotList.length === 0) {
|
||||||
|
obj.status = "ended";
|
||||||
|
} else {
|
||||||
|
obj.status = "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only get future or current timeslots only
|
||||||
|
* @returns {Promise<[]>}
|
||||||
|
*/
|
||||||
|
async getTimeslotList() {
|
||||||
|
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
|
||||||
|
SELECT maintenance_timeslot.*
|
||||||
|
FROM maintenance_timeslot, maintenance
|
||||||
|
WHERE maintenance_timeslot.maintenance_id = maintenance.id
|
||||||
|
AND maintenance.id = ?
|
||||||
|
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
|
||||||
|
`, [
|
||||||
|
this.id
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an object that ready to parse to JSON
|
* Return an object that ready to parse to JSON
|
||||||
* @param {string} timezone If not specified, the timeRange will be in UTC
|
* @param {string} timezone If not specified, the timeRange will be in UTC
|
||||||
@@ -118,7 +126,7 @@ class Maintenance extends BeanModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of days in month that maintenance is active for
|
* Get a list of days in month that maintenance is active for
|
||||||
* @returns {number[]|string[]} Array of active days in month
|
* @returns {number[]} Array of active days in month
|
||||||
*/
|
*/
|
||||||
getDayOfMonthList() {
|
getDayOfMonthList() {
|
||||||
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
||||||
@@ -127,10 +135,26 @@ class Maintenance extends BeanModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the duration of maintenance in seconds
|
* Get the start date and time for maintenance
|
||||||
|
* @returns {dayjs.Dayjs} Start date and time
|
||||||
|
*/
|
||||||
|
getStartDateTime() {
|
||||||
|
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
|
||||||
|
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
|
||||||
|
|
||||||
|
// Start Time
|
||||||
|
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
|
||||||
|
log.debug("timeslot", "startTime: " + startTimeSecond);
|
||||||
|
|
||||||
|
// Bake StartDate + StartTime = Start DateTime
|
||||||
|
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the duraction of maintenance in seconds
|
||||||
* @returns {number} Duration of maintenance
|
* @returns {number} Duration of maintenance
|
||||||
*/
|
*/
|
||||||
calcDuration() {
|
getDuration() {
|
||||||
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
|
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
|
||||||
// Add 24hours if it is across day
|
// Add 24hours if it is across day
|
||||||
if (duration < 0) {
|
if (duration < 0) {
|
||||||
@@ -145,270 +169,71 @@ class Maintenance extends BeanModel {
|
|||||||
* @param {Object} obj Data to fill bean with
|
* @param {Object} obj Data to fill bean with
|
||||||
* @returns {Bean} Filled bean
|
* @returns {Bean} Filled bean
|
||||||
*/
|
*/
|
||||||
static async jsonToBean(bean, obj) {
|
static jsonToBean(bean, obj) {
|
||||||
if (obj.id) {
|
if (obj.id) {
|
||||||
bean.id = obj.id;
|
bean.id = obj.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply timezone offset to timeRange, as it cannot apply automatically.
|
||||||
|
if (obj.timeRange[0]) {
|
||||||
|
timeObjectToUTC(obj.timeRange[0]);
|
||||||
|
if (obj.timeRange[1]) {
|
||||||
|
timeObjectToUTC(obj.timeRange[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bean.title = obj.title;
|
bean.title = obj.title;
|
||||||
bean.description = obj.description;
|
bean.description = obj.description;
|
||||||
bean.strategy = obj.strategy;
|
bean.strategy = obj.strategy;
|
||||||
bean.interval_day = obj.intervalDay;
|
bean.interval_day = obj.intervalDay;
|
||||||
bean.timezone = obj.timezoneOption;
|
|
||||||
bean.active = obj.active;
|
bean.active = obj.active;
|
||||||
|
|
||||||
if (obj.dateRange[0]) {
|
if (obj.dateRange[0]) {
|
||||||
bean.start_date = obj.dateRange[0];
|
bean.start_date = localToUTC(obj.dateRange[0]);
|
||||||
} else {
|
|
||||||
bean.start_date = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.dateRange[1]) {
|
if (obj.dateRange[1]) {
|
||||||
bean.end_date = obj.dateRange[1];
|
bean.end_date = localToUTC(obj.dateRange[1]);
|
||||||
} else {
|
}
|
||||||
bean.end_date = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bean.strategy === "cron") {
|
|
||||||
bean.duration = obj.durationMinutes * 60;
|
|
||||||
bean.cron = obj.cron;
|
|
||||||
this.validateCron(bean.cron);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bean.strategy.startsWith("recurring-")) {
|
|
||||||
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||||
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||||
|
|
||||||
bean.weekdays = JSON.stringify(obj.weekdays);
|
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||||
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||||
await bean.generateCron();
|
|
||||||
this.validateCron(bean.cron);
|
|
||||||
}
|
|
||||||
return bean;
|
return bean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throw error if cron is invalid
|
* SQL conditions for active maintenance
|
||||||
* @param cron
|
* @returns {string}
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
static async validateCron(cron) {
|
static getActiveMaintenanceSQLCondition() {
|
||||||
let job = new Cron(cron, () => {});
|
return `
|
||||||
job.stop();
|
(
|
||||||
|
(maintenance_timeslot.start_date <= DATETIME('now')
|
||||||
|
AND maintenance_timeslot.end_date >= DATETIME('now')
|
||||||
|
AND maintenance.active = 1)
|
||||||
|
OR
|
||||||
|
(maintenance.strategy = 'manual' AND active = 1)
|
||||||
|
)
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the cron
|
* SQL conditions for active and future maintenance
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
async run(throwError = false) {
|
static getActiveAndFutureMaintenanceSQLCondition() {
|
||||||
if (this.beanMeta.job) {
|
return `
|
||||||
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
|
(
|
||||||
this.stop();
|
((maintenance_timeslot.end_date >= DATETIME('now')
|
||||||
}
|
AND maintenance.active = 1)
|
||||||
|
OR
|
||||||
log.debug("maintenance", "Run maintenance id: " + this.id);
|
(maintenance.strategy = 'manual' AND active = 1))
|
||||||
|
)
|
||||||
// 1.21.2 migration
|
`;
|
||||||
if (!this.cron) {
|
|
||||||
await this.generateCron();
|
|
||||||
if (!this.timezone) {
|
|
||||||
this.timezone = "UTC";
|
|
||||||
}
|
|
||||||
if (this.cron) {
|
|
||||||
await R.store(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.strategy === "manual") {
|
|
||||||
// Do nothing, because it is controlled by the user
|
|
||||||
} else if (this.strategy === "single") {
|
|
||||||
this.beanMeta.job = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => {
|
|
||||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
|
||||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
|
||||||
apicache.clear();
|
|
||||||
});
|
|
||||||
} else if (this.cron != null) {
|
|
||||||
// Here should be cron or recurring
|
|
||||||
try {
|
|
||||||
this.beanMeta.status = "scheduled";
|
|
||||||
|
|
||||||
let startEvent = (customDuration = 0) => {
|
|
||||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
|
||||||
|
|
||||||
this.beanMeta.status = "under-maintenance";
|
|
||||||
clearTimeout(this.beanMeta.durationTimeout);
|
|
||||||
|
|
||||||
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
|
||||||
let duration;
|
|
||||||
|
|
||||||
if (customDuration > 0) {
|
|
||||||
duration = customDuration;
|
|
||||||
} else if (this.end_date) {
|
|
||||||
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
|
||||||
if (d < this.duration) {
|
|
||||||
duration = d * 1000;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
duration = this.duration * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
|
||||||
|
|
||||||
this.beanMeta.durationTimeout = setTimeout(() => {
|
|
||||||
// End of maintenance for this timeslot
|
|
||||||
this.beanMeta.status = "scheduled";
|
|
||||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
|
||||||
}, duration);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create Cron
|
|
||||||
this.beanMeta.job = new Cron(this.cron, {
|
|
||||||
timezone: await this.getTimezone(),
|
|
||||||
}, startEvent);
|
|
||||||
|
|
||||||
// Continue if the maintenance is still in the window
|
|
||||||
let runningTimeslot = this.getRunningTimeslot();
|
|
||||||
let current = dayjs();
|
|
||||||
|
|
||||||
if (runningTimeslot) {
|
|
||||||
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
|
|
||||||
log.debug("maintenance", "Maintenance id: " + this.id + " Remaining duration: " + duration + "ms");
|
|
||||||
startEvent(duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
log.error("maintenance", "Error in maintenance id: " + this.id);
|
|
||||||
log.error("maintenance", "Cron: " + this.cron);
|
|
||||||
log.error("maintenance", e);
|
|
||||||
|
|
||||||
if (throwError) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
log.error("maintenance", "Maintenance id: " + this.id + " has no cron");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getRunningTimeslot() {
|
|
||||||
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate()));
|
|
||||||
let end = start.add(this.duration, "second");
|
|
||||||
let current = dayjs();
|
|
||||||
|
|
||||||
if (current.isAfter(start) && current.isBefore(end)) {
|
|
||||||
return {
|
|
||||||
startDate: start.toISOString(),
|
|
||||||
endDate: end.toISOString(),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
if (this.beanMeta.job) {
|
|
||||||
this.beanMeta.job.stop();
|
|
||||||
delete this.beanMeta.job;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async isUnderMaintenance() {
|
|
||||||
return (await this.getStatus()) === "under-maintenance";
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTimezone() {
|
|
||||||
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
|
|
||||||
return await UptimeKumaServer.getInstance().getTimezone();
|
|
||||||
}
|
|
||||||
return this.timezone;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTimezoneOffset() {
|
|
||||||
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStatus() {
|
|
||||||
if (!this.active) {
|
|
||||||
return "inactive";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.strategy === "manual") {
|
|
||||||
return "under-maintenance";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the maintenance is started
|
|
||||||
if (this.start_date && dayjs().isBefore(dayjs.tz(this.start_date, await this.getTimezone()))) {
|
|
||||||
return "scheduled";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the maintenance is ended
|
|
||||||
if (this.end_date && dayjs().isAfter(dayjs.tz(this.end_date, await this.getTimezone()))) {
|
|
||||||
return "ended";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.strategy === "single") {
|
|
||||||
return "under-maintenance";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.beanMeta.status) {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.beanMeta.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Cron for recurring maintenance
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async generateCron() {
|
|
||||||
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
|
|
||||||
|
|
||||||
if (this.strategy === "cron") {
|
|
||||||
// Do nothing for cron
|
|
||||||
} else if (!this.strategy.startsWith("recurring-")) {
|
|
||||||
this.cron = "";
|
|
||||||
} else if (this.strategy === "recurring-interval") {
|
|
||||||
let array = this.start_time.split(":");
|
|
||||||
let hour = parseInt(array[0]);
|
|
||||||
let minute = parseInt(array[1]);
|
|
||||||
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
|
|
||||||
this.duration = this.calcDuration();
|
|
||||||
log.debug("maintenance", "Cron: " + this.cron);
|
|
||||||
log.debug("maintenance", "Duration: " + this.duration);
|
|
||||||
} else if (this.strategy === "recurring-weekday") {
|
|
||||||
let list = this.getDayOfWeekList();
|
|
||||||
let array = this.start_time.split(":");
|
|
||||||
let hour = parseInt(array[0]);
|
|
||||||
let minute = parseInt(array[1]);
|
|
||||||
this.cron = minute + " " + hour + " * * " + list.join(",");
|
|
||||||
this.duration = this.calcDuration();
|
|
||||||
} else if (this.strategy === "recurring-day-of-month") {
|
|
||||||
let list = this.getDayOfMonthList();
|
|
||||||
let array = this.start_time.split(":");
|
|
||||||
let hour = parseInt(array[0]);
|
|
||||||
let minute = parseInt(array[1]);
|
|
||||||
|
|
||||||
let dayList = [];
|
|
||||||
|
|
||||||
for (let day of list) {
|
|
||||||
if (typeof day === "string" && day.startsWith("lastDay")) {
|
|
||||||
if (day === "lastDay1") {
|
|
||||||
dayList.push("L");
|
|
||||||
}
|
|
||||||
// Unfortunately, lastDay2-4 is not supported by cron
|
|
||||||
} else {
|
|
||||||
dayList.push(day);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicate
|
|
||||||
dayList = [ ...new Set(dayList) ];
|
|
||||||
|
|
||||||
this.cron = minute + " " + hour + " " + dayList.join(",") + " * *";
|
|
||||||
this.duration = this.calcDuration();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
198
server/model/maintenance_timeslot.js
Normal file
198
server/model/maintenance_timeslot.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
|
||||||
|
class MaintenanceTimeslot extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toPublicJSON() {
|
||||||
|
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
id: this.id,
|
||||||
|
startDate: this.start_date,
|
||||||
|
endDate: this.end_date,
|
||||||
|
startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||||
|
endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||||
|
serverTimezoneOffset,
|
||||||
|
};
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toJSON() {
|
||||||
|
return await this.toPublicJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Maintenance} maintenance
|
||||||
|
* @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
|
||||||
|
* @param {boolean} removeExist Remove existing timeslot before create
|
||||||
|
* @returns {Promise<MaintenanceTimeslot>}
|
||||||
|
*/
|
||||||
|
static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
|
||||||
|
if (removeExist) {
|
||||||
|
await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
|
||||||
|
maintenance.id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maintenance.strategy === "manual") {
|
||||||
|
log.debug("maintenance", "No need to generate timeslot for manual type");
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "single") {
|
||||||
|
let bean = R.dispense("maintenance_timeslot");
|
||||||
|
bean.maintenance_id = maintenance.id;
|
||||||
|
bean.start_date = maintenance.start_date;
|
||||||
|
bean.end_date = maintenance.end_date;
|
||||||
|
bean.generated_next = true;
|
||||||
|
return await R.store(bean);
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "recurring-interval") {
|
||||||
|
// Prevent dead loop, in case interval_day is not set
|
||||||
|
if (!maintenance.interval_day || maintenance.interval_day <= 0) {
|
||||||
|
maintenance.interval_day = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||||
|
return startDateTime.add(maintenance.interval_day, "day");
|
||||||
|
}, () => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "recurring-weekday") {
|
||||||
|
let dayOfWeekList = maintenance.getDayOfWeekList();
|
||||||
|
log.debug("timeslot", dayOfWeekList);
|
||||||
|
|
||||||
|
if (dayOfWeekList.length <= 0) {
|
||||||
|
log.debug("timeslot", "No weekdays selected?");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = (startDateTime) => {
|
||||||
|
log.debug("timeslot", "nextDateTime: " + startDateTime);
|
||||||
|
|
||||||
|
let day = startDateTime.local().day();
|
||||||
|
log.debug("timeslot", "nextDateTime.day(): " + day);
|
||||||
|
|
||||||
|
return dayOfWeekList.includes(day);
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||||
|
while (true) {
|
||||||
|
startDateTime = startDateTime.add(1, "day");
|
||||||
|
|
||||||
|
if (isValid(startDateTime)) {
|
||||||
|
return startDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, isValid);
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "recurring-day-of-month") {
|
||||||
|
let dayOfMonthList = maintenance.getDayOfMonthList();
|
||||||
|
if (dayOfMonthList.length <= 0) {
|
||||||
|
log.debug("timeslot", "No day selected?");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = (startDateTime) => {
|
||||||
|
let day = parseInt(startDateTime.local().format("D"));
|
||||||
|
|
||||||
|
log.debug("timeslot", "day: " + day);
|
||||||
|
|
||||||
|
// Check 1-31
|
||||||
|
if (dayOfMonthList.includes(day)) {
|
||||||
|
return startDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check "lastDay1","lastDay2"...
|
||||||
|
let daysInMonth = startDateTime.daysInMonth();
|
||||||
|
let lastDayList = [];
|
||||||
|
|
||||||
|
// Small first, e.g. 28 > 29 > 30 > 31
|
||||||
|
for (let i = 4; i >= 1; i--) {
|
||||||
|
if (dayOfMonthList.includes("lastDay" + i)) {
|
||||||
|
lastDayList.push(daysInMonth - i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug("timeslot", lastDayList);
|
||||||
|
return lastDayList.includes(day);
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||||
|
while (true) {
|
||||||
|
startDateTime = startDateTime.add(1, "day");
|
||||||
|
if (isValid(startDateTime)) {
|
||||||
|
return startDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, isValid);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown maintenance strategy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a next timeslot for all recurring types
|
||||||
|
* @param maintenance
|
||||||
|
* @param minDate
|
||||||
|
* @param {function} nextDayCallback The logic how to get the next possible day
|
||||||
|
* @param {function} isValidCallback Check the day whether is matched the current strategy
|
||||||
|
* @returns {Promise<null|MaintenanceTimeslot>}
|
||||||
|
*/
|
||||||
|
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
|
||||||
|
let bean = R.dispense("maintenance_timeslot");
|
||||||
|
|
||||||
|
let duration = maintenance.getDuration();
|
||||||
|
let startDateTime = maintenance.getStartDateTime();
|
||||||
|
let endDateTime;
|
||||||
|
|
||||||
|
// Keep generating from the first possible date, until it is ok
|
||||||
|
while (true) {
|
||||||
|
log.debug("timeslot", "startDateTime: " + startDateTime.format());
|
||||||
|
|
||||||
|
// Handling out of effective date range
|
||||||
|
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||||
|
log.debug("timeslot", "Out of effective date range");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
endDateTime = startDateTime.add(duration, "second");
|
||||||
|
|
||||||
|
// If endDateTime is out of effective date range, use the end datetime from effective date range
|
||||||
|
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||||
|
endDateTime = dayjs.utc(maintenance.end_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If minDate is set, the endDateTime must be bigger than it.
|
||||||
|
// And the endDateTime must be bigger current time
|
||||||
|
// Is valid under current recurring strategy
|
||||||
|
if (
|
||||||
|
(!minDate || endDateTime.diff(minDate) > 0) &&
|
||||||
|
endDateTime.diff(dayjs()) > 0 &&
|
||||||
|
isValidCallback(startDateTime)
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startDateTime = nextDayCallback(startDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.maintenance_id = maintenance.id;
|
||||||
|
bean.start_date = localToUTC(startDateTime);
|
||||||
|
bean.end_date = localToUTC(endDateTime);
|
||||||
|
bean.generated_next = false;
|
||||||
|
return await R.store(bean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MaintenanceTimeslot;
|
@@ -2,11 +2,9 @@ const https = require("https");
|
|||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
|
||||||
SQL_DATETIME_FORMAT
|
|
||||||
} = require("../../src/util");
|
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||||
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials,
|
redisPingAsync, mongodbPing,
|
||||||
} = require("../util-server");
|
} = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
@@ -18,10 +16,9 @@ const apicache = require("../modules/apicache");
|
|||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||||
const { DockerHost } = require("../docker");
|
const { DockerHost } = require("../docker");
|
||||||
|
const Maintenance = require("./maintenance");
|
||||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||||
const Gamedig = require("gamedig");
|
const Gamedig = require("gamedig");
|
||||||
const jsonata = require("jsonata");
|
|
||||||
const jwt = require("jsonwebtoken");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
@@ -37,12 +34,11 @@ class Monitor extends BeanModel {
|
|||||||
* Only show necessary data to public
|
* Only show necessary data to public
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
async toPublicJSON(showTags = false) {
|
||||||
let obj = {
|
let obj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
sendUrl: this.sendUrl,
|
sendUrl: this.sendUrl,
|
||||||
type: this.type,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.sendUrl) {
|
if (this.sendUrl) {
|
||||||
@@ -52,13 +48,6 @@ class Monitor extends BeanModel {
|
|||||||
if (showTags) {
|
if (showTags) {
|
||||||
obj.tags = await this.getTags();
|
obj.tags = await this.getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (certExpiry && this.type === "http") {
|
|
||||||
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
|
|
||||||
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
|
|
||||||
obj.validCert = validCert;
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,34 +69,21 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
const tags = await this.getTags();
|
const tags = await this.getTags();
|
||||||
|
|
||||||
let screenshot = null;
|
|
||||||
|
|
||||||
if (this.type === "real-browser") {
|
|
||||||
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
|
||||||
pathName: await this.getPathName(),
|
|
||||||
parent: this.parent,
|
|
||||||
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
|
|
||||||
url: this.url,
|
url: this.url,
|
||||||
method: this.method,
|
method: this.method,
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
maxretries: this.maxretries,
|
maxretries: this.maxretries,
|
||||||
weight: this.weight,
|
weight: this.weight,
|
||||||
active: await this.isActive(),
|
active: this.active,
|
||||||
forceInactive: !await Monitor.isParentActive(this.id),
|
|
||||||
type: this.type,
|
type: this.type,
|
||||||
timeout: this.timeout,
|
|
||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
retryInterval: this.retryInterval,
|
retryInterval: this.retryInterval,
|
||||||
resendInterval: this.resendInterval,
|
resendInterval: this.resendInterval,
|
||||||
keyword: this.keyword,
|
keyword: this.keyword,
|
||||||
invertKeyword: this.isInvertKeyword(),
|
|
||||||
expiryNotification: this.isEnabledExpiryNotification(),
|
expiryNotification: this.isEnabledExpiryNotification(),
|
||||||
ignoreTls: this.getIgnoreTls(),
|
ignoreTls: this.getIgnoreTls(),
|
||||||
upsideDown: this.isUpsideDown(),
|
upsideDown: this.isUpsideDown(),
|
||||||
@@ -135,16 +111,6 @@ class Monitor extends BeanModel {
|
|||||||
radiusCalledStationId: this.radiusCalledStationId,
|
radiusCalledStationId: this.radiusCalledStationId,
|
||||||
radiusCallingStationId: this.radiusCallingStationId,
|
radiusCallingStationId: this.radiusCallingStationId,
|
||||||
game: this.game,
|
game: this.game,
|
||||||
gamedigGivenPortOnly: this.getGameDigGivenPortOnly(),
|
|
||||||
httpBodyEncoding: this.httpBodyEncoding,
|
|
||||||
jsonPath: this.jsonPath,
|
|
||||||
expectedValue: this.expectedValue,
|
|
||||||
kafkaProducerTopic: this.kafkaProducerTopic,
|
|
||||||
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
|
||||||
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
|
|
||||||
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
|
|
||||||
kafkaProducerMessage: this.kafkaProducerMessage,
|
|
||||||
screenshot,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
@@ -156,11 +122,6 @@ class Monitor extends BeanModel {
|
|||||||
grpcMetadata: this.grpcMetadata,
|
grpcMetadata: this.grpcMetadata,
|
||||||
basic_auth_user: this.basic_auth_user,
|
basic_auth_user: this.basic_auth_user,
|
||||||
basic_auth_pass: this.basic_auth_pass,
|
basic_auth_pass: this.basic_auth_pass,
|
||||||
oauth_client_id: this.oauth_client_id,
|
|
||||||
oauth_client_secret: this.oauth_client_secret,
|
|
||||||
oauth_token_url: this.oauth_token_url,
|
|
||||||
oauth_scopes: this.oauth_scopes,
|
|
||||||
oauth_auth_method: this.oauth_auth_method,
|
|
||||||
pushToken: this.pushToken,
|
pushToken: this.pushToken,
|
||||||
databaseConnectionString: this.databaseConnectionString,
|
databaseConnectionString: this.databaseConnectionString,
|
||||||
radiusUsername: this.radiusUsername,
|
radiusUsername: this.radiusUsername,
|
||||||
@@ -170,10 +131,6 @@ class Monitor extends BeanModel {
|
|||||||
mqttPassword: this.mqttPassword,
|
mqttPassword: this.mqttPassword,
|
||||||
authWorkstation: this.authWorkstation,
|
authWorkstation: this.authWorkstation,
|
||||||
authDomain: this.authDomain,
|
authDomain: this.authDomain,
|
||||||
tlsCa: this.tlsCa,
|
|
||||||
tlsCert: this.tlsCert,
|
|
||||||
tlsKey: this.tlsKey,
|
|
||||||
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,47 +138,12 @@ class Monitor extends BeanModel {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the monitor is active based on itself and its parents
|
|
||||||
* @returns {Promise<Boolean>}
|
|
||||||
*/
|
|
||||||
async isActive() {
|
|
||||||
const parentActive = await Monitor.isParentActive(this.id);
|
|
||||||
|
|
||||||
return (this.active === 1) && parentActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all tags applied to this monitor
|
* Get all tags applied to this monitor
|
||||||
* @returns {Promise<LooseObject<any>[]>}
|
* @returns {Promise<LooseObject<any>[]>}
|
||||||
*/
|
*/
|
||||||
async getTags() {
|
async getTags() {
|
||||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]);
|
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets certificate expiry for this monitor
|
|
||||||
* @param {number} monitorID ID of monitor to send
|
|
||||||
* @returns {Promise<LooseObject<any>>}
|
|
||||||
*/
|
|
||||||
async getCertExpiry(monitorID) {
|
|
||||||
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
let tlsInfo;
|
|
||||||
if (tlsInfoBean) {
|
|
||||||
tlsInfo = JSON.parse(tlsInfoBean?.info_json);
|
|
||||||
if (tlsInfo?.valid && tlsInfo?.certInfo?.daysRemaining) {
|
|
||||||
return {
|
|
||||||
certExpiryDaysRemaining: tlsInfo.certInfo.daysRemaining,
|
|
||||||
validCert: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
certExpiryDaysRemaining: "",
|
|
||||||
validCert: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,14 +179,6 @@ class Monitor extends BeanModel {
|
|||||||
return Boolean(this.upsideDown);
|
return Boolean(this.upsideDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse to boolean
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isInvertKeyword() {
|
|
||||||
return Boolean(this.invertKeyword);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse to boolean
|
* Parse to boolean
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -281,10 +195,6 @@ class Monitor extends BeanModel {
|
|||||||
return JSON.parse(this.accepted_statuscodes_json);
|
return JSON.parse(this.accepted_statuscodes_json);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGameDigGivenPortOnly() {
|
|
||||||
return Boolean(this.gamedigGivenPortOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitor
|
* Start monitor
|
||||||
* @param {Server} io Socket server instance
|
* @param {Server} io Socket server instance
|
||||||
@@ -293,7 +203,7 @@ class Monitor extends BeanModel {
|
|||||||
let previousBeat = null;
|
let previousBeat = null;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
|
|
||||||
this.prometheus = new Prometheus(this);
|
let prometheus = new Prometheus(this);
|
||||||
|
|
||||||
const beat = async () => {
|
const beat = async () => {
|
||||||
|
|
||||||
@@ -343,40 +253,7 @@ class Monitor extends BeanModel {
|
|||||||
if (await Monitor.isUnderMaintenance(this.id)) {
|
if (await Monitor.isUnderMaintenance(this.id)) {
|
||||||
bean.msg = "Monitor under maintenance";
|
bean.msg = "Monitor under maintenance";
|
||||||
bean.status = MAINTENANCE;
|
bean.status = MAINTENANCE;
|
||||||
} else if (this.type === "group") {
|
} else if (this.type === "http" || this.type === "keyword") {
|
||||||
const children = await Monitor.getChildren(this.id);
|
|
||||||
|
|
||||||
if (children.length > 0) {
|
|
||||||
bean.status = UP;
|
|
||||||
bean.msg = "All children up and running";
|
|
||||||
for (const child of children) {
|
|
||||||
if (!child.active) {
|
|
||||||
// Ignore inactive childs
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
|
||||||
|
|
||||||
// Only change state if the monitor is in worse conditions then the ones before
|
|
||||||
// lastBeat.status could be null
|
|
||||||
if (!lastBeat) {
|
|
||||||
bean.status = PENDING;
|
|
||||||
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
|
||||||
bean.status = lastBeat.status;
|
|
||||||
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
|
|
||||||
bean.status = lastBeat.status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bean.status !== UP) {
|
|
||||||
bean.msg = "Child inaccessible";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Set status pending if group is empty
|
|
||||||
bean.status = PENDING;
|
|
||||||
bean.msg = "Group empty";
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
|
|
||||||
// Do not do any queries/high loading things before the "bean.ping"
|
// Do not do any queries/high loading things before the "bean.ping"
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
@@ -388,24 +265,6 @@ class Monitor extends BeanModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDC: Basic client credential flow.
|
|
||||||
// Additional grants might be implemented in the future
|
|
||||||
let oauth2AuthHeader = {};
|
|
||||||
if (this.auth_method === "oauth2-cc") {
|
|
||||||
try {
|
|
||||||
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
|
|
||||||
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new one`);
|
|
||||||
this.oauthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
|
|
||||||
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken.expires_at * 1000)}`);
|
|
||||||
}
|
|
||||||
oauth2AuthHeader = {
|
|
||||||
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error("The oauth config is invalid. " + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpsAgentOptions = {
|
const httpsAgentOptions = {
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
@@ -413,35 +272,17 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||||
|
|
||||||
let contentType = null;
|
|
||||||
let bodyValue = null;
|
|
||||||
|
|
||||||
if (this.body && (typeof this.body === "string" && this.body.trim().length > 0)) {
|
|
||||||
if (!this.httpBodyEncoding || this.httpBodyEncoding === "json") {
|
|
||||||
try {
|
|
||||||
bodyValue = JSON.parse(this.body);
|
|
||||||
contentType = "application/json";
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error("Your JSON body is invalid. " + e.message);
|
|
||||||
}
|
|
||||||
} else if (this.httpBodyEncoding === "xml") {
|
|
||||||
bodyValue = this.body;
|
|
||||||
contentType = "text/xml; charset=utf-8";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios Options
|
// Axios Options
|
||||||
const options = {
|
const options = {
|
||||||
url: this.url,
|
url: this.url,
|
||||||
method: (this.method || "get").toLowerCase(),
|
method: (this.method || "get").toLowerCase(),
|
||||||
timeout: this.timeout * 1000,
|
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||||
|
timeout: this.interval * 1000 * 0.8,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
...(contentType ? { "Content-Type": contentType } : {}),
|
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||||
...(basicAuthHeader),
|
...(basicAuthHeader),
|
||||||
...(oauth2AuthHeader),
|
|
||||||
...(this.headers ? JSON.parse(this.headers) : {})
|
|
||||||
},
|
},
|
||||||
maxRedirects: this.maxredirects,
|
maxRedirects: this.maxredirects,
|
||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
@@ -449,10 +290,6 @@ class Monitor extends BeanModel {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bodyValue) {
|
|
||||||
options.data = bodyValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.proxy_id) {
|
if (this.proxy_id) {
|
||||||
const proxy = await R.load("proxy", this.proxy_id);
|
const proxy = await R.load("proxy", this.proxy_id);
|
||||||
|
|
||||||
@@ -471,18 +308,6 @@ class Monitor extends BeanModel {
|
|||||||
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.auth_method === "mtls") {
|
|
||||||
if (this.tlsCert !== null && this.tlsCert !== "") {
|
|
||||||
options.httpsAgent.options.cert = Buffer.from(this.tlsCert);
|
|
||||||
}
|
|
||||||
if (this.tlsCa !== null && this.tlsCa !== "") {
|
|
||||||
options.httpsAgent.options.ca = Buffer.from(this.tlsCa);
|
|
||||||
}
|
|
||||||
if (this.tlsKey !== null && this.tlsKey !== "") {
|
|
||||||
options.httpsAgent.options.key = Buffer.from(this.tlsKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||||
|
|
||||||
@@ -501,8 +326,8 @@ class Monitor extends BeanModel {
|
|||||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||||
|
|
||||||
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||||
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
|
log.debug("monitor", `[${this.name}] call sendCertNotification`);
|
||||||
await this.checkCertExpiryNotifications(tlsInfoObject);
|
await this.sendCertNotification(tlsInfoObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -523,7 +348,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
if (this.type === "http") {
|
if (this.type === "http") {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else if (this.type === "keyword") {
|
} else {
|
||||||
|
|
||||||
let data = res.data;
|
let data = res.data;
|
||||||
|
|
||||||
@@ -532,37 +357,17 @@ class Monitor extends BeanModel {
|
|||||||
data = JSON.stringify(data);
|
data = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
let keywordFound = data.includes(this.keyword);
|
if (data.includes(this.keyword)) {
|
||||||
if (keywordFound === !this.isInvertKeyword()) {
|
bean.msg += ", keyword is found";
|
||||||
bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
|
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
|
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
|
||||||
if (data.length > 50) {
|
if (data.length > 50) {
|
||||||
data = data.substring(0, 47) + "...";
|
data = data.substring(0, 47) + "...";
|
||||||
}
|
}
|
||||||
throw new Error(bean.msg + ", but keyword is " +
|
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
|
||||||
(keywordFound ? "present" : "not") + " in [" + data + "]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "json-query") {
|
|
||||||
let data = res.data;
|
|
||||||
|
|
||||||
// convert data to object
|
|
||||||
if (typeof data === "string") {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
let expression = jsonata(this.jsonPath);
|
|
||||||
|
|
||||||
let result = await expression.evaluate(data);
|
|
||||||
|
|
||||||
if (result.toString() === this.expectedValue) {
|
|
||||||
bean.msg += ", expected value is found";
|
|
||||||
bean.status = UP;
|
|
||||||
} else {
|
|
||||||
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "port") {
|
} else if (this.type === "port") {
|
||||||
@@ -637,7 +442,7 @@ class Monitor extends BeanModel {
|
|||||||
// No need to insert successful heartbeat for push type, so end here
|
// No need to insert successful heartbeat for push type, so end here
|
||||||
retries = 0;
|
retries = 0;
|
||||||
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
||||||
this.heartbeatInterval = setTimeout(safeBeat, timeout);
|
this.heartbeatInterval = setTimeout(beat, timeout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -654,7 +459,7 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let res = await axios.get(steamApiUrl, {
|
let res = await axios.get(steamApiUrl, {
|
||||||
timeout: this.timeout * 1000,
|
timeout: this.interval * 1000 * 0.8,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
@@ -692,7 +497,7 @@ class Monitor extends BeanModel {
|
|||||||
type: this.game,
|
type: this.game,
|
||||||
host: this.hostname,
|
host: this.hostname,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
givenPortOnly: this.getGameDigGivenPortOnly(),
|
givenPortOnly: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
bean.msg = state.name;
|
bean.msg = state.name;
|
||||||
@@ -726,22 +531,13 @@ class Monitor extends BeanModel {
|
|||||||
options.socketPath = dockerHost._dockerDaemon;
|
options.socketPath = dockerHost._dockerDaemon;
|
||||||
} else if (dockerHost._dockerType === "tcp") {
|
} else if (dockerHost._dockerType === "tcp") {
|
||||||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||||
options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent(
|
|
||||||
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||||
let res = await axios.request(options);
|
let res = await axios.request(options);
|
||||||
|
|
||||||
if (res.data.State.Running) {
|
if (res.data.State.Running) {
|
||||||
if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
|
|
||||||
bean.status = PENDING;
|
|
||||||
bean.msg = res.data.State.Health.Status;
|
|
||||||
} else {
|
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status;
|
bean.msg = res.data.State.Status;
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw Error("Container State is " + res.data.State.Status);
|
throw Error("Container State is " + res.data.State.Status);
|
||||||
}
|
}
|
||||||
@@ -770,6 +566,7 @@ class Monitor extends BeanModel {
|
|||||||
grpcEnableTls: this.grpcEnableTls,
|
grpcEnableTls: this.grpcEnableTls,
|
||||||
grpcMethod: this.grpcMethod,
|
grpcMethod: this.grpcMethod,
|
||||||
grpcBody: this.grpcBody,
|
grpcBody: this.grpcBody,
|
||||||
|
keyword: this.keyword
|
||||||
};
|
};
|
||||||
const response = await grpcQuery(options);
|
const response = await grpcQuery(options);
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
@@ -782,14 +579,13 @@ class Monitor extends BeanModel {
|
|||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
||||||
} else {
|
} else {
|
||||||
let keywordFound = response.data.toString().includes(this.keyword);
|
if (response.data.toString().includes(this.keyword)) {
|
||||||
if (keywordFound === !this.isInvertKeyword()) {
|
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
|
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
|
||||||
} else {
|
} else {
|
||||||
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`);
|
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`;
|
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.type === "postgres") {
|
} else if (this.type === "postgres") {
|
||||||
@@ -803,7 +599,9 @@ class Monitor extends BeanModel {
|
|||||||
} else if (this.type === "mysql") {
|
} else if (this.type === "mysql") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||||
|
|
||||||
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
} else if (this.type === "mongodb") {
|
} else if (this.type === "mongodb") {
|
||||||
@@ -828,6 +626,7 @@ class Monitor extends BeanModel {
|
|||||||
port = this.port;
|
port = this.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const resp = await radius(
|
const resp = await radius(
|
||||||
this.hostname,
|
this.hostname,
|
||||||
this.radiusUsername,
|
this.radiusUsername,
|
||||||
@@ -835,12 +634,20 @@ class Monitor extends BeanModel {
|
|||||||
this.radiusCalledStationId,
|
this.radiusCalledStationId,
|
||||||
this.radiusCallingStationId,
|
this.radiusCallingStationId,
|
||||||
this.radiusSecret,
|
this.radiusSecret,
|
||||||
port,
|
port
|
||||||
this.interval * 1000 * 0.4,
|
|
||||||
);
|
);
|
||||||
|
if (resp.code) {
|
||||||
bean.msg = resp.code;
|
bean.msg = resp.code;
|
||||||
|
}
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
} catch (error) {
|
||||||
|
bean.status = DOWN;
|
||||||
|
if (error.response?.code) {
|
||||||
|
bean.msg = error.response.code;
|
||||||
|
} else {
|
||||||
|
bean.msg = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
} else if (this.type === "redis") {
|
} else if (this.type === "redis") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
@@ -852,29 +659,11 @@ class Monitor extends BeanModel {
|
|||||||
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
||||||
await monitorType.check(this, bean, UptimeKumaServer.getInstance());
|
await monitorType.check(this, bean);
|
||||||
if (!bean.ping) {
|
if (!bean.ping) {
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "kafka-producer") {
|
|
||||||
let startTime = dayjs().valueOf();
|
|
||||||
|
|
||||||
bean.msg = await kafkaProducerAsync(
|
|
||||||
JSON.parse(this.kafkaProducerBrokers),
|
|
||||||
this.kafkaProducerTopic,
|
|
||||||
this.kafkaProducerMessage,
|
|
||||||
{
|
|
||||||
allowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation,
|
|
||||||
ssl: this.kafkaProducerSsl,
|
|
||||||
clientId: `Uptime-Kuma/${version}`,
|
|
||||||
interval: this.interval,
|
|
||||||
},
|
|
||||||
JSON.parse(this.kafkaProducerSaslOptions),
|
|
||||||
);
|
|
||||||
bean.status = UP;
|
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown Monitor Type");
|
throw new Error("Unknown Monitor Type");
|
||||||
}
|
}
|
||||||
@@ -966,7 +755,7 @@ class Monitor extends BeanModel {
|
|||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] prometheus.update`);
|
log.debug("monitor", `[${this.name}] prometheus.update`);
|
||||||
this.prometheus?.update(bean, tlsInfo);
|
prometheus.update(bean, tlsInfo);
|
||||||
|
|
||||||
previousBeat = bean;
|
previousBeat = bean;
|
||||||
|
|
||||||
@@ -1024,6 +813,7 @@ class Monitor extends BeanModel {
|
|||||||
domain: this.authDomain,
|
domain: this.authDomain,
|
||||||
workstation: this.authWorkstation ? this.authWorkstation : undefined
|
workstation: this.authWorkstation ? this.authWorkstation : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
res = await axios.request(options);
|
res = await axios.request(options);
|
||||||
}
|
}
|
||||||
@@ -1050,15 +840,15 @@ class Monitor extends BeanModel {
|
|||||||
clearTimeout(this.heartbeatInterval);
|
clearTimeout(this.heartbeatInterval);
|
||||||
this.isStop = true;
|
this.isStop = true;
|
||||||
|
|
||||||
this.prometheus?.remove();
|
this.prometheus().remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get prometheus instance
|
* Get a new prometheus instance
|
||||||
* @returns {Prometheus|undefined}
|
* @returns {Prometheus}
|
||||||
*/
|
*/
|
||||||
getPrometheus() {
|
prometheus() {
|
||||||
return this.prometheus;
|
return new Prometheus(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1352,18 +1142,12 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
for (let notification of notificationList) {
|
for (let notification of notificationList) {
|
||||||
try {
|
try {
|
||||||
const heartbeatJSON = bean.toJSON();
|
|
||||||
|
|
||||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||||
|
const heartbeatJSON = bean.toJSON();
|
||||||
if (!heartbeatJSON["msg"]) {
|
if (!heartbeatJSON["msg"]) {
|
||||||
heartbeatJSON["msg"] = "N/A";
|
heartbeatJSON["msg"] = "N/A";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also provide the time in server timezone
|
|
||||||
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
|
|
||||||
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
|
|
||||||
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
|
|
||||||
|
|
||||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
|
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("monitor", "Cannot send notification to " + notification.name);
|
log.error("monitor", "Cannot send notification to " + notification.name);
|
||||||
@@ -1386,19 +1170,13 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* checks certificate chain for expiring certificates
|
* Send notification about a certificate
|
||||||
* @param {Object} tlsInfoObject Information about certificate
|
* @param {Object} tlsInfoObject Information about certificate
|
||||||
*/
|
*/
|
||||||
async checkCertExpiryNotifications(tlsInfoObject) {
|
async sendCertNotification(tlsInfoObject) {
|
||||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||||
const notificationList = await Monitor.getNotificationList(this);
|
const notificationList = await Monitor.getNotificationList(this);
|
||||||
|
|
||||||
if (! notificationList.length > 0) {
|
|
||||||
// fail fast. If no notification is set, all the following checks can be skipped.
|
|
||||||
log.debug("monitor", "No notification, no need to send cert notification");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let notifyDays = await setting("tlsExpiryNotifyDays");
|
let notifyDays = await setting("tlsExpiryNotifyDays");
|
||||||
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
||||||
// Reset Default
|
// Reset Default
|
||||||
@@ -1406,19 +1184,10 @@ class Monitor extends BeanModel {
|
|||||||
notifyDays = [ 7, 14, 21 ];
|
notifyDays = [ 7, 14, 21 ];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(notifyDays)) {
|
if (notifyDays != null && Array.isArray(notifyDays)) {
|
||||||
for (const targetDays of notifyDays) {
|
for (const day of notifyDays) {
|
||||||
let certInfo = tlsInfoObject.certInfo;
|
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
|
||||||
while (certInfo) {
|
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
|
||||||
let subjectCN = certInfo.subject["CN"];
|
|
||||||
if (certInfo.daysRemaining > targetDays) {
|
|
||||||
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
|
||||||
} else {
|
|
||||||
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
|
|
||||||
await this.sendCertNotificationByTargetDays(subjectCN, certInfo.certType, certInfo.daysRemaining, targetDays, notificationList);
|
|
||||||
}
|
|
||||||
certInfo = certInfo.issuerCertificate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1427,16 +1196,21 @@ class Monitor extends BeanModel {
|
|||||||
/**
|
/**
|
||||||
* Send a certificate notification when certificate expires in less
|
* Send a certificate notification when certificate expires in less
|
||||||
* than target days
|
* than target days
|
||||||
* @param {string} certCN Common Name attribute from the certificate subject
|
* @param {number} daysRemaining Number of days remaining on certifcate
|
||||||
* @param {string} certType certificate type
|
|
||||||
* @param {number} daysRemaining Number of days remaining on certificate
|
|
||||||
* @param {number} targetDays Number of days to alert after
|
* @param {number} targetDays Number of days to alert after
|
||||||
* @param {LooseObject<any>[]} notificationList List of notification providers
|
* @param {LooseObject<any>[]} notificationList List of notification providers
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async sendCertNotificationByTargetDays(certCN, certType, daysRemaining, targetDays, notificationList) {
|
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
||||||
|
|
||||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
|
if (daysRemaining > targetDays) {
|
||||||
|
log.debug("monitor", `No need to send cert notification. ${daysRemaining} > ${targetDays}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationList.length > 0) {
|
||||||
|
|
||||||
|
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
|
||||||
"certificate",
|
"certificate",
|
||||||
this.id,
|
this.id,
|
||||||
targetDays,
|
targetDays,
|
||||||
@@ -1454,7 +1228,7 @@ class Monitor extends BeanModel {
|
|||||||
for (let notification of notificationList) {
|
for (let notification of notificationList) {
|
||||||
try {
|
try {
|
||||||
log.debug("monitor", "Sending to " + notification.name);
|
log.debug("monitor", "Sending to " + notification.name);
|
||||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will be expired in ${daysRemaining} days`);
|
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will be expired in ${daysRemaining} days`);
|
||||||
sent = true;
|
sent = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("monitor", "Cannot send cert notification to " + notification.name);
|
log.error("monitor", "Cannot send cert notification to " + notification.name);
|
||||||
@@ -1469,6 +1243,9 @@ class Monitor extends BeanModel {
|
|||||||
targetDays,
|
targetDays,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("monitor", "No notification, no need to send cert notification");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1491,24 +1268,18 @@ class Monitor extends BeanModel {
|
|||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
static async isUnderMaintenance(monitorID) {
|
static async isUnderMaintenance(monitorID) {
|
||||||
const maintenanceIDList = await R.getCol(`
|
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||||
SELECT maintenance_id FROM monitor_maintenance
|
const maintenance = await R.getRow(`
|
||||||
WHERE monitor_id = ?
|
SELECT COUNT(*) AS count
|
||||||
`, [ monitorID ]);
|
FROM monitor_maintenance mm
|
||||||
|
JOIN maintenance
|
||||||
for (const maintenanceID of maintenanceIDList) {
|
ON mm.maintenance_id = maintenance.id
|
||||||
const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
AND mm.monitor_id = ?
|
||||||
if (maintenance && await maintenance.isUnderMaintenance()) {
|
LEFT JOIN maintenance_timeslot
|
||||||
return true;
|
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||||
}
|
WHERE ${activeCondition}
|
||||||
}
|
LIMIT 1`, [ monitorID ]);
|
||||||
|
return maintenance.count !== 0;
|
||||||
const parent = await Monitor.getParent(monitorID);
|
|
||||||
if (parent != null) {
|
|
||||||
return await Monitor.isUnderMaintenance(parent.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Make sure monitor interval is between bounds */
|
/** Make sure monitor interval is between bounds */
|
||||||
@@ -1520,105 +1291,6 @@ class Monitor extends BeanModel {
|
|||||||
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets Parent of the monitor
|
|
||||||
* @param {number} monitorID ID of monitor to get
|
|
||||||
* @returns {Promise<LooseObject<any>>}
|
|
||||||
*/
|
|
||||||
static async getParent(monitorID) {
|
|
||||||
return await R.getRow(`
|
|
||||||
SELECT parent.* FROM monitor parent
|
|
||||||
LEFT JOIN monitor child
|
|
||||||
ON child.parent = parent.id
|
|
||||||
WHERE child.id = ?
|
|
||||||
`, [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all Children of the monitor
|
|
||||||
* @param {number} monitorID ID of monitor to get
|
|
||||||
* @returns {Promise<LooseObject<any>>}
|
|
||||||
*/
|
|
||||||
static async getChildren(monitorID) {
|
|
||||||
return await R.getAll(`
|
|
||||||
SELECT * FROM monitor
|
|
||||||
WHERE parent = ?
|
|
||||||
`, [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets Full Path-Name (Groups and Name)
|
|
||||||
* @returns {Promise<String>}
|
|
||||||
*/
|
|
||||||
async getPathName() {
|
|
||||||
let path = this.name;
|
|
||||||
|
|
||||||
if (this.parent === null) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parent = await Monitor.getParent(this.id);
|
|
||||||
while (parent !== null) {
|
|
||||||
path = `${parent.name} / ${path}`;
|
|
||||||
parent = await Monitor.getParent(parent.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets recursive all child ids
|
|
||||||
* @param {number} monitorID ID of the monitor to get
|
|
||||||
* @returns {Promise<Array>}
|
|
||||||
*/
|
|
||||||
static async getAllChildrenIDs(monitorID) {
|
|
||||||
const childs = await Monitor.getChildren(monitorID);
|
|
||||||
|
|
||||||
if (childs === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let childrenIDs = [];
|
|
||||||
|
|
||||||
for (const child of childs) {
|
|
||||||
childrenIDs.push(child.id);
|
|
||||||
childrenIDs = childrenIDs.concat(await Monitor.getAllChildrenIDs(child.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return childrenIDs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unlinks all children of the the group monitor
|
|
||||||
* @param {number} groupID ID of group to remove children of
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
static async unlinkAllChildren(groupID) {
|
|
||||||
return await R.exec("UPDATE `monitor` SET parent = ? WHERE parent = ? ", [
|
|
||||||
null, groupID
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks recursive if parent (ancestors) are active
|
|
||||||
* @param {number} monitorID ID of the monitor to get
|
|
||||||
* @returns {Promise<Boolean>}
|
|
||||||
*/
|
|
||||||
static async isParentActive(monitorID) {
|
|
||||||
const parent = await Monitor.getParent(monitorID);
|
|
||||||
|
|
||||||
if (parent === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentActive = await Monitor.isParentActive(parent.id);
|
|
||||||
return parent.active && parentActive;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Monitor;
|
module.exports = Monitor;
|
||||||
|
@@ -3,6 +3,7 @@ const { R } = require("redbean-node");
|
|||||||
const cheerio = require("cheerio");
|
const cheerio = require("cheerio");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const jsesc = require("jsesc");
|
const jsesc = require("jsesc");
|
||||||
|
const Maintenance = require("./maintenance");
|
||||||
const googleAnalytics = require("../google-analytics");
|
const googleAnalytics = require("../google-analytics");
|
||||||
|
|
||||||
class StatusPage extends BeanModel {
|
class StatusPage extends BeanModel {
|
||||||
@@ -59,11 +60,8 @@ class StatusPage extends BeanModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OG Meta Tags
|
// OG Meta Tags
|
||||||
let ogTitle = $("<meta property=\"og:title\" content=\"\" />").attr("content", statusPage.title);
|
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
|
||||||
head.append(ogTitle);
|
head.append(`<meta property="og:description" content="${description155}" />`);
|
||||||
|
|
||||||
let ogDescription = $("<meta property=\"og:description\" content=\"\" />").attr("content", description155);
|
|
||||||
head.append(ogDescription);
|
|
||||||
|
|
||||||
// Preload data
|
// Preload data
|
||||||
// Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
|
// Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
|
||||||
@@ -90,8 +88,6 @@ class StatusPage extends BeanModel {
|
|||||||
* @param {StatusPage} statusPage
|
* @param {StatusPage} statusPage
|
||||||
*/
|
*/
|
||||||
static async getStatusPageData(statusPage) {
|
static async getStatusPageData(statusPage) {
|
||||||
const config = await statusPage.toPublicJSON();
|
|
||||||
|
|
||||||
// Incident
|
// Incident
|
||||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||||
statusPage.id,
|
statusPage.id,
|
||||||
@@ -112,13 +108,13 @@ class StatusPage extends BeanModel {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
for (let groupBean of list) {
|
for (let groupBean of list) {
|
||||||
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
|
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
||||||
publicGroupList.push(monitorGroup);
|
publicGroupList.push(monitorGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response
|
// Response
|
||||||
return {
|
return {
|
||||||
config,
|
config: await statusPage.toPublicJSON(),
|
||||||
incident,
|
incident,
|
||||||
publicGroupList,
|
publicGroupList,
|
||||||
maintenanceList,
|
maintenanceList,
|
||||||
@@ -236,7 +232,6 @@ class StatusPage extends BeanModel {
|
|||||||
footerText: this.footer_text,
|
footerText: this.footer_text,
|
||||||
showPoweredBy: !!this.show_powered_by,
|
showPoweredBy: !!this.show_powered_by,
|
||||||
googleAnalyticsId: this.google_analytics_tag_id,
|
googleAnalyticsId: this.google_analytics_tag_id,
|
||||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +253,6 @@ class StatusPage extends BeanModel {
|
|||||||
footerText: this.footer_text,
|
footerText: this.footer_text,
|
||||||
showPoweredBy: !!this.show_powered_by,
|
showPoweredBy: !!this.show_powered_by,
|
||||||
googleAnalyticsId: this.google_analytics_tag_id,
|
googleAnalyticsId: this.google_analytics_tag_id,
|
||||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,17 +287,21 @@ class StatusPage extends BeanModel {
|
|||||||
try {
|
try {
|
||||||
const publicMaintenanceList = [];
|
const publicMaintenanceList = [];
|
||||||
|
|
||||||
let maintenanceIDList = await R.getCol(`
|
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||||
SELECT DISTINCT maintenance_id
|
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
|
||||||
FROM maintenance_status_page
|
SELECT DISTINCT maintenance.*
|
||||||
WHERE status_page_id = ?
|
FROM maintenance
|
||||||
`, [ statusPageId ]);
|
JOIN maintenance_status_page
|
||||||
|
ON maintenance_status_page.maintenance_id = maintenance.id
|
||||||
|
AND maintenance_status_page.status_page_id = ?
|
||||||
|
LEFT JOIN maintenance_timeslot
|
||||||
|
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||||
|
WHERE ${activeCondition}
|
||||||
|
ORDER BY maintenance.end_date
|
||||||
|
`, [ statusPageId ]));
|
||||||
|
|
||||||
for (const maintenanceID of maintenanceIDList) {
|
for (const bean of maintenanceBeanList) {
|
||||||
let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
publicMaintenanceList.push(await bean.toPublicJSON());
|
||||||
if (maintenance && await maintenance.isUnderMaintenance()) {
|
|
||||||
publicMaintenanceList.push(await maintenance.toPublicJSON());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return publicMaintenanceList;
|
return publicMaintenanceList;
|
||||||
|
@@ -6,10 +6,9 @@ class MonitorType {
|
|||||||
*
|
*
|
||||||
* @param {Monitor} monitor
|
* @param {Monitor} monitor
|
||||||
* @param {Heartbeat} heartbeat
|
* @param {Heartbeat} heartbeat
|
||||||
* @param {UptimeKumaServer} server
|
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async check(monitor, heartbeat, server) {
|
async check(monitor, heartbeat) {
|
||||||
throw new Error("You need to override check()");
|
throw new Error("You need to override check()");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,212 +0,0 @@
|
|||||||
const { MonitorType } = require("./monitor-type");
|
|
||||||
const { chromium } = require("playwright-core");
|
|
||||||
const { UP, log } = require("../../src/util");
|
|
||||||
const { Settings } = require("../settings");
|
|
||||||
const commandExistsSync = require("command-exists").sync;
|
|
||||||
const childProcess = require("child_process");
|
|
||||||
const path = require("path");
|
|
||||||
const Database = require("../database");
|
|
||||||
const jwt = require("jsonwebtoken");
|
|
||||||
const config = require("../config");
|
|
||||||
|
|
||||||
let browser = null;
|
|
||||||
|
|
||||||
let allowedList = [];
|
|
||||||
let lastAutoDetectChromeExecutable = null;
|
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
allowedList.push(process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe");
|
|
||||||
allowedList.push(process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe");
|
|
||||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Google\\Chrome\\Application\\chrome.exe");
|
|
||||||
|
|
||||||
// Allow Chromium too
|
|
||||||
allowedList.push(process.env.LOCALAPPDATA + "\\Chromium\\Application\\chrome.exe");
|
|
||||||
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
|
|
||||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
|
|
||||||
|
|
||||||
// For Loop A to Z
|
|
||||||
for (let i = 65; i <= 90; i++) {
|
|
||||||
let drive = String.fromCharCode(i);
|
|
||||||
allowedList.push(drive + ":\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
|
|
||||||
allowedList.push(drive + ":\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (process.platform === "linux") {
|
|
||||||
allowedList = [
|
|
||||||
"chromium",
|
|
||||||
"chromium-browser",
|
|
||||||
"google-chrome",
|
|
||||||
|
|
||||||
"/usr/bin/chromium",
|
|
||||||
"/usr/bin/chromium-browser",
|
|
||||||
"/usr/bin/google-chrome",
|
|
||||||
];
|
|
||||||
} else if (process.platform === "darwin") {
|
|
||||||
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
|
||||||
allowedList = [
|
|
||||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
||||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("chrome", allowedList);
|
|
||||||
|
|
||||||
async function isAllowedChromeExecutable(executablePath) {
|
|
||||||
console.log(config.args);
|
|
||||||
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the executablePath is in the list of allowed executables
|
|
||||||
return allowedList.includes(executablePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getBrowser() {
|
|
||||||
if (!browser) {
|
|
||||||
let executablePath = await Settings.get("chromeExecutable");
|
|
||||||
|
|
||||||
executablePath = await prepareChromeExecutable(executablePath);
|
|
||||||
|
|
||||||
browser = await chromium.launch({
|
|
||||||
//headless: false,
|
|
||||||
executablePath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return browser;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function prepareChromeExecutable(executablePath) {
|
|
||||||
// Special code for using the playwright_chromium
|
|
||||||
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
|
|
||||||
// Set to undefined = use playwright_chromium
|
|
||||||
executablePath = undefined;
|
|
||||||
} else if (!executablePath) {
|
|
||||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
|
||||||
executablePath = "/usr/bin/chromium";
|
|
||||||
|
|
||||||
// Install chromium in container via apt install
|
|
||||||
if ( !commandExistsSync(executablePath)) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
log.info("Chromium", "Installing Chromium...");
|
|
||||||
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
|
|
||||||
|
|
||||||
// On exit
|
|
||||||
child.on("exit", (code) => {
|
|
||||||
log.info("Chromium", "apt install chromium exited with code " + code);
|
|
||||||
|
|
||||||
if (code === 0) {
|
|
||||||
log.info("Chromium", "Installed Chromium");
|
|
||||||
let version = childProcess.execSync(executablePath + " --version").toString("utf8");
|
|
||||||
log.info("Chromium", "Chromium version: " + version);
|
|
||||||
resolve();
|
|
||||||
} else if (code === 100) {
|
|
||||||
reject(new Error("Installing Chromium, please wait..."));
|
|
||||||
} else {
|
|
||||||
reject(new Error("apt install chromium failed with code " + code));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
executablePath = findChrome(allowedList);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// User specified a path
|
|
||||||
// Check if the executablePath is in the list of allowed
|
|
||||||
if (!await isAllowedChromeExecutable(executablePath)) {
|
|
||||||
throw new Error("This Chromium executable path is not allowed by default. If you are sure this is safe, please add an environment variable UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC=1 to allow it.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return executablePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findChrome(executables) {
|
|
||||||
// Use the last working executable, so we don't have to search for it again
|
|
||||||
if (lastAutoDetectChromeExecutable) {
|
|
||||||
if (commandExistsSync(lastAutoDetectChromeExecutable)) {
|
|
||||||
return lastAutoDetectChromeExecutable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let executable of executables) {
|
|
||||||
if (commandExistsSync(executable)) {
|
|
||||||
lastAutoDetectChromeExecutable = executable;
|
|
||||||
return executable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetChrome() {
|
|
||||||
if (browser) {
|
|
||||||
await browser.close();
|
|
||||||
browser = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if the chrome executable is valid and return the version
|
|
||||||
* @param executablePath
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
async function testChrome(executablePath) {
|
|
||||||
try {
|
|
||||||
executablePath = await prepareChromeExecutable(executablePath);
|
|
||||||
|
|
||||||
log.info("Chromium", "Testing Chromium executable: " + executablePath);
|
|
||||||
|
|
||||||
const browser = await chromium.launch({
|
|
||||||
executablePath,
|
|
||||||
});
|
|
||||||
const version = browser.version();
|
|
||||||
await browser.close();
|
|
||||||
return version;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class RealBrowserMonitorType extends MonitorType {
|
|
||||||
|
|
||||||
name = "real-browser";
|
|
||||||
|
|
||||||
async check(monitor, heartbeat, server) {
|
|
||||||
const browser = await getBrowser();
|
|
||||||
const context = await browser.newContext();
|
|
||||||
const page = await context.newPage();
|
|
||||||
|
|
||||||
const res = await page.goto(monitor.url, {
|
|
||||||
waitUntil: "networkidle",
|
|
||||||
timeout: monitor.interval * 1000 * 0.8,
|
|
||||||
});
|
|
||||||
|
|
||||||
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
|
|
||||||
|
|
||||||
await page.screenshot({
|
|
||||||
path: path.join(Database.screenshotDir, filename),
|
|
||||||
});
|
|
||||||
|
|
||||||
await context.close();
|
|
||||||
|
|
||||||
if (res.status() >= 200 && res.status() < 400) {
|
|
||||||
heartbeat.status = UP;
|
|
||||||
heartbeat.msg = res.status();
|
|
||||||
|
|
||||||
const timing = res.request().timing();
|
|
||||||
heartbeat.ping = timing.responseEnd;
|
|
||||||
} else {
|
|
||||||
throw new Error(res.status() + "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
RealBrowserMonitorType,
|
|
||||||
testChrome,
|
|
||||||
resetChrome,
|
|
||||||
};
|
|
@@ -1,95 +0,0 @@
|
|||||||
const { MonitorType } = require("./monitor-type");
|
|
||||||
const { UP, log } = require("../../src/util");
|
|
||||||
const exec = require("child_process").exec;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A TailscalePing class extends the MonitorType.
|
|
||||||
* It runs Tailscale ping to monitor the status of a specific node.
|
|
||||||
*/
|
|
||||||
class TailscalePing extends MonitorType {
|
|
||||||
|
|
||||||
name = "tailscale-ping";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the ping status of the URL associated with the monitor.
|
|
||||||
* It then parses the Tailscale ping command output to update the heatrbeat.
|
|
||||||
*
|
|
||||||
* @param {Object} monitor - The monitor object associated with the check.
|
|
||||||
* @param {Object} heartbeat - The heartbeat object to update.
|
|
||||||
* @throws Will throw an error if checking Tailscale ping encounters any error
|
|
||||||
*/
|
|
||||||
async check(monitor, heartbeat) {
|
|
||||||
try {
|
|
||||||
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
|
||||||
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
|
||||||
} catch (err) {
|
|
||||||
log.debug("Tailscale", err);
|
|
||||||
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
|
||||||
throw new Error(`Error checking Tailscale ping: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the Tailscale ping command to the given URL.
|
|
||||||
*
|
|
||||||
* @param {string} hostname - The hostname to ping.
|
|
||||||
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
|
|
||||||
* @throws Will throw an error if the command execution encounters any error.
|
|
||||||
*/
|
|
||||||
async runTailscalePing(hostname, interval) {
|
|
||||||
let cmd = `tailscale ping ${hostname}`;
|
|
||||||
|
|
||||||
log.debug("Tailscale", cmd);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let timeout = interval * 1000 * 0.8;
|
|
||||||
exec(cmd, { timeout: timeout }, (error, stdout, stderr) => {
|
|
||||||
// we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues)
|
|
||||||
if (error) {
|
|
||||||
reject(`Execution error: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (stderr) {
|
|
||||||
reject(`Error in output: ${stderr}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(stdout);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the output of the Tailscale ping command to update the heartbeat.
|
|
||||||
*
|
|
||||||
* @param {string} tailscaleOutput - The output of the Tailscale ping command.
|
|
||||||
* @param {Object} heartbeat - The heartbeat object to update.
|
|
||||||
* @throws Will throw an eror if the output contains any unexpected string.
|
|
||||||
*/
|
|
||||||
parseTailscaleOutput(tailscaleOutput, heartbeat) {
|
|
||||||
let lines = tailscaleOutput.split("\n");
|
|
||||||
|
|
||||||
for (let line of lines) {
|
|
||||||
if (line.includes("pong from")) {
|
|
||||||
heartbeat.status = UP;
|
|
||||||
let time = line.split(" in ")[1].split(" ")[0];
|
|
||||||
heartbeat.ping = parseInt(time);
|
|
||||||
heartbeat.msg = line;
|
|
||||||
break;
|
|
||||||
} else if (line.includes("timed out")) {
|
|
||||||
throw new Error(`Ping timed out: "${line}"`);
|
|
||||||
// Immediately throws upon "timed out" message, the server is expected to re-call the check function
|
|
||||||
} else if (line.includes("no matching peer")) {
|
|
||||||
throw new Error(`Nonexistant or inaccessible due to ACLs: "${line}"`);
|
|
||||||
} else if (line.includes("is local Tailscale IP")) {
|
|
||||||
throw new Error(`Tailscale only works if used on other machines: "${line}"`);
|
|
||||||
} else if (line !== "") {
|
|
||||||
throw new Error(`Unexpected output: "${line}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
TailscalePing,
|
|
||||||
};
|
|
@@ -15,7 +15,7 @@ class DingDing extends NotificationProvider {
|
|||||||
msgtype: "markdown",
|
msgtype: "markdown",
|
||||||
markdown: {
|
markdown: {
|
||||||
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (this.sendToDingDing(notification, params)) {
|
if (this.sendToDingDing(notification, params)) {
|
||||||
|
@@ -59,8 +59,8 @@ class Discord extends NotificationProvider {
|
|||||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
name: "Time (UTC)",
|
||||||
value: heartbeatJSON["localDateTime"],
|
value: heartbeatJSON["time"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Error",
|
name: "Error",
|
||||||
@@ -94,8 +94,8 @@ class Discord extends NotificationProvider {
|
|||||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
name: "Time (UTC)",
|
||||||
value: heartbeatJSON["localDateTime"],
|
value: heartbeatJSON["time"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ping",
|
name: "Ping",
|
||||||
|
@@ -35,7 +35,8 @@ class Feishu extends NotificationProvider {
|
|||||||
text:
|
text:
|
||||||
"[Down] " +
|
"[Down] " +
|
||||||
heartbeatJSON["msg"] +
|
heartbeatJSON["msg"] +
|
||||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
"\nTime (UTC): " +
|
||||||
|
heartbeatJSON["time"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -61,7 +62,8 @@ class Feishu extends NotificationProvider {
|
|||||||
text:
|
text:
|
||||||
"[Up] " +
|
"[Up] " +
|
||||||
heartbeatJSON["msg"] +
|
heartbeatJSON["msg"] +
|
||||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
"\nTime (UTC): " +
|
||||||
|
heartbeatJSON["time"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@@ -1,98 +0,0 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
|
||||||
const axios = require("axios");
|
|
||||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
|
||||||
const { setting } = require("../util-server");
|
|
||||||
const successMessage = "Sent Successfully.";
|
|
||||||
|
|
||||||
class FlashDuty extends NotificationProvider {
|
|
||||||
name = "FlashDuty";
|
|
||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
|
||||||
try {
|
|
||||||
if (heartbeatJSON == null) {
|
|
||||||
const title = "Uptime Kuma Alert";
|
|
||||||
const monitor = {
|
|
||||||
type: "ping",
|
|
||||||
url: msg,
|
|
||||||
name: "https://flashcat.cloud"
|
|
||||||
};
|
|
||||||
return this.postNotification(notification, title, msg, monitor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heartbeatJSON.status === UP) {
|
|
||||||
const title = "Uptime Kuma Monitor ✅ Up";
|
|
||||||
|
|
||||||
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "Ok");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heartbeatJSON.status === DOWN) {
|
|
||||||
const title = "Uptime Kuma Monitor 🔴 Down";
|
|
||||||
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, notification.flashdutySeverity);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.throwGeneralAxiosError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Generate a monitor url from the monitors infomation
|
|
||||||
* @param {Object} monitorInfo Monitor details
|
|
||||||
* @returns {string|undefined}
|
|
||||||
*/
|
|
||||||
|
|
||||||
genMonitorUrl(monitorInfo) {
|
|
||||||
if (monitorInfo.type === "port" && monitorInfo.port) {
|
|
||||||
return monitorInfo.hostname + ":" + monitorInfo.port;
|
|
||||||
}
|
|
||||||
if (monitorInfo.hostname != null) {
|
|
||||||
return monitorInfo.hostname;
|
|
||||||
}
|
|
||||||
return monitorInfo.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the message
|
|
||||||
* @param {BeanModel} notification Message title
|
|
||||||
* @param {string} title Message
|
|
||||||
* @param {string} body Message
|
|
||||||
* @param {Object} monitorInfo Monitor details
|
|
||||||
* @param {string} eventStatus Monitor status (Info, Warning, Critical, Ok)
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
async postNotification(notification, title, body, monitorInfo, eventStatus) {
|
|
||||||
const options = {
|
|
||||||
method: "POST",
|
|
||||||
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
data: {
|
|
||||||
description: `[${title}] [${monitorInfo.name}] ${body}`,
|
|
||||||
title,
|
|
||||||
event_status: eventStatus || "Info",
|
|
||||||
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
|
|
||||||
labels: monitorInfo?.tags?.reduce((acc, item) => ({ ...acc,
|
|
||||||
[item.name]: item.value
|
|
||||||
}), { resource: this.genMonitorUrl(monitorInfo) }),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
|
||||||
if (baseURL && monitorInfo) {
|
|
||||||
options.client = "Uptime Kuma";
|
|
||||||
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = await axios.request(options);
|
|
||||||
if (result.status == null) {
|
|
||||||
throw new Error("FlashDuty notification failed with invalid response!");
|
|
||||||
}
|
|
||||||
if (result.status < 200 || result.status >= 300) {
|
|
||||||
throw new Error("FlashDuty notification failed with status code " + result.status);
|
|
||||||
}
|
|
||||||
if (result.statusText != null) {
|
|
||||||
return "FlashDuty notification succeed: " + result.statusText;
|
|
||||||
}
|
|
||||||
|
|
||||||
return successMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = FlashDuty;
|
|
@@ -11,7 +11,7 @@ class HomeAssistant extends NotificationProvider {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
|
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
|
||||||
{
|
{
|
||||||
title: "Uptime Kuma",
|
title: "Uptime Kuma",
|
||||||
message,
|
message,
|
||||||
|
@@ -33,10 +33,7 @@ class Line extends NotificationProvider {
|
|||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "UptimeKuma Alert: [🔴 Down]\n" +
|
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
"Name: " + monitorJSON["name"] + " \n" +
|
|
||||||
heartbeatJSON["msg"] +
|
|
||||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -47,10 +44,7 @@ class Line extends NotificationProvider {
|
|||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "UptimeKuma Alert: [✅ Up]\n" +
|
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
"Name: " + monitorJSON["name"] + " \n" +
|
|
||||||
heartbeatJSON["msg"] +
|
|
||||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@@ -24,18 +24,12 @@ class LineNotify extends NotificationProvider {
|
|||||||
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
||||||
} else if (heartbeatJSON["status"] === DOWN) {
|
} else if (heartbeatJSON["status"] === DOWN) {
|
||||||
let downMessage = {
|
let downMessage = {
|
||||||
"message": "\n[🔴 Down]\n" +
|
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
"Name: " + monitorJSON["name"] + " \n" +
|
|
||||||
heartbeatJSON["msg"] + "\n" +
|
|
||||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
|
||||||
};
|
};
|
||||||
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
||||||
} else if (heartbeatJSON["status"] === UP) {
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
let upMessage = {
|
let upMessage = {
|
||||||
"message": "\n[✅ Up]\n" +
|
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||||
"Name: " + monitorJSON["name"] + " \n" +
|
|
||||||
heartbeatJSON["msg"] + "\n" +
|
|
||||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
|
||||||
};
|
};
|
||||||
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
||||||
}
|
}
|
||||||
|
@@ -8,12 +8,7 @@ class LunaSea extends NotificationProvider {
|
|||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
let lunaseaurl = "";
|
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice;
|
||||||
if (notification.lunaseaTarget === "user") {
|
|
||||||
lunaseaurl = "https://notify.lunasea.app/v1/custom/user/" + notification.lunaseaUserID;
|
|
||||||
} else {
|
|
||||||
lunaseaurl = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
@@ -21,29 +16,25 @@ class LunaSea extends NotificationProvider {
|
|||||||
"title": "Uptime Kuma Alert",
|
"title": "Uptime Kuma Alert",
|
||||||
"body": msg,
|
"body": msg,
|
||||||
};
|
};
|
||||||
await axios.post(lunaseaurl, testdata);
|
await axios.post(lunaseadevice, testdata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (heartbeatJSON["status"] === DOWN) {
|
if (heartbeatJSON["status"] === DOWN) {
|
||||||
let downdata = {
|
let downdata = {
|
||||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||||
"body": "[🔴 Down] " +
|
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||||
heartbeatJSON["msg"] +
|
|
||||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
|
||||||
};
|
};
|
||||||
await axios.post(lunaseaurl, downdata);
|
await axios.post(lunaseadevice, downdata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (heartbeatJSON["status"] === UP) {
|
if (heartbeatJSON["status"] === UP) {
|
||||||
let updata = {
|
let updata = {
|
||||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||||
"body": "[✅ Up] " +
|
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||||
heartbeatJSON["msg"] +
|
|
||||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
|
||||||
};
|
};
|
||||||
await axios.post(lunaseaurl, updata);
|
await axios.post(lunaseadevice, updata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ class Mattermost extends NotificationProvider {
|
|||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
try {
|
try {
|
||||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||||
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
|
// If heartbeatJSON is null, assume we're testing.
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let mattermostTestData = {
|
let mattermostTestData = {
|
||||||
username: mattermostUserName,
|
username: mattermostUserName,
|
||||||
@@ -27,69 +27,42 @@ class Mattermost extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||||
let mattermostIconEmojiOnline = "";
|
|
||||||
let mattermostIconEmojiOffline = "";
|
|
||||||
|
|
||||||
if (mattermostIconEmoji && typeof mattermostIconEmoji === "string") {
|
|
||||||
const emojiArray = mattermostIconEmoji.split(" ");
|
|
||||||
if (emojiArray.length >= 2) {
|
|
||||||
mattermostIconEmojiOnline = emojiArray[0];
|
|
||||||
mattermostIconEmojiOffline = emojiArray[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const mattermostIconUrl = notification.mattermosticonurl;
|
const mattermostIconUrl = notification.mattermosticonurl;
|
||||||
let iconEmoji = mattermostIconEmoji;
|
|
||||||
let statusField = {
|
|
||||||
short: false,
|
|
||||||
title: "Error",
|
|
||||||
value: heartbeatJSON.msg,
|
|
||||||
};
|
|
||||||
let statusText = "unknown";
|
|
||||||
let color = "#000000";
|
|
||||||
if (heartbeatJSON.status === DOWN) {
|
|
||||||
iconEmoji = mattermostIconEmojiOffline || mattermostIconEmoji;
|
|
||||||
statusField = {
|
|
||||||
short: false,
|
|
||||||
title: "Error",
|
|
||||||
value: heartbeatJSON.msg,
|
|
||||||
};
|
|
||||||
statusText = "down.";
|
|
||||||
color = "#FF0000";
|
|
||||||
} else if (heartbeatJSON.status === UP) {
|
|
||||||
iconEmoji = mattermostIconEmojiOnline || mattermostIconEmoji;
|
|
||||||
statusField = {
|
|
||||||
short: false,
|
|
||||||
title: "Ping",
|
|
||||||
value: heartbeatJSON.ping + "ms",
|
|
||||||
};
|
|
||||||
statusText = "up!";
|
|
||||||
color = "#32CD32";
|
|
||||||
}
|
|
||||||
|
|
||||||
let mattermostdata = {
|
if (heartbeatJSON["status"] === DOWN) {
|
||||||
username: monitorJSON.name + " " + mattermostUserName,
|
let mattermostdowndata = {
|
||||||
|
username: mattermostUserName,
|
||||||
|
text: "Uptime Kuma Alert",
|
||||||
channel: mattermostChannel,
|
channel: mattermostChannel,
|
||||||
icon_emoji: iconEmoji,
|
icon_emoji: mattermostIconEmoji,
|
||||||
icon_url: mattermostIconUrl,
|
icon_url: mattermostIconUrl,
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
fallback:
|
fallback:
|
||||||
"Your " +
|
"Your " +
|
||||||
monitorJSON.name +
|
monitorJSON["name"] +
|
||||||
" service went " +
|
" service went down.",
|
||||||
statusText,
|
color: "#FF0000",
|
||||||
color: color,
|
|
||||||
title:
|
title:
|
||||||
monitorJSON.name +
|
"❌ " +
|
||||||
" service went " +
|
monitorJSON["name"] +
|
||||||
statusText,
|
" service went down. ❌",
|
||||||
title_link: monitorJSON.url,
|
title_link: monitorJSON["url"],
|
||||||
fields: [
|
fields: [
|
||||||
statusField,
|
|
||||||
{
|
{
|
||||||
short: true,
|
short: true,
|
||||||
title: `Time (${heartbeatJSON["timezone"]})`,
|
title: "Service Name",
|
||||||
value: heartbeatJSON.localDateTime,
|
value: monitorJSON["name"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
short: true,
|
||||||
|
title: "Time (UTC)",
|
||||||
|
value: heartbeatJSON["time"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
short: false,
|
||||||
|
title: "Error",
|
||||||
|
value: heartbeatJSON["msg"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -97,9 +70,54 @@ class Mattermost extends NotificationProvider {
|
|||||||
};
|
};
|
||||||
await axios.post(
|
await axios.post(
|
||||||
notification.mattermostWebhookUrl,
|
notification.mattermostWebhookUrl,
|
||||||
mattermostdata
|
mattermostdowndata
|
||||||
);
|
);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
} else if (heartbeatJSON["status"] === UP) {
|
||||||
|
let mattermostupdata = {
|
||||||
|
username: mattermostUserName,
|
||||||
|
text: "Uptime Kuma Alert",
|
||||||
|
channel: mattermostChannel,
|
||||||
|
icon_emoji: mattermostIconEmoji,
|
||||||
|
icon_url: mattermostIconUrl,
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
fallback:
|
||||||
|
"Your " +
|
||||||
|
monitorJSON["name"] +
|
||||||
|
" service went up!",
|
||||||
|
color: "#32CD32",
|
||||||
|
title:
|
||||||
|
"✅ " +
|
||||||
|
monitorJSON["name"] +
|
||||||
|
" service went up! ✅",
|
||||||
|
title_link: monitorJSON["url"],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
short: true,
|
||||||
|
title: "Service Name",
|
||||||
|
value: monitorJSON["name"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
short: true,
|
||||||
|
title: "Time (UTC)",
|
||||||
|
value: heartbeatJSON["time"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
short: false,
|
||||||
|
title: "Ping",
|
||||||
|
value: heartbeatJSON["ping"] + "ms",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await axios.post(
|
||||||
|
notification.mattermostWebhookUrl,
|
||||||
|
mattermostupdata
|
||||||
|
);
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwGeneralAxiosError(error);
|
this.throwGeneralAxiosError(error);
|
||||||
}
|
}
|
||||||
|
@@ -1,119 +0,0 @@
|
|||||||
const { log } = require("../../src/util");
|
|
||||||
const NotificationProvider = require("./notification-provider");
|
|
||||||
const {
|
|
||||||
relayInit,
|
|
||||||
getPublicKey,
|
|
||||||
getEventHash,
|
|
||||||
getSignature,
|
|
||||||
nip04,
|
|
||||||
nip19
|
|
||||||
} = require("nostr-tools");
|
|
||||||
|
|
||||||
// polyfills for node versions
|
|
||||||
const semver = require("semver");
|
|
||||||
const nodeVersion = process.version;
|
|
||||||
if (semver.lt(nodeVersion, "16.0.0")) {
|
|
||||||
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
|
|
||||||
} else if (semver.lt(nodeVersion, "18.0.0")) {
|
|
||||||
// polyfills for node 16
|
|
||||||
global.crypto = require("crypto");
|
|
||||||
global.WebSocket = require("isomorphic-ws");
|
|
||||||
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
|
|
||||||
crypto.subtle = crypto.webcrypto.subtle;
|
|
||||||
}
|
|
||||||
} else if (semver.lt(nodeVersion, "20.0.0")) {
|
|
||||||
// polyfills for node 18
|
|
||||||
global.crypto = require("crypto");
|
|
||||||
global.WebSocket = require("isomorphic-ws");
|
|
||||||
} else {
|
|
||||||
// polyfills for node 20
|
|
||||||
global.WebSocket = require("isomorphic-ws");
|
|
||||||
}
|
|
||||||
|
|
||||||
class Nostr extends NotificationProvider {
|
|
||||||
name = "nostr";
|
|
||||||
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
|
||||||
// All DMs should have same timestamp
|
|
||||||
const createdAt = Math.floor(Date.now() / 1000);
|
|
||||||
|
|
||||||
const senderPrivateKey = await this.getPrivateKey(notification.sender);
|
|
||||||
const senderPublicKey = getPublicKey(senderPrivateKey);
|
|
||||||
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
|
|
||||||
|
|
||||||
// Create NIP-04 encrypted direct message event for each recipient
|
|
||||||
const events = [];
|
|
||||||
for (const recipientPublicKey of recipientsPublicKeys) {
|
|
||||||
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
|
|
||||||
let event = {
|
|
||||||
kind: 4,
|
|
||||||
pubkey: senderPublicKey,
|
|
||||||
created_at: createdAt,
|
|
||||||
tags: [[ "p", recipientPublicKey ]],
|
|
||||||
content: ciphertext,
|
|
||||||
};
|
|
||||||
event.id = getEventHash(event);
|
|
||||||
event.sig = getSignature(event, senderPrivateKey);
|
|
||||||
events.push(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish events to each relay
|
|
||||||
const relays = notification.relays.split("\n");
|
|
||||||
let successfulRelays = 0;
|
|
||||||
|
|
||||||
// Connect to each relay
|
|
||||||
for (const relayUrl of relays) {
|
|
||||||
const relay = relayInit(relayUrl);
|
|
||||||
try {
|
|
||||||
await relay.connect();
|
|
||||||
successfulRelays++;
|
|
||||||
|
|
||||||
// Publish events
|
|
||||||
for (const event of events) {
|
|
||||||
relay.publish(event);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
continue;
|
|
||||||
} finally {
|
|
||||||
relay.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report success or failure
|
|
||||||
if (successfulRelays === 0) {
|
|
||||||
throw Error("Failed to connect to any relays.");
|
|
||||||
}
|
|
||||||
return `${successfulRelays}/${relays.length} relays connected.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPrivateKey(sender) {
|
|
||||||
try {
|
|
||||||
const senderDecodeResult = await nip19.decode(sender);
|
|
||||||
const { data } = senderDecodeResult;
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to get private key: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPublicKeys(recipients) {
|
|
||||||
const recipientsList = recipients.split("\n");
|
|
||||||
const publicKeys = [];
|
|
||||||
for (const recipient of recipientsList) {
|
|
||||||
try {
|
|
||||||
const recipientDecodeResult = await nip19.decode(recipient);
|
|
||||||
const { type, data } = recipientDecodeResult;
|
|
||||||
if (type === "npub") {
|
|
||||||
publicKeys.push(data);
|
|
||||||
} else {
|
|
||||||
throw new Error("not an npub");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Error decoding recipient: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return publicKeys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Nostr;
|
|
@@ -1,6 +1,5 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { DOWN, UP } = require("../../src/util");
|
|
||||||
|
|
||||||
class Ntfy extends NotificationProvider {
|
class Ntfy extends NotificationProvider {
|
||||||
|
|
||||||
@@ -10,54 +9,16 @@ class Ntfy extends NotificationProvider {
|
|||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
try {
|
try {
|
||||||
let headers = {};
|
let headers = {};
|
||||||
if (notification.ntfyAuthenticationMethod === "usernamePassword") {
|
if (notification.ntfyusername) {
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
|
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
|
||||||
};
|
};
|
||||||
} else if (notification.ntfyAuthenticationMethod === "accessToken") {
|
|
||||||
headers = {
|
|
||||||
"Authorization": "Bearer " + notification.ntfyaccesstoken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
|
|
||||||
if (heartbeatJSON == null) {
|
|
||||||
let ntfyTestData = {
|
|
||||||
"topic": notification.ntfytopic,
|
|
||||||
"title": (monitorJSON?.name || notification.ntfytopic) + " [Uptime-Kuma]",
|
|
||||||
"message": msg,
|
|
||||||
"priority": notification.ntfyPriority,
|
|
||||||
"tags": [ "test_tube" ],
|
|
||||||
};
|
|
||||||
await axios.post(`${notification.ntfyserverurl}`, ntfyTestData, { headers: headers });
|
|
||||||
return okMsg;
|
|
||||||
}
|
|
||||||
let tags = [];
|
|
||||||
let status = "unknown";
|
|
||||||
let priority = notification.ntfyPriority || 4;
|
|
||||||
if ("status" in heartbeatJSON) {
|
|
||||||
if (heartbeatJSON.status === DOWN) {
|
|
||||||
tags = [ "red_circle" ];
|
|
||||||
status = "Down";
|
|
||||||
// if priority is not 5, increase priority for down alerts
|
|
||||||
priority = priority === 5 ? priority : priority + 1;
|
|
||||||
} else if (heartbeatJSON["status"] === UP) {
|
|
||||||
tags = [ "green_circle" ];
|
|
||||||
status = "Up";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let data = {
|
let data = {
|
||||||
"topic": notification.ntfytopic,
|
"topic": notification.ntfytopic,
|
||||||
"message": heartbeatJSON.msg,
|
"message": msg,
|
||||||
"priority": priority,
|
"priority": notification.ntfyPriority || 4,
|
||||||
"title": monitorJSON.name + " " + status + " [Uptime-Kuma]",
|
"title": "Uptime-Kuma",
|
||||||
"tags": tags,
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"action": "view",
|
|
||||||
"label": "Open " + monitorJSON.name,
|
|
||||||
"url": monitorJSON.url,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (notification.ntfyIcon) {
|
if (notification.ntfyIcon) {
|
||||||
|
@@ -1,97 +0,0 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
|
||||||
const axios = require("axios");
|
|
||||||
const { UP, DOWN } = require("../../src/util");
|
|
||||||
|
|
||||||
const opsgenieAlertsUrlEU = "https://api.eu.opsgenie.com/v2/alerts";
|
|
||||||
const opsgenieAlertsUrlUS = "https://api.opsgenie.com/v2/alerts";
|
|
||||||
let okMsg = "Sent Successfully.";
|
|
||||||
|
|
||||||
class Opsgenie extends NotificationProvider {
|
|
||||||
|
|
||||||
name = "Opsgenie";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
|
||||||
let opsgenieAlertsUrl;
|
|
||||||
let priority = (!notification.opsgeniePriority) ? 3 : notification.opsgeniePriority;
|
|
||||||
const textMsg = "Uptime Kuma Alert";
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (notification.opsgenieRegion) {
|
|
||||||
case "US":
|
|
||||||
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
|
|
||||||
break;
|
|
||||||
case "EU":
|
|
||||||
opsgenieAlertsUrl = opsgenieAlertsUrlEU;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heartbeatJSON == null) {
|
|
||||||
let notificationTestAlias = "uptime-kuma-notification-test";
|
|
||||||
let data = {
|
|
||||||
"message": msg,
|
|
||||||
"alias": notificationTestAlias,
|
|
||||||
"source": "Uptime Kuma",
|
|
||||||
"priority": "P5"
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.post(notification, opsgenieAlertsUrl, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heartbeatJSON.status === DOWN) {
|
|
||||||
let data = {
|
|
||||||
"message": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
|
||||||
"alias": monitorJSON.name,
|
|
||||||
"description": msg,
|
|
||||||
"source": "Uptime Kuma",
|
|
||||||
"priority": `P${priority}`
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.post(notification, opsgenieAlertsUrl, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heartbeatJSON.status === UP) {
|
|
||||||
let opsgenieAlertsCloseUrl = `${opsgenieAlertsUrl}/${encodeURIComponent(monitorJSON.name)}/close?identifierType=alias`;
|
|
||||||
let data = {
|
|
||||||
"source": "Uptime Kuma",
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.post(notification, opsgenieAlertsCloseUrl, data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.throwGeneralAxiosError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {BeanModel} notification
|
|
||||||
* @param {string} url Request url
|
|
||||||
* @param {Object} data Request body
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
async post(notification, url, data) {
|
|
||||||
let config = {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `GenieKey ${notification.opsgenieApiKey}`,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = await axios.post(url, data, config);
|
|
||||||
if (res.status == null) {
|
|
||||||
return "Opsgenie notification failed with invalid response!";
|
|
||||||
}
|
|
||||||
if (res.status < 200 || res.status >= 300) {
|
|
||||||
return `Opsgenie notification failed with status code ${res.status}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return okMsg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Opsgenie;
|
|
@@ -1,91 +0,0 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
|
||||||
const axios = require("axios");
|
|
||||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
|
||||||
const { setting } = require("../util-server");
|
|
||||||
let successMessage = "Sent Successfully.";
|
|
||||||
|
|
||||||
class PagerTree extends NotificationProvider {
|
|
||||||
name = "PagerTree";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
|
||||||
try {
|
|
||||||
if (heartbeatJSON == null) {
|
|
||||||
// general messages
|
|
||||||
return this.postNotification(notification, msg, monitorJSON, heartbeatJSON);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heartbeatJSON.status === UP && notification.pagertreeAutoResolve === "resolve") {
|
|
||||||
return this.postNotification(notification, null, monitorJSON, heartbeatJSON, notification.pagertreeAutoResolve);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heartbeatJSON.status === DOWN) {
|
|
||||||
const title = `Uptime Kuma Monitor "${monitorJSON.name}" is DOWN`;
|
|
||||||
return this.postNotification(notification, title, monitorJSON, heartbeatJSON);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.throwGeneralAxiosError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if result is successful, result code should be in range 2xx
|
|
||||||
* @param {Object} result Axios response object
|
|
||||||
* @throws {Error} The status code is not in range 2xx
|
|
||||||
*/
|
|
||||||
checkResult(result) {
|
|
||||||
if (result.status == null) {
|
|
||||||
throw new Error("PagerTree notification failed with invalid response!");
|
|
||||||
}
|
|
||||||
if (result.status < 200 || result.status >= 300) {
|
|
||||||
throw new Error("PagerTree notification failed with status code " + result.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the message
|
|
||||||
* @param {BeanModel} notification Message title
|
|
||||||
* @param {string} title Message title
|
|
||||||
* @param {Object} monitorJSON Monitor details (For Up/Down only)
|
|
||||||
* @param {?string} eventAction Action event for PagerTree (create, resolve)
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
async postNotification(notification, title, monitorJSON, heartbeatJSON, eventAction = "create") {
|
|
||||||
|
|
||||||
if (eventAction == null) {
|
|
||||||
return "No action required";
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
method: "POST",
|
|
||||||
url: notification.pagertreeIntegrationUrl,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
data: {
|
|
||||||
event_type: eventAction,
|
|
||||||
id: heartbeatJSON?.monitorID || "uptime-kuma",
|
|
||||||
title: title,
|
|
||||||
urgency: notification.pagertreeUrgency,
|
|
||||||
heartbeat: heartbeatJSON,
|
|
||||||
monitor: monitorJSON
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
|
||||||
if (baseURL && monitorJSON) {
|
|
||||||
options.client = "Uptime Kuma";
|
|
||||||
options.client_url = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = await axios.request(options);
|
|
||||||
this.checkResult(result);
|
|
||||||
if (result.statusText != null) {
|
|
||||||
return "PagerTree notification succeed: " + result.statusText;
|
|
||||||
}
|
|
||||||
|
|
||||||
return successMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = PagerTree;
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user