Fix/sideload icon type (#1577)

* add content type to icon dataURL

* better handling of blob reading; remove verifying loader and reorganize html

* clean up PR feedback and create validation fn instead of boolean

* grpup upload state into one type

* better organize validation

* add server id to eos check for updates req

* fix patchdb to latest

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Lucy C
2022-06-27 15:25:42 -06:00
committed by GitHub
parent 123f71cb86
commit f22f11eb58
9 changed files with 186 additions and 102 deletions

View File

@@ -10,7 +10,7 @@ export interface MarketplaceManifest<T = unknown> {
long: string long: string
} }
'release-notes': string 'release-notes': string
license: string // name license: string // type of license
'wrapper-repo': Url 'wrapper-repo': Url
'upstream-repo': Url 'upstream-repo': Url
'support-site': Url 'support-site': Url

View File

@@ -10,7 +10,7 @@
<ion-content class="ion-text-center"> <ion-content class="ion-text-center">
<!-- file upload --> <!-- file upload -->
<div <div
*ngIf="!toUpload.file" *ngIf="!toUpload.file; else fileUploaded"
class="drop-area" class="drop-area"
[class.drop-area_mobile]="isMobile" [class.drop-area_mobile]="isMobile"
appDnd appDnd
@@ -39,56 +39,61 @@
</ion-button> </ion-button>
</div> </div>
<!-- file uploaded --> <!-- file uploaded -->
<div class="drop-area_filled" *ngIf="toUpload.file"> <ng-template #fileUploaded>
<div class="inline" *ngIf="valid; else invalid"> <div class="drop-area_filled">
<ion-icon name="checkmark-circle-outline" color="success"></ion-icon> <h4>
<h4>{{ message }}</h4> <ion-icon
</div> *ngIf="uploadState?.invalid"
<ng-template #invalid> name="close-circle-outline"
<div class="area"> color="danger"
<div class="inline"> class="inline"
<ion-icon ></ion-icon>
*ngIf="!valid" <ion-icon
name="close-circle-outline" *ngIf="!uploadState?.invalid"
color="danger" class="inline"
></ion-icon> name="checkmark-circle-outline"
<h4><ion-text color="danger">{{ message }}</ion-text></h4> color="success"
</div> ></ion-icon>
<ion-button color="primary" (click)="clearToUpload()"> {{ uploadState?.message }}
Try again </h4>
</ion-button> <div class="box" *ngIf="toUpload.icon && toUpload.manifest">
</div> <div class="service-card">
</ng-template> <div class="row row_end">
<br /> <ion-button
<div *ngIf="valid"> style="
<div *ngIf="toUpload.manifest " class="service-card"> --background-hover: transparent;
<div class="row row_end"> --padding-end: 0px;
<ion-button --padding-start: 0px;
style=" "
--background-hover: transparent; fill="clear"
--padding-end: 0px; size="small"
--padding-start: 0px; (click)="clearToUpload()"
" >
fill="clear" <ion-icon slot="icon-only" name="close" color="danger"></ion-icon>
size="small" </ion-button>
(click)="clearToUpload()" </div>
> <div class="row">
<ion-icon slot="icon-only" name="close" color="danger"></ion-icon> <img
</ion-button> [alt]="toUpload.manifest.title + ' Icon'"
</div> [src]="toUpload.icon | trustUrl"
<div class="row"> />
<img <h2>{{ toUpload.manifest.title }}</h2>
*ngIf="toUpload.icon" <p>{{ toUpload.manifest.version | displayEmver }}</p>
[alt]="toUpload.manifest.title + ' Icon'" </div>
[src]="toUpload.icon | trustUrl"
/>
<h2>{{ toUpload.manifest.title }}</h2>
<p>{{ toUpload.manifest.version | displayEmver }}</p>
</div> </div>
</div> </div>
<ion-button color="primary" (click)="handleUpload()"> <ion-button
Upload & Install *ngIf="!toUpload.icon && !toUpload.manifest; else uploadButton"
color="primary"
(click)="clearToUpload()"
>
Try again
</ion-button> </ion-button>
<ng-template #uploadButton>
<ion-button color="primary" (click)="handleUpload()">
Upload & Install
</ion-button>
</ng-template>
</div> </div>
</div> </ng-template>
</ion-content> </ion-content>

View File

