diff --git a/web/store/updateOs.ts b/web/store/updateOs.ts index 7ae843212..2aab82a9f 100644 --- a/web/store/updateOs.ts +++ b/web/store/updateOs.ts @@ -1,6 +1,10 @@ +import testOsReleasesResponse from '~/_data/osReleases'; // test data + import { BellAlertIcon } from '@heroicons/vue/24/solid'; import { defineStore, createPinia, setActivePinia } from 'pinia'; import gt from 'semver/functions/gt'; +import coerce from 'semver/functions/coerce'; +import type { SemVer } from 'semver'; import useInstallPlugin from '~/composables/installPlugin'; import { request } from '~/composables/services/request'; @@ -9,7 +13,7 @@ import { useCallbackStore } from '~/store/callbackActions'; import { useErrorsStore } from '~/store/errors'; import { useServerStore } from '~/store/server'; import type { InstallPluginPayload } from '~/composables/installPlugin'; -import type { OsRelease } from '~/store/callback'; +import type { OsRelease, OsReleasesResponse } from '~/store/callback'; import type { ServerStateDataAction } from '~/types/server'; /** @@ -18,6 +22,11 @@ import type { ServerStateDataAction } from '~/types/server'; */ setActivePinia(createPinia()); +export interface CachedOsReleasesResponse { + timestamp: number; + response: OsReleasesResponse; +} + export const useUpdateOsStore = defineStore('updateOs', () => { const callbackStore = useCallbackStore(); const errorsStore = useErrorsStore(); @@ -27,33 +36,94 @@ export const useUpdateOsStore = defineStore('updateOs', () => { // State const status = ref<'confirming' | 'failed' | 'ready' | 'success' | 'updating' | 'downgrading'>('ready'); + const releasesJson = ref(localStorage.getItem('releasesJson') ? JSON.parse(localStorage.getItem('releasesJson') ?? '') : undefined); const callbackUpdateRelease = ref(); // used when coming back from callback, this will be the release to install const updateAvailable = ref(); // used locally to show update action button const downgradeAvailable = ref(false); // Getters - const currentOsVersion = computed((): string => serverStore?.osVersion); + const currentOsVersion = computed(() => serverStore?.osVersion); + const isOsVersionStable = computed(() => serverStore?.isOsVersionStable); // used to determine if we should look for stable or next releases + // const currentOsVersionNext = computed((): boolean => serverStore?.osVersionNext); // Actions - const checkForOsUpdate = async () => { - console.debug('[checkForOsUpdate]'); - + const fetchOsReleases = async () => { + try { + // const response: OsReleasesResponse = await request.url(OS_RELEASES.toString()).get().json(); + const response = testOsReleasesResponse; + releasesJson.value = { + timestamp: Date.now(), + response, + }; + localStorage.setItem('releasesJson', JSON.stringify(releasesJson.value)); + } catch (error) { + console.error('[fetchOsReleases]', error); + } + }; + const purgeReleasesJsonCache = () => { + releasesJson.value = undefined; + localStorage.removeItem('releasesJson'); + }; + const checkForOsUpdate = async (skipCache: boolean = false, includeNext: boolean = false) => { if (!currentOsVersion.value) { return console.error('[checkForOsUpdate] currentOsVersion not found, skipping OS update check'); } - const response: OsRelease[] = await request.url(OS_RELEASES.toString()).get().json(); - console.debug('[checkForOsUpdate] response', response); + if (skipCache) { // forces new check + purgeReleasesJsonCache(); + } - if (response) { - response.forEach(release => { - const releaseVersion = release.name.replace('Unraid ', ''); - console.debug('[checkForOsUpdate] releaseVersion', releaseVersion); - if (gt(releaseVersion, '6.12.3')) { // currentOsVersion.value - updateAvailable.value = release; - return; // stop looping, we found an update + try { + /** + * Compare the timestamp of the cached data to the current time, + * if it's older than 7 days, reset releasesJson. + * Which will trigger a new API call to get the releases. + * Otherwise skip the API call and use the cached data. + */ + if (releasesJson.value) { + const currentTime = new Date().getTime(); + const localState = releasesJson.value; + const cacheDuration = import.meta.env.DEV ? 30000 : 604800000; // 30 seconds for testing, 7 days for prod + if (currentTime - localState.timestamp > cacheDuration) { + purgeReleasesJsonCache(); + await fetchOsReleases(); } - }); + } else { + await fetchOsReleases(); + } + + if (releasesJson.value && releasesJson.value.response) { + /** + * If we're on stable and the user hasn't requested to include next releases in the check + * then remove next releases from the cached data + */ + if (!includeNext && isOsVersionStable.value && releasesJson.value.response.next) { + delete releasesJson.value.response.next; + } + + Object.keys(releasesJson.value.response ?? {}).forEach(key => { + if (!releasesJson.value) { // this is just to make TS happy (it's already checked above…thanks github copilot for knowing what I needed) + return; + } + + if (updateAvailable.value) { + return; + } + + const releases = releasesJson.value.response[key as keyof OsReleasesResponse]; + + if (releases && releases.length > 0) { + releases.find(release => { + if (gt(release.version, currentOsVersion.value)) { /** @todo '6.12.0' temporary for dev. Replace with currentOsVersion.value */ + updateAvailable.value = release; + return true; // stop looping, we found an update + } + }); + } + }); + } + } catch (error) { + console.error('[checkForOsUpdate]', error); } }; @@ -78,15 +148,32 @@ export const useUpdateOsStore = defineStore('updateOs', () => { text: 'Unraid OS Update Available', } }); + /** + * @description When receiving the callback the Account update page we'll use the provided releaseMd5 to find the release in the releasesJson cache. + */ + const confirmUpdateOs = async (releaseMd5: string) => { + /** this should never happen, but if it does we should probably try to fetch the releases again */ + if (!releasesJson.value) { + await fetchOsReleases(); + }; + + Object.keys(releasesJson.value?.response ?? {}).forEach(key => { + const releases = releasesJson.value?.response[key as keyof OsReleasesResponse]; + + if (releases && releases.length > 0) { + releases.forEach(release => { + if (release.md5 === releaseMd5) { + callbackUpdateRelease.value = release; + return; + } + }); + } + }) - const confirmUpdateOs = (payload: OsRelease) => { - console.debug('[confirmUpdateOs]'); - callbackUpdateRelease.value = payload; setStatus('confirming'); }; const installOsUpdate = () => { - console.debug('[installOsUpdate]', callbackUpdateRelease.value); if (!callbackUpdateRelease.value) { return console.error('[installOsUpdate] release not found'); } @@ -100,7 +187,6 @@ export const useUpdateOsStore = defineStore('updateOs', () => { }; const downgradeOs = async () => { - console.debug('[downgradeOs]'); setStatus('downgrading'); };