mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Feature/sideload (#1520)
* base styling and action placeholders for package sideload * apparently didnt add new folder * wip * parse manifest and icon from s9pk to upload * wip handle s9pk upload * adjust types, finalize actions, cleanup * clean up and fix data clearing and response * include rest rpc in proxy conf sample * address feedback to use shorthand falsy coercion * update copy and invalid package file ux * do not wait package upload, instead show install progress * fix proxy for rest rpc rename sideload package page titles
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@
|
|||||||
/product_key.txt
|
/product_key.txt
|
||||||
/*_product_key.txt
|
/*_product_key.txt
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
deploy_web.sh
|
||||||
38
frontend/package-lock.json
generated
38
frontend/package-lock.json
generated
@@ -23,6 +23,8 @@
|
|||||||
"@start9labs/emver": "^0.1.5",
|
"@start9labs/emver": "^0.1.5",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
|
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||||
|
"cbor-web": "^8.1.0",
|
||||||
"core-js": "^3.21.1",
|
"core-js": "^3.21.1",
|
||||||
"dompurify": "^2.3.6",
|
"dompurify": "^2.3.6",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
@@ -4638,6 +4640,20 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/cbor": {
|
||||||
|
"name": "@jprochazk/cbor",
|
||||||
|
"version": "0.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jprochazk/cbor/-/cbor-0.4.9.tgz",
|
||||||
|
"integrity": "sha512-FWNnkOtWrFOLXKG2nzOHR/EnCCGZZPvatAvWXDmkTDxgjj9JHDK3DkMUHcFCY3a9weylMCSO/nLOUM170NAO0Q=="
|
||||||
|
},
|
||||||
|
"node_modules/cbor-web": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-2hWHHMVrfffgoEmsAUh8vCxHoLa1vgodtC73+C5cSarkJlwTapnqAzcHINlP6Ej0DXuP4OmmJ9LF+JaNM5Lj/g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
@@ -12290,9 +12306,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/semver-regex": {
|
"node_modules/semver-regex": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz",
|
||||||
"integrity": "sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==",
|
"integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -17690,6 +17706,16 @@
|
|||||||
"integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==",
|
"integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"cbor": {
|
||||||
|
"version": "npm:@jprochazk/cbor@0.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jprochazk/cbor/-/cbor-0.4.9.tgz",
|
||||||
|
"integrity": "sha512-FWNnkOtWrFOLXKG2nzOHR/EnCCGZZPvatAvWXDmkTDxgjj9JHDK3DkMUHcFCY3a9weylMCSO/nLOUM170NAO0Q=="
|
||||||
|
},
|
||||||
|
"cbor-web": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-2hWHHMVrfffgoEmsAUh8vCxHoLa1vgodtC73+C5cSarkJlwTapnqAzcHINlP6Ej0DXuP4OmmJ9LF+JaNM5Lj/g=="
|
||||||
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
@@ -23321,9 +23347,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"semver-regex": {
|
"semver-regex": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz",
|
||||||
"integrity": "sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==",
|
"integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"send": {
|
"send": {
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
"@start9labs/emver": "^0.1.5",
|
"@start9labs/emver": "^0.1.5",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
|
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||||
|
"cbor-web": "^8.1.0",
|
||||||
"core-js": "^3.21.1",
|
"core-js": "^3.21.1",
|
||||||
"dompurify": "^2.3.6",
|
"dompurify": "^2.3.6",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
@@ -49,8 +51,8 @@
|
|||||||
"patch-db-client": "file: ../../../patch-db/client",
|
"patch-db-client": "file: ../../../patch-db/client",
|
||||||
"pbkdf2": "^3.1.2",
|
"pbkdf2": "^3.1.2",
|
||||||
"rxjs": "^6.6.7",
|
"rxjs": "^6.6.7",
|
||||||
"tslib": "^2.3.0",
|
|
||||||
"ts-matches": "^5.1.0",
|
"ts-matches": "^5.1.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"zone.js": "^0.11.5"
|
"zone.js": "^0.11.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,65 +4,65 @@
|
|||||||
/** Ionic CSS Variables **/
|
/** Ionic CSS Variables **/
|
||||||
:root {
|
:root {
|
||||||
--ion-color-primary: #0075e1;
|
--ion-color-primary: #0075e1;
|
||||||
--ion-color-primary-rgb: 66,140,255;
|
--ion-color-primary-rgb: 66, 140, 255;
|
||||||
--ion-color-primary-contrast: #ffffff;
|
--ion-color-primary-contrast: #ffffff;
|
||||||
--ion-color-primary-contrast-rgb: 255,255,255;
|
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||||
--ion-color-primary-shade: #3a7be0;
|
--ion-color-primary-shade: #3a7be0;
|
||||||
--ion-color-primary-tint: #5598ff;
|
--ion-color-primary-tint: #5598ff;
|
||||||
|
|
||||||
--ion-color-secondary: #50c8ff;
|
--ion-color-secondary: #50c8ff;
|
||||||
--ion-color-secondary-rgb: 80,200,255;
|
--ion-color-secondary-rgb: 80, 200, 255;
|
||||||
--ion-color-secondary-contrast: #ffffff;
|
--ion-color-secondary-contrast: #ffffff;
|
||||||
--ion-color-secondary-contrast-rgb: 255,255,255;
|
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||||
--ion-color-secondary-shade: #46b0e0;
|
--ion-color-secondary-shade: #46b0e0;
|
||||||
--ion-color-secondary-tint: #62ceff;
|
--ion-color-secondary-tint: #62ceff;
|
||||||
|
|
||||||
--ion-color-tertiary: #6a64ff;
|
--ion-color-tertiary: #6a64ff;
|
||||||
--ion-color-tertiary-rgb: 106,100,255;
|
--ion-color-tertiary-rgb: 106, 100, 255;
|
||||||
--ion-color-tertiary-contrast: #ffffff;
|
--ion-color-tertiary-contrast: #ffffff;
|
||||||
--ion-color-tertiary-contrast-rgb: 255,255,255;
|
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||||
--ion-color-tertiary-shade: #5d58e0;
|
--ion-color-tertiary-shade: #5d58e0;
|
||||||
--ion-color-tertiary-tint: #7974ff;
|
--ion-color-tertiary-tint: #7974ff;
|
||||||
|
|
||||||
--ion-color-success: #2fdf75;
|
--ion-color-success: #2fdf75;
|
||||||
--ion-color-success-rgb: 47,223,117;
|
--ion-color-success-rgb: 47, 223, 117;
|
||||||
--ion-color-success-contrast: #000000;
|
--ion-color-success-contrast: #000000;
|
||||||
--ion-color-success-contrast-rgb: 0,0,0;
|
--ion-color-success-contrast-rgb: 0, 0, 0;
|
||||||
--ion-color-success-shade: #29c467;
|
--ion-color-success-shade: #29c467;
|
||||||
--ion-color-success-tint: #44e283;
|
--ion-color-success-tint: #44e283;
|
||||||
|
|
||||||
--ion-color-warning: #ffd534;
|
--ion-color-warning: #ffd534;
|
||||||
--ion-color-warning-rgb: 255,213,52;
|
--ion-color-warning-rgb: 255, 213, 52;
|
||||||
--ion-color-warning-contrast: #000000;
|
--ion-color-warning-contrast: #000000;
|
||||||
--ion-color-warning-contrast-rgb: 0,0,0;
|
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||||
--ion-color-warning-shade: #e0bb2e;
|
--ion-color-warning-shade: #e0bb2e;
|
||||||
--ion-color-warning-tint: #ffd948;
|
--ion-color-warning-tint: #ffd948;
|
||||||
|
|
||||||
--ion-color-danger: #ff4961;
|
--ion-color-danger: #ff4961;
|
||||||
--ion-color-danger-rgb: 255,73,97;
|
--ion-color-danger-rgb: 255, 73, 97;
|
||||||
--ion-color-danger-contrast: #ffffff;
|
--ion-color-danger-contrast: #ffffff;
|
||||||
--ion-color-danger-contrast-rgb: 255,255,255;
|
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||||
--ion-color-danger-shade: #e04055;
|
--ion-color-danger-shade: #e04055;
|
||||||
--ion-color-danger-tint: #ff5b71;
|
--ion-color-danger-tint: #ff5b71;
|
||||||
|
|
||||||
--ion-color-light: #181818;
|
--ion-color-light: #181818;
|
||||||
--ion-color-light-rgb: 24,24,24;
|
--ion-color-light-rgb: 24, 24, 24;
|
||||||
--ion-color-light-contrast: #ffffff;
|
--ion-color-light-contrast: #ffffff;
|
||||||
--ion-color-light-contrast-rgb: 0,0,0;
|
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||||
--ion-color-light-shade: #000000;
|
--ion-color-light-shade: #000000;
|
||||||
--ion-color-light-tint: #000000;
|
--ion-color-light-tint: #000000;
|
||||||
|
|
||||||
--ion-color-medium: #222428;
|
--ion-color-medium: #222428;
|
||||||
--ion-color-medium-rgb: 34,36,40;
|
--ion-color-medium-rgb: 34, 36, 40;
|
||||||
--ion-color-medium-contrast: #ffffff;
|
--ion-color-medium-contrast: #ffffff;
|
||||||
--ion-color-medium-contrast-rgb: 255,255,255;
|
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||||
--ion-color-medium-shade: #1e2023;
|
--ion-color-medium-shade: #1e2023;
|
||||||
--ion-color-medium-tint: #383a3e;
|
--ion-color-medium-tint: #383a3e;
|
||||||
|
|
||||||
--ion-color-dark: #e0e0e0;
|
--ion-color-dark: #e0e0e0;
|
||||||
--ion-color-dark-rgb: 224,224,224;
|
--ion-color-dark-rgb: 224, 224, 224;
|
||||||
--ion-color-dark-contrast: #000000;
|
--ion-color-dark-contrast: #000000;
|
||||||
--ion-color-dark-contrast-rgb: 0,0,0;
|
--ion-color-dark-contrast-rgb: 0, 0, 0;
|
||||||
--ion-color-dark-shade: #bfbfbf;
|
--ion-color-dark-shade: #bfbfbf;
|
||||||
--ion-color-dark-tint: #d8d8d8;
|
--ion-color-dark-tint: #d8d8d8;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<ion-item-divider></ion-item-divider>
|
<ion-item-divider></ion-item-divider>
|
||||||
<ion-item-group></ion-item-group>
|
<ion-item-group></ion-item-group>
|
||||||
<ion-label></ion-label>
|
<ion-label></ion-label>
|
||||||
|
<ion-label style="font-weight: bold"></ion-label>
|
||||||
<ion-list></ion-list>
|
<ion-list></ion-list>
|
||||||
<ion-loading></ion-loading>
|
<ion-loading></ion-loading>
|
||||||
<ion-modal></ion-modal>
|
<ion-modal></ion-modal>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const ICONS = [
|
|||||||
'play-outline',
|
'play-outline',
|
||||||
'power',
|
'power',
|
||||||
'pulse',
|
'pulse',
|
||||||
|
'push-outline',
|
||||||
'qr-code-outline',
|
'qr-code-outline',
|
||||||
'receipt-outline',
|
'receipt-outline',
|
||||||
'refresh',
|
'refresh',
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./sessions/sessions.module').then(m => m.SessionsPageModule),
|
import('./sessions/sessions.module').then(m => m.SessionsPageModule),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'sideload',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./sideload/sideload.module').then(m => m.SideloadPageModule),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'specs',
|
path: 'specs',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
|||||||
@@ -331,6 +331,17 @@ export class ServerShowPage {
|
|||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled: of(false),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Manually install a service',
|
||||||
|
description: `Install a service by drag n' drop`,
|
||||||
|
icon: 'push-outline',
|
||||||
|
action: () =>
|
||||||
|
this.navCtrl.navigateForward(['sideload'], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
}),
|
||||||
|
detail: true,
|
||||||
|
disabled: of(false),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Marketplace Settings',
|
title: 'Marketplace Settings',
|
||||||
description: 'Add or remove marketplaces',
|
description: 'Add or remove marketplaces',
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
Directive,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
HostBinding,
|
||||||
|
HostListener,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser'
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appDnd]',
|
||||||
|
})
|
||||||
|
export class DragNDropDirective {
|
||||||
|
@Output() onFileDropped: EventEmitter<any> = new EventEmitter()
|
||||||
|
|
||||||
|
@HostBinding('style.background') private background = 'rgba(24, 24, 24, 0.5)'
|
||||||
|
|
||||||
|
constructor(el: ElementRef, private sanitizer: DomSanitizer) {}
|
||||||
|
|
||||||
|
@HostListener('dragover', ['$event']) public onDragOver(evt: DragEvent) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
this.background = '#6a937b3c'
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('dragleave', ['$event']) public onDragLeave(evt: DragEvent) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
this.background = 'rgba(24, 24, 24, 0.5)'
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('drop', ['$event']) public onDrop(evt: DragEvent) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
this.background = ' rgba(24, 24, 24, 0.5)'
|
||||||
|
this.onFileDropped.emit(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { IonicModule } from '@ionic/angular'
|
||||||
|
import { SideloadPage } from './sideload.page'
|
||||||
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
|
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
|
||||||
|
import { DragNDropDirective } from './dnd.directive'
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: SideloadPage,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
SharedPipesModule,
|
||||||
|
EmverPipesModule,
|
||||||
|
],
|
||||||
|
declarations: [SideloadPage, DragNDropDirective],
|
||||||
|
})
|
||||||
|
export class SideloadPageModule {}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button defaultHref="embassy"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>Manually install a service</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content class="ion-text-center">
|
||||||
|
<!-- file upload -->
|
||||||
|
<div
|
||||||
|
*ngIf="!toUpload.file"
|
||||||
|
class="drop-area"
|
||||||
|
[class.drop-area_mobile]="isMobile"
|
||||||
|
appDnd
|
||||||
|
(onFileDropped)="handleFileDrop($event)"
|
||||||
|
>
|
||||||
|
<ion-icon
|
||||||
|
name="cloud-upload-outline"
|
||||||
|
color="dark"
|
||||||
|
style="font-size: 42px"
|
||||||
|
></ion-icon>
|
||||||
|
<h4>To install a service manually, upload the s9pk here</h4>
|
||||||
|
<p *ngIf="onTor">
|
||||||
|
<ion-text color="success"
|
||||||
|
>Tip: switch to LAN for faster uploads.</ion-text
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<ion-button color="primary" type="file">
|
||||||
|
<label for="upload-photo">Browse</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
style="position: absolute; opacity: 0; height: 100%"
|
||||||
|
id="upload-photo"
|
||||||
|
(change)="handleFileInput($event)"
|
||||||
|
/>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
<!-- file uploaded -->
|
||||||
|
<div class="drop-area_filled" *ngIf="toUpload.file">
|
||||||
|
<div class="inline" *ngIf="valid; else invalid">
|
||||||
|
<ion-icon name="checkmark-circle-outline" color="success"></ion-icon>
|
||||||
|
<h4>{{ message }}</h4>
|
||||||
|
</div>
|
||||||
|
<ng-template #invalid>
|
||||||
|
<div class="area">
|
||||||
|
<div class="inline">
|
||||||
|
<ion-icon
|
||||||
|
*ngIf="!valid"
|
||||||
|
name="close-circle-outline"
|
||||||
|
color="danger"
|
||||||
|
></ion-icon>
|
||||||
|
<h4><ion-text color="danger">{{ message }}</ion-text></h4>
|
||||||
|
</div>
|
||||||
|
<ion-button color="primary" (click)="clearToUpload()">
|
||||||
|
Try again
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<br />
|
||||||
|
<div *ngIf="valid">
|
||||||
|
<div *ngIf="toUpload.manifest " class="service-card">
|
||||||
|
<div class="row row_end">
|
||||||
|
<ion-button
|
||||||
|
style="
|
||||||
|
--background-hover: transparent;
|
||||||
|
--padding-end: 0px;
|
||||||
|
--padding-start: 0px;
|
||||||
|
"
|
||||||
|
fill="clear"
|
||||||
|
size="small"
|
||||||
|
(click)="clearToUpload()"
|
||||||
|
>
|
||||||
|
<ion-icon slot="icon-only" name="close" color="danger"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<img
|
||||||
|
*ngIf="toUpload.icon"
|
||||||
|
[alt]="toUpload.manifest.title + ' Icon'"
|
||||||
|
[src]="toUpload.icon | trustUrl"
|
||||||
|
/>
|
||||||
|
<h2>{{ toUpload.manifest.title }}</h2>
|
||||||
|
<p>{{ toUpload.manifest.version | displayEmver }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ion-button color="primary" (click)="handleUpload()">
|
||||||
|
Upload & Install
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
.inline {
|
||||||
|
* {
|
||||||
|
vertical-align: initial;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.area {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-area {
|
||||||
|
display: flex;
|
||||||
|
background-color: rgba(24, 24, 24, 0.5);
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: var(--ion-color-dark);
|
||||||
|
color: var(--ion-color-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 60px;
|
||||||
|
padding: 30px;
|
||||||
|
min-height: 600px;
|
||||||
|
|
||||||
|
&_filled {
|
||||||
|
display: flex;
|
||||||
|
background-color: rgba(24, 24, 24, 0.5);
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: var(--ion-color-dark);
|
||||||
|
color: var(--ion-color-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 60px;
|
||||||
|
padding: 30px;
|
||||||
|
min-height: 600px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
border-width: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-input {
|
||||||
|
color: var(--ion-color-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card {
|
||||||
|
background: radial-gradient(var(--ion-color-step-100), transparent);
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 300px;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px 20px 40px 20px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--ion-color-step-100);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 4px 8px 8px 8px;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
&_end {
|
||||||
|
align-self: end;
|
||||||
|
|
||||||
|
ion-button {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { isPlatform, LoadingController, NavController } from '@ionic/angular'
|
||||||
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { Manifest } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
import cbor from 'cbor'
|
||||||
|
import { ErrorToastService } from '@start9labs/shared'
|
||||||
|
interface Positions {
|
||||||
|
[key: string]: [bigint, bigint] // [position, length]
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAGIC = new Uint8Array([59, 59])
|
||||||
|
const VERSION = new Uint8Array([1])
|
||||||
|
@Component({
|
||||||
|
selector: 'sideload',
|
||||||
|
templateUrl: './sideload.page.html',
|
||||||
|
styleUrls: ['./sideload.page.scss'],
|
||||||
|
})
|
||||||
|
export class SideloadPage {
|
||||||
|
isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android')
|
||||||
|
toUpload: {
|
||||||
|
manifest: Manifest | null
|
||||||
|
icon: string | null
|
||||||
|
file: File | null
|
||||||
|
} = {
|
||||||
|
manifest: null,
|
||||||
|
icon: null,
|
||||||
|
file: null,
|
||||||
|
}
|
||||||
|
onTor = this.config.isTor()
|
||||||
|
valid: boolean
|
||||||
|
message: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly loadingCtrl: LoadingController,
|
||||||
|
private readonly api: ApiService,
|
||||||
|
private readonly navCtrl: NavController,
|
||||||
|
private readonly errToast: ErrorToastService,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
handleFileDrop(e: any) {
|
||||||
|
const files = e.dataTransfer.files
|
||||||
|
this.setFile(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileInput(e: any) {
|
||||||
|
const files = e.target.files
|
||||||
|
this.setFile(files)
|
||||||
|
}
|
||||||
|
async setFile(files?: File[]) {
|
||||||
|
const loader = await this.loadingCtrl.create({
|
||||||
|
spinner: 'lines',
|
||||||
|
message: 'Verifying Package',
|
||||||
|
cssClass: 'loader',
|
||||||
|
})
|
||||||
|
await loader.present()
|
||||||
|
if (!files || !files.length) return
|
||||||
|
this.toUpload.file = files[0]
|
||||||
|
// verify valid s9pk
|
||||||
|
const magic = new Uint8Array(
|
||||||
|
await readBlobToArrayBuffer(this.toUpload.file.slice(0, 2)),
|
||||||
|
)
|
||||||
|
const version = new Uint8Array(
|
||||||
|
await readBlobToArrayBuffer(this.toUpload.file.slice(2, 3)),
|
||||||
|
)
|
||||||
|
if (compare(magic, MAGIC) && compare(version, VERSION)) {
|
||||||
|
loader.dismiss()
|
||||||
|
this.valid = true
|
||||||
|
this.message = 'A valid package file has been detected!'
|
||||||
|
await this.parseS9pk(this.toUpload.file)
|
||||||
|
} else {
|
||||||
|
loader.dismiss()
|
||||||
|
this.valid = false
|
||||||
|
this.message = 'Invalid package file'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearToUpload() {
|
||||||
|
this.toUpload.file = null
|
||||||
|
this.toUpload.manifest = null
|
||||||
|
this.toUpload.icon = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleUpload() {
|
||||||
|
const loader = await this.loadingCtrl.create({
|
||||||
|
spinner: 'lines',
|
||||||
|
message: 'Uploading Package',
|
||||||
|
cssClass: 'loader',
|
||||||
|
})
|
||||||
|
await loader.present()
|
||||||
|
try {
|
||||||
|
const guid = await this.api.sideloadPackage({
|
||||||
|
manifest: this.toUpload.manifest!,
|
||||||
|
icon: this.toUpload.icon!,
|
||||||
|
})
|
||||||
|
this.api
|
||||||
|
.uploadPackage(guid, await readBlobToArrayBuffer(this.toUpload.file!))
|
||||||
|
.catch(e => {
|
||||||
|
this.errToast.present(e)
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errToast.present(e)
|
||||||
|
} finally {
|
||||||
|
loader.dismiss()
|
||||||
|
await this.navCtrl.navigateForward(
|
||||||
|
`/services/${this.toUpload.manifest!.id}`,
|
||||||
|
)
|
||||||
|
this.clearToUpload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseS9pk(file: Blob) {
|
||||||
|
const positions: Positions = {}
|
||||||
|
// magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point
|
||||||
|
let start = 103
|
||||||
|
let end = start + 1 // 104
|
||||||
|
const tocLength = new DataView(
|
||||||
|
await readBlobToArrayBuffer(
|
||||||
|
this.toUpload.file?.slice(99, 103) ?? new Blob(),
|
||||||
|
),
|
||||||
|
).getUint32(0, false)
|
||||||
|
await getPositions(start, end, file, positions, tocLength as any)
|
||||||
|
|
||||||
|
await this.getManifest(positions, file)
|
||||||
|
await this.getIcon(positions, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getManifest(positions: Positions, file: Blob) {
|
||||||
|
const data = await readBlobToArrayBuffer(
|
||||||
|
file.slice(
|
||||||
|
Number(positions['manifest'][0]),
|
||||||
|
Number(positions['manifest'][0]) + Number(positions['manifest'][1]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
this.toUpload.manifest = await cbor.decode(data, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIcon(positions: Positions, file: Blob) {
|
||||||
|
const data = file.slice(
|
||||||
|
Number(positions['icon'][0]),
|
||||||
|
Number(positions['icon'][0]) + Number(positions['icon'][1]),
|
||||||
|
)
|
||||||
|
this.toUpload.icon = await readBlobAsDataURL(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPositions(
|
||||||
|
initialStart: number,
|
||||||
|
initialEnd: number,
|
||||||
|
file: Blob,
|
||||||
|
positions: Positions,
|
||||||
|
tocLength: number,
|
||||||
|
) {
|
||||||
|
let start = initialStart
|
||||||
|
let end = initialEnd
|
||||||
|
const titleLength = new Uint8Array(
|
||||||
|
await readBlobToArrayBuffer(file.slice(start, end)),
|
||||||
|
)[0]
|
||||||
|
const tocTitle = await file.slice(end, end + titleLength).text()
|
||||||
|
start = end + titleLength
|
||||||
|
end = start + 8
|
||||||
|
const chapterPosition = new DataView(
|
||||||
|
await readBlobToArrayBuffer(file.slice(start, end)),
|
||||||
|
).getBigUint64(0, false)
|
||||||
|
start = end
|
||||||
|
end = start + 8
|
||||||
|
const chapterLength = new DataView(
|
||||||
|
await readBlobToArrayBuffer(file.slice(start, end)),
|
||||||
|
).getBigUint64(0, false)
|
||||||
|
|
||||||
|
positions[tocTitle] = [chapterPosition, chapterLength]
|
||||||
|
start = end
|
||||||
|
end = start + 1
|
||||||
|
if (end <= tocLength + (initialStart - 1)) {
|
||||||
|
await getPositions(start, end, file, positions, tocLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readBlobAsDataURL(f: Blob): Promise<string> {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(f)
|
||||||
|
return new Promise(resolve => {
|
||||||
|
reader.onloadend = () => {
|
||||||
|
resolve(reader.result as string)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readBlobToArrayBuffer(f: Blob): Promise<ArrayBuffer> {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsArrayBuffer(f)
|
||||||
|
return new Promise(resolve => {
|
||||||
|
reader.onloadend = () => {
|
||||||
|
resolve(reader.result as ArrayBuffer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(a: Uint8Array, b: Uint8Array) {
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
if (a[i] !== b[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
|||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
DependencyError,
|
DependencyError,
|
||||||
|
Manifest,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
export module RR {
|
export module RR {
|
||||||
@@ -239,6 +240,12 @@ export module RR {
|
|||||||
spec: ConfigSpec
|
spec: ConfigSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SideloadPackageReq {
|
||||||
|
manifest: Manifest
|
||||||
|
icon: string // base64
|
||||||
|
}
|
||||||
|
export type SideloadPacakgeRes = string //guid
|
||||||
|
|
||||||
// marketplace
|
// marketplace
|
||||||
|
|
||||||
export type GetMarketplaceDataReq = { 'server-id': string }
|
export type GetMarketplaceDataReq = { 'server-id': string }
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
|||||||
// for getting static files: ex icons, instructions, licenses
|
// for getting static files: ex icons, instructions, licenses
|
||||||
abstract getStatic(url: string): Promise<string>
|
abstract getStatic(url: string): Promise<string>
|
||||||
|
|
||||||
|
// for sideloading packages
|
||||||
|
abstract uploadPackage(guid: string, body: ArrayBuffer): Promise<string>
|
||||||
|
|
||||||
// db
|
// db
|
||||||
|
|
||||||
abstract getRevisions(since: number): Promise<RR.GetRevisionsRes>
|
abstract getRevisions(since: number): Promise<RR.GetRevisionsRes>
|
||||||
@@ -260,6 +263,10 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
|||||||
deleteRecoveredPackage = (params: RR.UninstallPackageReq) =>
|
deleteRecoveredPackage = (params: RR.UninstallPackageReq) =>
|
||||||
this.syncResponse(() => this.deleteRecoveredPackageRaw(params))()
|
this.syncResponse(() => this.deleteRecoveredPackageRaw(params))()
|
||||||
|
|
||||||
|
abstract sideloadPackage(
|
||||||
|
params: RR.SideloadPackageReq,
|
||||||
|
): Promise<RR.SideloadPacakgeRes>
|
||||||
|
|
||||||
// Helper allowing quick decoration to sync the response patch and return the response contents.
|
// Helper allowing quick decoration to sync the response patch and return the response contents.
|
||||||
// Pass in a tempUpdate function which returns a UpdateTemp corresponding to a temporary
|
// Pass in a tempUpdate function which returns a UpdateTemp corresponding to a temporary
|
||||||
// state change you'd like to enact prior to request and expired when request terminates.
|
// state change you'd like to enact prior to request and expired when request terminates.
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadPackage(guid: string, body: ArrayBuffer): Promise<string> {
|
||||||
|
return this.http.httpRequest({
|
||||||
|
method: Method.POST,
|
||||||
|
body,
|
||||||
|
url: `/rest/rpc/${guid}`,
|
||||||
|
responseType: 'text',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// db
|
// db
|
||||||
|
|
||||||
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
|
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
|
||||||
@@ -333,4 +342,13 @@ export class LiveApiService extends ApiService {
|
|||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sideloadPackage(
|
||||||
|
params: RR.SideloadPackageReq,
|
||||||
|
): Promise<RR.SideloadPacakgeRes> {
|
||||||
|
return this.http.rpcRequest({
|
||||||
|
method: 'package.sideload',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export class MockApiService extends ApiService {
|
|||||||
return markdown
|
return markdown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadPackage(guid: string, body: ArrayBuffer): Promise<string> {
|
||||||
|
await pauseFor(2000)
|
||||||
|
return 'success'
|
||||||
|
}
|
||||||
|
|
||||||
// db
|
// db
|
||||||
|
|
||||||
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
|
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
|
||||||
@@ -750,6 +755,13 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sideloadPackage(
|
||||||
|
params: RR.SideloadPackageReq,
|
||||||
|
): Promise<RR.SideloadPacakgeRes> {
|
||||||
|
await pauseFor(2000)
|
||||||
|
return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated
|
||||||
|
}
|
||||||
|
|
||||||
private async updateProgress(id: string): Promise<void> {
|
private async updateProgress(id: string): Promise<void> {
|
||||||
const progress = { ...PROGRESS }
|
const progress = { ...PROGRESS }
|
||||||
const phases = [
|
const phases = [
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null {
|
|||||||
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
|
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHealthStatus(status: Status): HealthStatus | null {
|
function getHealthStatus(
|
||||||
|
status: Status,
|
||||||
|
hasHealthChecks: boolean,
|
||||||
|
): HealthStatus | null {
|
||||||
if (status.main.status !== PackageMainStatus.Running || !status.main.health) {
|
if (status.main.status !== PackageMainStatus.Running || !status.main.health) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,11 @@
|
|||||||
"pathRewrite": {
|
"pathRewrite": {
|
||||||
"^/public": ""
|
"^/public": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/rest/rpc/*": {
|
||||||
|
"target": "http://<CHANGE_ME>/rest/rpc",
|
||||||
|
"pathRewrite": {
|
||||||
|
"^/rest/rpc": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user