@@ -1,8 +1,5 @@
.inline { .inline {
* { vertical-align: initial;
vertical-align: initial;
padding-right: 5px;
}
} }
.area { .area {
@@ -39,7 +36,7 @@
margin: 60px; margin: 60px;
padding: 30px; padding: 30px;
min-height: 600px; min-height: 600px;
min-width: 400px;
} }
&_mobile { &_mobile {
@@ -51,10 +48,15 @@
} }
} }
.box {
display: flex;
justify-content: space-evenly
}
.service-card { .service-card {
background: radial-gradient(var(--ion-color-step-100), transparent); background: radial-gradient(var(--ion-color-step-100), transparent);
min-width: 200px; min-width: 200px;
max-width: 300px; max-width: 200px;
height: auto; height: auto;
padding: 0; padding: 0;
display: flex; display: flex;

View File

@@ -5,12 +5,14 @@ import { Manifest } from 'src/app/services/patch-db/data-model'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import cbor from 'cbor' import cbor from 'cbor'
import { ErrorToastService } from '@start9labs/shared' import { ErrorToastService } from '@start9labs/shared'
interface Positions { interface Positions {
[key: string]: [bigint, bigint] // [position, length] [key: string]: [bigint, bigint] // [position, length]
} }
const MAGIC = new Uint8Array([59, 59]) const MAGIC = new Uint8Array([59, 59])
const VERSION = new Uint8Array([1]) const VERSION = new Uint8Array([1])
@Component({ @Component({
selector: 'sideload', selector: 'sideload',
templateUrl: './sideload.page.html', templateUrl: './sideload.page.html',
@@ -28,8 +30,10 @@ export class SideloadPage {
file: null, file: null,
} }
onTor = this.config.isTor() onTor = this.config.isTor()
valid: boolean uploadState: {
message: string invalid: boolean
message: string
}
constructor( constructor(
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
@@ -49,29 +53,27 @@ export class SideloadPage {
this.setFile(files) this.setFile(files)
} }
async setFile(files?: File[]) { async setFile(files?: File[]) {
const loader = await this.loadingCtrl.create({
message: 'Verifying package',
cssClass: 'loader',
})
await loader.present()
if (!files || !files.length) return if (!files || !files.length) return
this.toUpload.file = files[0] const file = files[0]
// verify valid s9pk if (!file) return
const magic = new Uint8Array( this.toUpload.file = file
await readBlobToArrayBuffer(this.toUpload.file.slice(0, 2)), this.uploadState = await this.validateS9pk(file)
) }
const version = new Uint8Array(
await readBlobToArrayBuffer(this.toUpload.file.slice(2, 3)), async validateS9pk(file: File) {
) const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2)))
const version = new Uint8Array(await blobToBuffer(file.slice(2, 3)))
if (compare(magic, MAGIC) && compare(version, VERSION)) { if (compare(magic, MAGIC) && compare(version, VERSION)) {
loader.dismiss() await this.parseS9pk(file)
this.valid = true return {
this.message = 'A valid package file has been detected!' invalid: false,
await this.parseS9pk(this.toUpload.file) message: 'A valid package file has been detected!',
}
} else { } else {
loader.dismiss() return {
this.valid = false invalid: true,
this.message = 'Invalid package file' message: 'Invalid package file',
}
} }
} }
@@ -93,7 +95,7 @@ export class SideloadPage {
icon: this.toUpload.icon!, icon: this.toUpload.icon!,
}) })
this.api this.api
.uploadPackage(guid, await readBlobToArrayBuffer(this.toUpload.file!)) .uploadPackage(guid, await blobToBuffer(this.toUpload.file!))
.catch(e => { .catch(e => {
this.errToast.present(e) this.errToast.present(e)
}) })
@@ -108,15 +110,13 @@ export class SideloadPage {
} }
} }
async parseS9pk(file: Blob) { async parseS9pk(file: File) {
const positions: Positions = {} const positions: Positions = {}
// magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point // magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point
let start = 103 let start = 103
let end = start + 1 // 104 let end = start + 1 // 104
const tocLength = new DataView( const tocLength = new DataView(
await readBlobToArrayBuffer( await blobToBuffer(file.slice(99, 103) ?? new Blob()),
this.toUpload.file?.slice(99, 103) ?? new Blob(),
),
).getUint32(0, false) ).getUint32(0, false)
await getPositions(start, end, file, positions, tocLength as any) await getPositions(start, end, file, positions, tocLength as any)
@@ -125,7 +125,7 @@ export class SideloadPage {
} }
async getManifest(positions: Positions, file: Blob) { async getManifest(positions: Positions, file: Blob) {
const data = await readBlobToArrayBuffer( const data = await blobToBuffer(
file.slice( file.slice(
Number(positions['manifest'][0]), Number(positions['manifest'][0]),
Number(positions['manifest'][0]) + Number(positions['manifest'][1]), Number(positions['manifest'][0]) + Number(positions['manifest'][1]),
@@ -135,11 +135,15 @@ export class SideloadPage {
} }
async getIcon(positions: Positions, file: Blob) { async getIcon(positions: Positions, file: Blob) {
const contentType = `image/${this.toUpload.manifest?.assets.icon
.split('.')
.pop()}`
const data = file.slice( const data = file.slice(
Number(positions['icon'][0]), Number(positions['icon'][0]),
Number(positions['icon'][0]) + Number(positions['icon'][1]), Number(positions['icon'][0]) + Number(positions['icon'][1]),
contentType,
) )
this.toUpload.icon = await readBlobAsDataURL(data) this.toUpload.icon = await blobToDataURL(data)
} }
} }
@@ -153,18 +157,18 @@ async function getPositions(
let start = initialStart let start = initialStart
let end = initialEnd let end = initialEnd
const titleLength = new Uint8Array( const titleLength = new Uint8Array(
await readBlobToArrayBuffer(file.slice(start, end)), await blobToBuffer(file.slice(start, end)),
)[0] )[0]
const tocTitle = await file.slice(end, end + titleLength).text() const tocTitle = await file.slice(end, end + titleLength).text()
start = end + titleLength start = end + titleLength
end = start + 8 end = start + 8
const chapterPosition = new DataView( const chapterPosition = new DataView(
await readBlobToArrayBuffer(file.slice(start, end)), await blobToBuffer(file.slice(start, end)),
).getBigUint64(0, false) ).getBigUint64(0, false)
start = end start = end
end = start + 8 end = start + 8
const chapterLength = new DataView( const chapterLength = new DataView(
await readBlobToArrayBuffer(file.slice(start, end)), await blobToBuffer(file.slice(start, end)),
).getBigUint64(0, false) ).getBigUint64(0, false)
positions[tocTitle] = [chapterPosition, chapterLength] positions[tocTitle] = [chapterPosition, chapterLength]
@@ -175,23 +179,48 @@ async function getPositions(
} }
} }
async function readBlobAsDataURL(f: Blob): Promise<string> { async function readBlobAsDataURL(
f: Blob | File,
): Promise<string | ArrayBuffer | null> {
const reader = new FileReader() const reader = new FileReader()
reader.readAsDataURL(f) return new Promise((resolve, reject) => {
return new Promise(resolve => {
reader.onloadend = () => { reader.onloadend = () => {
resolve(reader.result as string) resolve(reader.result)
} }
reader.readAsDataURL(f)
reader.onerror = _ => reject(new Error('error reading blob'))
}) })
} }
async function blobToDataURL(data: Blob | File): Promise<string> {
const res = await readBlobAsDataURL(data)
if (res instanceof ArrayBuffer)
throw new Error('readBlobAsDataURL response should not be an array buffer')
if (res == null)
throw new Error('readBlobAsDataURL response should not be null')
if (typeof res === 'string') return res
throw new Error('no possible blob to data url resolution found')
}
async function readBlobToArrayBuffer(f: Blob): Promise<ArrayBuffer> { async function blobToBuffer(data: Blob | File): Promise<ArrayBuffer> {
const res = await readBlobToArrayBuffer(data)
if (res instanceof String)
throw new Error('readBlobToArrayBuffer response should not be a string')
if (res == null)
throw new Error('readBlobToArrayBuffer response should not be null')
if (res instanceof ArrayBuffer) return res
throw new Error('no possible blob to array buffer resolution found')
}
async function readBlobToArrayBuffer(
f: Blob | File,
): Promise<string | ArrayBuffer | null> {
const reader = new FileReader() const reader = new FileReader()
reader.readAsArrayBuffer(f) return new Promise((resolve, reject) => {
return new Promise(resolve => {
reader.onloadend = () => { reader.onloadend = () => {
resolve(reader.result as ArrayBuffer) resolve(reader.result)
} }
reader.readAsArrayBuffer(f)
reader.onerror = _ => reject(new Error('error reading blob'))
}) })
} }

View File

@@ -50,6 +50,14 @@ export module Mock {
long: 'Bitcoin is a decentralized consensus protocol and settlement network.', long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
}, },
'release-notes': 'Taproot, Schnorr, and more.', 'release-notes': 'Taproot, Schnorr, and more.',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper', 'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
'upstream-repo': 'https://github.com/bitcoin/bitcoin', 'upstream-repo': 'https://github.com/bitcoin/bitcoin',
@@ -348,6 +356,14 @@ export module Mock {
long: 'More info about LND. More info about LND. More info about LND.', long: 'More info about LND. More info about LND. More info about LND.',
}, },
'release-notes': 'Dual funded channels!', 'release-notes': 'Dual funded channels!',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper', 'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'upstream-repo': 'https://github.com/lightningnetwork/lnd', 'upstream-repo': 'https://github.com/lightningnetwork/lnd',
@@ -494,6 +510,14 @@ export module Mock {
long: 'More info about Bitcoin Proxy. More info about Bitcoin Proxy. More info about Bitcoin Proxy.', long: 'More info about Bitcoin Proxy. More info about Bitcoin Proxy. More info about Bitcoin Proxy.',
}, },
'release-notes': 'Even better support for Bitcoin and wallets!', 'release-notes': 'Even better support for Bitcoin and wallets!',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper', 'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper',
'upstream-repo': 'https://github.com/Kixunil/btc-rpc-proxy', 'upstream-repo': 'https://github.com/Kixunil/btc-rpc-proxy',

