├── 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 |
2 |
3 |
4 |
5 |
6 | Logs, latest on top
7 |
8 |
9 |
10 |
{{ log.time }}
11 |
12 |
13 |
14 |
15 |
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 |
2 |
3 |
4 |
5 | 404
6 |
7 |
8 |
9 | Oops. Nothing here...
10 |
11 |
12 |
21 |
22 |
23 |
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 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
2 |
8 |
12 |
13 |
14 |
15 |
16 | {{ title }}
17 | {{ caption }}
18 |
19 |
20 |
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 |
2 |
3 |
4 |
5 | HB-Store CDN Server
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | List of local serving files will be here ...
9 |
10 |
11 | Found {{ files.length }} files
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
![]()
21 |
22 |
23 | {{ item.id }}
Is Backport
24 |
{{ item.name }}
25 | Version: {{ item.version }}
26 | Size: {{ item.Size }}
27 |
28 |
29 |
30 |
31 |
32 |
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 | [](https://ko-fi.com/M4M082WK8)
3 | [](#)
4 | [](#)
5 | [](#)
6 | [](#)
7 | [](#)
8 |
9 | This is the new HB-Store CDN Tool for you.
10 |
11 | 
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 | [](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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
HB-Store CDN Server
17 | Version {{ $root.version }}
18 | Made by Gkiokan
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
HB-Store (ps4 app)
28 | Created by LightningMods
29 |
30 |
31 |
PKG-Zone
32 | Official HB-Store
33 | Owned by LightningMods
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
59 |
60 |
62 |
--------------------------------------------------------------------------------
/src/layouts/MainLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 | Use this as CDN: {{ getCDNAdress }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Made by Gkiokan | Exclusive for HB-Store
35 |
36 |
37 | v{{ $root.version }}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
72 |
73 |
106 |
--------------------------------------------------------------------------------
/src/components/HBOperations.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Get store.log
11 |
12 |
13 |
14 |
15 |
16 | Get loader.log
17 |
18 |
19 |
20 |
21 |
22 | Get itemzflow.log
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Clear store.log on PS4
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Update CDN to My Server
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Restore CDN to pkg-zone.com
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
114 |
115 |
117 |
--------------------------------------------------------------------------------
/src/assets/quasar-logo-vertical.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
199 |
--------------------------------------------------------------------------------
/src/components/ServerBinariesDownload.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Current Version {{ binaryVersion }}
13 |
New Version {{ newUpdateAvailableVersion }}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ asset.name }}
22 |
23 |
24 |
25 |
26 |
28 |
29 |
30 |
31 |
32 |
33 |
172 |
173 |
175 |
--------------------------------------------------------------------------------
/src/components/Config.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
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 |
--------------------------------------------------------------------------------
/src-electron/src/baseImage.js:
--------------------------------------------------------------------------------
1 | export default "data:image/png;base64,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"
2 |
--------------------------------------------------------------------------------