mirror of
https://github.com/unraid/api.git
synced 2026-01-02 06:30:02 -06:00
Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8f469c4fb | ||
|
|
bc61b45f9f | ||
|
|
f530d9ea82 | ||
|
|
2046fa5310 | ||
|
|
9ea2327fa0 | ||
|
|
ff67b54a1b | ||
|
|
e6bd7a54be | ||
|
|
5827b5ffa3 | ||
|
|
572a1310e0 | ||
|
|
c1403d3826 | ||
|
|
29afe9b9e8 | ||
|
|
e9ff33d263 | ||
|
|
a62f60a436 | ||
|
|
838964c6ef | ||
|
|
800fc12c15 | ||
|
|
80175241e3 | ||
|
|
5d801f22f5 | ||
|
|
ba772add54 | ||
|
|
ff24f12cae | ||
|
|
487f5c1865 | ||
|
|
e0c90037fb | ||
|
|
aa5f603cba | ||
|
|
409db43973 | ||
|
|
cef1b29355 | ||
|
|
045750c87e | ||
|
|
85802e7af7 | ||
|
|
4bfdb66d46 | ||
|
|
81a6a52d9f | ||
|
|
7759fe1dc3 | ||
|
|
3b2acb29b5 | ||
|
|
5f2b949ecf | ||
|
|
1b956d563e | ||
|
|
c6a97f5082 | ||
|
|
7f512e47e9 | ||
|
|
5d725b0e76 | ||
|
|
fe63607260 | ||
|
|
0a1d4daf6e | ||
|
|
9e9e385bef | ||
|
|
6fed39e05b | ||
|
|
3dec53d13d | ||
|
|
f0ded9f5be | ||
|
|
7d55a1c2cd | ||
|
|
f3dc9663b8 | ||
|
|
05c7c481a9 | ||
|
|
adcc1543f0 | ||
|
|
95f873c752 | ||
|
|
ec90f8b295 | ||
|
|
f84195a98d | ||
|
|
5e98a68e2e | ||
|
|
b91dbca144 | ||
|
|
79a01da18d | ||
|
|
14951d3004 | ||
|
|
64c2061bea | ||
|
|
e3adc9a29a | ||
|
|
6b689ffcce | ||
|
|
c995a4c5c8 | ||
|
|
8d1e0f67d1 | ||
|
|
7877a5dca2 | ||
|
|
16db278ffd | ||
|
|
521b4381f2 | ||
|
|
9ae9d40f94 | ||
|
|
1d562d404c | ||
|
|
7ac1b268d9 | ||
|
|
4833e9dccf | ||
|
|
f28b7510fa | ||
|
|
37b717b142 | ||
|
|
fd8b40d9aa | ||
|
|
1d944781cf | ||
|
|
1f4c64d022 | ||
|
|
f69b5130a3 | ||
|
|
f8b143904b | ||
|
|
31a5413643 | ||
|
|
a95fc5ed07 | ||
|
|
fcd7bb790e | ||
|
|
008e10948e | ||
|
|
c97a4f1268 | ||
|
|
3eba95b8cc | ||
|
|
2bf8f0b937 | ||
|
|
9ae45d1258 | ||
|
|
1835a4cf3f | ||
|
|
2ab44b894d | ||
|
|
1108f49b07 | ||
|
|
cc69213beb | ||
|
|
460e557dd8 | ||
|
|
05e29468d2 | ||
|
|
4d3a311fb4 | ||
|
|
bc62d210ec | ||
|
|
43d3ea6553 | ||
|
|
882e3e1ef4 | ||
|
|
b33c86c99c | ||
|
|
cd0248e4c9 | ||
|
|
ecb3ed5003 | ||
|
|
0569339a41 | ||
|
|
3e9faead43 | ||
|
|
6e700b2385 | ||
|
|
464fc4993c | ||
|
|
4316c72809 | ||
|
|
ce0cebe09c | ||
|
|
23b90a0d56 | ||
|
|
3f8b3536b5 | ||
|
|
0dcf785b45 | ||
|
|
8cf4aff622 | ||
|
|
cefda7c42b | ||
|
|
0393b2382c | ||
|
|
23e900f7fd | ||
|
|
2d6aafc257 | ||
|
|
b191efece1 | ||
|
|
2a7f0043f5 | ||
|
|
607c7e3704 | ||
|
|
c246a443c5 | ||
|
|
fd495e1f5c | ||
|
|
621a06cafa | ||
|
|
f890b05151 | ||
|
|
567d8fdd6d | ||
|
|
7d996906ad | ||
|
|
b5ec076279 | ||
|
|
de8dfe3dba | ||
|
|
7249956d40 | ||
|
|
e6eb56466e | ||
|
|
8954700bcb | ||
|
|
eb595cea9e | ||
|
|
9a1a0a54e6 | ||
|
|
134396b602 | ||
|
|
2aa65fdb68 | ||
|
|
a9c4d7d5dd | ||
|
|
2cbbd5ee40 | ||
|
|
c84c55761c | ||
|
|
77eed36990 | ||
|
|
5c2d84d8b4 | ||
|
|
9883f0f82f | ||
|
|
e62b05b6f6 | ||
|
|
8e6ee8b770 | ||
|
|
666b51a28a | ||
|
|
1962097a66 | ||
|
|
7f010854b5 | ||
|
|
17288a4c02 | ||
|
|
ea48def9fc | ||
|
|
a1d5c29ffb | ||
|
|
bf99eb25c8 | ||
|
|
b35a440792 | ||
|
|
58f9eec8b1 | ||
|
|
26841aa10d | ||
|
|
e18a8d670e | ||
|
|
49d077db97 | ||
|
|
9dafe165b0 | ||
|
|
cce1953cb8 | ||
|
|
7e33b25593 | ||
|
|
78fb49a6fc | ||
|
|
f1e0d93bc5 | ||
|
|
195a178d15 | ||
|
|
b9257fce28 | ||
|
|
41eaf4ef1b | ||
|
|
93d0c08955 | ||
|
|
c5bc3454ff | ||
|
|
c33b4ef709 | ||
|
|
ce3ba7d070 | ||
|
|
639eb08291 | ||
|
|
6d109b4c4c | ||
|
|
6f3971dc47 | ||
|
|
2ccb503dc8 | ||
|
|
3cb9fdf102 | ||
|
|
40d81a4081 | ||
|
|
5a85f55be8 | ||
|
|
5455e211bc | ||
|
|
cb4cc989c7 | ||
|
|
037aa479bf | ||
|
|
08567f287a | ||
|
|
a57f1d890d | ||
|
|
3ab406e012 | ||
|
|
f36f4702a2 | ||
|
|
62697f7972 | ||
|
|
ec8d2bc0e8 | ||
|
|
d83664b6a3 | ||
|
|
6910a020d2 | ||
|
|
60e5c6e3e8 | ||
|
|
90b1432875 | ||
|
|
f1059aa381 | ||
|
|
01b4937f35 | ||
|
|
3e051815c5 | ||
|
|
e976daf8b0 | ||
|
|
422046dc03 | ||
|
|
9a270971d1 | ||
|
|
0742382ae1 | ||
|
|
763c38430e | ||
|
|
4d926bba8e | ||
|
|
4acc4ea9a9 | ||
|
|
565bf47818 | ||
|
|
176a0f30be | ||
|
|
6f4d983d89 | ||
|
|
6a0e258cf2 | ||
|
|
8d82064888 | ||
|
|
4300179b67 | ||
|
|
7e31ae2ebf | ||
|
|
7a27560b0d | ||
|
|
93655fef62 | ||
|
|
a581a95cb4 | ||
|
|
261fdda47c | ||
|
|
7a2a243a21 | ||
|
|
bead4256af | ||
|
|
e8dfd7e3b3 | ||
|
|
e456b7fcac | ||
|
|
fbe5e417ef | ||
|
|
5f80053a33 | ||
|
|
fa520a2d3e | ||
|
|
cf54f01945 | ||
|
|
44d2d58f12 | ||
|
|
daba2a352f | ||
|
|
d1ff2b1fad | ||
|
|
b1bd71f2e2 | ||
|
|
7f49816275 | ||
|
|
d73d460e88 | ||
|
|
ab1e852b6c | ||
|
|
117b7430db | ||
|
|
2e73f9e37a | ||
|
|
d3158983b4 | ||
|
|
dae7baa6ad | ||
|
|
e29f5e1adf | ||
|
|
e8d15c7dbb | ||
|
|
58be009da4 | ||
|
|
d4eb0ce3f2 | ||
|
|
d73324a141 | ||
|
|
7061be60f4 | ||
|
|
2a65f64ac1 |
16
.github/workflows/main.yml
vendored
16
.github/workflows/main.yml
vendored
@@ -81,10 +81,10 @@ jobs:
|
||||
- name: Build Docker Compose
|
||||
run: |
|
||||
docker network create mothership_default
|
||||
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose build builder
|
||||
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose build builder
|
||||
|
||||
- name: Run Docker Compose
|
||||
run: GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run builder npm run coverage
|
||||
run: GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose run builder npm run coverage
|
||||
|
||||
lint-web:
|
||||
defaults:
|
||||
@@ -298,6 +298,18 @@ jobs:
|
||||
source: staging-release
|
||||
out_dir: unraid-api
|
||||
|
||||
- name: Upload Staging Plugin to Cloudflare Bucket
|
||||
uses: jakejarvis/s3-sync-action@v0.5.1
|
||||
env:
|
||||
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
|
||||
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET_PREVIEW }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: 'auto'
|
||||
SOURCE_DIR: staging-release
|
||||
DEST_DIR: unraid-api
|
||||
|
||||
|
||||
create-draft-release:
|
||||
# Only create new draft if this is a version tag
|
||||
if: |
|
||||
|
||||
12
.github/workflows/pull-request.yml
vendored
12
.github/workflows/pull-request.yml
vendored
@@ -27,12 +27,12 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# network=host driver-opt needed to push to local registry
|
||||
driver-opts: network=host
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: api
|
||||
target: builder
|
||||
@@ -60,13 +60,13 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# network=host driver-opt needed to push to local registry
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: api
|
||||
target: builder
|
||||
@@ -100,13 +100,13 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# network=host driver-opt needed to push to local registry
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: api
|
||||
target: builder
|
||||
|
||||
57
.github/workflows/release-production.yml
vendored
Normal file
57
.github/workflows/release-production.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Publish Release to Digital Ocean
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-to-digital-ocean:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download Release Artifacts (Plugins)
|
||||
uses: dsaltares/fetch-gh-release-asset@master
|
||||
with:
|
||||
file: ".*"
|
||||
regex: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
target: "./"
|
||||
version: "latest"
|
||||
|
||||
- uses: cardinalby/git-get-release-action@v1
|
||||
id: release-info
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
latest: true
|
||||
prerelease: false
|
||||
- name: Get Release Changelog
|
||||
run: |
|
||||
notes=$(cat << EOF
|
||||
${{ steps.release-info.outputs.body }}
|
||||
EOF
|
||||
)
|
||||
escapedNotes=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"$notes")
|
||||
sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "dynamix.unraid.net.plg"
|
||||
sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "dynamix.unraid.net.staging.plg"
|
||||
|
||||
- name: Upload All Release Files to DO Spaces
|
||||
uses: BetaHuhn/do-spaces-action@v2
|
||||
with:
|
||||
access_key: ${{ secrets.DO_ACCESS_KEY }}
|
||||
secret_key: ${{ secrets.DO_SECRET_KEY }}
|
||||
space_name: ${{ secrets.DO_SPACE_NAME }}
|
||||
space_region: ${{ secrets.DO_SPACE_REGION }}
|
||||
source: "."
|
||||
out_dir: unraid-api
|
||||
|
||||
- name: Upload Staging Plugin to Cloudflare Bucket
|
||||
uses: jakejarvis/s3-sync-action@v0.5.1
|
||||
env:
|
||||
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
|
||||
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: 'auto'
|
||||
SOURCE_DIR: "."
|
||||
DEST_DIR: unraid-api
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -83,4 +83,6 @@ deploy/*
|
||||
.cache
|
||||
.output
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.example
|
||||
|
||||
fb_keepalive
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -29,5 +29,6 @@
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
],
|
||||
"i18n-ally.keystyle": "flat"
|
||||
"i18n-ally.keystyle": "flat",
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
}
|
||||
11
api/.env.test
Normal file
11
api/.env.test
Normal file
@@ -0,0 +1,11 @@
|
||||
VERSION="THIS_WILL_BE_REPLACED_WHEN_BUILT"
|
||||
|
||||
PATHS_UNRAID_DATA=./dev/data # Where we store plugin data (e.g. permissions.json)
|
||||
PATHS_STATES=./dev/states # Where .ini files live (e.g. vars.ini)
|
||||
PATHS_DYNAMIX_BASE=./dev/dynamix # Dynamix's data directory
|
||||
PATHS_DYNAMIX_CONFIG=./dev/dynamix/dynamix.cfg # Dynamix's config file
|
||||
PATHS_MY_SERVERS_CONFIG=./dev/Unraid.net/myservers.cfg # My servers config file
|
||||
PATHS_MY_SERVERS_FB=./dev/Unraid.net/fb_keepalive # My servers flashbackup timekeeper file
|
||||
PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location
|
||||
PORT=5000
|
||||
NODE_ENV=test
|
||||
@@ -1 +1 @@
|
||||
18.17.1
|
||||
18.19.1
|
||||
163
api/CHANGELOG.md
163
api/CHANGELOG.md
@@ -2,6 +2,169 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [3.8.1](https://github.com/unraid/api/compare/v3.8.0...v3.8.1) (2024-08-13)
|
||||
|
||||
## [3.8.0](https://github.com/unraid/api/compare/v3.7.1...v3.8.0) (2024-08-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* always force push ([662f3ce](https://github.com/unraid/api/commit/662f3ce440593e609c64364726f7da16dda0972b))
|
||||
* don't allow flash backup repos larger than 500MB ([#890](https://github.com/unraid/api/issues/890)) ([30a32f5](https://github.com/unraid/api/commit/30a32f5fe684bb32c084c4125aade5e63ffd788b))
|
||||
* downgradeOs callback for non stable osCurrentBranch ([17c4489](https://github.com/unraid/api/commit/17c4489e97bda504ca45e360591655ded166c355))
|
||||
* settings through the API ([#867](https://github.com/unraid/api/issues/867)) ([e73624b](https://github.com/unraid/api/commit/e73624be6be8bc2c70d898b8601a88cc8d20a3e4))
|
||||
* swap to docker compose from docker-compose ([ec16a6a](https://github.com/unraid/api/commit/ec16a6aab1a2d5c836387da438fbeade07d23425))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* apolloClient types ([f14c767](https://github.com/unraid/api/commit/f14c7673735b92aa167e9e8dcb14a045bcfea994))
|
||||
* **deps:** update dependency @vue/apollo-composable to v4.0.2 ([#787](https://github.com/unraid/api/issues/787)) ([edfc846](https://github.com/unraid/api/commit/edfc8464b0e0c2f38003ae8420e81532fd18351f))
|
||||
* formattedRegTm type ([748906e](https://github.com/unraid/api/commit/748906e15d30c6162e2f08f28724c9104c81d123))
|
||||
* i18n t prop type ([96d519f](https://github.com/unraid/api/commit/96d519f3e6b96ea7c4dc60616522216de20ee140))
|
||||
* lint error for web components ([bc27b20](https://github.com/unraid/api/commit/bc27b20524934cf896efb84a131cd270431c508c))
|
||||
* lint issues ([853dc19](https://github.com/unraid/api/commit/853dc195b13fff29160afb44f9ff11d4dd6a3232))
|
||||
* swap undefined to null ([ebba976](https://github.com/unraid/api/commit/ebba9769873a6536e3fce65978e6475d93280560))
|
||||
* tailwind config types ([0f77e55](https://github.com/unraid/api/commit/0f77e5596db3356b5dc05129b3ce215a8809e1dc))
|
||||
* ts-expect-error unneeded ([ee4d4e9](https://github.com/unraid/api/commit/ee4d4e9f12b4488ff39445bc72c1b83a9d93e993))
|
||||
* type check ([606aad7](https://github.com/unraid/api/commit/606aad703d91b72a14e15da3100dfa355052ed58))
|
||||
* type errors round 1 ([977d5da](https://github.com/unraid/api/commit/977d5daf04012f16e7b6602167338f0bc363735a))
|
||||
* update status button alignment ([4f2deaf](https://github.com/unraid/api/commit/4f2deaf70e5caa9f29fc5b2974b278f80b7b3a8a))
|
||||
|
||||
### [3.7.1](https://github.com/unraid/api/compare/v3.7.0...v3.7.1) (2024-05-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reboot required and available edge case ([#885](https://github.com/unraid/api/issues/885)) ([76e9cdf](https://github.com/unraid/api/commit/76e9cdf81f06a19c2e4c9a40a4d8e062bad2a607))
|
||||
|
||||
## [3.7.0](https://github.com/unraid/api/compare/v3.6.0...v3.7.0) (2024-05-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a timestamp to flash backup ([#877](https://github.com/unraid/api/issues/877)) ([b868fd4](https://github.com/unraid/api/commit/b868fd46c3886b2182245a61f20be6df65e46abe))
|
||||
* add support for outgoing proxies ([#863](https://github.com/unraid/api/issues/863)) ([223693e](https://github.com/unraid/api/commit/223693e0981d5f2884a1f8b8baf03d4dc58e8cb2))
|
||||
* array state on registration page ([d36fef0](https://github.com/unraid/api/commit/d36fef0545ddb820e67e8bc6cb42ea3644021d66))
|
||||
* downgradeOs callback ([154a976](https://github.com/unraid/api/commit/154a976109f0a32653a2851988420707631327ca))
|
||||
* Flash Backup requires connection to mothership ([#868](https://github.com/unraid/api/issues/868)) ([d127208](https://github.com/unraid/api/commit/d127208b5e0f7f9991f515f95b0e266d38cf3287))
|
||||
* **plg:** install prevent downgrade of shared page & php files ([#873](https://github.com/unraid/api/issues/873)) ([4ac72b1](https://github.com/unraid/api/commit/4ac72b16692c4246c9d2c0b53b23d8b2d95f5de6))
|
||||
* **plg:** plg install prevent web component downgrade ([8703bd4](https://github.com/unraid/api/commit/8703bd498108f5c05562584a708bd2306e53f7a6))
|
||||
* postbuild script to add timestamp to web component manifest ([47f08ea](https://github.com/unraid/api/commit/47f08ea3594a91098f67718c0123110c7b5f86f7))
|
||||
* registration page server error heading + subheading ([6038ebd](https://github.com/unraid/api/commit/6038ebdf39bf47f2cb5c0b1de84764795374f018))
|
||||
* remove cron to download JS daily ([#864](https://github.com/unraid/api/issues/864)) ([33f6d6b](https://github.com/unraid/api/commit/33f6d6b343de07dbe70de863926906736d42f371)), closes [#529](https://github.com/unraid/api/issues/529)
|
||||
* ui to allow second update without reboot ([b0f2d10](https://github.com/unraid/api/commit/b0f2d102917f54ab33f0ad10863522b8ff8e3ce5))
|
||||
* UI Update OS Cancel ([7c02308](https://github.com/unraid/api/commit/7c02308964d5e21990427a2c626c9db2d9e1fed0))
|
||||
* UnraidUpdateCancel script ([b73bdc0](https://github.com/unraid/api/commit/b73bdc021764762ed12dca494e1345412a45c677))
|
||||
* **web:** callback types myKeys & linkKey ([c88ee01](https://github.com/unraid/api/commit/c88ee01827396c3fa8a30bb88c4be712c80b1f4f))
|
||||
* **web:** Registration key linked to account status ([8f6182d](https://github.com/unraid/api/commit/8f6182d426453b73aa19c5f0f59469fa07571694))
|
||||
* **web:** registration page array status messaging ([23ef5a9](https://github.com/unraid/api/commit/23ef5a975e0d5ff0c246c2df5e6c2cb38979d12a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** readme discord url ([ffd5c6a](https://github.com/unraid/api/commit/ffd5c6afb64956e76df22c77104a21bc22798008))
|
||||
* keep minor enhancements from [#872](https://github.com/unraid/api/issues/872) ([#878](https://github.com/unraid/api/issues/878)) ([94a5aa8](https://github.com/unraid/api/commit/94a5aa87b9979fe0f02f884ac61298473bb3271a))
|
||||
* plugin file deployment script ([780d87d](https://github.com/unraid/api/commit/780d87d6589a5469f47ac3fdfd50610ecfc394c8))
|
||||
* prevent corrupt case model in state.php ([#874](https://github.com/unraid/api/issues/874)) ([4ad31df](https://github.com/unraid/api/commit/4ad31dfea9192146dbd2c90bc64a913c696ab0b7))
|
||||
* prevent local dev from throwing ssl error ([051f647](https://github.com/unraid/api/commit/051f6474becf3b25b242cdc6ceee67247b79f8ba))
|
||||
* rc.flashbackup needs to check both signed in and connected ([#882](https://github.com/unraid/api/issues/882)) ([ac8068c](https://github.com/unraid/api/commit/ac8068c9b084622d46fe2c9cb320b793c9ea8c52))
|
||||
* update os cancel refresh on update page ([213c16b](https://github.com/unraid/api/commit/213c16ba3d5a84ebf4965f9d2f4024c66605a613))
|
||||
* **web:** discord url ([1a6f4c6](https://github.com/unraid/api/commit/1a6f4c6db4ef0e5eefac467ec6583b14cb3546c4))
|
||||
* **web:** lint unused rebootVersion ([e198ec9](https://github.com/unraid/api/commit/e198ec9d458e262c412c2dcb5a9d279238de1730))
|
||||
* **web:** registration component remove unused ref ([76f556b](https://github.com/unraid/api/commit/76f556bd64b95ba96af795c9edfa045ebdff4444))
|
||||
|
||||
## [3.6.0](https://github.com/unraid/api/compare/v3.5.3...v3.6.0) (2024-03-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* server config enum message w/ ineligible support ([#861](https://github.com/unraid/api/issues/861)) ([4d3a351](https://github.com/unraid/api/commit/4d3a3510777090788573f4cee83694a0dc6f8df5))
|
||||
|
||||
### [3.5.3](https://github.com/unraid/api/compare/v3.5.2...v3.5.3) (2024-03-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* regDevs usage to allow more flexibility for STARTER ([#860](https://github.com/unraid/api/issues/860)) ([92a9600](https://github.com/unraid/api/commit/92a9600f3a242c5f263f1672eab81054b9cf4fae))
|
||||
|
||||
### [3.5.2](https://github.com/unraid/api/compare/v3.5.1...v3.5.2) (2024-03-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency vue-i18n to v9.10.1 ([#813](https://github.com/unraid/api/issues/813)) ([69b599c](https://github.com/unraid/api/commit/69b599c5ed8d44864201a32b4d952427d454dc74))
|
||||
* **deps:** update dependency wretch to v2.8.0 ([#814](https://github.com/unraid/api/issues/814)) ([66900b4](https://github.com/unraid/api/commit/66900b495b82b923264897d38b1529a22b10aa1c))
|
||||
* update os check modal button conditionals ([282a836](https://github.com/unraid/api/commit/282a83625f417ccefe090b65cc6b73a084727a87))
|
||||
* update os check modal ineligible date format ([83083de](https://github.com/unraid/api/commit/83083de1e698f73a35635ae6047dcf49fd4b8114))
|
||||
|
||||
### [3.5.1](https://github.com/unraid/api/compare/v3.5.0...v3.5.1) (2024-02-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* build docker command updated to use dc.sh script ([0b40886](https://github.com/unraid/api/commit/0b40886e84f27a94dbf67ef4ca0cd8539ef3913e))
|
||||
* date format in UnraidCheck.php ([#852](https://github.com/unraid/api/issues/852)) ([6465f2d](https://github.com/unraid/api/commit/6465f2d7b2394090f35e29cdd680d98ce37f3728))
|
||||
* **deps:** update dependency @apollo/client to v3.9.5 ([#785](https://github.com/unraid/api/issues/785)) ([75b98bc](https://github.com/unraid/api/commit/75b98bc1cbca5b66ae72f52a0b6f5f58230a2473))
|
||||
* **deps:** update dependency @heroicons/vue to v2.1.1 ([#804](https://github.com/unraid/api/issues/804)) ([a0eb7ee](https://github.com/unraid/api/commit/a0eb7ee3ec459dbe1992a7f85bf194da30395a74))
|
||||
* **deps:** update dependency focus-trap to v7.5.4 ([#788](https://github.com/unraid/api/issues/788)) ([fe000e8](https://github.com/unraid/api/commit/fe000e83825e82cac558d3277664a440e59c0e4a))
|
||||
* **deps:** update dependency graphql-ws to v5.15.0 ([#790](https://github.com/unraid/api/issues/790)) ([4773b13](https://github.com/unraid/api/commit/4773b132167d740d4c996efe22e0f1b99576fb9b))
|
||||
* display dropdown for pro key no connect installed ([#848](https://github.com/unraid/api/issues/848)) ([b559604](https://github.com/unraid/api/commit/b55960429895b46627f1cd3ed1683ee527e62944))
|
||||
* dropdown reboot link text ([#849](https://github.com/unraid/api/issues/849)) ([a8ed5e5](https://github.com/unraid/api/commit/a8ed5e5628bc71fb783a03c3db92d21805243738))
|
||||
* os updates rc to stable ([bf1bd88](https://github.com/unraid/api/commit/bf1bd887d60ac085bf4aeae90f11be3b45ee1182))
|
||||
* state connect values without connect installed ([e47de6c](https://github.com/unraid/api/commit/e47de6c2c5db7a2a1a9b24099feb02023b3a7bbf))
|
||||
* state php breaking with double quotes in server description ([c6e92aa](https://github.com/unraid/api/commit/c6e92aa3157c9fe9e7b83580881ebcc1cbd03658))
|
||||
* state php special chars for html attributes ([#853](https://github.com/unraid/api/issues/853)) ([dd4139c](https://github.com/unraid/api/commit/dd4139cf1a7ae5c6f9b00111c33ae124bb17e630))
|
||||
* unraid-api missing start command + var defaults ([ceb4c58](https://github.com/unraid/api/commit/ceb4c587d20c7527f2b36a3278c310b0e657bfba))
|
||||
* unraid-api.php $param1 fallback ([909c79c](https://github.com/unraid/api/commit/909c79c8c82500aea1a0d4d00766f788103c5fe3))
|
||||
|
||||
## [3.5.0](https://github.com/unraid/api/compare/v3.4.0...v3.5.0) (2024-02-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add manage account link to all versions of upc dropdown ([678e620](https://github.com/unraid/api/commit/678e620c1902a376b1866265711d5722b4119d8e))
|
||||
* add new staging url for connect website ([#841](https://github.com/unraid/api/issues/841)) ([4cfc07b](https://github.com/unraid/api/commit/4cfc07b6763dbb79b68cf01f7eaf7cf33370d4db))
|
||||
* also ship to cloudflare ([#844](https://github.com/unraid/api/issues/844)) ([41c4210](https://github.com/unraid/api/commit/41c42103685209592b272f81a877702da04d0915))
|
||||
* button add underline-hover-red style option ([f2fa5fa](https://github.com/unraid/api/commit/f2fa5fa49675ef461330be7b7eb3e3e4106983b0))
|
||||
* changelog modal ([2ddbacd](https://github.com/unraid/api/commit/2ddbacd137cc5748244c3d25ac91f82e64d77f99))
|
||||
* check update response modal ([39678f0](https://github.com/unraid/api/commit/39678f0bb0ddc5f87ea7f5ed80a0472100ea8b5d))
|
||||
* create WebguiCheckForUpdate endpoint ([41d546e](https://github.com/unraid/api/commit/41d546eea5fcf6593d7b5047274c074bb89c1802))
|
||||
* getOsReleaseBySha256 cached endpoint with keyfile header ([cd2413a](https://github.com/unraid/api/commit/cd2413abe8c5baab40e4e5974e08a5d18dce8e0d))
|
||||
* new check update buttons in dropdown ([ef5fcb9](https://github.com/unraid/api/commit/ef5fcb96a324143da864df803acaa0da1cd00eb7))
|
||||
* ship preview to different bucket ([#845](https://github.com/unraid/api/issues/845)) ([8e5d247](https://github.com/unraid/api/commit/8e5d247bca83d9e50977c9b16b212841ac9f70ad))
|
||||
* ship production to different bucket ([#846](https://github.com/unraid/api/issues/846)) ([63c0875](https://github.com/unraid/api/commit/63c08758c76425e007b1779bb2f77b75bc45896e))
|
||||
* unraidcheck callable from webgui with altUrl & json output ([ba8a67e](https://github.com/unraid/api/commit/ba8a67edfa043f442b11724227129f8d3f6cae0a))
|
||||
* update modals ([8ad7d8b](https://github.com/unraid/api/commit/8ad7d8be9437e0caa0409da8f7322050919fbbaa))
|
||||
* update os ignore release ([1955eb2](https://github.com/unraid/api/commit/1955eb23a3cdc30f0a67bc5950a047f83a860d99))
|
||||
* update os notifications enabled usage + link to enable & more options to account app ([5c82aff](https://github.com/unraid/api/commit/5c82aff80dc7e6d8f4b23e52af29abc2b8576424))
|
||||
* updateOs check response determines if update auth is required ([a9816d9](https://github.com/unraid/api/commit/a9816d9ad48ff80d87b5aeb236ff60c4979ad298))
|
||||
* updateOs store call local server-side endpoint & add modal support ([be48447](https://github.com/unraid/api/commit/be48447f943828af281095c5a092ac686e729030))
|
||||
* upgrade a ton of dependencies ([#842](https://github.com/unraid/api/issues/842)) ([94c1746](https://github.com/unraid/api/commit/94c174620c2347a3cf3d100404635f99a5b47287))
|
||||
* WebguiCheckForUpdate using server-side check ([590deb1](https://github.com/unraid/api/commit/590deb130c301d4004fecdc211270583806b5593))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* backport _var() PHP function to older versions of Unraid ([f53150e](https://github.com/unraid/api/commit/f53150e1fa33b3f45b66ad0dc5eaabc470564d45))
|
||||
* changlog relative links and external links ([a789e20](https://github.com/unraid/api/commit/a789e204ce7b966e6c935923626538ac344aeefe))
|
||||
* check update response modal expired key button styles ([92993e3](https://github.com/unraid/api/commit/92993e3e0b6240c83a6a64efedd8ddb3be3f9ef7))
|
||||
* **deps:** update dependency ws to v8.16.0 ([#815](https://github.com/unraid/api/issues/815)) ([212020e](https://github.com/unraid/api/commit/212020e78d4de0576137058a3374837b4a43e02d))
|
||||
* extraLinks when no updates available ([853a991](https://github.com/unraid/api/commit/853a9911e3fd7eec9bbc88468de78f87b448d477))
|
||||
* ignore release localStorage ([62c45ec](https://github.com/unraid/api/commit/62c45ec9d7c68498bbcfe933a5b63e4759c7129c))
|
||||
* lint ([83235f9](https://github.com/unraid/api/commit/83235f9db726f4582b9d353a66f2f5e8925b8e34))
|
||||
* lint unused value ([2c7e53b](https://github.com/unraid/api/commit/2c7e53bf67d1f214201624b39786bfb7de6aa520))
|
||||
* marked-base-url install ([416ba71](https://github.com/unraid/api/commit/416ba716aa750a094e8cd521a79f6deebcd37864))
|
||||
* missing translations ([faf17e4](https://github.com/unraid/api/commit/faf17e41e81c11443bc062d8ce35a33d9ae9ebbc))
|
||||
* regTm format after key install without page refresh ([f3ddb31](https://github.com/unraid/api/commit/f3ddb31f994de9192f7203698ecc5d7de673c6a3))
|
||||
* regTm format when already set ([5ad911f](https://github.com/unraid/api/commit/5ad911f8133daa60de53da738d41c6a59e2f02cc))
|
||||
* ServerUpdateOsResponse type ([78bdae8](https://github.com/unraid/api/commit/78bdae86c907142d3ee32d6715eaa8f5a974a1ed))
|
||||
* State Class usage in other files ([4ad7f53](https://github.com/unraid/api/commit/4ad7f53ec145b2e6d2895619523e90c1daa3f68f))
|
||||
* state data humanReadable switch fallthrus ([9144e39](https://github.com/unraid/api/commit/9144e39d39aa56af0ad897735d1a3545330920d0))
|
||||
* state php usage from cli ([46fd321](https://github.com/unraid/api/commit/46fd321707c14cd1f265ee806f673500d87132dd))
|
||||
* translations ([3fabd57](https://github.com/unraid/api/commit/3fabd5756674c06fa803729cf13d19c592d8d46a))
|
||||
* type issue with changlelog modal visibility ([e3c3f6b](https://github.com/unraid/api/commit/e3c3f6bf0f1882788291db17bd74865fefc3abf6))
|
||||
|
||||
## [3.4.0](https://github.com/unraid/api/compare/v3.3.0...v3.4.0) (2024-01-11)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
###########################################################
|
||||
# Development/Build Image
|
||||
###########################################################
|
||||
FROM node:18.17.1-bookworm-slim As development
|
||||
FROM node:18.19.1-bookworm-slim As development
|
||||
|
||||
# Install build tools and dependencies
|
||||
RUN apt-get update -y && apt-get install -y \
|
||||
|
||||
58
api/README.md
Normal file
58
api/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# @unraid/api
|
||||
|
||||
## Installation
|
||||
|
||||
Install the production plugin via the apps tab (search for "my servers") on Unraid 6.9.2 or later.
|
||||
|
||||
## CLI
|
||||
|
||||
If you're on a unraid v6.9.2 or later machine this should be available by running `unraid-api` in any directory.
|
||||
|
||||
```bash
|
||||
root@Devon:~# unraid-api
|
||||
|
||||
Unraid API
|
||||
|
||||
Thanks for using the official Unraid API
|
||||
|
||||
Usage:
|
||||
|
||||
$ unraid-api command <options>
|
||||
|
||||
Commands:
|
||||
|
||||
start/stop/restart/version/status/report/switch-env
|
||||
|
||||
Options:
|
||||
|
||||
-h, --help Prints this usage guide.
|
||||
-d, --debug Enabled debug mode.
|
||||
-p, --port string Set the graphql port.
|
||||
--environment production/staging/development Set the working environment.
|
||||
--log-level ALL/TRACE/DEBUG/INFO/WARN/ERROR/FATAL/MARK/OFF Set the log level.
|
||||
|
||||
Copyright © 2022 Lime Technology, Inc.
|
||||
|
||||
```
|
||||
|
||||
## Report
|
||||
To view the current status of the unraid-api and its connection to mothership, run:
|
||||
```
|
||||
unraid-api report
|
||||
```
|
||||
|
||||
To view verbose data (anonymized), run:
|
||||
```
|
||||
unraid-api report -v
|
||||
```
|
||||
|
||||
To view non-anonymized verbose data, run:
|
||||
```
|
||||
unraid-api report -vv
|
||||
```
|
||||
|
||||
## Secrets
|
||||
If you found this file you're likely a developer. If you'd like to know more about the API and when it's available please join [our discord](https://discord.unraid.net/).
|
||||
|
||||
## License
|
||||
Copyright 2019-2022 Lime Technology Inc. All rights reserved.
|
||||
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="3.3.0+eff31423"
|
||||
version="3.5.2+20f10951"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
[notifier]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="3.3.0+eff31423"
|
||||
version="3.5.2+20f10951"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
[notifier]
|
||||
@@ -16,9 +16,10 @@ regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
|
||||
idtoken=""
|
||||
accesstoken=""
|
||||
refreshtoken=""
|
||||
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
|
||||
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
|
||||
dynamicRemoteAccessType="DISABLED"
|
||||
[upc]
|
||||
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
|
||||
[connectionStatus]
|
||||
minigraph="ERROR_RETRYING"
|
||||
upnpStatus="Success: UPNP Lease Renewed [4/24/2024 5:04:54 PM] Public Port [59138] Local Port [443]"
|
||||
|
||||
229
api/docs/development.md
Normal file
229
api/docs/development.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Development
|
||||
|
||||
## Installation
|
||||
|
||||
Install the [production](https://unraid-dl.sfo2.digitaloceanspaces.com/unraid-api/dynamix.unraid.net.plg) or [staging](https://unraid-dl.sfo2.digitaloceanspaces.com/unraid-api/dynamix.unraid.net.staging.plg) plugin on Unraid 6.9.0-rc1 or later (6.9.2 or higher recommended).
|
||||
|
||||
## Connecting to the API
|
||||
|
||||
### HTTP
|
||||
|
||||
This can be accessed by default via `http://tower.local/graphql`.
|
||||
|
||||
See <https://graphql.org/learn/serving-over-http/#http-methods-headers-and-body>
|
||||
|
||||
### WS
|
||||
|
||||
If you're using the ApolloClient please see <https://github.com/apollographql/subscriptions-transport-ws#full-websocket-transport> otherwise see <https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md>
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Building in Docker
|
||||
|
||||
To get a development environment for testing start by running this docker command:
|
||||
|
||||
``docker compose run build-interactive``
|
||||
|
||||
which will give you an interactive shell inside of the newly build linux container.
|
||||
|
||||
To automatically build the plugin run the command below:
|
||||
|
||||
``docker compose run builder``
|
||||
|
||||
The builder command will build the plugin into deploy/release, and the interactive plugin lets you build the plugin or install node modules how you like.
|
||||
|
||||
## Logs
|
||||
|
||||
Logging can be configured via environment variables.
|
||||
|
||||
Log levels can be set when the api starts via `LOG_LEVEL=all/trace/debug/info/warn/error/fatal/mark/off`.
|
||||
|
||||
Additional detail for the log entry can be added with `LOG_CONTEXT=true` (warning, generates a lot of data).
|
||||
|
||||
By default, logs will be sent to syslog. Or you can set `LOG_TRANSPORT=file` to have logs saved in `/var/log/unraid-api/stdout.log`. Or enable debug mode to view logs inline.
|
||||
|
||||
Examples:
|
||||
|
||||
* `unraid-api start`
|
||||
* `LOG_LEVEL=debug unraid-api start --debug`
|
||||
* `LOG_LEVEL=trace LOG_CONTEXT=true LOG_TRANSPORT=file unraid-api start`
|
||||
|
||||
Log levels can be increased without restarting the api by issuing this command:
|
||||
|
||||
```
|
||||
kill -s SIGUSR2 `pidof unraid-api`
|
||||
```
|
||||
|
||||
and decreased via:
|
||||
|
||||
```
|
||||
kill -s SIGUSR1 `pidof unraid-api`
|
||||
```
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Viewing data sent to mothership
|
||||
|
||||
If the environment variable `LOG_MOTHERSHIP_MESSAGES=true` exists, any data the unraid-api sends to mothership will be saved in clear text here: `/var/log/unraid-api/relay-messages.log`
|
||||
|
||||
Examples:
|
||||
|
||||
* `LOG_MOTHERSHIP_MESSAGES=true unraid-api start`
|
||||
* `LOG_MOTHERSHIP_MESSAGES=true LOG_LEVEL=debug unraid-api start --debug`
|
||||
<br>
|
||||
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Debug mode
|
||||
|
||||
Debug mode can be enabled with the `-d` or `--debug` flag.
|
||||
This will enable the graphql playground and prevent the application starting as a daemon. Logs will be shown inline rather than saved to a file.
|
||||
|
||||
Examples:
|
||||
|
||||
* `unraid-api start --debug`
|
||||
* `LOG_LEVEL=debug unraid-api start --debug`
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Crash API On Demand
|
||||
|
||||
The `PLEASE_SEGFAULT_FOR_ME` env var can be to used to make the api crash after 30 seconds:
|
||||
|
||||
Examples:
|
||||
|
||||
* `PLEASE_SEGFAULT_FOR_ME=true LOG_LEVEL=debug unraid-api start --debug`
|
||||
* `PLEASE_SEGFAULT_FOR_ME=true unraid-api start`
|
||||
|
||||
The crash log will be stored here:
|
||||
|
||||
* `/var/log/unraid-api/crash.log`
|
||||
* `/var/log/unraid-api/crash.json`
|
||||
|
||||
`crash.json` just includes the most recent crash, while the reports get appended to `crash.log`.
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Switching between staging and production environments
|
||||
|
||||
1. Stop the api: `unraid-api stop`
|
||||
2. Switch environments: `unraid-api switch-env`
|
||||
3. Start the api: `unraid-api start`
|
||||
4. Confirm the environment: `unraid-api report`
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Playground
|
||||
|
||||
The playground can be access via `http://tower.local/graphql` while in debug mode.
|
||||
To get your API key open a terminal on your server and run `cat /boot/config/plugins/dynamix.my.servers/myservers.cfg | grep apikey=\"unraid | cut -d '"' -f2`. Add that API key in the "HTTP headers" panel of the playground.
|
||||
|
||||
```json
|
||||
{
|
||||
"x-api-key":"__REPLACE_ME_WITH_API_KEY__"
|
||||
}
|
||||
```
|
||||
|
||||
Next add the query you want to run and hit the play icon.
|
||||
|
||||
```gql
|
||||
query welcome {
|
||||
welcome {
|
||||
message
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should get something like this back.
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"welcome": {
|
||||
"message": "Welcome root to this Unraid 6.10.0 server"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Click the "Schema" and "Docs" button on the right side of the playground to learn more.
|
||||
|
||||
For exploring the schema visually I'd suggest using [Voyager](https://apis.guru/graphql-voyager/) (click Change Schema -> Introspection, then copy/paste the introspection query into the local Graph Playground, and copy/paste the results back into Voyager).
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Running this locally
|
||||
|
||||
```bash
|
||||
MOTHERSHIP_RELAY_WS_LINK=ws://localhost:8000 \ # Switch to local copy of mothership
|
||||
PATHS_UNRAID_DATA=$(pwd)/dev/data \ # Where we store plugin data (e.g. permissions.json)
|
||||
PATHS_STATES=$(pwd)/dev/states \ # Where .ini files live (e.g. vars.ini)
|
||||
PATHS_DYNAMIX_BASE=$(pwd)/dev/dynamix \ # Dynamix's data directory
|
||||
PATHS_DYNAMIX_CONFIG=$(pwd)/dev/dynamix/dynamix.cfg \ # Dynamix's config file
|
||||
PATHS_MY_SERVERS_CONFIG=$(pwd)/dev/Unraid.net/myservers.cfg \ # My servers config file
|
||||
PORT=8500 \ # What port unraid-api should start on (e.g. /var/run/unraid-api.sock or 8000)
|
||||
node dist/cli.js --debug # Enable debug logging
|
||||
```
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Create a new release
|
||||
|
||||
To create a new version run `npm run release` and then run **ONLY** the `git push` section of the commands it returns.
|
||||
To create a new prerelease run `npm run release -- --prerelease alpha`.
|
||||
|
||||
Pushing to this repo will cause an automatic "rolling" release to be built which can be accessed via the page for the associated Github action run.
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Using a custom version (e.g. testing a new release)
|
||||
|
||||
1. Install the staging or production plugin (links in the Installation section at the top of this file)
|
||||
2. Download or build the api tgz file you want
|
||||
|
||||
* Download from [the releases page](https://github.com/unraid/api/releases)
|
||||
* Build it on your local machine (``docker compose run builder``) and copy from the `deploy/release` folder
|
||||
|
||||
3. Copy the file to `/boot/config/plugins/dynamix.my.servers/unraid-api.tgz`.
|
||||
4. Install the new api: `/etc/rc.d/rc.unraid-api (install / _install)`
|
||||
|
||||
* `_install` will no start the plugin for you after running, so you can make sure you launch in dev mode
|
||||
* `install` will start the plugin after install
|
||||
5. Start the api: `unraid-api start`
|
||||
6. Confirm the version: `unraid-api report`
|
||||
|
||||
## Cloning Secrets from AWS
|
||||
|
||||
1. Go here to create security credentials for your user [S3 Creds](https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1&skipRegion=true#/security_credentials)
|
||||
2. Export your AWS secrets OR run `aws configure` to setup your environment
|
||||
|
||||
```sh
|
||||
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
||||
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||
export AWS_DEFAULT_REGION=us-east-1
|
||||
|
||||
```
|
||||
|
||||
3. Set variables for staging and production to the ARN of the secret you would like to clone:
|
||||
|
||||
* `STAGING_SECRET_ID`
|
||||
* `PRODUCTION_SECRET_ID`
|
||||
|
||||
4. Run `scripts/copy-env-from-aws.sh` to pull down the secrets into their respective files
|
||||
10054
api/package-lock.json
generated
10054
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
182
api/package.json
182
api/package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/api",
|
||||
"version": "3.4.0",
|
||||
"version": "3.8.1",
|
||||
"main": "dist/index.js",
|
||||
"bin": "dist/unraid-api.cjs",
|
||||
"type": "module",
|
||||
@@ -26,7 +26,7 @@
|
||||
"compile": "tsup --config ./tsup.config.ts",
|
||||
"bundle": "pkg . --public",
|
||||
"build": "npm run compile && npm run bundle",
|
||||
"build:docker": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm builder",
|
||||
"build:docker": "./scripts/dc.sh run --rm builder",
|
||||
"build-pkg": "./scripts/build.mjs",
|
||||
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.yml -r dotenv/config './.env.staging'",
|
||||
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen-esm --config codegen.yml --watch -r dotenv/config",
|
||||
@@ -34,8 +34,8 @@
|
||||
"tsc": "tsc --noEmit",
|
||||
"lint": "DEBUG=eslint:cli-engine eslint . --config .eslintrc.cjs",
|
||||
"lint:fix": "DEBUG=eslint:cli-engine eslint . --fix --config .eslintrc.cjs",
|
||||
"test:watch": "vitest --segfault-retry=3 --no-threads",
|
||||
"test": "vitest run --segfault-retry=3 --no-threads",
|
||||
"test:watch": "vitest --segfault-retry=3 --pool=forks",
|
||||
"test": "vitest run --segfault-retry=3 --pool=forks",
|
||||
"coverage": "vitest run --segfault-retry=3 --coverage",
|
||||
"patch:subscriptions-transport-ws": "node ./.scripts/patches/subscriptions-transport-ws.cjs",
|
||||
"release": "standard-version",
|
||||
@@ -59,21 +59,21 @@
|
||||
"unraid-api"
|
||||
],
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.12",
|
||||
"@apollo/server": "^4.6.0",
|
||||
"@apollo/client": "^3.10.4",
|
||||
"@apollo/server": "^4.10.4",
|
||||
"@as-integrations/fastify": "^2.1.1",
|
||||
"@graphql-codegen/client-preset": "^4.0.0",
|
||||
"@graphql-codegen/client-preset": "^4.2.5",
|
||||
"@graphql-tools/load-files": "^7.0.0",
|
||||
"@graphql-tools/merge": "^9.0.0",
|
||||
"@graphql-tools/schema": "^10.0.0",
|
||||
"@graphql-tools/utils": "^10.0.0",
|
||||
"@nestjs/apollo": "^12.0.11",
|
||||
"@nestjs/core": "^10.2.9",
|
||||
"@nestjs/graphql": "^12.0.11",
|
||||
"@nestjs/passport": "^10.0.2",
|
||||
"@nestjs/platform-fastify": "^10.2.9",
|
||||
"@nestjs/schedule": "^4.0.0",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@graphql-tools/merge": "^9.0.4",
|
||||
"@graphql-tools/schema": "^10.0.3",
|
||||
"@graphql-tools/utils": "^10.2.0",
|
||||
"@nestjs/apollo": "^12.1.0",
|
||||
"@nestjs/core": "^10.3.8",
|
||||
"@nestjs/graphql": "^12.1.1",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-fastify": "^10.3.8",
|
||||
"@nestjs/schedule": "^4.0.2",
|
||||
"@reduxjs/toolkit": "^2.2.4",
|
||||
"@reflet/cron": "^1.3.1",
|
||||
"@runonflux/nat-upnp": "^1.0.2",
|
||||
"accesscontrol": "^2.2.1",
|
||||
@@ -84,126 +84,126 @@
|
||||
"bytes": "^3.1.2",
|
||||
"cacheable-lookup": "^6.1.0",
|
||||
"catch-exit": "^1.2.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"chokidar": "^3.6.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"class-validator": "^0.14.1",
|
||||
"cli-table": "^0.3.11",
|
||||
"command-exists": "^1.2.9",
|
||||
"convert": "^4.10.0",
|
||||
"convert": "^4.14.1",
|
||||
"cors": "^2.8.5",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"docker-event-emitter": "^0.3.0",
|
||||
"dockerode": "^3.3.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"find-process": "^1.4.7",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-fields": "^2.0.3",
|
||||
"graphql-scalars": "^1.21.3",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"graphql-subscriptions": "^2.0.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"graphql-type-uuid": "^0.2.0",
|
||||
"graphql-ws": "^5.14.2",
|
||||
"graphql-ws": "^5.16.0",
|
||||
"htpasswd-js": "^1.0.2",
|
||||
"ini": "^4.1.0",
|
||||
"ip": "^1.1.8",
|
||||
"jose": "^4.14.2",
|
||||
"ini": "^4.1.2",
|
||||
"ip": "^2.0.1",
|
||||
"jose": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"multi-ini": "^2.2.0",
|
||||
"mustache": "^4.2.0",
|
||||
"nanobus": "^4.5.0",
|
||||
"nest-access-control": "^3.1.0",
|
||||
"nestjs-pino": "^3.5.0",
|
||||
"nestjs-pino": "^4.0.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-window-polyfill": "^1.0.2",
|
||||
"openid-client": "^5.4.0",
|
||||
"openid-client": "^5.6.5",
|
||||
"p-iteration": "^1.1.8",
|
||||
"p-retry": "^4.6.2",
|
||||
"passport-http-header-strategy": "^1.1.0",
|
||||
"pidusage": "^3.0.2",
|
||||
"pino": "^8.16.2",
|
||||
"pino-http": "^8.5.1",
|
||||
"pino-pretty": "^10.2.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"pino": "^9.1.0",
|
||||
"pino-http": "^9.0.0",
|
||||
"pino-pretty": "^11.0.0",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"request": "^2.88.2",
|
||||
"semver": "^7.4.0",
|
||||
"semver": "^7.6.2",
|
||||
"stoppable": "^1.1.0",
|
||||
"systeminformation": "^5.21.2",
|
||||
"ts-command-line-args": "^2.5.0",
|
||||
"uuid": "^9.0.0",
|
||||
"ws": "^8.13.0",
|
||||
"wtfnode": "^0.9.1",
|
||||
"systeminformation": "^5.22.9",
|
||||
"ts-command-line-args": "^2.5.1",
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "^8.17.0",
|
||||
"wtfnode": "^0.9.2",
|
||||
"xhr2": "^0.2.1",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "^7.21.0",
|
||||
"@graphql-codegen/add": "^5.0.0",
|
||||
"@graphql-codegen/cli": "^5.0.0",
|
||||
"@graphql-codegen/fragment-matcher": "^5.0.0",
|
||||
"@babel/runtime": "^7.24.5",
|
||||
"@graphql-codegen/add": "^5.0.2",
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/fragment-matcher": "^5.0.2",
|
||||
"@graphql-codegen/import-types-preset": "^3.0.0",
|
||||
"@graphql-codegen/typed-document-node": "^5.0.0",
|
||||
"@graphql-codegen/typescript": "^4.0.0",
|
||||
"@graphql-codegen/typescript-operations": "^4.0.0",
|
||||
"@graphql-codegen/typescript-resolvers": "4.0.1",
|
||||
"@graphql-codegen/typed-document-node": "^5.0.6",
|
||||
"@graphql-codegen/typescript": "^4.0.6",
|
||||
"@graphql-codegen/typescript-operations": "^4.2.0",
|
||||
"@graphql-codegen/typescript-resolvers": "4.0.6",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@nestjs/testing": "^10.2.10",
|
||||
"@swc/core": "^1.3.81",
|
||||
"@types/async-exit-hook": "^2.0.0",
|
||||
"@types/btoa": "^1.2.3",
|
||||
"@types/bytes": "^3.1.1",
|
||||
"@types/cli-table": "^0.3.1",
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@types/dockerode": "^3.3.16",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/graphql-fields": "^1.3.5",
|
||||
"@types/graphql-type-uuid": "^0.2.3",
|
||||
"@types/ini": "^1.3.31",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"@types/mustache": "^4.2.2",
|
||||
"@types/node": "^18.17.12",
|
||||
"@types/pidusage": "^2.0.2",
|
||||
"@types/pify": "^5.0.1",
|
||||
"@types/semver": "^7.3.13",
|
||||
"@types/sendmail": "^1.4.4",
|
||||
"@types/stoppable": "^1.1.1",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@types/wtfnode": "^0.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"@nestjs/testing": "^10.3.8",
|
||||
"@swc/core": "^1.5.7",
|
||||
"@types/async-exit-hook": "^2.0.2",
|
||||
"@types/btoa": "^1.2.5",
|
||||
"@types/bytes": "^3.1.4",
|
||||
"@types/cli-table": "^0.3.4",
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/dockerode": "^3.3.29",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/graphql-fields": "^1.3.9",
|
||||
"@types/graphql-type-uuid": "^0.2.6",
|
||||
"@types/ini": "^4.1.0",
|
||||
"@types/lodash": "^4.17.1",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/pidusage": "^2.0.5",
|
||||
"@types/pify": "^5.0.4",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/sendmail": "^1.4.7",
|
||||
"@types/stoppable": "^1.1.3",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/wtfnode": "^0.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||
"@typescript-eslint/parser": "^7.9.0",
|
||||
"@unraid/eslint-config": "github:unraid/eslint-config",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"@vitest/ui": "^0.34.0",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"camelcase-keys": "^8.0.2",
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^48.0.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^53.0.0",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"execa": "^7.1.1",
|
||||
"filter-obj": "^5.1.0",
|
||||
"got": "^13.0.0",
|
||||
"graphql-codegen-typescript-validation-schema": "^0.11.0",
|
||||
"got": "^13",
|
||||
"graphql-codegen-typescript-validation-schema": "^0.14.1",
|
||||
"ip-regex": "^5.0.0",
|
||||
"json-difference": "^1.9.1",
|
||||
"json-difference": "^1.16.1",
|
||||
"map-obj": "^5.0.2",
|
||||
"p-props": "^5.0.0",
|
||||
"path-exists": "^5.0.0",
|
||||
"path-type": "^5.0.0",
|
||||
"pkg": "^5.8.1",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"pretty-ms": "^8.0.0",
|
||||
"standard-version": "^9.5.0",
|
||||
"tsup": "^7.0.0",
|
||||
"typescript": "^4.9.4",
|
||||
"typesync": "^0.11.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.34.0",
|
||||
"zx": "^7.2.1"
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typesync": "^0.12.1",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"zx": "^7.2.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@vmngr/libvirt": "github:unraid/libvirt"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Pass all entered params after the docker-compose call
|
||||
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose -f docker-compose.yml "$@"
|
||||
# Pass all entered params after the docker compose call
|
||||
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose -f docker-compose.yml "$@"
|
||||
|
||||
@@ -36,7 +36,7 @@ test('Returns allowed origins', async () => {
|
||||
"https://google.com",
|
||||
"https://test.com",
|
||||
"https://connect.myunraid.net",
|
||||
"https://staging.connect.myunraid.net",
|
||||
"https://connect-staging.myunraid.net",
|
||||
"https://dev-my.myunraid.net:4000",
|
||||
]
|
||||
`);
|
||||
|
||||
@@ -54,7 +54,7 @@ test('it creates a MEMORY config with NO OPTIONAL values', () => {
|
||||
},
|
||||
"remote": {
|
||||
"accesstoken": "",
|
||||
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000",
|
||||
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
|
||||
"apikey": "",
|
||||
"avatar": "",
|
||||
"dynamicRemoteAccessType": "DISABLED",
|
||||
@@ -146,7 +146,7 @@ test('it creates a MEMORY config with OPTIONAL values', () => {
|
||||
"remote": {
|
||||
"2Fa": "yes",
|
||||
"accesstoken": "",
|
||||
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000",
|
||||
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
|
||||
"apikey": "",
|
||||
"avatar": "",
|
||||
"dynamicRemoteAccessType": "DISABLED",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { parse } from 'ini';
|
||||
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
|
||||
import { Serializer } from 'multi-ini';
|
||||
|
||||
test('MultiIni breaks when serializing an object with a boolean inside', async () => {
|
||||
const objectToSerialize = {
|
||||
root: {
|
||||
anonMode: false,
|
||||
},
|
||||
};
|
||||
const serializer = new Serializer({ keep_quotes: false });
|
||||
expect(serializer.serialize(objectToSerialize)).toMatchInlineSnapshot(`
|
||||
"[root]
|
||||
anonMode=false
|
||||
"
|
||||
`)
|
||||
});
|
||||
|
||||
test('MultiIni can safely serialize an object with a boolean inside', async () => {
|
||||
const objectToSerialize = {
|
||||
root: {
|
||||
anonMode: false,
|
||||
},
|
||||
};
|
||||
expect(safelySerializeObjectToIni(objectToSerialize)).toMatchInlineSnapshot(`
|
||||
"[root]
|
||||
anonMode="false"
|
||||
"
|
||||
`);
|
||||
const result = safelySerializeObjectToIni(objectToSerialize);
|
||||
expect(parse(result)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"root": {
|
||||
"anonMode": false,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
114
api/src/__test__/core/utils/misc/parse-config.test.ts
Normal file
114
api/src/__test__/core/utils/misc/parse-config.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { parseConfig } from '@app/core/utils/misc/parse-config';
|
||||
import { Parser as MultiIniParser } from 'multi-ini';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { parse } from 'ini';
|
||||
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
|
||||
|
||||
const iniTestData = `["root"]
|
||||
idx="0"
|
||||
name="root"
|
||||
desc="Console and webGui login account"
|
||||
passwd="yes"
|
||||
["xo"]
|
||||
idx="1"
|
||||
name="xo"
|
||||
desc=""
|
||||
passwd="yes"
|
||||
["test_user"]
|
||||
idx="2"
|
||||
name="test_user"
|
||||
desc=""
|
||||
passwd="no"`;
|
||||
|
||||
test('it loads a config from a passed in ini file successfully', () => {
|
||||
const res = parseConfig<any>({
|
||||
file: iniTestData,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
{
|
||||
"root": {
|
||||
"desc": "Console and webGui login account",
|
||||
"idx": "0",
|
||||
"name": "root",
|
||||
"passwd": "yes",
|
||||
},
|
||||
"testUser": {
|
||||
"desc": "",
|
||||
"idx": "2",
|
||||
"name": "test_user",
|
||||
"passwd": "no",
|
||||
},
|
||||
"xo": {
|
||||
"desc": "",
|
||||
"idx": "1",
|
||||
"name": "xo",
|
||||
"passwd": "yes",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(res?.root.desc).toEqual('Console and webGui login account');
|
||||
});
|
||||
|
||||
test('it loads a config from disk properly', () => {
|
||||
const path = './dev/states/var.ini';
|
||||
const res = parseConfig<any>({ filePath: path, type: 'ini' });
|
||||
expect(res.DOMAIN_SHORT).toEqual(undefined);
|
||||
expect(res.domainShort).toEqual('');
|
||||
expect(res.shareCount).toEqual('0');
|
||||
});
|
||||
|
||||
test('Confirm Multi-Ini Parser Still Broken', () => {
|
||||
const parser = new MultiIniParser();
|
||||
const res = parser.parse(iniTestData);
|
||||
expect(res).toMatchInlineSnapshot('{}');
|
||||
});
|
||||
|
||||
test('Combine Ini and Multi-Ini to read and then write a file with quotes', async () => {
|
||||
const parsedFile = parse(iniTestData);
|
||||
expect(parsedFile).toMatchInlineSnapshot(`
|
||||
{
|
||||
"root": {
|
||||
"desc": "Console and webGui login account",
|
||||
"idx": "0",
|
||||
"name": "root",
|
||||
"passwd": "yes",
|
||||
},
|
||||
"test_user": {
|
||||
"desc": "",
|
||||
"idx": "2",
|
||||
"name": "test_user",
|
||||
"passwd": "no",
|
||||
},
|
||||
"xo": {
|
||||
"desc": "",
|
||||
"idx": "1",
|
||||
"name": "xo",
|
||||
"passwd": "yes",
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
const ini = safelySerializeObjectToIni(parsedFile);
|
||||
await writeFile('/tmp/test.ini', ini);
|
||||
const file = await readFile('/tmp/test.ini', 'utf-8');
|
||||
expect(file).toMatchInlineSnapshot(`
|
||||
"[root]
|
||||
idx="0"
|
||||
name="root"
|
||||
desc="Console and webGui login account"
|
||||
passwd="yes"
|
||||
[xo]
|
||||
idx="1"
|
||||
name="xo"
|
||||
desc=""
|
||||
passwd="yes"
|
||||
[test_user]
|
||||
idx="2"
|
||||
name="test_user"
|
||||
desc=""
|
||||
passwd="no"
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { checkMothershipAuthentication } from "@app/graphql/resolvers/query/cloud/check-mothership-authentication";
|
||||
import { expect, test } from "vitest";
|
||||
import packageJson from '@app/../package.json'
|
||||
|
||||
test('It fails to authenticate with mothership with no credentials', async () => {
|
||||
await expect(checkMothershipAuthentication('BAD', 'BAD')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`);
|
||||
expect(packageJson.version).not.toBeNull();
|
||||
await expect(checkMothershipAuthentication(packageJson.version, 'BAD_API_KEY')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
|
||||
}, 15_000)
|
||||
188
api/src/__test__/graphql/resolvers/subscription/network.test.ts
Normal file
188
api/src/__test__/graphql/resolvers/subscription/network.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { type Nginx } from '../../../../core/types/states/nginx';
|
||||
import { getUrlForField, getUrlForServer, getServerIps, type NginxUrlFields } from '@app/graphql/resolvers/subscription/network';
|
||||
import { store } from '@app/store';
|
||||
import { loadStateFiles } from '@app/store/modules/emhttp';
|
||||
import { loadConfigFile } from '@app/store/modules/config';
|
||||
|
||||
test.each([
|
||||
[{ httpPort: 80, httpsPort: 443, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 123, httpsPort: 443, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 80, httpsPort: 12_345, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 212, httpsPort: 3_233, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 80, httpsPort: 443, url: 'https://BROKEN_URL' }],
|
||||
|
||||
])('getUrlForField', ({ httpPort, httpsPort, url }) => {
|
||||
const responseInsecure = getUrlForField({
|
||||
port: httpPort,
|
||||
url,
|
||||
});
|
||||
|
||||
const responseSecure = getUrlForField({
|
||||
portSsl: httpsPort,
|
||||
url,
|
||||
});
|
||||
if (httpPort === 80) {
|
||||
expect(responseInsecure.port).toBe('');
|
||||
} else {
|
||||
expect(responseInsecure.port).toBe(httpPort.toString());
|
||||
}
|
||||
|
||||
if (httpsPort === 443) {
|
||||
expect(responseSecure.port).toBe('');
|
||||
} else {
|
||||
expect(responseSecure.port).toBe(httpsPort.toString());
|
||||
}
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl disabled', () => {
|
||||
const result = getUrlForServer({ nginx: { lanIp: '192.168.1.1', sslEnabled: false, httpPort: 123, httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"http://192.168.1.1:123/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl yes', () => {
|
||||
const result = getUrlForServer({
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 123, httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://192.168.1.1:445/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl yes, port empty', () => {
|
||||
const result = getUrlForServer(
|
||||
{ nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://192.168.1.1/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl auto', () => {
|
||||
const getResult = async () => getUrlForServer({
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'auto', httpPort: 123, httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
void expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Cannot get IP Based URL for field: "lanIp" SSL mode auto]`);
|
||||
});
|
||||
|
||||
test('getUrlForServer - field does not exist, ssl disabled', () => {
|
||||
const getResult = async () => getUrlForServer(
|
||||
{
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: false, sslMode: 'no' } as const as Nginx,
|
||||
ports: {
|
||||
port: ':123', portSsl: ':445', defaultUrl: new URL('https://my-default-url.unraid.net'),
|
||||
},
|
||||
// @ts-expect-error Field doesn't exist
|
||||
field: 'idontexist',
|
||||
});
|
||||
void expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
|
||||
});
|
||||
|
||||
test('getUrlForServer - FQDN - field exists, port non-empty', () => {
|
||||
const result = getUrlForServer({
|
||||
nginx: { lanFqdn: 'my-fqdn.unraid.net', httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanFqdn',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net:445/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - FQDN - field exists, port empty', () => {
|
||||
const result = getUrlForServer({ nginx: { lanFqdn: 'my-fqdn.unraid.net', httpPort: 80, httpsPort: 443 } as const as Nginx,
|
||||
field: 'lanFqdn',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net/"');
|
||||
});
|
||||
|
||||
test.each([
|
||||
[{ nginx: { lanFqdn: 'my-fqdn.unraid.net', sslEnabled: false, sslMode: 'no', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'lanFqdn' as NginxUrlFields }],
|
||||
[{ nginx: { wanFqdn: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn' as NginxUrlFields }],
|
||||
[{ nginx: { wanFqdn6: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'auto', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn6' as NginxUrlFields }],
|
||||
|
||||
])('getUrlForServer - FQDN', ({ nginx, field }) => {
|
||||
const result = getUrlForServer({ nginx, field });
|
||||
expect(result.toString()).toBe('https://my-fqdn.unraid.net/');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field does not exist, ssl disabled', () => {
|
||||
const getResult = async () => getUrlForServer({ nginx:
|
||||
{ lanFqdn: 'my-fqdn.unraid.net' } as const as Nginx,
|
||||
ports: { portSsl: '', port: '', defaultUrl: new URL('https://my-default-url.unraid.net') },
|
||||
// @ts-expect-error Field doesn't exist
|
||||
field: 'idontexist' });
|
||||
void expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
|
||||
});
|
||||
|
||||
test('integration test, loading nginx ini and generating all URLs', async () => {
|
||||
await store.dispatch(loadStateFiles());
|
||||
await store.dispatch(loadConfigFile());
|
||||
|
||||
const urls = getServerIps();
|
||||
expect(urls.urls).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"ipv4": "https://tower.local:4443/",
|
||||
"ipv6": "https://tower.local:4443/",
|
||||
"name": "Default",
|
||||
"type": "DEFAULT",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://192.168.1.150:4443/",
|
||||
"name": "LAN IPv4",
|
||||
"type": "LAN",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://tower:4443/",
|
||||
"name": "LAN Name",
|
||||
"type": "MDNS",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://tower.local:4443/",
|
||||
"name": "LAN MDNS",
|
||||
"type": "MDNS",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443/",
|
||||
"name": "LAN FQDN",
|
||||
"type": "LAN",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443/",
|
||||
"name": "WAN FQDN",
|
||||
"type": "WAN",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-252-0-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 0",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-252-1-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 1",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-253-3-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 3",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-253-4-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 4",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-253-5-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 55",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(urls.errors).toMatchInlineSnapshot(`
|
||||
[
|
||||
[Error: IP URL Resolver: Could not resolve any access URL for field: "lanIp6", is FQDN?: false],
|
||||
[Error: IP URL Resolver: Could not resolve any access URL for field: "lanFqdn6", is FQDN?: true],
|
||||
[Error: No URL Provided],
|
||||
]
|
||||
`);
|
||||
});
|
||||
111
api/src/__test__/mothership/api-key/api-key-check-jobs.test.ts
Normal file
111
api/src/__test__/mothership/api-key/api-key-check-jobs.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types';
|
||||
import * as apiKeyCheckJobs from '@app/mothership/jobs/api-key-check-jobs';
|
||||
import * as apiKeyValidator from '@app/mothership/api-key/validate-api-key-with-keyserver';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { type RecursivePartial } from '@app/types/index';
|
||||
import { type RootState } from '@app/store/index';
|
||||
import { logoutUser } from '@app/store/modules/config';
|
||||
|
||||
describe('apiKeyCheckJob Tests', () => {
|
||||
it('API Check Job (with success)', async () => {
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer').mockResolvedValue(API_KEY_STATUS.API_KEY_VALID);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(true);
|
||||
|
||||
expect(validationSpy).toHaveBeenCalledOnce();
|
||||
|
||||
expect(dispatch).toHaveBeenLastCalledWith({
|
||||
payload: API_KEY_STATUS.API_KEY_VALID,
|
||||
type: 'apiKey/setApiKeyState',
|
||||
});
|
||||
});
|
||||
|
||||
it('API Check Job (with invalid length key)', async () => {
|
||||
// Setup state
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: 'too-short-key' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer').mockResolvedValue(API_KEY_STATUS.API_KEY_VALID);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(false);
|
||||
expect(dispatch).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
expect(validationSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('API Check Job (with a failure that throws an error - NETWORK_ERROR)', async () => {
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer')
|
||||
.mockResolvedValueOnce(API_KEY_STATUS.NETWORK_ERROR);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Keyserver Failure, must retry]`);
|
||||
|
||||
expect(validationSpy).toHaveBeenCalledOnce();
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: API_KEY_STATUS.NETWORK_ERROR,
|
||||
type: 'apiKey/setApiKeyState',
|
||||
});
|
||||
});
|
||||
|
||||
it('API Check Job (with a failure that throws an error - INVALID_RESPONSE)', async () => {
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer')
|
||||
.mockResolvedValueOnce(API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Keyserver Failure, must retry]`);
|
||||
|
||||
expect(validationSpy).toHaveBeenCalledOnce();
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE,
|
||||
type: 'apiKey/setApiKeyState',
|
||||
});
|
||||
}, 10_000);
|
||||
|
||||
it('API Check Job (with failure that results in a log out)', async () => {
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer')
|
||||
.mockResolvedValue(API_KEY_STATUS.API_KEY_INVALID);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(false);
|
||||
|
||||
expect(validationSpy).toHaveBeenCalledOnce();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith(expect.any(Function));
|
||||
}, 10_000);
|
||||
});
|
||||
@@ -20,6 +20,7 @@ test('Returns paths', async () => {
|
||||
"myservers-config",
|
||||
"myservers-config-states",
|
||||
"myservers-env",
|
||||
"myservers-keepalive",
|
||||
"keyfile-base",
|
||||
"machine-id",
|
||||
"log-base",
|
||||
|
||||
@@ -68,7 +68,7 @@ const getRemoteAccessUrlsForAllowedOrigins = (
|
||||
return [];
|
||||
};
|
||||
|
||||
const getExtraOrigins = (): string[] => {
|
||||
export const getExtraOrigins = (): string[] => {
|
||||
const { extraOrigins } = getters.config().api;
|
||||
if (extraOrigins) {
|
||||
return extraOrigins
|
||||
@@ -86,7 +86,7 @@ const getExtraOrigins = (): string[] => {
|
||||
|
||||
const getConnectOrigins = (): string[] => {
|
||||
const connectMain = 'https://connect.myunraid.net';
|
||||
const connectStaging = 'https://staging.connect.myunraid.net';
|
||||
const connectStaging = 'https://connect-staging.myunraid.net';
|
||||
const connectDev = 'https://dev-my.myunraid.net:4000';
|
||||
|
||||
return [connectMain, connectStaging, connectDev];
|
||||
|
||||
@@ -34,6 +34,7 @@ export const FIVE_MINUTES_MS = 5 * ONE_MINUTE;
|
||||
export const TEN_MINUTES_MS = 10 * ONE_MINUTE;
|
||||
export const THIRTY_MINUTES_MS = 30 * ONE_MINUTE;
|
||||
export const ONE_HOUR_MS = 60 * ONE_MINUTE;
|
||||
export const ONE_DAY_MS = ONE_HOUR_MS * 24;
|
||||
|
||||
// Seconds
|
||||
export const ONE_HOUR_SECS = 60 * 60;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as Types from '@app/graphql/generated/api/types';
|
||||
|
||||
import { z } from 'zod'
|
||||
import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, UnassignedDevice, Uptime, Usb, User, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
|
||||
import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
|
||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
|
||||
type Properties<T> = Required<{
|
||||
@@ -702,6 +702,15 @@ export function RelayResponseSchema(): z.ZodObject<Properties<RelayResponse>> {
|
||||
})
|
||||
}
|
||||
|
||||
export function RemoteAccessSchema(): z.ZodObject<Properties<RemoteAccess>> {
|
||||
return z.object({
|
||||
__typename: z.literal('RemoteAccess').optional(),
|
||||
accessType: WAN_ACCESS_TYPESchema,
|
||||
forwardType: WAN_FORWARD_TYPESchema.nullish(),
|
||||
port: z.number().nullish()
|
||||
})
|
||||
}
|
||||
|
||||
export function ServerSchema(): z.ZodObject<Properties<Server>> {
|
||||
return z.object({
|
||||
__typename: z.literal('Server').optional(),
|
||||
@@ -852,6 +861,15 @@ export function UserSchema(): z.ZodObject<Properties<User>> {
|
||||
})
|
||||
}
|
||||
|
||||
export function UserAccountSchema(): z.ZodObject<Properties<UserAccount>> {
|
||||
return z.object({
|
||||
description: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
roles: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function VarsSchema(): z.ZodObject<Properties<Vars>> {
|
||||
return z.object({
|
||||
__typename: z.literal('Vars').optional(),
|
||||
|
||||
@@ -874,6 +874,7 @@ export type Query = {
|
||||
dockerNetwork: DockerNetwork;
|
||||
/** All Docker networks */
|
||||
dockerNetworks: Array<Maybe<DockerNetwork>>;
|
||||
extraAllowedOrigins: Array<Scalars['String']['output']>;
|
||||
flash?: Maybe<Flash>;
|
||||
info?: Maybe<Info>;
|
||||
/** Current user account */
|
||||
@@ -883,6 +884,7 @@ export type Query = {
|
||||
owner?: Maybe<Owner>;
|
||||
parityHistory?: Maybe<Array<Maybe<ParityCheck>>>;
|
||||
registration?: Maybe<Registration>;
|
||||
remoteAccess: RemoteAccess;
|
||||
server?: Maybe<Server>;
|
||||
servers: Array<Server>;
|
||||
/** Network Shares */
|
||||
@@ -990,6 +992,13 @@ export type RelayResponse = {
|
||||
timeout?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type RemoteAccess = {
|
||||
__typename?: 'RemoteAccess';
|
||||
accessType: WAN_ACCESS_TYPE;
|
||||
forwardType?: Maybe<WAN_FORWARD_TYPE>;
|
||||
port?: Maybe<Scalars['Port']['output']>;
|
||||
};
|
||||
|
||||
export type Server = {
|
||||
__typename?: 'Server';
|
||||
apikey: Scalars['String']['output'];
|
||||
@@ -1646,6 +1655,7 @@ export type ResolversTypes = ResolversObject<{
|
||||
Registration: ResolverTypeWrapper<Registration>;
|
||||
RegistrationState: RegistrationState;
|
||||
RelayResponse: ResolverTypeWrapper<RelayResponse>;
|
||||
RemoteAccess: ResolverTypeWrapper<RemoteAccess>;
|
||||
Server: ResolverTypeWrapper<Server>;
|
||||
ServerStatus: ServerStatus;
|
||||
Service: ResolverTypeWrapper<Service>;
|
||||
@@ -1739,6 +1749,7 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
Query: {};
|
||||
Registration: Registration;
|
||||
RelayResponse: RelayResponse;
|
||||
RemoteAccess: RemoteAccess;
|
||||
Server: Server;
|
||||
Service: Service;
|
||||
SetupRemoteAccessInput: SetupRemoteAccessInput;
|
||||
@@ -2312,6 +2323,7 @@ export type QueryResolvers<ContextType = Context, ParentType extends ResolversPa
|
||||
dockerContainers?: Resolver<Array<ResolversTypes['DockerContainer']>, ParentType, ContextType, Partial<QuerydockerContainersArgs>>;
|
||||
dockerNetwork?: Resolver<ResolversTypes['DockerNetwork'], ParentType, ContextType, RequireFields<QuerydockerNetworkArgs, 'id'>>;
|
||||
dockerNetworks?: Resolver<Array<Maybe<ResolversTypes['DockerNetwork']>>, ParentType, ContextType, Partial<QuerydockerNetworksArgs>>;
|
||||
extraAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
flash?: Resolver<Maybe<ResolversTypes['Flash']>, ParentType, ContextType>;
|
||||
info?: Resolver<Maybe<ResolversTypes['Info']>, ParentType, ContextType>;
|
||||
me?: Resolver<Maybe<ResolversTypes['Me']>, ParentType, ContextType>;
|
||||
@@ -2320,6 +2332,7 @@ export type QueryResolvers<ContextType = Context, ParentType extends ResolversPa
|
||||
owner?: Resolver<Maybe<ResolversTypes['Owner']>, ParentType, ContextType>;
|
||||
parityHistory?: Resolver<Maybe<Array<Maybe<ResolversTypes['ParityCheck']>>>, ParentType, ContextType>;
|
||||
registration?: Resolver<Maybe<ResolversTypes['Registration']>, ParentType, ContextType>;
|
||||
remoteAccess?: Resolver<ResolversTypes['RemoteAccess'], ParentType, ContextType>;
|
||||
server?: Resolver<Maybe<ResolversTypes['Server']>, ParentType, ContextType>;
|
||||
servers?: Resolver<Array<ResolversTypes['Server']>, ParentType, ContextType>;
|
||||
shares?: Resolver<Maybe<Array<Maybe<ResolversTypes['Share']>>>, ParentType, ContextType>;
|
||||
@@ -2347,6 +2360,13 @@ export type RelayResponseResolvers<ContextType = Context, ParentType extends Res
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type RemoteAccessResolvers<ContextType = Context, ParentType extends ResolversParentTypes['RemoteAccess'] = ResolversParentTypes['RemoteAccess']> = ResolversObject<{
|
||||
accessType?: Resolver<ResolversTypes['WAN_ACCESS_TYPE'], ParentType, ContextType>;
|
||||
forwardType?: Resolver<Maybe<ResolversTypes['WAN_FORWARD_TYPE']>, ParentType, ContextType>;
|
||||
port?: Resolver<Maybe<ResolversTypes['Port']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type ServerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Server'] = ResolversParentTypes['Server']> = ResolversObject<{
|
||||
apikey?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
guid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
@@ -2755,6 +2775,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
|
||||
Query?: QueryResolvers<ContextType>;
|
||||
Registration?: RegistrationResolvers<ContextType>;
|
||||
RelayResponse?: RelayResponseResolvers<ContextType>;
|
||||
RemoteAccess?: RemoteAccessResolvers<ContextType>;
|
||||
Server?: ServerResolvers<ContextType>;
|
||||
Service?: ServiceResolvers<ContextType>;
|
||||
Share?: ShareResolvers<ContextType>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
|
||||
import type { FragmentDefinitionNode } from 'graphql';
|
||||
import type { Incremental } from './graphql.js';
|
||||
|
||||
@@ -27,12 +27,23 @@ enum WAN_FORWARD_TYPE {
|
||||
STATIC
|
||||
}
|
||||
|
||||
type RemoteAccess {
|
||||
accessType: WAN_ACCESS_TYPE!
|
||||
forwardType: WAN_FORWARD_TYPE
|
||||
port: Port
|
||||
}
|
||||
|
||||
input SetupRemoteAccessInput {
|
||||
accessType: WAN_ACCESS_TYPE!
|
||||
forwardType: WAN_FORWARD_TYPE
|
||||
port: Port
|
||||
}
|
||||
|
||||
type Query {
|
||||
remoteAccess: RemoteAccess!
|
||||
extraAllowedOrigins: [String!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
connectSignIn(input: ConnectSignInInput!): Boolean!
|
||||
connectSignOut: Boolean!
|
||||
|
||||
@@ -2,29 +2,54 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
import { join, resolve as resolvePath } from 'path';
|
||||
|
||||
const initialState = {
|
||||
core: __dirname,
|
||||
'unraid-api-base': '/usr/local/bin/unraid-api/' as const,
|
||||
'unraid-data': resolvePath(process.env.PATHS_UNRAID_DATA ?? '/boot/config/plugins/dynamix.my.servers/data/' as const),
|
||||
'docker-autostart': '/var/lib/docker/unraid-autostart' as const,
|
||||
'docker-socket': '/var/run/docker.sock' as const,
|
||||
'parity-checks': '/boot/config/parity-checks.log' as const,
|
||||
htpasswd: '/etc/nginx/htpasswd' as const,
|
||||
'emhttpd-socket': '/var/run/emhttpd.socket' as const,
|
||||
states: resolvePath(process.env.PATHS_STATES ?? '/usr/local/emhttp/state/' as const),
|
||||
'dynamix-base': resolvePath(process.env.PATHS_DYNAMIX_BASE ?? '/boot/config/plugins/dynamix/' as const),
|
||||
'dynamix-config': resolvePath(process.env.PATHS_DYNAMIX_CONFIG ?? '/boot/config/plugins/dynamix/dynamix.cfg' as const),
|
||||
'myservers-base': '/boot/config/plugins/dynamix.my.servers/' as const,
|
||||
'myservers-config': resolvePath(process.env.PATHS_MY_SERVERS_CONFIG ?? '/boot/config/plugins/dynamix.my.servers/myservers.cfg' as const),
|
||||
'myservers-config-states': join(resolvePath(process.env.PATHS_STATES ?? '/usr/local/emhttp/state/' as const), 'myservers.cfg' as const),
|
||||
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env' as const,
|
||||
'keyfile-base': resolvePath(process.env.PATHS_KEYFILE_BASE ?? '/boot/config' as const),
|
||||
'machine-id': resolvePath(process.env.PATHS_MACHINE_ID ?? '/var/lib/dbus/machine-id' as const),
|
||||
'log-base': resolvePath('/var/log/unraid-api/' as const),
|
||||
'var-run': '/var/run' as const,
|
||||
core: __dirname,
|
||||
'unraid-api-base': '/usr/local/bin/unraid-api/' as const,
|
||||
'unraid-data': resolvePath(
|
||||
process.env.PATHS_UNRAID_DATA ??
|
||||
('/boot/config/plugins/dynamix.my.servers/data/' as const)
|
||||
),
|
||||
'docker-autostart': '/var/lib/docker/unraid-autostart' as const,
|
||||
'docker-socket': '/var/run/docker.sock' as const,
|
||||
'parity-checks': '/boot/config/parity-checks.log' as const,
|
||||
htpasswd: '/etc/nginx/htpasswd' as const,
|
||||
'emhttpd-socket': '/var/run/emhttpd.socket' as const,
|
||||
states: resolvePath(
|
||||
process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)
|
||||
),
|
||||
'dynamix-base': resolvePath(
|
||||
process.env.PATHS_DYNAMIX_BASE ??
|
||||
('/boot/config/plugins/dynamix/' as const)
|
||||
),
|
||||
'dynamix-config': resolvePath(
|
||||
process.env.PATHS_DYNAMIX_CONFIG ??
|
||||
('/boot/config/plugins/dynamix/dynamix.cfg' as const)
|
||||
),
|
||||
'myservers-base': '/boot/config/plugins/dynamix.my.servers/' as const,
|
||||
'myservers-config': resolvePath(
|
||||
process.env.PATHS_MY_SERVERS_CONFIG ??
|
||||
('/boot/config/plugins/dynamix.my.servers/myservers.cfg' as const)
|
||||
),
|
||||
'myservers-config-states': join(
|
||||
resolvePath(
|
||||
process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)
|
||||
),
|
||||
'myservers.cfg' as const
|
||||
),
|
||||
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env' as const,
|
||||
'myservers-keepalive':
|
||||
process.env.PATHS_MY_SERVERS_FB ?? ('/boot/config/plugins/dynamix.my.servers/fb_keepalive' as const),
|
||||
'keyfile-base': resolvePath(
|
||||
process.env.PATHS_KEYFILE_BASE ?? ('/boot/config' as const)
|
||||
),
|
||||
'machine-id': resolvePath(
|
||||
process.env.PATHS_MACHINE_ID ?? ('/var/lib/dbus/machine-id' as const)
|
||||
),
|
||||
'log-base': resolvePath('/var/log/unraid-api/' as const),
|
||||
'var-run': '/var/run' as const,
|
||||
};
|
||||
|
||||
export const paths = createSlice({
|
||||
name: 'paths',
|
||||
initialState,
|
||||
reducers: {},
|
||||
name: 'paths',
|
||||
initialState,
|
||||
reducers: {},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { apiLogger } from '@app/core/log';
|
||||
import { BYPASS_PERMISSION_CHECKS } from '@app/environment';
|
||||
import { ServerHeaderStrategy } from '@app/unraid-api/auth/header.strategy';
|
||||
import { IS_PUBLIC_KEY } from '@app/unraid-api/auth/public.decorator';
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LogCleanupService } from '@app/unraid-api/cron/log-cleanup.service';
|
||||
import { WriteFlashFileService } from '@app/unraid-api/cron/write-flash-file.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
@Module({
|
||||
imports: [ScheduleModule.forRoot()],
|
||||
providers: [LogCleanupService],
|
||||
providers: [LogCleanupService, WriteFlashFileService],
|
||||
})
|
||||
export class CronModule {}
|
||||
|
||||
55
api/src/unraid-api/cron/write-flash-file.service.ts
Normal file
55
api/src/unraid-api/cron/write-flash-file.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ONE_DAY_MS, THIRTY_MINUTES_MS } from '@app/consts';
|
||||
import { sleep } from '@app/core/utils/misc/sleep';
|
||||
import { convertToFuzzyTime } from '@app/mothership/utils/convert-to-fuzzy-time';
|
||||
import { getters } from '@app/store/index';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
|
||||
@Injectable()
|
||||
export class WriteFlashFileService {
|
||||
constructor() {}
|
||||
private readonly logger = new Logger(WriteFlashFileService.name);
|
||||
private fileLocation = getters.paths()['myservers-keepalive'];
|
||||
public randomizeWriteTime = true;
|
||||
public writeNewTimestamp = async (): Promise<number> => {
|
||||
const wait = this.randomizeWriteTime
|
||||
? convertToFuzzyTime(0, THIRTY_MINUTES_MS)
|
||||
: 0;
|
||||
await sleep(wait);
|
||||
const newDate = new Date();
|
||||
try {
|
||||
await writeFile(this.fileLocation, newDate.toISOString());
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
return newDate.getTime();
|
||||
};
|
||||
|
||||
public getOrCreateTimestamp = async (): Promise<number> => {
|
||||
try {
|
||||
const file = (
|
||||
await readFile(this.fileLocation, 'utf-8')
|
||||
).toString();
|
||||
return Date.parse(file);
|
||||
} catch (error) {
|
||||
return await this.writeNewTimestamp();
|
||||
}
|
||||
};
|
||||
|
||||
@Cron('0 * * * *')
|
||||
async handleCron() {
|
||||
try {
|
||||
const currentDate = new Date().getTime();
|
||||
const prevDate = await this.getOrCreateTimestamp();
|
||||
if (currentDate - prevDate > ONE_DAY_MS * 7) {
|
||||
// Write new timestamp
|
||||
await this.writeNewTimestamp();
|
||||
}
|
||||
} catch (error) {
|
||||
// File does not exist, write it
|
||||
await this.writeNewTimestamp();
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
api/src/unraid-api/cron/write-flash-file.spec.ts
Normal file
43
api/src/unraid-api/cron/write-flash-file.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { WriteFlashFileService } from './write-flash-file.service';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { getters } from '@app/store/index';
|
||||
|
||||
describe('WriteFlashFileService', () => {
|
||||
let service: WriteFlashFileService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [WriteFlashFileService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<WriteFlashFileService>(WriteFlashFileService);
|
||||
service.randomizeWriteTime = false;
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should write and update the file when called', async () => {
|
||||
const timestamp = await service.writeNewTimestamp();
|
||||
expect(timestamp).toBeGreaterThan(0);
|
||||
|
||||
const file = readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString();
|
||||
expect(file).toBe(new Date(timestamp).toISOString(), 'file contents match the returned timestamp');
|
||||
|
||||
// Now make the file very old
|
||||
writeFileSync(getters.paths()['myservers-keepalive'], '2021-01-01T00:00:00.000Z');
|
||||
expect(readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString()).toBe('2021-01-01T00:00:00.000Z', 'file was updated');
|
||||
await service.handleCron();
|
||||
expect(readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString()).not.toBe('2021-01-01T00:00:00.000Z', 'file was updated');
|
||||
|
||||
// Now make the file kind of old (one day )
|
||||
writeFileSync(getters.paths()['myservers-keepalive'], new Date(Date.now() - (1_000 * 60 * 60 * 24)).toISOString());
|
||||
const now = Date.now();
|
||||
await service.handleCron();
|
||||
const contents = readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString();
|
||||
expect(new Date(contents).getTime() + (1_000 * 60 * 60 * 12)).toBeLessThan(new Date(now).getTime(), 'file was updated but is still older than today');
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { ArrayResolver } from './array.resolver';
|
||||
|
||||
describe('ArrayResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { CloudResolver } from './cloud.resolver';
|
||||
|
||||
describe('CloudResolver', () => {
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { getAllowedOrigins } from '@app/common/allowed-origins';
|
||||
import {
|
||||
getAllowedOrigins,
|
||||
getExtraOrigins,
|
||||
} from '@app/common/allowed-origins';
|
||||
import {
|
||||
WAN_ACCESS_TYPE,
|
||||
WAN_FORWARD_TYPE,
|
||||
type ConnectSignInInput,
|
||||
type SetupRemoteAccessInput,
|
||||
} from '@app/graphql/generated/api/types';
|
||||
import type { Cloud } from '@app/graphql/generated/api/types';
|
||||
import type { Cloud, RemoteAccess } from '@app/graphql/generated/api/types';
|
||||
|
||||
import { connectSignIn } from '@app/graphql/resolvers/mutation/connect/connect-sign-in';
|
||||
import { checkApi } from '@app/graphql/resolvers/query/cloud/check-api';
|
||||
import { checkCloud } from '@app/graphql/resolvers/query/cloud/check-cloud';
|
||||
import { checkMinigraphql } from '@app/graphql/resolvers/query/cloud/check-minigraphql';
|
||||
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
|
||||
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access';
|
||||
import { store } from '@app/store/index';
|
||||
import { getters, store } from '@app/store/index';
|
||||
import { logoutUser } from '@app/store/modules/config';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { UseRoles } from 'nest-access-control';
|
||||
@@ -45,6 +52,44 @@ export class CloudResolver {
|
||||
};
|
||||
}
|
||||
|
||||
@Query()
|
||||
@UseRoles({
|
||||
resource: 'connect',
|
||||
action: 'read',
|
||||
possession: 'own',
|
||||
})
|
||||
public async remoteAccess(): Promise<RemoteAccess> {
|
||||
const hasWanAccess = getters.config().remote.wanaccess === 'yes';
|
||||
const dynamicRemoteAccessSettings: RemoteAccess = {
|
||||
accessType: hasWanAccess
|
||||
? getters.config().remote.dynamicRemoteAccessType !==
|
||||
DynamicRemoteAccessType.DISABLED
|
||||
? WAN_ACCESS_TYPE.DYNAMIC
|
||||
: WAN_ACCESS_TYPE.ALWAYS
|
||||
: WAN_ACCESS_TYPE.DISABLED,
|
||||
forwardType: getters.config().remote.upnpEnabled
|
||||
? WAN_FORWARD_TYPE.UPNP
|
||||
: WAN_FORWARD_TYPE.STATIC,
|
||||
port: getters.config().remote.wanport
|
||||
? Number(getters.config().remote.wanport)
|
||||
: null,
|
||||
};
|
||||
|
||||
return dynamicRemoteAccessSettings;
|
||||
}
|
||||
|
||||
@Query()
|
||||
@UseRoles({
|
||||
resource: 'connect',
|
||||
action: 'read',
|
||||
possession: 'own',
|
||||
})
|
||||
public async extraAllowedOrigins(): Promise<Array<string>> {
|
||||
const extraOrigins = getExtraOrigins();
|
||||
|
||||
return extraOrigins;
|
||||
}
|
||||
|
||||
@Mutation()
|
||||
@UseRoles({
|
||||
resource: 'connect',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { ConfigResolver } from './config.resolver';
|
||||
|
||||
describe('ConfigResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { DisksResolver } from './disks.resolver';
|
||||
|
||||
describe('DisksResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { DisplayResolver } from './display.resolver';
|
||||
|
||||
describe('DisplayResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { DockerContainersResolver } from './docker-containers.resolver';
|
||||
|
||||
describe('DockerContainersResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { FlashResolver } from './flash.resolver';
|
||||
|
||||
describe('FlashResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { InfoResolver } from './info.resolver';
|
||||
|
||||
describe('InfoResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { NotificationsResolver } from './notifications.resolver';
|
||||
|
||||
describe('NotificationsResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { OnlineResolver } from './online.resolver';
|
||||
|
||||
describe('OnlineResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { OwnerResolver } from './owner.resolver';
|
||||
|
||||
describe('OwnerResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { RegistrationResolver } from './registration.resolver';
|
||||
|
||||
describe('RegistrationResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { VarsResolver } from './vars.resolver';
|
||||
|
||||
describe('VarsResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { VmsResolver } from './vms.resolver';
|
||||
|
||||
describe('VmsResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { RestService } from './rest.service';
|
||||
|
||||
describe('RestService', () => {
|
||||
|
||||
@@ -7,7 +7,6 @@ export default defineConfig(() => {
|
||||
// Manually set NODE_ENV to make sure we always run tests in test mode
|
||||
process.env.NODE_ENV = 'test';
|
||||
return {
|
||||
|
||||
plugins: [tsconfigPaths()],
|
||||
test: {
|
||||
globals: true,
|
||||
|
||||
@@ -161,11 +161,8 @@ exit 0
|
||||
<?
|
||||
$msini = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
|
||||
|
||||
# for convenience, scan myservers.cfg for deleteOnUninstall="no" and if that exists,
|
||||
# then skip the rest of the cleanup.
|
||||
$deleteOnUninstall = ($msini === false || empty($msini['plugin']['deleteOnUninstall']) || $msini['plugin']['deleteOnUninstall'] == 'yes');
|
||||
|
||||
if (!$deleteOnUninstall) {
|
||||
# if no_delete_on_uninstall exists on flash drive then skip the rest of the cleanup (useful when switching between staging and production)
|
||||
if (file_exists("/boot/config/plugins/dynamix.my.servers/no_delete_on_uninstall")) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
@@ -313,21 +310,31 @@ if [ -e /etc/rc.d/rc.unraid-api ]; then
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/Registration.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/include/Wrappers.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/include/ShowChanges.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/showchanges && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers1.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/sbin/upgradepkg && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components && [[ -d "$DIR-" ]] && mv -f "$DIR-" "$DIR"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
||||
# certain instances where the directory is not present and others where it is, ensure we delete it before we restore it
|
||||
if [[ -d "$DIR" ]]; then
|
||||
rm -rf "$DIR"
|
||||
fi
|
||||
if [[ -d "$DIR-" ]]; then
|
||||
mv -f "$DIR-" "$DIR"
|
||||
fi
|
||||
# delete plugin files from flash drive and OS
|
||||
rm -f /boot/config/plugins/dynamix.my.servers/.gitignore
|
||||
rm -f /etc/rc.d/rc.unraid-api
|
||||
@@ -405,22 +412,53 @@ echo
|
||||
|
||||
# NOTE: any 'exit 1' after this point will result in a broken install
|
||||
|
||||
# Preserve in case plugin is removed
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/Registration.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers1.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/sbin/upgradepkg && [[ -f "$FILE" ]] && cp -f "$FILE" "$FILE-"
|
||||
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components && [[ -d "$DIR" ]] && mv -f "$DIR" "$DIR-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
|
||||
# Loop through the array of preserveFilesDirs and perform actions
|
||||
# string param format
|
||||
# "{move|copy|move_dir}:{path}:{preventDowngrade|skip}"
|
||||
# move: move the file to a backup file
|
||||
# copy: copy the file to a backup file
|
||||
# move_dir: move the directory to a backup directory
|
||||
# preventDowngrade: during plg install, if the file exists, do not overwrite it if the plg manifest version is less than the installed webgui version
|
||||
# skip: do not perform any action if there is a manifest version difference
|
||||
preserveFilesDirs=(
|
||||
"move:/usr/local/emhttp/plugins/dynamix/Registration.page:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page:skip"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page:skip"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers1.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php:preventDowngrade"
|
||||
"copy:/sbin/upgradepkg:skip"
|
||||
"move_dir:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components:move_dir:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php:preventDowngrade"
|
||||
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php:preventDowngrade"
|
||||
)
|
||||
|
||||
preserveAction() {
|
||||
local action="$1"
|
||||
local path="$2"
|
||||
|
||||
if [[ "$action" == "move" ]]; then
|
||||
[[ -f "$path" ]] && mv -f "$path" "$path-"
|
||||
elif [[ "$action" == "copy" ]]; then
|
||||
[[ -f "$path" ]] && cp -f "$path" "$path-"
|
||||
elif [[ "$action" == "move_dir" ]]; then
|
||||
[[ -d "$path" ]] && mv -f "$path" "$path-"
|
||||
fi
|
||||
}
|
||||
|
||||
# Loop through the array of preserveFilesDirs and perform actions
|
||||
for obj in "${preserveFilesDirs[@]}"
|
||||
do
|
||||
IFS=':' read -r action path preventType <<< "$obj"
|
||||
preserveAction "$action" "$path" "$preventType"
|
||||
done
|
||||
|
||||
# patch DefaultPageLayout.php
|
||||
# search text: <?=_('Version')?>: <?=_var($var,'version','?')?><?=$notes?>
|
||||
@@ -499,6 +537,103 @@ if test -f "${FILE}" && grep -q "top.Shadowbox" "${FILE}" &>/dev/null; then
|
||||
sed -i 's/top.Shadowbox/parent.Shadowbox/gm' "${FILE}"
|
||||
fi
|
||||
|
||||
# ensure _var() is defined, brings older versions of Unraid in sync with 6.12.0
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/include/Wrappers.php
|
||||
if test -f "${FILE}" && ! grep -q "function _var" "${FILE}" &>/dev/null; then
|
||||
ADDTEXT1=$(
|
||||
cat <<'END_HEREDOC'
|
||||
// backported by Unraid Connect
|
||||
function _var(&$name, $key=null, $default='') {
|
||||
return is_null($key) ? ($name ?? $default) : ($name[$key] ?? $default);
|
||||
}
|
||||
END_HEREDOC
|
||||
)
|
||||
fi
|
||||
# ensure my_logger() is defined, brings older versions of Unraid in sync with 6.13.0
|
||||
if test -f "${FILE}" && ! grep -q "function my_logger" "${FILE}" &>/dev/null; then
|
||||
ADDTEXT2=$(
|
||||
cat <<'END_HEREDOC'
|
||||
// backported by Unraid Connect
|
||||
// ensure params passed to logger are properly escaped
|
||||
function my_logger($message, $logger='webgui') {
|
||||
exec('logger -t '.escapeshellarg($logger).' -- '.escapeshellarg($message));
|
||||
}
|
||||
END_HEREDOC
|
||||
)
|
||||
fi
|
||||
# ensure http_get_contents() is defined, brings older versions of Unraid in sync with 6.13.0
|
||||
if test -f "${FILE}" && ! grep -q "function http_get_contents" "${FILE}" &>/dev/null; then
|
||||
ADDTEXT3=$(
|
||||
cat <<'END_HEREDOC'
|
||||
// backported by Unraid Connect
|
||||
// Original PHP code by Chirp Internet: www.chirpinternet.eu
|
||||
// Please acknowledge use of this code by including this header.
|
||||
// https://www.the-art-of-web.com/php/http-get-contents/
|
||||
// Modified for Unraid
|
||||
/**
|
||||
* Fetches URL and returns content
|
||||
* @param string $url The URL to fetch
|
||||
* @param array $opts Array of options to pass to curl_setopt()
|
||||
* @param array $getinfo Empty array passed by reference, will contain results of curl_getinfo and curl_error
|
||||
* @return string|false $out The fetched content
|
||||
*/
|
||||
function http_get_contents(string $url, array $opts = [], array &$getinfo = NULL) {
|
||||
$ch = curl_init();
|
||||
if(isset($getinfo)) {
|
||||
curl_setopt($ch, CURLINFO_HEADER_OUT, TRUE);
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 45);
|
||||
curl_setopt($ch, CURLOPT_ENCODING, "");
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_REFERER, "");
|
||||
curl_setopt($ch, CURLOPT_FAILONERROR, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Unraid');
|
||||
if(is_array($opts) && $opts) {
|
||||
foreach($opts as $key => $val) {
|
||||
curl_setopt($ch, $key, $val);
|
||||
}
|
||||
}
|
||||
$out = curl_exec($ch);
|
||||
if (curl_errno($ch) == 23) {
|
||||
// error 23 detected, try CURLOPT_ENCODING = "deflate"
|
||||
curl_setopt($ch, CURLOPT_ENCODING, "deflate");
|
||||
$out = curl_exec($ch);
|
||||
}
|
||||
if (isset($getinfo)) {
|
||||
$getinfo = curl_getinfo($ch);
|
||||
}
|
||||
if ($errno = curl_errno($ch)) {
|
||||
$msg = "Curl error $errno: " . (curl_error($ch) ?: curl_strerror($errno)) . ". Requested url: '$url'";
|
||||
if(isset($getinfo)) {
|
||||
$getinfo['error'] = $msg;
|
||||
}
|
||||
my_logger($msg, "http_get_contents");
|
||||
}
|
||||
curl_close($ch);
|
||||
return $out;
|
||||
}
|
||||
END_HEREDOC
|
||||
)
|
||||
fi
|
||||
if [[ -n "${ADDTEXT1}" || -n "${ADDTEXT2}" || -n "${ADDTEXT3}" ]]; then
|
||||
TMP="$FILE.$RANDOM"
|
||||
cp -f "$FILE" "$TMP"
|
||||
cp -f "$FILE" "$FILE-"
|
||||
# delete last line of the file if it contains `?>`
|
||||
if test $( tail -n 1 "${TMP}" ) = '?>' ; then
|
||||
sed -i '$ d' "${TMP}"
|
||||
fi
|
||||
[[ -n "${ADDTEXT1}" ]] && echo "${ADDTEXT1}" >>"${TMP}"
|
||||
[[ -n "${ADDTEXT2}" ]] && echo "${ADDTEXT2}" >>"${TMP}"
|
||||
[[ -n "${ADDTEXT3}" ]] && echo "${ADDTEXT3}" >>"${TMP}"
|
||||
echo "?>" >>"${TMP}"
|
||||
mv "${TMP}" "${FILE}"
|
||||
fi
|
||||
|
||||
# install the main txz
|
||||
upgradepkg --install-new --reinstall "${MAINTXZ}"
|
||||
|
||||
@@ -566,7 +701,7 @@ if [ "${PLGTYPE}" = "staging" ]; then
|
||||
CHANGED=yes
|
||||
[[ ! -f "$FILE-" ]] && cp "$FILE" "$FILE-"
|
||||
OLD="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect.myunraid.net/\";"
|
||||
NEW="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect.myunraid.net/ https://dev-my.myunraid.net:4000/\";"
|
||||
NEW="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect-staging.myunraid.net https://connect.myunraid.net/ https://dev-my.myunraid.net:4000/\";"
|
||||
sed -i "s#${OLD}#${NEW}#" "${FILE}"
|
||||
fi
|
||||
fi
|
||||
@@ -590,6 +725,54 @@ if [[ "${CHANGED}" == "yes" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Prevent web component file downgrade if the webgui version is newer than the plugin version
|
||||
# Function to extract "ts" value from JSON file
|
||||
extract_ts() {
|
||||
local filepath="$1"
|
||||
local ts_value=null
|
||||
ts_value=$(jq -r '.ts' "$filepath" 2>/dev/null)
|
||||
echo "$ts_value"
|
||||
}
|
||||
|
||||
preventDowngradeAction() {
|
||||
local action="$1"
|
||||
local path="$2"
|
||||
local preventType="$3" # preventDowngrade or skip
|
||||
|
||||
# if skip, do nothing
|
||||
if [[ "$preventType" == "skip" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# restore the "backup" but keep the original backup for the uninstall plg script
|
||||
# otherwise, the uninstall script will NOT be able to restore the original file
|
||||
if [[ "$action" == "move" || "$action" == "copy" ]]; then
|
||||
[[ -f "$path-" ]] && cp -f "$path-" "$path"
|
||||
elif [[ "$action" == "move_dir" ]]; then
|
||||
# if directory exists rm the original and copy the backup
|
||||
# glob expansion via "$path-/"* …yes the * is necessary as we want to copy the contents of the directory
|
||||
[[ -d "$path-" ]] && rm -rf "$path" && mkdir "$path" && cp -rf "$path-/"* "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract "ts" values from both files
|
||||
plgWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components"
|
||||
backupWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components-"
|
||||
|
||||
plgManifestTs=$(extract_ts "$plgWebComponentPath/manifest.json")
|
||||
webguiManifestTs=$(extract_ts "$backupWebComponentPath/manifest.json")
|
||||
|
||||
# Compare the "ts" values and return the file path of the higher value
|
||||
if [[ "$webguiManifestTs" -gt "$plgManifestTs" ]]; then
|
||||
# Loop through the array of preserveFilesDirs and perform actions
|
||||
for obj in "${preserveFilesDirs[@]}"
|
||||
do
|
||||
IFS=':' read -r action path preventType <<< "$obj"
|
||||
preventDowngradeAction "$action" "$path" "$preventType"
|
||||
done
|
||||
echo "♻️ Reverted to stock web component files"
|
||||
fi
|
||||
|
||||
# start background process to install/start the api and flash backup
|
||||
echo
|
||||
if [ -f /var/local/emhttp/var.ini ]; then
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Arguments
|
||||
# $1: SSH server name
|
||||
# $2: {--wc-deploy|--wc-build|--wc-skip} / deploy or build web components w/o prompt
|
||||
|
||||
# Path to store the last used server name
|
||||
state_file="$HOME/.deploy_state"
|
||||
|
||||
@@ -38,6 +42,30 @@ echo "$rsync_command"
|
||||
eval "$rsync_command"
|
||||
exit_code=$?
|
||||
|
||||
# if $2 is --wc-deploy, deploy the web components without prompting
|
||||
if [ "$2" = "--wc-deploy" ]; then
|
||||
deploy="yes"
|
||||
elif [ "$2" = "--wc-build" ]; then
|
||||
deploy="build"
|
||||
elif [ "$2" = "--wc-skip" ]; then
|
||||
deploy="no"
|
||||
fi
|
||||
|
||||
# if not deploy yes then ask
|
||||
if [ -z "$deploy" ]; then
|
||||
echo
|
||||
echo
|
||||
read -rp "Do you want to also deploy the built web components? (yes/no/build): " deploy
|
||||
fi
|
||||
|
||||
if [ "$deploy" = "yes" ]; then
|
||||
cd web || exit
|
||||
npm run deploy-wc:dev
|
||||
elif [ "$deploy" = "build" ]; then
|
||||
cd web || exit
|
||||
npm run build:dev
|
||||
fi
|
||||
|
||||
# Play built-in sound based on the operating system
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
|
||||
217
plugin/source/dynamix.unraid.net/etc/rc.d/rc.flash_backup
Executable file
217
plugin/source/dynamix.unraid.net/etc/rc.d/rc.flash_backup
Executable file
@@ -0,0 +1,217 @@
|
||||
#!/bin/bash
|
||||
# This file is /etc/rc.d/rc.flash_backup
|
||||
# use at queue "f" for flash backup
|
||||
QUEUE=" -q f "
|
||||
TASKNAME="/etc/rc.d/rc.flash_backup watch"
|
||||
TASKACTION="/usr/local/emhttp/plugins/dynamix.my.servers/scripts/UpdateFlashBackup update"
|
||||
last=$(date +%s)
|
||||
# set GIT_OPTIONAL_LOCKS=0 globally to reduce/eliminate writes to /boot
|
||||
export GIT_OPTIONAL_LOCKS=0
|
||||
|
||||
FAST=1 # 1 second delay when waiting for git
|
||||
SLOW=10 # 10 second delay when waiting for git
|
||||
# wait for existing git commands to complete
|
||||
# $1 is the time in seconds to sleep when waiting. SLOW or FAST
|
||||
_waitforgit() {
|
||||
while [[ $(pgrep -f '^git -C /boot' -c) -ne 0 ]]; do
|
||||
sleep "$1"
|
||||
done
|
||||
}
|
||||
# log to syslog, then wait for existing git commands to complete
|
||||
# $1 is the time in seconds to sleep when waiting. SLOW or FAST
|
||||
_waitforgitlog() {
|
||||
if [[ $(pgrep -f '^git -C /boot' -c) -ne 0 ]]; then
|
||||
logger "waiting for current backup to complete" --tag flash_backup
|
||||
_waitforgit "$1"
|
||||
fi
|
||||
}
|
||||
status() {
|
||||
_connected && CONNECTED="system is connected to Unraid Connect Cloud." || CONNECTED="system is not connected to Unraid Connect Cloud."
|
||||
if _watching; then
|
||||
echo "flash backup monitor is running. ${CONNECTED}"
|
||||
_hasqueue && echo "changes detected, backup queued."
|
||||
exit 0
|
||||
else
|
||||
if _enabled; then
|
||||
echo "flash backup is enabled but the monitor is not running. ${CONNECTED}"
|
||||
else
|
||||
echo "flash backup is disabled so the monitor is disabled. ${CONNECTED}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
start() {
|
||||
_start
|
||||
exit 0
|
||||
}
|
||||
stop() {
|
||||
_stop
|
||||
exit 0
|
||||
}
|
||||
reload() {
|
||||
_start
|
||||
sleep 1
|
||||
status
|
||||
}
|
||||
_start() {
|
||||
# Note: can start if not signed in, but watcher loop will not process until signed in
|
||||
# only run if flash_backup is enabled
|
||||
if ! _enabled; then
|
||||
logger "flash backup disabled, exiting" --tag flash_backup
|
||||
exit 1
|
||||
fi
|
||||
_stop
|
||||
# start watcher loop as background process
|
||||
exec ${TASKNAME} &>/dev/null &
|
||||
}
|
||||
_stop() {
|
||||
if _watching; then
|
||||
logger "stop watching for file changes" --tag flash_backup
|
||||
# terminate watcher loop/process
|
||||
pkill --full "${TASKNAME}" &>/dev/null
|
||||
fi
|
||||
# do not flush. better to have unsaved changes than to corrupt the backup during shutdown
|
||||
# note that an existing git process could still be running
|
||||
}
|
||||
flush() {
|
||||
# remove any queued jobs
|
||||
_removequeue
|
||||
# wait for existing git commands to finish before flushing
|
||||
_waitforgitlog "${FAST}"
|
||||
logger "flush: ${TASKACTION}" --tag flash_backup
|
||||
# if _connected, push any changes ad-hoc
|
||||
if _connected; then
|
||||
# shellcheck disable=SC2086
|
||||
echo "${TASKACTION}_nolimit &>/dev/null" | at ${QUEUE} -M now &>/dev/null
|
||||
fi
|
||||
}
|
||||
_watching() {
|
||||
local flash_backup_pid
|
||||
flash_backup_pid=$(pgrep --full "${TASKNAME}")
|
||||
if [[ ${flash_backup_pid} ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
_watch() {
|
||||
# safely clean up git *.lock files
|
||||
_clearlocks
|
||||
# flush: this will ensure we start with a clean repo
|
||||
flush
|
||||
# wait for flush to complete
|
||||
sleep 3
|
||||
_waitforgitlog "${FAST}"
|
||||
logger "start watching for file changes" --tag flash_backup
|
||||
# start watcher loop
|
||||
while true; do
|
||||
# if system is connected to Unraid Connect Cloud, see if there are updates to process
|
||||
_connected && _f1
|
||||
sleep 60
|
||||
done
|
||||
}
|
||||
_f1() {
|
||||
# wait for existing git commands to finish before checking for updates
|
||||
_waitforgit "${SLOW}"
|
||||
if [ "$(git -C /boot status -s)" ]; then
|
||||
_hasqueue || _f2
|
||||
elif _haserror && _beenawhile; then
|
||||
# we are in an error state and it has been 3 hours since we last tried submitting. run the task now.
|
||||
_runtaskaction
|
||||
fi
|
||||
}
|
||||
_f2() {
|
||||
if ! _haserror || [[ $(($(date +"%M") % 10)) -eq 0 ]]; then
|
||||
logger "adding task: ${TASKACTION}" --tag flash_backup
|
||||
fi
|
||||
sed -i "s@uptodate=yes@uptodate=no@" /var/local/emhttp/flashbackup.ini &>/dev/null
|
||||
_runtaskaction
|
||||
}
|
||||
_hasqueue() {
|
||||
# returns false if the queue is empty, true otherwise
|
||||
# shellcheck disable=SC2086
|
||||
if [ -z "$(atq ${QUEUE})" ]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
_removequeue() {
|
||||
# delete any at jobs in queue f
|
||||
# @TODO shellcheck SC2162
|
||||
# shellcheck disable=SC2086
|
||||
atq ${QUEUE} | while read line; do
|
||||
id=$(echo ${line} | cut -d " " -f 1)
|
||||
atrm ${id}
|
||||
done
|
||||
}
|
||||
_runtaskaction() {
|
||||
# shellcheck disable=SC2086
|
||||
echo "${TASKACTION} &>/dev/null" | at ${QUEUE} -M now +1 minute &>/dev/null
|
||||
last=$(date +%s)
|
||||
}
|
||||
_enabled() {
|
||||
local output
|
||||
output=$(git -C /boot config --get remote.origin.url 2>&1)
|
||||
if [[ "${output}" == *"backup.unraid.net"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
_connected() {
|
||||
CFG=/var/local/emhttp/myservers.cfg
|
||||
[[ ! -f "${CFG}" ]] && return 1
|
||||
# shellcheck disable=SC1090
|
||||
source <(sed -nr '/\[remote\]/,/\[/{/username/p}' "${CFG}" 2>/dev/null)
|
||||
# ensure signed in
|
||||
if [ -z "${username}" ]; then
|
||||
return 1
|
||||
fi
|
||||
# shellcheck disable=SC1090
|
||||
source <(sed -nr '/\[connectionStatus\]/,/\[/{/minigraph/p}' "${CFG}" 2>/dev/null)
|
||||
# ensure connected
|
||||
if [[ -z "${minigraph}" || "${minigraph}" != "CONNECTED" ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
_haserror() {
|
||||
errorstring=$(awk -F "=" '/error/ {print $2}' /var/local/emhttp/flashbackup.ini 2>&1 || echo '')
|
||||
if [ ${#errorstring} -le 2 ]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
_beenawhile() {
|
||||
now=$(date +%s)
|
||||
age=$((now - last))
|
||||
maxage=$((3 * 60 * 60)) # three hours
|
||||
[[ $age -gt $maxage ]] && return 0
|
||||
return 1
|
||||
}
|
||||
# wait for git commands to end, then delete any stale lock files
|
||||
_clearlocks() {
|
||||
_waitforgitlog "${FAST}"
|
||||
find /boot/.git -type f -name '*.lock' -delete
|
||||
}
|
||||
case "$1" in
|
||||
'status')
|
||||
status
|
||||
;;
|
||||
'start')
|
||||
start
|
||||
;;
|
||||
'stop')
|
||||
stop
|
||||
;;
|
||||
'reload')
|
||||
reload
|
||||
;;
|
||||
'flush')
|
||||
flush
|
||||
;;
|
||||
'watch')
|
||||
_watch
|
||||
;;
|
||||
*)
|
||||
echo "usage $0 status|start|stop|reload|flush"
|
||||
;;
|
||||
esac
|
||||
182
plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid-api
Executable file
182
plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid-api
Executable file
@@ -0,0 +1,182 @@
|
||||
#!/bin/bash
|
||||
# unraid-api-handler
|
||||
flash="/boot/config/plugins/dynamix.my.servers"
|
||||
[[ ! -d "${flash}" ]] && echo "Please reinstall the Unraid Connect plugin" && exit 1
|
||||
[[ ! -f "${flash}/env" ]] && echo 'env=production' >"${flash}/env"
|
||||
# define env to avoid shellcheck SC2154. Will be overridden by the source command below
|
||||
env=production
|
||||
# shellcheck disable=SC1091
|
||||
source "${flash}/env"
|
||||
api_base_directory="/usr/local/bin"
|
||||
|
||||
# Only allow specific envs
|
||||
if [ "${env}" != "staging" ] && [ "${env}" != "production" ]; then
|
||||
echo "\"${env}\" is an unsupported env. Please use \"staging\" or \"production\"."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
switchenv() {
|
||||
stop
|
||||
# Get current environment from file
|
||||
local envFile="${flash}/env"
|
||||
local currentEnv
|
||||
currentEnv=$(
|
||||
# shellcheck disable=SC1090
|
||||
source "${envFile}"
|
||||
echo "${env}"
|
||||
)
|
||||
|
||||
if [[ "${currentEnv}" = "production" ]]; then
|
||||
echo "Switching from production to staging"
|
||||
echo 'env="staging"' >"${envFile}"
|
||||
cp "${api_base_directory}/unraid-api/.env.staging" "${api_base_directory}/unraid-api/.env"
|
||||
elif [[ "${currentEnv}" = "staging" ]]; then
|
||||
echo "Switching from staging to production"
|
||||
echo 'env="production"' >"${envFile}"
|
||||
cp "${api_base_directory}/unraid-api/.env.production" "${api_base_directory}/unraid-api/.env"
|
||||
fi
|
||||
echo "Run \"unraid-api start\" to start the API."
|
||||
}
|
||||
raiseloglevel() {
|
||||
kill -s SIGUSR2 "$(pidof unraid-api)"
|
||||
}
|
||||
lowerloglevel() {
|
||||
kill -s SIGUSR1 "$(pidof unraid-api)"
|
||||
}
|
||||
status() {
|
||||
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" status
|
||||
}
|
||||
start() {
|
||||
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" start 2>&1 | logger &
|
||||
}
|
||||
report() {
|
||||
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" report "$1" "$2"
|
||||
}
|
||||
startdebug() {
|
||||
LOG_CONTEXT=true LOG_STACKTRACE=true LOG_TRACING=true LOG_LEVEL=debug "${api_base_directory}/unraid-api/unraid-api" start --debug
|
||||
}
|
||||
stop() {
|
||||
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" stop 2>/dev/null
|
||||
}
|
||||
reload() {
|
||||
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" restart
|
||||
}
|
||||
_install() {
|
||||
# process file from commandline
|
||||
if [[ -n "$1" ]]; then
|
||||
file=$(realpath "${flash}/$1")
|
||||
if [[ "${file}" == "${flash}"* ]] && [[ "${file}" == *".tgz" || "${file}" == *".zip" ]] && [[ -f "${file}" ]]; then
|
||||
[[ "${file}" == *".tgz" ]] && ext=tgz || ext=zip
|
||||
echo "installing $1"
|
||||
cp "${file}" "${flash}/unraid-api.${ext}"
|
||||
else
|
||||
echo "invalid installation file: $1"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# If this was downloaded from a Github action it'll be a zip with a tgz inside
|
||||
# Let's extract the tgz and rename it for the next step
|
||||
if [[ -f "${flash}/unraid-api.zip" ]]; then
|
||||
for f in ${flash}/unraid-api.zip; do unzip -p "${f}" >"${flash}/${f%.zip}.tgz"; done
|
||||
rm -f "${flash}/unraid-api.zip"
|
||||
fi
|
||||
|
||||
# Ensure installation tgz exists
|
||||
[[ ! -f "${flash}/unraid-api.tgz" ]] && echo "Please reinstall the Unraid Connect plugin" && exit 1
|
||||
|
||||
# Stop old process
|
||||
[[ -f "${api_base_directory}/unraid-api/unraid-api" ]] && stop
|
||||
|
||||
# Install unraid-api
|
||||
rm -rf "${api_base_directory}/unraid-api"
|
||||
mkdir -p "${api_base_directory}/unraid-api"
|
||||
tar -C "${api_base_directory}/unraid-api" -xzf "${flash}/unraid-api.tgz" --strip 1
|
||||
|
||||
# Reset permissions
|
||||
rm -f "${flash}/data/permissions.json"
|
||||
|
||||
# Copy env file
|
||||
cp "${api_base_directory}/unraid-api/.env.${env}" "${api_base_directory}/unraid-api/.env"
|
||||
|
||||
# Copy wc files from flash
|
||||
if [ -f "${flash}/webComps/unraid.min.js" ]; then
|
||||
rm -rf /usr/local/emhttp/webGui/webComps
|
||||
mkdir -p /usr/local/emhttp/webGui/webComps
|
||||
cp ${flash}/webComps/* /usr/local/emhttp/webGui/webComps
|
||||
else
|
||||
# not fatal, previous version of unraid.min.js should still exist in /usr/local/emhttp/webGui/webComps
|
||||
echo "Note: ${flash}/webComps/unraid.min.js is missing"
|
||||
fi
|
||||
|
||||
# bail if expected file does not exist
|
||||
[[ ! -f "${api_base_directory}/unraid-api/unraid-api" ]] && echo "unraid-api install failed" && exit 1
|
||||
}
|
||||
install() {
|
||||
# Install the files
|
||||
_install "$1"
|
||||
|
||||
# if nginx is running, start the api. if not, it will be started by rc.nginx
|
||||
if /etc/rc.d/rc.nginx status &>/dev/null; then
|
||||
# Start new process
|
||||
start
|
||||
# Note: do not run another unraid-api command until you see "UNRAID API started successfully!" in syslog
|
||||
sleep 3
|
||||
echo "unraid-api installed and started"
|
||||
else
|
||||
echo "unraid-api installed"
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
uninstall() {
|
||||
# Stop old process
|
||||
[[ -f "${api_base_directory}/unraid-api/unraid-api" ]] && stop
|
||||
|
||||
# Remove all unraid-api files
|
||||
rm -rf "${api_base_directory}/unraid-api"
|
||||
rm -f /var/run/unraid-api.sock
|
||||
}
|
||||
case "$1" in
|
||||
'status')
|
||||
status
|
||||
;;
|
||||
'start')
|
||||
start
|
||||
;;
|
||||
'report')
|
||||
report "$2" "$3"
|
||||
;;
|
||||
'switch-env')
|
||||
switchenv
|
||||
;;
|
||||
'start-debug')
|
||||
startdebug
|
||||
;;
|
||||
'raise-log-level')
|
||||
raiseloglevel
|
||||
;;
|
||||
'lower-log-level')
|
||||
lowerloglevel
|
||||
;;
|
||||
'stop')
|
||||
stop
|
||||
;;
|
||||
'reload')
|
||||
reload
|
||||
;;
|
||||
'restart')
|
||||
reload
|
||||
;;
|
||||
'install')
|
||||
install "$2"
|
||||
;;
|
||||
'_install')
|
||||
_install "$2"
|
||||
;;
|
||||
'uninstall')
|
||||
uninstall
|
||||
;;
|
||||
*)
|
||||
echo "usage $0 status|start|report|switch-env|start-debug|raise-log-level|lower-log-level|stop|reload|install|uninstall"
|
||||
;;
|
||||
esac
|
||||
79
plugin/source/dynamix.unraid.net/pkg_build.sh
Executable file
79
plugin/source/dynamix.unraid.net/pkg_build.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
# passes `shellcheck` and `shfmt -i 2`
|
||||
|
||||
[[ "$1" == "s" ]] && env=staging
|
||||
[[ "$1" == "p" ]] && env=production
|
||||
[[ -z "${env}" ]] && echo "usage: [s|p]" && exit 1
|
||||
|
||||
DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
||||
MAINDIR=$(dirname "$(dirname "${DIR}")")
|
||||
tmpdir=/tmp/tmp.$((RANDOM * 19318203981230 + 40))
|
||||
pluginSrc=$(basename "${DIR}")
|
||||
plugin="${pluginSrc}"
|
||||
[[ "${env}" == "staging" ]] && plugin="${plugin}.staging" && cp "${MAINDIR}/plugins/${pluginSrc}.plg" "${MAINDIR}/plugins/${plugin}.plg"
|
||||
version=$(date +"%Y.%m.%d.%H%M")
|
||||
plgfile="${MAINDIR}/plugins/${plugin}.plg"
|
||||
txzfile="${MAINDIR}/archive/${plugin}-${version}.txz"
|
||||
|
||||
# create txz package
|
||||
mkdir -p "$(dirname "${txzfile}")"
|
||||
mkdir -p "${tmpdir}"
|
||||
# shellcheck disable=SC2046
|
||||
cp --parents -f $(find . -type f ! \( -iname ".DS_Store" -o -iname "pkg_build.sh" -o -iname "makepkg" -o -iname "explodepkg" -o -iname "sftp-config.json" \)) "${tmpdir}/"
|
||||
cd "${tmpdir}" || exit 1
|
||||
if [[ "${env}" == "staging" ]]; then
|
||||
# create README.md for staging plugin
|
||||
mv "${tmpdir}/usr/local/emhttp/plugins/dynamix.unraid.net" "${tmpdir}/usr/local/emhttp/plugins/dynamix.unraid.net.staging"
|
||||
sed -i "s@\*\*Unraid Connect\*\*@\*\*Unraid Connect \(staging\)\*\*@" "${tmpdir}/usr/local/emhttp/plugins/dynamix.unraid.net.staging/README.md"
|
||||
sed -i "s@dynamix.unraid.net.plg@dynamix.unraid.net.staging.plg@" "${tmpdir}/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page"
|
||||
fi
|
||||
chmod 0755 -R .
|
||||
sudo chown root:root -R .
|
||||
sudo "${MAINDIR}/source/dynamix.unraid.net/makepkg" -l y -c y "${txzfile}"
|
||||
sudo rm -rf "${tmpdir}"
|
||||
md5=$(md5sum "${txzfile}" | cut -f 1 -d ' ')
|
||||
echo "MD5: ${md5}"
|
||||
sha256=$(sha256sum "${txzfile}" | cut -f 1 -d ' ')
|
||||
echo "SHA256: ${sha256}"
|
||||
|
||||
# test txz package
|
||||
mkdir -p "${tmpdir}"
|
||||
cd "${tmpdir}" || exit 1
|
||||
RET=$(sudo "${MAINDIR}/source/dynamix.unraid.net/explodepkg" "${txzfile}" 2>&1 >/dev/null)
|
||||
sudo rm -rf "${tmpdir}"
|
||||
[[ "${RET}" != "" ]] && echo "Error: invalid txz package created: ${txzfile}" && exit 1
|
||||
cd "${DIR}" || exit 1
|
||||
|
||||
# define vars for plg
|
||||
pluginURL="https://stable.dl.unraid.net/unraid-api/\&name;.plg"
|
||||
downloadserver="https://stable.dl.unraid.net"
|
||||
js_dl_server="https://registration.unraid.net"
|
||||
if [[ "${env}" == "staging" ]]; then
|
||||
pluginURL="https://preview.dl.unraid.net/unraid-api/\&name;.plg"
|
||||
downloadserver="https://preview.dl.unraid.net"
|
||||
js_dl_server="https://registration-dev.unraid.net"
|
||||
fi
|
||||
|
||||
# update plg file
|
||||
sed -i -E "s#(ENTITY name\s*)\".*\"#\1\"${plugin}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY env\s*)\".*\"#\1\"${env}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY version\s*)\".*\"#\1\"${version}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY pluginURL\s*)\".*\"#\1\"${pluginURL}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY MD5\s*)\".*\"#\1\"${md5}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY SHA256\s*)\".*\"#\1\"${sha256}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY downloadserver\s*)\".*\"#\1\"${downloadserver}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY js_dl_server\s*)\".*\"#\1\"${js_dl_server}\"#g" "${plgfile}"
|
||||
|
||||
# set from environment vars
|
||||
sed -i -E "s#(ENTITY API_version\s*)\".*\"#\1\"${API_VERSION}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY API_MD5\s*)\".*\"#\1\"${API_MD5}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY API_SHA256\s*)\".*\"#\1\"${API_SHA256}\"#g" "${plgfile}"
|
||||
|
||||
# add changelog for major versions
|
||||
# sed -i "/<CHANGES>/a ###${version}\n" ${plgfile}
|
||||
|
||||
echo
|
||||
grep -E "ENTITY (name|pluginURL|env|version|MD5|SHA256|node_api_version)" "${plgfile}"
|
||||
echo
|
||||
echo "${env} plugin: ${plgfile}"
|
||||
echo "${env} txz: ${txzfile}"
|
||||
@@ -14,6 +14,7 @@ Tag="globe"
|
||||
* all copies or substantial portions of the Software.
|
||||
*/
|
||||
require_once "$docroot/plugins/dynamix.my.servers/include/state.php";
|
||||
require_once "$docroot/webGui/include/Wrappers.php";
|
||||
$serverState = new ServerState();
|
||||
|
||||
$keyfile = $serverState->keyfileBase64;
|
||||
@@ -140,7 +141,7 @@ function registerServer(button) {
|
||||
button.form.submit();
|
||||
});
|
||||
<?else:?>
|
||||
// give the unraid-api time to call rc.nginx and UpdateDNS before refreshing the page
|
||||
// give the unraid-api time to call rc.nginx before refreshing the page
|
||||
const delay = 4000;
|
||||
setTimeout(function() {
|
||||
button.form.submit();
|
||||
@@ -256,10 +257,10 @@ function changeRemoteAccess(dropdown) {
|
||||
$useConnectMsgTxt = '';
|
||||
break;
|
||||
case 'DYNAMIC_MANUAL':
|
||||
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/connect/remote-access' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>.";
|
||||
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/go/connect-remote-access/' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>.";
|
||||
break;
|
||||
case 'DYNAMIC_UPNP':
|
||||
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/connect/remote-access' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>, a random WAN port will be assigned by UPnP.";
|
||||
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/go/connect-remote-access/' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>, a random WAN port will be assigned by UPnP.";
|
||||
break;
|
||||
case 'ALWAYS_MANUAL':
|
||||
$remoteAccessMsgTxt = "Remote Access is always on.";
|
||||
@@ -521,7 +522,7 @@ _(Allow Remote Access)_:
|
||||
<?if(!$isRegistered): // NOTE: manually added close tags so the next section would not be indented ?>
|
||||
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you have signed in)_</span></dd></dl>
|
||||
<?elseif(!$isMiniGraphConnected && $myServersFlashCfg['remote']['wanaccess']!="yes"): // NOTE: manually added close tags so the next section would not be indented ?>
|
||||
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud)_</span></dd></dl>
|
||||
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud - try reloading the page)_</span></dd></dl>
|
||||
<?elseif(!$hasMyUnraidNetCert): // NOTE: manually added close tags so the next section would not be indented ?>
|
||||
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you Provision a myunraid.net SSL Cert)_</span><input type="hidden" id="wanport" value="0"></dd></dl>
|
||||
<?elseif(!$boolWebUIAuth): // NOTE: manually added close tags so the next section would not be indented ?>
|
||||
@@ -546,7 +547,7 @@ _(Allow Remote Access)_:
|
||||
<?endif?>
|
||||
|
||||
|
||||
: <unraid-i18n-host><unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check></unraid-i18n-host>
|
||||
: <unraid-i18n-host><unraid-wan-ip-check php-wan-ip="<?=http_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check></unraid-i18n-host>
|
||||
|
||||
<div markdown="1" id="wanpanel" style="display:'none'">
|
||||
|
||||
@@ -626,8 +627,8 @@ _(Enable Transparent 2FA for Local Access)_<!-- do not index -->:
|
||||
_(Flash backup)_:
|
||||
<?if(!$isRegistered):?>
|
||||
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you have signed in)_</span>
|
||||
<?elseif(!$isMiniGraphConnected && empty($flashbackup_status['activated'])):?>
|
||||
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud)_</span>
|
||||
<?elseif(!$isMiniGraphConnected):?>
|
||||
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud - try reloading the page)_</span>
|
||||
<?else: // begin show flash backup form ?>
|
||||
: <span id='flashbackuptext'><span class='blue p0'>_(Loading)_ <i class="fa fa-spinner fa-spin" aria-hidden="true"></i></span></span>
|
||||
|
||||
@@ -646,7 +647,7 @@ _(Flash backup)_:
|
||||
<div markdown="1" id="inactivespanel" style="display:none">
|
||||
|
||||
<?if(disk_free_space('/boot') > 1024*1000*1000):?>
|
||||
: <button type="button" onclick="enableFlashBackup(this)">_(Activate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/connect/help#automated-flash-backup" target="_blank">_(More information.)_</a></span>
|
||||
: <button type="button" onclick="enableFlashBackup(this)">_(Activate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/go/connect-flash-backup/" target="_blank">_(More information.)_</a></span>
|
||||
<?else:?>
|
||||
: <button type="button" disabled>_(Activate)_</button> <span><i class="fa fa-warning icon warning"></i> _(In order to activate Flash Backup there must be at least 1GB of free space on your flash drive.)_</span>
|
||||
<?endif?>
|
||||
@@ -673,7 +674,7 @@ _(Flash backup)_:
|
||||
</div>
|
||||
<div markdown="1" id="activepanel" style="display:none">
|
||||
|
||||
: <button type="button" onclick="enableFlashBackup(this)">_(Deactivate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/connect/help#automated-flash-backup" target="_blank">_(More information.)_</a></span>
|
||||
: <button type="button" onclick="enableFlashBackup(this)">_(Deactivate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/go/connect-flash-backup/" target="_blank">_(More information.)_</a></span>
|
||||
|
||||
<?if (in_array($_COOKIE['UPC_ENV']??'', ['development','staging']) && file_exists("/var/log/gitflash") && filesize("/var/log/gitflash")):?>
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ function save_flash_backup_state($loading='') {
|
||||
rename($flashbackup_tmp, $flashbackup_ini);
|
||||
}
|
||||
|
||||
function load_flash_backup_state() {
|
||||
global $arrState,$flashbackup_ini,$isRegistered;
|
||||
function default_flash_backup_state() {
|
||||
global $arrState;
|
||||
|
||||
$arrState = [
|
||||
'activated' => 'no',
|
||||
@@ -80,6 +80,12 @@ function load_flash_backup_state() {
|
||||
'error' => '',
|
||||
'remoteerror' => ''
|
||||
];
|
||||
}
|
||||
|
||||
function load_flash_backup_state() {
|
||||
global $arrState,$flashbackup_ini,$isRegistered;
|
||||
|
||||
default_flash_backup_state();
|
||||
|
||||
$arrNewState = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
|
||||
if ($arrNewState) {
|
||||
@@ -120,7 +126,7 @@ function set_git_config($name, $value) {
|
||||
|
||||
function readFromFile($file): string {
|
||||
$text = "";
|
||||
if (file_exists($file)) {
|
||||
if (file_exists($file) && filesize($file) > 0) {
|
||||
$fp = fopen($file,"r");
|
||||
if (flock($fp, LOCK_EX)) {
|
||||
$text = fread($fp, filesize($file));
|
||||
@@ -193,6 +199,7 @@ function deleteLocalRepo() {
|
||||
if (is_dir($mainGitDir)) {
|
||||
rename($mainGitDir, $tmpGitDir);
|
||||
exec('echo "rm -rf '.$tmpGitDir.' &>/dev/null" | at -q f -M now &>/dev/null');
|
||||
write_log("local repo deleted");
|
||||
}
|
||||
|
||||
// reset state
|
||||
@@ -201,6 +208,7 @@ function deleteLocalRepo() {
|
||||
$arrState['loading'] = '';
|
||||
$arrState['error'] = '';
|
||||
$arrState['remoteerror'] = '';
|
||||
save_flash_backup_state();
|
||||
}
|
||||
|
||||
$validCommands = [
|
||||
@@ -277,7 +285,14 @@ if ($pgrep_output[0] != "0") {
|
||||
|
||||
// check if signed-in
|
||||
if (!$isRegistered) {
|
||||
response_complete(406, array('error' => 'Must be signed in to My Servers to use Flash Backup'));
|
||||
default_flash_backup_state();
|
||||
response_complete(406, array('error' => 'Must be signed in to Unraid Connect to use Flash Backup'));
|
||||
}
|
||||
|
||||
// check if connected to Unraid Connect Cloud
|
||||
if (!$isConnected) {
|
||||
default_flash_backup_state();
|
||||
response_complete(406, array('error' => 'Must be connected to Unraid Connect Cloud to use Flash Backup'));
|
||||
}
|
||||
|
||||
// keyfile
|
||||
@@ -314,12 +329,46 @@ if (!empty($loadingMessage)) {
|
||||
}
|
||||
|
||||
if ($command == 'deactivate') {
|
||||
exec_log('git -C /boot remote remove origin');
|
||||
exec('/etc/rc.d/rc.flash_backup stop &>/dev/null');
|
||||
deleteLocalRepo();
|
||||
response_complete(200, '{}');
|
||||
}
|
||||
|
||||
// determine size of local repo
|
||||
$maxRepoSize = 100 * 1000; // 100 MB, for comparison without output of 'du -s'
|
||||
$repoDelFlag = '/boot/config/plugins/dynamix.my.servers/repodeleted';
|
||||
$output = [];
|
||||
if (file_exists('/boot/.git')) exec('du -s /boot/.git/ | cut -f 1', $output);
|
||||
$repoSize = ($output && $output[0]) ? intval($output[0]) : 0;
|
||||
if ($repoSize > $maxRepoSize) {
|
||||
// the local repo is too large
|
||||
$okToDelRepo = true;
|
||||
if (file_exists($repoDelFlag)) {
|
||||
// the local repo is too large, but we have already auto-deleted it in the past. determine how long ago this happened
|
||||
$repoDelTime = intval(@trim(@file_get_contents($repoDelFlag))); // epoch
|
||||
$repoAge = round((time()-$repoDelTime)/(60*60*24)); // days
|
||||
$repoMaxAge = 90; // days
|
||||
if ($repoAge < $repoMaxAge) {
|
||||
// the local repo was deleted and recreated less than repoMaxAge days ago, do not delete
|
||||
write_log("local repo is too large ($repoSize > $maxRepoSize) but was auto-deleted recently ($repoAge < $repoMaxAge)");
|
||||
$okToDelRepo = false;
|
||||
}
|
||||
}
|
||||
if ($okToDelRepo) {
|
||||
// the local repo is too large, delete and reactivate it
|
||||
write_log("local repo is too large ($repoSize > $maxRepoSize), about to delete and reactivate");
|
||||
file_put_contents($repoDelFlag, time());
|
||||
exec('/etc/rc.d/rc.flash_backup stop &>/dev/null');
|
||||
deleteLocalRepo();
|
||||
// change command to 'activate' and continue script
|
||||
$command = 'activate';
|
||||
$loadingMessage = 'Activating';
|
||||
save_flash_backup_state($loadingMessage);
|
||||
}
|
||||
} else {
|
||||
write_log("local repo size is acceptable ($repoSize < $maxRepoSize)");
|
||||
}
|
||||
|
||||
// build a list of sha256 hashes of the bzfiles
|
||||
$bzfilehashes = [];
|
||||
$allbzfiles = ['bzimage','bzfirmware','bzmodules','bzroot','bzroot-gui'];
|
||||
@@ -422,29 +471,29 @@ if (!file_exists('/boot/.git/info/exclude')) {
|
||||
}
|
||||
|
||||
// setup a nice git description
|
||||
$gitdesc_text='Unraid flash drive for '.$var['NAME']."\n";
|
||||
$gitdesc_file='/boot/.git/description';
|
||||
if (!file_exists($gitdesc_file) || strpos(file_get_contents($gitdesc_file),$var['NAME']) === false) {
|
||||
file_put_contents($gitdesc_file, 'Unraid flash drive for '.$var['NAME']."\n");
|
||||
if (!file_exists($gitdesc_file) || (file_get_contents($gitdesc_file) != $gitdesc_text)) {
|
||||
file_put_contents($gitdesc_file, $gitdesc_text);
|
||||
}
|
||||
|
||||
// configure git to use the noprivatekeys filter
|
||||
set_git_config('filter.noprivatekeys.clean', '/usr/local/emhttp/plugins/dynamix.my.servers/scripts/git-noprivatekeys-clean');
|
||||
|
||||
// configure git to apply the noprivatekeys filter to wireguard config files
|
||||
$gitattributes_file='/boot/.gitattributes';
|
||||
if (!file_exists($gitattributes_file) || strpos(file_get_contents($gitattributes_file),'noprivatekeys') === false) {
|
||||
file_put_contents($gitattributes_file, '# file managed by Unraid, do not modify
|
||||
$gitattributes_text='# file managed by Unraid, do not modify
|
||||
config/wireguard/*.cfg filter=noprivatekeys
|
||||
config/wireguard/*.conf filter=noprivatekeys
|
||||
config/wireguard/peers/*.conf filter=noprivatekeys
|
||||
');
|
||||
';
|
||||
$gitattributes_file='/boot/.gitattributes';
|
||||
if (!file_exists($gitattributes_file) || (file_get_contents($gitattributes_file) != $gitattributes_text)) {
|
||||
file_put_contents($gitattributes_file, $gitattributes_text);
|
||||
}
|
||||
|
||||
// setup git ignore for files we dont need in the flash backup
|
||||
$gitexclude_file='/boot/.git/info/exclude';
|
||||
if (!file_exists($gitexclude_file) || strpos(file_get_contents($gitexclude_file),'# version 1.0') === false) {
|
||||
file_put_contents($gitexclude_file, '# file managed by Unraid, do not modify
|
||||
# version 1.0
|
||||
// setup master git exclude file to specify what to include/exclude from repo
|
||||
$gitexclude_text = '# file managed by Unraid, do not modify
|
||||
# version 1.2
|
||||
|
||||
# Blacklist everything
|
||||
/*
|
||||
@@ -479,8 +528,20 @@ config/plugins/**/*.tar.bz2
|
||||
config/plugins-error
|
||||
config/plugins-old-versions
|
||||
config/plugins/dockerMan/images
|
||||
config/plugins/dynamix.file.integrity/logs
|
||||
config/wireguard/peers/*.png
|
||||
');
|
||||
';
|
||||
|
||||
// find large files to exclude from flash backup
|
||||
$oversize_files = $return_var = null;
|
||||
exec('find /boot/config -type f -size +10M 2>/dev/null | sed "s|^/boot/||g" 2>/dev/null', $oversize_files, $return_var);
|
||||
if ($oversize_files && is_array($oversize_files)) {
|
||||
$gitexclude_text .= "\n# Blacklist large files on this system\n".implode("\n", $oversize_files)."\n";
|
||||
}
|
||||
|
||||
$gitexclude_file='/boot/.git/info/exclude';
|
||||
if (!file_exists($gitexclude_file) || (file_get_contents($gitexclude_file) != $gitexclude_text)) {
|
||||
file_put_contents($gitexclude_file, $gitexclude_text);
|
||||
}
|
||||
|
||||
// ensure git user is configured
|
||||
@@ -529,7 +590,7 @@ if (empty($SSH_PORT)) {
|
||||
} else {
|
||||
$arrState['loading'] = '';
|
||||
if (stripos(implode($ssh_output),'permission denied') !== false) {
|
||||
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud';
|
||||
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to Unraid Connect Cloud';
|
||||
} else {
|
||||
$arrState['error'] = 'Unable to connect to backup.unraid.net:22';
|
||||
}
|
||||
@@ -557,7 +618,7 @@ if ($command == 'activate') {
|
||||
exec_log('git -C /boot checkout -B master origin/master');
|
||||
|
||||
// establish status
|
||||
exec_log('git -C /boot status --porcelain 2>&1', $status_output, $return_var);
|
||||
exec_log('git -C /boot status --porcelain', $status_output, $return_var);
|
||||
|
||||
if ($return_var != 0) {
|
||||
// detect git submodule
|
||||
@@ -600,7 +661,7 @@ if ($command == 'activate') {
|
||||
}
|
||||
|
||||
// detect corruption #1
|
||||
exec_log('git -C /boot show --summary 2>&1', $show_output, $return_var);
|
||||
exec_log('git -C /boot show --summary', $show_output, $return_var);
|
||||
if ($return_var != 0) {
|
||||
if (stripos(implode($show_output),'fatal: your current branch appears to be broken') !== false) {
|
||||
$arrState['error'] = 'Error: Backup corrupted';
|
||||
@@ -616,24 +677,31 @@ if ($command == 'activate') {
|
||||
} // end check for ($command == 'activate')
|
||||
|
||||
if ($command == 'update' || $command == 'activate') {
|
||||
|
||||
|
||||
// note: this section only runs if there are changes detected
|
||||
if ($arrState['uptodate'] == 'no') {
|
||||
// increment git commit counter
|
||||
appendToFile($commitCountFile, $time."\n");
|
||||
|
||||
// find files that are in repo but should not be, according to /boot/.git/info/exclude and various .gitignore files
|
||||
$invalid_files = $return_var = null;
|
||||
exec_log('git -C /boot ls-files --cached --ignored --exclude-standard', $invalid_files, $return_var);
|
||||
foreach ((array) $invalid_files as $invalid_file) {
|
||||
// remove each of these files from the repo
|
||||
// this prevents future changes from being tracked but does not remove the file from history.
|
||||
exec_log("git -C /boot rm --cached --ignore-unmatch '$invalid_file'");
|
||||
}
|
||||
|
||||
// add and commit all file changes
|
||||
exec_log('git -C /boot add -A');
|
||||
exec_log('git -C /boot commit -m ' . escapeshellarg($commitmsg));
|
||||
|
||||
// push changes upstream
|
||||
exec_log('git -C /boot push --set-upstream origin master', $push_output, $return_var);
|
||||
if ($return_var != 0) {
|
||||
exec_log('git -C /boot push --force --set-upstream origin master', $push_output, $return_var);
|
||||
}
|
||||
// push changes upstream
|
||||
exec_log('git -C /boot push --force --set-upstream origin master', $push_output, $return_var);
|
||||
if ($return_var != 0) {
|
||||
// check for permission denied
|
||||
if (stripos(implode($push_output),'permission denied') !== false) {
|
||||
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud';
|
||||
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to Unraid Connect Cloud';
|
||||
} elseif (stripos(implode($push_output),'fatal: loose object') !== false && stripos(implode($push_output),'is corrupt') !== false) {
|
||||
// detect corruption #2
|
||||
$arrState['error'] = 'Error: Backup corrupted';
|
||||
|
||||
@@ -30,8 +30,7 @@ if (!document.getElementsByTagName(modalsWebComponent).length) {
|
||||
$i18nHost.appendChild($modals);
|
||||
}
|
||||
</script>
|
||||
<?
|
||||
echo "
|
||||
|
||||
<unraid-i18n-host>
|
||||
<unraid-user-profile server='" . $serverState->getServerStateJson() . "'></unraid-user-profile>
|
||||
</unraid-i18n-host>";
|
||||
<unraid-user-profile server="<?= $serverState->getServerStateJsonForHtmlAttr() ?>"></unraid-user-profile>
|
||||
</unraid-i18n-host>
|
||||
|
||||
@@ -14,15 +14,25 @@
|
||||
* Usage:
|
||||
* ```
|
||||
* $rebootDetails = new RebootDetails();
|
||||
* $rebootType = $rebootDetails->getRebootType();
|
||||
* $rebootType = $rebootDetails->rebootType;
|
||||
* ```
|
||||
*/
|
||||
class RebootDetails
|
||||
{
|
||||
/**
|
||||
* @var string $rebootType Stores the type of reboot required, which can be 'update', 'downgrade', or 'thirdPartyDriversDownloading'.
|
||||
*/
|
||||
private $rebootType = '';
|
||||
const CURRENT_CHANGES_TXT_PATH = '/boot/changes.txt';
|
||||
const CURRENT_README_RELATIVE_PATH = 'plugins/unRAIDServer/README.md';
|
||||
const CURRENT_VERSION_PATH = '/etc/unraid-version';
|
||||
const PREVIOUS_BZ_ROOT_PATH = '/boot/previous/bzroot';
|
||||
const PREVIOUS_CHANGES_TXT_PATH = '/boot/previous/changes.txt';
|
||||
|
||||
private $currentVersion = '';
|
||||
|
||||
public $rebootType = ''; // 'update', 'downgrade', 'thirdPartyDriversDownloading'
|
||||
public $rebootReleaseDate = '';
|
||||
public $rebootVersion = '';
|
||||
|
||||
public $previousReleaseDate = '';
|
||||
public $previousVersion = '';
|
||||
|
||||
/**
|
||||
* Constructs a new RebootDetails object and automatically detects the reboot type during initialization.
|
||||
@@ -40,66 +50,119 @@ class RebootDetails
|
||||
{
|
||||
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
||||
|
||||
$rebootReadme = @file_get_contents("$docroot/plugins/unRAIDServer/README.md", false, null, 0, 20) ?: '';
|
||||
/**
|
||||
* Read the reboot readme, and see if it says "REBOOT REQUIRED" or "DOWNGRADE"
|
||||
* only relying on the README.md file to save reads from the flash drive.
|
||||
* because we started allowing downgrades from the account.unraid.net Update OS page, we can't
|
||||
* fully rely on the README.md value of being accurate.
|
||||
* For instance if on 6.13.0-beta.2.1 then chose to "Downgrade" to 6.13.0-beta.1.10 from the account app
|
||||
* the README.md file would still say "REBOOT REQUIRED".
|
||||
*/
|
||||
$rebootReadme = @file_get_contents("$docroot/" . self::CURRENT_README_RELATIVE_PATH, false, null, 0, 20) ?: '';
|
||||
$rebootDetected = preg_match("/^\*\*(REBOOT REQUIRED|DOWNGRADE)/", $rebootReadme);
|
||||
if (!$rebootDetected) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* if a reboot is required, then:
|
||||
* get current Unraid version from /etc/unraid-version
|
||||
* then get the version of the last update from self::CURRENT_CHANGES_TXT_PATH
|
||||
* if they're different, then a reboot is required
|
||||
* if the version in self::CURRENT_CHANGES_TXT_PATH is less than the current version, then a downgrade is required
|
||||
* if the version in self::CURRENT_CHANGES_TXT_PATH is greater than the current version, then an update is required
|
||||
*/
|
||||
$this->setCurrentVersion();
|
||||
$this->setRebootDetails();
|
||||
if ($this->currentVersion == '' || $this->rebootVersion == '') {
|
||||
return; // return to prevent potential incorrect outcome
|
||||
}
|
||||
|
||||
$rebootForDowngrade = $rebootDetected && strpos($rebootReadme, 'DOWNGRADE') !== false;
|
||||
$rebootForUpdate = $rebootDetected && strpos($rebootReadme, 'REBOOT REQUIRED') !== false;
|
||||
|
||||
$this->rebootType = $rebootForDowngrade ? 'downgrade' : ($rebootForUpdate ? 'update' : '');
|
||||
$compareVersions = version_compare($this->rebootVersion, $this->currentVersion);
|
||||
switch ($compareVersions) {
|
||||
case -1:
|
||||
$this->setRebootType('downgrade');
|
||||
break;
|
||||
case 0:
|
||||
// we should never get here, but if we do, then no reboot is required and just return
|
||||
return;
|
||||
case 1:
|
||||
$this->setRebootType('update');
|
||||
break;
|
||||
}
|
||||
|
||||
// Detect if third-party drivers were part of the update process
|
||||
$processWaitingThirdPartyDrivers = "inotifywait -q /boot/changes.txt -e move_self,delete_self";
|
||||
$processWaitingThirdPartyDrivers = "inotifywait -q " . self::CURRENT_CHANGES_TXT_PATH . " -e move_self,delete_self";
|
||||
// Run the ps command to list processes and check if the process is running
|
||||
$ps_command = "ps aux | grep -E \"$processWaitingThirdPartyDrivers\" | grep -v \"grep -E\"";
|
||||
$output = shell_exec($ps_command) ?? '';
|
||||
if ($this->rebootType != '' && strpos($output, $processWaitingThirdPartyDrivers) !== false) {
|
||||
$this->rebootType = 'thirdPartyDriversDownloading';
|
||||
$this->setRebootType('thirdPartyDriversDownloading');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of reboot required, which can be 'update', 'downgrade', or 'thirdPartyDriversDownloading'.
|
||||
*
|
||||
* @return string The type of reboot required.
|
||||
*/
|
||||
public function getRebootType()
|
||||
{
|
||||
return $this->rebootType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects and retrieves the version information related to the system reboot based on the contents of the '/boot/changes.txt' file.
|
||||
*
|
||||
* @return string The system version information or 'Not found' if not found, or 'File not found' if the file is not present.
|
||||
*/
|
||||
public function getRebootVersion()
|
||||
private function readChangesTxt(string $file_path = self::CURRENT_CHANGES_TXT_PATH)
|
||||
{
|
||||
$file_path = '/boot/changes.txt';
|
||||
|
||||
// Check if the file exists
|
||||
if (file_exists($file_path)) {
|
||||
// Open the file for reading
|
||||
$file = fopen($file_path, 'r');
|
||||
|
||||
// Read the file line by line until we find a line that starts with '# Version'
|
||||
while (($line = fgets($file)) !== false) {
|
||||
if (strpos($line, '# Version') === 0) {
|
||||
// Use a regular expression to extract the full version string
|
||||
if (preg_match('/# Version\s+(\S+)/', $line, $matches)) {
|
||||
$fullVersion = $matches[1];
|
||||
return $fullVersion;
|
||||
} else {
|
||||
return 'Not found';
|
||||
}
|
||||
exec("head -n4 $file_path", $rows);
|
||||
foreach ($rows as $row) {
|
||||
$i = stripos($row,'version');
|
||||
if ($i !== false) {
|
||||
[$version, $releaseDate] = explode(' ', trim(substr($row, $i+7)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Close the file
|
||||
fclose($file);
|
||||
return [
|
||||
'releaseDate' => $releaseDate ?? 'Not found',
|
||||
'version' => $version ?? 'Not found',
|
||||
];
|
||||
} else {
|
||||
return 'File not found';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current version of the Unraid server for comparison with the reboot version.
|
||||
*/
|
||||
private function setCurrentVersion() {
|
||||
// output ex: version="6.13.0-beta.2.1"
|
||||
$raw = @file_get_contents(self::CURRENT_VERSION_PATH) ?: '';
|
||||
// Regular expression to match the version between the quotes
|
||||
$pattern = '/version="([^"]+)"/';
|
||||
if (preg_match($pattern, $raw, $matches)) {
|
||||
$this->currentVersion = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
private function setRebootDetails()
|
||||
{
|
||||
$rebootDetails = $this->readChangesTxt();
|
||||
$this->rebootReleaseDate = $rebootDetails['releaseDate'];
|
||||
$this->rebootVersion = $rebootDetails['version'];
|
||||
}
|
||||
|
||||
private function setRebootType($rebootType)
|
||||
{
|
||||
$this->rebootType = $rebootType;
|
||||
}
|
||||
|
||||
/**
|
||||
* If self::PREVIOUS_BZ_ROOT_PATH exists, then the user has the option to downgrade to the previous version.
|
||||
* Parse the text file /boot/previous/changes.txt to get the version number of the previous version.
|
||||
* Then we move some files around and reboot.
|
||||
*/
|
||||
public function setPrevious()
|
||||
{
|
||||
if (@file_exists(self::PREVIOUS_BZ_ROOT_PATH) && @file_exists(self::PREVIOUS_CHANGES_TXT_PATH)) {
|
||||
$parseOutput = $this->readChangesTxt(self::PREVIOUS_CHANGES_TXT_PATH);
|
||||
$this->previousVersion = $parseOutput['version'];
|
||||
$this->previousReleaseDate = $parseOutput['releaseDate'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ $webguiGlobals = $GLOBALS;
|
||||
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
||||
|
||||
require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php";
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
|
||||
/**
|
||||
* ServerState class encapsulates server-related information and settings.
|
||||
*
|
||||
@@ -33,11 +34,20 @@ class ServerState
|
||||
protected $webguiGlobals;
|
||||
|
||||
private $var;
|
||||
private $flashbackupCfg;
|
||||
private $apiKey = '';
|
||||
private $apiVersion = '';
|
||||
private $avatar = '';
|
||||
private $email = '';
|
||||
private $extraOrigins = [];
|
||||
private $flashBackupActivated = '';
|
||||
private $hasRemoteApikey = false;
|
||||
private $registeredTime = '';
|
||||
private $username = '';
|
||||
private $connectPluginInstalled = '';
|
||||
private $connectPluginVersion;
|
||||
private $configErrorEnum = [
|
||||
"error" => 'UNKNOWN_ERROR',
|
||||
"ineligible" => 'INELIGIBLE',
|
||||
"invalid" => 'INVALID',
|
||||
"nokeyserver" => 'NO_KEY_SERVER',
|
||||
"withdrawn" => 'WITHDRAWN',
|
||||
@@ -47,16 +57,19 @@ class ServerState
|
||||
private $rebootDetails;
|
||||
private $caseModel = '';
|
||||
private $keyfileBase64UrlSafe = '';
|
||||
private $updateOsCheck;
|
||||
private $updateOsNotificationsEnabled = false;
|
||||
private $updateOsResponse;
|
||||
private $updateOsIgnoredReleases = [];
|
||||
|
||||
public $myServersFlashCfg = [];
|
||||
public $myServersMemoryCfg = [];
|
||||
public $host = 'unknown';
|
||||
public $combinedKnownOrigins = [];
|
||||
|
||||
public $nginxCfg;
|
||||
public $flashbackupStatus;
|
||||
public $registered;
|
||||
public $nginxCfg = [];
|
||||
public $flashbackupStatus = [];
|
||||
public $registered = false;
|
||||
public $myServersMiniGraphConnected = false;
|
||||
public $keyfileBase64 = '';
|
||||
|
||||
@@ -74,11 +87,42 @@ class ServerState
|
||||
// echo "<pre>" . json_encode($this->webguiGlobals, JSON_PRETTY_PRINT) . "</pre>";
|
||||
|
||||
$this->var = (array)parse_ini_file('state/var.ini');
|
||||
$this->nginxCfg = parse_ini_file('/var/local/emhttp/nginx.ini');
|
||||
$this->nginxCfg = @parse_ini_file('/var/local/emhttp/nginx.ini') ?? [];
|
||||
|
||||
$this->flashbackupCfg = '/var/local/emhttp/flashbackup.ini';
|
||||
$this->flashbackupStatus = (file_exists($this->flashbackupCfg)) ? @parse_ini_file($this->flashbackupCfg) : [];
|
||||
$this->osVersion = $this->var['version'];
|
||||
$this->osVersionBranch = trim(@exec('plugin category /var/log/plugins/unRAIDServer.plg') ?? 'stable');
|
||||
|
||||
$caseModelFile = '/boot/config/plugins/dynamix/case-model.cfg';
|
||||
$this->caseModel = file_exists($caseModelFile) ? htmlspecialchars(@file_get_contents($caseModelFile), ENT_HTML5, 'UTF-8') : '';
|
||||
|
||||
$this->rebootDetails = new RebootDetails();
|
||||
|
||||
$this->keyfileBase64 = empty($this->var['regFILE']) ? null : @file_get_contents($this->var['regFILE']);
|
||||
if ($this->keyfileBase64 !== false) {
|
||||
$this->keyfileBase64 = @base64_encode($this->keyfileBase64);
|
||||
$this->keyfileBase64UrlSafe = str_replace(['+', '/', '='], ['-', '_', ''], trim($this->keyfileBase64));
|
||||
}
|
||||
|
||||
$this->updateOsCheck = new UnraidOsCheck();
|
||||
$this->updateOsIgnoredReleases = $this->updateOsCheck->getIgnoredReleases();
|
||||
$this->updateOsNotificationsEnabled = !empty(@$this->getWebguiGlobal('notify', 'unraidos'));
|
||||
$this->updateOsResponse = $this->updateOsCheck->getUnraidOSCheckResult();
|
||||
|
||||
$this->setConnectValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value of a webgui global setting.
|
||||
*/
|
||||
public function getWebguiGlobal(string $key, string $subkey = null) {
|
||||
if (!$subkey) {
|
||||
return _var($this->webguiGlobals, $key, '');
|
||||
}
|
||||
$keyArray = _var($this->webguiGlobals, $key, []);
|
||||
return _var($keyArray, $subkey, '');
|
||||
}
|
||||
|
||||
private function setConnectValues() {
|
||||
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) {
|
||||
$this->connectPluginInstalled = 'dynamix.unraid.net.plg';
|
||||
}
|
||||
@@ -89,11 +133,29 @@ class ServerState
|
||||
$this->connectPluginInstalled .= '_installFailed';
|
||||
}
|
||||
|
||||
// exit early if the plugin is not installed
|
||||
if (!$this->connectPluginInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connectPluginVersion = file_exists('/var/log/plugins/dynamix.unraid.net.plg')
|
||||
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
|
||||
: (file_exists('/var/log/plugins/dynamix.unraid.net.staging.plg')
|
||||
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
|
||||
: 'base-' . $this->var['version']);
|
||||
|
||||
$this->getMyServersCfgValues();
|
||||
$this->getConnectKnownOrigins();
|
||||
$this->getFlashBackupStatus();
|
||||
}
|
||||
|
||||
private function getFlashBackupStatus() {
|
||||
$flashbackupCfg = '/var/local/emhttp/flashbackup.ini';
|
||||
$this->flashbackupStatus = (file_exists($flashbackupCfg)) ? @parse_ini_file($flashbackupCfg) : [];
|
||||
$this->flashBackupActivated = empty($this->flashbackupStatus['activated']) ? '' : 'true';
|
||||
}
|
||||
|
||||
private function getMyServersCfgValues() {
|
||||
/**
|
||||
* @todo can we read this from somewhere other than the flash? Connect page uses this path and /boot/config/plugins/dynamix.my.servers/myservers.cfg…
|
||||
* - $myservers_memory_cfg_path ='/var/local/emhttp/myservers.cfg';
|
||||
@@ -118,15 +180,17 @@ class ServerState
|
||||
$this->myServersFlashCfg['remote']['dynamicRemoteAccessType'] = "DISABLED";
|
||||
}
|
||||
|
||||
$this->osVersion = $this->var['version'];
|
||||
$this->osVersionBranch = trim(@exec('plugin category /var/log/plugins/unRAIDServer.plg') ?? 'stable');
|
||||
$this->apiKey = $this->myServersFlashCfg['upc']['apikey'] ?? '';
|
||||
$this->apiVersion = $this->myServersFlashCfg['api']['version'] ?? '';
|
||||
$this->avatar = (!empty($this->myServersFlashCfg['remote']['avatar']) && $this->connectPluginInstalled) ? $this->myServersFlashCfg['remote']['avatar'] : '';
|
||||
$this->email = $this->myServersFlashCfg['remote']['email'] ?? '';
|
||||
$this->hasRemoteApikey = !empty($this->myServersFlashCfg['remote']['apikey']);
|
||||
$this->registered = !empty($this->myServersFlashCfg['remote']['apikey']) && $this->connectPluginInstalled;
|
||||
$this->registeredTime = $this->myServersFlashCfg['remote']['regWizTime'] ?? '';
|
||||
$this->username = $this->myServersFlashCfg['remote']['username'] ?? '';
|
||||
}
|
||||
|
||||
$caseModelFile = '/boot/config/plugins/dynamix/case-model.cfg';
|
||||
$this->caseModel = file_exists($caseModelFile) ? file_get_contents($caseModelFile) : '';
|
||||
|
||||
$this->rebootDetails = new RebootDetails();
|
||||
|
||||
private function getConnectKnownOrigins() {
|
||||
/**
|
||||
* Allowed origins warning displayed when the current webGUI URL is NOT included in the known lists of allowed origins.
|
||||
* Include localhost in the test, but only display HTTP(S) URLs that do not include localhost.
|
||||
@@ -141,6 +205,11 @@ class ServerState
|
||||
$combinedOrigins = $allowedOrigins . "," . $extraOrigins; // combine the two strings for easier searching
|
||||
$combinedOrigins = str_replace(" ", "", $combinedOrigins); // replace any spaces with nothing
|
||||
$hostNotKnown = stripos($combinedOrigins, $this->host) === false; // check if the current host is in the combined list of origins
|
||||
|
||||
if ($extraOrigins) {
|
||||
$this->extraOrigins = explode(",", $extraOrigins);
|
||||
}
|
||||
|
||||
if ($hostNotKnown) {
|
||||
$this->combinedKnownOrigins = explode(",", $combinedOrigins);
|
||||
|
||||
@@ -157,25 +226,8 @@ class ServerState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->keyfileBase64 = empty($this->var['regFILE']) ? null : @file_get_contents($this->var['regFILE']);
|
||||
if ($this->keyfileBase64 !== false) {
|
||||
$this->keyfileBase64 = @base64_encode($this->keyfileBase64);
|
||||
$this->keyfileBase64UrlSafe = str_replace(['+', '/', '='], ['-', '_', ''], trim($this->keyfileBase64));
|
||||
}
|
||||
|
||||
/**
|
||||
* updateOsResponse is provided by the dynamix.plugin.manager/scripts/unraidcheck script saving to /tmp/unraidcheck/result.json
|
||||
*/
|
||||
$this->updateOsResponse = @json_decode(@file_get_contents('/tmp/unraidcheck/result.json'), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value of a webgui global setting.
|
||||
*/
|
||||
public function getWebguiGlobal(string $key) {
|
||||
return $this->webguiGlobals[$key];
|
||||
}
|
||||
/**
|
||||
* Retrieve the server information as an associative array
|
||||
*
|
||||
@@ -184,71 +236,84 @@ class ServerState
|
||||
public function getServerState()
|
||||
{
|
||||
$serverState = [
|
||||
"apiKey" => $this->myServersFlashCfg['upc']['apikey'] ?? '',
|
||||
"apiVersion" => $this->myServersFlashCfg['api']['version'] ?? '',
|
||||
"avatar" => (!empty($this->myServersFlashCfg['remote']['avatar']) && $this->connectPluginInstalled) ? $this->myServersFlashCfg['remote']['avatar'] : '',
|
||||
"array" => [
|
||||
"state" => @$this->getWebguiGlobal('var', 'fsState'),
|
||||
"progress" => @$this->getWebguiGlobal('var', 'fsProgress'),
|
||||
],
|
||||
"apiKey" => $this->apiKey,
|
||||
"apiVersion" => $this->apiVersion,
|
||||
"avatar" => $this->avatar,
|
||||
"caseModel" => $this->caseModel,
|
||||
"config" => [
|
||||
'valid' => ($this->var['configValid'] === 'yes'),
|
||||
'error' => isset($this->configErrorEnum[$this->var['configValid']]) ? $this->configErrorEnum[$this->var['configValid']] : 'UNKNOWN_ERROR',
|
||||
'error' => isset($this->configErrorEnum[$this->var['configValid']]) ? $this->configErrorEnum[$this->var['configValid']] : null,
|
||||
],
|
||||
"connectPluginInstalled" => $this->connectPluginInstalled,
|
||||
"connectPluginVersion" => $this->connectPluginVersion,
|
||||
"csrf" => $this->var['csrf_token'],
|
||||
"dateTimeFormat" => [
|
||||
"date" => @$this->getWebguiGlobal('display')['date'] ?? '',
|
||||
"time" => @$this->getWebguiGlobal('display')['time'] ?? '',
|
||||
"date" => @$this->getWebguiGlobal('display', 'date') ?? '',
|
||||
"time" => @$this->getWebguiGlobal('display', 'time') ?? '',
|
||||
],
|
||||
"description" => $this->var['COMMENT'] ? htmlspecialchars($this->var['COMMENT']) : '',
|
||||
"description" => $this->var['COMMENT'] ? htmlspecialchars($this->var['COMMENT'], ENT_HTML5, 'UTF-8') : '',
|
||||
"deviceCount" => $this->var['deviceCount'],
|
||||
"email" => $this->myServersFlashCfg['remote']['email'] ?? '',
|
||||
"email" => $this->email,
|
||||
"expireTime" => 1000 * (($this->var['regTy'] === 'Trial' || strstr($this->var['regTy'], 'expired')) ? $this->var['regTm2'] : 0),
|
||||
"extraOrigins" => explode(',', $this->myServersFlashCfg['api']['extraOrigins'] ?? ''),
|
||||
"extraOrigins" => $this->extraOrigins,
|
||||
"flashProduct" => $this->var['flashProduct'],
|
||||
"flashVendor" => $this->var['flashVendor'],
|
||||
"flashBackupActivated" => empty($this->flashbackupStatus['activated']) ? '' : 'true',
|
||||
"flashBackupActivated" => $this->flashBackupActivated,
|
||||
"guid" => $this->var['flashGUID'],
|
||||
"hasRemoteApikey" => !empty($this->myServersFlashCfg['remote']['apikey']),
|
||||
"internalPort" => $_SERVER['SERVER_PORT'],
|
||||
"hasRemoteApikey" => $this->hasRemoteApikey,
|
||||
"internalPort" => _var($_SERVER, 'SERVER_PORT'),
|
||||
"keyfile" => $this->keyfileBase64UrlSafe,
|
||||
"lanIp" => ipaddr(),
|
||||
"locale" => (!empty($_SESSION) && $_SESSION['locale']) ? $_SESSION['locale'] : 'en_US',
|
||||
"model" => $this->var['SYS_MODEL'],
|
||||
"name" => htmlspecialchars($this->var['NAME']),
|
||||
"model" => $this->var['SYS_MODEL'] ? htmlspecialchars($this->var['SYS_MODEL'], ENT_HTML5, 'UTF-8') : '',
|
||||
"name" => htmlspecialchars($this->var['NAME'], ENT_HTML5, 'UTF-8'),
|
||||
"osVersion" => $this->osVersion,
|
||||
"osVersionBranch" => $this->osVersionBranch,
|
||||
"protocol" => $_SERVER['REQUEST_SCHEME'],
|
||||
"rebootType" => $this->rebootDetails->getRebootType(),
|
||||
"regDev" => @(int)$this->var['regDev'] ?? 0,
|
||||
"protocol" => _var($_SERVER, 'REQUEST_SCHEME'),
|
||||
"rebootType" => $this->rebootDetails->rebootType,
|
||||
"rebootVersion" => $this->rebootDetails->rebootVersion,
|
||||
"regDevs" => @(int)$this->var['regDevs'] ?? 0,
|
||||
"regGen" => @(int)$this->var['regGen'],
|
||||
"regGuid" => @$this->var['regGUID'] ?? '',
|
||||
"regTo" => @htmlspecialchars($this->var['regTo']) ?? '',
|
||||
"regTo" => @htmlspecialchars($this->var['regTo'], ENT_HTML5, 'UTF-8') ?? '',
|
||||
"regTm" => $this->var['regTm'] ? @$this->var['regTm'] * 1000 : '', // JS expects milliseconds
|
||||
"regTy" => @$this->var['regTy'] ?? '',
|
||||
"regExp" => $this->var['regExp'] ? @$this->var['regExp'] * 1000 : '', // JS expects milliseconds
|
||||
"registered" => $this->registered,
|
||||
"registeredTime" => $this->myServersFlashCfg['remote']['regWizTime'] ?? '',
|
||||
"site" => $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'],
|
||||
"registeredTime" => $this->registeredTime,
|
||||
"site" => _var($_SERVER, 'REQUEST_SCHEME') . "://" . _var($_SERVER, 'HTTP_HOST'),
|
||||
"state" => strtoupper(empty($this->var['regCheck']) ? $this->var['regTy'] : $this->var['regCheck']),
|
||||
"theme" => [
|
||||
"banner" => !empty($this->getWebguiGlobal('display')['banner']),
|
||||
"bannerGradient" => $this->getWebguiGlobal('display')['showBannerGradient'] === 'yes' ?? false,
|
||||
"bgColor" => ($this->getWebguiGlobal('display')['background']) ? '#' . $this->getWebguiGlobal('display')['background'] : '',
|
||||
"descriptionShow" => (!empty($this->getWebguiGlobal('display')['headerdescription']) && $this->getWebguiGlobal('display')['headerdescription'] !== 'no'),
|
||||
"metaColor" => ($this->getWebguiGlobal('display')['headermetacolor'] ?? '') ? '#' . $this->getWebguiGlobal('display')['headermetacolor'] : '',
|
||||
"name" => $this->getWebguiGlobal('display')['theme'],
|
||||
"textColor" => ($this->getWebguiGlobal('display')['header']) ? '#' . $this->getWebguiGlobal('display')['header'] : '',
|
||||
"banner" => !empty($this->getWebguiGlobal('display', 'banner')),
|
||||
"bannerGradient" => $this->getWebguiGlobal('display', 'showBannerGradient') === 'yes' ?? false,
|
||||
"bgColor" => ($this->getWebguiGlobal('display', 'background')) ? '#' . $this->getWebguiGlobal('display', 'background') : '',
|
||||
"descriptionShow" => (!empty($this->getWebguiGlobal('display', 'headerdescription')) && $this->getWebguiGlobal('display', 'headerdescription') !== 'no'),
|
||||
"metaColor" => ($this->getWebguiGlobal('display', 'headermetacolor') ?? '') ? '#' . $this->getWebguiGlobal('display', 'headermetacolor') : '',
|
||||
"name" => $this->getWebguiGlobal('display', 'theme'),
|
||||
"textColor" => ($this->getWebguiGlobal('display', 'header')) ? '#' . $this->getWebguiGlobal('display', 'header') : '',
|
||||
],
|
||||
"ts" => time(),
|
||||
"uptime" => 1000 * (time() - round(strtok(exec("cat /proc/uptime"), ' '))),
|
||||
"username" => $this->myServersFlashCfg['remote']['username'] ?? '',
|
||||
"wanFQDN" => $this->nginxCfg['NGINX_WANFQDN'] ?? '',
|
||||
"username" => $this->username,
|
||||
"wanFQDN" => @$this->nginxCfg['NGINX_WANFQDN'] ?? '',
|
||||
];
|
||||
|
||||
if ($this->combinedKnownOrigins) {
|
||||
$serverState['combinedKnownOrigins'] = $this->combinedKnownOrigins;
|
||||
}
|
||||
|
||||
if ($this->updateOsIgnoredReleases) {
|
||||
$serverState['updateOsIgnoredReleases'] = $this->updateOsIgnoredReleases;
|
||||
}
|
||||
|
||||
if ($this->updateOsNotificationsEnabled) {
|
||||
$serverState['updateOsNotificationsEnabled'] = $this->updateOsNotificationsEnabled;
|
||||
}
|
||||
|
||||
if ($this->updateOsResponse) {
|
||||
$serverState['updateOsResponse'] = $this->updateOsResponse;
|
||||
}
|
||||
@@ -257,11 +322,21 @@ class ServerState
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the server information as a JSON string
|
||||
* Retrieve the server information as JSON
|
||||
*
|
||||
* @return string A JSON string containing server information.
|
||||
* @return string
|
||||
*/
|
||||
public function getServerStateJson() {
|
||||
return json_encode($this->getServerState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the server information as JSON string with converted special characters to HTML entities
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getServerStateJsonForHtmlAttr() {
|
||||
$json = json_encode($this->getServerState());
|
||||
return htmlspecialchars($json, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,13 +72,13 @@ class WebComponentTranslations
|
||||
'<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>' => '<p>' . _('To continue using Unraid OS you may purchase a license key.') . ' ' . _('Alternately, you may request a Trial extension.') . '</p>',
|
||||
'<p>To support more storage devices as your server grows, click Upgrade Key.</p>' => '<p>' . _('To support more storage devices as your server grows, click Upgrade Key.') . '</p>',
|
||||
'<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>' => '<p>' . _('You have used all your Trial extensions.') . ' ' . _('To continue using Unraid OS you may purchase a license key.') . '</p>',
|
||||
'<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>' => '<p>' . _('Your **Trial** key includes all the functionality and device support of a **Pro** key') . '</p><p>' . _('After your **Trial** has reached expiration, your server *still functions normally* until the next time you Stop the array or reboot your server') . '</p><p>' . _('At that point you may either purchase a license key or request a *Trial* extension.') . '</p>',
|
||||
'<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>' => '<p>' . _('Your **Trial** key includes all the functionality and device support of an **Unleashed** key') . '</p><p>' . _('After your **Trial** has reached expiration, your server *still functions normally* until the next time you Stop the array or reboot your server') . '</p><p>' . _('At that point you may either purchase a license key or request a *Trial* extension.') . '</p>',
|
||||
'<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>' => '<p>' . _('Your license key file is corrupted or missing.') . ' ' . _('The key file should be located in the /config directory on your USB Flash boot device') . '</p><p>' . _('If you do not have a backup copy of your license key file you may attempt to recover your key with your Unraid.net account') . '</p><p>' . _('If this was an expired Trial installation, you may purchase a license key.') . '</p>',
|
||||
'<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>' => '<p>' . _('Your license key file is corrupted or missing.') . ' ' . _('The key file should be located in the /config directory on your USB Flash boot device') . '</p><p>' . _('If you do not have a backup copy of your license key file you may attempt to recover your key with your Unraid.net account') . '</p><p>' . _('If this was an expired Trial installation, you may purchase a license key.') . '</p>',
|
||||
'<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class="list-disc pl-16px"><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>' => '<p>' . _('Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key.') . ' ' . _('A <em>Trial</em> key provides all the functionality of a Pro Registration key.') . '</p><p>' . _('Registration keys are bound to your USB Flash boot device serial number (GUID).') . ' ' . _('Please use a high quality name brand device at least 1GB in size.') . '</p><p>' . _('Note: USB memory card readers are generally not supported because most do not present unique serial numbers.') . '</p><p>' . _('*Important:*') . '</p><ul class="list-disc pl-16px"><li>' . _('Please make sure your server time is accurate to within 5 minutes') . '</li><li>' . _('Please make sure there is a DNS server specified') . '</li>>' . '</ul>',
|
||||
'<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of an Unleashed Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class="list-disc pl-16px"><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>' => '<p>' . _('Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key.') . ' ' . _('A <em>Trial</em> key provides all the functionality of an Unleashed Registration key.') . '</p><p>' . _('Registration keys are bound to your USB Flash boot device serial number (GUID).') . ' ' . _('Please use a high quality name brand device at least 1GB in size.') . '</p><p>' . _('Note: USB memory card readers are generally not supported because most do not present unique serial numbers.') . '</p><p>' . _('*Important:*') . '</p><ul class="list-disc pl-16px"><li>' . _('Please make sure your server time is accurate to within 5 minutes') . '</li><li>' . _('Please make sure there is a DNS server specified') . '</li>>' . '</ul>',
|
||||
'<p>Your Trial key requires an internet connection.</p><p><a href="/Settings/NetworkSettings" class="underline">Please check Settings > Network</a></p>' => '<p>' . _('Your Trial key requires an internet connection') . '</p><p><a href="/Settings/NetworkSettings" class="underline">' . _('Please check Settings > Network') . '</a></p>',
|
||||
'<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>' => '<p>' . _('Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.') . '</p>',
|
||||
'A Trial key provides all the functionality of a Pro Registration key' => _('A Trial key provides all the functionality of a Pro Registration key'),
|
||||
'A Trial key provides all the functionality of an Unleashed Registration key' => _('A Trial key provides all the functionality of an Unleashed Registration key'),
|
||||
'Acklowledge that you have made a Flash Backup to enable this action' => _('Acklowledge that you have made a Flash Backup to enable this action'),
|
||||
'ago' => _('ago'),
|
||||
'All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.' => _('All you need is an active internet connection, an Unraid.net account, and the Connect plugin.') . ' ' . _('Get started by installing the plugin.'),
|
||||
@@ -89,8 +89,10 @@ class WebComponentTranslations
|
||||
'Beta' => _('Beta'),
|
||||
'Blacklisted USB Flash GUID' => _('Blacklisted USB Flash GUID'),
|
||||
'BLACKLISTED' => _('BLACKLISTED'),
|
||||
'Calculating OS Update Eligibility…' => _('Calculating OS Update Eligibility…'),
|
||||
'Calculating trial expiration…' => _('Calculating trial expiration…'),
|
||||
'Callback redirect type not present or incorrect' => _('Callback redirect type not present or incorrect'),
|
||||
'Cancel {0}' => sprintf(_('Cancel %s'), '{0}'),
|
||||
'Cancel' => _('Cancel'),
|
||||
'Cannot access your USB Flash boot device' => _('Cannot access your USB Flash boot device'),
|
||||
'Cannot validate Unraid Trial key' => _('Cannot validate Unraid Trial key'),
|
||||
@@ -107,8 +109,10 @@ class WebComponentTranslations
|
||||
'Close' => _('Close'),
|
||||
'Configure Connect Features' => _('Configure Connect Features'),
|
||||
'Confirm and start update' => _('Confirm and start update'),
|
||||
'Confirm to Install Unraid OS {0}' => sprintf(_('Confirm to Install Unraid OS %s'), '{0}'),
|
||||
'Connected' => _('Connected'),
|
||||
'Contact Support' => _('Contact Support'),
|
||||
'Continue' => _('Continue'),
|
||||
'Copied' => _('Copied'),
|
||||
'Copy Key URL' => _('Copy Key URL'),
|
||||
'Copy your Key URL: {0}' => sprintf(_('Copy your Key URL: %s'), '{0}'),
|
||||
@@ -122,24 +126,30 @@ class WebComponentTranslations
|
||||
'Downgrade Unraid OS to {0}' => sprintf(_('Downgrade Unraid OS to %s'), '{0}'),
|
||||
'Downgrade Unraid OS' => _('Downgrade Unraid OS'),
|
||||
'Downgrades are only recommended if you\'re unable to solve a critical issue.' => _('Downgrades are only recommended if you\'re unable to solve a critical issue.'),
|
||||
'Download Diagnostics' => _('Download Diagnostics'),
|
||||
'Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.' => _('Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.'),
|
||||
'Download unraid-api Logs' => _('Download unraid-api Logs'),
|
||||
'Dynamic Remote Access' => _('Dynamic Remote Access'),
|
||||
'Enable update notifications' => _('Enable update notifications'),
|
||||
'Enhance your experience with Unraid Connect' => _('Enhance your experience with Unraid Connect'),
|
||||
'Enhance your Unraid experience with Connect' => _('Enhance your Unraid experience with Connect'),
|
||||
'Enhance your Unraid experience' => _('Enhance your Unraid experience'),
|
||||
'Error creatiing a trial key. Please try again later.' => _('Error creatiing a trial key. Please try again later.'),
|
||||
'Error creating a trial key. Please try again later.' => _('Error creating a trial key. Please try again later.'),
|
||||
'Error Parsing Changelog • {0}' => sprintf(_('Error Parsing Changelog • %s'), '{0}'),
|
||||
'Error' => _('Error'),
|
||||
'Expired {0}' => sprintf(_('Expired %s'), '{0}'),
|
||||
'Expired' => _('Expired'),
|
||||
'Expires at {0}' => sprintf(_('Expires at %s'), '{0}'),
|
||||
'Expires in {0}' => sprintf(_('Expires in %s'), '{0}'),
|
||||
'Extend License to Update' => _('Extend License to Update'),
|
||||
'Extend License' => _('Extend License'),
|
||||
'Extend Trial' => _('Extend Trial'),
|
||||
'Extending your free trial by 15 days' => _('Extending your free trial by 15 days'),
|
||||
'Extension Installed' => _('Extension Installed'),
|
||||
'Failed to {0} {1} Key' => sprintf(_('Failed to %1s %2s Key'), '{0}', '{1}'),
|
||||
'Failed to install key' => _('Failed to install key'),
|
||||
'Failed to update Connect account configuration' => _('Failed to update Connect account configuration'),
|
||||
'Fetching & parsing changelog…' => _('Fetching & parsing changelog…'),
|
||||
'Fix Error' => _('Fix Error'),
|
||||
'Flash Backup is not available. Navigate to {0}/Main/Settings/Flash to try again then come back to this page.' => sprintf(_('Flash Backup is not available. Navigate to %s/Main/Settings/Flash to try again then come back to this page.'), '{0}'),
|
||||
'Flash GUID Error' => _('Flash GUID Error'),
|
||||
@@ -152,18 +162,24 @@ class WebComponentTranslations
|
||||
'Go to Connect plugin settings' => _('Go to Connect plugin settings'),
|
||||
'Go to Connect' => _('Go to Connect'),
|
||||
'Go to Management Access Now' => _('Go to Management Access Now'),
|
||||
'Go to Settings > Notifications to enable automatic OS update notifications for future releases.' => _('Go to Settings > Notifications to enable automatic OS update notifications for future releases.'),
|
||||
'Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.' => _('Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.'),
|
||||
'Go to Tools > Management Access to ensure your backup is up-to-date.' => _('Go to Tools > Management Access to ensure your backup is up-to-date.'),
|
||||
'Go to Tools > Registration to fix' => _('Go to Tools > Registration to fix'),
|
||||
'Go to Tools > Registration to Learn More' => _('Go to Tools > Registration to Learn More'),
|
||||
'Go to Tools > Update OS for more options.' => _('Go to Tools > Update OS for more options.'),
|
||||
'Go to Tools > Update' => _('Go to Tools > Update'),
|
||||
'hour' => sprintf(_('%s hour'), '{n}') . ' | ' . sprintf(_('%s hours'), '{n}'),
|
||||
'I have made a Flash Backup' => _('I have made a Flash Backup'),
|
||||
'If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.' => _('If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.'),
|
||||
'Ignore this message if you are currently connected via Remote Access or VPN.' => _('Ignore this message if you are currently connected via Remote Access or VPN.'),
|
||||
'Ignore this release until next reboot' => _('Ignore this release until next reboot'),
|
||||
'Ignored Releases' => _('Ignored Releases'),
|
||||
'In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.' => _('In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.'),
|
||||
'Install Connect' => _('Install Connect'),
|
||||
'Install Recovered' => _('Install Recovered'),
|
||||
'Install Replaced' => _('Install Replaced'),
|
||||
'Install Unraid OS {0}' => sprintf(_('Install Unraid OS %s'), '{0}'),
|
||||
'Install' => _('Install'),
|
||||
'Installed' => _('Installed'),
|
||||
'Installing Extended Trial' => _('Installing Extended Trial'),
|
||||
@@ -175,6 +191,9 @@ class WebComponentTranslations
|
||||
'Invalid API Key Format' => _('Invalid API Key Format'),
|
||||
'Invalid API Key' => _('Invalid API Key'),
|
||||
'Invalid installation' => _('Invalid installation'),
|
||||
'It\s highly recommended to review the changelog before continuing your update.' => _('It\'s highly recommended to review the changelog before continuing your update.'),
|
||||
'Key ineligible for {0}' => sprintf(_('Key ineligible for %s'), '{0}'),
|
||||
'Key ineligible for future releases' => _('Key ineligible for future releases'),
|
||||
'Keyfile required to check replacement status' => _('Keyfile required to check replacement status'),
|
||||
'LAN IP {0}' => sprintf(_('LAN IP %s'), '{0}'),
|
||||
'LAN IP Copied' => _('LAN IP Copied'),
|
||||
@@ -182,12 +201,15 @@ class WebComponentTranslations
|
||||
'Last checked: {0}' => sprintf(_('Last checked: %s'), '{0}'),
|
||||
'Learn more about the error' => _('Learn more about the error'),
|
||||
'Learn more and fix' => _('Learn more and fix'),
|
||||
'Learn more and link your key to your account' => _('Learn more and link your key to your account'),
|
||||
'Learn More' => _('Learn More'),
|
||||
'Learn more' => _('Learn more'),
|
||||
'Let\'s Unleash your Hardware!' => _('Let\'s Unleash your Hardware!'),
|
||||
'License key actions' => _('License key actions'),
|
||||
'License key type' => _('License key type'),
|
||||
'License Management' => _('License Management'),
|
||||
'Link Key' => _('Link Key'),
|
||||
'Linked to Unraid.net account' => _('Linked to Unraidnet account'),
|
||||
'Loading' => _('Loading'),
|
||||
'Manage Unraid.net Account in new tab' => _('Manage Unraid.net Account in new tab'),
|
||||
'Manage Unraid.net Account' => _('Manage Unraid.net Account'),
|
||||
@@ -196,6 +218,7 @@ class WebComponentTranslations
|
||||
'minute' => sprintf(_('%s minute'), '{n}') . ' | ' . sprintf(_('%s minutes'), '{n}'),
|
||||
'Missing key file' => _('Missing key file'),
|
||||
'month' => sprintf(_('%s month'), '{n}') . ' | ' . sprintf(_('%s months'), '{n}'),
|
||||
'More options' => _('More options'),
|
||||
'Multiple License Keys Present' => _('Multiple License Keys Present'),
|
||||
'Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.' => _('Never ever be left without a backup of your config.') . ' ' . _('If you need to change flash drives, generate a backup from Connect and be up and running in minutes.'),
|
||||
'New Version: {0}' => sprintf(_('New Version: %s'), '{0}'),
|
||||
@@ -204,14 +227,18 @@ class WebComponentTranslations
|
||||
'No Keyfile' => _('No Keyfile'),
|
||||
'No thanks' => _('No thanks'),
|
||||
'No USB flash configuration data' => _('No USB flash configuration data'),
|
||||
'Not Linked' => _('Not Linked'),
|
||||
'On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.' => _('On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.'),
|
||||
'Online Flash Backup' => _('Online Flash Backup'),
|
||||
'Open a bug report' => _('Open a bug report'),
|
||||
'Open Dropdown' => _('Open Dropdown'),
|
||||
'Opens Connect in new tab' => _('Opens Connect in new tab'),
|
||||
'Original release date {0}' => sprintf(_('Original release date %s'), '{0}'),
|
||||
'OS Update Eligibility Expired' => _('OS Update Eligibility Expired'),
|
||||
'Performing actions' => _('Performing actions'),
|
||||
'Please confirm the update details below' => _('Please confirm the update details below'),
|
||||
'Please finish the initiated downgrade to enable updates.' => _('Please finish the initiated downgrade to enable updates.'),
|
||||
'Please finish the initiated update to enable a downgrade.' => _('Please finish the initiated update to enable a downgrade.'),
|
||||
'Please fix any errors and try again.' => _('Please fix any errors and try again.'),
|
||||
'Please keep this window open while we perform some actions' => _('Please keep this window open while we perform some actions'),
|
||||
'Please keep this window open' => _('Please keep this window open'),
|
||||
@@ -238,14 +265,20 @@ class WebComponentTranslations
|
||||
'Recover Key' => _('Recover Key'),
|
||||
'Recovered' => _('Recovered'),
|
||||
'Redeem Activation Code' => _('Redeem Activation Code'),
|
||||
'Refresh' => _('Refresh'),
|
||||
'Registered on' => _('Registered on'),
|
||||
'Registered to' => _('Registered to'),
|
||||
'Registration key / USB Flash GUID mismatch' => _('Registration key / USB Flash GUID mismatch'),
|
||||
'Release date {0}' => sprintf(_('Release date %s'), '{0}'),
|
||||
'Release requires verification to update' => _('Release requires verification to update'),
|
||||
'Reload' => _('Reload'),
|
||||
'Remark: Unraid\'s WAN IPv4 {0} does not match your client\'s WAN IPv4 {1}.' => sprintf(_('Remark: Unraid\'s WAN IPv4 %1s does not match your client\'s WAN IPv4 %2s.'), '{0}', '{1}'),
|
||||
'Remark: your WAN IPv4 is {0}' => sprintf(_('Remark: your WAN IPv4 is %s'), '{0}'),
|
||||
'Remove from ignore list' => _('Remove from ignore list'),
|
||||
'Remove' => _('Remove'),
|
||||
'Replace Key' => _('Replace Key'),
|
||||
'Replaced' => _('Replaced'),
|
||||
'Requires the local unraid-api to be running successfully' => _('Requires the local unraid-api to be running successfully'),
|
||||
'Restarting unraid-api…' => _('Restarting unraid-api…'),
|
||||
'second' => sprintf(_('%s second'), '{n}') . ' | ' . sprintf(_('%s seconds'), '{n}'),
|
||||
'Server Up Since {0}' => sprintf(_('Server Up Since %s'), '{0}'),
|
||||
@@ -257,6 +290,7 @@ class WebComponentTranslations
|
||||
'Sign In to utilize Unraid Connect' => _('Sign In to utilize Unraid Connect'),
|
||||
'Sign In to your Unraid.net account to get started' => _('Sign In to your Unraid.net account to get started'),
|
||||
'Sign In with Unraid.net Account' => _('Sign In with Unraid.net Account'),
|
||||
'Sign In' => _('Sign In'),
|
||||
'Sign Out Failed' => _('Sign Out Failed'),
|
||||
'Sign Out of Unraid.net' => _('Sign Out of Unraid.net'),
|
||||
'Sign Out requires the local unraid-api to be running' => _('Sign Out requires the local unraid-api to be running'),
|
||||
@@ -298,6 +332,7 @@ class WebComponentTranslations
|
||||
'Unable to fetch client WAN IPv4' => _('Unable to fetch client WAN IPv4'),
|
||||
'Unable to open release notes' => _('Unable to open release notes'),
|
||||
'Unknown error' => _('Unknown error'),
|
||||
'Unknown' => _('Unknown'),
|
||||
'unlimited' => _('unlimited'),
|
||||
'Unraid {0} Available' => sprintf(_('Unraid %s Available'), '{0}'),
|
||||
'Unraid {0} Update Available' => sprintf(_('Unraid %s Update Available'), '{0}'),
|
||||
@@ -310,10 +345,13 @@ class WebComponentTranslations
|
||||
'Unraid logo animating with a wave like effect' => _('Unraid logo animating with a wave like effect'),
|
||||
'Unraid OS {0} Released' => sprintf(_('Unraid OS %s Released'), '{0}'),
|
||||
'Unraid OS {0} Update Available' => sprintf(_('Unraid OS %s Update Available'), '{0}'),
|
||||
'Unraid OS is up-to-date' => _('Unraid OS is up-to-date'),
|
||||
'Unraid OS Update Available' => _('Unraid OS Update Available'),
|
||||
'unraid-api is offline' => _('unraid-api is offline'),
|
||||
'Up-to-date with eligible releases' => _('Up-to-date with eligible releases'),
|
||||
'Up-to-date' => _('Up-to-date'),
|
||||
'Update Available' => _('Update Available'),
|
||||
'Update Released' => _('Update Released'),
|
||||
'Update Unraid OS confirmation required' => _('Update Unraid OS confirmation required'),
|
||||
'Update Unraid OS' => _('Update Unraid OS'),
|
||||
'Updating 3rd party drivers' => _('Updating 3rd party drivers'),
|
||||
@@ -322,15 +360,20 @@ class WebComponentTranslations
|
||||
'Uptime {0}' => sprintf(_('Uptime %s'), '{0}'),
|
||||
'USB Flash device error' => _('USB Flash device error'),
|
||||
'USB Flash has no serial number' => _('USB Flash has no serial number'),
|
||||
'Verify to Update' => _('Verify to Update'),
|
||||
'Version available for restore {0}' => sprintf(_('Version available for restore %s'), '{0}'),
|
||||
'Version: {0}' => sprintf(_('Version: %s'), '{0}'),
|
||||
'View Available Updates' => _('View Available Updates'),
|
||||
'View Changelog & Update' => _('View Changelog & Update'),
|
||||
'View Changelog for {0}' => sprintf(_('View Changelog for %s'), '{0}'),
|
||||
'View Changelog on Docs' => _('View Changelog on Docs'),
|
||||
'View Changelog to Start Update' => _('View Changelog to Start Update'),
|
||||
'View Changelog' => _('View Changelog'),
|
||||
'View on Docs' => _('View on Docs'),
|
||||
'View release notes' => _('View release notes'),
|
||||
'We recommend backing up your USB Flash Boot Device before starting the update.' => _('We recommend backing up your USB Flash Boot Device before starting the update.'),
|
||||
'year' => sprintf(_('%s year'), '{n}') . ' | ' . sprintf(_('%s years'), '{n}'),
|
||||
'You are still eligible to access OS updates that were published on or before {1}.' => sprintf(_('You are still eligible to access OS updates that were published on or before %s.'), '{1}'),
|
||||
'You can also manually create a new backup by clicking the Create Flash Backup button.' => _('You can also manually create a new backup by clicking the Create Flash Backup button.'),
|
||||
'You can manually create a backup by clicking the Create Flash Backup button.' => _('You can manually create a backup by clicking the Create Flash Backup button.'),
|
||||
'You have already activated the Flash Backup feature via the Unraid Connect plugin.' => _('You have already activated the Flash Backup feature via the Unraid Connect plugin.'),
|
||||
@@ -339,7 +382,10 @@ class WebComponentTranslations
|
||||
'You may still update to releases dated prior to your update expiration date.' => _('You may still update to releases dated prior to your update expiration date.'),
|
||||
'You\'re one step closer to enhancing your Unraid experience' => _('You\'re one step closer to enhancing your Unraid experience'),
|
||||
'Your {0} Key has been replaced!' => sprintf(_('Your %s Key has been replaced!'), '{0}'),
|
||||
'Your free Trial key provides all the functionality of a Pro Registration key' => _('Your free Trial key provides all the functionality of a Pro Registration key'),
|
||||
'Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.' => sprintf(_('Your %s license included one year of free updates at the time of purchase.'), '{0}') . ' ' . _('You are now eligible to extend your license and access the latest OS updates.') . ' ' . sprintf(_('You are still eligible to access OS updates that were published on or before %s.'), '{1}'),
|
||||
'Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates.' => sprintf(_('Your %s license included one year of free updates at the time of purchase.'), '{0}') . ' ' . _('You are now eligible to extend your license and access the latest OS updates.'),
|
||||
'Your free Trial key provides all the functionality of an Unleashed Registration key' => _('Your free Trial key provides all the functionality of an Unleashed Registration key'),
|
||||
'Your license key is not eligible for Unraid OS {0}' => sprintf(_('Your license key is not eligible for Unraid OS %s'), '{0}'),
|
||||
'Your Trial has expired' => _('Your Trial has expired'),
|
||||
'Your Trial key has been extended!' => _('Your Trial key has been extended!'),
|
||||
];
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
$cli = php_sapi_name() == 'cli';
|
||||
|
||||
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
||||
require_once "$docroot/webGui/include/Wrappers.php";
|
||||
|
||||
/**
|
||||
* @name response_complete
|
||||
@@ -49,16 +50,20 @@ if ($cli) {
|
||||
if ($argc > 2) $param1 = $argv[2];
|
||||
} else {
|
||||
$command = $_POST['command'];
|
||||
$param1 = $_POST['param1'];
|
||||
$param1 = $_POST['param1'] ?? '';
|
||||
}
|
||||
if (!in_array($command, $validCommands)) $command = 'none';
|
||||
|
||||
if (!file_exists('/usr/local/sbin/unraid-api') || !file_exists('/usr/local/bin/unraid-api/unraid-api')) {
|
||||
response_complete(406, array('error' => 'Please reinstall the My Servers plugin'));
|
||||
response_complete(406, array('error' => 'Please reinstall the Unraid Connect plugin'));
|
||||
}
|
||||
|
||||
$output = [];
|
||||
$retval = null;
|
||||
|
||||
switch ($command) {
|
||||
case 'start':
|
||||
exec('unraid-api start 2>/dev/null', $output, $retval);
|
||||
$output = implode(PHP_EOL, $output);
|
||||
response_complete(200, array('result' => $output), $output);
|
||||
break;
|
||||
@@ -81,7 +86,7 @@ switch ($command) {
|
||||
response_complete(200, array('result' => $output), $output);
|
||||
break;
|
||||
case 'wanip':
|
||||
$wanip = trim(@file_get_contents("https://wanip4.unraid.net/"));
|
||||
$wanip = trim(http_get_contents("https://wanip4.unraid.net/"));
|
||||
response_complete(200, array('result' => $wanip), $wanip);
|
||||
break;
|
||||
case 'none':
|
||||
|
||||
@@ -13,37 +13,17 @@ Tag="upload"
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*/
|
||||
/**
|
||||
* @note icon-update is rotated via CSS in myservers1.php
|
||||
*/
|
||||
|
||||
require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php";
|
||||
// Create an instance of the RebootDetails class
|
||||
$rebootDetails = new RebootDetails();
|
||||
/**
|
||||
* @note icon-update is rotated via CSS in myservers1.php
|
||||
*
|
||||
* If /boot/previous/bzroot exists, then the user has the option to downgrade to the previous version.
|
||||
* Parse the text file /boot/previous/changes.txt to get the version number of the previous version.
|
||||
* Then we move some files around and reboot.
|
||||
*/
|
||||
$restoreVersion = $restoreBranch = $restoreVersionReleaseDate = 'unknown';
|
||||
$restoreExists = file_exists('/boot/previous/bzroot');
|
||||
$restoreChangelogPath = '/boot/previous/changes.txt';
|
||||
// Get the current reboot details if there are any
|
||||
$rebootDetails->setPrevious();
|
||||
|
||||
$serverNameEscaped = htmlspecialchars(str_replace(' ', '_', strtolower($var['NAME'])));
|
||||
|
||||
if (file_exists($restoreChangelogPath)) {
|
||||
exec("head -n4 $restoreChangelogPath", $rows);
|
||||
foreach ($rows as $row) {
|
||||
$i = stripos($row,'version');
|
||||
if ($i !== false) {
|
||||
[$restoreVersion, $restoreVersionReleaseDate] = explode(' ', trim(substr($row, $i+7)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
$restoreBranch = strpos($restoreVersion, 'rc') !== false
|
||||
? _('Next')
|
||||
: (strpos($restoreVersion, 'beta') !== false
|
||||
? _('Beta')
|
||||
: _('Stable'));
|
||||
}
|
||||
?>
|
||||
|
||||
<script>
|
||||
@@ -139,7 +119,7 @@ function startDowngrade() {
|
||||
$.get(
|
||||
'/plugins/dynamix.plugin.manager/include/Downgrade.php',
|
||||
{
|
||||
version: '<?=$restoreVersion?>',
|
||||
version: '<?= $rebootDetails->previousVersion ?>',
|
||||
},
|
||||
function() {
|
||||
refresh();
|
||||
@@ -150,7 +130,7 @@ function startDowngrade() {
|
||||
function confirmDowngrade() {
|
||||
swal({
|
||||
title: "_(Confirm Downgrade)_",
|
||||
text: "<?= $restoreVersion ?><br>_(A reboot will be required)_",
|
||||
text: "<?= $rebootDetails->previousVersion ?><br>_(A reboot will be required)_",
|
||||
html: true,
|
||||
type: 'warning',
|
||||
showCancelButton: true,
|
||||
@@ -167,7 +147,7 @@ function confirmDowngrade() {
|
||||
|
||||
<unraid-i18n-host>
|
||||
<unraid-downgrade-os
|
||||
reboot-version="<?= $rebootDetails->getRebootVersion() ?>"
|
||||
restore-version="<?= $restoreExists && $restoreVersion != 'unknown' ? $restoreVersion : '' ?>"
|
||||
restore-release-date="<?= $restoreExists && $restoreVersionReleaseDate != 'unknown' ? $restoreVersionReleaseDate : '' ?>"></unraid-downgrade-os>
|
||||
reboot-version="<?= $rebootDetails->rebootVersion ?>"
|
||||
restore-version="<?= $rebootDetails->previousVersion ?>"
|
||||
restore-release-date="<?= $rebootDetails->previousReleaseDate ?>"></unraid-downgrade-os>
|
||||
</unraid-i18n-host>
|
||||
|
||||
@@ -46,5 +46,5 @@ function flashBackup() {
|
||||
</script>
|
||||
|
||||
<unraid-i18n-host>
|
||||
<unraid-update-os reboot-version="<?= $rebootDetails->getRebootVersion() ?>"></unraid-update-os>
|
||||
<unraid-update-os reboot-version="<?= $rebootDetails->rebootVersion ?>"></unraid-update-os>
|
||||
</unraid-i18n-host>
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
/* Copyright 2005-2024, Lime Technology
|
||||
* Copyright 2012-2024, Bergware International.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License version 2,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*/
|
||||
/**
|
||||
* Abstracting this code into a separate file allows us to use it in multiple places without duplicating code.
|
||||
* 1. unraidcheck script can call this
|
||||
* require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
|
||||
* $unraidOsCheck = new UnraidOsCheck();
|
||||
* $unraidOsCheck->checkForUpdate();
|
||||
*
|
||||
* 2. Unraid webgui web components can GET this file with action params to get updates, ignore updates, etc.
|
||||
* - EX: Unraid webgui web components can check for updates via a GET request and receive a response with the json file directly
|
||||
* - this is useful for the UPC to check for updates and display a model based on the value
|
||||
* - `/plugins/dynamix.plugin.manager/scripts/unraidcheck.php?json=true`
|
||||
* - note the json=true query param to receive a json response
|
||||
*
|
||||
* @param action {'check'|'removeAllIgnored'|'removeIgnoredVersion'|'ignoreVersion'} - the action to perform
|
||||
* @param version {string} - the version to ignore or remove
|
||||
* @param json {string} - if set to true, will return the json response from the external request
|
||||
* @param altUrl {URL} - if set, will use this url instead of the default
|
||||
*/
|
||||
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
||||
require_once "$docroot/webGui/include/Wrappers.php";
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/PluginHelpers.php";
|
||||
|
||||
class UnraidOsCheck
|
||||
{
|
||||
private const BASE_RELEASES_URL = 'https://releases.unraid.net/os';
|
||||
private const JSON_FILE_IGNORED = '/tmp/unraidcheck/ignored.json';
|
||||
private const JSON_FILE_IGNORED_KEY = 'updateOsIgnoredReleases';
|
||||
private const JSON_FILE_RESULT = '/tmp/unraidcheck/result.json';
|
||||
private const PLG_PATH = '/usr/local/emhttp/plugins/unRAIDServer/unRAIDServer.plg';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$isGetRequest = !empty($_SERVER) && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'GET';
|
||||
$getHasAction = $_GET !== null && !empty($_GET) && isset($_GET['action']);
|
||||
|
||||
if ($isGetRequest && $getHasAction) {
|
||||
$this->handleGetRequestWithActions();
|
||||
}
|
||||
}
|
||||
|
||||
private function handleGetRequestWithActions()
|
||||
{
|
||||
switch ($_GET['action']) {
|
||||
case 'check':
|
||||
$this->checkForUpdate();
|
||||
break;
|
||||
|
||||
case 'removeAllIgnored':
|
||||
$this->removeAllIgnored();
|
||||
break;
|
||||
|
||||
case 'removeIgnoredVersion':
|
||||
if (isset($_GET['version'])) {
|
||||
$this->removeIgnoredVersion($_GET['version']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ignoreVersion':
|
||||
if (isset($_GET['version'])) {
|
||||
$this->ignoreVersion($_GET['version']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->respondWithError(400, "Unhandled action");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getUnraidOSCheckResult()
|
||||
{
|
||||
if (file_exists(self::JSON_FILE_RESULT)) {
|
||||
return $this->readJsonFile(self::JSON_FILE_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
public function getIgnoredReleases()
|
||||
{
|
||||
if (!file_exists(self::JSON_FILE_IGNORED)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ignoredData = $this->readJsonFile(self::JSON_FILE_IGNORED);
|
||||
|
||||
if (is_array($ignoredData) && array_key_exists(self::JSON_FILE_IGNORED_KEY, $ignoredData)) {
|
||||
return $ignoredData[self::JSON_FILE_IGNORED_KEY];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @todo clean up this method to be more extensible */
|
||||
public function checkForUpdate()
|
||||
{
|
||||
// Multi-language support
|
||||
if (!function_exists('_')) {
|
||||
function _($text) {return $text;}
|
||||
}
|
||||
|
||||
// this command will set the $notify array
|
||||
extract(parse_plugin_cfg('dynamix', true));
|
||||
|
||||
$var = (array)@parse_ini_file('/var/local/emhttp/var.ini');
|
||||
|
||||
$params = [];
|
||||
$params['branch'] = plugin('category', self::PLG_PATH, 'stable');
|
||||
$params['current_version'] = plugin('version', self::PLG_PATH) ?: _var($var,'version');
|
||||
if (_var($var,'regExp')) $params['update_exp'] = date('Y-m-d', _var($var,'regExp')*1);
|
||||
$defaultUrl = self::BASE_RELEASES_URL;
|
||||
// pass a param of altUrl to use the provided url instead of the default
|
||||
$parsedAltUrl = (array_key_exists('altUrl',$_GET) && $_GET['altUrl']) ? $_GET['altUrl'] : null;
|
||||
// if $parsedAltUrl pass to params
|
||||
if ($parsedAltUrl) $params['altUrl'] = $parsedAltUrl;
|
||||
|
||||
$urlbase = $parsedAltUrl ?? $defaultUrl;
|
||||
$url = $urlbase.'?'.http_build_query($params);
|
||||
$curlinfo = [];
|
||||
$response = http_get_contents($url,[],$curlinfo);
|
||||
if (array_key_exists('error', $curlinfo)) {
|
||||
$response = json_encode(array('error' => $curlinfo['error']), JSON_PRETTY_PRINT);
|
||||
}
|
||||
$responseMutated = json_decode($response, true);
|
||||
if (!$responseMutated) {
|
||||
$response = json_encode(array('error' => 'Invalid response from '.$urlbase), JSON_PRETTY_PRINT);
|
||||
$responseMutated = json_decode($response, true);
|
||||
}
|
||||
|
||||
// add params that were used for debugging
|
||||
$responseMutated['params'] = $params;
|
||||
|
||||
// store locally for UPC to access
|
||||
$this->writeJsonFile(self::JSON_FILE_RESULT, $responseMutated);
|
||||
|
||||
// if we have a query param of json=true then just output the json
|
||||
if (array_key_exists('json',$_GET) && $_GET['json']) {
|
||||
header('Content-Type: application/json');
|
||||
echo $response;
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// send notification if a newer version is available and not ignored
|
||||
$isNewerVersion = array_key_exists('isNewer',$responseMutated) ? $responseMutated['isNewer'] : false;
|
||||
$isReleaseIgnored = array_key_exists('version',$responseMutated) ? in_array($responseMutated['version'], $this->getIgnoredReleases()) : false;
|
||||
|
||||
if ($responseMutated && $isNewerVersion && !$isReleaseIgnored) {
|
||||
$output = _var($notify,'plugin');
|
||||
$server = strtoupper(_var($var,'NAME','server'));
|
||||
$newver = (array_key_exists('version',$responseMutated) && $responseMutated['version']) ? $responseMutated['version'] : 'unknown';
|
||||
$script = '/usr/local/emhttp/webGui/scripts/notify';
|
||||
$event = "System - Unraid [$newver]";
|
||||
$subject = "Notice [$server] - Version update $newver";
|
||||
$description = "A new version of Unraid is available";
|
||||
exec("$script -e ".escapeshellarg($event)." -s ".escapeshellarg($subject)." -d ".escapeshellarg($description)." -i ".escapeshellarg("normal $output")." -l '/Tools/Update' -x");
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
private function removeAllIgnored()
|
||||
{
|
||||
if (file_exists(self::JSON_FILE_IGNORED)) {
|
||||
$this->deleteJsonFile(self::JSON_FILE_IGNORED);
|
||||
$this->respondWithSuccess([]);
|
||||
}
|
||||
// fail silently if file doesn't exist
|
||||
}
|
||||
|
||||
private function removeIgnoredVersion($removeVersion)
|
||||
{
|
||||
if ($this->isValidSemVerFormat($removeVersion)) {
|
||||
if (file_exists(self::JSON_FILE_IGNORED)) {
|
||||
$existingData = $this->readJsonFile(self::JSON_FILE_IGNORED);
|
||||
|
||||
if (isset($existingData[self::JSON_FILE_IGNORED_KEY])) {
|
||||
$existingData[self::JSON_FILE_IGNORED_KEY] = array_diff($existingData[self::JSON_FILE_IGNORED_KEY], [$removeVersion]);
|
||||
$this->writeJsonFile(self::JSON_FILE_IGNORED, $existingData);
|
||||
$this->respondWithSuccess($existingData);
|
||||
} else {
|
||||
$this->respondWithError(400, "No versions to remove in the JSON file");
|
||||
}
|
||||
} else {
|
||||
$this->respondWithError(400, "No JSON file found");
|
||||
}
|
||||
} else {
|
||||
$this->respondWithError(400, "Invalid removeVersion format");
|
||||
}
|
||||
}
|
||||
|
||||
private function ignoreVersion($version)
|
||||
{
|
||||
if ($this->isValidSemVerFormat($version)) {
|
||||
$newData = [$this::JSON_FILE_IGNORED_KEY => [$version]];
|
||||
$existingData = file_exists(self::JSON_FILE_IGNORED) ? $this->readJsonFile(self::JSON_FILE_IGNORED) : [];
|
||||
|
||||
if (isset($existingData[self::JSON_FILE_IGNORED_KEY])) {
|
||||
$existingData[self::JSON_FILE_IGNORED_KEY][] = $version;
|
||||
} else {
|
||||
$existingData[self::JSON_FILE_IGNORED_KEY] = [$version];
|
||||
}
|
||||
|
||||
$this->writeJsonFile(self::JSON_FILE_IGNORED, $existingData);
|
||||
$this->respondWithSuccess($existingData);
|
||||
} else {
|
||||
$this->respondWithError(400, "Invalid version format");
|
||||
}
|
||||
}
|
||||
|
||||
private function isValidSemVerFormat($version)
|
||||
{
|
||||
return preg_match('/^\d+\.\d+(\.\d+)?(-.+)?$/', $version);
|
||||
}
|
||||
|
||||
private function readJsonFile($file)
|
||||
{
|
||||
return @json_decode(@file_get_contents($file), true) ?? [];
|
||||
}
|
||||
|
||||
private function writeJsonFile($file, $data)
|
||||
{
|
||||
if (!is_dir(dirname($file))) { // prevents errors when directory doesn't exist
|
||||
mkdir(dirname($file));
|
||||
}
|
||||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
private function deleteJsonFile($file)
|
||||
{
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
private function respondWithError($statusCode, $message)
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
echo $message;
|
||||
}
|
||||
|
||||
private function respondWithSuccess($data)
|
||||
{
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data, JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate and handle the request for GET requests with actions – vars are duplicated here for multi-use of this file
|
||||
$isGetRequest = !empty($_SERVER) && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'GET';
|
||||
$getHasAction = $_GET !== null && !empty($_GET) && isset($_GET['action']);
|
||||
if ($isGetRequest && $getHasAction) {
|
||||
new UnraidOsCheck();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
class UnraidUpdateCancel
|
||||
{
|
||||
private $PLG_FILENAME;
|
||||
private $PLG_BOOT;
|
||||
private $PLG_VAR;
|
||||
private $USR_LOCAL_PLUGIN_UNRAID_PATH;
|
||||
|
||||
public function __construct() {
|
||||
$this->PLG_FILENAME = "unRAIDServer.plg";
|
||||
$this->PLG_BOOT = "/boot/config/plugins/{$this->PLG_FILENAME}";
|
||||
$this->PLG_VAR = "/var/log/plugins/{$this->PLG_FILENAME}";
|
||||
$this->USR_LOCAL_PLUGIN_UNRAID_PATH = "/usr/local/emhttp/plugins/unRAIDServer";
|
||||
|
||||
// Handle the cancellation
|
||||
$revertResult = $this->revertFiles();
|
||||
// Return JSON response for front-end client
|
||||
$statusCode = $revertResult['success'] ? 200 : 500;
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($revertResult);
|
||||
}
|
||||
|
||||
public function revertFiles() {
|
||||
try {
|
||||
$command = '/sbin/mount | grep -q "/boot/previous/bz"';
|
||||
exec($command, $output, $returnCode);
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
return ['success' => true]; // Nothing to revert
|
||||
}
|
||||
|
||||
// Clear the results of previous unraidcheck run
|
||||
@unlink("/tmp/unraidcheck/result.json");
|
||||
|
||||
// Revert changes made by unRAIDServer.plg
|
||||
shell_exec("mv -f /boot/previous/* /boot");
|
||||
unlink($this->PLG_BOOT);
|
||||
unlink($this->PLG_VAR);
|
||||
symlink("{$this->USR_LOCAL_PLUGIN_UNRAID_PATH}/{$this->PLG_FILENAME}", $this->PLG_VAR);
|
||||
|
||||
// Restore README.md by echoing the content into the file
|
||||
$readmeFile = "{$this->USR_LOCAL_PLUGIN_UNRAID_PATH}/README.md";
|
||||
$readmeContent = "**Unraid OS**\n\n";
|
||||
$readmeContent .= "Unraid OS by [Lime Technology, Inc.](https://lime-technology.com).\n";
|
||||
file_put_contents($readmeFile, $readmeContent);
|
||||
|
||||
return ['success' => true]; // Upgrade handled successfully
|
||||
} catch (\Throwable $th) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $th->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Self instantiate the class and handle the cancellation
|
||||
new UnraidUpdateCancel();
|
||||
@@ -13,60 +13,7 @@
|
||||
?>
|
||||
<?
|
||||
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
||||
require_once "$docroot/webGui/include/Wrappers.php";
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/PluginHelpers.php";
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
|
||||
|
||||
// Multi-language support
|
||||
if (!function_exists('_')) {
|
||||
function _($text) {return $text;}
|
||||
}
|
||||
|
||||
extract(parse_plugin_cfg('dynamix', true));
|
||||
|
||||
$var = (array)@parse_ini_file('/var/local/emhttp/var.ini');
|
||||
$script = "$docroot/webGui/scripts/notify";
|
||||
$server = strtoupper(_var($var,'NAME','server'));
|
||||
$output = _var($notify,'plugin');
|
||||
$plg = '/var/log/plugins/unRAIDServer.plg';
|
||||
|
||||
$params = [];
|
||||
$params['branch'] = plugin('category', $plg, 'stable');
|
||||
$params['current_version'] = plugin('version', $plg) ?: _var($var,'version');
|
||||
if (_var($var,'regExp')) $params['update_exp'] = date('m-d-Y', _var($var,'regExp')*1);
|
||||
$urlbase = 'https://releases.unraid.net/os';
|
||||
$url = $urlbase.'?'.http_build_query($params);
|
||||
|
||||
$response = "";
|
||||
// use error handler to convert warnings from file_get_contents to errors so they can be captured
|
||||
function warning_as_error($severity, $message, $filename, $lineno) {
|
||||
throw new ErrorException($message, 0, $severity, $filename, $lineno);
|
||||
}
|
||||
set_error_handler("warning_as_error");
|
||||
try {
|
||||
$response = file_get_contents($url);
|
||||
} catch (Exception $e) {
|
||||
$response = json_encode(array('error' => $e->getMessage()), JSON_PRETTY_PRINT);
|
||||
}
|
||||
restore_error_handler();
|
||||
|
||||
$json = json_decode($response, true);
|
||||
if (!$json) {
|
||||
$response = json_encode(array('error' => 'Invalid response from '.$urlbase), JSON_PRETTY_PRINT);
|
||||
$json = json_decode($response, true);
|
||||
}
|
||||
|
||||
// add params that were sent to $urlbase
|
||||
$json['params'] = $params;
|
||||
|
||||
// store locally for UPC to access
|
||||
$file = '/tmp/unraidcheck/result.json';
|
||||
if (!is_dir(dirname($file))) mkdir(dirname($file));
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT));
|
||||
|
||||
// send notification if a newer version is available
|
||||
if ($json && array_key_exists('isNewer',$json) && $json['isNewer']) {
|
||||
$newver = (array_key_exists('version',$json) && $json['version']) ? $json['version'] : 'unknown';
|
||||
exec("$script -e ".escapeshellarg("System - Unraid [$newver]")." -s ".escapeshellarg("Notice [$server] - Version update $newver")." -d ".escapeshellarg("A new version of Unraid is available")." -i ".escapeshellarg("normal $output")." -l '/Tools/Update' -x");
|
||||
}
|
||||
exit(0);
|
||||
?>
|
||||
$unraidOsCheck = new UnraidOsCheck();
|
||||
$unraidOsCheck->checkForUpdate();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?PHP
|
||||
/* Copyright 2005-2023, Lime Technology
|
||||
* Copyright 2012-2023, Bergware International.
|
||||
/* Copyright 2005-2024, Lime Technology
|
||||
* Copyright 2012-2024, Bergware International.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License version 2,
|
||||
@@ -11,429 +11,12 @@
|
||||
*/
|
||||
?>
|
||||
<?
|
||||
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
||||
// add translations
|
||||
$_SERVER['REQUEST_URI'] = 'settings';
|
||||
require_once "$docroot/webGui/include/Translations.php";
|
||||
require_once "$docroot/webGui/include/Helpers.php";
|
||||
|
||||
function host_lookup_ip($host) {
|
||||
$result = @dns_get_record($host, DNS_A);
|
||||
$ip = ($result) ? $result[0]['ip']??'' : '';
|
||||
return($ip);
|
||||
}
|
||||
function rebindDisabled() {
|
||||
global $isLegacyCert;
|
||||
$rebindtesturl = $isLegacyCert ? "rebindtest.unraid.net" : "rebindtest.myunraid.net";
|
||||
// DNS Rebind Protection - this checks the server but clients could still have issues
|
||||
$validResponse = array("192.168.42.42", "fd42");
|
||||
$response = host_lookup_ip($rebindtesturl);
|
||||
return in_array(explode('::',$response)[0], $validResponse);
|
||||
}
|
||||
function format_port($port) {
|
||||
return ($port != 80 && $port != 443) ? ':'.$port : '';
|
||||
}
|
||||
function anonymize_host($host) {
|
||||
global $anon;
|
||||
if ($anon) {
|
||||
$host = preg_replace('/.*\.myunraid\.net/', '*.hash.myunraid.net', $host);
|
||||
$host = preg_replace('/.*\.unraid\.net/', 'hash.unraid.net', $host);
|
||||
}
|
||||
return $host;
|
||||
}
|
||||
function anonymize_ip($ip) {
|
||||
global $anon;
|
||||
if ($anon && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
|
||||
$ip = "[redacted]";
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
function generate_internal_host($host, $ip) {
|
||||
if (strpos($host,'.myunraid.net') !== false) {
|
||||
$host = str_replace('*', str_replace('.', '-', $ip), $host);
|
||||
}
|
||||
return $host;
|
||||
}
|
||||
function generate_external_host($host, $ip) {
|
||||
if (strpos($host,'.myunraid.net') !== false) {
|
||||
$host = str_replace('*', str_replace('.', '-', $ip), $host);
|
||||
} elseif (strpos($host,'.unraid.net') !== false) {
|
||||
$host = "www.".$host;
|
||||
}
|
||||
return $host;
|
||||
}
|
||||
function verbose_output($httpcode, $result) {
|
||||
global $cli, $verbose, $anon, $plgversion, $post, $var, $isRegistered, $myservers, $reloadNginx, $nginx, $isLegacyCert;
|
||||
global $remoteaccess;
|
||||
global $icon_warn, $icon_ok;
|
||||
if (!$cli || !$verbose) return;
|
||||
|
||||
if ($anon) echo "(Output is anonymized, use '-vv' to see full details)".PHP_EOL;
|
||||
echo "Unraid OS {$var['version']}".((strpos($plgversion, "base-") === false) ? " with My Servers plugin version {$plgversion}" : '').PHP_EOL;
|
||||
echo ($isRegistered) ? "{$icon_ok}Signed in to Unraid.net as {$myservers['remote']['username']}".PHP_EOL : "{$icon_warn}Not signed in to Unraid.net".PHP_EOL ;
|
||||
echo "Use SSL is {$nginx['NGINX_USESSL']}".PHP_EOL;
|
||||
echo (rebindDisabled()) ? "{$icon_ok}Rebind protection is disabled" : "{$icon_warn}Rebind protection is enabled";
|
||||
echo " for ".($isLegacyCert ? "unraid.net" : "myunraid.net").PHP_EOL;
|
||||
if ($post) {
|
||||
$wanip = trim(@file_get_contents("https://wanip4.unraid.net/"));
|
||||
// check the data
|
||||
$certhostname = $nginx['NGINX_CERTNAME'];
|
||||
if ($certhostname) {
|
||||
// $certhostname is $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
|
||||
$certhostip = host_lookup_ip(generate_internal_host($certhostname, $post['internalip']));
|
||||
$certhosterr = ($certhostip != $post['internalip']);
|
||||
}
|
||||
if ($post['internalhostname'] != $certhostname) {
|
||||
// $post['internalhostname'] is $nginx['NGINX_LANMDNS'] (no cert, or Server_unraid_bundle.pem) || $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
|
||||
$internalhostip = host_lookup_ip(generate_internal_host($post['internalhostname'], $post['internalip']));
|
||||
$internalhosterr = ($internalhostip != $post['internalip']);
|
||||
}
|
||||
if (!empty($post['externalhostname'])) {
|
||||
// $post['externalhostname'] is $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
|
||||
$externalhostip = host_lookup_ip(generate_external_host($post['externalhostname'], $wanip));
|
||||
$externalhosterr = ($externalhostip != $wanip);
|
||||
}
|
||||
// anonymize data. no caclulations can be done with this data beyond this point.
|
||||
if ($anon) {
|
||||
if (!empty($certhostip)) $certhostip = anonymize_ip($certhostip);
|
||||
if (!empty($certhostname)) $certhostname = anonymize_host($certhostname);
|
||||
if (!empty($internalhostip)) $internalhostip = anonymize_ip($internalhostip);
|
||||
if (!empty($externalhostip)) $externalhostip = anonymize_ip($externalhostip);
|
||||
if (!empty($wanip)) $wanip = anonymize_ip($wanip);
|
||||
if (!empty($post['internalip'])) $post['internalip'] = anonymize_ip($post['internalip']);
|
||||
if (!empty($post['internalhostname'])) $post['internalhostname'] = anonymize_host($post['internalhostname']);
|
||||
if (!empty($post['externalhostname'])) $post['externalhostname'] = anonymize_host($post['externalhostname']);
|
||||
if (!empty($post['externalport'])) $post['externalport'] = "[redacted]";
|
||||
}
|
||||
// always anonymize the keyfile
|
||||
if (!empty($post['keyfile'])) $post['keyfile'] = "[redacted]";
|
||||
// output notes
|
||||
if (!empty($post['internalprotocol']) && !empty($post['internalhostname']) && !empty($post['internalport'])) {
|
||||
$localurl = $post['internalprotocol']."://".generate_internal_host($post['internalhostname'], $post['internalip']).format_port($post['internalport']);
|
||||
echo 'Local Access url: '.$localurl.PHP_EOL;
|
||||
if ($internalhostip) {
|
||||
// $internalhostip will not be defined for .local domains, ok to skip
|
||||
echo ($internalhosterr) ? $icon_warn : $icon_ok;
|
||||
echo generate_internal_host($post['internalhostname'], $post['internalip'])." resolves to {$internalhostip}";
|
||||
echo ($internalhosterr) ? ", it should resolve to {$post['internalip']}" : "";
|
||||
echo PHP_EOL;
|
||||
}
|
||||
if ($certhostname) {
|
||||
echo ($certhosterr) ? $icon_warn : $icon_ok;
|
||||
echo generate_internal_host($certhostname, $post['internalip']).' ';
|
||||
echo ($certhostip) ? "resolves to {$certhostip}" : "does not resolve to an IP address";
|
||||
echo ($certhosterr) ? ", it should resolve to {$post['internalip']}" : "";
|
||||
echo PHP_EOL;
|
||||
}
|
||||
if ($remoteaccess == 'yes' && !empty($post['externalprotocol']) && !empty($post['externalhostname']) && !empty($post['externalport'])) {
|
||||
$remoteurl = $post['externalprotocol']."://".generate_external_host($post['externalhostname'], $wanip).format_port($post['externalport']);
|
||||
echo 'Remote Access url: '.$remoteurl.PHP_EOL;
|
||||
echo ($externalhosterr) ? $icon_warn : $icon_ok;
|
||||
echo generate_external_host($post['externalhostname'], $wanip).' ';
|
||||
echo ($externalhosterr) ? "does not resolve to an IP address" : "resolves to {$externalhostip}";
|
||||
echo PHP_EOL;
|
||||
}
|
||||
if ($reloadNginx) {
|
||||
echo "IP address changes were detected, nginx was reloaded".PHP_EOL;
|
||||
}
|
||||
}
|
||||
// output post data
|
||||
echo PHP_EOL.'Request:'.PHP_EOL;
|
||||
echo @json_encode($post, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
|
||||
}
|
||||
if ($result) {
|
||||
echo "Response (HTTP $httpcode):".PHP_EOL;
|
||||
$mutatedResult = is_array($result) ? json_encode($result) : $result;
|
||||
echo @json_encode(@json_decode($mutatedResult, true), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @name response_complete
|
||||
* @param {HTTP Response Status Code} $httpcode https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
||||
* @param {String|Array} $result - strings are assumed to be encoded JSON. Arrays will be encoded to JSON.
|
||||
* @param {String} $cli_success_msg
|
||||
*/
|
||||
function response_complete($httpcode, $result, $cli_success_msg='') {
|
||||
global $cli, $verbose;
|
||||
$mutatedResult = is_array($result) ? json_encode($result) : $result;
|
||||
if ($cli) {
|
||||
if ($verbose) verbose_output($httpcode, $result);
|
||||
$json = @json_decode($mutatedResult,true);
|
||||
if (!empty($json['error'])) {
|
||||
echo 'Error: '.$json['error'].PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
exit($cli_success_msg.PHP_EOL);
|
||||
}
|
||||
header('Content-Type: application/json');
|
||||
http_response_code($httpcode);
|
||||
exit((string)$mutatedResult);
|
||||
}
|
||||
|
||||
// This is a stub, does nothing but return success
|
||||
$cli = php_sapi_name()=='cli';
|
||||
$verbose = $anon = false;
|
||||
if ($cli && ($argc > 1) && $argv[1] == "-v") {
|
||||
$verbose = true;
|
||||
$anon = true;
|
||||
}
|
||||
if ($cli && ($argc > 1) && $argv[1] == "-vv") {
|
||||
$verbose = true;
|
||||
}
|
||||
$var = parse_ini_file('/var/local/emhttp/var.ini');
|
||||
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
|
||||
$is69 = version_compare($var['version'],"6.9.9","<");
|
||||
$reloadNginx = false;
|
||||
$dnserr = false;
|
||||
$icon_warn = "⚠️ ";
|
||||
$icon_ok = "✅ ";
|
||||
|
||||
$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg';
|
||||
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
|
||||
// ensure some vars are defined here so we don't have to test them later
|
||||
if (empty($myservers['remote']['apikey'])) {
|
||||
$myservers['remote']['apikey'] = "";
|
||||
}
|
||||
if (empty($myservers['remote']['wanaccess'])) {
|
||||
$myservers['remote']['wanaccess'] = "no";
|
||||
}
|
||||
if (empty($myservers['remote']['wanport'])) {
|
||||
$myservers['remote']['wanport'] = 443;
|
||||
}
|
||||
// remoteaccess, externalport
|
||||
if ($cli) {
|
||||
$remoteaccess = (empty($nginx['NGINX_WANFQDN'])) ? 'no' : 'yes';
|
||||
$externalport = $myservers['remote']['wanport'];
|
||||
} else {
|
||||
$remoteaccess = $_POST['remoteaccess']??'no';
|
||||
$externalport = intval($_POST['externalport']??443);
|
||||
|
||||
if ($remoteaccess != 'yes') {
|
||||
$remoteaccess = 'no';
|
||||
}
|
||||
|
||||
if ($externalport < 1 || $externalport > 65535) {
|
||||
$externalport = 443;
|
||||
}
|
||||
|
||||
if ($myservers['remote']['wanaccess'] != $remoteaccess) {
|
||||
// update the wanaccess ini value
|
||||
$orig = file_exists($myservers_flash_cfg_path) ? parse_ini_file($myservers_flash_cfg_path,true) : [];
|
||||
if (!$orig) {
|
||||
$orig = ['remote' => $myservers['remote']];
|
||||
}
|
||||
$orig['remote']['wanaccess'] = $remoteaccess;
|
||||
$text = '';
|
||||
foreach ($orig as $section => $block) {
|
||||
$pairs = "";
|
||||
foreach ($block as $key => $value) if (strlen($value)) $pairs .= "$key=\"$value\"\n";
|
||||
if ($pairs) $text .= "[$section]\n".$pairs;
|
||||
}
|
||||
if ($text) file_put_contents($myservers_flash_cfg_path, $text);
|
||||
// need nginx reload
|
||||
$reloadNginx = true;
|
||||
}
|
||||
exit("success".PHP_EOL);
|
||||
}
|
||||
$isRegistered = !empty($myservers['remote']['username']);
|
||||
|
||||
// protocols, hostnames, ports
|
||||
$internalprotocol = 'http';
|
||||
$internalport = $nginx['NGINX_PORT'];
|
||||
$internalhostname = $nginx['NGINX_LANMDNS'];
|
||||
$externalprotocol = 'https';
|
||||
// keyserver will expand *.hash.myunraid.net or add www to hash.unraid.net as needed
|
||||
$externalhostname = $nginx['NGINX_CERTNAME'];
|
||||
$isLegacyCert = preg_match('/.*\.unraid\.net$/', $nginx['NGINX_CERTNAME']);
|
||||
$isWildcardCert = preg_match('/.*\.myunraid\.net$/', $nginx['NGINX_CERTNAME']);
|
||||
$internalip = $nginx['NGINX_LANIP'];
|
||||
|
||||
if ($nginx['NGINX_USESSL']=='yes') {
|
||||
// When NGINX_USESSL is 'yes' in 6.9, it could be using either Server_unraid_bundle.pem or certificate_bundle.pem
|
||||
// When NGINX_USESSL is 'yes' in 6.10, it is is using Server_unraid_bundle.pem
|
||||
$internalprotocol = 'https';
|
||||
$internalport = $nginx['NGINX_PORTSSL'];
|
||||
if ($is69 && $nginx['NGINX_CERTNAME']) {
|
||||
// this is from certificate_bundle.pem
|
||||
$internalhostname = $nginx['NGINX_CERTNAME'];
|
||||
}
|
||||
}
|
||||
if ($nginx['NGINX_USESSL']=='auto') {
|
||||
// NGINX_USESSL cannot be 'auto' in 6.9, it is either 'yes' or 'no'
|
||||
// When NGINX_USESSL is 'auto' in 6.10, it is using certificate_bundle.pem
|
||||
$internalprotocol = 'https';
|
||||
$internalport = $nginx['NGINX_PORTSSL'];
|
||||
// keyserver will expand *.hash.myunraid.net as needed
|
||||
$internalhostname = $nginx['NGINX_CERTNAME'];
|
||||
}
|
||||
|
||||
// My Servers version
|
||||
$plgversion = file_exists("/var/log/plugins/dynamix.unraid.net.plg") ? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
|
||||
: ( file_exists("/var/log/plugins/dynamix.unraid.net.staging.plg") ? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
|
||||
: 'base-'.$var['version'] );
|
||||
|
||||
// only proceed when when signed in or when legacy unraid.net SSL certificate exists
|
||||
if (!$isRegistered && !$isLegacyCert) {
|
||||
response_complete(406, array('error' => _('Nothing to do')));
|
||||
}
|
||||
|
||||
// keyfile
|
||||
$keyfile = empty($var['regFILE']) ? false : @file_get_contents($var['regFILE']);
|
||||
if ($keyfile === false) {
|
||||
response_complete(406, array('error' => _('Registration key required')));
|
||||
}
|
||||
$keyfile = @base64_encode($keyfile);
|
||||
|
||||
// build post array
|
||||
$post = [
|
||||
'keyfile' => $keyfile,
|
||||
'plgversion' => $plgversion
|
||||
];
|
||||
if ($isLegacyCert) {
|
||||
// sign in not required to maintain local ddns for unraid.net cert
|
||||
// enable local ddns regardless of use_ssl value
|
||||
$post['internalip'] = $internalip;
|
||||
// if host.unraid.net does not resolve to the internalip and DNS Rebind Protection is disabled, disable caching
|
||||
if (host_lookup_ip(generate_internal_host($nginx['NGINX_CERTNAME'], $post['internalip'])) != $post['internalip'] && rebindDisabled()) $dnserr = true;
|
||||
}
|
||||
if ($isRegistered) {
|
||||
// if signed in, send data needed to maintain My Servers Dashboard
|
||||
$post['internalhostname'] = $internalhostname;
|
||||
$post['internalport'] = $internalport;
|
||||
$post['internalprotocol'] = $internalprotocol;
|
||||
$post['remoteaccess'] = $remoteaccess;
|
||||
$post['servercomment'] = $var['COMMENT'];
|
||||
$post['servername'] = $var['NAME'];
|
||||
if ($isWildcardCert) {
|
||||
// keyserver needs the internalip to generate the local access url
|
||||
$post['internalip'] = $internalip;
|
||||
}
|
||||
if ($remoteaccess == 'yes') {
|
||||
// include wanip in the cache file so we can track if it changes
|
||||
$post['_wanip'] = trim(@file_get_contents("https://wanip4.unraid.net/"));
|
||||
$post['externalhostname'] = $externalhostname;
|
||||
$post['externalport'] = $externalport;
|
||||
$post['externalprotocol'] = $externalprotocol;
|
||||
// if wanip.hash.myunraid.net or www.hash.unraid.net does not resolve to the wanip, disable caching
|
||||
if (host_lookup_ip(generate_external_host($post['externalhostname'], $post['_wanip'])) != $post['_wanip']) $dnserr = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Include unraid-api report
|
||||
$unraidreport = [];
|
||||
if (file_exists('/usr/local/sbin/unraid-api')) {
|
||||
$jsonString = trim(@exec("/usr/local/sbin/unraid-api report --json 2>/dev/null"));
|
||||
$unraidreport = @json_decode($jsonString, true);
|
||||
if ($unraidreport === false) {
|
||||
$post['unraidreport'] = $jsonString;
|
||||
} else {
|
||||
// remove fields we don't need to submit
|
||||
unset($unraidreport['servers']);
|
||||
}
|
||||
} elseif (strpos($plgversion, "base-") === false) {
|
||||
// The plugin is installed but the api doesn't exist. This is a failed install. Generate basic troubleshooting data.
|
||||
if (file_exists('/boot/config/plugins/dynamix.my.servers/env')) {
|
||||
@extract(parse_ini_file('/boot/config/plugins/dynamix.my.servers/env',true));
|
||||
}
|
||||
if (empty($env)) {
|
||||
$env = "production";
|
||||
}
|
||||
$unraidreport['os']['version'] = $var['version'];
|
||||
$unraidreport['api']['version'] = "failed install";
|
||||
$unraidreport['api']['status'] = "missing";
|
||||
$unraidreport['api']['environment'] = $env;
|
||||
$unraidreport['relay']['status'] = "disconnected";
|
||||
$unraidreport['minigraph']['status'] = "disconnected";
|
||||
if ($isRegistered) {
|
||||
$unraidreport['myServers']['status'] = "authenticated";
|
||||
$unraidreport['myServers']['myServersUsername'] = $myservers['remote']['username'];
|
||||
} else {
|
||||
$unraidreport['myServers']['status'] = "signed out";
|
||||
}
|
||||
$unraidreport['apiKey'] = (empty($myservers['remote']['apikey'])) ? "invalid" : "exists";
|
||||
}
|
||||
|
||||
if (!empty($unraidreport)) {
|
||||
// include unraid-api crash logs
|
||||
$crashLog = '/var/log/unraid-api/crash.json';
|
||||
$crashAge = 0;
|
||||
if (file_exists($crashLog)) {
|
||||
$crashTime = filemtime($crashLog);
|
||||
$crashAge = time() - $crashTime; // age of crashLog in seconds
|
||||
$crashDetails = @json_decode(@file_get_contents($crashLog), true);
|
||||
if (empty($crashDetails['apiVersion']) && $crashAge < 30*60) {
|
||||
// found a recent crash log without an apiVersion, assume was created by current version of api
|
||||
$crashDetails['apiVersion'] = $unraidreport['api']['version'];
|
||||
// overwrite the crash log so it will always have the apiVersion
|
||||
file_put_contents($crashLog, json_encode($crashDetails, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
// reset to original timestamp so crashAge remains accurate
|
||||
touch($crashLog, $crashTime);
|
||||
}
|
||||
$unraidreport['crashAge'] = $crashAge;
|
||||
$unraidreport['crashLogs'] = $crashDetails;
|
||||
}
|
||||
|
||||
// add flash backup status
|
||||
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
|
||||
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
|
||||
if (empty($flashbackup_status['activated'])) {
|
||||
$flashbackup_status['activated'] = "";
|
||||
}
|
||||
if (empty($flashbackup_status['error'])) {
|
||||
$flashbackup_status['error'] = "";
|
||||
}
|
||||
$unraidreport['flashbackup']['activated'] = ($flashbackup_status['activated']) ? "yes" : "no";
|
||||
$unraidreport['flashbackup']['error'] = ($flashbackup_status['error']) ? $flashbackup_status['error'] : "no";
|
||||
|
||||
// add unraidreport to payload
|
||||
$post['unraidreport'] = json_encode($unraidreport);
|
||||
|
||||
// if the api is stopped and there are no crashLogs, or any crashLogs are more than maxCrashAge, start the api
|
||||
$maxCrashAge = 1*60*60; // 1 hour
|
||||
if ($unraidreport['api']['status'] == 'stopped' && (empty($unraidreport['crashLogs']) || $crashAge > $maxCrashAge)) {
|
||||
exec("echo \"/usr/local/sbin/unraid-api start\" | at -M now >/dev/null 2>&1");
|
||||
}
|
||||
}
|
||||
|
||||
// if remoteaccess is enabled in 6.10.0-rc3+ and WANIP has changed since nginx started, reload nginx
|
||||
if (isset($post['_wanip']) && ($post['_wanip'] != $nginx['NGINX_WANIP']) && version_compare($var['version'],"6.10.0-rc2",">")) $reloadNginx = true;
|
||||
// if remoteaccess is currently disabled (perhaps because a wanip was not available when nginx was started)
|
||||
// BUT the system is configured to have it enabled AND a wanip is now available
|
||||
// then reload nginx
|
||||
if ($remoteaccess == 'no' && $nginx['NGINX_WANACCESS'] == 'yes' && !empty(trim(@file_get_contents("https://wanip4.unraid.net/")))) $reloadNginx = true;
|
||||
if ($reloadNginx) {
|
||||
exec("/etc/rc.d/rc.nginx reload &>/dev/null");
|
||||
}
|
||||
|
||||
// maxage is 36 hours
|
||||
$maxage = 36*60*60;
|
||||
if ($dnserr || $verbose) $maxage = 0;
|
||||
$datafile = "/tmp/UpdateDNS.txt";
|
||||
$datafiletmp = "/tmp/UpdateDNS.txt.new";
|
||||
$dataprev = @file_get_contents($datafile) ?: '';
|
||||
$datanew = implode("\n",$post)."\n";
|
||||
if ($datanew == $dataprev && (time()-filemtime($datafile) < $maxage)) {
|
||||
response_complete(204, null, _('No change to report'));
|
||||
}
|
||||
file_put_contents($datafiletmp,$datanew);
|
||||
rename($datafiletmp, $datafile);
|
||||
|
||||
// do not submit the wanip, it will be captured from the submission if needed for remote access
|
||||
unset($post['_wanip']);
|
||||
|
||||
// report necessary server details to limetech for DNS updates
|
||||
$ch = curl_init('https://keys.lime-technology.com/account/server/register');
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$result = curl_exec($ch);
|
||||
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ( ($result === false) || ($httpcode != "200") ) {
|
||||
// delete cache file to retry submission on next run
|
||||
@unlink($datafile);
|
||||
response_complete($httpcode ?? "500", array('error' => $error));
|
||||
}
|
||||
|
||||
response_complete($httpcode, $result, _('success'));
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(204);
|
||||
exit(0);
|
||||
?>
|
||||
|
||||
@@ -2,4 +2,5 @@ VITE_ACCOUNT=https://localhost:8008
|
||||
VITE_CONNECT=https://connect.myunraid.net
|
||||
VITE_UNRAID_NET=https://unraid.ddev.site
|
||||
VITE_CALLBACK_KEY=aNotSoSecretKeyUsedToObfuscateQueryParams
|
||||
VITE_ALLOW_CONSOLE_LOGS=false
|
||||
VITE_ALLOW_CONSOLE_LOGS=false
|
||||
VITE_WEBGUI=http://localhost
|
||||
@@ -1,26 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { readFileSync } = require('fs');
|
||||
const { parse } = require('dotenv');
|
||||
|
||||
const envConfig = parse(readFileSync('.env'));
|
||||
for (const k in envConfig) {
|
||||
process.env[k] = envConfig[k];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extends: ['@nuxtjs/eslint-config-typescript'],
|
||||
ignorePatterns: ['composables/gql/'],
|
||||
rules: {
|
||||
'comma-dangle': ['warn', 'only-multiline'],
|
||||
semi: ['error', 'always'],
|
||||
quotes: ['warn', 'single'],
|
||||
'no-console': (process.env.NODE_ENV === 'production' ? 'error' : 'off'),
|
||||
'no-debugger': (process.env.NODE_ENV === 'production' ? 'error' : 'off'),
|
||||
'@typescript-eslint/no-unused-vars': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
|
||||
'max-len': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/v-on-event-hyphenation': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'no-fallthrough': 'off',
|
||||
}
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
18.17.1
|
||||
18.19.1
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
Server,
|
||||
ServerState,
|
||||
// ServerUpdateOsResponse,
|
||||
} from '~/types/server';
|
||||
} from "~/types/server";
|
||||
|
||||
// dayjs plugins
|
||||
// extend(customParseFormat);
|
||||
@@ -24,28 +24,14 @@ import type {
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// '1111-1111-5GDB-123412341234' Starter.key = TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg
|
||||
const staticGuid = '1111-1111-5GDB-123412341234';
|
||||
|
||||
// const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
|
||||
// const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered
|
||||
// const regWizTime = `1616711990500_${randomGuid}`;
|
||||
// const blacklistedGuid = '154B-00EE-0700-9B50CF819816';
|
||||
|
||||
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
|
||||
const twentyDaysAgo = Date.now() - 20 * 24 * 60 * 60 * 1000; // 20 days ago
|
||||
const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000; // 2 days ago
|
||||
// const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; // 1 day ago
|
||||
const oneHourFromNow = Date.now() + 60 * 60 * 1000; // 1 hour from now
|
||||
// const oneDayFromNow = Date.now() + 24 * 60 * 60 * 1000; // 1 day from now
|
||||
let expireTime = 0;
|
||||
let regExp: number | undefined;
|
||||
|
||||
// ENOKEYFILE
|
||||
// TRIAL
|
||||
// BASIC
|
||||
// PLUS
|
||||
// PRO
|
||||
// STARTER
|
||||
// UNLEASHED
|
||||
// LIFETIME
|
||||
// EEXPIRED
|
||||
// EGUID
|
||||
// EGUID1
|
||||
@@ -57,54 +43,74 @@ let regExp: number | undefined;
|
||||
// EBLACKLISTED1
|
||||
// EBLACKLISTED2
|
||||
// ENOCONN
|
||||
const state: ServerState = 'STARTER';
|
||||
let regDev = 0;
|
||||
let regTy = '';
|
||||
|
||||
// '1111-1111-5GDB-123412341234' Starter.key = TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg
|
||||
|
||||
const state: ServerState = "BASIC" as ServerState;
|
||||
const currentFlashGuid = "1111-1111-CFXF-TEST1234ZACK"; // this is the flash drive that's been booted from
|
||||
const regGuid = "1111-1111-CFXF-TEST1234ZACK"; // this guid is registered in key server
|
||||
const keyfileBase64 = "asdf"; // @todo raycast download key to base64
|
||||
|
||||
// const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
|
||||
// const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered
|
||||
// const regWizTime = `1616711990500_${randomGuid}`;
|
||||
// const blacklistedGuid = '154B-00EE-0700-9B50CF819816';
|
||||
|
||||
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
|
||||
// const twentyDaysAgo = Date.now() - 20 * 24 * 60 * 60 * 1000; // 20 days ago
|
||||
const ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000; // 90 days ago
|
||||
const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000; // 2 days ago
|
||||
// const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; // 1 day ago
|
||||
const oneHourFromNow = Date.now() + 60 * 60 * 1000; // 1 hour from now
|
||||
// const oneDayFromNow = Date.now() + 24 * 60 * 60 * 1000; // 1 day from now
|
||||
let expireTime = 0;
|
||||
let regExp: number | undefined;
|
||||
|
||||
let regDevs = 0;
|
||||
let regTy = "";
|
||||
switch (state) {
|
||||
// @ts-ignore
|
||||
case 'EEXPIRED':
|
||||
case "EEXPIRED":
|
||||
expireTime = uptime; // 1 hour ago
|
||||
// @ts-ignore
|
||||
case 'ENOCONN':
|
||||
// @ts-ignore
|
||||
case 'TRIAL':
|
||||
break;
|
||||
case "ENOCONN":
|
||||
break;
|
||||
case "TRIAL":
|
||||
expireTime = oneHourFromNow; // in 1 hour
|
||||
regTy = 'Trial';
|
||||
// @ts-ignore
|
||||
case 'BASIC':
|
||||
regDev = 6;
|
||||
// @ts-ignore
|
||||
case 'PLUS':
|
||||
regDev = 12;
|
||||
// @ts-ignore
|
||||
case 'PRO':
|
||||
// @ts-ignore
|
||||
case 'STARTER':
|
||||
regDev = 4;
|
||||
// regExp = oneHourFromNow;
|
||||
// regExp = oneDayFromNow;
|
||||
regExp = twentyDaysAgo;
|
||||
// regExp = uptime;
|
||||
// regExp = 1696363920000; // nori.local's expiration
|
||||
// @ts-ignore
|
||||
case 'UNLEASHED':
|
||||
// regExp = oneHourFromNow;
|
||||
// regExp = oneDayFromNow;
|
||||
// regExp = oneDayAgo;
|
||||
// regExp = uptime;
|
||||
// regExp = 1696363920000; // nori.local's expiration
|
||||
// @ts-ignore
|
||||
case 'LIFETIME':
|
||||
if (regDev === 0) { regDev = 99999; }
|
||||
if (regTy === '') { regTy = state.charAt(0).toUpperCase() + state.substring(1).toLowerCase(); } // title case
|
||||
regTy = "Trial";
|
||||
break;
|
||||
case "BASIC":
|
||||
regDevs = 6;
|
||||
regTy = "Basic";
|
||||
break;
|
||||
case "PLUS":
|
||||
regDevs = 12;
|
||||
regTy = "Plus";
|
||||
break;
|
||||
case "PRO":
|
||||
regDevs = -1;
|
||||
regTy = "Pro";
|
||||
break;
|
||||
case "STARTER":
|
||||
regDevs = 6;
|
||||
regExp = ninetyDaysAgo;
|
||||
regTy = "Starter";
|
||||
break;
|
||||
case "UNLEASHED":
|
||||
regDevs = -1;
|
||||
regExp = ninetyDaysAgo;
|
||||
regTy = "Unleashed";
|
||||
break;
|
||||
case "LIFETIME":
|
||||
regDevs = -1;
|
||||
regTy = "Lifetime";
|
||||
break;
|
||||
}
|
||||
|
||||
const connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
|
||||
// const connectPluginInstalled = '';
|
||||
// const connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
|
||||
const connectPluginInstalled = "";
|
||||
|
||||
const osVersion = '6.13.0-beta0.22';
|
||||
const osVersionBranch = 'preview';
|
||||
const osVersion = "6.12.8";
|
||||
const osVersionBranch = "stable";
|
||||
// const parsedRegExp = regExp ? dayjs(regExp).format('YYYY-MM-DD') : undefined;
|
||||
|
||||
// const mimicWebguiUnraidCheck = async (): Promise<ServerUpdateOsResponse | undefined> => {
|
||||
@@ -129,56 +135,60 @@ const osVersionBranch = 'preview';
|
||||
// };
|
||||
|
||||
export const serverState: Server = {
|
||||
apiKey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
|
||||
avatar: 'https://source.unsplash.com/300x300/?portrait',
|
||||
apiKey: "unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810",
|
||||
avatar: "https://source.unsplash.com/300x300/?portrait",
|
||||
config: {
|
||||
// error: 'INVALID',
|
||||
valid: true,
|
||||
error: null,
|
||||
valid: false,
|
||||
},
|
||||
connectPluginInstalled,
|
||||
description: 'DevServer9000',
|
||||
description: "DevServer9000",
|
||||
deviceCount: 3,
|
||||
expireTime,
|
||||
flashBackupActivated: !!connectPluginInstalled,
|
||||
flashProduct: 'SanDisk_3.2Gen1',
|
||||
flashVendor: 'USB',
|
||||
guid: staticGuid,
|
||||
flashProduct: "SanDisk_3.2Gen1",
|
||||
flashVendor: "USB",
|
||||
guid: currentFlashGuid,
|
||||
// "guid": "0781-5583-8355-81071A2B0211",
|
||||
inIframe: false,
|
||||
// keyfile: 'DUMMY_KEYFILE',
|
||||
keyfile: 'TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg',
|
||||
lanIp: '192.168.254.36',
|
||||
license: '',
|
||||
locale: 'en_US', // en_US, ja
|
||||
name: 'dev-static',
|
||||
keyfile: keyfileBase64,
|
||||
lanIp: "192.168.254.36",
|
||||
license: "",
|
||||
locale: "en_US", // en_US, ja
|
||||
name: "dev-static",
|
||||
osVersion,
|
||||
osVersionBranch,
|
||||
// registered: connectPluginInstalled ? true : false,
|
||||
registered: false,
|
||||
regGen: 0,
|
||||
regTm: twoDaysAgo,
|
||||
regTo: 'Zack Spear',
|
||||
regTo: "Zack Spear",
|
||||
regTy,
|
||||
regDevs,
|
||||
regExp,
|
||||
// "regGuid": "0781-5583-8355-81071A2B0211",
|
||||
site: 'http://localhost:4321',
|
||||
regGuid,
|
||||
site: "http://localhost:4321",
|
||||
state,
|
||||
theme: {
|
||||
banner: false,
|
||||
bannerGradient: false,
|
||||
bgColor: '',
|
||||
bgColor: "",
|
||||
descriptionShow: true,
|
||||
metaColor: '',
|
||||
name: 'white',
|
||||
textColor: ''
|
||||
},
|
||||
updateOsResponse: {
|
||||
version: '6.13.0-beta0.27',
|
||||
name: 'Unraid 6.13.0-beta0.27',
|
||||
date: '2023-12-13',
|
||||
isNewer: true,
|
||||
metaColor: "",
|
||||
name: "white",
|
||||
textColor: "",
|
||||
},
|
||||
// updateOsResponse: {
|
||||
// version: '6.12.6',
|
||||
// name: 'Unraid 6.12.6',
|
||||
// date: '2023-12-13',
|
||||
// isNewer: true,
|
||||
// isEligible: false,
|
||||
// changelog: 'https://docs.unraid.net/unraid-os/release-notes/6.12.6/',
|
||||
// sha256: '2f5debaf80549029cf6dfab0db59180e7e3391c059e6521aace7971419c9c4bf',
|
||||
// },
|
||||
uptime,
|
||||
username: 'zspearmint',
|
||||
wanFQDN: ''
|
||||
username: "zspearmint",
|
||||
wanFQDN: "",
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ Tag="globe"
|
||||
/**
|
||||
* @todo create web component env switcher liker upcEnv(). If we utilize manifest.json then we'll be switching its path.
|
||||
*/
|
||||
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
||||
require_once "$docroot/webGui/include/Wrappers.php";
|
||||
$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg';
|
||||
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
|
||||
// print_r($mystatus);
|
||||
@@ -142,7 +144,7 @@ if ($display['theme'] === 'black' || $display['theme'] === 'azure') {
|
||||
<unraid-key-actions></connect-key-actions>
|
||||
</div>
|
||||
<div class="ComponentWrapper">
|
||||
<unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
|
||||
<unraid-wan-ip-check php-wan-ip="<?=http_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
const nuxtApp = useNuxtApp();
|
||||
const { registerEntry } = useCustomElements();
|
||||
onBeforeMount(() => {
|
||||
// @ts-ignore
|
||||
nuxtApp.$customElements.registerEntry('UnraidComponents');
|
||||
registerEntry('UnraidComponents');
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
// eslint-disable vue/no-v-html
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -25,7 +26,7 @@ const { authAction, stateData } = storeToRefs(serverStore);
|
||||
size="12px"
|
||||
:text="t(authAction.text)"
|
||||
:title="authAction?.title ? t(authAction?.title) : undefined"
|
||||
@click="authAction.click()"
|
||||
@click="authAction.click?.()"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
type Component,
|
||||
} from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { ButtonProps } from '~/types/ui/button';
|
||||
|
||||
export interface ButtonProps {
|
||||
btnStyle?: 'black' | 'fill' | 'gray' | 'outline' | 'outline-black' | 'outline-white' | 'underline' | 'white';
|
||||
btnType?: 'button' | 'submit' | 'reset';
|
||||
click?: () => void;
|
||||
disabled?: boolean;
|
||||
download?: boolean;
|
||||
external?: boolean;
|
||||
href?: string;
|
||||
icon?: Component;
|
||||
iconRight?: Component;
|
||||
iconRightHoverDisplay?: boolean;
|
||||
// iconRightHoverAnimate?: boolean;
|
||||
size?: '12px' | '14px' | '16px' | '18px' | '20px' | '24px';
|
||||
text?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
btnStyle: 'fill',
|
||||
btnType: 'button',
|
||||
class: undefined,
|
||||
click: undefined,
|
||||
href: undefined,
|
||||
icon: undefined,
|
||||
iconRight: undefined,
|
||||
iconRightHoverDisplay: false,
|
||||
// iconRightHoverAnimate: true,
|
||||
noPadding: false,
|
||||
size: '16px',
|
||||
text: '',
|
||||
title: '',
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
@@ -63,6 +50,9 @@ const classes = computed(() => {
|
||||
case 'underline':
|
||||
buttonColors = 'opacity-75 underline border-transparent transition hover:text-alpha hover:bg-beta hover:border-beta focus:text-alpha focus:bg-beta focus:border-beta hover:opacity-100 focus:opacity-100';
|
||||
break;
|
||||
case 'underline-hover-red':
|
||||
buttonColors = 'opacity-75 underline border-transparent transition hover:text-white hover:bg-unraid-red hover:border-unraid-red focus:text-white focus:bg-unraid-red focus:border-unraid-red hover:opacity-100 focus:opacity-100';
|
||||
break;
|
||||
case 'white':
|
||||
buttonColors = 'text-black bg-white transition hover:bg-grey focus:bg-grey';
|
||||
break;
|
||||
@@ -70,33 +60,35 @@ const classes = computed(() => {
|
||||
|
||||
switch (props.size) {
|
||||
case '12px':
|
||||
buttonSize = 'text-12px p-8px gap-4px';
|
||||
buttonSize = `text-12px ${props.noPadding ? 'p-0' : 'p-8px'} gap-4px`;
|
||||
iconSize = 'w-12px';
|
||||
break;
|
||||
case '14px':
|
||||
buttonSize = 'text-14px p-8px gap-8px';
|
||||
buttonSize = `text-14px ${props.noPadding ? 'p-0' : 'p-8px'} gap-8px`;
|
||||
iconSize = 'w-14px';
|
||||
break;
|
||||
case '16px':
|
||||
buttonSize = 'text-16px p-12px gap-8px';
|
||||
buttonSize = `text-16px ${props.noPadding ? 'p-0' : 'p-12px'} gap-8px`;
|
||||
iconSize = 'w-16px';
|
||||
break;
|
||||
case '18px':
|
||||
buttonSize = 'text-18px p-12px gap-8px';
|
||||
buttonSize = `text-18px ${props.noPadding ? 'p-0' : 'p-12px'} gap-8px`;
|
||||
iconSize = 'w-18px';
|
||||
break;
|
||||
case '20px':
|
||||
buttonSize = 'text-20px p-16px gap-8px';
|
||||
buttonSize = `text-20px ${props.noPadding ? 'p-0' : 'p-16px'} gap-8px`;
|
||||
iconSize = 'w-20px';
|
||||
break;
|
||||
case '24px':
|
||||
buttonSize = 'text-24px p-16px gap-8px';
|
||||
buttonSize = `text-24px ${props.noPadding ? 'p-0' : 'p-16px'} gap-8px`;
|
||||
iconSize = 'w-24px';
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
button: `${buttonSize} ${buttonColors} ${buttonDefaults}`,
|
||||
button: props.btnStyle === 'none'
|
||||
? `${buttonSize} ${props.class}`
|
||||
: `${buttonSize} ${buttonColors} ${buttonDefaults} ${props.class}`,
|
||||
icon: `${iconSize} fill-current flex-shrink-0`,
|
||||
};
|
||||
});
|
||||
@@ -111,6 +103,7 @@ const classes = computed(() => {
|
||||
:target="external ? '_blank' : ''"
|
||||
:type="!href ? btnType : ''"
|
||||
:class="classes.button"
|
||||
:title="title"
|
||||
@click="click ?? $emit('click')"
|
||||
>
|
||||
<div
|
||||
|
||||
47
web/components/ConnectSettings/AllowedOrigins.vue
Normal file
47
web/components/ConnectSettings/AllowedOrigins.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { useUnraidApiSettingsStore } from '~/store/unraidApiSettings';
|
||||
|
||||
const apiSettingsStore = useUnraidApiSettingsStore();
|
||||
|
||||
const originsText = ref<string>('');
|
||||
const errors = ref<string[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const allowedOriginsSettings = await apiSettingsStore.getAllowedOrigins();
|
||||
originsText.value = allowedOriginsSettings.join(', ');
|
||||
});
|
||||
|
||||
const origins = computed<string[]>(() => {
|
||||
console.log('originsText.value: ' + originsText.value);
|
||||
const newOrigins: string[] = [];
|
||||
if (originsText.value) {
|
||||
originsText.value.split(',').forEach((origin) => {
|
||||
try {
|
||||
const newUrl = new URL(origin.trim());
|
||||
newOrigins.push(newUrl.toString());
|
||||
} catch (e) {
|
||||
errors.value.push(`Invalid origin: ${origin}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newOrigins;
|
||||
});
|
||||
|
||||
const setAllowedOrigins = () => {
|
||||
apiSettingsStore.setAllowedOrigins(origins.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<h2>Setup Allowed Origins</h2>
|
||||
<input v-model="originsText" type="text" placeholder="Input Comma Separated List of URLs">
|
||||
<button type="button" @click="setAllowedOrigins()">
|
||||
Set Allowed Origins
|
||||
</button>
|
||||
<div v-for="(error, index) of errors" :key="index">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
17
web/components/ConnectSettings/ConnectSettings.ce.vue
Normal file
17
web/components/ConnectSettings/ConnectSettings.ce.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
// import { useI18n } from 'vue-i18n';
|
||||
|
||||
// const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthCe />
|
||||
<!-- @todo: flashback up -->
|
||||
<WanIpCheckCe />
|
||||
<ConnectSettingsRemoteAccess />
|
||||
<ConnectSettingsAllowedOrigins />
|
||||
<DownloadApiLogsCe />
|
||||
</template>
|
||||
60
web/components/ConnectSettings/RemoteAccess.vue
Normal file
60
web/components/ConnectSettings/RemoteAccess.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" setup>
|
||||
import { WAN_ACCESS_TYPE, WAN_FORWARD_TYPE } from '~/composables/gql/graphql';
|
||||
import { useUnraidApiSettingsStore } from '~/store/unraidApiSettings';
|
||||
|
||||
const apiSettingsStore = useUnraidApiSettingsStore();
|
||||
|
||||
const accessType = ref<WAN_ACCESS_TYPE>(WAN_ACCESS_TYPE.Disabled);
|
||||
const forwardType = ref<WAN_FORWARD_TYPE | null>(null);
|
||||
const port = ref<number | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
const remoteAccessSettings = await apiSettingsStore.getRemoteAccess();
|
||||
accessType.value =
|
||||
remoteAccessSettings?.accessType ?? WAN_ACCESS_TYPE.Disabled;
|
||||
forwardType.value = remoteAccessSettings?.forwardType ?? null;
|
||||
port.value = remoteAccessSettings?.port ?? null;
|
||||
});
|
||||
|
||||
const setRemoteAccess = () => {
|
||||
apiSettingsStore.setupRemoteAccess({
|
||||
accessType: accessType.value,
|
||||
...(forwardType.value ? { forwardType: forwardType.value } : {}),
|
||||
...(port.value ? { port: port.value } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
watch(accessType, (newVal) => {
|
||||
if (newVal !== WAN_ACCESS_TYPE.Disabled) {
|
||||
forwardType.value = WAN_FORWARD_TYPE.Static;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<h2>Setup Remote Access</h2>
|
||||
<label for="forwardType">Forward Type</label>
|
||||
<select id="forwardType" v-model="accessType">
|
||||
<option v-for="(val, index) in Object.values(WAN_ACCESS_TYPE)" :key="index" :value="val">
|
||||
{{ val }}
|
||||
</option>
|
||||
</select>
|
||||
<template v-if="accessType !== WAN_ACCESS_TYPE.Disabled">
|
||||
<label for="forwardType">Forward Type</label>
|
||||
<select id="forwardType" v-model="forwardType">
|
||||
<option v-for="(val, index) in Object.values(WAN_FORWARD_TYPE)" :key="index" :value="val">
|
||||
{{ val }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
<template v-if="forwardType === WAN_FORWARD_TYPE.Static && accessType !== WAN_ACCESS_TYPE.Disabled">
|
||||
<label for="port">Port</label>
|
||||
<input id="port" v-model="port" type="number">
|
||||
</template>
|
||||
|
||||
<button @click="setRemoteAccess">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -39,7 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const { rebootType } = storeToRefs(serverStore);
|
||||
const { rebootType, osVersionBranch } = storeToRefs(serverStore);
|
||||
|
||||
const subtitle = computed(() => {
|
||||
if (rebootType.value === 'update') {
|
||||
@@ -48,6 +48,8 @@ const subtitle = computed(() => {
|
||||
return '';
|
||||
});
|
||||
|
||||
const showExternalDowngrade = computed(() => osVersionBranch.value !== 'stable');
|
||||
|
||||
onBeforeMount(() => {
|
||||
serverStore.setRebootVersion(props.rebootVersion);
|
||||
});
|
||||
@@ -59,6 +61,7 @@ onBeforeMount(() => {
|
||||
:title="t('Downgrade Unraid OS')"
|
||||
:subtitle="subtitle"
|
||||
:downgrade-not-available="restoreVersion === '' && rebootType === ''"
|
||||
:show-external-downgrade="showExternalDowngrade"
|
||||
:t="t"
|
||||
/>
|
||||
<UpdateOsDowngrade
|
||||
|
||||
@@ -14,6 +14,8 @@ import { WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
import type { UiBadgeProps, UiBadgePropsColor } from '~/types/ui/badge';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -21,20 +23,48 @@ const serverStore = useServerStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
|
||||
const { osVersion, rebootType } = storeToRefs(serverStore);
|
||||
const { available } = storeToRefs(updateOsStore);
|
||||
const { ineligibleText, rebootTypeText } = storeToRefs(updateOsActionsStore);
|
||||
const { osVersion, rebootType, stateDataError } = storeToRefs(serverStore);
|
||||
const { available, availableWithRenewal } = storeToRefs(updateOsStore);
|
||||
const { rebootTypeText } = storeToRefs(updateOsActionsStore);
|
||||
|
||||
const showUpdateAvailable = computed(() => !ineligibleText.value && available.value && rebootType.value === '');
|
||||
export interface UpdateOsStatus extends UserProfileLink {
|
||||
badge: UiBadgeProps;
|
||||
}
|
||||
const updateOsStatus = computed(() => {
|
||||
if (stateDataError.value) { // only allowed to update when server is does not have a state error
|
||||
return null;
|
||||
}
|
||||
|
||||
const rebootRequiredLink = computed(() => {
|
||||
if (rebootType.value === 'downgrade') {
|
||||
return WEBGUI_TOOLS_DOWNGRADE.toString();
|
||||
if (rebootTypeText.value) {
|
||||
return {
|
||||
badge: {
|
||||
color: 'yellow' as UiBadgePropsColor,
|
||||
icon: ExclamationTriangleIcon,
|
||||
},
|
||||
href: rebootType.value === 'downgrade'
|
||||
? WEBGUI_TOOLS_DOWNGRADE.toString()
|
||||
: WEBGUI_TOOLS_UPDATE.toString(),
|
||||
text: t(rebootTypeText.value),
|
||||
};
|
||||
}
|
||||
if (rebootType.value === 'thirdPartyDriversDownloading' || rebootType.value === 'update') {
|
||||
return WEBGUI_TOOLS_UPDATE.toString();
|
||||
|
||||
if (availableWithRenewal.value || available.value) {
|
||||
return {
|
||||
badge: {
|
||||
color: 'orange' as UiBadgePropsColor,
|
||||
icon: BellAlertIcon,
|
||||
},
|
||||
click: () => { updateOsStore.setModalOpen(true); },
|
||||
text: availableWithRenewal.value
|
||||
? t('Update Released')
|
||||
: t('Update Available'),
|
||||
title: availableWithRenewal.value
|
||||
? t('Unraid OS {0} Released', [availableWithRenewal.value])
|
||||
: t('Unraid OS {0} Update Available', [available.value]),
|
||||
};
|
||||
}
|
||||
return '';
|
||||
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -56,33 +86,26 @@ const rebootRequiredLink = computed(() => {
|
||||
</UiBadge>
|
||||
</button>
|
||||
|
||||
<a
|
||||
v-if="showUpdateAvailable"
|
||||
:href="WEBGUI_TOOLS_UPDATE.toString()"
|
||||
<component
|
||||
:is="updateOsStatus.href ? 'a' : 'button'"
|
||||
v-if="updateOsStatus"
|
||||
:href="updateOsStatus.href ?? undefined"
|
||||
:title="updateOsStatus.title ?? undefined"
|
||||
class="group"
|
||||
:title="t('Unraid OS {0} Update Available', [available])"
|
||||
@click="updateOsStatus.click?.()"
|
||||
>
|
||||
<UiBadge
|
||||
color="orange"
|
||||
:icon="BellAlertIcon"
|
||||
v-if="updateOsStatus.badge"
|
||||
:color="updateOsStatus.badge.color"
|
||||
:icon="updateOsStatus.badge.icon"
|
||||
size="12px"
|
||||
>
|
||||
{{ t('Update Available') }}
|
||||
{{ updateOsStatus.text }}
|
||||
</UiBadge>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="rebootRequiredLink"
|
||||
:href="rebootRequiredLink"
|
||||
class="group"
|
||||
>
|
||||
<UiBadge
|
||||
:color="'yellow'"
|
||||
:icon="ExclamationTriangleIcon"
|
||||
size="12px"
|
||||
>
|
||||
{{ t(rebootTypeText) }}
|
||||
</UiBadge>
|
||||
</a>
|
||||
<template v-else>
|
||||
{{ updateOsStatus.text }}
|
||||
</template>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createI18n, I18nInjectionKey } from 'vue-i18n';
|
||||
|
||||
import { disableProductionConsoleLogs } from '~/helpers/functions';
|
||||
|
||||
import en_US from '~/locales/en_US.json'; // eslint-disable-line camelcase
|
||||
import en_US from '~/locales/en_US.json';
|
||||
disableProductionConsoleLogs();
|
||||
// import ja from '~/locales/ja.json';
|
||||
|
||||
@@ -17,6 +17,7 @@ let nonDefaultLocale = false;
|
||||
* Unfortunately, this was the only way I could get the data from PHP to vue-i18n :(
|
||||
* I tried using i18n.setLocaleMessage() but it didn't work no matter what I tried.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const windowLocaleData = (window as any).LOCALE_DATA || null;
|
||||
if (windowLocaleData) {
|
||||
try {
|
||||
@@ -33,7 +34,7 @@ const i18n = createI18n<false>({
|
||||
locale: nonDefaultLocale ? parsedLocale : defaultLocale,
|
||||
fallbackLocale: defaultLocale,
|
||||
messages: {
|
||||
en_US, // eslint-disable-line camelcase
|
||||
en_US,
|
||||
// ja,
|
||||
...(nonDefaultLocale ? parsedMessages : {}),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { ServerStateDataAction } from '~/types/server';
|
||||
@@ -10,7 +11,7 @@ const props = withDefaults(defineProps<{
|
||||
filterBy?: string[] | undefined;
|
||||
filterOut?: string[] | undefined;
|
||||
maxWidth?: boolean;
|
||||
t: any;
|
||||
t: ComposerTranslation;
|
||||
}>(), {
|
||||
actions: undefined,
|
||||
filterBy: undefined,
|
||||
@@ -49,7 +50,7 @@ const filteredKeyActions = computed((): ServerStateDataAction[] | undefined => {
|
||||
:icon-right-hover-display="true"
|
||||
:text="t(action.text)"
|
||||
:title="action.title ? t(action.title) : undefined"
|
||||
@click="action.click()"
|
||||
@click="action.click?.()"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { TransitionChild, TransitionRoot } from '@headlessui/vue';
|
||||
import { XMarkIcon } from '@heroicons/vue/24/outline';
|
||||
import useFocusTrap from '~/composables/useFocusTrap';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
export interface Props {
|
||||
centerContent?: boolean;
|
||||
description?: string;
|
||||
error?: boolean;
|
||||
maxWidth?: string;
|
||||
open?: boolean;
|
||||
showCloseX?: boolean;
|
||||
success?: boolean;
|
||||
t: any;
|
||||
t: ComposerTranslation;
|
||||
tallContent?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
centerContent: true,
|
||||
description: '',
|
||||
error: false,
|
||||
maxWidth: 'sm:max-w-lg',
|
||||
open: false,
|
||||
showCloseX: false,
|
||||
success: false,
|
||||
tallContent: false,
|
||||
title: '',
|
||||
});
|
||||
watchEffect(() => {
|
||||
// toggle body scrollability
|
||||
return props.open
|
||||
? document.body.style.setProperty('overflow', 'hidden')
|
||||
: document.body.style.removeProperty('overflow');
|
||||
if (props.open) {
|
||||
document.body.style.setProperty('overflow', 'hidden')
|
||||
} else {
|
||||
document.body.style.removeProperty('overflow');
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
@@ -34,8 +40,6 @@ const closeModal = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const { trapRef } = useFocusTrap();
|
||||
|
||||
const ariaLablledById = computed((): string|undefined => props.title ? `ModalTitle-${Math.random()}`.replace('0.', '') : undefined);
|
||||
|
||||
/**
|
||||
@@ -46,7 +50,6 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
||||
<template>
|
||||
<TransitionRoot appear :show="open" as="template">
|
||||
<div
|
||||
ref="trapRef"
|
||||
class="fixed inset-0 z-10 overflow-y-auto"
|
||||
role="dialog"
|
||||
aria-dialog="true"
|
||||
@@ -54,23 +57,29 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
||||
tabindex="-1"
|
||||
@keyup.esc="closeModal"
|
||||
>
|
||||
<TransitionChild
|
||||
appear
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
<div
|
||||
class="fixed inset-0 flex min-h-screen w-screen justify-center p-8px sm:p-16px overflow-y-auto"
|
||||
:class="{
|
||||
'items-start sm:items-center': tallContent,
|
||||
'items-center': !tallContent,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-0 bg-black bg-opacity-80 transition-opacity"
|
||||
:title="t('Click to close modal')"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</TransitionChild>
|
||||
<div class="text-center flex min-h-full items-center justify-center p-4 md:p-0">
|
||||
<TransitionChild
|
||||
appear
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-0 bg-black bg-opacity-80 transition-opacity"
|
||||
:title="t('Click to close modal')"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</TransitionChild>
|
||||
<TransitionChild
|
||||
appear
|
||||
as="template"
|
||||
@@ -88,30 +97,49 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
||||
success ? 'shadow-green-600/30 border-green-600/10' : '',
|
||||
!error && !success ? 'shadow-orange/10 border-white/10' : '',
|
||||
]"
|
||||
class="text-16px text-beta bg-alpha text-left relative flex flex-col justify-around p-16px my-24px sm:p-24px border-2 border-solid shadow-xl transform overflow-hidden rounded-lg transition-all sm:w-full"
|
||||
class="text-16px text-beta bg-alpha text-left relative z-10 flex flex-col justify-around border-2 border-solid shadow-xl transform overflow-hidden rounded-lg transition-all sm:w-full"
|
||||
>
|
||||
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 hidden pt-2 pr-2 sm:block">
|
||||
<button type="button" class="rounded-md text-beta bg-alpha p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" @click="closeModal">
|
||||
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px hidden sm:block">
|
||||
<button
|
||||
class="rounded-md text-beta bg-transparent p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
>
|
||||
<span class="sr-only">{{ t('Close') }}</span>
|
||||
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<header class="text-center">
|
||||
<header
|
||||
class="relative z-0 grid items-start gap-2 p-16px md:p-24px rounded-t"
|
||||
:class="{
|
||||
'sm:pr-40px': showCloseX,
|
||||
'justify-between': $slots['header'],
|
||||
'justify-center': !$slots['header'],
|
||||
}"
|
||||
>
|
||||
<div class="absolute -z-10 inset-0 opacity-10 bg-beta" />
|
||||
<template v-if="!$slots['header']">
|
||||
<h1 v-if="title" :id="ariaLablledById" class="text-24px font-semibold flex flex-wrap justify-center gap-x-1">
|
||||
<h1 v-if="title" :id="ariaLablledById" class="text-center text-20px sm:text-24px font-semibold flex flex-wrap justify-center gap-x-4px">
|
||||
{{ title }}
|
||||
<slot name="headerTitle" />
|
||||
</h1>
|
||||
<h2 v-if="description" class="text-20px opacity-75">
|
||||
{{ description }}
|
||||
</h2>
|
||||
</template>
|
||||
<slot name="header" />
|
||||
</header>
|
||||
<slot name="main" />
|
||||
|
||||
<footer v-if="$slots['footer']" class="text-14px relative -mx-16px -mb-16px sm:-mx-24px sm:-mb-24px p-4 sm:p-6">
|
||||
<div
|
||||
v-if="$slots['main'] || description"
|
||||
class="relative max-h-[65vh] tall:max-h-[75vh] flex flex-col gap-y-16px sm:gap-y-24px p-16px md:p-24px overflow-y-auto shadow-inner"
|
||||
:class="centerContent && 'text-center'"
|
||||
>
|
||||
<h2 v-if="description" class="text-18px sm:text-20px opacity-75" v-html="description" />
|
||||
<div v-if="$slots['main']">
|
||||
<slot name="main" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer v-if="$slots['footer']" class="text-14px relative p-16px md:p-24px">
|
||||
<div class="absolute z-0 inset-0 opacity-10 bg-beta" />
|
||||
<div class="relative z-10">
|
||||
<slot name="footer" />
|
||||
|
||||
@@ -7,11 +7,15 @@ import '~/assets/main.css';
|
||||
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useTrialStore } from '~/store/trial';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { callbackStatus } = storeToRefs(useCallbackActionsStore());
|
||||
const { trialModalVisible } = storeToRefs(useTrialStore());
|
||||
const { modalOpen: updateOsModalVisible } = storeToRefs(useUpdateOsStore());
|
||||
const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(useUpdateOsChangelogStore());
|
||||
// import { usePromoStore } from '~/store/promo';
|
||||
// const { promoVisible } = storeToRefs(usePromoStore());
|
||||
// <UpcPromo :t="t" :open="promoVisible" />
|
||||
@@ -21,6 +25,8 @@ const { trialModalVisible } = storeToRefs(useTrialStore());
|
||||
<div class="relative z-[99999]">
|
||||
<UpcCallbackFeedback :t="t" :open="callbackStatus !== 'ready'" />
|
||||
<UpcTrial :t="t" :open="trialModalVisible" />
|
||||
<UpdateOsCheckUpdateResponseModal :t="t" :open="updateOsModalVisible" />
|
||||
<UpdateOsChangelogModal :t="t" :open="!!updateOsChangelogModalVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -25,18 +25,25 @@ import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import useDateTimeHelper from '~/composables/dateTime';
|
||||
|
||||
import { useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { RegistrationItemProps } from '~/types/registration';
|
||||
|
||||
import KeyActions from '~/components/KeyActions.vue';
|
||||
import RegistrationReplaceCheck from '~/components/Registration/ReplaceCheck.vue';
|
||||
import RegistrationKeyLinkedStatus from '~/components/Registration/KeyLinkedStatus.vue';
|
||||
import RegistrationUpdateExpirationAction from '~/components/Registration/UpdateExpirationAction.vue';
|
||||
import UserProfileUptimeExpire from '~/components/UserProfile/UptimeExpire.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const replaceRenewCheckStore = useReplaceRenewStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const {
|
||||
computedArray,
|
||||
arrayWarning,
|
||||
authAction,
|
||||
dateTimeFormat,
|
||||
deviceCount,
|
||||
@@ -44,46 +51,80 @@ const {
|
||||
flashVendor,
|
||||
flashProduct,
|
||||
keyActions,
|
||||
keyfile,
|
||||
computedRegDevs,
|
||||
regGuid,
|
||||
regTm,
|
||||
regTo,
|
||||
regTy,
|
||||
regExp,
|
||||
regUpdatesExpired,
|
||||
serverErrors,
|
||||
state,
|
||||
stateData,
|
||||
stateDataError,
|
||||
tooManyDevices,
|
||||
} = storeToRefs(serverStore);
|
||||
|
||||
const { outputDateTimeFormatted: formattedRegTm } = useDateTimeHelper(dateTimeFormat.value, t, false, regTm.value);
|
||||
const formattedRegTm = ref<string>();
|
||||
/**
|
||||
* regTm may not have a value until we get a response from the refreshServerState action
|
||||
* So we need to watch for this value to be able to format it based on the user's date time preferences.
|
||||
*/
|
||||
const setFormattedRegTm = () => {
|
||||
if (!regTm.value) { return; }
|
||||
|
||||
const devicesAvailable = computed((): number => {
|
||||
switch (regTy.value) {
|
||||
case 'Starter':
|
||||
return 4;
|
||||
case 'Basic':
|
||||
return 6;
|
||||
case 'Plus':
|
||||
return 12;
|
||||
case 'Unleashed':
|
||||
case 'Lifetime':
|
||||
case 'Pro':
|
||||
case 'Trial':
|
||||
return 9999;
|
||||
default:
|
||||
return 0;
|
||||
const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, t, true, regTm.value);
|
||||
formattedRegTm.value = outputDateTimeFormatted.value;
|
||||
};
|
||||
watch(regTm, (_newV) => {
|
||||
setFormattedRegTm();
|
||||
});
|
||||
onBeforeMount(() => {
|
||||
setFormattedRegTm();
|
||||
/** automatically check for replacement and renewal eligibility…will prompt user if eligible for a renewal / key re-roll for legacy keys */
|
||||
if (guid.value && keyfile.value) {
|
||||
replaceRenewCheckStore.check();
|
||||
}
|
||||
});
|
||||
|
||||
const headingIcon = computed(() => serverErrors.value.length ? ShieldExclamationIcon : ShieldCheckIcon);
|
||||
const heading = computed(() => {
|
||||
if (serverErrors.value.length) { // It's rare to have multiple errors but for the time being only show the first error
|
||||
return serverErrors.value[0]?.heading;
|
||||
}
|
||||
return stateData.value.heading;
|
||||
});
|
||||
const subheading = computed(() => {
|
||||
if (serverErrors.value.length) { // It's rare to have multiple errors but for the time being only show the first error
|
||||
return serverErrors.value[0]?.message;
|
||||
}
|
||||
return stateData.value.message;
|
||||
});
|
||||
|
||||
const showTrialExpiration = computed((): boolean => state.value === 'TRIAL' || state.value === 'EEXPIRED');
|
||||
const showUpdateEligibility = computed((): boolean => !!(regExp.value));
|
||||
const keyInstalled = computed((): boolean => !!(!stateDataError.value && state.value !== 'ENOKEYFILE'));
|
||||
const showLinkedAndTransferStatus = computed((): boolean => !!(keyInstalled.value && guid.value && !showTrialExpiration.value));
|
||||
// filter out renew action and only display other key actions…renew is displayed in RegistrationUpdateExpirationAction
|
||||
const showFilteredKeyActions = computed((): boolean => !!(keyActions.value && keyActions.value?.filter(action => !['renew'].includes(action.name)).length > 0));
|
||||
|
||||
const items = computed((): RegistrationItemProps[] => {
|
||||
return [
|
||||
...(computedArray.value
|
||||
? [{
|
||||
label: t('Array status'),
|
||||
text: computedArray.value,
|
||||
warning: arrayWarning.value,
|
||||
}]
|
||||
: []),
|
||||
...(regTy.value
|
||||
? [{
|
||||
label: t('License key type'),
|
||||
text: regTy.value,
|
||||
}]
|
||||
: []),
|
||||
...(state.value === 'TRIAL' || state.value === 'EEXPIRED'
|
||||
...(showTrialExpiration.value
|
||||
? [{
|
||||
error: state.value === 'EEXPIRED',
|
||||
label: t('Trial expiration'),
|
||||
@@ -102,13 +143,13 @@ const items = computed((): RegistrationItemProps[] => {
|
||||
text: regTo.value,
|
||||
}]
|
||||
: []),
|
||||
...(regTo.value && regTm.value
|
||||
...(regTo.value && regTm.value && formattedRegTm.value
|
||||
? [{
|
||||
label: t('Registered on'),
|
||||
text: formattedRegTm.value,
|
||||
}]
|
||||
: []),
|
||||
...(regExp.value && (state.value === 'STARTER' || state.value === 'UNLEASHED')
|
||||
...(showUpdateEligibility.value
|
||||
? [{
|
||||
label: t('OS Update Eligibility'),
|
||||
warning: regUpdatesExpired.value,
|
||||
@@ -141,26 +182,32 @@ const items = computed((): RegistrationItemProps[] => {
|
||||
text: flashProduct.value,
|
||||
}]
|
||||
: []),
|
||||
...(!stateDataError.value
|
||||
...(keyInstalled.value
|
||||
? [{
|
||||
error: deviceCount.value > devicesAvailable.value,
|
||||
error: tooManyDevices.value,
|
||||
label: t('Attached Storage Devices'),
|
||||
text: deviceCount.value > devicesAvailable.value
|
||||
? t('{0} out of {1} allowed devices – upgrade your key to support more devices', [deviceCount.value, devicesAvailable.value > 12 ? t('unlimited') : devicesAvailable.value])
|
||||
: t('{0} out of {1} devices', [deviceCount.value, devicesAvailable.value > 12 ? t('unlimited') : devicesAvailable.value]),
|
||||
text: tooManyDevices.value
|
||||
? t('{0} out of {1} allowed devices – upgrade your key to support more devices', [deviceCount.value, computedRegDevs.value])
|
||||
: t('{0} out of {1} devices', [deviceCount.value, computedRegDevs.value === -1 ? t('unlimited') : computedRegDevs.value]),
|
||||
}]
|
||||
: []),
|
||||
...(!stateDataError.value && guid.value
|
||||
...(showLinkedAndTransferStatus.value
|
||||
? [{
|
||||
label: t('Transfer License to New Flash'),
|
||||
component: RegistrationReplaceCheck,
|
||||
componentProps: { t },
|
||||
}]
|
||||
: []),
|
||||
// filter out renew action and only display other key actions…renew is displayed in RegistrationUpdateExpirationAction
|
||||
...(keyActions.value && keyActions.value?.filter(action => !['renew'].includes(action.name)).length > 0
|
||||
...(regTo.value && showLinkedAndTransferStatus.value
|
||||
? [{
|
||||
label: t('Linked to Unraid.net account'),
|
||||
component: RegistrationKeyLinkedStatus,
|
||||
componentProps: { t },
|
||||
}]
|
||||
: []),
|
||||
|
||||
...(showFilteredKeyActions.value
|
||||
? [{
|
||||
label: t('License key actions'),
|
||||
component: KeyActions,
|
||||
componentProps: {
|
||||
filterOut: ['renew'],
|
||||
@@ -179,17 +226,17 @@ const items = computed((): RegistrationItemProps[] => {
|
||||
<header class="flex flex-col gap-y-16px">
|
||||
<h3
|
||||
class="text-20px md:text-24px font-semibold leading-normal flex flex-row items-center gap-8px"
|
||||
:class="stateDataError ? 'text-unraid-red' : 'text-green-500'"
|
||||
:class="serverErrors.length ? 'text-unraid-red' : 'text-green-500'"
|
||||
>
|
||||
<component :is="stateDataError ? ShieldExclamationIcon : ShieldCheckIcon" class="w-24px h-24px" />
|
||||
<component :is="headingIcon" class="w-24px h-24px" />
|
||||
<span>
|
||||
{{ stateData.heading }}
|
||||
{{ heading }}
|
||||
</span>
|
||||
</h3>
|
||||
<div
|
||||
v-if="stateData.message"
|
||||
v-if="subheading"
|
||||
class="prose text-16px leading-relaxed whitespace-normal opacity-75"
|
||||
v-html="stateData.message"
|
||||
v-html="subheading"
|
||||
/>
|
||||
<span v-if="authAction" class="grow-0">
|
||||
<BrandButton
|
||||
@@ -197,7 +244,7 @@ const items = computed((): RegistrationItemProps[] => {
|
||||
:icon="authAction.icon"
|
||||
:text="t(authAction.text)"
|
||||
:title="authAction.title ? t(authAction.title) : undefined"
|
||||
@click="authAction.click()"
|
||||
@click="authAction.click?.()"
|
||||
/>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
@@ -25,14 +25,23 @@ const evenBgColor = computed(() => {
|
||||
error && 'text-white bg-unraid-red',
|
||||
warning && 'text-black bg-yellow-100',
|
||||
]"
|
||||
class="text-16px p-12px grid grid-cols-1 gap-4px sm:px-20px sm:grid-cols-3 sm:gap-16px items-start rounded"
|
||||
class="text-16px p-12px grid grid-cols-1 gap-4px sm:px-20px sm:grid-cols-5 sm:gap-16px items-baseline rounded"
|
||||
>
|
||||
<dt class="font-semibold flex flex-row justify-start items-center gap-x-8px">
|
||||
<dt v-if="label" class="font-semibold leading-normal sm:col-span-2 flex flex-row sm:justify-end sm:text-right items-center gap-x-8px">
|
||||
<ShieldExclamationIcon v-if="error" class="w-16px h-16px fill-current" />
|
||||
<span>{{ label }}</span>
|
||||
<span v-html="label" />
|
||||
</dt>
|
||||
<dd class="leading-normal sm:col-span-2">
|
||||
<span v-if="text" class="select-all" :class="!error ? 'opacity-75' : ''">
|
||||
<dd
|
||||
class="leading-normal sm:col-span-3"
|
||||
:class="!label && 'sm:col-start-2'"
|
||||
>
|
||||
<span
|
||||
v-if="text"
|
||||
class="select-all"
|
||||
:class="{
|
||||
'opacity-75': !error,
|
||||
}"
|
||||
>
|
||||
{{ text }}
|
||||
</span>
|
||||
<template v-if="$slots['right']">
|
||||
|
||||
74
web/components/Registration/KeyLinkedStatus.vue
Normal file
74
web/components/Registration/KeyLinkedStatus.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ArrowPathIcon,
|
||||
LinkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const replaceRenewStore = useReplaceRenewStore();
|
||||
const { keyLinkedStatus, keyLinkedOutput } = storeToRefs(replaceRenewStore);
|
||||
|
||||
defineProps<{
|
||||
t: ComposerTranslation;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center justify-between gap-8px">
|
||||
<BrandButton
|
||||
v-if="keyLinkedStatus !== 'linked' && keyLinkedStatus !== 'checking'"
|
||||
btn-style="none"
|
||||
:no-padding="true"
|
||||
:title="t('Refresh')"
|
||||
class="group"
|
||||
@click="replaceRenewStore.check(true)"
|
||||
>
|
||||
<UiBadge
|
||||
v-if="keyLinkedOutput"
|
||||
:color="keyLinkedOutput.color"
|
||||
:icon="keyLinkedOutput.icon"
|
||||
:icon-right="ArrowPathIcon"
|
||||
size="16px"
|
||||
>
|
||||
{{ t(keyLinkedOutput.text ?? 'Unknown') }}
|
||||
</UiBadge>
|
||||
</BrandButton>
|
||||
<UiBadge
|
||||
v-else
|
||||
:color="keyLinkedOutput.color"
|
||||
:icon="keyLinkedOutput.icon"
|
||||
size="16px"
|
||||
>
|
||||
{{ t(keyLinkedOutput.text ?? 'Unknown') }}
|
||||
</UiBadge>
|
||||
|
||||
<span class="inline-flex flex-wrap-items-start gap-8px">
|
||||
<BrandButton
|
||||
v-if="keyLinkedStatus === 'notLinked'"
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:icon="LinkIcon"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
:text="t('Link Key')"
|
||||
:title="t('Learn more and link your key to your account')"
|
||||
class="text-14px"
|
||||
@click="accountStore.linkKey"
|
||||
/>
|
||||
<BrandButton
|
||||
v-else
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
:text="t('Learn More')"
|
||||
class="text-14px"
|
||||
@click="accountStore.myKeys"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
KeyIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import { DOCS_REGISTRATION_REPLACE_KEY } from '~/helpers/urls';
|
||||
import { useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
@@ -12,12 +13,12 @@ const replaceRenewStore = useReplaceRenewStore();
|
||||
const { replaceStatusOutput } = storeToRefs(replaceRenewStore);
|
||||
|
||||
defineProps<{
|
||||
t: any;
|
||||
t: ComposerTranslation;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-start justify-between gap-8px">
|
||||
<div class="flex flex-wrap items-center justify-between gap-8px">
|
||||
<BrandButton
|
||||
v-if="!replaceStatusOutput"
|
||||
:icon="KeyIcon"
|
||||
@@ -32,16 +33,18 @@ defineProps<{
|
||||
:icon="replaceStatusOutput.icon"
|
||||
size="16px"
|
||||
>
|
||||
{{ t(replaceStatusOutput.text) }}
|
||||
{{ t(replaceStatusOutput.text ?? 'Unknown') }}
|
||||
</UiBadge>
|
||||
|
||||
<BrandButton
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:href="DOCS_REGISTRATION_REPLACE_KEY.toString()"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
:text="t('Learn More')"
|
||||
class="text-14px"
|
||||
/>
|
||||
<span class="inline-flex flex-wrap items-center justify-end gap-8px">
|
||||
<BrandButton
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:href="DOCS_REGISTRATION_REPLACE_KEY.toString()"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
:text="t('Learn More')"
|
||||
class="text-14px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import useDateTimeHelper from '~/composables/dateTime';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
export interface Props {
|
||||
componentIs?: string;
|
||||
t: any;
|
||||
t: ComposerTranslation;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -27,11 +28,11 @@ const output = computed(() => {
|
||||
}
|
||||
return {
|
||||
text: regUpdatesExpired.value
|
||||
? props.t('Ineligible for updates released after {0}', [outputDateTimeFormatted.value])
|
||||
: props.t('Eligible for updates until {0}', [outputDateTimeFormatted.value]),
|
||||
? props.t('Ineligible for feature updates released after {0}', [outputDateTimeFormatted.value])
|
||||
: props.t('Eligible for free feature updates until {0}', [outputDateTimeFormatted.value]),
|
||||
title: regUpdatesExpired.value
|
||||
? props.t('Ineligible as of {0}', [outputDateTimeReadableDiff.value])
|
||||
: props.t('Eligible for updates for {0}', [outputDateTimeReadableDiff.value]),
|
||||
: props.t('Eligible for free feature updates for {0}', [outputDateTimeReadableDiff.value]),
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import useDateTimeHelper from '~/composables/dateTime';
|
||||
import { DOCS_REGISTRATION_LICENSING } from '~/helpers/urls';
|
||||
@@ -8,7 +9,7 @@ import { useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
export interface Props {
|
||||
t: any;
|
||||
t: ComposerTranslation;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@@ -39,11 +40,11 @@ const output = computed(() => {
|
||||
}
|
||||
return {
|
||||
text: regUpdatesExpired.value
|
||||
? props.t('Ineligible for updates released after {0}', [formattedRegExp.value])
|
||||
: props.t('Eligible for updates until {0}', [formattedRegExp.value]),
|
||||
? props.t('Ineligible for feature updates released after {0}', [formattedRegExp.value])
|
||||
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]),
|
||||
title: regUpdatesExpired.value
|
||||
? props.t('Ineligible as of {0}', [readableDiffRegExp.value])
|
||||
: props.t('Eligible for updates for {0}', [readableDiffRegExp.value]),
|
||||
: props.t('Eligible for free feature updates for {0}', [readableDiffRegExp.value]),
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -75,7 +76,7 @@ const output = computed(() => {
|
||||
:text="t('Extend License')"
|
||||
:title="t('Pay your annual fee to continue receiving OS updates.')"
|
||||
class="flex-grow"
|
||||
@click="renewAction.click()"
|
||||
@click="renewAction.click?.()"
|
||||
/>
|
||||
|
||||
<BrandButton
|
||||
|
||||
34
web/components/Ui/Switch.vue
Normal file
34
web/components/Ui/Switch.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
|
||||
|
||||
export interface Props {
|
||||
label?: string;
|
||||
// propChecked?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
label: '',
|
||||
// propChecked: false,
|
||||
});
|
||||
|
||||
const checked = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchGroup>
|
||||
<div class="flex items-center gap-8px p-8px rounded">
|
||||
<Switch
|
||||
v-model="checked"
|
||||
:class="checked ? 'bg-gradient-to-r from-unraid-red to-orange' : 'bg-transparent'"
|
||||
class="relative inline-flex h-24px w-[48px] items-center rounded-full overflow-hidden"
|
||||
>
|
||||
<span v-show="!checked" class="absolute z-0 inset-0 opacity-10 bg-beta" />
|
||||
<span
|
||||
:class="checked ? 'translate-x-[26px]' : 'translate-x-[2px]'"
|
||||
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
|
||||
/>
|
||||
</Switch>
|
||||
<SwitchLabel>{{ label }}</SwitchLabel>
|
||||
</div>
|
||||
</SwitchGroup>
|
||||
</template>
|
||||
@@ -19,8 +19,8 @@ import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
@@ -34,8 +34,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
rebootVersion: '',
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const serverStore = useServerStore();
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
const { rebootType } = storeToRefs(serverStore);
|
||||
|
||||
const subtitle = computed(() => {
|
||||
@@ -50,7 +50,7 @@ const showLoader = computed(() => window.location.pathname === WEBGUI_TOOLS_UPDA
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (showLoader.value) {
|
||||
updateOsActionsStore.executeUpdateOsCallback(true);
|
||||
accountStore.updateOs(true);
|
||||
}
|
||||
serverStore.setRebootVersion(props.rebootVersion);
|
||||
});
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import type { ButtonStyle } from '~/types/ui/button';
|
||||
|
||||
defineProps<{
|
||||
t: any;
|
||||
btnStyle?: ButtonStyle;
|
||||
t: ComposerTranslation;
|
||||
}>();
|
||||
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
const accountStore = useAccountStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col sm:flex-shrink-0 sm:flex-grow-0 items-center">
|
||||
<BrandButton
|
||||
:btn-style="btnStyle"
|
||||
:icon="ArrowPathIcon"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
:text="t('Check for OS Updates')"
|
||||
class="flex-0"
|
||||
@click="updateOsActionsStore.executeUpdateOsCallback()"
|
||||
@click="accountStore.updateOs()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
140
web/components/UpdateOs/ChangelogModal.vue
Normal file
140
web/components/UpdateOs/ChangelogModal.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ArrowSmallRightIcon,
|
||||
EyeIcon,
|
||||
KeyIcon,
|
||||
ServerStackIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import { usePurchaseStore } from '~/store/purchase';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
// import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: ComposerTranslation;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const purchaseStore = usePurchaseStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
// const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
||||
|
||||
const { availableWithRenewal } = storeToRefs(updateOsStore);
|
||||
const {
|
||||
releaseForUpdate,
|
||||
mutatedParsedChangelog,
|
||||
parseChangelogFailed,
|
||||
parsedChangelogTitle,
|
||||
} = storeToRefs(updateOsChangelogStore);
|
||||
|
||||
const showExtendKeyButton = computed(() => {
|
||||
return availableWithRenewal.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:center-content="false"
|
||||
:error="!!parseChangelogFailed"
|
||||
max-width="max-w-800px"
|
||||
:open="!!releaseForUpdate"
|
||||
:show-close-x="true"
|
||||
:t="t"
|
||||
:tall-content="true"
|
||||
:title="parsedChangelogTitle ?? undefined"
|
||||
@close="updateOsChangelogStore.setReleaseForUpdate(null)"
|
||||
>
|
||||
<template #main>
|
||||
<div
|
||||
v-if="mutatedParsedChangelog"
|
||||
class="text-16px sm:text-18px prose prose-a:text-unraid-red hover:prose-a:no-underline hover:prose-a:text-unraid-red/60 dark:prose-a:text-orange hover:dark:prose-a:text-orange/60"
|
||||
v-html="mutatedParsedChangelog"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else-if="parseChangelogFailed"
|
||||
class="text-center flex flex-col gap-4 prose"
|
||||
>
|
||||
<h2 class="text-lg text-unraid-red italic font-semibold">
|
||||
{{ props.t(`Error Parsing Changelog • {0}`, [parseChangelogFailed]) }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ props.t(`It's highly recommended to review the changelog before continuing your update`) }}
|
||||
</p>
|
||||
<div
|
||||
v-if="releaseForUpdate?.changelogPretty"
|
||||
class="flex self-center"
|
||||
>
|
||||
<BrandButton
|
||||
:href="releaseForUpdate?.changelogPretty"
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
>
|
||||
{{ props.t("View Changelog on Docs") }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="text-center flex flex-col justify-center w-full min-h-[250px] min-w-[280px] sm:min-w-[400px]"
|
||||
>
|
||||
<BrandLoading class="w-[150px] mx-auto mt-24px" />
|
||||
<p>{{ props.t("Fetching & parsing changelog…") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex flex-col-reverse xs:flex-row justify-between gap-12px md:gap-16px">
|
||||
<div class="flex flex-col-reverse xs:flex-row xs:justify-start gap-12px md:gap-16px">
|
||||
<BrandButton
|
||||
btn-style="underline-hover-red"
|
||||
:icon="XMarkIcon"
|
||||
@click="updateOsChangelogStore.setReleaseForUpdate(null)"
|
||||
>
|
||||
{{ props.t("Close") }}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
v-if="releaseForUpdate?.changelogPretty"
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:href="releaseForUpdate?.changelogPretty"
|
||||
:icon="EyeIcon"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
>
|
||||
{{ props.t("View on Docs") }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
<BrandButton
|
||||
v-if="showExtendKeyButton"
|
||||
btn-style="fill"
|
||||
:icon="KeyIcon"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
@click="purchaseStore.renew()"
|
||||
>
|
||||
{{ props.t("Extend License to Update") }}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
v-else-if="releaseForUpdate?.sha256"
|
||||
:icon="ServerStackIcon"
|
||||
:icon-right="ArrowSmallRightIcon"
|
||||
@click="updateOsChangelogStore.fetchAndConfirmInstall(releaseForUpdate.sha256)"
|
||||
>
|
||||
{{ props.t('Continue') }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
335
web/components/UpdateOs/CheckUpdateResponseModal.vue
Normal file
335
web/components/UpdateOs/CheckUpdateResponseModal.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
CogIcon,
|
||||
EyeIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import useDateTimeHelper from '~/composables/dateTime';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { usePurchaseStore } from '~/store/purchase';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
import type { ButtonProps } from '~/types/ui/button';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: ComposerTranslation;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const purchaseStore = usePurchaseStore();
|
||||
const serverStore = useServerStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
||||
|
||||
const {
|
||||
regExp,
|
||||
regUpdatesExpired,
|
||||
dateTimeFormat,
|
||||
updateOsIgnoredReleases,
|
||||
updateOsNotificationsEnabled,
|
||||
updateOsResponse,
|
||||
} = storeToRefs(serverStore);
|
||||
const {
|
||||
available,
|
||||
availableWithRenewal,
|
||||
availableReleaseDate,
|
||||
availableRequiresAuth,
|
||||
checkForUpdatesLoading,
|
||||
} = storeToRefs(updateOsStore);
|
||||
|
||||
/**
|
||||
* regExp may not have a value until we get a response from the refreshServerState action
|
||||
* So we need to watch for this value to be able to format it based on the user's date time preferences.
|
||||
*/
|
||||
const formattedRegExp = ref<string>();
|
||||
const setFormattedRegExp = () => { // ran in watch on regExp and onBeforeMount
|
||||
if (!regExp.value) { return; }
|
||||
|
||||
const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
|
||||
formattedRegExp.value = outputDateTimeFormatted.value;
|
||||
};
|
||||
watch(regExp, (_newV) => {
|
||||
setFormattedRegExp();
|
||||
});
|
||||
|
||||
const ignoreThisRelease = ref(false);
|
||||
// if we had a release ignored and now we don't set ignoreThisRelease to false
|
||||
watch(updateOsIgnoredReleases, (newVal, oldVal) => {
|
||||
if (oldVal.length > 0 && newVal.length === 0) {
|
||||
ignoreThisRelease.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const notificationsSettings = computed(() => {
|
||||
return !updateOsNotificationsEnabled.value
|
||||
? props.t('Go to Settings > Notifications to enable automatic OS update notifications for future releases.')
|
||||
: undefined;
|
||||
});
|
||||
|
||||
interface ModalCopy {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
const modalCopy = computed((): ModalCopy | null => {
|
||||
if (checkForUpdatesLoading.value) {
|
||||
return {
|
||||
title: props.t('Checking for OS updates...'),
|
||||
};
|
||||
}
|
||||
|
||||
// Use the release date
|
||||
let formattedReleaseDate = '';
|
||||
if (availableReleaseDate.value) {
|
||||
// build string with prefix
|
||||
formattedReleaseDate = props.t('Release date {0}', [userFormattedReleaseDate.value]);
|
||||
}
|
||||
|
||||
if (availableWithRenewal.value) {
|
||||
const description = regUpdatesExpired.value
|
||||
? props.t('Ineligible for feature updates released after {0}', [formattedRegExp.value])
|
||||
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]);
|
||||
return {
|
||||
title: props.t('Unraid OS {0} Released', [availableWithRenewal.value]),
|
||||
description: `<p>${formattedReleaseDate}</p><p>${description}</p>`,
|
||||
};
|
||||
} else if (available.value) {
|
||||
const description = availableRequiresAuth.value
|
||||
? props.t('Release requires verification to update')
|
||||
: undefined;
|
||||
return {
|
||||
title: props.t('Unraid OS {0} Update Available', [available.value]),
|
||||
description: description ? `<p>${formattedReleaseDate}</p><p>${description}</p>` : formattedReleaseDate,
|
||||
};
|
||||
} else if (!available.value && !availableWithRenewal.value) {
|
||||
return {
|
||||
title: props.t('Unraid OS is up-to-date'),
|
||||
description: notificationsSettings.value ?? undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const showNotificationsSettingsLink = computed(() => {
|
||||
return !updateOsNotificationsEnabled.value && !available.value && !availableWithRenewal.value;
|
||||
});
|
||||
|
||||
const extraLinks = computed((): ButtonProps[] => {
|
||||
const buttons: ButtonProps[] = [];
|
||||
|
||||
if (showNotificationsSettingsLink.value) {
|
||||
buttons.push({
|
||||
btnStyle: 'outline',
|
||||
href: '/Settings/Notifications',
|
||||
icon: CogIcon,
|
||||
text: props.t('Enable update notifications'),
|
||||
});
|
||||
}
|
||||
|
||||
return buttons;
|
||||
});
|
||||
|
||||
const actionButtons = computed((): ButtonProps[] | null => {
|
||||
// update not available or no action buttons default closing
|
||||
if (!available.value || ignoreThisRelease.value) { return null; }
|
||||
|
||||
const buttons: ButtonProps[] = [];
|
||||
|
||||
// update available but not stable branch - should link out to account update callback
|
||||
// if availableWithRenewal.value is true, then we need to renew the license before we can update so don't show the verify button
|
||||
if (availableRequiresAuth.value && !availableWithRenewal.value) {
|
||||
buttons.push({
|
||||
click: async () => await accountStore.updateOs(),
|
||||
icon: IdentificationIcon,
|
||||
text: props.t('Verify to Update'),
|
||||
});
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
// update available - open changelog to commence update
|
||||
if (available.value && updateOsResponse.value?.changelog) {
|
||||
buttons.push({
|
||||
btnStyle: availableWithRenewal.value
|
||||
? 'outline'
|
||||
: undefined,
|
||||
click: async () => await updateOsChangelogStore.setReleaseForUpdate(updateOsResponse.value ?? null),
|
||||
icon: EyeIcon,
|
||||
text: availableWithRenewal.value
|
||||
? props.t('View Changelog')
|
||||
: props.t('View Changelog to Start Update'),
|
||||
});
|
||||
}
|
||||
|
||||
// update available with renewal - open changelog and Extend License options
|
||||
if (availableWithRenewal.value) {
|
||||
buttons.push({
|
||||
click: async () => await purchaseStore.renew(),
|
||||
icon: KeyIcon,
|
||||
iconRight: ArrowTopRightOnSquareIcon,
|
||||
iconRightHoverDisplay: false,
|
||||
text: props.t('Extend License'),
|
||||
title: props.t('Pay your annual fee to continue receiving OS updates.'),
|
||||
});
|
||||
}
|
||||
|
||||
return buttons;
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
// close it
|
||||
updateOsStore.setModalOpen(false);
|
||||
// then ignore the release if applicable
|
||||
if (ignoreThisRelease.value && (availableWithRenewal.value || available.value)) {
|
||||
setTimeout(() => {
|
||||
serverStore.updateOsIgnoreRelease(availableWithRenewal.value ?? available.value ?? '');
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const renderMainSlot = computed(() => {
|
||||
return !!(checkForUpdatesLoading.value || available.value || availableWithRenewal.value || extraLinks.value?.length > 0 || updateOsIgnoredReleases.value.length > 0);
|
||||
});
|
||||
|
||||
const userFormattedReleaseDate = ref<string>();
|
||||
/**
|
||||
* availableReleaseDate may not have a value until we get a release in the update os check response.
|
||||
* So we need to watch for this value to be able to format it based on the user's date time preferences.
|
||||
*/
|
||||
const setUserFormattedReleaseDate = () => {
|
||||
if (!availableReleaseDate.value) { return; }
|
||||
|
||||
const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, props.t, true, availableReleaseDate.value.valueOf());
|
||||
userFormattedReleaseDate.value = outputDateTimeFormatted.value;
|
||||
};
|
||||
watch(availableReleaseDate, (_newV) => {
|
||||
setUserFormattedReleaseDate();
|
||||
});
|
||||
onBeforeMount(() => {
|
||||
if (availableReleaseDate.value) {
|
||||
setUserFormattedReleaseDate();
|
||||
}
|
||||
setFormattedRegExp();
|
||||
});
|
||||
|
||||
const modalWidth = computed(() => {
|
||||
if (availableWithRenewal.value) { // wider since we'll have four buttons
|
||||
return 'max-w-800px';
|
||||
}
|
||||
return 'max-w-640px';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:t="t"
|
||||
:open="open"
|
||||
:title="modalCopy?.title"
|
||||
:description="modalCopy?.description"
|
||||
:show-close-x="!checkForUpdatesLoading"
|
||||
:max-width="modalWidth"
|
||||
@close="close"
|
||||
>
|
||||
<template v-if="renderMainSlot" #main>
|
||||
<BrandLoading v-if="checkForUpdatesLoading" class="w-[150px] mx-auto" />
|
||||
<div v-else class="flex flex-col gap-y-16px">
|
||||
<div v-if="extraLinks.length > 0" class="flex flex-col xs:flex-row justify-center gap-8px">
|
||||
<BrandButton
|
||||
v-for="item in extraLinks"
|
||||
:key="item.text"
|
||||
:btn-style="item.btnStyle ?? undefined"
|
||||
:href="item.href ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="t(item.text ?? '')"
|
||||
:title="item.title ? t(item.title) : undefined"
|
||||
@click="item.click?.()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="available || availableWithRenewal" class="mx-auto">
|
||||
<SwitchGroup>
|
||||
<div class="flex justify-center items-center gap-8px p-8px rounded">
|
||||
<Switch
|
||||
v-model="ignoreThisRelease"
|
||||
:class="ignoreThisRelease ? 'bg-gradient-to-r from-unraid-red to-orange' : 'bg-transparent'"
|
||||
class="relative inline-flex h-24px w-[48px] items-center rounded-full overflow-hidden"
|
||||
>
|
||||
<span v-show="!ignoreThisRelease" class="absolute z-0 inset-0 opacity-10 bg-beta" />
|
||||
<span
|
||||
:class="ignoreThisRelease ? 'translate-x-[26px]' : 'translate-x-[2px]'"
|
||||
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
|
||||
/>
|
||||
</Switch>
|
||||
<SwitchLabel class="text-16px">
|
||||
{{ t('Ignore this release until next reboot') }}
|
||||
</SwitchLabel>
|
||||
</div>
|
||||
</SwitchGroup>
|
||||
</div>
|
||||
<div v-else-if="updateOsIgnoredReleases.length > 0" class="w-full max-w-640px mx-auto flex flex-col gap-8px">
|
||||
<h3 class="text-left text-16px font-semibold italic">
|
||||
{{ t('Ignored Releases') }}
|
||||
</h3>
|
||||
<UpdateOsIgnoredRelease
|
||||
v-for="ignoredRelease in updateOsIgnoredReleases"
|
||||
:key="ignoredRelease"
|
||||
:label="ignoredRelease"
|
||||
:t="t"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="!checkForUpdatesLoading" #footer>
|
||||
<div
|
||||
class="w-full flex gap-8px mx-auto"
|
||||
:class="{
|
||||
'flex-col-reverse xs:flex-row justify-between': actionButtons,
|
||||
'justify-center': !actionButtons,
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col-reverse xs:flex-row justify-start gap-8px">
|
||||
<BrandButton
|
||||
btn-style="underline-hover-red"
|
||||
:icon="XMarkIcon"
|
||||
:text="t('Close')"
|
||||
@click="close"
|
||||
/>
|
||||
<BrandButton
|
||||
btn-style="underline"
|
||||
:icon="ArrowTopRightOnSquareIcon"
|
||||
:text="t('More options')"
|
||||
@click="accountStore.updateOs()"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="actionButtons" class="flex flex-col xs:flex-row justify-end gap-8px">
|
||||
<BrandButton
|
||||
v-for="item in actionButtons"
|
||||
:key="item.text"
|
||||
:btn-style="item.btnStyle ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="t(item.text ?? '')"
|
||||
:title="item.title ? t(item.title) : undefined"
|
||||
@click="item.click?.()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import dayjs from 'dayjs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
@@ -20,7 +21,7 @@ import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
const props = defineProps<{
|
||||
t: any;
|
||||
t: ComposerTranslation;
|
||||
releaseDate: string;
|
||||
version: string;
|
||||
}>();
|
||||
@@ -35,7 +36,7 @@ const {
|
||||
|
||||
const diagnosticsButton = ref<UserProfileLink | undefined>({
|
||||
click: () => {
|
||||
// @ts-ignore – global function provided by the webgui on the update page
|
||||
// @ts-expect-error – global function provided by the webgui on the update page
|
||||
downloadDiagnostics();
|
||||
},
|
||||
icon: FolderArrowDownIcon,
|
||||
@@ -45,7 +46,7 @@ const diagnosticsButton = ref<UserProfileLink | undefined>({
|
||||
|
||||
const downgradeButton = ref<UserProfileLink>({
|
||||
click: () => {
|
||||
// @ts-ignore – global function provided by the webgui on the update page
|
||||
// @ts-expect-error – global function provided by the webgui on the update page
|
||||
confirmDowngrade();
|
||||
},
|
||||
name: 'downgrade',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user