feat: update os ignore release

This commit is contained in:
Zack Spear
2024-01-26 18:35:14 -08:00
committed by Zack Spear
parent e976daf8b0
commit 3e051815c5
12 changed files with 336 additions and 29 deletions

View File

@@ -48,6 +48,7 @@ class ServerState
private $caseModel = '';
private $keyfileBase64UrlSafe = '';
private $updateOsResponse;
private $updateOsIgnoredReleases = [];
public $myServersFlashCfg = [];
public $myServersMemoryCfg = [];
@@ -168,6 +169,11 @@ class ServerState
* updateOsResponse is provided by the dynamix.plugin.manager/scripts/unraidcheck script saving to /tmp/unraidcheck/result.json
*/
$this->updateOsResponse = @json_decode(@file_get_contents('/tmp/unraidcheck/result.json'), true);
/**
* updateOsIgnoredReleases is set by the dynamix.plugin.manager/inclue/UnraidIgnore.php script saving to /tmp/unraidcheck/ignored.json
*/
$this->updateOsIgnoredReleases = @json_decode(@file_get_contents('/tmp/unraidcheck/ignored.json'), true) ?? [];
}
/**
@@ -253,6 +259,10 @@ class ServerState
$serverState['updateOsResponse'] = $this->updateOsResponse;
}
if ($this->updateOsIgnoredReleases) {
$serverState['updateOsIgnoredReleases'] = $this->updateOsIgnoredReleases;
}
return $serverState;
}

View File

@@ -88,10 +88,23 @@ if (array_key_exists('json',$_GET) && $_GET['json']) {
exit(0);
}
// before sending a notification, check to see if the user requested to ignore the version
$ignoredReleasesFile = '/tmp/unraidcheck/ignored.json';
$ignoredReleasesResult = [];
if (file_exists($ignoredReleasesFile)) {
$ignoredData = json_decode(file_get_contents($ignoredReleasesFile), true);
if (is_array($ignoredData) && array_key_exists('updateOsIgnoredReleases', $ignoredData)) {
$ignoredReleasesResult = $ignoredData['updateOsIgnoredReleases'];
}
}
// send notification if a newer version is available
if ($json && array_key_exists('isNewer',$json) && $json['isNewer']) {
$isNewerVersion = array_key_exists('isNewer',$json) ? $json['isNewer'] : false;
$isReleaseIgnored = in_array($json['version'], $ignoredReleasesResult);
if ($json && $isNewerVersion && !$isReleaseIgnored) {
$newver = (array_key_exists('version',$json) && $json['version']) ? $json['version'] : 'unknown';
exec("$script -e ".escapeshellarg("System - Unraid [$newver]")." -s ".escapeshellarg("Notice [$server] - Version update $newver")." -d ".escapeshellarg("A new version of Unraid is available")." -i ".escapeshellarg("normal $output")." -l '/Tools/Update' -x");
}
exit(0);
?>
?>

View File

@@ -0,0 +1,143 @@
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
/**
* @param removeAll {boolean} - if true, will remove all versions from the JSON file
* @param removeVersion {string} - the version of the OS release we want to remove
* @param version {string} - the version of the OS release we want to ignore
*/
$json_file_key = 'updateOsIgnoredReleases';
$json_file = '/tmp/unraidcheck/ignored.json';
function isValidSemVerFormat($version) {
return preg_match('/^\d+\.\d+(\.\d+)?(-.+)?$/',$version);
}
// Ensure that the request is a GET request
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Read the JSON data from the request body
// $json_data = file_get_contents('php://input');
$json_data = $_GET;
if (empty($json_data)) {
http_response_code(400); // Bad Request
echo "No JSON data found";
return;
}
$data = $json_data;
if ($data !== null) {
// Check if the "removeAll" key exists in the $data array
if (isset($data['removeAll']) && $data['removeAll'] === true) {
// Check if the JSON file exists
if (file_exists($json_file)) {
// Delete the JSON file
unlink($json_file);
// return empty array to represent no ignored versions
header('Content-Type: application/json');
echo json_encode([$json_file_key => []], JSON_PRETTY_PRINT);
} else {
http_response_code(400); // Bad Request
echo "No JSON file found";
}
}
// Check if the "removeVersion" key exists in the $data array
else if (isset($data['removeVersion'])) {
// Check if the "removeVersion" value is a valid PHP-standardized version number string
$remove_version = $data['removeVersion'];
if (isValidSemVerFormat($remove_version)) {
// Check if the JSON file exists
if (file_exists($json_file)) {
// If the file exists, read its content
$existing_data = json_decode(file_get_contents($json_file), true);
// Check if key exists
if (isset($existing_data[$json_file_key])) {
// Remove the specified version from the array
$existing_data[$json_file_key] = array_diff($existing_data[$json_file_key], [$remove_version]);
// Save the updated data to the JSON file
file_put_contents($json_file, json_encode($existing_data, JSON_PRETTY_PRINT));
http_response_code(200); // OK
header('Content-Type: application/json');
echo json_encode($existing_data, JSON_PRETTY_PRINT);
} else {
http_response_code(400); // Bad Request
echo "No versions to remove in the JSON file";
}
} else {
http_response_code(400); // Bad Request
echo "No JSON file found";
}
} else {
http_response_code(400); // Bad Request
echo "Invalid removeVersion format";
}
}
// Check if the "version" key exists in the $data array
else if (isset($data['version'])) {
// Check if the "version" value is a valid PHP-standardized version number string
$version = $data['version'];
if (isValidSemVerFormat($version)) {
// Prepare the new data structure
$new_data = [$json_file_key => [$version]];
// Check if the JSON file already exists
if (file_exists($json_file)) {
// If the file exists, read its content
$existing_data = json_decode(file_get_contents($json_file), true);
// Check if key already exists
if (isset($existing_data[$json_file_key])) {
// Append the new version to the existing array
$existing_data[$json_file_key][] = $version;
} else {
// If key doesn't exist, create it
$existing_data[$json_file_key] = [$version];
}
// Update the data to be saved
$new_data = $existing_data;
}
// Save the data to the JSON file
file_put_contents($json_file, json_encode($new_data, JSON_PRETTY_PRINT));
http_response_code(200); // OK
header('Content-Type: application/json');
echo json_encode($new_data, JSON_PRETTY_PRINT);
} else {
http_response_code(400); // Bad Request
echo "Invalid version format";
}
} else {
http_response_code(400); // Bad Request
echo "Invalid param data";
}
} else {
http_response_code(400); // Bad Request
echo "Error decoding JSON data";
}
} else {
// Handle non-GET requests
http_response_code(405); // Method Not Allowed
echo "Only GET requests are allowed";
}

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
export interface Props {
label?: string;
// propChecked?: boolean;
}
withDefaults(defineProps<Props>(), {
label: '',
// propChecked: false,
});
const checked = ref(false);
</script>
<template>
<SwitchGroup>
<div class="flex items-center gap-8px p-8px rounded">
<Switch
v-model="checked"
:class="checked ? 'bg-gradient-to-r from-unraid-red to-orange' : 'bg-transparent'"
class="relative inline-flex h-24px w-[48px] items-center rounded-full overflow-hidden"
>
<span v-show="!checked" class="absolute z-0 inset-0 opacity-10 bg-beta" />
<span
:class="checked ? 'translate-x-[26px]' : 'translate-x-[2px]'"
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
/>
</Switch>
<SwitchLabel>{{ label }}</SwitchLabel>
</div>
</SwitchGroup>
</template>