View File

@@ -249,6 +249,7 @@ export module RR {
export type GetMarketplaceDataRes = MarketplaceData export type GetMarketplaceDataRes = MarketplaceData
export type GetMarketplaceEOSReq = { export type GetMarketplaceEOSReq = {
'server-id': string
'eos-version': string 'eos-version': string
} }
export type GetMarketplaceEOSRes = MarketplaceEOS export type GetMarketplaceEOSRes = MarketplaceEOS

View File

@@ -56,6 +56,14 @@ export const mockPatchData: DataModel = {
long: 'Bitcoin is a decentralized consensus protocol and settlement network.', long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
}, },
'release-notes': 'Taproot, Schnorr, and more.', 'release-notes': 'Taproot, Schnorr, and more.',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper', 'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
'upstream-repo': 'https://github.com/bitcoin/bitcoin', 'upstream-repo': 'https://github.com/bitcoin/bitcoin',
@@ -437,6 +445,14 @@ export const mockPatchData: DataModel = {
long: 'More info about LND. More info about LND. More info about LND.', long: 'More info about LND. More info about LND. More info about LND.',
}, },
'release-notes': 'Dual funded channels!', 'release-notes': 'Dual funded channels!',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper', 'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'upstream-repo': 'https://github.com/lightningnetwork/lnd', 'upstream-repo': 'https://github.com/lightningnetwork/lnd',

