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
}
'release-notes': string
license: string // name
license: string // type of license
'wrapper-repo': Url
'upstream-repo': Url
'support-site': Url

View File

@@ -10,7 +10,7 @@
<ion-content class="ion-text-center">
<!-- file upload -->
<div
*ngIf="!toUpload.file"
*ngIf="!toUpload.file; else fileUploaded"
class="drop-area"
[class.drop-area_mobile]="isMobile"
appDnd
@@ -39,56 +39,61 @@
</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>
<ng-template #fileUploaded>
<div class="drop-area_filled">
<h4>
<ion-icon
*ngIf="uploadState?.invalid"
name="close-circle-outline"
color="danger"
class="inline"
></ion-icon>
<ion-icon
*ngIf="!uploadState?.invalid"
class="inline"
name="checkmark-circle-outline"
color="success"
></ion-icon>
{{ uploadState?.message }}
</h4>
<div class="box" *ngIf="toUpload.icon && toUpload.manifest">
<div 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
[alt]="toUpload.manifest.title + ' Icon'"
[src]="toUpload.icon | trustUrl"
/>
<h2>{{ toUpload.manifest.title }}</h2>
<p>{{ toUpload.manifest.version | displayEmver }}</p>
</div>
</div>
</div>
<ion-button color="primary" (click)="handleUpload()">
Upload & Install
<ion-button
*ngIf="!toUpload.icon && !toUpload.manifest; else uploadButton"
color="primary"
(click)="clearToUpload()"
>
Try again
</ion-button>
<ng-template #uploadButton>
<ion-button color="primary" (click)="handleUpload()">
Upload & Install
</ion-button>
</ng-template>
</div>
</div>
</ng-template>
</ion-content>

View File

@@ -1,8 +1,5 @@
.inline {
* {
vertical-align: initial;
padding-right: 5px;
}
vertical-align: initial;
}
.area {
@@ -39,7 +36,7 @@
margin: 60px;
padding: 30px;
min-height: 600px;
min-width: 400px;
}
&_mobile {
@@ -51,10 +48,15 @@
}
}
.box {
display: flex;
justify-content: space-evenly
}
.service-card {
background: radial-gradient(var(--ion-color-step-100), transparent);
min-width: 200px;
max-width: 300px;
max-width: 200px;
height: auto;
padding: 0;
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 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',
@@ -28,8 +30,10 @@ export class SideloadPage {
file: null,
}
onTor = this.config.isTor()
valid: boolean
message: string
uploadState: {
invalid: boolean
message: string
}
constructor(
private readonly loadingCtrl: LoadingController,
@@ -49,29 +53,27 @@ export class SideloadPage {
this.setFile(files)
}
async setFile(files?: File[]) {
const loader = await this.loadingCtrl.create({
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)),
)
const file = files[0]
if (!file) return
this.toUpload.file = file
this.uploadState = await this.validateS9pk(file)
}
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)) {
loader.dismiss()
this.valid = true
this.message = 'A valid package file has been detected!'
await this.parseS9pk(this.toUpload.file)
await this.parseS9pk(file)
return {
invalid: false,
message: 'A valid package file has been detected!',
}
} else {
loader.dismiss()
this.valid = false
this.message = 'Invalid package file'
return {
invalid: true,
message: 'Invalid package file',
}
}
}
@@ -93,7 +95,7 @@ export class SideloadPage {
icon: this.toUpload.icon!,
})
this.api
.uploadPackage(guid, await readBlobToArrayBuffer(this.toUpload.file!))
.uploadPackage(guid, await blobToBuffer(this.toUpload.file!))
.catch(e => {
this.errToast.present(e)
})
@@ -108,15 +110,13 @@ export class SideloadPage {
}
}
async parseS9pk(file: Blob) {
async parseS9pk(file: File) {
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(),
),
await blobToBuffer(file.slice(99, 103) ?? new Blob()),
).getUint32(0, false)
await getPositions(start, end, file, positions, tocLength as any)
@@ -125,7 +125,7 @@ export class SideloadPage {
}
async getManifest(positions: Positions, file: Blob) {
const data = await readBlobToArrayBuffer(
const data = await blobToBuffer(
file.slice(
Number(positions['manifest'][0]),
Number(positions['manifest'][0]) + Number(positions['manifest'][1]),
@@ -135,11 +135,15 @@ export class SideloadPage {
}
async getIcon(positions: Positions, file: Blob) {
const contentType = `image/${this.toUpload.manifest?.assets.icon
.split('.')
.pop()}`
const data = file.slice(
Number(positions['icon'][0]),
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 end = initialEnd
const titleLength = new Uint8Array(
await readBlobToArrayBuffer(file.slice(start, end)),
await blobToBuffer(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)),
await blobToBuffer(file.slice(start, end)),
).getBigUint64(0, false)
start = end
end = start + 8
const chapterLength = new DataView(
await readBlobToArrayBuffer(file.slice(start, end)),
await blobToBuffer(file.slice(start, end)),
).getBigUint64(0, false)
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()
reader.readAsDataURL(f)
return new Promise(resolve => {
return new Promise((resolve, reject) => {
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()
reader.readAsArrayBuffer(f)
return new Promise(resolve => {
return new Promise((resolve, reject) => {
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.',
},
'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',
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
'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.',
},
'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',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'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.',
},
'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',
'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper',
'upstream-repo': 'https://github.com/Kixunil/btc-rpc-proxy',

View File

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

View File

@@ -56,6 +56,14 @@ export const mockPatchData: DataModel = {
long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
},
'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',
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
'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.',
},
'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',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'upstream-repo': 'https://github.com/lightningnetwork/lnd',

View File

@@ -50,15 +50,13 @@ export class EOSService {
) {}
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({
'server-id': server.id,
'eos-version': version,
})
const updateAvailable =
this.emver.compare(
this.eos.version,
this.patch.getData()['server-info'].version,
) === 1
const updateAvailable = this.emver.compare(this.eos.version, version) === 1
this.updateAvailable$.next(updateAvailable)
return updateAvailable
}

View File

@@ -2,6 +2,7 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { Url } from '@start9labs/shared'
import { MarketplaceManifest } from '@start9labs/marketplace'
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'
import { string } from 'ts-matches'
export interface DataModel {
'server-info': ServerInfo
@@ -120,6 +121,14 @@ export interface CurrentDependencyInfo {
}
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
'health-checks': Record<
string,