diff --git a/src/@types/pocketbase-types.ts b/src/@types/pocketbase-types.ts index eeb1f3b..892020e 100644 --- a/src/@types/pocketbase-types.ts +++ b/src/@types/pocketbase-types.ts @@ -3,6 +3,7 @@ export enum Collections { Profiles = 'profiles', Tunes = 'tunes', + IniFiles = 'iniFiles', } export type ProfilesRecord = { @@ -37,3 +38,9 @@ export type TunesRecord = { logFiles?: string[]; toothLogFiles?: string[]; } + +export type IniFilesRecord = { + signature: string; + file: string; + ecosystem: string; +} diff --git a/src/hooks/useDb.ts b/src/hooks/useDb.ts index 291ef52..6b2fc5a 100644 --- a/src/hooks/useDb.ts +++ b/src/hooks/useDb.ts @@ -4,6 +4,7 @@ import { formatError, } from '../pocketbase'; import { + IniFilesRecordFull, TunesRecordFull, TunesRecordPartial, } from '../types/dbData'; @@ -55,6 +56,21 @@ const useDb = () => { } }; + const getIni = async (signature: string) => { + try { + const tune = await client.records.getList(Collections.IniFiles, 1, 1, { + filter: `signature = "${signature}"`, + }); + + return Promise.resolve(tune.totalItems > 0 ? tune.items[0] as IniFilesRecordFull : null); + } catch (error) { + Sentry.captureException(error); + databaseGenericError(new Error(formatError(error))); + + return Promise.reject(error); + } + }; + const searchTunes = async (search?: string) => { // TODO: add pagination const batchSide = 100; @@ -84,6 +100,7 @@ const useDb = () => { updateTune: (tuneId: string, data: TunesRecordPartial): Promise => updateTune(tuneId, data), createTune: (data: TunesRecord): Promise => createTune(data), getTune: (tuneId: string): Promise => getTune(tuneId), + getIni: (tuneId: string): Promise => getIni(tuneId), searchTunes: (search?: string): Promise => searchTunes(search), }; }; diff --git a/src/hooks/useServerStorage.ts b/src/hooks/useServerStorage.ts index 76eec3e..3343637 100644 --- a/src/hooks/useServerStorage.ts +++ b/src/hooks/useServerStorage.ts @@ -1,63 +1,44 @@ -import { notification } from 'antd'; import Pako from 'pako'; import * as Sentry from '@sentry/browser'; import { fetchEnv } from '../utils/env'; import { API_URL } from '../pocketbase'; import { Collections } from '../@types/pocketbase-types'; +import useDb from './useDb'; -const PUBLIC_PATH = 'public'; -const INI_PATH = `${PUBLIC_PATH}/ini`; export const CDN_URL = fetchEnv('VITE_CDN_URL'); -const fetchFromServer = async (path: string): Promise => { - const response = await fetch(`${CDN_URL}/${path}`); - return Promise.resolve(response.arrayBuffer()); -}; - -const fetchFileFromServer = async (recordId: string, filename: string, inflate = true): Promise => { - const response = await fetch(`${API_URL}/api/files/${Collections.Tunes}/${recordId}/${filename}`); - - if (inflate) { - return Pako.inflate(new Uint8Array(await response.arrayBuffer())); - } - - return response.arrayBuffer(); -}; - const useServerStorage = () => { - const getINIFile = async (signature: string) => { - const { version, baseVersion } = /.+?(?(?\d+)(-\w+)*)/.exec(signature)?.groups || { version: null, baseVersion: null }; + const { getIni } = useDb(); - try { - return Pako.inflate(new Uint8Array(await fetchFromServer(`${INI_PATH}/${version}.ini.gz`))); - } catch (error) { + // TODO: use built in pocketbase function + const buildFileUrl = (collection: Collections, recordId: string, filename: string) => `${API_URL}/api/files/${collection}/${recordId}/${filename}`; + + const fetchTuneFile = async (recordId: string, filename: string): Promise => { + const response = await fetch(buildFileUrl(Collections.Tunes, recordId, filename)); + + return Pako.inflate(new Uint8Array(await response.arrayBuffer())); + }; + + const fetchINIFile = async (signature: string): Promise => { + // const { version, baseVersion } = /.+?(?(?\d+)(-\w+)*)/.exec(signature)?.groups || { version: null, baseVersion: null }; + const ini = await getIni(signature); + + if (!ini) { + const msg = `Signature: "${signature}" not supported!`; + const error = new Error(msg); Sentry.captureException(error); - console.error(error); - - notification.warning({ - message: 'INI not found', - description: `INI version: "${version}" not found. Trying base version: "${baseVersion}"!`, - }); - - try { - return fetchFromServer(`${INI_PATH}/${baseVersion}.ini.gz`); - } catch (err) { - Sentry.captureException(err); - console.error(err); - - notification.error({ - message: 'INI not found', - description: `INI version: "${baseVersion}" not found. Try uploading custom INI file!`, - }); - } return Promise.reject(error); } + + const response = await fetch(buildFileUrl(Collections.IniFiles, ini.id, ini.file)); + + return Pako.inflate(new Uint8Array(await response.arrayBuffer())); }; return { - getINIFile: (signature: string): Promise => getINIFile(signature), - fetchFileFromServer: (recordId: string, filename: string): Promise => fetchFileFromServer(recordId, filename), + fetchTuneFile: (recordId: string, filename: string): Promise => fetchTuneFile(recordId, filename), + fetchINIFile: (signature: string): Promise => fetchINIFile(signature), }; }; diff --git a/src/pages/Upload.tsx b/src/pages/Upload.tsx index 7116f13..181d2ed 100644 --- a/src/pages/Upload.tsx +++ b/src/pages/Upload.tsx @@ -132,10 +132,10 @@ const UploadPage = () => { const shareSupported = 'share' in navigator; const { currentUser, refreshUser } = useAuth(); const navigate = useNavigate(); - const { fetchFileFromServer } = useServerStorage(); + const { fetchTuneFile } = useServerStorage(); const { createTune, updateTune, getTune } = useDb(); - const fetchFile = async (tuneId: string, fileName: string) => bufferToFile(await fetchFileFromServer(tuneId, fileName), fileName); + const fetchFile = async (tuneId: string, fileName: string) => bufferToFile(await fetchTuneFile(tuneId, fileName), fileName); const noop = () => { }; diff --git a/src/pages/auth/notifications.ts b/src/pages/auth/notifications.ts index 2a2b484..64455f2 100644 --- a/src/pages/auth/notifications.ts +++ b/src/pages/auth/notifications.ts @@ -119,6 +119,12 @@ const databaseGenericError = (err: Error) => notification.error({ ...baseOptions, }); +const iniLoadingError = (err: Error) => notification.error({ + message: 'INI not found', + description: err.message, + ...baseOptions, +}); + const copiedToClipboard = () => notification.success({ message: 'Copied to clipboard', ...baseOptions, @@ -145,4 +151,5 @@ export { passwordUpdateFailed, databaseGenericError, copiedToClipboard, + iniLoadingError, }; diff --git a/src/types/dbData.ts b/src/types/dbData.ts index e72de98..38c3b48 100644 --- a/src/types/dbData.ts +++ b/src/types/dbData.ts @@ -1,5 +1,6 @@ import { Record } from 'pocketbase'; import { + IniFilesRecord, ProfilesRecord, TunesRecord, } from '../@types/pocketbase-types'; @@ -13,3 +14,5 @@ export type TunesRecordPartial = Partial; export interface TunesRecordFull extends TunesRecord, Record { } export interface ProfilesRecordFull extends ProfilesRecord, Record { } + +export interface IniFilesRecordFull extends IniFilesRecord, Record { } diff --git a/src/utils/api.ts b/src/utils/api.ts index f6e2a11..88dc23f 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -11,6 +11,7 @@ import { import TuneParser from './tune/TuneParser'; import useServerStorage, { CDN_URL } from '../hooks/useServerStorage'; import { TunesRecordFull } from '../types/dbData'; +import { iniLoadingError } from '../pages/auth/notifications'; // TODO: refactor this!! export const loadTune = async (tuneData: TunesRecordFull | null) => { @@ -21,10 +22,10 @@ export const loadTune = async (tuneData: TunesRecordFull | null) => { } // eslint-disable-next-line react-hooks/rules-of-hooks - const { getINIFile, fetchFileFromServer } = useServerStorage(); + const { fetchINIFile, fetchTuneFile } = useServerStorage(); const started = new Date(); - const tuneRaw = await fetchFileFromServer(tuneData.id, tuneData.tuneFile); + const tuneRaw = await fetchTuneFile(tuneData.id, tuneData.tuneFile); const tuneParser = new TuneParser().parse(tuneRaw); @@ -36,26 +37,30 @@ export const loadTune = async (tuneData: TunesRecordFull | null) => { } const tune = tuneParser.getTune(); - const iniRaw = tuneData.customIniFile ? fetchFileFromServer(tuneData.id, tuneData.customIniFile) : getINIFile(tuneData.signature); - const config = new INI(await iniRaw).parse().getResults(); + try { + const iniRaw = tuneData.customIniFile ? fetchTuneFile(tuneData.id, tuneData.customIniFile) : fetchINIFile(tuneData.signature); + const config = new INI(await iniRaw).parse().getResults(); - // override / merge standard dialogs, constants and help - config.dialogs = { - ...config.dialogs, - ...stdDialogs, - }; - config.help = { - ...config.help, - ...help, - }; - config.constants.pages[0].data.divider = divider; + // override / merge standard dialogs, constants and help + config.dialogs = { + ...config.dialogs, + ...stdDialogs, + }; + config.help = { + ...config.help, + ...help, + }; + config.constants.pages[0].data.divider = divider; - const loadingTimeInfo = `Tune loaded in ${(new Date().getTime() - started.getTime())}ms`; - console.info(loadingTimeInfo); + const loadingTimeInfo = `Tune loaded in ${(new Date().getTime() - started.getTime())}ms`; + console.info(loadingTimeInfo); - store.dispatch({ type: 'config/load', payload: config }); - store.dispatch({ type: 'tune/load', payload: tune }); - store.dispatch({ type: 'status', payload: loadingTimeInfo }); + store.dispatch({ type: 'config/load', payload: config }); + store.dispatch({ type: 'tune/load', payload: tune }); + store.dispatch({ type: 'status', payload: loadingTimeInfo }); + } catch (error) { + iniLoadingError((error as Error)); + } }; export const loadLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>