View File

@@ -7,7 +7,6 @@ import { useAccountStore } from '~/store/account';
import { usePurchaseStore } from '~/store/purchase';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
// import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
import type { ButtonProps } from '~/types/ui/button';
@@ -24,10 +23,9 @@ const accountStore = useAccountStore();
const purchaseStore = usePurchaseStore();
const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
// const updateOsActionsStore = useUpdateOsActionsStore();
const updateOsChangelogStore = useUpdateOsChangelogStore();
const { osVersionBranch, updateOsResponse } = storeToRefs(serverStore);
const { osVersionBranch, updateOsResponse, updateOsIgnoredReleases } = storeToRefs(serverStore);
const { available, availableWithRenewal, checkForUpdatesLoading } = storeToRefs(updateOsStore);
interface ModalCopy {
@@ -109,13 +107,13 @@ const close = () => {
// then ignore the release if applicable
if (ignoreThisRelease.value && (availableWithRenewal.value || available.value)) {
setTimeout(() => {
updateOsStore.ignoreRelease(availableWithRenewal.value ?? available.value ?? '');
serverStore.updateOsIgnoreRelease(availableWithRenewal.value ?? available.value ?? '');
}, 500);
}
};
const renderMainSlot = computed(() => {
return checkForUpdatesLoading.value || available.value || availableWithRenewal.value;
return checkForUpdatesLoading.value || available.value || availableWithRenewal.value || updateOsIgnoredReleases.value.length > 0;
});
</script>
@@ -145,10 +143,21 @@ const renderMainSlot = computed(() => {
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
/>
</Switch>
<SwitchLabel>Ignore this release</SwitchLabel>
<SwitchLabel>{{ t('Ignore this release') }}</SwitchLabel>
</div>
</SwitchGroup>
</div>
<div v-else-if="updateOsIgnoredReleases.length > 0" class="w-full flex flex-col gap-8px my-24px">
<h3 class="text-16px font-semibold italic">
{{ t('Ignored Releases') }}
</h3>
<UpdateOsIgnoredRelease
v-for="ignoredRelease in updateOsIgnoredReleases"
:key="ignoredRelease"
:label="ignoredRelease"
:t="t"
/>
</div>
</template>
<template v-if="!checkForUpdatesLoading" #footer>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import { XMarkIcon } from '@heroicons/vue/24/solid';
import { useServerStore } from '~/store/server';
import { useThemeStore } from '~/store/theme';
export interface Props {
label: string;
t: any;
}
withDefaults(defineProps<Props>(), {
label: '',
});
const serverStore = useServerStore();
const { darkMode } = storeToRefs(useThemeStore());
const evenBgColor = computed(() => {
return darkMode.value ? 'even:bg-grey-darkest' : 'even:bg-black/5';
});
</script>
<template>
<div
class="text-16px p-12px flex flex-row gap-4px sm:px-20px sm:gap-16px items-center justify-between rounded"
:class="evenBgColor"
>
<span class="font-semibold">{{ label }}</span>
<BrandButton
:btn-style="'underline'"
:icon-right="XMarkIcon"
:text="t('Remove')"
:title="t('Remove from ignore list')"
@click="serverStore.updateOsRemoveIgnoredRelease(label)"
/>
</div>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { useServerStore } from '~/store/server';
const serverStore = useServerStore();
const { updateOsIgnoredReleases } = storeToRefs(serverStore);
</script>
<template>
<UpdateOsIgnoredRelease
v-for="ignoredRelease in updateOsIgnoredReleases"
:key="ignoredRelease"
:label="ignoredRelease"
/>
</template>

View File

@@ -118,3 +118,30 @@ export const WebguiCheckForUpdate = async (): Promise<ServerUpdateOsResponse | u
throw new Error('Error checking for updates');
}
};
export interface WebguiUpdateIgnorePayload {
removeAll?: boolean;
removeVersion?: string;
version?: string;
}
export const WebguiUpdateIgnore = async (payload: WebguiUpdateIgnorePayload): Promise<any | void> => {
console.debug('[WebguiUpdateIgnore] payload', payload);
try {
const response = await request
.url('/plugins/dynamix.plugin.manager/include/UnraidIgnore.php')
.query(payload)
.get()
.json((json) => {
console.debug('[WebguiUpdateIgnore] response', json);
return json;
})
.catch((error) => {
console.error('[WebguiUpdateIgnore] catch failed to execute UpdateIgnore', error);
throw new Error('Error ignoring update');
});
return response;
} catch (error) {
console.error('[WebguiUpdateIgnore] catch failed to execute UpdateIgnore', error);
throw new Error('Error ignoring update');
}
};

View File

@@ -344,5 +344,8 @@
"Extend Key to Update": "Extend Key to Update",
"Install Unraid OS {0}": "Install Unraid OS {0}",
"View Changelog to Start Update": "View Changelog to Start Update",
"Unraid OS {0} Update Available": "Unraid OS {0} Update Available"
"Unraid OS {0} Update Available": "Unraid OS {0} Update Available",
"Remove": "Remove",
"Remove from ignore list": "Remove from ignore list",
"Ignored Releases": "Ignored Releases"
}

