├── src ├── boot │ ├── .gitkeep │ ├── plugins.js │ ├── i18n.js │ ├── components.js │ └── axios.js ├── store │ ├── module-example │ │ ├── getters.js │ │ ├── actions.js │ │ ├── mutations.js │ │ ├── state.js │ │ └── index.js │ ├── pathify.js │ ├── store-flag.d.ts │ ├── modules │ │ ├── server.js │ │ └── app.js │ └── index.js ├── i18n │ ├── index.js │ └── en-US │ │ └── index.js ├── router │ ├── routes.js │ └── index.js ├── components │ ├── index.js │ ├── Logs.vue │ ├── EssentialLink.vue │ ├── TitleBar.vue │ ├── Files.vue │ ├── HBOperations.vue │ ├── ServerBinariesDownload.vue │ └── Config.vue ├── pages │ ├── ErrorNotFound.vue │ └── IndexPage.vue ├── css │ ├── quasar.variables.scss │ └── app.scss ├── plugins │ ├── util.js │ └── hb.js ├── index.template.html ├── layouts │ ├── MainDrawer.vue │ └── MainLayout.vue ├── assets │ └── quasar-logo-vertical.svg └── App.vue ├── public ├── logo.png ├── store.db ├── hbcdn.png ├── icon0.png ├── favicon.ico ├── hbglobal.png ├── store.clean.db ├── icons │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ └── favicon-128x128.png ├── settings.ini └── kofi.svg ├── src-electron ├── icons │ ├── icon.ico │ ├── icon.png │ └── icon.icns ├── electron-flag.d.ts ├── src │ ├── ipcMain-Server.js │ ├── ipcMain-FTP.js │ ├── ipcMain.js │ ├── ipcMain-ServerBinaryDownload.js │ ├── db.js │ ├── hb.js │ ├── ftp.js │ ├── server.js │ └── baseImage.js ├── electron-preload.js └── electron-main.js ├── .vscode ├── settings.json └── extensions.json ├── .editorconfig ├── .postcssrc.js ├── babel.config.js ├── .gitignore ├── jsconfig.json ├── package.json ├── README.md └── quasar.config.js /src/boot/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/module-example/getters.js: -------------------------------------------------------------------------------- 1 | export function someGetter (/* state */) { 2 | } 3 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/store.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/store.db -------------------------------------------------------------------------------- /src/store/module-example/actions.js: -------------------------------------------------------------------------------- 1 | export function someAction (/* context */) { 2 | } 3 | -------------------------------------------------------------------------------- /src/store/module-example/mutations.js: -------------------------------------------------------------------------------- 1 | export function someMutation (/* state */) { 2 | } 3 | -------------------------------------------------------------------------------- /public/hbcdn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/hbcdn.png -------------------------------------------------------------------------------- /public/icon0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/icon0.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/hbglobal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/hbglobal.png -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import enUS from './en-US' 2 | 3 | export default { 4 | 'en-US': enUS 5 | } 6 | -------------------------------------------------------------------------------- /public/store.clean.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/store.clean.db -------------------------------------------------------------------------------- /src/store/module-example/state.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return { 3 | // 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src-electron/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/src-electron/icons/icon.ico -------------------------------------------------------------------------------- /src-electron/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/src-electron/icons/icon.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "editor.guides.bracketPairs": true 4 | } -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /src-electron/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/src-electron/icons/icon.icns -------------------------------------------------------------------------------- /public/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gkiokan/hb-store-cdn-server/HEAD/public/icons/favicon-128x128.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/store/pathify.js: -------------------------------------------------------------------------------- 1 | // "vuex-pathify": "^1.5.1" 2 | import pathify from 'vuex-pathify' 3 | export default pathify 4 | 5 | // options 6 | pathify.options.mapping = 'simple' 7 | pathify.options.deep = 2 8 | -------------------------------------------------------------------------------- /src/i18n/en-US/index.js: -------------------------------------------------------------------------------- 1 | // This is just an example, 2 | // so you can safely delete all default props below 3 | 4 | export default { 5 | failed: 'Action failed', 6 | success: 'Action was successful' 7 | } 8 | -------------------------------------------------------------------------------- /src/boot/plugins.js: -------------------------------------------------------------------------------- 1 | import util from '~/plugins/util' 2 | import hb from '~/plugins/hb' 3 | 4 | export default ({ app }) => { 5 | app.config.globalProperties.$hb = hb 6 | app.config.globalProperties.$util = util 7 | } 8 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // https://github.com/michael-ciniawsky/postcss-load-config 3 | 4 | module.exports = { 5 | plugins: [ 6 | // to edit target browsers: use "browserslist" field in package.json 7 | require('autoprefixer') 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /public/settings.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | CDN=http://192.168.43.93:6448 3 | Secure_Boot=1 4 | temppath=/user/app/NPXS39041/downloads 5 | TTF_Font=/system_ex/app/NPXS20113/bdjstack/lib/fonts/SCE-PS3-RD-R-LATIN.TTF 6 | StoreOnUSB=0 7 | Show_install_prog=0 8 | HomeMenu_Redirection=0 9 | Daemon_on_start=1 10 | Legacy=0 11 | -------------------------------------------------------------------------------- /src/store/module-example/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import * as mutations from './mutations' 4 | import * as actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | getters, 9 | mutations, 10 | actions, 11 | state 12 | } 13 | -------------------------------------------------------------------------------- /src/boot/i18n.js: -------------------------------------------------------------------------------- 1 | import { boot } from 'quasar/wrappers' 2 | import { createI18n } from 'vue-i18n' 3 | import messages from 'src/i18n' 4 | 5 | export default boot(({ app }) => { 6 | const i18n = createI18n({ 7 | locale: 'en-US', 8 | messages 9 | }) 10 | 11 | // Set i18n instance on app 12 | app.use(i18n) 13 | }) 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = api => { 4 | return { 5 | presets: [ 6 | [ 7 | '@quasar/babel-preset-app', 8 | api.caller(caller => caller && caller.target === 'node') 9 | ? { targets: { node: 'current' } } 10 | : {} 11 | ] 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/store/store-flag.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED, 3 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING 4 | import "quasar/dist/types/feature-flag"; 5 | 6 | declare module "quasar/dist/types/feature-flag" { 7 | interface QuasarFeatureFlags { 8 | store: true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "johnsoncodehk.volar", 5 | "wayou.vscode-todo-highlight" 6 | ], 7 | "unwantedRecommendations": [ 8 | "octref.vetur", 9 | "hookyqr.beautify", 10 | "dbaeumer.jshint", 11 | "ms-vscode.vscode-typescript-tslint-plugin" 12 | ] 13 | } -------------------------------------------------------------------------------- /src-electron/electron-flag.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED, 3 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING 4 | import "quasar/dist/types/feature-flag"; 5 | 6 | declare module "quasar/dist/types/feature-flag" { 7 | interface QuasarFeatureFlags { 8 | electron: true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | 2 | const routes = [ 3 | { 4 | path: '/', 5 | component: () => import('layouts/MainLayout.vue'), 6 | children: [ 7 | { path: '', component: () => import('pages/IndexPage.vue') } 8 | ] 9 | }, 10 | 11 | // Always leave this as last one, 12 | // but you can also remove it 13 | { 14 | path: '/:catchAll(.*)*', 15 | component: () => import('pages/ErrorNotFound.vue') 16 | } 17 | ] 18 | 19 | export default routes 20 | -------------------------------------------------------------------------------- /src-electron/src/ipcMain-Server.js: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | import server from './server' 3 | 4 | ipcMain.handle('server-start', async(event, config) => { 5 | server.start(JSON.parse(config)) 6 | }) 7 | 8 | ipcMain.handle('server-restart', async(event, config) => { 9 | server.restart(JSON.parse(config)) 10 | }) 11 | 12 | ipcMain.handle('server-stop', async(event, config) => { 13 | server.stop() 14 | }) 15 | 16 | ipcMain.handle('server-scan', async(event, config) => { 17 | server.rescanFolder(JSON.parse(config)) 18 | }) 19 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Autoload all current vue files as component and register them by their name. 3 | --- 4 | Author: Gkiokan Sali 5 | Date: 2019-05-09 6 | */ 7 | 8 | import Vue from 'vue' 9 | 10 | const requireContext = require.context('./', false, /.*\.vue$/) 11 | const layouts = requireContext.keys() 12 | .map(file => 13 | [file.replace(/(^.\/)|(\.vue$)/g, ''), requireContext(file)] 14 | ) 15 | .reduce((components, [name, component]) => { 16 | let Component = component.default || component 17 | Vue.component(Component.name, Component) 18 | }, {}) 19 | -------------------------------------------------------------------------------- /src/components/Logs.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | 29 | 31 | -------------------------------------------------------------------------------- /src-electron/src/ipcMain-FTP.js: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron' 2 | import ftp from './ftp' 3 | 4 | ipcMain.handle('get-logs', (event, config, log) => { 5 | ftp.getLogs(JSON.parse(config), log) 6 | }) 7 | 8 | ipcMain.handle('clean-logs', (event, config) => { 9 | ftp.cleanLogs(JSON.parse(config)) 10 | }) 11 | 12 | 13 | ipcMain.handle('get-settings', async (event, config) => { 14 | await ftp.getSettings(JSON.parse(config)) 15 | }) 16 | 17 | ipcMain.handle('update-settings', async (event, config) => { 18 | // await ftp.getSettings(JSON.parse(config)) 19 | await ftp.updateSettings(JSON.parse(config)) 20 | }) 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | /dist 8 | 9 | # Cordova related directories and files 10 | /src-cordova/node_modules 11 | /src-cordova/platforms 12 | /src-cordova/plugins 13 | /src-cordova/www 14 | 15 | # Capacitor related directories and files 16 | /src-capacitor/www 17 | /src-capacitor/node_modules 18 | 19 | # BEX related directories and files 20 | /src-bex/www 21 | /src-bex/js/core 22 | 23 | # Log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Editor directories and files 29 | .idea 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | -------------------------------------------------------------------------------- /src/store/modules/server.js: -------------------------------------------------------------------------------- 1 | import { make } from 'vuex-pathify' 2 | 3 | export const state = { 4 | state: null, 5 | ip: "", 6 | port: "6448", 7 | 8 | ps4ip: "", 9 | ps4port: "2121", 10 | 11 | basePath: null, 12 | binaryVersion: "0.00", 13 | files: [], 14 | assets: [], 15 | } 16 | 17 | 18 | // make all mutations 19 | export const mutations = { 20 | ...make.mutations(state), 21 | 22 | } 23 | 24 | // actions 25 | export const actions = { 26 | ...make.actions(state), 27 | } 28 | 29 | // getters 30 | export const getters = { 31 | // make all getters (optional) 32 | ...make.getters(state), 33 | } 34 | 35 | // console.log({ 36 | // mutations, actions, getters 37 | // }) 38 | -------------------------------------------------------------------------------- /src/boot/components.js: -------------------------------------------------------------------------------- 1 | // import something here 2 | // import { boot } from 'quasar/wrappers' 3 | // import '~/components' 4 | 5 | // "async" is optional; 6 | // more info on params: https://quasar.dev/quasar-cli/boot-files 7 | 8 | 9 | export default async ( { app, store, router, Vue } ) => { 10 | 11 | const requireContext = require.context('~/components', true, /.*\.vue$/) 12 | const layouts = requireContext.keys() 13 | .map(file => 14 | [file.replace(/(^.\/)|(\.vue$)/g, ''), requireContext(file)] 15 | ) 16 | .reduce((components, [name, component]) => { 17 | let Component = component.default || component 18 | app.component(Component.name, Component) 19 | }, {}) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/ErrorNotFound.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "src/*": [ 6 | "src/*" 7 | ], 8 | "app/*": [ 9 | "*" 10 | ], 11 | "components/*": [ 12 | "src/components/*" 13 | ], 14 | "layouts/*": [ 15 | "src/layouts/*" 16 | ], 17 | "pages/*": [ 18 | "src/pages/*" 19 | ], 20 | "assets/*": [ 21 | "src/assets/*" 22 | ], 23 | "boot/*": [ 24 | "src/boot/*" 25 | ], 26 | "stores/*": [ 27 | "src/stores/*" 28 | ], 29 | "vue$": [ 30 | "node_modules/vue/dist/vue.runtime.esm-bundler.js" 31 | ] 32 | } 33 | }, 34 | "exclude": [ 35 | "dist", 36 | ".quasar", 37 | "node_modules" 38 | ] 39 | } -------------------------------------------------------------------------------- /src/pages/IndexPage.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 33 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import { make } from 'vuex-pathify' 2 | 3 | export const state = { 4 | search: '', 5 | selected: null, 6 | serverFilesVersion: false, 7 | pkgs: [], 8 | logs: [], 9 | } 10 | 11 | 12 | // make all mutations 13 | export const mutations = { 14 | ...make.mutations(state), 15 | 16 | } 17 | 18 | // actions 19 | export const actions = { 20 | ...make.actions(state), 21 | 22 | // addFiles({ commit, dispatch, state}, payload){ 23 | // commit('addFiles', payload) 24 | // } 25 | } 26 | 27 | // getters 28 | export const getters = { 29 | // make all getters (optional) 30 | ...make.getters(state), 31 | 32 | // overwrite default `items` getter 33 | // allFiles: state => { 34 | // return state.images 35 | // }, 36 | } 37 | 38 | // console.log({ 39 | // mutations, actions, getters 40 | // }) 41 | -------------------------------------------------------------------------------- /src/css/quasar.variables.scss: -------------------------------------------------------------------------------- 1 | // Quasar SCSS (& Sass) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary : #1976D2; 16 | $secondary : #26A69A; 17 | $accent : #9C27B0; 18 | 19 | $dark : #1D1D1D; 20 | 21 | $positive : #21BA45; 22 | $negative : #C10015; 23 | $info : #31CCEC; 24 | $warning : #F2C037; 25 | -------------------------------------------------------------------------------- /src/components/EssentialLink.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 50 | -------------------------------------------------------------------------------- /src/plugins/util.js: -------------------------------------------------------------------------------- 1 | let util = { 2 | getNetWorkInterfaces() { 3 | console.log('running') 4 | let os = require('os'); 5 | let ifaces = []; 6 | Object.keys(os.networkInterfaces()).forEach(function (ifname) { 7 | var alias = 0; 8 | os.networkInterfaces()[ifname].forEach(function (iface) { 9 | if ('IPv4' !== iface.family || iface.internal !== false) { 10 | return; 11 | } 12 | 13 | if (alias >= 1) { 14 | ifaces.push({ 15 | title: `${ifname}-${alias}:${iface.address}`, 16 | ip: iface.address 17 | }); 18 | } else { 19 | ifaces.push({ 20 | title: `${ifname}: ${iface.address}`, 21 | ip: iface.address 22 | }); 23 | } 24 | ++alias; 25 | }); 26 | }); 27 | return ifaces; 28 | }, 29 | } 30 | 31 | export default util 32 | -------------------------------------------------------------------------------- /src/boot/axios.js: -------------------------------------------------------------------------------- 1 | import { boot } from 'quasar/wrappers' 2 | import axios from 'axios' 3 | 4 | // Be careful when using SSR for cross-request state pollution 5 | // due to creating a Singleton instance here; 6 | // If any client changes this (global) instance, it might be a 7 | // good idea to move this instance creation inside of the 8 | // "export default () => {}" function below (which runs individually 9 | // for each client) 10 | const api = axios.create({ }) 11 | 12 | export default boot(({ app }) => { 13 | // for use inside Vue files (Options API) through this.$axios and this.$api 14 | 15 | app.config.globalProperties.$axios = axios 16 | // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form) 17 | // so you won't necessarily have to import axios in each vue file 18 | 19 | app.config.globalProperties.$api = api 20 | // ^ ^ ^ this will allow you to use this.$api (for Vue Options API form) 21 | // so you can easily perform requests against your app's API 22 | }) 23 | 24 | export { api } 25 | -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { route } from 'quasar/wrappers' 2 | import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router' 3 | import routes from './routes' 4 | 5 | /* 6 | * If not building with SSR mode, you can 7 | * directly export the Router instantiation; 8 | * 9 | * The function below can be async too; either use 10 | * async/await or return a Promise which resolves 11 | * with the Router instance. 12 | */ 13 | 14 | export default route(function (/* { store, ssrContext } */) { 15 | const createHistory = process.env.SERVER 16 | ? createMemoryHistory 17 | : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory) 18 | 19 | const Router = createRouter({ 20 | scrollBehavior: () => ({ left: 0, top: 0 }), 21 | routes, 22 | 23 | // Leave this as is and make changes in quasar.conf.js instead! 24 | // quasar.conf.js -> build -> vueRouterMode 25 | // quasar.conf.js -> build -> publicPath 26 | history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE) 27 | }) 28 | 29 | return Router 30 | }) 31 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | $offsetForTitleBar: -3px; 2 | 3 | 4 | html, body { 5 | /* Foreground, Background */ 6 | scrollbar-color: #999 #333; 7 | 8 | ::-webkit-scrollbar { 9 | width: 10px; /* Mostly for vertical scrollbars */ 10 | height: 5px; /* Mostly for horizontal scrollbars */ 11 | } 12 | 13 | ::-webkit-scrollbar-thumb { /* Foreground */ 14 | background: #2a2a2a; 15 | } 16 | ::-webkit-scrollbar-track { /* Background */ 17 | background: #1a1a1a; 18 | } 19 | 20 | } 21 | 22 | .q-panel-parent.panel-wrapper { 23 | position: absolute; 24 | top: 0px; left: 0px; right: 0px; bottom: 0px; 25 | border: 0px solid red; 26 | } 27 | 28 | .titleBar { 29 | cursor: pointer; 30 | margin-left: $offsetForTitleBar; 31 | margin-right: $offsetForTitleBar; 32 | margin-top: $offsetForTitleBar; 33 | } 34 | 35 | .draggable { 36 | -webkit-user-select: none; 37 | -webkit-app-region: drag; 38 | } 39 | 40 | .cursor-pointer { 41 | cursor: pointer !important; 42 | } 43 | 44 | .q-field--outlined.q-field--readonly .q-field__control:before { 45 | border-style: solid !important; 46 | } 47 | -------------------------------------------------------------------------------- /src-electron/src/ipcMain.js: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, app, ipcMain, dialog, shell } from 'electron' 2 | import { download } from 'electron-dl' 3 | 4 | ipcMain.handle('open-dir', async (event, path) => { 5 | return dialog.showOpenDialog({ properties: ['openDirectory'] }) 6 | }) 7 | 8 | ipcMain.handle('getNetWorkInterfaces', async (event) => { 9 | let os = require('os'); 10 | let ifaces = []; 11 | Object.keys(os.networkInterfaces()).forEach(function (ifname) { 12 | var alias = 0; 13 | os.networkInterfaces()[ifname].forEach(function (iface) { 14 | if ('IPv4' !== iface.family || iface.internal !== false) { 15 | return; 16 | } 17 | 18 | if (alias >= 1) { 19 | ifaces.push({ 20 | title: `${ifname}-${alias}:${iface.address}`, 21 | ip: iface.address 22 | }); 23 | } else { 24 | ifaces.push({ 25 | title: `${ifname}: ${iface.address}`, 26 | ip: iface.address 27 | }); 28 | } 29 | ++alias; 30 | }); 31 | }); 32 | return ifaces; 33 | }) 34 | 35 | ipcMain.handle('open-url', (e, url) => { 36 | shell.openExternal(url) 37 | }) 38 | 39 | ipcMain.handle('closeApplication', () => { 40 | app.quit() 41 | }) 42 | -------------------------------------------------------------------------------- /src/components/TitleBar.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 45 | 46 | 48 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { store } from 'quasar/wrappers' 2 | import { createStore } from 'vuex' 3 | import pathify from './pathify' 4 | 5 | // import example from './module-example' 6 | 7 | /* 8 | * If not building with SSR mode, you can 9 | * directly export the Store instantiation; 10 | * 11 | * The function below can be async too; either use 12 | * async/await or return a Promise which resolves 13 | * with the Store instance. 14 | */ 15 | 16 | const requireContext = require.context('./modules', false, /.*\.js$/) 17 | 18 | const modules = requireContext.keys() 19 | .map(file => 20 | [file.replace(/(^.\/)|(\.js$)/g, ''), requireContext(file)] 21 | ) 22 | .reduce((modules, [name, module]) => { 23 | if (module.namespaced === undefined) { 24 | module.namespaced = true 25 | } 26 | 27 | return { ...modules, [name]: module } 28 | }, {}) 29 | 30 | 31 | const Store = createStore({ 32 | plugins: [ pathify.plugin ], 33 | modules, 34 | 35 | mutations: { 36 | initialiseStore(state) { 37 | if(localStorage.getItem('store')) { 38 | let initialState = JSON.parse(JSON.stringify(state)) 39 | let restoredStore = JSON.parse(localStorage.getItem('store')) 40 | let finalStore = { ...initialState, ...restoredStore } 41 | 42 | // console.log(initialState) 43 | // console.log(finalStore) 44 | 45 | this.replaceState( 46 | Object.assign(state, finalStore) 47 | ); 48 | } 49 | } 50 | } 51 | // enable strict mode (adds overhead!) 52 | // for dev mode and --debug builds only 53 | // strict: process.env.DEBUGGING 54 | }) 55 | 56 | export default Store 57 | -------------------------------------------------------------------------------- /src/components/Files.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 57 | 58 | 65 | -------------------------------------------------------------------------------- /src/plugins/hb.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from '~/store' 3 | import semver from 'semver' 4 | 5 | const hb = { 6 | data: { 7 | source: 'https://api.github.com/repos/LightningMods/PS4-Store/releases', 8 | files: [ 9 | 'homebrew.elf', 10 | 'homebrew.elf.sig', 11 | 'remote.md5', 12 | 'store.prx', 13 | 'store.prx.sig', 14 | ], 15 | }, 16 | 17 | async getRelease(){ 18 | let { data } = await axios.get(this.data.source) 19 | return data.length ? data[0] : false 20 | }, 21 | 22 | getName(release=null){ 23 | if(!release) return "NO_RELEASE_OBJECT" 24 | return release.name 25 | }, 26 | 27 | getVersion(release=null){ 28 | if(!release) return "NO_RELEASE_OBJECT" 29 | return release.tag_name 30 | }, 31 | 32 | getAssets(release=null){ 33 | if(!release) return "NO_RELEASE_OBJECT" 34 | let assets = release.assets 35 | let urls = [] 36 | 37 | assets.map( f => { 38 | urls.push({ 39 | name: f.name, 40 | progress: 0, 41 | url: f.browser_download_url 42 | }) 43 | }) 44 | 45 | return urls 46 | }, 47 | 48 | checkVersion(currentVersion="0.00", version="0.00"){ 49 | console.log("compare ", currentVersion, version) 50 | return this.compareVersion(currentVersion, version) 51 | }, 52 | 53 | compareVersion(v1, v2) { 54 | const v1Parts = v1.split('.') 55 | const v2Parts = v2.split('.') 56 | const length = Math.max(v1Parts.length, v2Parts.length) 57 | for (let i = 0; i < length; i++) { 58 | const value = (parseInt(v1Parts[i]) || 0) - (parseInt(v2Parts[i]) || 0) 59 | if (value < 0) return -1 60 | if (value > 0) return 1 61 | } 62 | return 0 63 | }, 64 | 65 | 66 | } 67 | 68 | export default hb 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hb-store-cdn-server", 3 | "version": "1.8.0", 4 | "description": "HB-Store CDN Server", 5 | "productName": "HB-Store CDN Server", 6 | "homepage": "https://gkiokan.net", 7 | "author": "Gkiokan ", 8 | "private": true, 9 | "scripts": { 10 | "test": "echo \"No test specified\" && exit 0", 11 | "postinstall": "electron-builder install-app-deps", 12 | "dev": "quasar dev -m electron", 13 | "build:win": "quasar build -m electron --target win32", 14 | "build:linux": "quasar build -m electron --target linux", 15 | "build:mac": "quasar build -m electron --target darwin", 16 | "build:all": "quasar build -m electron --target all" 17 | }, 18 | "dependencies": { 19 | "@njzy/ps4-pkg-info": "^0.1.0", 20 | "@quasar/extras": "^1.0.0", 21 | "axios": "^0.21.1", 22 | "basic-ftp": "^4.6.6", 23 | "better-sqlite3": "^7.5.1", 24 | "config-ini-parser": "^1.5.9", 25 | "core-js": "^3.6.5", 26 | "electron-dl": "^3.3.1", 27 | "express": "^4.17.3", 28 | "extract-zip": "^2.0.1", 29 | "fast-glob": "^3.2.11", 30 | "http": "0.0.1-security", 31 | "md5-file": "^5.0.0", 32 | "node-polyfill-webpack-plugin": "^1.1.4", 33 | "normalize-path": "^3.0.0", 34 | "os": "^0.1.2", 35 | "path": "^0.12.7", 36 | "ps4-pkg-info": "^1.0.1", 37 | "quasar": "^2.6.0", 38 | "semver": "^7.3.7", 39 | "url": "^0.11.0", 40 | "vue": "^3.0.0", 41 | "vue-i18n": "^9.0.0", 42 | "vue-router": "^4.0.0", 43 | "vuex": "^4.0.1", 44 | "vuex-pathify": "^3.0.0-beta" 45 | }, 46 | "devDependencies": { 47 | "@quasar/app-webpack": "^3.0.0", 48 | "electron": "^18.0.1", 49 | "electron-builder": "^23.0.3" 50 | }, 51 | "browserslist": [ 52 | "last 10 Chrome versions", 53 | "last 10 Firefox versions", 54 | "last 4 Edge versions", 55 | "last 7 Safari versions", 56 | "last 8 Android versions", 57 | "last 8 ChromeAndroid versions", 58 | "last 8 FirefoxAndroid versions", 59 | "last 10 iOS versions", 60 | "last 5 Opera versions" 61 | ], 62 | "engines": { 63 | "node": ">= 12.22.1", 64 | "npm": ">= 6.13.4", 65 | "yarn": ">= 1.21.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src-electron/electron-preload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically for security reasons. 3 | * Here you can access Nodejs stuff and inject functionality into 4 | * the renderer thread (accessible there through the "window" object) 5 | * 6 | * WARNING! 7 | * If you import anything from node_modules, then make sure that the package is specified 8 | * in package.json > dependencies and NOT in devDependencies 9 | * 10 | * Example (injects window.myAPI.doAThing() into renderer thread): 11 | * 12 | * import { contextBridge } from 'electron' 13 | * 14 | * contextBridge.exposeInMainWorld('myAPI', { 15 | * doAThing: () => {} 16 | * }) 17 | */ 18 | 19 | import { contextBridge } from 'electron' 20 | import { ipcRenderer } from 'electron' 21 | 22 | contextBridge.exposeInMainWorld('hb', { 23 | openBasePathDialog: () => ipcRenderer.invoke('open-dir'), 24 | getNetWorkInterfaces: () => ipcRenderer.invoke('getNetWorkInterfaces'), 25 | downloadServerBinaries: (f) => ipcRenderer.invoke('download-server-binaries', f), 26 | closeApplication: () => ipcRenderer.invoke('closeApplication'), 27 | }) 28 | 29 | contextBridge.exposeInMainWorld('ipc', { 30 | on: (channel, cb) => ipcRenderer.on(channel, cb), 31 | open: (url) => ipcRenderer.invoke('open-url', url), 32 | removeListener: (channel, cb) => ipcRenderer.removeAllListeners(channel), 33 | checkServerBinaries: (ch, cb) => ipcRenderer.invoke('trigger-check-server-binaries') 34 | }) 35 | 36 | contextBridge.exposeInMainWorld('server', { 37 | start: (server) => ipcRenderer.invoke('server-start', server), 38 | restart: (server) => ipcRenderer.invoke('server-restart', server), 39 | stop: () => ipcRenderer.invoke('server-stop'), 40 | scan: (server) => ipcRenderer.invoke('server-scan', server), 41 | }) 42 | 43 | contextBridge.exposeInMainWorld('ftp', { 44 | getLogs: (server, log) => ipcRenderer.invoke('get-logs', server, log), 45 | cleanLogs: (server) => ipcRenderer.invoke('clean-logs', server), 46 | getSettings: (server) => ipcRenderer.invoke('get-settings', server), 47 | updateSettings: (server) => ipcRenderer.invoke('update-settings', server), 48 | restoreSettings: (server) => ipcRenderer.invoke('restore-settings', server), 49 | }) 50 | -------------------------------------------------------------------------------- /src-electron/src/ipcMain-ServerBinaryDownload.js: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, app, ipcMain, dialog } from 'electron' 2 | import { download } from 'electron-dl' 3 | import fs from 'fs' 4 | import path from 'path' 5 | import extract from 'extract-zip' 6 | 7 | console.assert = () => {}; 8 | 9 | ipcMain.handle('download-server-binaries', async(event, file) => { 10 | console.log("Server assets to download", file) 11 | 12 | // const win = BrowserWindow.getFocusedWindow(); 13 | let win = BrowserWindow.getFocusedWindow(); 14 | 15 | if(!win){ 16 | let all = BrowserWindow.getAllWindows() 17 | win = all[0] 18 | } 19 | 20 | const binPath = app.getPath('userData') + '/bin' 21 | 22 | if (!fs.existsSync(binPath)){ 23 | fs.mkdirSync(binPath); 24 | } 25 | 26 | console.log("Server Binary Folder", binPath) 27 | console.log("Download file", file) 28 | 29 | try { 30 | await download(win, file, { 31 | directory: binPath, 32 | overwrite: true, 33 | onProgress: o => win.webContents.send('download-complete', { file, item: o }), 34 | errorMessage: e => alert(e), 35 | }) 36 | 37 | console.log("File should be downloaded " + file) 38 | 39 | // Post downlaod process 40 | if( file.includes('.zip')){ 41 | console.log("Found a zip file to extract at " + file) 42 | 43 | let filename = path.basename(file) 44 | let filePath = binPath + '/' + filename 45 | 46 | console.log({ filename, filePath, binPath }) 47 | 48 | try { 49 | console.log("[....] Extracting " + filename) 50 | extract(filePath, { dir: binPath }) 51 | console.log("[done] Extracting " + filename) 52 | } 53 | catch(e){ 54 | console.log("Error Extracting file " + filename) 55 | } 56 | } 57 | } 58 | catch (e) { alert(e); console.error('(download)', e); } 59 | 60 | // console.log(await download(win, url)); 61 | }) 62 | 63 | 64 | ipcMain.handle('trigger-check-server-binaries', () => { 65 | let win = BrowserWindow.getFocusedWindow(); 66 | 67 | if(!win){ 68 | let all = BrowserWindow.getAllWindows() 69 | win = all[0] 70 | } 71 | 72 | win.webContents.send('check-server-binaries') 73 | }) 74 | -------------------------------------------------------------------------------- /src-electron/src/db.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import Database from 'better-sqlite3'; 5 | 6 | let db; 7 | 8 | export default { 9 | 10 | getWindow(){ 11 | let win = BrowserWindow.getFocusedWindow(); 12 | 13 | if(!win){ 14 | let all = BrowserWindow.getAllWindows() 15 | win = all[0] 16 | } 17 | 18 | return win 19 | }, 20 | 21 | error(err=null){ 22 | this.getWindow().webContents.send('error', err) 23 | this.log(err) 24 | }, 25 | 26 | log(msg=null){ 27 | this.getWindow().webContents.send('log', msg) 28 | console.log("Server:: " + msg) 29 | }, 30 | 31 | 32 | getCleanStorePath(){ 33 | return path.resolve(__dirname, process.env.QUASAR_PUBLIC_FOLDER) + '/store.clean.db' 34 | }, 35 | 36 | getStorePath(){ 37 | return app.getPath('userData') + '/bin/store.db' 38 | }, 39 | 40 | renewDB(){ 41 | let clean = this.getCleanStorePath() 42 | let store = this.getStorePath() 43 | let dir = path.dirname(store) 44 | 45 | if (!fs.existsSync(dir)) { 46 | fs.mkdirSync(dir); 47 | } 48 | 49 | try { 50 | fs.copyFileSync(clean, store) 51 | this.log("store.db has been renewed") 52 | } 53 | catch(e){ 54 | this.error(e) 55 | } 56 | }, 57 | 58 | instance(){ 59 | const db = new Database(this.getStorePath()) 60 | this.db = db 61 | 62 | return db 63 | }, 64 | 65 | addAllItems(items){ 66 | const db = this.instance() 67 | 68 | const insert = db.prepare("INSERT INTO homebrews (pid,id,name,desc,image,package,version,picpath,desc_1,desc_2,ReviewStars,Size,Author,apptype,pv,main_icon_path,main_menu_pic,releaseddate) VALUES (CAST(@pid AS INTEGER),@id,@name,@desc,@image,@package,@version,@picpath,@desc_1,@desc_2,@ReviewStars,@Size,@Author,@apptype,@pv,@main_icon_path,@main_menu_pic,@releaseddate)") 69 | 70 | const insertAll = db.transaction( items => { 71 | for (const item of items) 72 | insert.run(item) 73 | }) 74 | 75 | insertAll(items) 76 | }, 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src-electron/electron-main.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu, nativeTheme } from 'electron' 2 | import path from 'path' 3 | import os from 'os' 4 | import electronDl from 'electron-dl' 5 | 6 | // Initialise 7 | import './src/ipcMain' 8 | import './src/ipcMain-ServerBinaryDownload' 9 | import './src/ipcMain-Server' 10 | import './src/ipcMain-FTP' 11 | 12 | // Initialise config 13 | electronDl({ 14 | directory: app.getPath('userData') + '/bin', 15 | overwrite: true, 16 | }) 17 | 18 | // needed in case process is undefined under Linux 19 | const platform = process.platform || os.platform() 20 | 21 | try { 22 | if (platform === 'win32' && nativeTheme.shouldUseDarkColors === true) { 23 | require('fs').unlinkSync(path.join(app.getPath('userData'), 'DevTools Extensions')) 24 | } 25 | } 26 | catch (_) { } 27 | 28 | let mainWindow 29 | 30 | function createWindow () { 31 | /** 32 | * Initial window options 33 | */ 34 | mainWindow = new BrowserWindow({ 35 | icon: path.resolve(__dirname, 'icons/icon.png'), // tray icon 36 | width: 440, 37 | height: 600, 38 | useContentSize: true, 39 | frame: false, 40 | icon: path.join(__dirname, '/icons/icon.png'), 41 | webPreferences: { 42 | nodeIntegration: true, 43 | contextIsolation: true, 44 | enableRemoteModule: true, 45 | // More info: /quasar-cli/developing-electron-apps/electron-preload-script 46 | preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD), 47 | allowRunningInsecureContent: false, 48 | sandbox: false, 49 | } 50 | }) 51 | 52 | mainWindow.setMenu(null) 53 | mainWindow.loadURL(process.env.APP_URL) 54 | 55 | if (process.env.DEBUGGING) { 56 | // if on DEV or Production with debug enabled 57 | mainWindow.webContents.openDevTools() 58 | } 59 | else { 60 | // we're on production; no access to devtools pls 61 | mainWindow.webContents.on('devtools-opened', () => { 62 | mainWindow.webContents.closeDevTools() 63 | }) 64 | } 65 | 66 | mainWindow.on('closed', () => { 67 | mainWindow = null 68 | }) 69 | 70 | // Menu.setApplicationMenu(null) 71 | } 72 | 73 | app.whenReady().then(createWindow) 74 | 75 | app.on('window-all-closed', () => { 76 | if (platform !== 'darwin') { 77 | app.quit() 78 | } 79 | }) 80 | 81 | app.on('activate', () => { 82 | if (mainWindow === null) { 83 | createWindow() 84 | } 85 | }) 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HB-Store CDN Server 2 | [![ko-fi](https://img.shields.io/badge/Buy%20me%20a%20Shisha%20on-Ko--fi-red)](https://ko-fi.com/M4M082WK8) 3 | [![os](https://img.shields.io/badge/platform-windows%20%7C%20macos%20%7C%20linux-lightgrey)](#) 4 | [![commits_since_release](https://img.shields.io/github/commits-since/gkiokan/hb-store-cdn-server/v1.7.2)](#) 5 | [![version](https://img.shields.io/github/package-json/v/gkiokan/hb-store-cdn-server)](#) 6 | [![downloads](https://img.shields.io/github/downloads/gkiokan/hb-store-cdn-server/total)](#) 7 | [![last_commit](https://img.shields.io/github/last-commit/gkiokan/hb-store-cdn-server)](#) 8 | 9 | This is the new HB-Store CDN Tool for you. 10 | 11 | ![HB-Store CDN Server](https://user-images.githubusercontent.com/7249224/170701265-ccd48140-e497-4c15-8382-efb100d3fc1b.jpeg) 12 | 13 | 14 | ## Features 15 | - [x] Dead simple usage 16 | - [x] Custom Network Interface and Port 17 | - [x] Select base path to scan all pkgs (deep scan) 18 | - [x] Integrated node express server 19 | - [x] Maps file paths correctly, even with special character and empty spaces 20 | - [x] List of found files with preview of icon0 and information 21 | - [x] Add Button for "update my settings.ini" (update of HB-Store Settings by one click) 22 | - [x] Add Button for "Get HB-Store logs" (retrieve the logs by one click) 23 | - [x] Configuration Validation 24 | - [x] Autoscan BasePath after you change it 25 | - [x] Public Domain Host support 26 | - [x] Add PS5 Support 27 | 28 | ## ToDo 29 | - [ ] More Tweaks 30 | - [x] CLI version [HB-Store CDN Server CLI-Version available here](https://github.com/Gkiokan/hb-store-cdn-cli-server) 31 | 32 | ## How To 33 | 1.) Selct your IP and choose a Port 34 | 2.) Add your base path where your pkgs lives 35 | 3.) Check for Server Binary 36 | -- sofar this steps only required once, it will autosave the values -- 37 | 4.) Start the Server -> it will show your CDN Address below the title bar 38 | 39 | ## How to configure HB-Store 40 | 1.) Open HB-Store app on your 41 | 2.) Set the given CDN Address as your CDN 42 | 3.) Save Settings 43 | 4.) Close the HB-Store by pressing options button and open it again 44 | -- settings needs to be done once -- 45 | 5.) You should see the content from your Server now. 46 | 47 | 48 | ## Support 49 | If you want to Support me and my development, you can do this here. 50 | 51 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/M4M082WK8) 52 | -------------------------------------------------------------------------------- /src-electron/src/hb.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import pkgInfo from 'ps4-pkg-info' 3 | import path from 'path' 4 | import baseImage from './baseImage' 5 | 6 | export default { 7 | files: [], 8 | 9 | createItem(data, file, pid=1){ 10 | // let patchedFilename = item.name.replace(/[^a-zA-Z0-9-_.]/g, '') 11 | let patchedFilename = (file.charAt(0) == "/") ? file.substr(1).replace(/[^a-zA-Z0-9-_./]/g, '') : file.replace(/[^a-zA-Z0-9-_./]/g, '') 12 | let stats = fs.lstatSync(file) 13 | let size = this.formatBytes(stats.size, 2) 14 | 15 | let item = { 16 | "pid": pid, 17 | "id": data.paramSfo.TITLE_ID, 18 | "name": data.paramSfo.TITLE, 19 | "desc": "", 20 | "image": "__image", 21 | "package": "__package", 22 | "version": data.paramSfo.APP_VER, 23 | "picpath": "/user/app/NPXS39041/storedata/" + data.paramSfo.TITLE_ID + ".png", 24 | "desc_1": "", 25 | "desc_2": "", 26 | "ReviewStars": "Custom Rating", 27 | "Size": size, 28 | "Author": "HB-Store CDN", 29 | "apptype": "HB Game", 30 | "pv": "5.05+", 31 | "main_icon_path": "__image", 32 | "main_menu_pic": "/user/app/NPXS39041/storedata/" + data.paramSfo.TITLE_ID + ".png", 33 | "releaseddate": "2019-04-30", 34 | path: file, 35 | filename: path.basename(file), 36 | patchedFilename, 37 | icon0: data.icon0 ?? baseImage, 38 | } 39 | 40 | return item 41 | }, 42 | 43 | removeBasePath(item, toRemove){ 44 | item.patchedFilename = item.patchedFilename.replace(toRemove, '') 45 | 46 | if(item.patchedFilename.charAt(0) == '/') 47 | item.patchedFilename = item.patchedFilename.substr(1) 48 | 49 | return item 50 | }, 51 | 52 | getImagePathURI(data){ 53 | return data.patchedFilename + '/icon0.png' 54 | }, 55 | 56 | addImages(data=null, base){ 57 | let id = data.id 58 | let patched 59 | let image = base + '/' + this.getImagePathURI(data) 60 | 61 | data.image = image 62 | data.main_icon_path = image 63 | 64 | return data 65 | }, 66 | 67 | formatBytes(bytes, decimals=2, k=1000) { 68 | if (bytes === 0) return '0 Bytes'; 69 | 70 | const dm = decimals < 0 ? 0 : decimals; 71 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 72 | 73 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 74 | 75 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 76 | }, 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/layouts/MainDrawer.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 59 | 60 | 62 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 72 | 73 | 106 | -------------------------------------------------------------------------------- /src/components/HBOperations.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 114 | 115 | 117 | -------------------------------------------------------------------------------- /src/assets/quasar-logo-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 199 | -------------------------------------------------------------------------------- /src/components/ServerBinariesDownload.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 172 | 173 | 175 | -------------------------------------------------------------------------------- /src/components/Config.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 175 | 176 | 178 | -------------------------------------------------------------------------------- /src-electron/src/ftp.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, dialog } from 'electron' 2 | import { download } from 'electron-dl' 3 | import { Client } from 'basic-ftp' 4 | import { ConfigIniParser } from 'config-ini-parser' 5 | import path from 'path' 6 | import fs from 'fs' 7 | 8 | export default { 9 | ip: "", 10 | port: "", 11 | 12 | files: { 13 | loader: "/user/app/NPXS39041/logs/loader.log", 14 | log: "/user/app/NPXS39041/logs/store.log", 15 | itemzflow: "/user/app/NPXS39041/logs/itemzflow.log", 16 | settings: "/user/app/NPXS39041/settings.ini", 17 | }, 18 | 19 | getWindow(){ 20 | let win = BrowserWindow.getFocusedWindow(); 21 | 22 | if(!win){ 23 | let all = BrowserWindow.getAllWindows() 24 | win = all[0] 25 | } 26 | 27 | return win 28 | }, 29 | 30 | getLocalFile(file=null){ 31 | return app.getPath('userData') + '/data/' + path.basename(file) 32 | }, 33 | 34 | setConfig(config){ 35 | this.ip = config.ps4ip 36 | this.port = config.ps4port 37 | }, 38 | 39 | error(err=null){ 40 | // deprecated 41 | // const win = BrowserWindow.getFocusedWindow(); 42 | this.getWindow().webContents.send('error', err) 43 | this.log(err) 44 | }, 45 | 46 | notify(msg=null){ 47 | this.getWindow().webContents.send('notify', msg) 48 | this.log(msg) 49 | }, 50 | 51 | log(msg=null){ 52 | this.getWindow().webContents.send('log', msg) 53 | console.log("FTP:: " + msg) 54 | }, 55 | 56 | loading(msg=null){ 57 | this.getWindow().webContents.send('loading', msg) 58 | console.log("Loading:: " + JSON.stringify(msg)) 59 | }, 60 | 61 | async getClient(){ 62 | const client = new Client(1000) 63 | client.ftp.verbose = true 64 | 65 | try { 66 | this.log("Connecting to ps4 though ftp " + this.ip + ':' + this.port) 67 | await client.access({ 68 | host: this.ip, 69 | port: this.port, 70 | user: "anonymous", 71 | password: "anonymous", 72 | secureOptions: { 73 | rejectUnauthorized: false, 74 | } 75 | }) 76 | 77 | // client.trackProgress(info => { 78 | // console.log("File", info.name) 79 | // console.log("Type", info.type) 80 | // console.log("Transferred", info.bytes) 81 | // console.log("Transferred Overall", info.bytesOverall) 82 | // }) 83 | } 84 | catch(err) { 85 | this.error("Cannot connect to PS4") 86 | throw err 87 | return 88 | } 89 | 90 | return client 91 | }, 92 | 93 | async download(target, source){ 94 | let client = await this.getClient() 95 | let size = await client.size(source) 96 | let dir = path.dirname(target) 97 | 98 | if (!fs.existsSync(dir)) { 99 | fs.mkdirSync(dir); 100 | } 101 | 102 | if(size === 0){ 103 | console.log(source, size) 104 | 105 | // double check for backward compatibility 106 | source = source.replace('store.log', 'log.txt') 107 | size = await client.size(source) 108 | 109 | if(size == 0){ 110 | throw path.basename(source) + " not found or empty" 111 | client.close() 112 | return 113 | } 114 | } 115 | 116 | let get = await client.downloadTo(target, source) 117 | client.close() 118 | return get 119 | }, 120 | 121 | async upload(source, target){ 122 | let client = await this.getClient() 123 | let get = await client.uploadFrom(source, target) 124 | client.close() 125 | return get 126 | }, 127 | 128 | async getLogs(config, log){ 129 | this.setConfig(config) 130 | 131 | let theFile = null; 132 | let theFileName = null; 133 | if(log in this.files) 134 | theFile = this.files[log] 135 | 136 | theFileName = path.basename(theFile) 137 | 138 | this.log("Trying to get logs from HB-Store ") 139 | this.loading({ message: "Trying to get " + theFileName + " from HB-Store" }) 140 | 141 | try { 142 | await this.download(this.getLocalFile(theFile), theFile) 143 | } 144 | catch(e){ 145 | console.log("DOWNLOAD ERROR FOR LOG.TXT") 146 | this.loading({ hide: true }) 147 | this.error(e) 148 | return 149 | } 150 | 151 | this.log("got " + theFileName + ", let's save it to the user space") 152 | this.loading({ message: "Loading " + theFileName + ", where should we save it?" }) 153 | 154 | let win = this.getWindow() 155 | let targetLogFile = await dialog.showSaveDialog(win, { 156 | title: "Save HB-Store " + theFileName, 157 | defaultPath: "*/" + theFileName, 158 | buttonLabel: "Save HB-Store Log", 159 | filters: [ 160 | { name: "HB-Store " + theFileName, extensions: ['log'] } 161 | ] 162 | }) 163 | 164 | this.loading({ hide: true }) 165 | if(targetLogFile.canceled) return 166 | 167 | await fs.copyFileSync(this.getLocalFile(theFile), targetLogFile.filePath) 168 | this.notify("HB-Store " + theFileName + " downloaded") 169 | }, 170 | 171 | async cleanLogs(config){ 172 | this.setConfig(config) 173 | this.loading({ message: "Cleaning HB-Store Logs"}) 174 | 175 | try { 176 | await fs.writeFileSync(this.getLocalFile(this.files.log), "===== CLEARED LOGS ======\n") 177 | } 178 | catch(e){ 179 | this.loading({ hide: true }) 180 | return this.error(e) 181 | } 182 | 183 | try { 184 | await this.upload(this.getLocalFile(this.files.log), this.files.log) 185 | } 186 | catch(e){ 187 | console.log("UPLOAD ERROR CATCH") 188 | this.loading({ hide: true }) 189 | // return this.error(e) 190 | return 191 | } 192 | 193 | this.loading({ hide: true }) 194 | this.notify("HB-Store log has been cleared") 195 | }, 196 | 197 | async getSettings(config){ 198 | this.setConfig(config) 199 | this.loading({ message: "Loading settings.ini from PS4" }) 200 | 201 | try { 202 | await this.download(this.getLocalFile(this.files.settings), this.files.settings) 203 | } 204 | catch(e){ 205 | this.loading({ hide: true }) 206 | this.error(e) 207 | console.log("ERROR IN GET SETTINGS.INI") 208 | throw e 209 | } 210 | 211 | this.loading({ hide: true }) 212 | }, 213 | 214 | async updateSettings(config){ 215 | try { 216 | await this.getSettings(config) 217 | } 218 | catch(e){ return } 219 | 220 | console.log("CONTINUE IN UPDATE SETTING") 221 | 222 | this.setConfig(config) 223 | this.loading({ message: "Updating Settings.ini "}) 224 | 225 | let parser = new ConfigIniParser() 226 | let cdn = config.cdn 227 | 228 | // load ini 229 | try { 230 | let ini = await fs.readFileSync(this.getLocalFile(this.files.settings), 'utf8') 231 | parser.parse(ini) 232 | } 233 | catch(e){ 234 | this.loading({ hide: true }) 235 | return this.error("Error in reading settings.ini") 236 | } 237 | 238 | // update CDN 239 | parser.set('Settings', 'CDN', cdn) 240 | try { 241 | await fs.writeFileSync(this.getLocalFile(this.files.settings), parser.stringify()) 242 | } 243 | catch(e){ 244 | this.loading({ hide: true }) 245 | return this.error(e) 246 | } 247 | 248 | // upload to ftp 249 | this.loading({ message: "Uploading new settings.ini to PS4"}) 250 | try { 251 | await this.upload(this.getLocalFile(this.files.settings), this.files.settings) 252 | } 253 | catch(e){ 254 | this.loading({ hide: true }) 255 | return this.error(e) 256 | } 257 | 258 | this.loading({ hide: true }) 259 | this.notify("Update HB-Store CDN to " + cdn) 260 | }, 261 | 262 | 263 | } 264 | -------------------------------------------------------------------------------- /quasar.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | /* 4 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 5 | * the ES6 features that are supported by your Node version. https://node.green/ 6 | */ 7 | 8 | // Configuration for your app 9 | // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js 10 | const path = require('path'); 11 | const fs = require('fs'); 12 | const webpack = require('webpack') 13 | const { configure } = require('quasar/wrappers'); 14 | 15 | module.exports = configure(function (ctx) { 16 | return { 17 | target: 'electron-renderer', 18 | // https://v2.quasar.dev/quasar-cli-webpack/supporting-ts 19 | supportTS: false, 20 | 21 | // https://v2.quasar.dev/quasar-cli-webpack/prefetch-feature 22 | // preFetch: true, 23 | 24 | // app boot file (/src/boot) 25 | // --> boot files are part of "main.js" 26 | // https://v2.quasar.dev/quasar-cli-webpack/boot-files 27 | boot: [ 28 | 'i18n', 29 | 'axios', 30 | 'components', 31 | 'plugins', 32 | ], 33 | 34 | // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css 35 | css: [ 36 | 'app.scss' 37 | ], 38 | 39 | // https://github.com/quasarframework/quasar/tree/dev/extras 40 | extras: [ 41 | // 'ionicons-v4', 42 | // 'mdi-v5', 43 | 'fontawesome-v5', 44 | // 'eva-icons', 45 | // 'themify', 46 | // 'line-awesome', 47 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 48 | 49 | 'roboto-font', // optional, you are not bound to it 50 | 'material-icons', // optional, you are not bound to it 51 | ], 52 | 53 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-build 54 | build: { 55 | vueRouterMode: 'hash', // available values: 'hash', 'history' 56 | 57 | // transpile: false, 58 | // publicPath: '/', 59 | 60 | // Add dependencies for transpiling with Babel (Array of string/regex) 61 | // (from node_modules, which are by default not transpiled). 62 | // Applies only if "transpile" is set to true. 63 | // transpileDependencies: [], 64 | 65 | // rtl: true, // https://quasar.dev/options/rtl-support 66 | // preloadChunks: true, 67 | // showProgress: false, 68 | // gzip: true, 69 | // analyze: true, 70 | 71 | // Options below are automatically set depending on the env, set them if you want to override 72 | // extractCSS: false, 73 | 74 | // https://v2.quasar.dev/quasar-cli-webpack/handling-webpack 75 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 76 | 77 | env: { 78 | version: require(__dirname + '/package.json').version, 79 | }, 80 | 81 | chainWebpack (chain){ 82 | const nodePolyfillWebpackPlugin = require('node-polyfill-webpack-plugin') 83 | chain.plugin('node-polyfill').use(nodePolyfillWebpackPlugin) 84 | }, 85 | 86 | extendWebpack (cfg) { 87 | cfg.resolve.alias = { 88 | ...cfg.resolve.alias, 89 | '~': path.resolve(__dirname, './src'), 90 | 'public': path.resolve(__dirname, './public'), 91 | } 92 | 93 | cfg.plugins.push( 94 | // new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 95 | ) 96 | 97 | // console.log(cfg.optimization) 98 | // console.log(cfg.optimization.splitChunks.cacheGroups) 99 | }, 100 | }, 101 | 102 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer 103 | devServer: { 104 | server: { 105 | type: 'http' 106 | }, 107 | port: 8080, 108 | open: true // opens browser window automatically 109 | }, 110 | 111 | // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework 112 | framework: { 113 | config: {}, 114 | 115 | // iconSet: 'material-icons', // Quasar icon set 116 | // lang: 'en-US', // Quasar language pack 117 | 118 | // For special cases outside of where the auto-import strategy can have an impact 119 | // (like functional components as one of the examples), 120 | // you can manually specify Quasar components/directives to be available everywhere: 121 | // 122 | // components: [], 123 | // directives: [], 124 | 125 | // Quasar plugins 126 | plugins: [ 127 | 'Loading', 128 | 'Dialog', 129 | 'Notify', 130 | ] 131 | }, 132 | 133 | // animations: 'all', // --- includes all animations 134 | // https://quasar.dev/options/animations 135 | animations: [], 136 | 137 | // https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/configuring-ssr 138 | ssr: { 139 | pwa: false, 140 | 141 | // manualStoreHydration: true, 142 | // manualPostHydrationTrigger: true, 143 | 144 | prodPort: 3000, // The default port that the production server should use 145 | // (gets superseded if process.env.PORT is specified at runtime) 146 | 147 | maxAge: 1000 * 60 * 60 * 24 * 30, 148 | // Tell browser when a file from the server should expire from cache (in ms) 149 | 150 | 151 | chainWebpackWebserver (/* chain */) {}, 152 | 153 | 154 | middlewares: [ 155 | ctx.prod ? 'compression' : '', 156 | 'render' // keep this as last one 157 | ] 158 | }, 159 | 160 | // https://v2.quasar.dev/quasar-cli-webpack/developing-pwa/configuring-pwa 161 | pwa: { 162 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest' 163 | workboxOptions: {}, // only for GenerateSW 164 | 165 | // for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts]) 166 | // if using workbox in InjectManifest mode 167 | 168 | chainWebpackCustomSW (/* chain */) {}, 169 | 170 | 171 | manifest: { 172 | name: `hb-store-cdn-server`, 173 | short_name: `hb-store-cdn-server`, 174 | description: `Local HB-Store Server`, 175 | display: 'standalone', 176 | orientation: 'portrait', 177 | background_color: '#ffffff', 178 | theme_color: '#027be3', 179 | icons: [ 180 | { 181 | src: 'icons/icon-128x128.png', 182 | sizes: '128x128', 183 | type: 'image/png' 184 | }, 185 | { 186 | src: 'icons/icon-192x192.png', 187 | sizes: '192x192', 188 | type: 'image/png' 189 | }, 190 | { 191 | src: 'icons/icon-256x256.png', 192 | sizes: '256x256', 193 | type: 'image/png' 194 | }, 195 | { 196 | src: 'icons/icon-384x384.png', 197 | sizes: '384x384', 198 | type: 'image/png' 199 | }, 200 | { 201 | src: 'icons/icon-512x512.png', 202 | sizes: '512x512', 203 | type: 'image/png' 204 | } 205 | ] 206 | } 207 | }, 208 | 209 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-cordova-apps/configuring-cordova 210 | cordova: { 211 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 212 | }, 213 | 214 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-capacitor-apps/configuring-capacitor 215 | capacitor: { 216 | hideSplashscreen: true 217 | }, 218 | 219 | // Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-electron-apps/configuring-electron 220 | electron: { 221 | bundler: 'builder', // 'packager' or 'builder' 222 | 223 | packager: { 224 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 225 | 226 | // OS X / Mac App Store 227 | // appBundleId: '', 228 | // appCategoryType: '', 229 | // osxSign: '', 230 | // protocol: 'myapp://path', 231 | 232 | // Windows only 233 | // win32metadata: { ... } 234 | }, 235 | 236 | builder: { 237 | // https://www.electron.build/configuration/configuration 238 | appId: 'gkiokan.net.hb-store-cdn-server', 239 | directories: { 240 | output: "release" 241 | }, 242 | publish: { 243 | "provider": "github", 244 | "owner": "gkiokan", 245 | "repo": "hb-store-cdn-server", 246 | "vPrefixedTagName": true 247 | }, 248 | "win": { 249 | "target": [ 250 | { 251 | "target": "zip", 252 | "arch": [ "x64", "ia32" ] 253 | }, 254 | { 255 | "target": "portable", 256 | "arch": [ "x64", "ia32" ] 257 | } 258 | ] 259 | }, 260 | "linux": { 261 | "icon": "icons/icon.png", 262 | "target": [ 263 | { 264 | "target": "AppImage", 265 | "arch": [ "x64" ] 266 | }, 267 | { 268 | "target": "snap" 269 | }, 270 | { 271 | "target": "deb" 272 | } 273 | ] 274 | }, 275 | "portable": { 276 | "artifactName": "${productName}.exe", 277 | "unpackDirName": "HBStoreCDNServer" 278 | } 279 | }, 280 | 281 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 282 | 283 | chainWebpackMain (/* chain */) {}, 284 | 285 | 286 | 287 | chainWebpackPreload (/* chain */) {}, 288 | 289 | } 290 | } 291 | }); 292 | -------------------------------------------------------------------------------- /src-electron/src/server.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | 3 | import express from 'express' 4 | import http from 'http' 5 | import fg from 'fast-glob' 6 | import path from 'path' 7 | import hb from './hb' 8 | import db from './db' 9 | import pkgInfo from 'ps4-pkg-info' 10 | import { getPs4PkgInfo } from "@njzy/ps4-pkg-info" 11 | import md5File from 'md5-file' 12 | import normalize from 'normalize-path' 13 | 14 | export default { 15 | ip: null, 16 | port: null, 17 | basePath: null, 18 | files: [], 19 | host: { 20 | app: null, 21 | server: null, 22 | router: null, 23 | }, 24 | 25 | getWindow(){ 26 | let win = BrowserWindow.getFocusedWindow(); 27 | 28 | if(!win){ 29 | let all = BrowserWindow.getAllWindows() 30 | win = all[0] 31 | } 32 | 33 | return win 34 | }, 35 | 36 | getBaseURI(){ 37 | return 'http://' + this.ip + ':' + this.port 38 | }, 39 | 40 | setConfig(config){ 41 | this.ip = config.ip 42 | this.port = config.port 43 | this.basePath = config.basePath 44 | }, 45 | 46 | error(err=null){ 47 | // deprecated 48 | // const win = BrowserWindow.getFocusedWindow(); 49 | this.getWindow().webContents.send('error', err) 50 | this.log(err) 51 | }, 52 | 53 | log(msg=null){ 54 | this.getWindow().webContents.send('log', msg) 55 | console.log("Server:: " + msg) 56 | }, 57 | 58 | notify(msg=null){ 59 | this.getWindow().webContents.send('notify', msg) 60 | this.log(msg) 61 | }, 62 | 63 | sendFiles(){ 64 | this.getWindow().webContents.send('server-files', this.files) 65 | }, 66 | 67 | setState(state=null){ 68 | this.getWindow().send('server-state', state) 69 | this.log("Set Server State to " + state) 70 | }, 71 | 72 | updatePS4IP(ip){ 73 | this.getWindow().send('update-ps4-ip', ip) 74 | this.log("I guess we have a ps4 IP here " + ip) 75 | }, 76 | 77 | addCORSHandler(){ 78 | this.host.app.use((req, res, next) => { 79 | res.setHeader('Access-Control-Allow-Origin', '*') 80 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); 81 | // res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); 82 | // res.setHeader('Access-Control-Allow-Credentials', true); 83 | next() 84 | }) 85 | }, 86 | 87 | addRouterMiddleware(){ 88 | this.host.app.use((req, res, next) => { 89 | this.host.router(req, res, next) 90 | }) 91 | }, 92 | 93 | createPaths(){ 94 | this.log("Server is ready to create paths") 95 | db.renewDB() 96 | this.files = [] 97 | this.host.router = new express.Router() 98 | this.addHearthbeatEndpoint() 99 | this.addFilesFromBasePath() 100 | }, 101 | 102 | async rescanFolder(config){ 103 | console.log("Trigger re-scan") 104 | this.setConfig(config) 105 | this.createPaths() 106 | this.notify("Re-scaned BasePath") 107 | }, 108 | 109 | addHearthbeatEndpoint(){ 110 | this.log("Create Hearthbeat endpoint") 111 | this.host.router.get('/hb', function(request, response){ 112 | response.status(200).json({ 113 | remoteAddress: request.connection.remoteAddress, 114 | remotePort: request.connection.remotePort, 115 | localAddress: request.connection.localAddress, 116 | localPort: request.connection.localPort, 117 | message: "Hearthbeat of HB-Store CDN Server is working" 118 | }) 119 | }) 120 | 121 | // sample icon0.png 122 | this.host.router.get('/icon0.png', function(request, response){ 123 | let image = path.resolve(__dirname, process.env.QUASAR_PUBLIC_FOLDER) + '/icon0.png' 124 | response.status(200).download(image, 'icon0.png') 125 | }) 126 | 127 | // storage database 128 | this.host.router.get('/store.db', (request, response) => { 129 | console.log("HB-Store Download store.db Request", request) 130 | console.log("PS4 IP", request.ip ) 131 | 132 | // for local tests this throws 133 | try { 134 | var r = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/ 135 | let ip = request.ip 136 | let cleanedIP = ip.match(r)[0] 137 | this.updatePS4IP(cleanedIP) 138 | } 139 | catch( e ){ 140 | 141 | } 142 | 143 | let store = db.getStorePath() 144 | console.log(store) 145 | response.status(200).download(store, 'store.db') 146 | }) 147 | 148 | // check the storage checksum 149 | this.host.router.get('/api.php', function(request, response){ 150 | if('db_check_hash' in request.query){ 151 | let hash = md5File.sync(db.getStorePath()) 152 | response.status(200).json({ 153 | hash, 154 | params: request.query, 155 | }) 156 | } 157 | }) 158 | 159 | // number of downloads? 160 | this.host.router.get('/download.php', function(request, response){ 161 | response.status(200).json({ 162 | number_of_downloads: "1337", 163 | }) 164 | }) 165 | 166 | // load server binaries 167 | for (const asset of [ 168 | 'homebrew.elf', 169 | 'homebrew.elf.sig', 170 | 'remote.md5', 171 | 'store.prx', 172 | 'store.prx.sig', 173 | ]) 174 | this.host.router.get('/update/' + asset, function(request, response){ 175 | let file = app.getPath('userData') + '/bin/' + asset 176 | response.status(200).download(file, asset) 177 | }) 178 | }, 179 | 180 | async addFilesFromBasePath(){ 181 | this.log("Search for pkg files in basePath at " + this.basePath) 182 | let patchedBasePath = normalize(this.basePath) 183 | let toRemoveBasePath = (patchedBasePath.charAt(0) == "/") ? patchedBasePath.substr(1).replace(/[^a-zA-Z0-9-_./]/g, '') : patchedBasePath.replace(/[^a-zA-Z0-9-_./]/g, '') 184 | 185 | let files = fg.sync([patchedBasePath + '/**/*.pkg']) 186 | this.log("Found " + files.length + " files in basePath") 187 | 188 | // loop for files and map the files to a file object 189 | let base = this.getBaseURI() 190 | let i = 1 191 | for (const file of files){ 192 | // console.log("Start file ", file) 193 | try { 194 | // let data = await pkgInfo.extract(file) 195 | let data = await getPs4PkgInfo(file, { generateBase64Icon: true }) 196 | .catch( e => { 197 | this.error("Error in PKG Extraction: "+ e + '; File: ' + file) 198 | throw e 199 | }) 200 | // console.log(data) 201 | let item = hb.createItem(data, file, i) 202 | item = hb.removeBasePath(item, toRemoveBasePath) 203 | item = hb.addImages(item, base) 204 | item = this.addFileEndpoint(item, base) 205 | 206 | this.files.push(item) 207 | // console.log(item) 208 | i = i+1 209 | } 210 | catch(e){ 211 | console.log("Error", e) 212 | } 213 | 214 | // console.log("End file ", file) 215 | // console.log("====") 216 | } 217 | 218 | db.addAllItems(this.files) 219 | // console.log("=====================================") 220 | // console.log("patched file 0 ", this.files[0] ) 221 | // console.log("=====================================") 222 | 223 | this.sendFiles() 224 | }, 225 | 226 | addFileEndpoint(item, base){ 227 | this.host.router.get(`/${item.patchedFilename}`, function(request, response){ 228 | response.status(200).download(item.path, item.filename) 229 | }) 230 | 231 | this.host.router.get(`/${item.patchedFilename}/icon0.png`, function(request, response){ 232 | let imgData = item.icon0.replace(/^data:image\/png;base64,/, ''); 233 | let img = Buffer.from(imgData, 'base64') 234 | 235 | response.writeHead(200, { 236 | 'Content-Type': 'image/png', 237 | 'Content-Length': img.length 238 | }) 239 | 240 | response.end(img) 241 | }) 242 | 243 | item.package = base + '/' + item.patchedFilename 244 | 245 | return item 246 | }, 247 | 248 | createServer(){ 249 | const app = express(); 250 | this.host.app = app 251 | this.host.router = express.Router() 252 | this.log("Server created") 253 | }, 254 | 255 | async start(config){ 256 | this.setConfig(config) 257 | 258 | if(!this.host.app){ 259 | this.createServer() 260 | } 261 | 262 | // console.log(this.ip, this.ip.length, this.port, this.port.length) 263 | if(this.ip.length == 0 || this.port.length == 0){ 264 | this.error("Server cannot start. Please configure IP and Port") 265 | // this.$message({ type: 'warning', message: error }); 266 | return 267 | } 268 | 269 | this.host.server = await this.host.app.listen(this.port, () => { 270 | this.notify('Server is running on ' + this.ip + ' at port ' + this.port) 271 | this.setState('running') 272 | 273 | this.addCORSHandler() 274 | this.addRouterMiddleware() 275 | this.createPaths() 276 | }) 277 | .on('error', (e) => { 278 | // console.log({ ...e }) 279 | this.setState('stopped') 280 | 281 | if(e.code === 'EADDRINUSE'){ 282 | let error = "Port " + this.port + " is already in use.
Choose another port and restart the Server" 283 | this.error(error) 284 | } 285 | else { 286 | this.error('Error in listening on ' + this.ip + ' at port ' + this.port + ". Error: " + e.code) 287 | } 288 | }) 289 | }, 290 | 291 | async stop(){ 292 | this.log('Closing Server') 293 | 294 | if(this.host.server) 295 | await this.host.server.close(() => { 296 | this.log('Server closed') 297 | this.setState('stopped') 298 | }) 299 | else 300 | this.error("Server can not be closed. Server Object does't exist") 301 | }, 302 | 303 | restart(config){ 304 | this.log("Server restarting triggered") 305 | this.stop() 306 | this.start(config) 307 | }, 308 | 309 | } 310 | -------------------------------------------------------------------------------- /public/kofi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src-electron/src/baseImage.js: -------------------------------------------------------------------------------- 1 | export default "data:image/png;base64,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" 2 | --------------------------------------------------------------------------------