mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -249,6 +249,7 @@ export module RR {
|
||||
export type GetMarketplaceDataRes = MarketplaceData
|
||||
|
||||
export type GetMarketplaceEOSReq = {
|
||||
'server-id': string
|
||||
'eos-version': string
|
||||
}
|
||||
export type GetMarketplaceEOSRes = MarketplaceEOS
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user