View File

@@ -50,15 +50,13 @@ export class EOSService {
) {} ) {}
async getEOS(): Promise<boolean> { async getEOS(): Promise<boolean> {
const version = this.patch.getData()['server-info'].version const server = this.patch.getData()['server-info']
const version = server.version
this.eos = await this.api.getEos({ this.eos = await this.api.getEos({
'server-id': server.id,
'eos-version': version, 'eos-version': version,
}) })
const updateAvailable = const updateAvailable = this.emver.compare(this.eos.version, version) === 1
this.emver.compare(
this.eos.version,
this.patch.getData()['server-info'].version,
) === 1
this.updateAvailable$.next(updateAvailable) this.updateAvailable$.next(updateAvailable)
return updateAvailable return updateAvailable
} }

View File

@@ -2,6 +2,7 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { Url } from '@start9labs/shared' import { Url } from '@start9labs/shared'
import { MarketplaceManifest } from '@start9labs/marketplace' import { MarketplaceManifest } from '@start9labs/marketplace'
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info' import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'
import { string } from 'ts-matches'
export interface DataModel { export interface DataModel {
'server-info': ServerInfo 'server-info': ServerInfo
@@ -120,6 +121,14 @@ export interface CurrentDependencyInfo {
} }
export interface Manifest extends MarketplaceManifest<DependencyConfig | null> { export interface Manifest extends MarketplaceManifest<DependencyConfig | null> {
assets: {
license: string // filename
instructions: string // filename
icon: string // filename
docker_images: string // filename
assets: string // path to assets folder
scripts: string // path to scripts folder
}
main: ActionImpl main: ActionImpl
'health-checks': Record< 'health-checks': Record<
string, string,