From 7916a2352fd03d73eaf9e77d582438b612295daf Mon Sep 17 00:00:00 2001 From: Lucy C <12953208+elvece@users.noreply.github.com> Date: Mon, 13 Jun 2022 12:41:19 -0600 Subject: [PATCH] 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 --- .gitignore | 1 + frontend/package-lock.json | 38 +++- frontend/package.json | 4 +- .../projects/shared/styles/variables.scss | 38 ++-- .../app/preloader/preloader.component.html | 3 +- .../app/app/preloader/preloader.component.ts | 1 + .../server-routes/server-routing.module.ts | 5 + .../server-show/server-show.page.ts | 11 + .../server-routes/sideload/dnd.directive.ts | 39 ++++ .../server-routes/sideload/sideload.module.ts | 26 +++ .../server-routes/sideload/sideload.page.html | 94 ++++++++ .../server-routes/sideload/sideload.page.scss | 90 ++++++++ .../server-routes/sideload/sideload.page.ts | 205 ++++++++++++++++++ .../ui/src/app/services/api/api.types.ts | 7 + .../app/services/api/embassy-api.service.ts | 7 + .../services/api/embassy-live-api.service.ts | 18 ++ .../services/api/embassy-mock-api.service.ts | 12 + .../services/pkg-status-rendering.service.ts | 5 +- frontend/proxy.conf-sample.json | 6 + 19 files changed, 582 insertions(+), 28 deletions(-) create mode 100644 frontend/projects/ui/src/app/pages/server-routes/sideload/dnd.directive.ts create mode 100644 frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts create mode 100644 frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html create mode 100644 frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.scss create mode 100644 frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts diff --git a/.gitignore b/.gitignore index 40d14db5b..d902e3c4f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /product_key.txt /*_product_key.txt .vscode/settings.json +deploy_web.sh \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 58440d553..a242987b0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,8 @@ "@start9labs/emver": "^0.1.5", "aes-js": "^3.1.2", "ansi-to-html": "^0.7.2", + "cbor": "npm:@jprochazk/cbor@^0.4.9", + "cbor-web": "^8.1.0", "core-js": "^3.21.1", "dompurify": "^2.3.6", "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": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -12290,9 +12306,9 @@ "dev": true }, "node_modules/semver-regex": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.3.tgz", - "integrity": "sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz", + "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==", "dev": true, "engines": { "node": ">=8" @@ -17690,6 +17706,16 @@ "integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==", "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": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -23321,9 +23347,9 @@ "dev": true }, "semver-regex": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.3.tgz", - "integrity": "sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz", + "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==", "dev": true }, "send": { diff --git a/frontend/package.json b/frontend/package.json index 23ebf90b8..a5275347c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,8 @@ "@start9labs/emver": "^0.1.5", "aes-js": "^3.1.2", "ansi-to-html": "^0.7.2", + "cbor": "npm:@jprochazk/cbor@^0.4.9", + "cbor-web": "^8.1.0", "core-js": "^3.21.1", "dompurify": "^2.3.6", "fast-json-patch": "^3.1.1", @@ -49,8 +51,8 @@ "patch-db-client": "file: ../../../patch-db/client", "pbkdf2": "^3.1.2", "rxjs": "^6.6.7", - "tslib": "^2.3.0", "ts-matches": "^5.1.0", + "tslib": "^2.3.0", "uuid": "^8.3.2", "zone.js": "^0.11.5" }, diff --git a/frontend/projects/shared/styles/variables.scss b/frontend/projects/shared/styles/variables.scss index ef72a4110..12c95fcd6 100644 --- a/frontend/projects/shared/styles/variables.scss +++ b/frontend/projects/shared/styles/variables.scss @@ -4,65 +4,65 @@ /** Ionic CSS Variables **/ :root { --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-rgb: 255,255,255; + --ion-color-primary-contrast-rgb: 255, 255, 255; --ion-color-primary-shade: #3a7be0; --ion-color-primary-tint: #5598ff; --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-rgb: 255,255,255; + --ion-color-secondary-contrast-rgb: 255, 255, 255; --ion-color-secondary-shade: #46b0e0; --ion-color-secondary-tint: #62ceff; --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-rgb: 255,255,255; + --ion-color-tertiary-contrast-rgb: 255, 255, 255; --ion-color-tertiary-shade: #5d58e0; --ion-color-tertiary-tint: #7974ff; --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-rgb: 0,0,0; + --ion-color-success-contrast-rgb: 0, 0, 0; --ion-color-success-shade: #29c467; --ion-color-success-tint: #44e283; --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-rgb: 0,0,0; + --ion-color-warning-contrast-rgb: 0, 0, 0; --ion-color-warning-shade: #e0bb2e; --ion-color-warning-tint: #ffd948; --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-rgb: 255,255,255; + --ion-color-danger-contrast-rgb: 255, 255, 255; --ion-color-danger-shade: #e04055; --ion-color-danger-tint: #ff5b71; --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-rgb: 0,0,0; + --ion-color-light-contrast-rgb: 0, 0, 0; --ion-color-light-shade: #000000; --ion-color-light-tint: #000000; --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-rgb: 255,255,255; + --ion-color-medium-contrast-rgb: 255, 255, 255; --ion-color-medium-shade: #1e2023; --ion-color-medium-tint: #383a3e; --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-rgb: 0,0,0; + --ion-color-dark-contrast-rgb: 0, 0, 0; --ion-color-dark-shade: #bfbfbf; --ion-color-dark-tint: #d8d8d8; @@ -85,4 +85,4 @@ --ion-color-step-850: #dbdbdb; --ion-color-step-900: #e7e7e7; --ion-color-step-950: #f3f3f3; -} +} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.html b/frontend/projects/ui/src/app/app/preloader/preloader.component.html index 5662f3e76..7d486676b 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.component.html +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.html @@ -33,6 +33,7 @@ + @@ -68,4 +69,4 @@ - \ No newline at end of file + diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts index e18e96ba7..b5393a724 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts @@ -60,6 +60,7 @@ const ICONS = [ 'play-outline', 'power', 'pulse', + 'push-outline', 'qr-code-outline', 'receipt-outline', 'refresh', diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts index 918c312b2..2549db866 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts @@ -67,6 +67,11 @@ const routes: Routes = [ loadChildren: () => import('./sessions/sessions.module').then(m => m.SessionsPageModule), }, + { + path: 'sideload', + loadChildren: () => + import('./sideload/sideload.module').then(m => m.SideloadPageModule), + }, { path: 'specs', loadChildren: () => diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index 6e2fa550a..d615996ca 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -331,6 +331,17 @@ export class ServerShowPage { detail: true, 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', description: 'Add or remove marketplaces', diff --git a/frontend/projects/ui/src/app/pages/server-routes/sideload/dnd.directive.ts b/frontend/projects/ui/src/app/pages/server-routes/sideload/dnd.directive.ts new file mode 100644 index 000000000..35daf09ef --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/sideload/dnd.directive.ts @@ -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 = 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) + } +} diff --git a/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts b/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts new file mode 100644 index 000000000..863b2d127 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html b/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html new file mode 100644 index 000000000..3929a8ad0 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html @@ -0,0 +1,94 @@ + + + + + + Manually install a service + + + + + +
+ +

To install a service manually, upload the s9pk here

+

+ Tip: switch to LAN for faster uploads. +

+
+ + + + +
+ +
+
+ +

{{ message }}

+
+ +
+
+ +

{{ message }}

+
+ + Try again + +
+
+
+
+
+
+ + + +
+
+ +

{{ toUpload.manifest.title }}

+

{{ toUpload.manifest.version | displayEmver }}

+
+
+ + Upload & Install + +
+
+
diff --git a/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.scss b/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.scss new file mode 100644 index 000000000..4d96f15a7 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.scss @@ -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; + } +} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts b/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts new file mode 100644 index 000000000..a0d38ec49 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts @@ -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 { + const reader = new FileReader() + reader.readAsDataURL(f) + return new Promise(resolve => { + reader.onloadend = () => { + resolve(reader.result as string) + } + }) +} + +async function readBlobToArrayBuffer(f: Blob): Promise { + 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 +} diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index 6e3f568ae..ca09f5846 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -5,6 +5,7 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types' import { DataModel, DependencyError, + Manifest, } from 'src/app/services/patch-db/data-model' export module RR { @@ -239,6 +240,12 @@ export module RR { spec: ConfigSpec } + export interface SideloadPackageReq { + manifest: Manifest + icon: string // base64 + } + export type SideloadPacakgeRes = string //guid + // marketplace export type GetMarketplaceDataReq = { 'server-id': string } diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index d6c475ae2..dca58e3e6 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -27,6 +27,9 @@ export abstract class ApiService implements Source, Http { // for getting static files: ex icons, instructions, licenses abstract getStatic(url: string): Promise + // for sideloading packages + abstract uploadPackage(guid: string, body: ArrayBuffer): Promise + // db abstract getRevisions(since: number): Promise @@ -260,6 +263,10 @@ export abstract class ApiService implements Source, Http { deleteRecoveredPackage = (params: RR.UninstallPackageReq) => this.syncResponse(() => this.deleteRecoveredPackageRaw(params))() + abstract sideloadPackage( + params: RR.SideloadPackageReq, + ): Promise + // 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 // state change you'd like to enact prior to request and expired when request terminates. diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index 1dc34fe06..c487c0960 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -23,6 +23,15 @@ export class LiveApiService extends ApiService { }) } + async uploadPackage(guid: string, body: ArrayBuffer): Promise { + return this.http.httpRequest({ + method: Method.POST, + body, + url: `/rest/rpc/${guid}`, + responseType: 'text', + }) + } + // db async getRevisions(since: number): Promise { @@ -333,4 +342,13 @@ export class LiveApiService extends ApiService { params, }) } + + async sideloadPackage( + params: RR.SideloadPackageReq, + ): Promise { + return this.http.rpcRequest({ + method: 'package.sideload', + params, + }) + } } diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index b10516660..625e7e160 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -48,6 +48,11 @@ export class MockApiService extends ApiService { return markdown } + async uploadPackage(guid: string, body: ArrayBuffer): Promise { + await pauseFor(2000) + return 'success' + } + // db async getRevisions(since: number): Promise { @@ -750,6 +755,13 @@ export class MockApiService extends ApiService { } } + async sideloadPackage( + params: RR.SideloadPackageReq, + ): Promise { + await pauseFor(2000) + return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated + } + private async updateProgress(id: string): Promise { const progress = { ...PROGRESS } const phases = [ diff --git a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts index 914c54fd3..db9597d4c 100644 --- a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -47,7 +47,10 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null { 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) { return null } diff --git a/frontend/proxy.conf-sample.json b/frontend/proxy.conf-sample.json index 7c4053c78..23bc247ca 100644 --- a/frontend/proxy.conf-sample.json +++ b/frontend/proxy.conf-sample.json @@ -12,5 +12,11 @@ "pathRewrite": { "^/public": "" } + }, + "/rest/rpc/*": { + "target": "http:///rest/rpc", + "pathRewrite": { + "^/rest/rpc": "" + } } }