mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
948580917d | ||
|
|
39e83b2aa1 | ||
|
|
831050f4e8 | ||
|
|
7ace6d3076 | ||
|
|
1ff3d7285e | ||
|
|
874a507e60 | ||
|
|
32c6fe6295 | ||
|
|
586653ccc1 | ||
|
|
c5a394eddf | ||
|
|
1d30b25a0a | ||
|
|
026b0b344c | ||
|
|
72860e71fe | ||
|
|
1236b7743e | ||
|
|
ea77de8800 | ||
|
|
ad0f4c8b55 | ||
|
|
d74d9f1246 | ||
|
|
45ecab6914 | ||
|
|
e65775f878 | ||
|
|
33ad1fd63b | ||
|
|
234573264c | ||
|
|
f5724abffb | ||
|
|
8bb9efcb68 | ||
|
|
97ab6fbe32 | ||
|
|
36a7a28ed5 | ||
|
|
03be042410 | ||
|
|
cd323acd49 | ||
|
|
457d338150 | ||
|
|
04caaf3b25 | ||
|
|
4b5743906a | ||
|
|
f65788aa94 | ||
|
|
8a5b23856c | ||
|
|
23c60dad0c | ||
|
|
1bbe7d27b0 | ||
|
|
bc3ca92fb0 | ||
|
|
c4fdff8149 | ||
|
|
0e008aaf1e | ||
|
|
da8dac3940 | ||
|
|
187a6ec670 | ||
|
|
819ba0bd71 | ||
|
|
1217d0b100 | ||
|
|
15dc458751 | ||
|
|
a151dc1f73 | ||
|
|
006fe1e762 | ||
|
|
7e89cd2a3e | ||
|
|
38a9e36fcd | ||
|
|
d701151729 | ||
|
|
0f682b5f23 | ||
|
|
3fff76d155 | ||
|
|
d8d7075ed8 | ||
|
|
e27e38645f | ||
|
|
4bb00dd981 | ||
|
|
60f16bde41 | ||
|
|
cacb1c1d3d | ||
|
|
a562f77163 | ||
|
|
37e72f9729 | ||
|
|
662d5f64c9 | ||
|
|
79397eecff | ||
|
|
99d8b31fa8 |
12
.cursor/rules/api-rules.mdc
Normal file
12
.cursor/rules/api-rules.mdc
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
description:
|
||||
globs: api/**/*,api/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
* pnpm ONLY
|
||||
* always run scripts from api/package.json unless requested
|
||||
* prefer adding new files to the nest repo located at api/src/unraid-api/ instead of the legacy code
|
||||
* Test suite is VITEST, do not use jest
|
||||
* Prefer to not mock simple dependencies
|
||||
|
||||
8
.cursor/rules/default.mdc
Normal file
8
.cursor/rules/default.mdc
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
Never add comments unless they are needed for clarity of function
|
||||
|
||||
Be CONCISE, keep replies shorter than a paragraph if at all passible.
|
||||
240
.cursor/rules/web-testing-rules.mdc
Normal file
240
.cursor/rules/web-testing-rules.mdc
Normal file
@@ -0,0 +1,240 @@
|
||||
---
|
||||
description:
|
||||
globs: **/*.test.ts,**/__test__/components/**/*.ts,**/__test__/store/**/*.ts,**/__test__/mocks/**/*.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
## Vue Component Testing Best Practices
|
||||
- This is a Nuxt.js app but we are testing with vitest outside of the Nuxt environment
|
||||
- Nuxt is currently set to auto import so some vue files may need compute or ref imported
|
||||
- Use pnpm when running termical commands and stay within the web directory.
|
||||
- The directory for tests is located under `web/__test__` when running test just run `pnpm test`
|
||||
|
||||
### Setup
|
||||
- Use `mount` from Vue Test Utils for component testing
|
||||
- Stub complex child components that aren't the focus of the test
|
||||
- Mock external dependencies and services
|
||||
|
||||
```typescript
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { useSomeStore } from '@/stores/myStore'
|
||||
import YourComponent from '~/components/YourComponent.vue';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('~/helpers/someHelper', () => ({
|
||||
SOME_CONSTANT: 'mocked-value',
|
||||
}));
|
||||
|
||||
describe('YourComponent', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = mount(YourComponent, {
|
||||
global: {
|
||||
plugins: [createTestingPinia()],
|
||||
stubs: {
|
||||
// Stub child components when needed
|
||||
ChildComponent: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const store = useSomeStore() // uses the testing pinia!
|
||||
// state can be directly manipulated
|
||||
store.name = 'my new name'
|
||||
|
||||
// actions are stubbed by default, meaning they don't execute their code by default.
|
||||
// See below to customize this behavior.
|
||||
store.someAction()
|
||||
|
||||
expect(store.someAction).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Assertions on components
|
||||
expect(wrapper.text()).toContain('Expected content');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Patterns
|
||||
- Test component behavior and output, not implementation details
|
||||
- Verify that the expected elements are rendered
|
||||
- Test component interactions (clicks, inputs, etc.)
|
||||
- Check for expected prop handling and event emissions
|
||||
- Use `createTestingPinia()` for mocking stores in components
|
||||
|
||||
### Finding Elements
|
||||
- Use semantic queries like `find('button')` or `find('[data-test="id"]')` but prefer not to use data test ID's
|
||||
- Find components with `findComponent(ComponentName)`
|
||||
- Use `findAll` to check for multiple elements
|
||||
|
||||
### Assertions
|
||||
- Assert on rendered text content with `wrapper.text()`
|
||||
- Assert on element attributes with `element.attributes()`
|
||||
- Verify element existence with `expect(element.exists()).toBe(true)`
|
||||
- Check component state through rendered output
|
||||
|
||||
### Component Interaction
|
||||
- Trigger events with `await element.trigger('click')`
|
||||
- Set input values with `await input.setValue('value')`
|
||||
- Test emitted events with `wrapper.emitted()`
|
||||
|
||||
### Mocking
|
||||
- Mock external services and API calls
|
||||
- Prefer not using mocks whenever possible
|
||||
- Use `vi.mock()` for module-level mocks
|
||||
- Specify return values for component methods with `vi.spyOn()`
|
||||
- Reset mocks between tests with `vi.clearAllMocks()`
|
||||
- Frequently used mocks are stored under `web/test/mocks`
|
||||
|
||||
### Async Testing
|
||||
- Use `await nextTick()` for DOM updates
|
||||
- Use `flushPromises()` for more complex promise chains
|
||||
- Always await async operations before making assertions
|
||||
|
||||
## Store Testing with Pinia
|
||||
|
||||
### Basic Setup
|
||||
- When testing Store files use `createPinia` and `setActivePinia`
|
||||
|
||||
```typescript
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useYourStore } from '~/store/your-store';
|
||||
|
||||
// Mock declarations must be at top level due to hoisting
|
||||
const mockDependencyFn = vi.fn();
|
||||
|
||||
// Module mocks must use factory functions
|
||||
vi.mock('~/store/dependency', () => ({
|
||||
useDependencyStore: () => ({
|
||||
someMethod: mockDependencyFn,
|
||||
someProperty: 'mockValue'
|
||||
})
|
||||
}));
|
||||
|
||||
describe('Your Store', () => {
|
||||
let store: ReturnType<typeof useYourStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useYourStore();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('tests some action', () => {
|
||||
store.someAction();
|
||||
expect(mockDependencyFn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Important Guidelines
|
||||
1. **Store Initialization**
|
||||
- Use `createPinia()` instead of `createTestingPinia()` for most cases
|
||||
- Only use `createTestingPinia` if you specifically need its testing features
|
||||
- Let stores initialize with their natural default state instead of forcing initial state
|
||||
- Do not mock the store we're actually testing in the test file. That's why we're using `createPinia()`
|
||||
|
||||
2. **Vue Reactivity**
|
||||
- Ensure Vue reactivity imports are added to original store files as they may be missing because Nuxt auto import was turned on
|
||||
- Don't rely on Nuxt auto-imports in tests
|
||||
|
||||
```typescript
|
||||
// Required in store files, even with Nuxt auto-imports
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
```
|
||||
|
||||
3. **Mocking Best Practices**
|
||||
- Place all mock declarations at the top level
|
||||
- Use factory functions for module mocks to avoid hoisting issues
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - will cause hoisting issues
|
||||
const mockFn = vi.fn();
|
||||
vi.mock('module', () => ({ method: mockFn }));
|
||||
|
||||
// ✅ Correct - using factory function
|
||||
vi.mock('module', () => {
|
||||
const mockFn = vi.fn();
|
||||
return { method: mockFn };
|
||||
});
|
||||
```
|
||||
|
||||
4. **Testing Actions**
|
||||
- Test action side effects and state changes
|
||||
- Verify actions are called with correct parameters
|
||||
- Mock external dependencies appropriately
|
||||
|
||||
```typescript
|
||||
it('should handle action correctly', () => {
|
||||
store.yourAction();
|
||||
expect(mockDependencyFn).toHaveBeenCalledWith(
|
||||
expectedArg1,
|
||||
expectedArg2
|
||||
);
|
||||
expect(store.someState).toBe(expectedValue);
|
||||
});
|
||||
```
|
||||
|
||||
5. **Common Pitfalls**
|
||||
- Don't mix mock declarations and module mocks incorrectly
|
||||
- Avoid relying on Nuxt's auto-imports in test environment
|
||||
- Clear mocks between tests to ensure isolation
|
||||
- Remember that `vi.mock()` calls are hoisted
|
||||
|
||||
### Testing State & Getters
|
||||
- Test computed properties by accessing them directly
|
||||
- Verify state changes after actions
|
||||
- Test getter dependencies are properly mocked
|
||||
|
||||
```typescript
|
||||
it('computes derived state correctly', () => {
|
||||
store.setState('new value');
|
||||
expect(store.computedValue).toBe('expected result');
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Complex Interactions
|
||||
- Test store interactions with other stores
|
||||
- Verify proper error handling
|
||||
- Test async operations completely
|
||||
|
||||
```typescript
|
||||
it('handles async operations', async () => {
|
||||
const promise = store.asyncAction();
|
||||
expect(store.status).toBe('loading');
|
||||
await promise;
|
||||
expect(store.status).toBe('success');
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Actions
|
||||
- Verify actions are called with the right parameters
|
||||
- Test action side effects if not stubbed
|
||||
- Override specific action implementations when needed
|
||||
|
||||
```typescript
|
||||
// Test action calls
|
||||
store.yourAction(params);
|
||||
expect(store.yourAction).toHaveBeenCalledWith(params);
|
||||
|
||||
// Test with real implementation
|
||||
const pinia = createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false,
|
||||
});
|
||||
```
|
||||
|
||||
### Testing State & Getters
|
||||
- Set initial state for focused testing
|
||||
- Test computed properties by accessing them directly
|
||||
- Verify state changes by updating the store
|
||||
|
||||
190
.github/workflows/build-plugin.yml
vendored
Normal file
190
.github/workflows/build-plugin.yml
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
name: Build Plugin Component
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
RELEASE_CREATED:
|
||||
type: string
|
||||
required: true
|
||||
description: "Whether a release was created"
|
||||
RELEASE_TAG:
|
||||
type: string
|
||||
required: false
|
||||
description: "Name of the tag when a release is created"
|
||||
TAG:
|
||||
type: string
|
||||
required: false
|
||||
description: "Tag for the build (e.g. PR number or version)"
|
||||
BUCKET_PATH:
|
||||
type: string
|
||||
required: true
|
||||
description: "Path in the bucket where artifacts should be stored"
|
||||
BASE_URL:
|
||||
type: string
|
||||
required: true
|
||||
description: "Base URL for the plugin builds"
|
||||
secrets:
|
||||
CF_ACCESS_KEY_ID:
|
||||
required: true
|
||||
CF_SECRET_ACCESS_KEY:
|
||||
required: true
|
||||
CF_BUCKET_PREVIEW:
|
||||
required: true
|
||||
CF_ENDPOINT:
|
||||
required: true
|
||||
jobs:
|
||||
build-plugin:
|
||||
name: Build and Deploy Plugin
|
||||
defaults:
|
||||
run:
|
||||
working-directory: plugin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get API Version
|
||||
id: vars
|
||||
run: |
|
||||
GIT_SHA=$(git rev-parse --short HEAD)
|
||||
IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '')
|
||||
PACKAGE_LOCK_VERSION=$(jq -r '.version' package.json)
|
||||
API_VERSION=$([[ -n "$IS_TAGGED" ]] && echo "$PACKAGE_LOCK_VERSION" || echo "${PACKAGE_LOCK_VERSION}+${GIT_SHA}")
|
||||
echo "API_VERSION=${API_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd ${{ github.workspace }}
|
||||
pnpm install --frozen-lockfile --filter @unraid/connect-plugin
|
||||
|
||||
- name: Download Unraid UI Components
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unraid-wc-ui
|
||||
path: ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/uui
|
||||
merge-multiple: true
|
||||
- name: Download Unraid Web Components
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: unraid-wc-rich
|
||||
path: ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt
|
||||
merge-multiple: true
|
||||
- name: Download Unraid API
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unraid-api
|
||||
path: ${{ github.workspace }}/plugin/api/
|
||||
- name: Download PNPM Store
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: packed-node-modules
|
||||
path: ${{ github.workspace }}/plugin/
|
||||
- name: Extract Unraid API
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
|
||||
tar -xzf ${{ github.workspace }}/plugin/api/unraid-api.tgz -C ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
|
||||
- name: Build Plugin and TXZ Based on Event and Tag
|
||||
id: build-plugin
|
||||
run: |
|
||||
cd ${{ github.workspace }}/plugin
|
||||
ls -al
|
||||
pnpm run build:txz
|
||||
pnpm run build:plugin --tag="${{ inputs.TAG }}" --base-url="${{ inputs.BASE_URL }}"
|
||||
|
||||
- name: Ensure Plugin Files Exist
|
||||
run: |
|
||||
ls -al ./deploy
|
||||
if [ ! -f ./deploy/*.plg ]; then
|
||||
echo "Error: .plg file not found in plugin/deploy/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f ./deploy/*.txz ]; then
|
||||
echo "Error: .txz file not found in plugin/deploy/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f ./deploy/*.tar.xz ]; then
|
||||
echo "Error: .tar.xz file not found in plugin/deploy/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload to GHA
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unraid-plugin-${{ github.run_id }}-${{ inputs.RELEASE_TAG }}
|
||||
path: plugin/deploy/
|
||||
|
||||
- name: Upload Release Assets
|
||||
if: inputs.RELEASE_CREATED == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.RELEASE_TAG }}
|
||||
run: |
|
||||
# For each file in release directory
|
||||
for file in deploy/*; do
|
||||
echo "Uploading $file to release..."
|
||||
gh release upload "${RELEASE_TAG}" "$file" --clobber
|
||||
done
|
||||
|
||||
- name: Workflow Dispatch and wait
|
||||
if: inputs.RELEASE_CREATED == 'true'
|
||||
uses: the-actions-org/workflow-dispatch@v4.0.0
|
||||
with:
|
||||
workflow: release-production.yml
|
||||
inputs: '{ "version": "${{ steps.vars.outputs.API_VERSION }}" }'
|
||||
token: ${{ secrets.WORKFLOW_TRIGGER_PAT }}
|
||||
|
||||
- name: Upload to Cloudflare
|
||||
if: inputs.RELEASE_CREATED == 'false'
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
run: |
|
||||
# Sync the deploy directory to the Cloudflare bucket with explicit content encoding and public-read ACL
|
||||
aws s3 sync deploy/ s3://${{ secrets.CF_BUCKET_PREVIEW }}/${{ inputs.BUCKET_PATH }} \
|
||||
--endpoint-url ${{ secrets.CF_ENDPOINT }} \
|
||||
--checksum-algorithm CRC32 \
|
||||
--no-guess-mime-type \
|
||||
--content-encoding none \
|
||||
--acl public-read
|
||||
|
||||
- name: Comment URL
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
comment-tag: prlink
|
||||
mode: recreate
|
||||
message: |
|
||||
This plugin has been deployed to Cloudflare R2 and is available for testing.
|
||||
Download it at this URL:
|
||||
```
|
||||
${{ inputs.BASE_URL }}/tag/${{ inputs.TAG }}/dynamix.unraid.net.plg
|
||||
```
|
||||
258
.github/workflows/main.yml
vendored
258
.github/workflows/main.yml
vendored
@@ -13,7 +13,6 @@ concurrency:
|
||||
jobs:
|
||||
release-please:
|
||||
name: Release Please
|
||||
# Only run release-please on pushes to main
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -21,6 +20,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
# Only run release-please on pushes to main
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
- id: release
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
- name: Cache APT Packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
|
||||
with:
|
||||
packages: bash procps python3 libvirt-dev jq zstd git build-essential
|
||||
packages: bash procps python3 libvirt-dev jq zstd git build-essential libvirt-daemon-system
|
||||
version: 1.0
|
||||
|
||||
- name: Install pnpm
|
||||
@@ -72,6 +72,45 @@ jobs:
|
||||
- name: PNPM Install
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup libvirt
|
||||
run: |
|
||||
# Create required groups (if they don't already exist)
|
||||
sudo groupadd -f libvirt
|
||||
sudo groupadd -f kvm
|
||||
|
||||
# Create libvirt user if not present, and add it to the kvm group
|
||||
sudo useradd -m -s /bin/bash -g libvirt libvirt || true
|
||||
sudo usermod -aG kvm libvirt || true
|
||||
|
||||
# Set up libvirt directories and permissions
|
||||
sudo mkdir -p /var/run/libvirt /var/log/libvirt /etc/libvirt
|
||||
sudo chown root:libvirt /var/run/libvirt /var/log/libvirt
|
||||
sudo chmod g+w /var/run/libvirt /var/log/libvirt
|
||||
|
||||
# Configure libvirt by appending required settings
|
||||
sudo tee -a /etc/libvirt/libvirtd.conf > /dev/null <<EOF
|
||||
unix_sock_group = "libvirt"
|
||||
unix_sock_rw_perms = "0770"
|
||||
auth_unix_rw = "none"
|
||||
EOF
|
||||
|
||||
# Add the current user to libvirt and kvm groups (note: this change won’t apply to the current session)
|
||||
sudo usermod -aG libvirt,kvm $USER
|
||||
|
||||
sudo mkdir -p /var/run/libvirt
|
||||
sudo chown root:libvirt /var/run/libvirt
|
||||
sudo chmod 775 /var/run/libvirt
|
||||
|
||||
|
||||
# Start libvirtd in the background
|
||||
sudo /usr/sbin/libvirtd --daemon
|
||||
|
||||
# Wait a bit longer for libvirtd to start
|
||||
sleep 5
|
||||
|
||||
# Verify libvirt is running using sudo to bypass group membership delays
|
||||
sudo virsh list --all || true
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
@@ -152,11 +191,11 @@ jobs:
|
||||
with:
|
||||
name: unraid-api
|
||||
path: ${{ github.workspace }}/api/deploy/unraid-api.tgz
|
||||
- name: Upload PNPM Store to Github artifacts
|
||||
- name: Upload Node Modules to Github artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packed-pnpm-store
|
||||
path: ${{ github.workspace }}/api/deploy/packed-pnpm-store.txz
|
||||
name: packed-node-modules
|
||||
path: ${{ github.workspace }}/api/deploy/packed-node-modules.tar.xz
|
||||
|
||||
build-unraid-ui-webcomponents:
|
||||
name: Build Unraid UI Library (Webcomponent Version)
|
||||
@@ -203,6 +242,9 @@ jobs:
|
||||
cd ${{ github.workspace }}
|
||||
pnpm install --frozen-lockfile --filter @unraid/ui
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build:wc
|
||||
|
||||
@@ -284,7 +326,7 @@ jobs:
|
||||
name: unraid-wc-rich
|
||||
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
|
||||
|
||||
build-plugin:
|
||||
build-plugin-staging-pr:
|
||||
name: Build and Deploy Plugin
|
||||
needs:
|
||||
- release-please
|
||||
@@ -292,176 +334,36 @@ jobs:
|
||||
- build-web
|
||||
- build-unraid-ui-webcomponents
|
||||
- test-api
|
||||
defaults:
|
||||
run:
|
||||
working-directory: plugin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set Timezone
|
||||
uses: szenius/set-timezone@v2.0
|
||||
with:
|
||||
timezoneLinux: "America/Los_Angeles"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
uses: ./.github/workflows/build-plugin.yml
|
||||
with:
|
||||
RELEASE_CREATED: false
|
||||
TAG: ${{ github.event.pull_request.number && format('PR{0}', github.event.pull_request.number) || '' }}
|
||||
BUCKET_PATH: ${{ github.event.pull_request.number && format('unraid-api/tag/PR{0}', github.event.pull_request.number) || 'unraid-api' }}
|
||||
BASE_URL: "https://preview.dl.unraid.net/unraid-api"
|
||||
secrets:
|
||||
CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }}
|
||||
CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get API Version
|
||||
id: vars
|
||||
run: |
|
||||
GIT_SHA=$(git rev-parse --short HEAD)
|
||||
IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '')
|
||||
PACKAGE_LOCK_VERSION=$(jq -r '.version' package.json)
|
||||
API_VERSION=$([[ -n "$IS_TAGGED" ]] && echo "$PACKAGE_LOCK_VERSION" || echo "${PACKAGE_LOCK_VERSION}+${GIT_SHA}")
|
||||
echo "API_VERSION=${API_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd ${{ github.workspace }}
|
||||
pnpm install --frozen-lockfile --filter @unraid/connect-plugin
|
||||
|
||||
- name: Download Unraid UI Components
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unraid-wc-ui
|
||||
path: ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/uui
|
||||
merge-multiple: true
|
||||
- name: Download Unraid Web Components
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: unraid-wc-rich
|
||||
path: ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt
|
||||
merge-multiple: true
|
||||
- name: Download Unraid API
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unraid-api
|
||||
path: ${{ github.workspace }}/plugin/api/
|
||||
- name: Download PNPM Store
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: packed-pnpm-store
|
||||
path: ${{ github.workspace }}/plugin/
|
||||
- name: Extract Unraid API
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
|
||||
tar -xzf ${{ github.workspace }}/plugin/api/unraid-api.tgz -C ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
|
||||
- name: Build Plugin and TXZ Based on Event and Tag
|
||||
id: build-plugin
|
||||
run: |
|
||||
cd ${{ github.workspace }}/plugin
|
||||
ls -al
|
||||
pnpm run build:txz
|
||||
|
||||
if [ -n "${{ github.event.pull_request.number }}" ]; then
|
||||
TAG="PR${{ github.event.pull_request.number }}"
|
||||
BUCKET_PATH="unraid-api/tag/${TAG}"
|
||||
else
|
||||
TAG=""
|
||||
BUCKET_PATH="unraid-api"
|
||||
fi
|
||||
|
||||
# On release, build both prod and preview plugins
|
||||
if [ "${{ needs.release-please.outputs.releases_created }}" == 'true' ]; then
|
||||
BASE_URL="https://stable.dl.unraid.net/unraid-api"
|
||||
pnpm run build:plugin --tag="${TAG}" --base-url="${BASE_URL}"
|
||||
cp -r ./deploy ./deploy-prod
|
||||
fi
|
||||
|
||||
BASE_URL="https://preview.dl.unraid.net/unraid-api"
|
||||
echo "BUCKET_PATH=${BUCKET_PATH}" >> $GITHUB_OUTPUT
|
||||
echo "TAG=${TAG}" >> $GITHUB_OUTPUT
|
||||
pnpm run build:plugin --tag="${TAG}" --base-url="${BASE_URL}"
|
||||
- name: Ensure Plugin Files Exist
|
||||
run: |
|
||||
ls -al ./deploy
|
||||
if [ ! -f ./deploy/*.plg ]; then
|
||||
echo "Error: .plg file not found in plugin/deploy/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f ./deploy/*.txz ]; then
|
||||
echo "Error: .txz file not found in plugin/deploy/"
|
||||
exit 1
|
||||
fi
|
||||
- name: Ensure Production Plugin Files Exist
|
||||
if: needs.release-please.outputs.releases_created == 'true'
|
||||
run: |
|
||||
ls -al ./deploy-prod
|
||||
if [ ! -f ./deploy-prod/*.plg ]; then
|
||||
echo "Error: .plg file not found in plugin/deploy-prod/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f ./deploy-prod/*.txz ]; then
|
||||
echo "Error: .txz file not found in plugin/deploy-prod/"
|
||||
exit 1
|
||||
fi
|
||||
- name: Upload to GHA
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unraid-plugin
|
||||
path: plugin/deploy/
|
||||
- name: Upload to Cloudflare
|
||||
if: github.event_name == 'pull_request' || startsWith(github.ref, 'refs/heads/main')
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
run: |
|
||||
# Sync the deploy directory to the Cloudflare bucket with explicit content encoding and public-read ACL
|
||||
aws s3 sync deploy/ s3://${{ secrets.CF_BUCKET_PREVIEW }}/${{ steps.build-plugin.outputs.BUCKET_PATH }} \
|
||||
--endpoint-url ${{ secrets.CF_ENDPOINT }} \
|
||||
--checksum-algorithm CRC32 \
|
||||
--no-guess-mime-type \
|
||||
--content-encoding none \
|
||||
--acl public-read
|
||||
|
||||
- name: Upload Release Assets
|
||||
if: needs.release-please.outputs.releases_created == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
release_name=$(gh release list --repo ${{ github.repository }} --json name,isDraft --jq '.[] | select(.isDraft == true) | .name' | head -n 1)
|
||||
# For each file in release directory
|
||||
for file in deploy-prod/*; do
|
||||
echo "Uploading $file to release..."
|
||||
gh release upload "${release_name}" "$file" --clobber
|
||||
done
|
||||
|
||||
- name: Comment URL
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
comment-tag: prlink
|
||||
mode: recreate
|
||||
message: |
|
||||
This plugin has been deployed to Cloudflare R2 and is available for testing.
|
||||
Download it at this URL:
|
||||
```
|
||||
https://preview.dl.unraid.net/unraid-api/tag/${{ steps.build-plugin.outputs.tag }}/dynamix.unraid.net.plg
|
||||
```
|
||||
build-plugin-production:
|
||||
if: ${{ needs.release-please.outputs.releases_created == 'true' }}
|
||||
name: Build and Deploy Production Plugin
|
||||
needs:
|
||||
- release-please
|
||||
- build-api
|
||||
- build-web
|
||||
- build-unraid-ui-webcomponents
|
||||
- test-api
|
||||
uses: ./.github/workflows/build-plugin.yml
|
||||
with:
|
||||
RELEASE_CREATED: true
|
||||
RELEASE_TAG: ${{ needs.release-please.outputs.tag_name }}
|
||||
TAG: ""
|
||||
BUCKET_PATH: unraid-api
|
||||
BASE_URL: "https://stable.dl.unraid.net/unraid-api"
|
||||
secrets:
|
||||
CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }}
|
||||
CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
|
||||
|
||||
17
.github/workflows/release-production.yml
vendored
17
.github/workflows/release-production.yml
vendored
@@ -1,17 +1,14 @@
|
||||
name: Publish Release to Digital Ocean
|
||||
name: Publish Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Tag to update'
|
||||
description: 'Tag to release - will replace active release'
|
||||
required: true
|
||||
|
||||
|
||||
jobs:
|
||||
publish-to-digital-ocean:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -34,6 +31,8 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
- run: |
|
||||
echo '${{ steps.release-info.outputs.body }}' >> release-notes.txt
|
||||
- run: npm install html-escaper@2 xml2js
|
||||
- name: Update Plugin Changelog
|
||||
uses: actions/github-script@v7
|
||||
@@ -41,7 +40,8 @@ jobs:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const { escape } = require('html-escaper');
|
||||
const releaseNotes = escape(`${{ steps.release-info.outputs.body }}`);
|
||||
|
||||
const releaseNotes = escape(fs.readFileSync('release-notes.txt', 'utf8'));
|
||||
|
||||
if (!releaseNotes) {
|
||||
console.error('No release notes found');
|
||||
@@ -92,6 +92,9 @@ jobs:
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
- name: Cleanup Inline Scripts
|
||||
run: |
|
||||
rm -rf node_modules/
|
||||
- name: Upload Release Files to DO Spaces
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.DO_ACCESS_KEY }}
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -105,4 +105,7 @@ result-*
|
||||
web/scripts/.sync-webgui-repo-*
|
||||
|
||||
# Activation code data
|
||||
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php
|
||||
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php
|
||||
|
||||
# Config file that changes between versions
|
||||
api/dev/Unraid.net/myservers.cfg
|
||||
12
.husky/_/pre-commit
Executable file
12
.husky/_/pre-commit
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$SKIP_SIMPLE_GIT_HOOKS" = "1" ]; then
|
||||
echo "[INFO] SKIP_SIMPLE_GIT_HOOKS is set to 1, skipping hook."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "$SIMPLE_GIT_HOOKS_RC" ]; then
|
||||
. "$SIMPLE_GIT_HOOKS_RC"
|
||||
fi
|
||||
|
||||
pnpm lint-staged
|
||||
@@ -1 +1 @@
|
||||
{".":"4.6.1"}
|
||||
{".":"4.8.0"}
|
||||
|
||||
@@ -9,6 +9,10 @@ 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
|
||||
PATHS_MACHINE_ID=./dev/data/machine-id
|
||||
PATHS_PARITY_CHECKS=./dev/states/parity-checks.log
|
||||
PATHS_CONFIG_MODULES=./dev/configs
|
||||
PATHS_ACTIVATION_BASE=./dev/activation
|
||||
PATHS_PASSWD=./dev/passwd
|
||||
ENVIRONMENT="development"
|
||||
NODE_ENV="development"
|
||||
PORT="3001"
|
||||
@@ -20,4 +24,4 @@ BYPASS_PERMISSION_CHECKS=false
|
||||
BYPASS_CORS_CHECKS=true
|
||||
CHOKIDAR_USEPOLLING=true
|
||||
LOG_TRANSPORT=console
|
||||
LOG_LEVEL=trace
|
||||
LOG_LEVEL=trace
|
||||
|
||||
@@ -2,3 +2,4 @@ ENVIRONMENT="production"
|
||||
NODE_ENV="production"
|
||||
PORT="/var/run/unraid-api.sock"
|
||||
MOTHERSHIP_GRAPHQL_LINK="https://mothership.unraid.net/ws"
|
||||
PATHS_CONFIG_MODULES="/boot/config/plugins/dynamix.my.servers/configs"
|
||||
|
||||
@@ -2,3 +2,4 @@ ENVIRONMENT="staging"
|
||||
NODE_ENV="production"
|
||||
PORT="/var/run/unraid-api.sock"
|
||||
MOTHERSHIP_GRAPHQL_LINK="https://staging.mothership.unraid.net/ws"
|
||||
PATHS_CONFIG_MODULES="/boot/config/plugins/dynamix.my.servers/configs"
|
||||
|
||||
@@ -9,5 +9,9 @@ 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
|
||||
PATHS_MACHINE_ID=./dev/data/machine-id
|
||||
PATHS_PARITY_CHECKS=./dev/states/parity-checks.log
|
||||
PATHS_CONFIG_MODULES=./dev/configs
|
||||
PATHS_ACTIVATION_BASE=./dev/activation
|
||||
PATHS_PASSWD=./dev/passwd
|
||||
PORT=5000
|
||||
NODE_ENV="test"
|
||||
NODE_ENV="test"
|
||||
|
||||
@@ -1,5 +1,77 @@
|
||||
# Changelog
|
||||
|
||||
## [4.8.0](https://github.com/unraid/api/compare/v4.7.0...v4.8.0) (2025-05-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* move activation code logic into the API ([#1369](https://github.com/unraid/api/issues/1369)) ([39e83b2](https://github.com/unraid/api/commit/39e83b2aa156586ab4da362137194280fccefe7c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 400 error when submitting connect settings ([831050f](https://github.com/unraid/api/commit/831050f4e8c3af4cbcc123a3a609025f250f0824))
|
||||
|
||||
## [4.7.0](https://github.com/unraid/api/compare/v4.6.6...v4.7.0) (2025-04-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add basic docker network listing ([#1317](https://github.com/unraid/api/issues/1317)) ([c4fdff8](https://github.com/unraid/api/commit/c4fdff8149eb2812707605b3a98eabc795d18c5e))
|
||||
* add permission documentation by using a custom decorator ([#1355](https://github.com/unraid/api/issues/1355)) ([45ecab6](https://github.com/unraid/api/commit/45ecab6914e2e4dd48438352eb9a5084a6a4b996))
|
||||
* basic vm controls ([#1293](https://github.com/unraid/api/issues/1293)) ([bc3ca92](https://github.com/unraid/api/commit/bc3ca92fb02387bc019bb001809df96974737b50))
|
||||
* code first graphql ([#1347](https://github.com/unraid/api/issues/1347)) ([f5724ab](https://github.com/unraid/api/commit/f5724abffbcb8c8a4885c487df4119787fd1d541))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* container names always null ([#1335](https://github.com/unraid/api/issues/1335)) ([8a5b238](https://github.com/unraid/api/commit/8a5b23856c006827229812e558f7d1af92be80e0))
|
||||
* **deps:** update all non-major dependencies ([#1337](https://github.com/unraid/api/issues/1337)) ([2345732](https://github.com/unraid/api/commit/234573264cfed1409a767927ff95f132be393ea9))
|
||||
* hide reboot notice for patch releases ([#1341](https://github.com/unraid/api/issues/1341)) ([4b57439](https://github.com/unraid/api/commit/4b5743906a172f84bb46011fe2c3e0c8f64059a2))
|
||||
* move docker mutations to the mutations resolver ([#1333](https://github.com/unraid/api/issues/1333)) ([1bbe7d2](https://github.com/unraid/api/commit/1bbe7d27b0e87b5ffcd57ac9cc28e64b046055be))
|
||||
* PR build issue ([457d338](https://github.com/unraid/api/commit/457d338150774ddc14cde6562e226a6a565aca48))
|
||||
* remove some unused fields from the report object ([#1342](https://github.com/unraid/api/issues/1342)) ([cd323ac](https://github.com/unraid/api/commit/cd323acd4905a558786b029ff5a30371c4512956))
|
||||
* sso unreliable if API outputs more than raw json ([#1353](https://github.com/unraid/api/issues/1353)) ([e65775f](https://github.com/unraid/api/commit/e65775f8782714d1cc29c8f2801244b5a4043409))
|
||||
* vms now can detect starting of libvirt and start local hypervisor ([#1356](https://github.com/unraid/api/issues/1356)) ([ad0f4c8](https://github.com/unraid/api/commit/ad0f4c8b55c7f7e94fbae2108f17715b1373a3ef))
|
||||
|
||||
## [4.6.6](https://github.com/unraid/api/compare/v4.6.5...v4.6.6) (2025-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* issue with invalid builds for prod and tagging ([7e89cd2](https://github.com/unraid/api/commit/7e89cd2a3e06a4abc8164f2f4985ad9f6cc9388d))
|
||||
|
||||
## [4.6.5](https://github.com/unraid/api/compare/v4.6.4...v4.6.5) (2025-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* unique artifact ID ([0f682b5](https://github.com/unraid/api/commit/0f682b5f23f4319a1ad8f0e8f2b5e5ae0a2293db))
|
||||
|
||||
## [4.6.4](https://github.com/unraid/api/compare/v4.6.3...v4.6.4) (2025-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cleanup build pipeline ([#1326](https://github.com/unraid/api/issues/1326)) ([60f16bd](https://github.com/unraid/api/commit/60f16bde416993771fce2ad5861a671504af4b7d))
|
||||
* remove unneeded workflow secret pass ([4bb00dd](https://github.com/unraid/api/commit/4bb00dd981384083cec40d804209ec2ca18d7aae))
|
||||
|
||||
## [4.6.3](https://github.com/unraid/api/compare/v4.6.2...v4.6.3) (2025-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* copy dynamix.unraid.net ([662d5f6](https://github.com/unraid/api/commit/662d5f64c94586e35bfdaae2df0716c3754b2c45))
|
||||
* make backup of txz ([37e72f9](https://github.com/unraid/api/commit/37e72f9729f6ab385ed1070fbdca6028688fbd92))
|
||||
* ordering in build script ([a562f77](https://github.com/unraid/api/commit/a562f7716380bde4a1ae0d6960eff51c37b9291c))
|
||||
|
||||
## [4.6.2](https://github.com/unraid/api/compare/v4.6.1...v4.6.2) (2025-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* build issue ([99d8b31](https://github.com/unraid/api/commit/99d8b31fa8bef13ae6c7dcf74593bc2999a676ed))
|
||||
|
||||
## [4.6.1](https://github.com/unraid/api/compare/v4.6.0...v4.6.1) (2025-04-03)
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import type { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
emitLegacyCommonJSImports: false,
|
||||
verbose: true,
|
||||
config: {
|
||||
namingConvention: {
|
||||
typeNames: './fix-array-type.cjs',
|
||||
enumValues: 'change-case#upperCase',
|
||||
enumValues: 'change-case-all#upperCase',
|
||||
transformUnderscore: true,
|
||||
useTypeImports: true,
|
||||
},
|
||||
scalars: {
|
||||
@@ -31,6 +27,7 @@ const config: CodegenConfig = {
|
||||
},
|
||||
},
|
||||
generates: {
|
||||
// Generate Types for Mothership GraphQL Client
|
||||
'src/graphql/generated/client/': {
|
||||
documents: './src/graphql/mothership/*.ts',
|
||||
schema: {
|
||||
@@ -50,40 +47,6 @@ const config: CodegenConfig = {
|
||||
},
|
||||
plugins: [{ add: { content: '/* eslint-disable */' } }],
|
||||
},
|
||||
// Generate Types for the API Server
|
||||
'src/graphql/generated/api/types.ts': {
|
||||
schema: ['./src/graphql/types.ts', './src/graphql/schema/types/**/*.graphql'],
|
||||
plugins: [
|
||||
'typescript',
|
||||
'typescript-resolvers',
|
||||
{ add: { content: '/* eslint-disable */\n/* @ts-nocheck */' } },
|
||||
],
|
||||
config: {
|
||||
contextType: '@app/graphql/schema/utils.js#Context',
|
||||
useIndexSignature: true,
|
||||
},
|
||||
},
|
||||
// Generate Operations for any built-in API Server Operations (e.g., report.ts)
|
||||
'src/graphql/generated/api/operations.ts': {
|
||||
documents: './src/graphql/client/api/*.ts',
|
||||
schema: ['./src/graphql/types.ts', './src/graphql/schema/types/**/*.graphql'],
|
||||
preset: 'import-types',
|
||||
presetConfig: {
|
||||
typesPath: '@app/graphql/generated/api/types.js',
|
||||
},
|
||||
plugins: [
|
||||
'typescript-validation-schema',
|
||||
'typescript-operations',
|
||||
'typed-document-node',
|
||||
{ add: { content: '/* eslint-disable */' } },
|
||||
],
|
||||
config: {
|
||||
importFrom: '@app/graphql/generated/api/types.js',
|
||||
strictScalars: true,
|
||||
schema: 'zod',
|
||||
withObjectType: true,
|
||||
},
|
||||
},
|
||||
'src/graphql/generated/client/validators.ts': {
|
||||
schema: {
|
||||
[process.env.MOTHERSHIP_GRAPHQL_LINK as string]: {
|
||||
@@ -102,4 +65,4 @@ const config: CodegenConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
export default config;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="4.1.3"
|
||||
version="4.7.0"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox="yes"
|
||||
|
||||
20
api/dev/Unraid.net/myservers.example.cfg
Normal file
20
api/dev/Unraid.net/myservers.example.cfg
Normal file
@@ -0,0 +1,20 @@
|
||||
[api]
|
||||
version="4.4.1"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox="yes"
|
||||
[remote]
|
||||
wanaccess="yes"
|
||||
wanport="8443"
|
||||
upnpEnabled="no"
|
||||
apikey="_______________________BIG_API_KEY_HERE_________________________"
|
||||
localApiKey="_______________________LOCAL_API_KEY_HERE_________________________"
|
||||
email="test@example.com"
|
||||
username="zspearmint"
|
||||
avatar="https://via.placeholder.com/200"
|
||||
regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
|
||||
accesstoken=""
|
||||
idtoken=""
|
||||
refreshtoken=""
|
||||
dynamicRemoteAccessType="DISABLED"
|
||||
ssoSubIds=""
|
||||
13
api/dev/activation/activation_code_12345.activationcode
Normal file
13
api/dev/activation/activation_code_12345.activationcode
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"code": "EXAMPLE_CODE_123",
|
||||
"partnerName": "MyPartner Inc.",
|
||||
"partnerUrl": "https://partner.example.com",
|
||||
"serverName": "MyAwesomeServer",
|
||||
"sysModel": "CustomBuild v1.0",
|
||||
"comment": "This is a test activation code for development.",
|
||||
"header": "#336699",
|
||||
"headermetacolor": "#FFFFFF",
|
||||
"background": "#F0F0F0",
|
||||
"showBannerGradient": "yes",
|
||||
"theme": "black"
|
||||
}
|
||||
1
api/dev/activation/applied.txt
Normal file
1
api/dev/activation/applied.txt
Normal file
@@ -0,0 +1 @@
|
||||
true
|
||||
BIN
api/dev/activation/assets/case-model.png
Normal file
BIN
api/dev/activation/assets/case-model.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
19
api/dev/activation/assets/logo.svg
Normal file
19
api/dev/activation/assets/logo.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="442" height="221">
|
||||
<defs>
|
||||
<linearGradient id="gradient_0" gradientUnits="userSpaceOnUse" x1="608.84924" y1="48.058002" x2="447.47684" y2="388.15295">
|
||||
<stop offset="0" stop-color="#ECC02F"/>
|
||||
<stop offset="1" stop-color="#B8436B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#gradient_0)" transform="scale(0.431641 0.431641)" d="M126.543 236.139C141.269 184.983 170.747 148.08 228.938 144.823C240.378 144.182 259.66 144.749 271.333 145.215C299.585 144.391 350.558 142.667 377.842 145.685C414.099 149.696 443.185 175.429 472.192 195.251L586.561 274.337C636.114 308.874 627.234 309.151 685.21 309.042L778.304 309.082C799.091 309.099 813.482 308.867 828.82 292.529C857.893 261.561 843.003 209.317 800.506 200.17C790.505 198.018 779.334 199.535 769.11 199.523L702.658 199.488C690.005 186.062 675.199 151.817 658.182 145.215L739.199 145.198C765.636 145.196 796.164 142.886 821.565 150.344C923.889 180.389 922.324 331.136 816.611 357.807C802.524 361.361 788.425 361.034 774.035 361.031L663.497 361.009C623.773 360.859 603.599 349.313 572.35 327.596L430.421 229.848C415.731 219.804 401.419 209.118 386.451 199.488C377.579 199.501 368.42 200.01 359.582 199.488L272.561 199.497C258.582 199.485 235.352 198.06 222.607 200.981C192.741 207.825 177.956 234.361 180.015 263.294C177.545 260.392 178.63 254.678 178.838 251.164C179.877 233.569 187.409 224.968 197.345 212.22C184.786 202.853 156.933 193.749 149.447 186.645C143.454 196.583 136.881 205.628 132.955 216.732C130.766 222.921 130.678 230.967 127.506 236.625L126.543 236.139Z"/>
|
||||
<path fill="#308DAF" transform="scale(0.431641 0.431641)" d="M149.447 186.645C156.933 193.749 184.786 202.853 197.345 212.22C187.409 224.968 179.877 233.569 178.838 251.164C178.63 254.678 177.545 260.392 180.015 263.294C192.489 309.751 221.563 309.078 263.512 309.07L322.096 309.048C333.708 325.984 348.958 344.904 361.795 361.006L232.654 361.03C176.801 360.579 130.605 315.939 126.498 260.613C125.893 252.473 126.453 244.293 126.543 236.139L127.506 236.625C130.678 230.967 130.766 222.921 132.955 216.732C136.881 205.628 143.454 196.583 149.447 186.645Z"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient_1" gradientUnits="userSpaceOnUse" x1="620.42566" y1="140.57172" x2="611.08759" y2="282.2207">
|
||||
<stop offset="0" stop-color="#F5A22C"/>
|
||||
<stop offset="1" stop-color="#E17543"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#gradient_1)" transform="scale(0.431641 0.431641)" d="M570.215 137.504C646.214 133.055 670.623 188.789 707.064 241.977L726.71 270.658C729.065 274.1 737.591 284.13 737.576 287.916L674.645 287.916C674.5 287.132 659.251 264.134 658.182 263.294C658.133 262.92 623.915 212.832 620.593 208.697C602.652 186.369 565.856 181.796 545.393 203.424C542.002 207.007 539.705 211.779 535.713 214.764C534.409 212.586 496.093 187.105 490.641 183.32C508.306 154.99 539.004 142.872 570.215 137.504Z"/>
|
||||
<path fill="#308DAF" transform="scale(0.431641 0.431641)" d="M286.656 221.485L350.512 221.485C354.248 227.374 358.556 232.986 362.565 238.698L379.9 263.82C397.44 289.065 410.994 321.185 447.698 317.317C464.599 315.536 476.472 305.449 486.751 292.741C494.293 298.818 530.089 320.341 533.124 324.28C532.441 328.231 526.229 334.319 522.861 336.255C521.587 339.958 509.164 348.519 505.635 350.88C463.781 378.879 411.472 377.537 373.808 343.464C365.331 335.795 359.734 326.969 353.351 317.641L336.798 293.614C320.035 269.591 302.915 245.863 286.656 221.485Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
3
api/dev/configs/connect.json
Normal file
3
api/dev/configs/connect.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"demo": "hello.unraider"
|
||||
}
|
||||
@@ -1,36 +1,42 @@
|
||||
[display]
|
||||
date="%c"
|
||||
time="%I:%M %p"
|
||||
number=".,"
|
||||
scale="-1"
|
||||
tabs="1"
|
||||
users="Tasks:3"
|
||||
resize="0"
|
||||
wwn="0"
|
||||
total="1"
|
||||
usage="0"
|
||||
banner="image"
|
||||
dashapps="icons"
|
||||
theme="white"
|
||||
text="1"
|
||||
unit="C"
|
||||
warning="70"
|
||||
critical="90"
|
||||
hot="45"
|
||||
max="55"
|
||||
sysinfo="/Tools/SystemProfiler"
|
||||
date=%c
|
||||
time=%I:%M %p
|
||||
number=.,
|
||||
scale=-1
|
||||
tabs=1
|
||||
users=Tasks:3
|
||||
resize=0
|
||||
wwn=0
|
||||
total=1
|
||||
usage=0
|
||||
banner=image
|
||||
dashapps=icons
|
||||
theme=black
|
||||
text=1
|
||||
unit=C
|
||||
warning=70
|
||||
critical=90
|
||||
hot=45
|
||||
max=55
|
||||
sysinfo=/Tools/SystemProfiler
|
||||
header=336699
|
||||
headermetacolor=FFFFFF
|
||||
background=F0F0F0
|
||||
showBannerGradient=yes
|
||||
|
||||
[notify]
|
||||
entity="1"
|
||||
normal="1"
|
||||
warning="1"
|
||||
alert="1"
|
||||
unraid="1"
|
||||
plugin="1"
|
||||
docker_notify="1"
|
||||
report="1"
|
||||
display="0"
|
||||
date="d-m-Y"
|
||||
time="H:i"
|
||||
position="top-right"
|
||||
path="./dev/notifications"
|
||||
system="*/1 * * * *"
|
||||
entity=1
|
||||
normal=1
|
||||
warning=1
|
||||
alert=1
|
||||
unraid=1
|
||||
plugin=1
|
||||
docker_notify=1
|
||||
report=1
|
||||
display=0
|
||||
date=d-m-Y
|
||||
time=H:i
|
||||
position=top-right
|
||||
path=./dev/notifications
|
||||
system=*/1 * * * *
|
||||
|
||||
|
||||
36
api/dev/ident.cfg
Normal file
36
api/dev/ident.cfg
Normal file
@@ -0,0 +1,36 @@
|
||||
# Generated settings:
|
||||
NAME="Unraid"
|
||||
timeZone="America/New_York"
|
||||
COMMENT="Media server"
|
||||
SECURITY="user"
|
||||
WORKGROUP="WORKGROUP"
|
||||
DOMAIN=""
|
||||
DOMAIN_SHORT=""
|
||||
hideDotFiles="no"
|
||||
enableFruit="yes"
|
||||
USE_NETBIOS="no"
|
||||
localMaster="yes"
|
||||
serverMultiChannel="no"
|
||||
USE_WSD="yes"
|
||||
WSD_OPT=""
|
||||
WSD2_OPT=""
|
||||
USE_NTP="yes"
|
||||
NTP_SERVER1="time1.google.com"
|
||||
NTP_SERVER2="time2.google.com"
|
||||
NTP_SERVER3="time3.google.com"
|
||||
NTP_SERVER4="time4.google.com"
|
||||
DOMAIN_LOGIN="Administrator"
|
||||
DOMAIN_PASSWD=""
|
||||
SYS_MODEL="Custom"
|
||||
SYS_ARRAY_SLOTS="24"
|
||||
USE_SSL="yes"
|
||||
PORT="80"
|
||||
PORTSSL="8443"
|
||||
LOCAL_TLD="local"
|
||||
BIND_MGT="no"
|
||||
USE_TELNET="no"
|
||||
PORTTELNET="23"
|
||||
USE_SSH="yes"
|
||||
PORTSSH="22"
|
||||
USE_UPNP="yes"
|
||||
START_PAGE="Main"
|
||||
@@ -6,6 +6,6 @@
|
||||
"name": "Connect",
|
||||
"permissions": [],
|
||||
"roles": [
|
||||
"connect"
|
||||
"CONNECT"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="4.4.1"
|
||||
version="4.7.0"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox="yes"
|
||||
|
||||
@@ -102,6 +102,7 @@ regTm="1833409182"
|
||||
regTm2="0"
|
||||
regExp=""
|
||||
regGen="0"
|
||||
regState="ENOKEYFILE"
|
||||
sbName="/boot/config/super.dat"
|
||||
sbVersion="2.9.13"
|
||||
sbUpdated="1596079143"
|
||||
|
||||
1
api/dev/webGui/images/UN-logotype-gradient.svg
Normal file
1
api/dev/webGui/images/UN-logotype-gradient.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 222.36 39.04"><defs><linearGradient id="header-logo" x1="47.53" y1="79.1" x2="170.71" y2="-44.08" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e32929"/><stop offset="1" stop-color="#ff8d30"/></linearGradient></defs><title>unraid.net</title><path d="M146.7,29.47H135l-3,9h-6.49L138.93,0h8l13.41,38.49h-7.09L142.62,6.93l-5.83,16.88h8ZM29.69,0V25.4c0,8.91-5.77,13.64-14.9,13.64S0,34.31,0,25.4V0H6.54V25.4c0,5.17,3.19,7.92,8.25,7.92s8.36-2.75,8.36-7.92V0ZM50.86,12v26.5H44.31V0h6.11l17,26.5V0H74V38.49H67.9ZM171.29,0h6.54V38.49h-6.54Zm51.07,24.69c0,9-5.88,13.8-15.17,13.8H192.67V0H207.3c9.18,0,15.06,4.78,15.06,13.8ZM215.82,13.8c0-5.28-3.3-8.14-8.52-8.14h-8.08V32.77h8c5.33,0,8.63-2.8,8.63-8.08ZM108.31,23.92c4.34-1.6,6.93-5.28,6.93-11.55C115.24,3.68,110.18,0,102.48,0H88.84V38.49h6.55V5.66h6.87c3.8,0,6.21,1.82,6.21,6.71s-2.41,6.76-6.21,6.76H98.88l9.21,19.36h7.53Z" fill="url(#header-logo)"/></svg>
|
||||
|
After Width: | Height: | Size: 1008 B |
31
api/docs/developer/api-plugins.md
Normal file
31
api/docs/developer/api-plugins.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Working with API plugins
|
||||
|
||||
Under the hood, API plugins (i.e. plugins to the `@unraid/api` project) are represented
|
||||
as npm `peerDependencies`. This is npm's intended package plugin mechanism, and given that
|
||||
peer dependencies are installed by default as of npm v7, it supports bi-directional plugin functionality,
|
||||
where the API provides dependencies for the plugin while the plugin provides functionality to the API.
|
||||
|
||||
## Private Workspace plugins
|
||||
|
||||
### Adding a local workspace package as an API plugin
|
||||
|
||||
The challenge with local workspace plugins is that they aren't available via npm during production.
|
||||
To solve this, we vendor them inside `dist/plugins`. To prevent the build from breaking, however,
|
||||
you should mark the workspace dependency as optional. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"peerDependencies": {
|
||||
"unraid-api-plugin-connect": "workspace:*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"unraid-api-plugin-connect": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
By marking the workspace dependency "optional", npm will not attempt to install it.
|
||||
Thus, even though the "workspace:*" identifier will be invalid during build-time and run-time,
|
||||
it will not cause problems.
|
||||
@@ -11,7 +11,7 @@
|
||||
"max_restarts": 10,
|
||||
"min_uptime": 10000,
|
||||
"watch": false,
|
||||
"interpreter": "/usr/local/node/bin/node",
|
||||
"interpreter": "/usr/local/bin/node",
|
||||
"ignore_watch": ["node_modules", "src", ".env.*", "myservers.cfg"],
|
||||
"log_file": "/var/log/graphql-api.log",
|
||||
"kill_timeout": 10000
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* This function wraps constant case, that turns any string into CONSTANT_CASE
|
||||
* However, this function has a bug that, if you pass _ to it it will return an empty
|
||||
* string. This small module fixes that
|
||||
*
|
||||
* @param {string*} str
|
||||
* @return {string}
|
||||
*/
|
||||
function FixArrayType(str) {
|
||||
if (str === 'Array') {
|
||||
return 'ArrayType';
|
||||
}
|
||||
|
||||
// If result is an empty string, just return the original string
|
||||
return str;
|
||||
}
|
||||
|
||||
module.exports = FixArrayType;
|
||||
1563
api/generated-schema-new.graphql
Normal file
1563
api/generated-schema-new.graphql
Normal file
File diff suppressed because it is too large
Load Diff
1679
api/generated-schema.graphql
Normal file
1679
api/generated-schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,3 +14,7 @@ default:
|
||||
|
||||
alias b := build
|
||||
alias d := deploy
|
||||
|
||||
sync-env server:
|
||||
rsync -avz --progress --stats -e ssh .env* root@{{server}}:/usr/local/unraid-api
|
||||
ssh root@{{server}} 'cp /usr/local/unraid-api/.env.staging /usr/local/unraid-api/.env'
|
||||
|
||||
3
api/legacy/README.md
Normal file
3
api/legacy/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Legacy Assets
|
||||
|
||||
This folder will store legacy types / functionality that may be useful but is not currently a part of the API
|
||||
1365
api/legacy/generated-schema-legacy.graphql
Normal file
1365
api/legacy/generated-schema-legacy.graphql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/api",
|
||||
"version": "4.6.1",
|
||||
"version": "4.8.0",
|
||||
"main": "src/cli/index.ts",
|
||||
"type": "module",
|
||||
"corepack": {
|
||||
@@ -20,7 +20,7 @@
|
||||
"command:raw": "./dist/cli.js",
|
||||
"// Build and Deploy": "",
|
||||
"build": "vite build --mode=production",
|
||||
"postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js",
|
||||
"postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js && node scripts/copy-plugins.js",
|
||||
"build:watch": "nodemon --watch src --ext ts,js,json --exec 'tsx ./scripts/build.ts'",
|
||||
"build:docker": "./scripts/dc.sh run --rm builder",
|
||||
"build:release": "tsx ./scripts/build.ts",
|
||||
@@ -43,7 +43,9 @@
|
||||
"container:start": "pnpm run container:stop && ./scripts/dc.sh run --rm --service-ports dev",
|
||||
"container:stop": "./scripts/dc.sh stop dev",
|
||||
"container:test": "./scripts/dc.sh run --rm builder pnpm run test",
|
||||
"container:enter": "./scripts/dc.sh exec dev /bin/bash"
|
||||
"container:enter": "./scripts/dc.sh exec dev /bin/bash",
|
||||
"// Migration Scripts": "",
|
||||
"migration:codefirst": "tsx ./src/unraid-api/graph/migration-script.ts"
|
||||
},
|
||||
"bin": {
|
||||
"unraid-api": "dist/cli.js"
|
||||
@@ -61,7 +63,9 @@
|
||||
"@graphql-tools/utils": "^10.5.5",
|
||||
"@jsonforms/core": "^3.5.1",
|
||||
"@nestjs/apollo": "^13.0.3",
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.11",
|
||||
"@nestjs/graphql": "^13.0.3",
|
||||
"@nestjs/passport": "^11.0.0",
|
||||
@@ -71,15 +75,18 @@
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"@runonflux/nat-upnp": "^1.0.2",
|
||||
"@types/diff": "^7.0.1",
|
||||
"@unraid/libvirt": "^1.1.3",
|
||||
"@unraid/libvirt": "^2.1.0",
|
||||
"accesscontrol": "^2.2.1",
|
||||
"bycontract": "^2.0.11",
|
||||
"bytes": "^3.1.2",
|
||||
"cache-manager": "^6.4.2",
|
||||
"cacheable-lookup": "^7.0.0",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"casbin": "^5.32.0",
|
||||
"change-case": "^5.4.4",
|
||||
"chokidar": "^4.0.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cli-table": "^0.3.11",
|
||||
"command-exists": "^1.2.9",
|
||||
"convert": "^5.8.0",
|
||||
@@ -87,8 +94,7 @@
|
||||
"cron": "3.5.0",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"docker-event-emitter": "^0.3.0",
|
||||
"dockerode": "^3.3.5",
|
||||
"dockerode": "^4.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"execa": "^9.5.1",
|
||||
"exit-hook": "^4.0.0",
|
||||
@@ -136,6 +142,14 @@
|
||||
"zen-observable-ts": "^1.1.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"unraid-api-plugin-connect": "workspace:*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"unraid-api-plugin-connect": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@graphql-codegen/add": "^5.0.3",
|
||||
@@ -163,6 +177,7 @@
|
||||
"@types/ini": "^4.1.1",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/pify": "^6.0.0",
|
||||
@@ -193,7 +208,7 @@
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"unplugin-swc": "^1.5.1",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-node": "^4.0.0",
|
||||
"vite-plugin-node": "^5.0.0",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"vitest": "^3.0.5",
|
||||
"zx": "^8.3.2"
|
||||
@@ -201,8 +216,15 @@
|
||||
"overrides": {
|
||||
"eslint": {
|
||||
"jiti": "2"
|
||||
},
|
||||
"@as-integrations/fastify": {
|
||||
"fastify": "$fastify"
|
||||
},
|
||||
"nest-authz": {
|
||||
"@nestjs/common": "$@nestjs/common",
|
||||
"@nestjs/core": "$@nestjs/core"
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.7.1"
|
||||
"packageManager": "pnpm@10.8.1"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#!/usr/bin/env zx
|
||||
import { mkdir, readFile, rm, writeFile } from 'fs/promises';
|
||||
import { mkdir, readFile, writeFile } from 'fs/promises';
|
||||
import { exit } from 'process';
|
||||
|
||||
import type { PackageJson } from 'type-fest';
|
||||
import { $, cd } from 'zx';
|
||||
|
||||
import { getDeploymentVersion } from './get-deployment-version.js';
|
||||
|
||||
type ApiPackageJson = PackageJson & {
|
||||
version: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
};
|
||||
|
||||
try {
|
||||
// Create release and pack directories
|
||||
await mkdir('./deploy/release', { recursive: true });
|
||||
@@ -19,13 +25,12 @@ try {
|
||||
|
||||
// Get package details
|
||||
const packageJson = await readFile('./package.json', 'utf-8');
|
||||
const parsedPackageJson = JSON.parse(packageJson);
|
||||
|
||||
const parsedPackageJson = JSON.parse(packageJson) as ApiPackageJson;
|
||||
const deploymentVersion = await getDeploymentVersion(process.env, parsedPackageJson.version);
|
||||
|
||||
// Update the package.json version to the deployment version
|
||||
parsedPackageJson.version = deploymentVersion;
|
||||
// omit dev dependencies from release build
|
||||
// omit dev dependencies from vendored dependencies in release build
|
||||
parsedPackageJson.devDependencies = {};
|
||||
|
||||
// Create a temporary directory for packaging
|
||||
@@ -38,18 +43,19 @@ try {
|
||||
// Change to the pack directory and install dependencies
|
||||
cd('./deploy/pack');
|
||||
|
||||
console.log('Building production pnpm store...');
|
||||
console.log('Building production node_modules...');
|
||||
$.verbose = true;
|
||||
await $`pnpm install --prod --ignore-workspace --store-dir=../.pnpm-store`;
|
||||
await $`npm install --omit=dev`;
|
||||
|
||||
await writeFile('package.json', JSON.stringify(parsedPackageJson, null, 4));
|
||||
|
||||
await $`rm -rf node_modules`; // Don't include node_modules in final package
|
||||
|
||||
const sudoCheck = await $`command -v sudo`.nothrow();
|
||||
const SUDO = sudoCheck.exitCode === 0 ? 'sudo' : '';
|
||||
await $`${SUDO} chown -R 0:0 ../.pnpm-store`;
|
||||
await $`${SUDO} chown -R 0:0 node_modules`;
|
||||
|
||||
await $`XZ_OPT=-5 tar -cJf ../packed-pnpm-store.txz ../.pnpm-store`;
|
||||
await $`${SUDO} rm -rf ../.pnpm-store`;
|
||||
await $`XZ_OPT=-5 tar -cJf packed-node-modules.tar.xz node_modules`;
|
||||
await $`mv packed-node-modules.tar.xz ../`;
|
||||
await $`${SUDO} rm -rf node_modules`;
|
||||
|
||||
// chmod the cli
|
||||
await $`chmod +x ./dist/cli.js`;
|
||||
|
||||
59
api/scripts/copy-plugins.js
Normal file
59
api/scripts/copy-plugins.js
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* This AI-generated script copies workspace plugin dist folders to the dist/plugins directory
|
||||
* to ensure they're available for dynamic imports in production.
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Get the package.json to find workspace dependencies
|
||||
const packageJsonPath = path.resolve(__dirname, '../package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
// Create the plugins directory if it doesn't exist
|
||||
const pluginsDir = path.resolve(__dirname, '../dist/plugins');
|
||||
if (!fs.existsSync(pluginsDir)) {
|
||||
fs.mkdirSync(pluginsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Find all workspace plugins
|
||||
const pluginPrefix = 'unraid-api-plugin-';
|
||||
const workspacePlugins = Object.keys(packageJson.peerDependencies || {}).filter((pkgName) =>
|
||||
pkgName.startsWith(pluginPrefix)
|
||||
);
|
||||
|
||||
// Copy each plugin's dist folder to the plugins directory
|
||||
for (const pkgName of workspacePlugins) {
|
||||
const pluginPath = path.resolve(__dirname, `../../packages/${pkgName}`);
|
||||
const pluginDistPath = path.resolve(pluginPath, 'dist');
|
||||
const targetPath = path.resolve(pluginsDir, pkgName);
|
||||
|
||||
console.log(`Building ${pkgName}...`);
|
||||
try {
|
||||
execSync('pnpm build', {
|
||||
cwd: pluginPath,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
console.log(`Successfully built ${pkgName}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to build ${pkgName}:`, error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pluginDistPath)) {
|
||||
console.warn(`Plugin ${pkgName} dist folder not found at ${pluginDistPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Copying ${pkgName} dist folder to ${targetPath}`);
|
||||
fs.mkdirSync(targetPath, { recursive: true });
|
||||
fs.cpSync(pluginDistPath, targetPath, { recursive: true });
|
||||
console.log(`Successfully copied ${pkgName} dist folder`);
|
||||
}
|
||||
|
||||
console.log('Plugin dist folders copied successfully');
|
||||
@@ -29,7 +29,7 @@ fi
|
||||
destination_directory="/usr/local/unraid-api"
|
||||
|
||||
# Replace the value inside the rsync command with the user's input
|
||||
rsync_command="rsync -avz --progress --stats -e ssh \"$source_directory\" \"root@${server_name}:$destination_directory\""
|
||||
rsync_command="rsync -avz --delete --progress --stats -e ssh \"$source_directory\" \"root@${server_name}:$destination_directory\""
|
||||
|
||||
echo "Executing the following command:"
|
||||
echo "$rsync_command"
|
||||
|
||||
@@ -9,36 +9,37 @@ import { expect, test } from 'vitest';
|
||||
|
||||
test('Returns allowed origins', async () => {
|
||||
// Load state files into store
|
||||
await store.dispatch(loadStateFiles());
|
||||
await store.dispatch(loadConfigFile());
|
||||
await store.dispatch(loadStateFiles()).unwrap();
|
||||
await store.dispatch(loadConfigFile()).unwrap();
|
||||
|
||||
// Get allowed origins
|
||||
expect(getAllowedOrigins()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"/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://10-100-0-1.hash.myunraid.net:4443",
|
||||
"https://10-100-0-2.hash.myunraid.net:4443",
|
||||
"https://10-123-1-2.hash.myunraid.net:4443",
|
||||
"https://221-123-121-112.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",
|
||||
]
|
||||
`);
|
||||
const allowedOrigins = getAllowedOrigins();
|
||||
|
||||
// Test that the result is an array
|
||||
expect(Array.isArray(allowedOrigins)).toBe(true);
|
||||
|
||||
// Test that it contains the expected socket paths
|
||||
expect(allowedOrigins).toContain('/var/run/unraid-notifications.sock');
|
||||
expect(allowedOrigins).toContain('/var/run/unraid-php.sock');
|
||||
expect(allowedOrigins).toContain('/var/run/unraid-cli.sock');
|
||||
|
||||
// Test that it contains the expected local URLs
|
||||
expect(allowedOrigins).toContain('http://localhost:8080');
|
||||
expect(allowedOrigins).toContain('https://localhost:4443');
|
||||
|
||||
// Test that it contains the expected connect URLs
|
||||
expect(allowedOrigins).toContain('https://connect.myunraid.net');
|
||||
expect(allowedOrigins).toContain('https://connect-staging.myunraid.net');
|
||||
expect(allowedOrigins).toContain('https://dev-my.myunraid.net:4000');
|
||||
|
||||
// Test that it contains the extra origins from config
|
||||
expect(allowedOrigins).toContain('https://google.com');
|
||||
expect(allowedOrigins).toContain('https://test.com');
|
||||
|
||||
// Test that it contains some of the remote URLs
|
||||
expect(allowedOrigins).toContain('https://tower.local:4443');
|
||||
expect(allowedOrigins).toContain('https://192.168.1.150:4443');
|
||||
|
||||
// Test that there are no duplicates
|
||||
expect(allowedOrigins.length).toBe(new Set(allowedOrigins).size);
|
||||
});
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Adds a disk to the array');
|
||||
|
||||
test.todo('Fails to add the disk if the array is started');
|
||||
@@ -1,209 +0,0 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import { loadConfigFile } from '@app/store/modules/config.js';
|
||||
import { loadStateFiles } from '@app/store/modules/emhttp.js';
|
||||
|
||||
vi.mock('@app/core/pubsub.js', () => ({
|
||||
pubsub: { publish: vi.fn() },
|
||||
}));
|
||||
|
||||
test('Creates an array event', async () => {
|
||||
// Load state files into store
|
||||
await store.dispatch(loadStateFiles());
|
||||
|
||||
await store.dispatch(loadConfigFile());
|
||||
|
||||
const arrayEvent = getArrayData(store.getState);
|
||||
expect(arrayEvent).toMatchObject({
|
||||
boot: {
|
||||
comment: 'Unraid OS boot device',
|
||||
critical: null,
|
||||
device: 'sda',
|
||||
exportable: true,
|
||||
format: 'unknown',
|
||||
fsFree: 3191407,
|
||||
fsSize: 4042732,
|
||||
fsType: 'vfat',
|
||||
fsUsed: 851325,
|
||||
id: 'Cruzer',
|
||||
idx: 32,
|
||||
name: 'flash',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 3956700,
|
||||
status: 'DISK_OK',
|
||||
temp: null,
|
||||
transport: 'usb',
|
||||
type: 'Flash',
|
||||
warning: null,
|
||||
},
|
||||
caches: [
|
||||
{
|
||||
comment: '',
|
||||
critical: null,
|
||||
device: 'sdi',
|
||||
exportable: false,
|
||||
format: 'MBR: 4KiB-aligned',
|
||||
fsFree: 111810683,
|
||||
fsSize: 250059317,
|
||||
fsType: 'btrfs',
|
||||
fsUsed: 137273827,
|
||||
id: 'Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z',
|
||||
idx: 30,
|
||||
name: 'cache',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: false,
|
||||
size: 244198552,
|
||||
status: 'DISK_OK',
|
||||
temp: 22,
|
||||
transport: 'ata',
|
||||
type: 'Cache',
|
||||
warning: null,
|
||||
},
|
||||
{
|
||||
comment: null,
|
||||
critical: null,
|
||||
device: 'nvme0n1',
|
||||
exportable: false,
|
||||
format: 'MBR: 4KiB-aligned',
|
||||
fsFree: null,
|
||||
fsSize: null,
|
||||
fsType: null,
|
||||
fsUsed: null,
|
||||
id: 'KINGSTON_SA2000M8250G_50026B7282669D9E',
|
||||
idx: 31,
|
||||
name: 'cache2',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: false,
|
||||
size: 244198552,
|
||||
status: 'DISK_OK',
|
||||
temp: 27,
|
||||
transport: 'nvme',
|
||||
type: 'Cache',
|
||||
warning: null,
|
||||
},
|
||||
],
|
||||
capacity: {
|
||||
disks: {
|
||||
free: '27',
|
||||
total: '30',
|
||||
used: '3',
|
||||
},
|
||||
kilobytes: {
|
||||
free: '19495825571',
|
||||
total: '41994745901',
|
||||
used: '22498920330',
|
||||
},
|
||||
},
|
||||
disks: [
|
||||
{
|
||||
comment: 'Seagate Exos',
|
||||
critical: 75,
|
||||
device: 'sdf',
|
||||
exportable: false,
|
||||
format: 'GPT: 4KiB-aligned',
|
||||
fsFree: 13882739732,
|
||||
fsSize: 17998742753,
|
||||
fsType: 'xfs',
|
||||
fsUsed: 4116003021,
|
||||
id: 'ST18000NM000J-2TV103_ZR5B1W9X',
|
||||
idx: 1,
|
||||
name: 'disk1',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 17578328012,
|
||||
status: 'DISK_OK',
|
||||
temp: 30,
|
||||
transport: 'ata',
|
||||
type: 'Data',
|
||||
warning: 50,
|
||||
},
|
||||
{
|
||||
comment: '',
|
||||
critical: null,
|
||||
device: 'sdj',
|
||||
exportable: false,
|
||||
format: 'GPT: 4KiB-aligned',
|
||||
fsFree: 93140746,
|
||||
fsSize: 11998001574,
|
||||
fsType: 'xfs',
|
||||
fsUsed: 11904860828,
|
||||
id: 'WDC_WD120EDAZ-11F3RA0_5PJRD45C',
|
||||
idx: 2,
|
||||
name: 'disk2',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 11718885324,
|
||||
status: 'DISK_OK',
|
||||
temp: 30,
|
||||
transport: 'ata',
|
||||
type: 'Data',
|
||||
warning: null,
|
||||
},
|
||||
{
|
||||
comment: '',
|
||||
critical: null,
|
||||
device: 'sde',
|
||||
exportable: false,
|
||||
format: 'GPT: 4KiB-aligned',
|
||||
fsFree: 5519945093,
|
||||
fsSize: 11998001574,
|
||||
fsType: 'xfs',
|
||||
fsUsed: 6478056481,
|
||||
id: 'WDC_WD120EMAZ-11BLFA0_5PH8BTYD',
|
||||
idx: 3,
|
||||
name: 'disk3',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 11718885324,
|
||||
status: 'DISK_OK',
|
||||
temp: 30,
|
||||
transport: 'ata',
|
||||
type: 'Data',
|
||||
warning: null,
|
||||
},
|
||||
],
|
||||
id: expect.any(String),
|
||||
parities: [
|
||||
{
|
||||
comment: null,
|
||||
critical: null,
|
||||
device: 'sdh',
|
||||
exportable: false,
|
||||
format: 'GPT: 4KiB-aligned',
|
||||
fsFree: null,
|
||||
fsSize: null,
|
||||
fsType: null,
|
||||
fsUsed: null,
|
||||
id: 'ST18000NM000J-2TV103_ZR585CPY',
|
||||
idx: 0,
|
||||
name: 'parity',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 17578328012,
|
||||
status: 'DISK_OK',
|
||||
temp: 25,
|
||||
transport: 'ata',
|
||||
type: 'Parity',
|
||||
warning: null,
|
||||
},
|
||||
],
|
||||
state: 'STOPPED',
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Removes a disk from the array');
|
||||
|
||||
test.todo('Fails to remove the disk if the array is started');
|
||||
@@ -1,5 +0,0 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Starts the array');
|
||||
|
||||
test.todo('Stops the array');
|
||||
@@ -1,7 +0,0 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Can start a parity check');
|
||||
|
||||
test.todo('Can pause a parity check');
|
||||
|
||||
test.todo('Can start a parity check');
|
||||
@@ -80,7 +80,6 @@ test('it creates a FLASH config with OPTIONAL values', () => {
|
||||
// 2fa & t2fa should be ignored
|
||||
basicConfig.remote['2Fa'] = 'yes';
|
||||
basicConfig.local['2Fa'] = 'yes';
|
||||
basicConfig.local.showT2Fa = 'yes';
|
||||
|
||||
basicConfig.api.extraOrigins = 'myextra.origins';
|
||||
basicConfig.remote.upnpEnabled = 'yes';
|
||||
@@ -120,7 +119,6 @@ test('it creates a MEMORY config with OPTIONAL values', () => {
|
||||
// 2fa & t2fa should be ignored
|
||||
basicConfig.remote['2Fa'] = 'yes';
|
||||
basicConfig.local['2Fa'] = 'yes';
|
||||
basicConfig.local.showT2Fa = 'yes';
|
||||
basicConfig.api.extraOrigins = 'myextra.origins';
|
||||
basicConfig.remote.upnpEnabled = 'yes';
|
||||
basicConfig.connectionStatus.upnpStatus = 'Turned On';
|
||||
|
||||
@@ -44,7 +44,7 @@ test('Returns empty key if key location is empty', async () => {
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { status } = store.getState().registration;
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
expect(status).toBe(FileLoadStatus.UNLOADED);
|
||||
await expect(getKeyFile()).resolves.toBe('');
|
||||
});
|
||||
|
||||
@@ -53,10 +53,10 @@ test(
|
||||
async () => {
|
||||
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file.js');
|
||||
const { loadStateFiles } = await import('@app/store/modules/emhttp.js');
|
||||
|
||||
const { loadRegistrationKey } = await import('@app/store/modules/registration.js');
|
||||
// Load state files into store
|
||||
await store.dispatch(loadStateFiles());
|
||||
|
||||
await store.dispatch(loadRegistrationKey());
|
||||
// Check if store has state files loaded
|
||||
const { status } = store.getState().registration;
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
|
||||
@@ -8,229 +8,240 @@ test('Returns both disk and user shares', async () => {
|
||||
await store.dispatch(loadStateFiles());
|
||||
|
||||
expect(getShares()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"disks": [],
|
||||
"users": [
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "ISO images",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "isos",
|
||||
"nameOrig": "isos",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "system data",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "system",
|
||||
"nameOrig": "system",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
{
|
||||
"disks": [],
|
||||
"users": [
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "appdata",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "domains",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "ISO images",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "isos",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "isos",
|
||||
"nameOrig": "isos",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "system data",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "system",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "system",
|
||||
"nameOrig": "system",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('Returns shares by type', async () => {
|
||||
await store.dispatch(loadStateFiles());
|
||||
expect(getShares('user')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
}
|
||||
`);
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "appdata",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
}
|
||||
`);
|
||||
expect(getShares('users')).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "ISO images",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "isos",
|
||||
"nameOrig": "isos",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "system data",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "system",
|
||||
"nameOrig": "system",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
]
|
||||
`);
|
||||
[
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "appdata",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "domains",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "ISO images",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "isos",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "isos",
|
||||
"nameOrig": "isos",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "system data",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "system",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "system",
|
||||
"nameOrig": "system",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(getShares('disk')).toMatchInlineSnapshot('null');
|
||||
expect(getShares('disks')).toMatchInlineSnapshot('[]');
|
||||
});
|
||||
|
||||
test('Returns shares by name', async () => {
|
||||
await store.dispatch(loadStateFiles());
|
||||
expect(getShares('user', { name: 'domains' })).toMatchInlineSnapshot(`
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
}
|
||||
`);
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "domains",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"nfs": {},
|
||||
"size": 0,
|
||||
"smb": {},
|
||||
"splitLevel": "1",
|
||||
"type": "user",
|
||||
"used": 33619300,
|
||||
}
|
||||
`);
|
||||
expect(getShares('user', { name: 'non-existent-user-share' })).toMatchInlineSnapshot('null');
|
||||
// @TODO: disk shares need to be added to the dev ini files
|
||||
expect(getShares('disk', { name: 'disk1' })).toMatchInlineSnapshot('null');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { NginxUrlFields } from '@app/graphql/resolvers/subscription/network.js';
|
||||
import { type Nginx } from '@app/core/types/states/nginx.js';
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { store } from '@app/store/index.js';
|
||||
import { loadConfigFile } from '@app/store/modules/config.js';
|
||||
import { loadStateFiles } from '@app/store/modules/emhttp.js';
|
||||
import { URL_TYPE } from '@app/unraid-api/graph/resolvers/connect/connect.model.js';
|
||||
|
||||
test.each([
|
||||
[{ httpPort: 80, httpsPort: 443, url: 'my-default-url.com' }],
|
||||
@@ -190,90 +191,37 @@ test('integration test, loading nginx ini and generating all URLs', async () =>
|
||||
await store.dispatch(loadStateFiles());
|
||||
await store.dispatch(loadConfigFile());
|
||||
|
||||
// Instead of mocking the getServerIps function, we'll use the actual function
|
||||
// and verify the structure of the returned URLs
|
||||
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": "FQDN LAN",
|
||||
"type": "LAN",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443/",
|
||||
"name": "FQDN WAN",
|
||||
"type": "WAN",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-252-0-1.hash.myunraid.net:4443/",
|
||||
"name": "FQDN WG 0",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-252-1-1.hash.myunraid.net:4443/",
|
||||
"name": "FQDN WG 1",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-253-3-1.hash.myunraid.net:4443/",
|
||||
"name": "FQDN WG 2",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-253-4-1.hash.myunraid.net:4443/",
|
||||
"name": "FQDN WG 3",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-253-5-1.hash.myunraid.net:4443/",
|
||||
"name": "FQDN WG 4",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-100-0-1.hash.myunraid.net:4443/",
|
||||
"name": "FQDN TAILSCALE 0",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-100-0-2.hash.myunraid.net:4443/",
|
||||
"name": "FQDN TAILSCALE 1",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-123-1-2.hash.myunraid.net:4443/",
|
||||
"name": "FQDN CUSTOM 0",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://221-123-121-112.hash.myunraid.net:4443/",
|
||||
"name": "FQDN CUSTOM 1",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(urls.errors).toMatchInlineSnapshot(`
|
||||
[
|
||||
[Error: IP URL Resolver: Could not resolve any access URL for field: "lanIp6", is FQDN?: false],
|
||||
]
|
||||
`);
|
||||
|
||||
// Verify that we have URLs
|
||||
expect(urls.urls.length).toBeGreaterThan(0);
|
||||
expect(urls.errors.length).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Verify that each URL has the expected structure
|
||||
urls.urls.forEach((url) => {
|
||||
expect(url).toHaveProperty('ipv4');
|
||||
expect(url).toHaveProperty('name');
|
||||
expect(url).toHaveProperty('type');
|
||||
|
||||
// Verify that the URL matches the expected pattern based on its type
|
||||
if (url.type === URL_TYPE.DEFAULT) {
|
||||
expect(url.ipv4?.toString()).toMatch(/^https:\/\/.*:\d+\/$/);
|
||||
expect(url.ipv6?.toString()).toMatch(/^https:\/\/.*:\d+\/$/);
|
||||
} else if (url.type === URL_TYPE.LAN) {
|
||||
expect(url.ipv4?.toString()).toMatch(/^https:\/\/.*:\d+\/$/);
|
||||
} else if (url.type === URL_TYPE.MDNS) {
|
||||
expect(url.ipv4?.toString()).toMatch(/^https:\/\/.*:\d+\/$/);
|
||||
} else if (url.type === URL_TYPE.WIREGUARD) {
|
||||
expect(url.ipv4?.toString()).toMatch(/^https:\/\/.*:\d+\/$/);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that the error message contains the expected text
|
||||
if (urls.errors.length > 0) {
|
||||
expect(urls.errors[0].message).toContain(
|
||||
'IP URL Resolver: Could not resolve any access URL for field:'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
8
api/src/__test__/setup.ts
Normal file
8
api/src/__test__/setup.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import '@app/__test__/setup/env-setup.js';
|
||||
// import './setup/mock-fs-setup';
|
||||
import '@app/__test__/setup/keyserver-mock.js';
|
||||
import '@app/__test__/setup/config-setup.js';
|
||||
import '@app/__test__/setup/store-reset.js';
|
||||
|
||||
// This file is automatically loaded by Vitest before running tests
|
||||
// It imports all the setup files that need to be run before tests
|
||||
17
api/src/__test__/setup/config-setup.ts
Normal file
17
api/src/__test__/setup/config-setup.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { copyFileSync, existsSync } from 'fs';
|
||||
import { join, resolve } from 'path';
|
||||
|
||||
// Get the project root directory
|
||||
const projectRoot = resolve(process.cwd());
|
||||
|
||||
// Define paths
|
||||
const sourceFile = join(projectRoot, 'dev/Unraid.net/myservers.example.cfg');
|
||||
const destFile = join(projectRoot, 'dev/Unraid.net/myservers.cfg');
|
||||
|
||||
// Ensure the example file exists
|
||||
if (!existsSync(sourceFile)) {
|
||||
console.error('Error: myservers.example.cfg not found!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
copyFileSync(sourceFile, destFile);
|
||||
45
api/src/__test__/setup/mock-fs-setup.ts
Normal file
45
api/src/__test__/setup/mock-fs-setup.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { beforeEach, vi } from 'vitest';
|
||||
|
||||
// Create a global mock file system that can be used across all tests
|
||||
export const mockFileSystem = new Map<string, string>();
|
||||
|
||||
// Mock fs/promises
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
writeFile: vi.fn().mockImplementation((path, content) => {
|
||||
mockFileSystem.set(path.toString(), content.toString());
|
||||
return Promise.resolve();
|
||||
}),
|
||||
readFile: vi.fn().mockImplementation((path) => {
|
||||
const content = mockFileSystem.get(path.toString());
|
||||
if (content === undefined) {
|
||||
return Promise.reject(new Error(`File not found: ${path}`));
|
||||
}
|
||||
return Promise.resolve(content);
|
||||
}),
|
||||
access: vi.fn().mockImplementation((path) => {
|
||||
if (mockFileSystem.has(path.toString())) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(`File not found: ${path}`));
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock fs-extra
|
||||
vi.mock('fs-extra', () => ({
|
||||
emptyDir: vi.fn().mockImplementation(() => {
|
||||
mockFileSystem.clear();
|
||||
return Promise.resolve();
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock file-exists utility
|
||||
vi.mock('@app/core/utils/files/file-exists.js', () => ({
|
||||
fileExists: vi.fn().mockImplementation((path) => {
|
||||
return Promise.resolve(mockFileSystem.has(path.toString()));
|
||||
}),
|
||||
}));
|
||||
|
||||
// Clear the mock file system before each test
|
||||
beforeEach(() => {
|
||||
mockFileSystem.clear();
|
||||
});
|
||||
8
api/src/__test__/setup/store-reset.ts
Normal file
8
api/src/__test__/setup/store-reset.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { beforeEach } from 'vitest';
|
||||
|
||||
import { resetStore } from '@app/store/actions/reset-store.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
|
||||
beforeEach(() => {
|
||||
store.dispatch(resetStore());
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Returns paths 1`] = `
|
||||
[
|
||||
"core",
|
||||
"unraid-api-base",
|
||||
"unraid-data",
|
||||
"docker-autostart",
|
||||
"docker-socket",
|
||||
"parity-checks",
|
||||
"htpasswd",
|
||||
"emhttpd-socket",
|
||||
"states",
|
||||
"dynamix-base",
|
||||
"dynamix-config",
|
||||
"myservers-base",
|
||||
"myservers-config",
|
||||
"myservers-config-states",
|
||||
"myservers-env",
|
||||
"myservers-keepalive",
|
||||
"keyfile-base",
|
||||
"machine-id",
|
||||
"log-base",
|
||||
"unraid-log-base",
|
||||
"var-run",
|
||||
"auth-sessions",
|
||||
"auth-keys",
|
||||
"passwd",
|
||||
"libvirt-pid",
|
||||
"activationBase",
|
||||
"webGuiBase",
|
||||
"identConfig",
|
||||
"activation",
|
||||
"boot",
|
||||
"webgui",
|
||||
]
|
||||
`;
|
||||
@@ -1,7 +1,99 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import { GraphQLClient } from '@app/mothership/graphql-client.js';
|
||||
import { stopPingTimeoutJobs } from '@app/mothership/jobs/ping-timeout-jobs.js';
|
||||
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js';
|
||||
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import { MyServersConfigMemory } from '@app/types/my-servers-config.js';
|
||||
import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js';
|
||||
import {
|
||||
WAN_ACCESS_TYPE,
|
||||
WAN_FORWARD_TYPE,
|
||||
} from '@app/unraid-api/graph/resolvers/connect/connect.model.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@app/core/pubsub.js', () => {
|
||||
const mockPublish = vi.fn();
|
||||
return {
|
||||
pubsub: {
|
||||
publish: mockPublish,
|
||||
},
|
||||
PUBSUB_CHANNEL: {
|
||||
OWNER: 'OWNER',
|
||||
SERVERS: 'SERVERS',
|
||||
},
|
||||
__esModule: true,
|
||||
default: {
|
||||
pubsub: {
|
||||
publish: mockPublish,
|
||||
},
|
||||
PUBSUB_CHANNEL: {
|
||||
OWNER: 'OWNER',
|
||||
SERVERS: 'SERVERS',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Get the mock function for pubsub.publish
|
||||
const mockPublish = vi.mocked(pubsub.publish);
|
||||
|
||||
// Clear mock before each test
|
||||
beforeEach(() => {
|
||||
mockPublish.mockClear();
|
||||
});
|
||||
|
||||
vi.mock('@app/mothership/graphql-client.js', () => ({
|
||||
GraphQLClient: {
|
||||
clearInstance: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@app/mothership/jobs/ping-timeout-jobs.js', () => ({
|
||||
stopPingTimeoutJobs: vi.fn(),
|
||||
}));
|
||||
|
||||
const createConfigMatcher = (specificValues: Partial<MyServersConfigMemory> = {}) => {
|
||||
const defaultMatcher = {
|
||||
api: expect.objectContaining({
|
||||
extraOrigins: expect.any(String),
|
||||
version: expect.any(String),
|
||||
}),
|
||||
connectionStatus: expect.objectContaining({
|
||||
minigraph: expect.any(String),
|
||||
upnpStatus: expect.any(String),
|
||||
}),
|
||||
local: expect.objectContaining({
|
||||
sandbox: expect.any(String),
|
||||
}),
|
||||
nodeEnv: expect.any(String),
|
||||
remote: expect.objectContaining({
|
||||
accesstoken: expect.any(String),
|
||||
allowedOrigins: expect.any(String),
|
||||
apikey: expect.any(String),
|
||||
avatar: expect.any(String),
|
||||
dynamicRemoteAccessType: expect.any(String),
|
||||
email: expect.any(String),
|
||||
idtoken: expect.any(String),
|
||||
localApiKey: expect.any(String),
|
||||
refreshtoken: expect.any(String),
|
||||
regWizTime: expect.any(String),
|
||||
ssoSubIds: expect.any(String),
|
||||
upnpEnabled: expect.any(String),
|
||||
username: expect.any(String),
|
||||
wanaccess: expect.any(String),
|
||||
wanport: expect.any(String),
|
||||
}),
|
||||
status: expect.any(String),
|
||||
};
|
||||
|
||||
return expect.objectContaining({
|
||||
...defaultMatcher,
|
||||
...specificValues,
|
||||
});
|
||||
};
|
||||
|
||||
test('Before init returns default values for all fields', async () => {
|
||||
const state = store.getState().config;
|
||||
@@ -16,40 +108,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
|
||||
// Check if store has cfg contents loaded
|
||||
const state = store.getState().config;
|
||||
expect(state).toMatchObject(
|
||||
expect.objectContaining({
|
||||
api: {
|
||||
extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
|
||||
version: expect.any(String),
|
||||
},
|
||||
connectionStatus: {
|
||||
minigraph: 'PRE_INIT',
|
||||
upnpStatus: '',
|
||||
},
|
||||
local: {
|
||||
sandbox: expect.any(String),
|
||||
},
|
||||
nodeEnv: 'test',
|
||||
remote: {
|
||||
accesstoken: '',
|
||||
allowedOrigins: '',
|
||||
apikey: '_______________________BIG_API_KEY_HERE_________________________',
|
||||
avatar: 'https://via.placeholder.com/200',
|
||||
dynamicRemoteAccessType: 'DISABLED',
|
||||
email: 'test@example.com',
|
||||
idtoken: '',
|
||||
localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________',
|
||||
refreshtoken: '',
|
||||
regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0',
|
||||
ssoSubIds: '',
|
||||
upnpEnabled: 'no',
|
||||
username: 'zspearmint',
|
||||
wanaccess: 'yes',
|
||||
wanport: '8443',
|
||||
},
|
||||
status: 'LOADED',
|
||||
})
|
||||
);
|
||||
expect(state).toMatchObject(createConfigMatcher());
|
||||
});
|
||||
|
||||
test('updateUserConfig merges in changes to current state', async () => {
|
||||
@@ -67,37 +126,185 @@ test('updateUserConfig merges in changes to current state', async () => {
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state).toMatchObject(
|
||||
expect.objectContaining({
|
||||
api: {
|
||||
extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
|
||||
version: expect.any(String),
|
||||
},
|
||||
connectionStatus: {
|
||||
minigraph: 'PRE_INIT',
|
||||
upnpStatus: '',
|
||||
},
|
||||
local: {
|
||||
sandbox: expect.any(String),
|
||||
},
|
||||
nodeEnv: 'test',
|
||||
remote: {
|
||||
accesstoken: '',
|
||||
allowedOrigins: '',
|
||||
apikey: '_______________________BIG_API_KEY_HERE_________________________',
|
||||
createConfigMatcher({
|
||||
remote: expect.objectContaining({
|
||||
avatar: 'https://via.placeholder.com/200',
|
||||
dynamicRemoteAccessType: 'DISABLED',
|
||||
email: 'test@example.com',
|
||||
idtoken: '',
|
||||
localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________',
|
||||
refreshtoken: '',
|
||||
regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0',
|
||||
ssoSubIds: '',
|
||||
upnpEnabled: 'no',
|
||||
username: 'zspearmint',
|
||||
wanaccess: 'yes',
|
||||
wanport: '8443',
|
||||
},
|
||||
status: 'LOADED',
|
||||
} as MyServersConfigMemory)
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('loginUser updates state and publishes to pubsub', async () => {
|
||||
const { loginUser } = await import('@app/store/modules/config.js');
|
||||
const userInfo = {
|
||||
email: 'test@example.com',
|
||||
avatar: 'https://via.placeholder.com/200',
|
||||
username: 'testuser',
|
||||
apikey: 'test-api-key',
|
||||
localApiKey: 'test-local-api-key',
|
||||
};
|
||||
|
||||
await store.dispatch(loginUser(userInfo));
|
||||
|
||||
expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.OWNER, {
|
||||
owner: {
|
||||
username: userInfo.username,
|
||||
url: '',
|
||||
avatar: userInfo.avatar,
|
||||
},
|
||||
});
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state).toMatchObject(
|
||||
createConfigMatcher({
|
||||
remote: expect.objectContaining(userInfo),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('logoutUser clears state and publishes to pubsub', async () => {
|
||||
const { logoutUser } = await import('@app/store/modules/config.js');
|
||||
|
||||
await store.dispatch(logoutUser({ reason: 'test logout' }));
|
||||
|
||||
expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.SERVERS, { servers: [] });
|
||||
expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.OWNER, {
|
||||
owner: {
|
||||
username: 'root',
|
||||
url: '',
|
||||
avatar: '',
|
||||
},
|
||||
});
|
||||
expect(stopPingTimeoutJobs).toHaveBeenCalled();
|
||||
expect(GraphQLClient.clearInstance).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('updateAccessTokens updates token fields', async () => {
|
||||
const { updateAccessTokens } = await import('@app/store/modules/config.js');
|
||||
const tokens = {
|
||||
accesstoken: 'new-access-token',
|
||||
refreshtoken: 'new-refresh-token',
|
||||
idtoken: 'new-id-token',
|
||||
};
|
||||
|
||||
store.dispatch(updateAccessTokens(tokens));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state).toMatchObject(
|
||||
createConfigMatcher({
|
||||
remote: expect.objectContaining(tokens),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('updateAllowedOrigins updates extraOrigins', async () => {
|
||||
const { updateAllowedOrigins } = await import('@app/store/modules/config.js');
|
||||
const origins = ['https://test1.com', 'https://test2.com'];
|
||||
|
||||
store.dispatch(updateAllowedOrigins(origins));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.api.extraOrigins).toBe(origins.join(', '));
|
||||
});
|
||||
|
||||
test('setUpnpState updates upnp settings', async () => {
|
||||
const { setUpnpState } = await import('@app/store/modules/config.js');
|
||||
|
||||
store.dispatch(setUpnpState({ enabled: 'yes', status: 'active' }));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.remote.upnpEnabled).toBe('yes');
|
||||
expect(state.connectionStatus.upnpStatus).toBe('active');
|
||||
});
|
||||
|
||||
test('setWanPortToValue updates wanport', async () => {
|
||||
const { setWanPortToValue } = await import('@app/store/modules/config.js');
|
||||
|
||||
store.dispatch(setWanPortToValue(8443));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.remote.wanport).toBe('8443');
|
||||
});
|
||||
|
||||
test('setWanAccess updates wanaccess', async () => {
|
||||
const { setWanAccess } = await import('@app/store/modules/config.js');
|
||||
|
||||
store.dispatch(setWanAccess('yes'));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.remote.wanaccess).toBe('yes');
|
||||
});
|
||||
|
||||
test('addSsoUser adds user to ssoSubIds', async () => {
|
||||
const { addSsoUser } = await import('@app/store/modules/config.js');
|
||||
|
||||
store.dispatch(addSsoUser('user1'));
|
||||
store.dispatch(addSsoUser('user2'));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.remote.ssoSubIds).toBe('user1,user2');
|
||||
});
|
||||
|
||||
test('removeSsoUser removes user from ssoSubIds', async () => {
|
||||
const { addSsoUser, removeSsoUser } = await import('@app/store/modules/config.js');
|
||||
|
||||
store.dispatch(addSsoUser('user1'));
|
||||
store.dispatch(addSsoUser('user2'));
|
||||
store.dispatch(removeSsoUser('user1'));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.remote.ssoSubIds).toBe('user2');
|
||||
});
|
||||
|
||||
test('removeSsoUser with null clears all ssoSubIds', async () => {
|
||||
const { addSsoUser, removeSsoUser } = await import('@app/store/modules/config.js');
|
||||
|
||||
store.dispatch(addSsoUser('user1'));
|
||||
store.dispatch(addSsoUser('user2'));
|
||||
store.dispatch(removeSsoUser(null));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.remote.ssoSubIds).toBe('');
|
||||
});
|
||||
|
||||
test('setLocalApiKey updates localApiKey', async () => {
|
||||
const { setLocalApiKey } = await import('@app/store/modules/config.js');
|
||||
|
||||
store.dispatch(setLocalApiKey('new-local-api-key'));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.remote.localApiKey).toBe('new-local-api-key');
|
||||
});
|
||||
|
||||
test('setLocalApiKey with null clears localApiKey', async () => {
|
||||
const { setLocalApiKey } = await import('@app/store/modules/config.js');
|
||||
|
||||
store.dispatch(setLocalApiKey(null));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.remote.localApiKey).toBe('');
|
||||
});
|
||||
|
||||
test('setGraphqlConnectionStatus updates minigraph status', async () => {
|
||||
store.dispatch(setGraphqlConnectionStatus({ status: MinigraphStatus.CONNECTED, error: null }));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.connectionStatus.minigraph).toBe(MinigraphStatus.CONNECTED);
|
||||
});
|
||||
|
||||
test('setupRemoteAccessThunk.fulfilled updates remote access settings', async () => {
|
||||
const remoteAccessSettings = {
|
||||
accessType: WAN_ACCESS_TYPE.DYNAMIC,
|
||||
forwardType: WAN_FORWARD_TYPE.UPNP,
|
||||
};
|
||||
|
||||
await store.dispatch(setupRemoteAccessThunk(remoteAccessSettings));
|
||||
|
||||
const state = store.getState().config;
|
||||
expect(state.remote).toMatchObject({
|
||||
wanaccess: 'no',
|
||||
dynamicRemoteAccessType: 'UPNP',
|
||||
wanport: '',
|
||||
upnpEnabled: 'yes',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,253 +197,257 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
}
|
||||
`);
|
||||
expect(disks).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "sdh",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "ST18000NM000J-2TV103_ZR585CPY",
|
||||
"idx": 0,
|
||||
"name": "parity",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 25,
|
||||
"transport": "ata",
|
||||
"type": "Parity",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "Seagate Exos",
|
||||
"critical": 75,
|
||||
"device": "sdf",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 13882739732,
|
||||
"fsSize": 17998742753,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 4116003021,
|
||||
"id": "ST18000NM000J-2TV103_ZR5B1W9X",
|
||||
"idx": 1,
|
||||
"name": "disk1",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "Data",
|
||||
"warning": 50,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdj",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 93140746,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 11904860828,
|
||||
"id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C",
|
||||
"idx": 2,
|
||||
"name": "disk2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "Data",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sde",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 5519945093,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 6478056481,
|
||||
"id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD",
|
||||
"idx": 3,
|
||||
"name": "disk3",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "Data",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdi",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": 111810683,
|
||||
"fsSize": 250059317,
|
||||
"fsType": "btrfs",
|
||||
"fsUsed": 137273827,
|
||||
"id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z",
|
||||
"idx": 30,
|
||||
"name": "cache",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 22,
|
||||
"transport": "ata",
|
||||
"type": "Cache",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "nvme0n1",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "KINGSTON_SA2000M8250G_50026B7282669D9E",
|
||||
"idx": 31,
|
||||
"name": "cache2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 27,
|
||||
"transport": "nvme",
|
||||
"type": "Cache",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "Unraid OS boot device",
|
||||
"critical": null,
|
||||
"device": "sda",
|
||||
"exportable": true,
|
||||
"format": "unknown",
|
||||
"fsFree": 3191407,
|
||||
"fsSize": 4042732,
|
||||
"fsType": "vfat",
|
||||
"fsUsed": 851325,
|
||||
"id": "Cruzer",
|
||||
"idx": 32,
|
||||
"name": "flash",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 3956700,
|
||||
"status": "DISK_OK",
|
||||
"temp": null,
|
||||
"transport": "usb",
|
||||
"type": "Flash",
|
||||
"warning": null,
|
||||
},
|
||||
]
|
||||
`);
|
||||
[
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "sdh",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "ST18000NM000J-2TV103_ZR585CPY",
|
||||
"idx": 0,
|
||||
"name": "parity",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 25,
|
||||
"transport": "ata",
|
||||
"type": "PARITY",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "Seagate Exos",
|
||||
"critical": 75,
|
||||
"device": "sdf",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 13882739732,
|
||||
"fsSize": 17998742753,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 4116003021,
|
||||
"id": "ST18000NM000J-2TV103_ZR5B1W9X",
|
||||
"idx": 1,
|
||||
"name": "disk1",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "DATA",
|
||||
"warning": 50,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdj",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 93140746,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 11904860828,
|
||||
"id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C",
|
||||
"idx": 2,
|
||||
"name": "disk2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "DATA",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sde",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 5519945093,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 6478056481,
|
||||
"id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD",
|
||||
"idx": 3,
|
||||
"name": "disk3",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "DATA",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdi",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": 111810683,
|
||||
"fsSize": 250059317,
|
||||
"fsType": "btrfs",
|
||||
"fsUsed": 137273827,
|
||||
"id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z",
|
||||
"idx": 30,
|
||||
"name": "cache",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 22,
|
||||
"transport": "ata",
|
||||
"type": "CACHE",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "nvme0n1",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "KINGSTON_SA2000M8250G_50026B7282669D9E",
|
||||
"idx": 31,
|
||||
"name": "cache2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 27,
|
||||
"transport": "nvme",
|
||||
"type": "CACHE",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "Unraid OS boot device",
|
||||
"critical": null,
|
||||
"device": "sda",
|
||||
"exportable": true,
|
||||
"format": "unknown",
|
||||
"fsFree": 3191407,
|
||||
"fsSize": 4042732,
|
||||
"fsType": "vfat",
|
||||
"fsUsed": 851325,
|
||||
"id": "Cruzer",
|
||||
"idx": 32,
|
||||
"name": "flash",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 3956700,
|
||||
"status": "DISK_OK",
|
||||
"temp": null,
|
||||
"transport": "usb",
|
||||
"type": "FLASH",
|
||||
"warning": null,
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(shares).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"size": 0,
|
||||
"splitLevel": "",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"size": 0,
|
||||
"splitLevel": "1",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": true,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "ISO images",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "isos",
|
||||
"nameOrig": "isos",
|
||||
"size": 0,
|
||||
"splitLevel": "",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "system data",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "system",
|
||||
"nameOrig": "system",
|
||||
"size": 0,
|
||||
"splitLevel": "1",
|
||||
"used": 33619300,
|
||||
},
|
||||
]
|
||||
`);
|
||||
[
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "appdata",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"size": 0,
|
||||
"splitLevel": "",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "domains",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"size": 0,
|
||||
"splitLevel": "1",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": true,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "ISO images",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "isos",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "isos",
|
||||
"nameOrig": "isos",
|
||||
"size": 0,
|
||||
"splitLevel": "",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "system data",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "system",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "system",
|
||||
"nameOrig": "system",
|
||||
"size": 0,
|
||||
"splitLevel": "1",
|
||||
"used": 33619300,
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(nfsShares).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
@@ -957,7 +961,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
"configErrorState": "INELIGIBLE",
|
||||
"configValid": false,
|
||||
"csrfToken": "0000000000000000",
|
||||
"defaultFsType": "xfs",
|
||||
"defaultFsType": "XFS",
|
||||
"deviceCount": 4,
|
||||
"domain": "",
|
||||
"domainLogin": "Administrator",
|
||||
|
||||
@@ -4,31 +4,63 @@ import { store } from '@app/store/index.js';
|
||||
|
||||
test('Returns paths', async () => {
|
||||
const { paths } = store.getState();
|
||||
expect(Object.keys(paths)).toMatchInlineSnapshot(`
|
||||
[
|
||||
"core",
|
||||
"unraid-api-base",
|
||||
"unraid-data",
|
||||
"docker-autostart",
|
||||
"docker-socket",
|
||||
"parity-checks",
|
||||
"htpasswd",
|
||||
"emhttpd-socket",
|
||||
"states",
|
||||
"dynamix-base",
|
||||
"dynamix-config",
|
||||
"myservers-base",
|
||||
"myservers-config",
|
||||
"myservers-config-states",
|
||||
"myservers-env",
|
||||
"myservers-keepalive",
|
||||
"keyfile-base",
|
||||
"machine-id",
|
||||
"log-base",
|
||||
"unraid-log-base",
|
||||
"var-run",
|
||||
"auth-sessions",
|
||||
"auth-keys",
|
||||
]
|
||||
`);
|
||||
expect(Object.keys(paths)).toMatchSnapshot();
|
||||
|
||||
expect(paths).toMatchObject({
|
||||
core: expect.stringContaining('api/src/store/modules'),
|
||||
'unraid-api-base': '/usr/local/unraid-api/',
|
||||
'unraid-data': expect.stringContaining('api/dev/data'),
|
||||
'docker-autostart': '/var/lib/docker/unraid-autostart',
|
||||
'docker-socket': '/var/run/docker.sock',
|
||||
'parity-checks': expect.stringContaining('api/dev/states/parity-checks.log'),
|
||||
htpasswd: '/etc/nginx/htpasswd',
|
||||
'emhttpd-socket': '/var/run/emhttpd.socket',
|
||||
states: expect.stringContaining('api/dev/states'),
|
||||
'dynamix-base': expect.stringContaining('api/dev/dynamix'),
|
||||
'dynamix-config': expect.arrayContaining([
|
||||
expect.stringContaining('api/dev/dynamix/default.cfg'),
|
||||
expect.stringContaining('api/dev/dynamix/dynamix.cfg'),
|
||||
]),
|
||||
'myservers-base': '/boot/config/plugins/dynamix.my.servers/',
|
||||
'myservers-config': expect.stringContaining('api/dev/Unraid.net/myservers.cfg'),
|
||||
'myservers-config-states': expect.stringContaining('api/dev/states/myservers.cfg'),
|
||||
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env',
|
||||
'myservers-keepalive': './dev/Unraid.net/fb_keepalive',
|
||||
'keyfile-base': expect.stringContaining('api/dev/Unraid.net'),
|
||||
'machine-id': expect.stringContaining('api/dev/data/machine-id'),
|
||||
'log-base': '/var/log/unraid-api',
|
||||
'unraid-log-base': '/var/log',
|
||||
'var-run': '/var/run',
|
||||
'auth-sessions': './dev/sessions',
|
||||
'auth-keys': expect.stringContaining('api/dev/keys'),
|
||||
passwd: expect.stringContaining('api/dev/passwd'),
|
||||
'libvirt-pid': '/var/run/libvirt/libvirtd.pid',
|
||||
activationBase: expect.stringContaining('api/dev/activation'),
|
||||
webGuiBase: '/usr/local/emhttp/webGui',
|
||||
identConfig: '/boot/config/ident.cfg',
|
||||
activation: {
|
||||
assets: expect.stringContaining('api/dev/activation/assets'),
|
||||
logo: expect.stringContaining('api/dev/activation/assets/logo.svg'),
|
||||
caseModel: expect.stringContaining('api/dev/activation/assets/case-model.png'),
|
||||
banner: expect.stringContaining('api/dev/activation/assets/banner.png'),
|
||||
},
|
||||
boot: {
|
||||
caseModel: expect.stringContaining('api/dev/dynamix/case-model.png'),
|
||||
},
|
||||
webgui: {
|
||||
imagesBase: '/usr/local/emhttp/webGui/images',
|
||||
logo: {
|
||||
fullPath: '/usr/local/emhttp/webGui/images/UN-logotype-gradient.svg',
|
||||
assetPath: '/webGui/images/UN-logotype-gradient.svg',
|
||||
},
|
||||
caseModel: {
|
||||
fullPath: '/usr/local/emhttp/webGui/images/case-model.png',
|
||||
assetPath: '/webGui/images/case-model.png',
|
||||
},
|
||||
banner: {
|
||||
fullPath: '/usr/local/emhttp/webGui/images/banner.png',
|
||||
assetPath: '/webGui/images/banner.png',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { store } from '@app/store/index.js';
|
||||
import { getters, store } from '@app/store/index.js';
|
||||
import { loadRegistrationKey } from '@app/store/modules/registration.js';
|
||||
import { FileLoadStatus, StateFileKey } from '@app/store/types.js';
|
||||
|
||||
// Preloading imports for faster tests
|
||||
|
||||
test('Before loading key returns null', async () => {
|
||||
const { status, keyFile } = store.getState().registration;
|
||||
const { status, keyFile } = getters.registration();
|
||||
expect(status).toBe(FileLoadStatus.UNLOADED);
|
||||
expect(keyFile).toBe(null);
|
||||
});
|
||||
@@ -17,7 +17,7 @@ test('Requires emhttp to be loaded to find key file', async () => {
|
||||
await store.dispatch(loadRegistrationKey());
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { status, keyFile } = store.getState().registration;
|
||||
const { status, keyFile } = getters.registration();
|
||||
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
expect(keyFile).toBe(null);
|
||||
@@ -42,7 +42,7 @@ test('Returns empty key if key location is empty', async () => {
|
||||
await store.dispatch(loadRegistrationKey());
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { status, keyFile } = store.getState().registration;
|
||||
const { status, keyFile } = getters.registration();
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
expect(keyFile).toBe('');
|
||||
});
|
||||
|
||||
@@ -15,79 +15,83 @@ test('Returns parsed state file', async () => {
|
||||
type: 'ini',
|
||||
});
|
||||
expect(parse(stateFile)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"size": 0,
|
||||
"splitLevel": "",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"size": 0,
|
||||
"splitLevel": "1",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": true,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "ISO images",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "isos",
|
||||
"nameOrig": "isos",
|
||||
"size": 0,
|
||||
"splitLevel": "",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "system data",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "system",
|
||||
"nameOrig": "system",
|
||||
"size": 0,
|
||||
"splitLevel": "1",
|
||||
"used": 33619300,
|
||||
},
|
||||
]
|
||||
`);
|
||||
[
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "appdata",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "appdata",
|
||||
"nameOrig": "appdata",
|
||||
"size": 0,
|
||||
"splitLevel": "",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "saved VM instances",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "domains",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "domains",
|
||||
"nameOrig": "domains",
|
||||
"size": 0,
|
||||
"splitLevel": "1",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": true,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "ISO images",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "isos",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "isos",
|
||||
"nameOrig": "isos",
|
||||
"size": 0,
|
||||
"splitLevel": "",
|
||||
"used": 33619300,
|
||||
},
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cache": false,
|
||||
"cachePool": "cache",
|
||||
"color": "yellow-on",
|
||||
"comment": "system data",
|
||||
"cow": "auto",
|
||||
"exclude": [],
|
||||
"floor": "0",
|
||||
"free": 9309372,
|
||||
"id": "system",
|
||||
"include": [],
|
||||
"luksStatus": "0",
|
||||
"name": "system",
|
||||
"nameOrig": "system",
|
||||
"size": 0,
|
||||
"splitLevel": "1",
|
||||
"used": 33619300,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -15,175 +15,175 @@ test('Returns parsed state file', async () => {
|
||||
type: 'ini',
|
||||
});
|
||||
expect(parse(stateFile)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "sdh",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "ST18000NM000J-2TV103_ZR585CPY",
|
||||
"idx": 0,
|
||||
"name": "parity",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 25,
|
||||
"transport": "ata",
|
||||
"type": "Parity",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "Seagate Exos",
|
||||
"critical": 75,
|
||||
"device": "sdf",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 13882739732,
|
||||
"fsSize": 17998742753,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 4116003021,
|
||||
"id": "ST18000NM000J-2TV103_ZR5B1W9X",
|
||||
"idx": 1,
|
||||
"name": "disk1",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "Data",
|
||||
"warning": 50,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdj",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 93140746,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 11904860828,
|
||||
"id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C",
|
||||
"idx": 2,
|
||||
"name": "disk2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "Data",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sde",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 5519945093,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 6478056481,
|
||||
"id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD",
|
||||
"idx": 3,
|
||||
"name": "disk3",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "Data",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdi",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": 111810683,
|
||||
"fsSize": 250059317,
|
||||
"fsType": "btrfs",
|
||||
"fsUsed": 137273827,
|
||||
"id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z",
|
||||
"idx": 30,
|
||||
"name": "cache",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 22,
|
||||
"transport": "ata",
|
||||
"type": "Cache",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "nvme0n1",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "KINGSTON_SA2000M8250G_50026B7282669D9E",
|
||||
"idx": 31,
|
||||
"name": "cache2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 27,
|
||||
"transport": "nvme",
|
||||
"type": "Cache",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "Unraid OS boot device",
|
||||
"critical": null,
|
||||
"device": "sda",
|
||||
"exportable": true,
|
||||
"format": "unknown",
|
||||
"fsFree": 3191407,
|
||||
"fsSize": 4042732,
|
||||
"fsType": "vfat",
|
||||
"fsUsed": 851325,
|
||||
"id": "Cruzer",
|
||||
"idx": 32,
|
||||
"name": "flash",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 3956700,
|
||||
"status": "DISK_OK",
|
||||
"temp": null,
|
||||
"transport": "usb",
|
||||
"type": "Flash",
|
||||
"warning": null,
|
||||
},
|
||||
]
|
||||
`);
|
||||
[
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "sdh",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "ST18000NM000J-2TV103_ZR585CPY",
|
||||
"idx": 0,
|
||||
"name": "parity",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 25,
|
||||
"transport": "ata",
|
||||
"type": "PARITY",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "Seagate Exos",
|
||||
"critical": 75,
|
||||
"device": "sdf",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 13882739732,
|
||||
"fsSize": 17998742753,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 4116003021,
|
||||
"id": "ST18000NM000J-2TV103_ZR5B1W9X",
|
||||
"idx": 1,
|
||||
"name": "disk1",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "DATA",
|
||||
"warning": 50,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdj",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 93140746,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 11904860828,
|
||||
"id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C",
|
||||
"idx": 2,
|
||||
"name": "disk2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "DATA",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sde",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 5519945093,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 6478056481,
|
||||
"id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD",
|
||||
"idx": 3,
|
||||
"name": "disk3",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "DATA",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdi",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": 111810683,
|
||||
"fsSize": 250059317,
|
||||
"fsType": "btrfs",
|
||||
"fsUsed": 137273827,
|
||||
"id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z",
|
||||
"idx": 30,
|
||||
"name": "cache",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 22,
|
||||
"transport": "ata",
|
||||
"type": "CACHE",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "nvme0n1",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "KINGSTON_SA2000M8250G_50026B7282669D9E",
|
||||
"idx": 31,
|
||||
"name": "cache2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 27,
|
||||
"transport": "nvme",
|
||||
"type": "CACHE",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "Unraid OS boot device",
|
||||
"critical": null,
|
||||
"device": "sda",
|
||||
"exportable": true,
|
||||
"format": "unknown",
|
||||
"fsFree": 3191407,
|
||||
"fsSize": 4042732,
|
||||
"fsType": "vfat",
|
||||
"fsUsed": 851325,
|
||||
"id": "Cruzer",
|
||||
"idx": 32,
|
||||
"name": "flash",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 3956700,
|
||||
"status": "DISK_OK",
|
||||
"temp": null,
|
||||
"transport": "usb",
|
||||
"type": "FLASH",
|
||||
"warning": null,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ test('Returns parsed state file', async () => {
|
||||
"configErrorState": "INELIGIBLE",
|
||||
"configValid": false,
|
||||
"csrfToken": "0000000000000000",
|
||||
"defaultFsType": "xfs",
|
||||
"defaultFsType": "XFS",
|
||||
"deviceCount": 4,
|
||||
"domain": "",
|
||||
"domainLogin": "Administrator",
|
||||
|
||||
@@ -20,16 +20,7 @@ const getUnraidApiLocation = async () => {
|
||||
};
|
||||
|
||||
try {
|
||||
// Register plugins and create a dynamic module configuration
|
||||
const dynamicModule = await CliModule.registerWithPlugins();
|
||||
|
||||
// Create a new class that extends CliModule with the dynamic configuration
|
||||
const DynamicCliModule = class extends CliModule {
|
||||
static module = dynamicModule.module;
|
||||
static imports = dynamicModule.imports;
|
||||
static providers = dynamicModule.providers;
|
||||
};
|
||||
await CommandFactory.run(DynamicCliModule, {
|
||||
await CommandFactory.run(CliModule, {
|
||||
cliName: 'unraid-api',
|
||||
logger: LOG_LEVEL === 'TRACE' ? new LogService() : false, // - enable this to see nest initialization issues
|
||||
completion: {
|
||||
|
||||
@@ -20,6 +20,7 @@ const getAllowedSocks = (): string[] => [
|
||||
|
||||
const getLocalAccessUrlsForServer = (state: RootState = store.getState()): string[] => {
|
||||
const { emhttp } = state;
|
||||
|
||||
if (emhttp.status !== FileLoadStatus.LOADED) {
|
||||
return [];
|
||||
}
|
||||
@@ -90,7 +91,7 @@ const getApolloSandbox = (): string[] => {
|
||||
export const getAllowedOrigins = (state: RootState = store.getState()): string[] =>
|
||||
uniq([
|
||||
...getAllowedSocks(),
|
||||
...getLocalAccessUrlsForServer(),
|
||||
...getLocalAccessUrlsForServer(state),
|
||||
...getRemoteAccessUrlsForAllowedOrigins(state),
|
||||
...getExtraOrigins(),
|
||||
...getConnectOrigins(),
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ArrayRunningError } from '@app/core/errors/array-running-error.js';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error.js';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types/index.js';
|
||||
import { arrayIsRunning } from '@app/core/utils/array/array-is-running.js';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd.js';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission.js';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields.js';
|
||||
|
||||
/**
|
||||
* Add a disk to the array.
|
||||
*/
|
||||
export const addDiskToArray = async function (context: CoreContext): Promise<CoreResult> {
|
||||
const { data = {}, user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const missingFields = hasFields(data, ['id']);
|
||||
if (missingFields.length !== 0) {
|
||||
// Just log first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
|
||||
if (arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
|
||||
const { id: diskId, slot: preferredSlot } = data;
|
||||
const slot = Number.parseInt(preferredSlot as string, 10);
|
||||
|
||||
// Add disk
|
||||
await emcmd({
|
||||
changeDevice: 'apply',
|
||||
[`slotId.${slot}`]: diskId,
|
||||
});
|
||||
|
||||
const array = getArrayData();
|
||||
|
||||
// Disk added successfully
|
||||
return {
|
||||
text: `Disk was added to the array in slot ${slot}.`,
|
||||
json: array,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,16 @@
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { sum } from 'lodash-es';
|
||||
|
||||
import type { ArrayCapacity, ArrayType } from '@app/graphql/generated/api/types.js';
|
||||
import { ArrayDiskType } from '@app/graphql/generated/api/types.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import { FileLoadStatus } from '@app/store/types.js';
|
||||
import {
|
||||
ArrayCapacity,
|
||||
ArrayDiskType,
|
||||
ArrayState,
|
||||
UnraidArray,
|
||||
} from '@app/unraid-api/graph/resolvers/array/array.model.js';
|
||||
|
||||
export const getArrayData = (getState = store.getState): ArrayType => {
|
||||
export const getArrayData = (getState = store.getState): UnraidArray => {
|
||||
// Var state isn't loaded
|
||||
const state = getState();
|
||||
if (
|
||||
@@ -51,7 +55,7 @@ export const getArrayData = (getState = store.getState): ArrayType => {
|
||||
|
||||
return {
|
||||
id: 'array',
|
||||
state: emhttp.var.mdState,
|
||||
state: emhttp.var.mdState as ArrayState,
|
||||
capacity,
|
||||
boot,
|
||||
parities,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Created from 'create-ts-index'
|
||||
|
||||
export * from './add-disk-to-array.js';
|
||||
export * from './remove-disk-from-array.js';
|
||||
export * from './update-array.js';
|
||||
export * from './update-parity-check.js';
|
||||
@@ -1,45 +0,0 @@
|
||||
import { ArrayRunningError } from '@app/core/errors/array-running-error.js';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error.js';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types/index.js';
|
||||
import { arrayIsRunning } from '@app/core/utils/array/array-is-running.js';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields.js';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
data: {
|
||||
/** The slot the disk is in. */
|
||||
slot: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a disk from the array.
|
||||
* @returns The updated array.
|
||||
*/
|
||||
export const removeDiskFromArray = async (context: Context): Promise<CoreResult> => {
|
||||
const { data } = context;
|
||||
const missingFields = hasFields(data, ['id']);
|
||||
|
||||
if (missingFields.length !== 0) {
|
||||
// Only log first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
|
||||
if (arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
|
||||
const { slot } = data;
|
||||
|
||||
// Error removing disk
|
||||
// if () {
|
||||
// }
|
||||
|
||||
const array = getArrayData();
|
||||
|
||||
// Disk removed successfully
|
||||
return {
|
||||
text: `Disk was removed from the array in slot ${slot}.`,
|
||||
json: array,
|
||||
};
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types/index.js';
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error.js';
|
||||
import { ParamInvalidError } from '@app/core/errors/param-invalid-error.js';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { arrayIsRunning } from '@app/core/utils/array/array-is-running.js';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd.js';
|
||||
import { uppercaseFirstChar } from '@app/core/utils/misc/uppercase-first-char.js';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission.js';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields.js';
|
||||
|
||||
// @TODO: Fix this not working across node apps
|
||||
// each app has it's own lock since the var is scoped
|
||||
// ideally this should have a timeout to prevent it sticking
|
||||
let locked = false;
|
||||
|
||||
export const updateArray = async (context: CoreContext): Promise<CoreResult> => {
|
||||
const { data = {}, user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const missingFields = hasFields(data, ['state']);
|
||||
|
||||
if (missingFields.length !== 0) {
|
||||
// Only log first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
|
||||
const { state: nextState } = data as { state: string };
|
||||
const startState = arrayIsRunning() ? 'started' : 'stopped';
|
||||
const pendingState = nextState === 'stop' ? 'stopping' : 'starting';
|
||||
|
||||
if (!['start', 'stop'].includes(nextState)) {
|
||||
throw new ParamInvalidError('state', nextState);
|
||||
}
|
||||
|
||||
// Prevent this running multiple times at once
|
||||
if (locked) {
|
||||
throw new AppError('Array state is still being updated.');
|
||||
}
|
||||
|
||||
// Prevent starting/stopping array when it's already in the same state
|
||||
if ((arrayIsRunning() && nextState === 'start') || (!arrayIsRunning() && nextState === 'stop')) {
|
||||
throw new AppError(`The array is already ${startState}`);
|
||||
}
|
||||
|
||||
// Set lock then start/stop array
|
||||
locked = true;
|
||||
const command = {
|
||||
[`cmd${uppercaseFirstChar(nextState)}`]: uppercaseFirstChar(nextState),
|
||||
startState: startState.toUpperCase(),
|
||||
};
|
||||
|
||||
// `await` has to be used otherwise the catch
|
||||
// will finish after the return statement below
|
||||
await emcmd(command).finally(() => {
|
||||
locked = false;
|
||||
});
|
||||
|
||||
// Get new array JSON
|
||||
const array = getArrayData();
|
||||
|
||||
/**
|
||||
* Update array details
|
||||
*
|
||||
* @memberof Core
|
||||
* @module array/update-array
|
||||
* @param {Core~Context} context Context object.
|
||||
* @param {Object} context.data The data object.
|
||||
* @param {'start'|'stop'} context.data.state If the array should be started or stopped.
|
||||
* @param {State~User} context.user The current user.
|
||||
* @returns {Core~Result} The updated array.
|
||||
*/
|
||||
return {
|
||||
text: `Array was ${startState}, ${pendingState}.`,
|
||||
json: {
|
||||
...array,
|
||||
state: nextState === 'start' ? 'started' : 'stopped',
|
||||
previousState: startState,
|
||||
pendingState,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types/index.js';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error.js';
|
||||
import { ParamInvalidError } from '@app/core/errors/param-invalid-error.js';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd.js';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
|
||||
type State = 'start' | 'cancel' | 'resume' | 'cancel';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
data: {
|
||||
state?: State;
|
||||
correct?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a disk from the array.
|
||||
* @returns The update array.
|
||||
*/
|
||||
export const updateParityCheck = async (context: Context): Promise<CoreResult> => {
|
||||
const { user, data } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
// Validation
|
||||
if (!data.state) {
|
||||
throw new FieldMissingError('state');
|
||||
}
|
||||
|
||||
const { state: wantedState } = data;
|
||||
const emhttp = getters.emhttp();
|
||||
const running = emhttp.var.mdResync !== 0;
|
||||
const states = {
|
||||
pause: {
|
||||
cmdNoCheck: 'Pause',
|
||||
},
|
||||
resume: {
|
||||
cmdCheck: 'Resume',
|
||||
},
|
||||
cancel: {
|
||||
cmdNoCheck: 'Cancel',
|
||||
},
|
||||
start: {
|
||||
cmdCheck: 'Check',
|
||||
},
|
||||
};
|
||||
|
||||
let allowedStates = Object.keys(states);
|
||||
|
||||
// Only allow starting a check if there isn't already one running
|
||||
if (running) {
|
||||
allowedStates = allowedStates.splice(allowedStates.indexOf('start'), 1);
|
||||
}
|
||||
|
||||
// Only allow states from states object
|
||||
if (!allowedStates.includes(wantedState)) {
|
||||
throw new ParamInvalidError('state', wantedState);
|
||||
}
|
||||
|
||||
// Should we write correction to the parity during the check
|
||||
const writeCorrectionsToParity = wantedState === 'start' && data.correct;
|
||||
|
||||
await emcmd({
|
||||
startState: 'STARTED',
|
||||
...states[wantedState],
|
||||
...(writeCorrectionsToParity ? { optionCorrect: 'correct' } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
json: {},
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types/index.js';
|
||||
import { Disk } from '@app/graphql/generated/api/types.js';
|
||||
import { ArrayDisk } from '@app/unraid-api/graph/resolvers/array/array.model.js';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
params: {
|
||||
@@ -11,7 +11,7 @@ interface Context extends CoreContext {
|
||||
/**
|
||||
* Get a single disk.
|
||||
*/
|
||||
export const getDisk = async (context: Context, Disks: Disk[]): Promise<CoreResult> => {
|
||||
export const getDisk = async (context: Context, Disks: ArrayDisk[]): Promise<CoreResult> => {
|
||||
const { params } = context;
|
||||
|
||||
const { id } = params;
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import fs from 'fs';
|
||||
|
||||
import camelCaseKeys from 'camelcase-keys';
|
||||
|
||||
import type { ContainerPort, Docker, DockerContainer } from '@app/graphql/generated/api/types.js';
|
||||
import { dockerLogger } from '@app/core/log.js';
|
||||
import { docker } from '@app/core/utils/clients/docker.js';
|
||||
import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js';
|
||||
import { ContainerPortType, ContainerState } from '@app/graphql/generated/api/types.js';
|
||||
import { getters, store } from '@app/store/index.js';
|
||||
import { updateDockerState } from '@app/store/modules/docker.js';
|
||||
|
||||
export interface ContainerListingOptions {
|
||||
useCache?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Docker containers.
|
||||
* @returns All the in/active Docker containers on the system.
|
||||
*/
|
||||
export const getDockerContainers = async (
|
||||
{ useCache }: ContainerListingOptions = { useCache: true }
|
||||
): Promise<Array<DockerContainer>> => {
|
||||
const dockerState = getters.docker();
|
||||
if (useCache && dockerState.containers) {
|
||||
dockerLogger.trace('Using docker container cache');
|
||||
return dockerState.containers;
|
||||
}
|
||||
|
||||
dockerLogger.trace('Skipping docker container cache');
|
||||
|
||||
/**
|
||||
* Docker auto start file
|
||||
*
|
||||
* @note Doesn't exist if array is offline.
|
||||
* @see https://github.com/limetech/webgui/issues/502#issue-480992547
|
||||
*/
|
||||
const autoStartFile = await fs.promises
|
||||
.readFile(getters.paths()['docker-autostart'], 'utf8')
|
||||
.then((file) => file.toString())
|
||||
.catch(() => '');
|
||||
const autoStarts = autoStartFile.split('\n');
|
||||
const rawContainers = await docker
|
||||
.listContainers({
|
||||
all: true,
|
||||
size: true,
|
||||
})
|
||||
// If docker throws an error return no containers
|
||||
.catch(catchHandlers.docker);
|
||||
|
||||
// Cleanup container object
|
||||
const containers: Array<DockerContainer> = rawContainers.map((container) => {
|
||||
const names = container.Names[0];
|
||||
const containerData: DockerContainer = camelCaseKeys<DockerContainer>(
|
||||
{
|
||||
labels: container.Labels ?? {},
|
||||
sizeRootFs: undefined,
|
||||
imageId: container.ImageID,
|
||||
state:
|
||||
typeof container.State === 'string'
|
||||
? (ContainerState[container.State.toUpperCase()] ?? ContainerState.EXITED)
|
||||
: ContainerState.EXITED,
|
||||
autoStart: autoStarts.includes(names.split('/')[1]),
|
||||
ports: container.Ports.map<ContainerPort>((port) => ({
|
||||
...port,
|
||||
type: ContainerPortType[port.Type.toUpperCase()],
|
||||
})),
|
||||
command: container.Command,
|
||||
created: container.Created,
|
||||
mounts: container.Mounts,
|
||||
networkSettings: container.NetworkSettings,
|
||||
hostConfig: {
|
||||
networkMode: container.HostConfig.NetworkMode,
|
||||
},
|
||||
id: container.Id,
|
||||
image: container.Image,
|
||||
status: container.Status,
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
return containerData;
|
||||
});
|
||||
|
||||
// Get all of the current containers
|
||||
const installed = containers.length;
|
||||
const running = containers.filter((container) => container.state === ContainerState.RUNNING).length;
|
||||
|
||||
store.dispatch(updateDockerState({ containers, installed, running }));
|
||||
return containers;
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import camelCaseKeys from 'camelcase-keys';
|
||||
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types/index.js';
|
||||
import { docker } from '@app/core/utils/index.js';
|
||||
import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission.js';
|
||||
|
||||
export const getDockerNetworks = async (context: CoreContext): Promise<CoreResult> => {
|
||||
const { user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'docker/network',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const networks = await docker
|
||||
.listNetworks()
|
||||
// If docker throws an error return no networks
|
||||
.catch(catchHandlers.docker)
|
||||
.then((networks = []) =>
|
||||
networks.map((object) =>
|
||||
camelCaseKeys(object as unknown as Record<string, unknown>, { deep: true })
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get all Docker networks
|
||||
*
|
||||
* @memberof Core
|
||||
* @module docker/get-networks
|
||||
* @param {Core~Context} context
|
||||
* @returns {Core~Result} All the in/active Docker networks on the system.
|
||||
*/
|
||||
return {
|
||||
json: networks,
|
||||
};
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
// Created from 'create-ts-index'
|
||||
|
||||
export * from './get-docker-containers.js';
|
||||
export * from './get-docker-networks.js';
|
||||
@@ -1,92 +0,0 @@
|
||||
import type { Systeminformation } from 'systeminformation';
|
||||
import { execa } from 'execa';
|
||||
import { blockDevices, diskLayout } from 'systeminformation';
|
||||
|
||||
import type { Disk } from '@app/graphql/generated/api/types.js';
|
||||
import { graphqlLogger } from '@app/core/log.js';
|
||||
import { DiskFsType, DiskInterfaceType, DiskSmartStatus } from '@app/graphql/generated/api/types.js';
|
||||
import { batchProcess } from '@app/utils.js';
|
||||
|
||||
const getTemperature = async (disk: Systeminformation.DiskLayoutData): Promise<number> => {
|
||||
try {
|
||||
const stdout = await execa('smartctl', ['-A', disk.device])
|
||||
.then(({ stdout }) => stdout)
|
||||
.catch(() => '');
|
||||
const lines = stdout.split('\n');
|
||||
const header = lines.find((line) => line.startsWith('ID#')) ?? '';
|
||||
const fields = lines.splice(lines.indexOf(header) + 1, lines.length);
|
||||
const field = fields.find(
|
||||
(line) => line.includes('Temperature_Celsius') || line.includes('Airflow_Temperature_Cel')
|
||||
);
|
||||
|
||||
if (!field) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (field.includes('Min/Max')) {
|
||||
return Number.parseInt(field.split(' - ')[1].trim().split(' ')[0], 10);
|
||||
}
|
||||
|
||||
const line = field.split(' ');
|
||||
return Number.parseInt(line[line.length - 1], 10);
|
||||
} catch (error) {
|
||||
graphqlLogger.warn('Caught error fetching disk temperature: %o', error);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
const parseDisk = async (
|
||||
disk: Systeminformation.DiskLayoutData,
|
||||
partitionsToParse: Systeminformation.BlockDevicesData[],
|
||||
temperature = false
|
||||
): Promise<Disk> => {
|
||||
const partitions = partitionsToParse
|
||||
// Only get partitions from this disk
|
||||
.filter((partition) => partition.name.startsWith(disk.device.split('/dev/')[1]))
|
||||
// Remove unneeded fields
|
||||
.map(({ name, fsType, size }) => ({
|
||||
name,
|
||||
fsType: typeof fsType === 'string' ? DiskFsType[fsType] : undefined,
|
||||
size,
|
||||
}));
|
||||
|
||||
return {
|
||||
...disk,
|
||||
smartStatus:
|
||||
typeof disk.smartStatus === 'string'
|
||||
? DiskSmartStatus[disk.smartStatus.toUpperCase()]
|
||||
: undefined,
|
||||
interfaceType:
|
||||
typeof disk.interfaceType === 'string'
|
||||
? DiskInterfaceType[disk.interfaceType]
|
||||
: DiskInterfaceType.UNKNOWN,
|
||||
temperature: temperature ? await getTemperature(disk) : -1,
|
||||
partitions,
|
||||
id: disk.serialNum,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all disks.
|
||||
*/
|
||||
export const getDisks = async (options?: { temperature: boolean }): Promise<Disk[]> => {
|
||||
// Return all fields but temperature
|
||||
if (options?.temperature === false) {
|
||||
const partitions = await blockDevices().then((devices) =>
|
||||
devices.filter((device) => device.type === 'part')
|
||||
);
|
||||
const diskLayoutData = await diskLayout();
|
||||
const disks = await Promise.all(diskLayoutData.map((disk) => parseDisk(disk, partitions)));
|
||||
|
||||
return disks;
|
||||
}
|
||||
|
||||
const partitions = await blockDevices().then((devices) =>
|
||||
devices.filter((device) => device.type === 'part')
|
||||
);
|
||||
|
||||
const { data } = await batchProcess(await diskLayout(), async (disk) =>
|
||||
parseDisk(disk, partitions, true)
|
||||
);
|
||||
return data;
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import Table from 'cli-table';
|
||||
|
||||
import { FileMissingError } from '@app/core/errors/file-missing-error.js';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types/index.js';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
|
||||
/**
|
||||
* Get parity history.
|
||||
* @returns All parity checks with their respective date, duration, speed, status and errors.
|
||||
*/
|
||||
export const getParityHistory = async (context: CoreContext): Promise<CoreResult> => {
|
||||
const { user } = context;
|
||||
|
||||
// Bail if the user doesn't have permission
|
||||
ensurePermission(user, {
|
||||
resource: 'parity-history',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const historyFilePath = getters.paths()['parity-checks'];
|
||||
const history = await fs.readFile(historyFilePath).catch(() => {
|
||||
throw new FileMissingError(historyFilePath);
|
||||
});
|
||||
|
||||
// Convert checks into array of objects
|
||||
const lines = history.toString().trim().split('\n').reverse();
|
||||
const parityChecks = lines.map((line) => {
|
||||
const [date, duration, speed, status, errors = '0'] = line.split('|');
|
||||
return {
|
||||
date,
|
||||
duration: Number.parseInt(duration, 10),
|
||||
speed,
|
||||
status,
|
||||
errors: Number.parseInt(errors, 10),
|
||||
};
|
||||
});
|
||||
|
||||
// Create table for text output
|
||||
const table = new Table({
|
||||
head: ['Date', 'Duration', 'Speed', 'Status', 'Errors'],
|
||||
});
|
||||
// Update raw values with strings
|
||||
parityChecks.forEach((check) => {
|
||||
const array = Object.values({
|
||||
date: check.date,
|
||||
speed: check.speed ? check.speed : 'Unavailable',
|
||||
duration: check.duration >= 0 ? check.duration.toString() : 'Unavailable',
|
||||
status: check.status === '-4' ? 'Cancelled' : 'OK',
|
||||
errors: check.errors.toString(),
|
||||
});
|
||||
table.push(array);
|
||||
});
|
||||
|
||||
return {
|
||||
text: table.toString(),
|
||||
json: parityChecks,
|
||||
};
|
||||
};
|
||||
@@ -1,20 +1,15 @@
|
||||
// Created from 'create-ts-index'
|
||||
|
||||
export * from './array/index.js';
|
||||
export * from './debug/index.js';
|
||||
export * from './disks/index.js';
|
||||
export * from './docker/index.js';
|
||||
export * from './services/index.js';
|
||||
export * from './settings/index.js';
|
||||
export * from './shares/index.js';
|
||||
export * from './users/index.js';
|
||||
export * from './vms/index.js';
|
||||
export * from './add-share.js';
|
||||
export * from './add-user.js';
|
||||
export * from './get-apps.js';
|
||||
export * from './get-devices.js';
|
||||
export * from './get-disks.js';
|
||||
export * from './get-parity-history.js';
|
||||
export * from './get-services.js';
|
||||
export * from './get-users.js';
|
||||
export * from './get-welcome.js';
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { GraphQLError } from 'graphql';
|
||||
|
||||
import type { VmDomain } from '@app/graphql/generated/api/types.js';
|
||||
import { VmState } from '@app/graphql/generated/api/types.js';
|
||||
|
||||
const states = {
|
||||
0: 'NOSTATE',
|
||||
1: 'RUNNING',
|
||||
2: 'IDLE',
|
||||
3: 'PAUSED',
|
||||
4: 'SHUTDOWN',
|
||||
5: 'SHUTOFF',
|
||||
6: 'CRASHED',
|
||||
7: 'PMSUSPENDED',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get vm domains.
|
||||
*/
|
||||
export const getDomains = async () => {
|
||||
try {
|
||||
const { ConnectListAllDomainsFlags } = await import('@unraid/libvirt');
|
||||
const { UnraidHypervisor } = await import('@app/core/utils/vms/get-hypervisor.js');
|
||||
|
||||
const hypervisor = await UnraidHypervisor.getInstance().getHypervisor();
|
||||
if (!hypervisor) {
|
||||
throw new GraphQLError('VMs Disabled');
|
||||
}
|
||||
|
||||
const autoStartDomains = await hypervisor.connectListAllDomains(
|
||||
ConnectListAllDomainsFlags.AUTOSTART
|
||||
);
|
||||
|
||||
const autoStartDomainNames = await Promise.all(
|
||||
autoStartDomains.map(async (domain) => hypervisor.domainGetName(domain))
|
||||
);
|
||||
|
||||
// Get all domains
|
||||
const domains = await hypervisor.connectListAllDomains();
|
||||
|
||||
const resolvedDomains: Array<VmDomain> = await Promise.all(
|
||||
domains.map(async (domain) => {
|
||||
const info = await hypervisor.domainGetInfo(domain);
|
||||
const name = await hypervisor.domainGetName(domain);
|
||||
const features = {};
|
||||
return {
|
||||
name,
|
||||
uuid: await hypervisor.domainGetUUIDString(domain),
|
||||
state: VmState[states[info.state]] ?? VmState.NOSTATE,
|
||||
autoStart: autoStartDomainNames.includes(name),
|
||||
features,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return resolvedDomains;
|
||||
} catch (error: unknown) {
|
||||
// If we hit an error expect libvirt to be offline
|
||||
throw new GraphQLError(
|
||||
`Failed to fetch domains with error: ${error instanceof Error ? error.message : 'Unknown Error'}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
// Created from 'create-ts-index'
|
||||
export * from './get-domains.js';
|
||||
@@ -19,6 +19,7 @@ export enum PUBSUB_CHANNEL {
|
||||
VMS = 'VMS',
|
||||
REGISTRATION = 'REGISTRATION',
|
||||
LOG_FILE = 'LOG_FILE',
|
||||
PARITY = 'PARITY',
|
||||
}
|
||||
|
||||
export const pubsub = new PubSub({ eventEmitter });
|
||||
|
||||
@@ -23,6 +23,9 @@ interface Display {
|
||||
scale: string;
|
||||
tabs: string;
|
||||
text: string;
|
||||
/**
|
||||
* 'black' or 'white' or 'azure' or 'gray'
|
||||
*/
|
||||
theme: string;
|
||||
total: string;
|
||||
unit: Unit;
|
||||
@@ -30,6 +33,32 @@ interface Display {
|
||||
warning: string;
|
||||
wwn: string;
|
||||
locale: string;
|
||||
/**
|
||||
* hex color (without #)
|
||||
*/
|
||||
headerMetaColor: string;
|
||||
/**
|
||||
* 'yes' or 'no'
|
||||
*/
|
||||
showBannerGradient: string;
|
||||
/**
|
||||
* 'yes' or 'no'
|
||||
*/
|
||||
headerdescription: string;
|
||||
/**
|
||||
* Header Secondary Text Color (without #)
|
||||
*/
|
||||
headermetacolor: string;
|
||||
/**
|
||||
* Header Text Color (without #)
|
||||
*/
|
||||
header: string;
|
||||
/**
|
||||
* Header Background Color (without #)
|
||||
*/
|
||||
background: string;
|
||||
tty: string;
|
||||
raw: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type {
|
||||
ArrayState,
|
||||
DiskFsType,
|
||||
import { ArrayState } from '@app/unraid-api/graph/resolvers/array/array.model.js';
|
||||
import { DiskFsType } from '@app/unraid-api/graph/resolvers/disks/disks.model.js';
|
||||
import {
|
||||
RegistrationState,
|
||||
registrationType,
|
||||
} from '@app/graphql/generated/api/types.js';
|
||||
import { ConfigErrorState } from '@app/graphql/generated/api/types.js';
|
||||
RegistrationType,
|
||||
} from '@app/unraid-api/graph/resolvers/registration/registration.model.js';
|
||||
import { ConfigErrorState } from '@app/unraid-api/graph/resolvers/vars/vars.model.js';
|
||||
|
||||
/**
|
||||
* Global vars
|
||||
@@ -128,7 +128,7 @@ export type Var = {
|
||||
/** Who the current Unraid key is registered to. */
|
||||
regTo: string;
|
||||
/** Which type of key this is. */
|
||||
regTy: registrationType;
|
||||
regTy: RegistrationType;
|
||||
/** Is the server currently in safe mode. */
|
||||
safeMode: boolean;
|
||||
sbClean: boolean;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ArrayState } from '@app/graphql/generated/api/types.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
|
||||
/**
|
||||
* Is the array running?
|
||||
*/
|
||||
export const arrayIsRunning = () => {
|
||||
const emhttp = getters.emhttp();
|
||||
return emhttp.var.mdState === ArrayState.STARTED;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
// Created from 'create-ts-index'
|
||||
|
||||
export * from './array-is-running.js';
|
||||
@@ -1,41 +1,76 @@
|
||||
import { got } from 'got';
|
||||
import retry from 'p-retry';
|
||||
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
import { logger } from '@app/core/log.js';
|
||||
import { type LooseObject } from '@app/core/types/index.js';
|
||||
import { DRY_RUN } from '@app/environment.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
import { appLogger } from '@app/core/log.js';
|
||||
import { LooseObject } from '@app/core/types/global.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import { loadSingleStateFile } from '@app/store/modules/emhttp.js';
|
||||
import { StateFileKey } from '@app/store/types.js';
|
||||
|
||||
/**
|
||||
* Run a command with emcmd.
|
||||
*/
|
||||
export const emcmd = async (commands: LooseObject) => {
|
||||
export const emcmd = async (
|
||||
commands: LooseObject,
|
||||
{ waitForToken = false }: { waitForToken?: boolean } = {}
|
||||
) => {
|
||||
const { getters } = await import('@app/store/index.js');
|
||||
const socketPath = getters.paths()['emhttpd-socket'];
|
||||
const { csrfToken } = getters.emhttp().var;
|
||||
|
||||
const url = `http://unix:${socketPath}:/update.htm`;
|
||||
const options = {
|
||||
qs: {
|
||||
...commands,
|
||||
csrf_token: csrfToken,
|
||||
},
|
||||
};
|
||||
|
||||
if (DRY_RUN) {
|
||||
logger.debug(url, options);
|
||||
|
||||
// Ensure we only log on dry-run
|
||||
return;
|
||||
if (!socketPath) {
|
||||
appLogger.error('No emhttpd socket path found');
|
||||
throw new AppError('No emhttpd socket path found');
|
||||
}
|
||||
return got
|
||||
.get(url, {
|
||||
enableUnixSockets: true,
|
||||
searchParams: { ...commands, csrf_token: csrfToken },
|
||||
})
|
||||
.catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new AppError('emhttpd socket unavailable.');
|
||||
|
||||
let { csrfToken } = getters.emhttp().var;
|
||||
|
||||
if (!csrfToken && waitForToken) {
|
||||
csrfToken = await retry(
|
||||
async (retries) => {
|
||||
if (retries > 1) {
|
||||
appLogger.info('Waiting for CSRF token...');
|
||||
}
|
||||
const loadedState = await store.dispatch(loadSingleStateFile(StateFileKey.var)).unwrap();
|
||||
|
||||
let token: string | undefined;
|
||||
if (loadedState && 'var' in loadedState) {
|
||||
token = loadedState.var.csrfToken;
|
||||
}
|
||||
if (!token) {
|
||||
throw new Error('CSRF token not found yet');
|
||||
}
|
||||
return token;
|
||||
},
|
||||
{
|
||||
minTimeout: 5000,
|
||||
maxTimeout: 10000,
|
||||
retries: 10,
|
||||
}
|
||||
throw error;
|
||||
).catch((error) => {
|
||||
appLogger.error('Failed to load CSRF token after multiple retries', error);
|
||||
throw new AppError('Failed to load CSRF token after multiple retries');
|
||||
});
|
||||
}
|
||||
|
||||
appLogger.debug(`Executing emcmd with commands: ${JSON.stringify(commands)}`);
|
||||
|
||||
try {
|
||||
const paramsObj = { ...commands, csrf_token: csrfToken };
|
||||
const params = new URLSearchParams(paramsObj);
|
||||
const response = await got.get(`http://unix:${socketPath}:/update.htm`, {
|
||||
enableUnixSockets: true,
|
||||
searchParams: params,
|
||||
});
|
||||
|
||||
appLogger.debug('emcmd executed successfully');
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
appLogger.error('emhttpd socket unavailable.', error);
|
||||
throw new Error('emhttpd socket unavailable.');
|
||||
}
|
||||
appLogger.error(`emcmd execution failed: ${error.message}`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Created from 'create-ts-index'
|
||||
|
||||
export * from './array/index.js';
|
||||
export * from './clients/index.js';
|
||||
export * from './plugins/index.js';
|
||||
export * from './shares/index.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DiskShare, Share, UserShare } from '@app/core/types/states/share.js';
|
||||
import type { ArrayDisk } from '@app/graphql/generated/api/types.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
import { ArrayDisk } from '@app/unraid-api/graph/resolvers/array/array.model.js';
|
||||
|
||||
const processors = {
|
||||
user(share: Share) {
|
||||
|
||||
21
api/src/core/utils/validation/is-gui-mode.ts
Normal file
21
api/src/core/utils/validation/is-gui-mode.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { execa } from 'execa';
|
||||
|
||||
import { internalLogger } from '@app/core/log.js';
|
||||
|
||||
/**
|
||||
* Check if Unraid is in GUI mode by looking for the slim process.
|
||||
* @returns true if Unraid is in GUI mode, false otherwise.
|
||||
*/
|
||||
const isGuiMode = async (): Promise<boolean> => {
|
||||
try {
|
||||
// Use pgrep to check if slim process is running
|
||||
const { exitCode } = await execa('pgrep', ['slim'], { reject: false });
|
||||
// exitCode 0 means process was found, 1 means not found
|
||||
return exitCode === 0;
|
||||
} catch (error) {
|
||||
internalLogger.error('Error checking GUI mode: %s', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default isGuiMode;
|
||||
@@ -1,57 +0,0 @@
|
||||
import { constants } from 'fs';
|
||||
import { access } from 'fs/promises';
|
||||
|
||||
import { type Hypervisor as HypervisorType } from '@unraid/libvirt';
|
||||
|
||||
import { libvirtLogger } from '@app/core/log.js';
|
||||
|
||||
const uri = process.env.LIBVIRT_URI ?? 'qemu:///system';
|
||||
|
||||
const libvirtPid = '/var/run/libvirt/libvirtd.pid';
|
||||
|
||||
const isLibvirtRunning = async (): Promise<boolean> => {
|
||||
try {
|
||||
await access(libvirtPid, constants.F_OK | constants.R_OK);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export class UnraidHypervisor {
|
||||
private static instance: UnraidHypervisor | null = null;
|
||||
private hypervisor: HypervisorType | null = null;
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): UnraidHypervisor {
|
||||
if (this.instance === null) {
|
||||
this.instance = new UnraidHypervisor();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
public async getHypervisor(): Promise<HypervisorType | null> {
|
||||
// Return hypervisor if it's already connected
|
||||
const running = await isLibvirtRunning();
|
||||
|
||||
if (this.hypervisor && running) {
|
||||
return this.hypervisor;
|
||||
}
|
||||
|
||||
if (!running) {
|
||||
this.hypervisor = null;
|
||||
throw new Error('Libvirt is not running');
|
||||
}
|
||||
const { Hypervisor } = await import('@unraid/libvirt');
|
||||
this.hypervisor = new Hypervisor({ uri });
|
||||
await this.hypervisor.connectOpen().catch((error: unknown) => {
|
||||
libvirtLogger.error(
|
||||
`Failed starting VM hypervisor connection with "${(error as Error).message}"`
|
||||
);
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
return this.hypervisor;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
// Created from 'create-ts-index'
|
||||
|
||||
export * from './filter-devices.js';
|
||||
export * from './get-hypervisor.js';
|
||||
export * from './get-pci-devices.js';
|
||||
export * from './parse-domain.js';
|
||||
export * from './parse-domains.js';
|
||||
export * from './system-network-interfaces.js';
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { type Domain } from '@app/core/types/index.js';
|
||||
|
||||
export type DomainLookupType = 'id' | 'uuid' | 'name';
|
||||
|
||||
/**
|
||||
* Parse domain
|
||||
*
|
||||
* @param type What lookup type to use.
|
||||
* @param id The domain's ID, UUID or name.
|
||||
* @private
|
||||
*/
|
||||
export const parseDomain = async (type: DomainLookupType, id: string): Promise<Domain> => {
|
||||
const types = {
|
||||
id: 'lookupDomainByIdAsync',
|
||||
uuid: 'lookupDomainByUUIDAsync',
|
||||
name: 'lookupDomainByNameAsync',
|
||||
};
|
||||
|
||||
if (!type || !Object.keys(types).includes(type)) {
|
||||
throw new Error(`Type must be one of [${Object.keys(types).join(', ')}], ${type} given.`);
|
||||
}
|
||||
|
||||
const { UnraidHypervisor } = await import('@app/core/utils/vms/get-hypervisor.js');
|
||||
const client = await UnraidHypervisor.getInstance().getHypervisor();
|
||||
const method = types[type];
|
||||
const domain = await client[method](id);
|
||||
const info = await domain.getInfoAsync();
|
||||
|
||||
const [uuid, osType, autostart, maxMemory, schedulerType, schedulerParameters, securityLabel, name] =
|
||||
await Promise.all([
|
||||
domain.getUUIDAsync(),
|
||||
domain.getOSTypeAsync(),
|
||||
domain.getAutostartAsync(),
|
||||
domain.getMaxMemoryAsync(),
|
||||
domain.getSchedulerTypeAsync(),
|
||||
domain.getSchedulerParametersAsync(),
|
||||
domain.getSecurityLabelAsync(),
|
||||
domain.getNameAsync(),
|
||||
]);
|
||||
|
||||
const results = {
|
||||
uuid,
|
||||
osType,
|
||||
autostart,
|
||||
maxMemory,
|
||||
schedulerType,
|
||||
schedulerParameters,
|
||||
securityLabel,
|
||||
name,
|
||||
...info,
|
||||
state: info.state.replace(' ', '_'),
|
||||
};
|
||||
|
||||
if (info.state === 'running') {
|
||||
results.vcpus = await domain.getVcpusAsync();
|
||||
results.memoryStats = await domain.getMemoryStatsAsync();
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { DomainLookupType } from '@app/core/utils/vms/parse-domain.js';
|
||||
import { type Domain } from '@app/core/types/index.js';
|
||||
import { parseDomain } from '@app/core/utils/vms/parse-domain.js';
|
||||
|
||||
/**
|
||||
* Parse domains.
|
||||
*/
|
||||
export const parseDomains = async (type: DomainLookupType, domains: string[]): Promise<Domain[]> =>
|
||||
Promise.all(domains.map(async (domain) => parseDomain(type, domain)));
|
||||
@@ -1,3 +0,0 @@
|
||||
import { networkInterfaces } from 'systeminformation';
|
||||
|
||||
export const systemNetworkInterfaces = networkInterfaces();
|
||||
0
api/src/customization/activation-code.dto.ts
Normal file
0
api/src/customization/activation-code.dto.ts
Normal file
@@ -5,40 +5,49 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
import type { PackageJson, SetRequired } from 'type-fest';
|
||||
|
||||
import { fileExistsSync } from '@app/core/utils/files/file-exists.js';
|
||||
|
||||
/**
|
||||
* Tries to get the package.json at the given location.
|
||||
* @param location - The location of the package.json file, relative to the current file
|
||||
* @returns The package.json object or undefined if unable to read
|
||||
* Returns the absolute path to the given file.
|
||||
* @param location - The location of the file, relative to the current file
|
||||
* @returns The absolute path to the file
|
||||
*/
|
||||
function readPackageJson(location: string): PackageJson | undefined {
|
||||
function getAbsolutePath(location: string): string {
|
||||
try {
|
||||
let packageJsonPath: string;
|
||||
try {
|
||||
const packageJsonUrl = import.meta.resolve(location);
|
||||
packageJsonPath = fileURLToPath(packageJsonUrl);
|
||||
} catch {
|
||||
// Fallback (e.g. for local development): resolve the path relative to this module
|
||||
packageJsonPath = fileURLToPath(new URL(location, import.meta.url));
|
||||
}
|
||||
const packageJsonRaw = readFileSync(packageJsonPath, 'utf-8');
|
||||
return JSON.parse(packageJsonRaw) as PackageJson;
|
||||
const fileUrl = import.meta.resolve(location);
|
||||
return fileURLToPath(fileUrl);
|
||||
} catch {
|
||||
return undefined;
|
||||
return fileURLToPath(new URL(location, import.meta.url));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the path to the api's package.json file. Throws if unable to find.
|
||||
* @param possiblePaths - The possible locations of the package.json file, relative to the current file
|
||||
* @returns The absolute path to the package.json file
|
||||
*/
|
||||
export function getPackageJsonPath(possiblePaths = ['../package.json', '../../package.json']): string {
|
||||
for (const location of possiblePaths) {
|
||||
const packageJsonPath = getAbsolutePath(location);
|
||||
if (fileExistsSync(packageJsonPath)) {
|
||||
return packageJsonPath;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Could not find package.json in any of the expected locations: ${possiblePaths.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Unraid API package.json. Throws if unable to find.
|
||||
* Retrieves the Unraid API package.json. Throws if unable to find or parse.
|
||||
* This should be considered a fatal error.
|
||||
*
|
||||
* @param pathOverride - The path to the package.json file. If not provided, the default path will be found & used.
|
||||
* @returns The package.json object
|
||||
*/
|
||||
export const getPackageJson = () => {
|
||||
const packageJson = readPackageJson('../package.json') || readPackageJson('../../package.json');
|
||||
if (!packageJson) {
|
||||
throw new Error('Could not find package.json in any of the expected locations');
|
||||
}
|
||||
return packageJson as SetRequired<PackageJson, 'version' | 'dependencies'>;
|
||||
export const getPackageJson = (pathOverride?: string) => {
|
||||
const packageJsonPath = pathOverride ?? getPackageJsonPath();
|
||||
const packageJsonRaw = readFileSync(packageJsonPath, 'utf-8');
|
||||
return JSON.parse(packageJsonRaw) as SetRequired<PackageJson, 'version' | 'dependencies'>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -69,7 +78,6 @@ export const ENVIRONMENT = process.env.ENVIRONMENT
|
||||
: 'production';
|
||||
export const GRAPHQL_INTROSPECTION = Boolean(INTROSPECTION ?? DEBUG ?? ENVIRONMENT !== 'production');
|
||||
export const PORT = process.env.PORT ?? '/var/run/unraid-api.sock';
|
||||
export const DRY_RUN = process.env.DRY_RUN === 'true';
|
||||
export const BYPASS_PERMISSION_CHECKS = process.env.BYPASS_PERMISSION_CHECKS === 'true';
|
||||
export const BYPASS_CORS_CHECKS = process.env.BYPASS_CORS_CHECKS === 'true';
|
||||
export const LOG_CORS = process.env.LOG_CORS === 'true';
|
||||
@@ -86,3 +94,4 @@ export const MOTHERSHIP_GRAPHQL_LINK = process.env.MOTHERSHIP_GRAPHQL_LINK
|
||||
: 'https://mothership.unraid.net/ws';
|
||||
|
||||
export const PM2_HOME = process.env.PM2_HOME ?? join(homedir(), '.pm2');
|
||||
export const PATHS_CONFIG_MODULES = process.env.PATHS_CONFIG_MODULES!;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,14 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
|
||||
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
|
||||
*/
|
||||
type Documents = {
|
||||
"\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": typeof types.sendRemoteGraphQLResponseDocument,
|
||||
"\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": typeof types.RemoteGraphQLEventFragmentFragmentDoc,
|
||||
"\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": typeof types.eventsDocument,
|
||||
"\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": typeof types.SendRemoteGraphQlResponseDocument,
|
||||
"\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": typeof types.RemoteGraphQlEventFragmentFragmentDoc,
|
||||
"\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": typeof types.EventsDocument,
|
||||
};
|
||||
const documents: Documents = {
|
||||
"\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": types.sendRemoteGraphQLResponseDocument,
|
||||
"\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": types.RemoteGraphQLEventFragmentFragmentDoc,
|
||||
"\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": types.eventsDocument,
|
||||
"\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": types.SendRemoteGraphQlResponseDocument,
|
||||
"\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": types.RemoteGraphQlEventFragmentFragmentDoc,
|
||||
"\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": types.EventsDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,14 +35,14 @@ export type AccessUrl = {
|
||||
ipv4?: Maybe<Scalars['URL']['output']>;
|
||||
ipv6?: Maybe<Scalars['URL']['output']>;
|
||||
name?: Maybe<Scalars['String']['output']>;
|
||||
type: URL_TYPE;
|
||||
type: UrlType;
|
||||
};
|
||||
|
||||
export type AccessUrlInput = {
|
||||
ipv4?: InputMaybe<Scalars['URL']['input']>;
|
||||
ipv6?: InputMaybe<Scalars['URL']['input']>;
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
type: URL_TYPE;
|
||||
type: UrlType;
|
||||
};
|
||||
|
||||
export type ArrayCapacity = {
|
||||
@@ -305,7 +305,7 @@ export type DashboardVmsInput = {
|
||||
started: Scalars['Int']['input'];
|
||||
};
|
||||
|
||||
export type Event = ClientConnectedEvent | ClientDisconnectedEvent | ClientPingEvent | RemoteAccessEvent | RemoteGraphQLEvent | UpdateEvent;
|
||||
export type Event = ClientConnectedEvent | ClientDisconnectedEvent | ClientPingEvent | RemoteAccessEvent | RemoteGraphQlEvent | UpdateEvent;
|
||||
|
||||
export enum EventType {
|
||||
CLIENT_CONNECTED_EVENT = 'CLIENT_CONNECTED_EVENT',
|
||||
@@ -373,32 +373,32 @@ export type Mutation = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationremoteGraphQLResponseArgs = {
|
||||
input: RemoteGraphQLServerInput;
|
||||
export type MutationRemoteGraphQlResponseArgs = {
|
||||
input: RemoteGraphQlServerInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationremoteMutationArgs = {
|
||||
input: RemoteGraphQLClientInput;
|
||||
export type MutationRemoteMutationArgs = {
|
||||
input: RemoteGraphQlClientInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationremoteSessionArgs = {
|
||||
export type MutationRemoteSessionArgs = {
|
||||
remoteAccess: RemoteAccessInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationsendNotificationArgs = {
|
||||
export type MutationSendNotificationArgs = {
|
||||
notification: NotificationInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationupdateDashboardArgs = {
|
||||
export type MutationUpdateDashboardArgs = {
|
||||
data: DashboardInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationupdateNetworkArgs = {
|
||||
export type MutationUpdateNetworkArgs = {
|
||||
data: NetworkInput;
|
||||
};
|
||||
|
||||
@@ -474,17 +474,17 @@ export type Query = {
|
||||
};
|
||||
|
||||
|
||||
export type QuerydashboardArgs = {
|
||||
export type QueryDashboardArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryremoteQueryArgs = {
|
||||
input: RemoteGraphQLClientInput;
|
||||
export type QueryRemoteQueryArgs = {
|
||||
input: RemoteGraphQlClientInput;
|
||||
};
|
||||
|
||||
|
||||
export type QueryserverStatusArgs = {
|
||||
export type QueryServerStatusArgs = {
|
||||
apiKey: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
@@ -557,7 +557,7 @@ export type RemoteAccessInput = {
|
||||
url?: InputMaybe<AccessUrlInput>;
|
||||
};
|
||||
|
||||
export type RemoteGraphQLClientInput = {
|
||||
export type RemoteGraphQlClientInput = {
|
||||
apiKey: Scalars['String']['input'];
|
||||
body: Scalars['String']['input'];
|
||||
/** Time in milliseconds to wait for a response from the remote server (defaults to 15000) */
|
||||
@@ -566,34 +566,34 @@ export type RemoteGraphQLClientInput = {
|
||||
ttl?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type RemoteGraphQLEvent = {
|
||||
export type RemoteGraphQlEvent = {
|
||||
__typename?: 'RemoteGraphQLEvent';
|
||||
data: RemoteGraphQLEventData;
|
||||
data: RemoteGraphQlEventData;
|
||||
type: EventType;
|
||||
};
|
||||
|
||||
export type RemoteGraphQLEventData = {
|
||||
export type RemoteGraphQlEventData = {
|
||||
__typename?: 'RemoteGraphQLEventData';
|
||||
/** Contains mutation / subscription / query data in the form of body: JSON, variables: JSON */
|
||||
body: Scalars['String']['output'];
|
||||
/** sha256 hash of the body */
|
||||
sha256: Scalars['String']['output'];
|
||||
type: RemoteGraphQLEventType;
|
||||
type: RemoteGraphQlEventType;
|
||||
};
|
||||
|
||||
export enum RemoteGraphQLEventType {
|
||||
export enum RemoteGraphQlEventType {
|
||||
REMOTE_MUTATION_EVENT = 'REMOTE_MUTATION_EVENT',
|
||||
REMOTE_QUERY_EVENT = 'REMOTE_QUERY_EVENT',
|
||||
REMOTE_SUBSCRIPTION_EVENT = 'REMOTE_SUBSCRIPTION_EVENT',
|
||||
REMOTE_SUBSCRIPTION_EVENT_PING = 'REMOTE_SUBSCRIPTION_EVENT_PING'
|
||||
}
|
||||
|
||||
export type RemoteGraphQLServerInput = {
|
||||
export type RemoteGraphQlServerInput = {
|
||||
/** Body - contains an object containing data: (GQL response data) or errors: (GQL Errors) */
|
||||
body: Scalars['String']['input'];
|
||||
/** sha256 hash of the body */
|
||||
sha256: Scalars['String']['input'];
|
||||
type: RemoteGraphQLEventType;
|
||||
type: RemoteGraphQlEventType;
|
||||
};
|
||||
|
||||
export type Server = {
|
||||
@@ -654,8 +654,8 @@ export type Subscription = {
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionremoteSubscriptionArgs = {
|
||||
input: RemoteGraphQLClientInput;
|
||||
export type SubscriptionRemoteSubscriptionArgs = {
|
||||
input: RemoteGraphQlClientInput;
|
||||
};
|
||||
|
||||
export type TwoFactorLocal = {
|
||||
@@ -681,7 +681,7 @@ export type TwoFactorWithoutToken = {
|
||||
remote?: Maybe<TwoFactorRemote>;
|
||||
};
|
||||
|
||||
export enum URL_TYPE {
|
||||
export enum UrlType {
|
||||
DEFAULT = 'DEFAULT',
|
||||
LAN = 'LAN',
|
||||
MDNS = 'MDNS',
|
||||
@@ -726,23 +726,23 @@ export type Vars = {
|
||||
regTy?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type sendRemoteGraphQLResponseMutationVariables = Exact<{
|
||||
input: RemoteGraphQLServerInput;
|
||||
export type SendRemoteGraphQlResponseMutationVariables = Exact<{
|
||||
input: RemoteGraphQlServerInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type sendRemoteGraphQLResponseMutation = { __typename?: 'Mutation', remoteGraphQLResponse: boolean };
|
||||
export type SendRemoteGraphQlResponseMutation = { __typename?: 'Mutation', remoteGraphQLResponse: boolean };
|
||||
|
||||
export type RemoteGraphQLEventFragmentFragment = { __typename?: 'RemoteGraphQLEvent', remoteGraphQLEventData: { __typename?: 'RemoteGraphQLEventData', type: RemoteGraphQLEventType, body: string, sha256: string } } & { ' $fragmentName'?: 'RemoteGraphQLEventFragmentFragment' };
|
||||
export type RemoteGraphQlEventFragmentFragment = { __typename?: 'RemoteGraphQLEvent', remoteGraphQLEventData: { __typename?: 'RemoteGraphQLEventData', type: RemoteGraphQlEventType, body: string, sha256: string } } & { ' $fragmentName'?: 'RemoteGraphQlEventFragmentFragment' };
|
||||
|
||||
export type eventsSubscriptionVariables = Exact<{ [key: string]: never; }>;
|
||||
export type EventsSubscriptionVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type eventsSubscription = { __typename?: 'Subscription', events?: Array<{ __typename: 'ClientConnectedEvent', connectedEvent: EventType, connectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientDisconnectedEvent', disconnectedEvent: EventType, disconnectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientPingEvent' } | { __typename: 'RemoteAccessEvent' } | (
|
||||
export type EventsSubscription = { __typename?: 'Subscription', events?: Array<{ __typename: 'ClientConnectedEvent', connectedEvent: EventType, connectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientDisconnectedEvent', disconnectedEvent: EventType, disconnectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientPingEvent' } | { __typename: 'RemoteAccessEvent' } | (
|
||||
{ __typename: 'RemoteGraphQLEvent' }
|
||||
& { ' $fragmentRefs'?: { 'RemoteGraphQLEventFragmentFragment': RemoteGraphQLEventFragmentFragment } }
|
||||
& { ' $fragmentRefs'?: { 'RemoteGraphQlEventFragmentFragment': RemoteGraphQlEventFragmentFragment } }
|
||||
) | { __typename: 'UpdateEvent' }> | null };
|
||||
|
||||
export const RemoteGraphQLEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode<RemoteGraphQLEventFragmentFragment, unknown>;
|
||||
export const sendRemoteGraphQLResponseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendRemoteGraphQLResponse"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteGraphQLResponse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<sendRemoteGraphQLResponseMutation, sendRemoteGraphQLResponseMutationVariables>;
|
||||
export const eventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientConnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"connectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"connectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientDisconnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"disconnectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"disconnectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode<eventsSubscription, eventsSubscriptionVariables>;
|
||||
export const RemoteGraphQlEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode<RemoteGraphQlEventFragmentFragment, unknown>;
|
||||
export const SendRemoteGraphQlResponseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendRemoteGraphQLResponse"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteGraphQLResponse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<SendRemoteGraphQlResponseMutation, SendRemoteGraphQlResponseMutationVariables>;
|
||||
export const EventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientConnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"connectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"connectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientDisconnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"disconnectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"disconnectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode<EventsSubscription, EventsSubscriptionVariables>;
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable */
|
||||
import { z } from 'zod'
|
||||
import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQLClientInput, RemoteGraphQLEventType, RemoteGraphQLServerInput, ServerStatus, URL_TYPE, UpdateType } from '@app/graphql/generated/client/graphql.js'
|
||||
import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQlClientInput, RemoteGraphQlEventType, RemoteGraphQlServerInput, ServerStatus, UrlType, UpdateType } from '@app/graphql/generated/client/graphql.js'
|
||||
|
||||
type Properties<T> = Required<{
|
||||
[K in keyof T]: z.ZodType<T[K], any, T[K]>;
|
||||
@@ -28,11 +28,11 @@ export const RegistrationStateSchema = z.nativeEnum(RegistrationState);
|
||||
|
||||
export const RemoteAccessEventActionTypeSchema = z.nativeEnum(RemoteAccessEventActionType);
|
||||
|
||||
export const RemoteGraphQLEventTypeSchema = z.nativeEnum(RemoteGraphQLEventType);
|
||||
export const RemoteGraphQlEventTypeSchema = z.nativeEnum(RemoteGraphQlEventType);
|
||||
|
||||
export const ServerStatusSchema = z.nativeEnum(ServerStatus);
|
||||
|
||||
export const URL_TYPESchema = z.nativeEnum(URL_TYPE);
|
||||
export const UrlTypeSchema = z.nativeEnum(UrlType);
|
||||
|
||||
export const UpdateTypeSchema = z.nativeEnum(UpdateType);
|
||||
|
||||
@@ -41,7 +41,7 @@ export function AccessUrlInputSchema(): z.ZodObject<Properties<AccessUrlInput>>
|
||||
ipv4: z.instanceof(URL).nullish(),
|
||||
ipv6: z.instanceof(URL).nullish(),
|
||||
name: z.string().nullish(),
|
||||
type: URL_TYPESchema
|
||||
type: UrlTypeSchema
|
||||
})
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ export function RemoteAccessInputSchema(): z.ZodObject<Properties<RemoteAccessIn
|
||||
})
|
||||
}
|
||||
|
||||
export function RemoteGraphQLClientInputSchema(): z.ZodObject<Properties<RemoteGraphQLClientInput>> {
|
||||
export function RemoteGraphQlClientInputSchema(): z.ZodObject<Properties<RemoteGraphQlClientInput>> {
|
||||
return z.object({
|
||||
apiKey: z.string(),
|
||||
body: z.string(),
|
||||
@@ -207,10 +207,10 @@ export function RemoteGraphQLClientInputSchema(): z.ZodObject<Properties<RemoteG
|
||||
})
|
||||
}
|
||||
|
||||
export function RemoteGraphQLServerInputSchema(): z.ZodObject<Properties<RemoteGraphQLServerInput>> {
|
||||
export function RemoteGraphQlServerInputSchema(): z.ZodObject<Properties<RemoteGraphQlServerInput>> {
|
||||
return z.object({
|
||||
body: z.string(),
|
||||
sha256: z.string(),
|
||||
type: RemoteGraphQLEventTypeSchema
|
||||
type: RemoteGraphQlEventTypeSchema
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user