View File

@@ -16,7 +16,7 @@ import { useQuery } from '@vue/apollo-composable';
import { SERVER_CLOUD_FRAGMENT, SERVER_STATE_QUERY } from './server.fragment';
import { useFragment } from '~/composables/gql/fragment-masking';
import { WebguiState } from '~/composables/services/webgui';
import { WebguiState, WebguiUpdateIgnore } from '~/composables/services/webgui';
import { WEBGUI_SETTINGS_MANAGMENT_ACCESS } from '~/helpers/urls';
import { useAccountStore } from '~/store/account';
import { useErrorsStore, type Error } from '~/store/errors';
@@ -117,6 +117,7 @@ export const useServerStore = defineStore('server', () => {
if (newVal) { themeStore.setTheme(newVal); }
});
const updateOsResponse = ref<ServerUpdateOsResponse>();
const updateOsIgnoredReleases = ref<string[]>([]);
const uptime = ref<number>(0);
const username = ref<string>(''); // @todo potentially move to a user store
const wanFQDN = ref<string>('');
@@ -822,6 +823,7 @@ export const useServerStore = defineStore('server', () => {
if (typeof data?.state !== 'undefined') { state.value = data.state; }
if (typeof data?.theme !== 'undefined') { theme.value = data.theme; }
if (typeof data?.updateOsResponse !== 'undefined') { updateOsResponse.value = data.updateOsResponse; }
if (typeof data?.updateOsIgnoredReleases !== 'undefined') { updateOsIgnoredReleases.value = data.updateOsIgnoredReleases; }
if (typeof data?.uptime !== 'undefined') { uptime.value = data.uptime; }
if (typeof data?.username !== 'undefined') { username.value = data.username; }
if (typeof data?.wanFQDN !== 'undefined') { wanFQDN.value = data.wanFQDN; }
@@ -965,6 +967,31 @@ export const useServerStore = defineStore('server', () => {
}
});
const updateOsIgnoreRelease = (release: string) => {
updateOsIgnoredReleases.value.push(release);
const response = WebguiUpdateIgnore({
version: release,
});
console.debug('[updateOsIgnoreRelease] response', response);
/** @todo when update check modal is displayed and there's no available updates, allow users to remove ignored releases from the list */
};
const updateOsRemoveIgnoredRelease = (release: string) => {
updateOsIgnoredReleases.value = updateOsIgnoredReleases.value.filter(r => r !== release);
const response = WebguiUpdateIgnore({
removeVersion: release,
});
console.debug('[updateOsRemoveIgnoredRelease] response', response);
};
const updateOsRemoveAllIgnoredReleases = () => {
updateOsIgnoredReleases.value = [];
const response = WebguiUpdateIgnore({
removeAll: true,
});
console.debug('[updateOsRemoveAllIgnoredReleases] response', response);
};
return {
// state
apiKey,
@@ -1003,6 +1030,7 @@ export const useServerStore = defineStore('server', () => {
site,
state,
theme,
updateOsIgnoredReleases,
updateOsResponse,
uptime,
username,
@@ -1030,5 +1058,8 @@ export const useServerStore = defineStore('server', () => {
refreshServerState,
filteredKeyActions,
setRebootVersion,
updateOsIgnoreRelease,
updateOsRemoveIgnoredRelease,
updateOsRemoveAllIgnoredReleases,
};
});

View File

@@ -17,31 +17,24 @@ setActivePinia(createPinia());
extend(customParseFormat);
extend(relativeTime);
const KEY_IGNORED_RELEASES = 'updateOsIgnoredReleases';
export const useUpdateOsStore = defineStore('updateOs', () => {
// state
const checkForUpdatesLoading = ref<boolean>(false);
const modalOpen = ref<boolean>(false);
const ignoredReleases = ref<string[]>(
localStorage.getItem(KEY_IGNORED_RELEASES)
? (JSON.parse(localStorage.getItem(KEY_IGNORED_RELEASES) ?? '') ?? [])
: []
);
// getters from other stores
const serverStore = useServerStore();
const regExp = computed(() => serverStore.regExp);
const regUpdatesExpired = computed(() => serverStore.regUpdatesExpired);
const updateOsResponse = computed(() => serverStore.updateOsResponse);
const updateOsIgnoredReleases = computed(() => serverStore.updateOsIgnoredReleases);
// local getters
const available = computed(() => {
if (!updateOsResponse.value) {
return undefined;
}
// ignore any releases that are in the ignoredReleases array
if (ignoredReleases.value.includes(updateOsResponse.value.version)) {
// ignore any releases that are in the updateOsIgnoredReleases array
if (updateOsIgnoredReleases.value.includes(updateOsResponse.value.version)) {
return undefined;
}
return updateOsResponse.value.isNewer ? updateOsResponse.value.version : undefined;
@@ -83,25 +76,17 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
modalOpen.value = val;
};
const ignoreRelease = (release: string) => {
ignoredReleases.value.push(release);
localStorage.setItem(KEY_IGNORED_RELEASES, JSON.stringify(ignoredReleases.value));
/** @todo submit to an endpoint on the server to save to a file */
/** @todo when update check modal is displayed and there's no available updates, allow users to remove ignored releases from the list */
};
return {
// state
available,
availableWithRenewal,
checkForUpdatesLoading,
modalOpen,
ignoredReleases,
updateOsIgnoredReleases,
// getters
availableReleaseDate,
// actions
localCheckForUpdate,
setModalOpen,
ignoreRelease,
};
});

View File

@@ -101,6 +101,7 @@ export interface Server {
state?: ServerState;
theme?: Theme | undefined;
updateOsResponse?: ServerUpdateOsResponse;
updateOsIgnoredReleases?: string[];
uptime?: number;
username?: string;
wanFQDN?: string;