Files
start-os/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts
Jade fb074c8c32 036 migration (#2750)
* chore: convert to use a value, cause why not

* wip: Add the up for this going up

* wip: trait changes

* wip: Add in some more of the private transformations

* chore(wip): Adding the ssh_keys todo

* wip: Add cifs

* fix migration structure

* chore: Fix the trait for the version

* wip(feat): Notifications are in the system

* fix marker trait hell

* handle key todos

* wip: Testing the migration in a system.

* fix pubkey parser

* fix: migration works

* wip: Trying to get the migration stuff?

* fix: Can now install the packages that we wanted, yay!"

* Merge branch 'next/minor' of github.com:Start9Labs/start-os into feat/migration

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2024-10-16 10:09:30 -06:00

264 lines
7.4 KiB
TypeScript

import { Component } from '@angular/core'
import { isPlatform } from '@ionic/angular'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { S9pk, T } from '@start9labs/start-sdk'
import cbor from 'cbor'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
import { SideloadService } from './sideload.service'
import { firstValueFrom } from 'rxjs'
import mime from 'mime'
interface Positions {
[key: string]: [bigint, bigint] // [position, length]
}
const MAGIC = new Uint8Array([59, 59])
const VERSION_1 = new Uint8Array([1])
const VERSION_2 = new Uint8Array([2])
@Component({
selector: 'sideload',
templateUrl: './sideload.page.html',
styleUrls: ['./sideload.page.scss'],
})
export class SideloadPage {
isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android')
toUpload: {
manifest: { title: string; version: string } | null
icon: string | null
file: File | null
} = {
manifest: null,
icon: null,
file: null,
}
onTor = this.config.isTor()
uploadState?: {
invalid: boolean
message: string
}
readonly progress$ = this.sideloadService.progress$
constructor(
private readonly loader: LoadingService,
private readonly api: ApiService,
private readonly errorService: ErrorService,
private readonly config: ConfigService,
private readonly sideloadService: SideloadService,
) {}
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[]) {
if (!files || !files.length) return
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)) {
try {
if (compare(version, VERSION_1)) {
await this.parseS9pkV1(file)
return {
invalid: false,
message: 'A valid package file has been detected!',
}
} else if (compare(version, VERSION_2)) {
await this.parseS9pkV2(file)
return {
invalid: false,
message: 'A valid package file has been detected!',
}
} else {
console.error(version)
return {
invalid: true,
message: 'Invalid package file',
}
}
} catch (e) {
console.error(e)
return {
invalid: true,
message:
e instanceof Error
? `Invalid package file: ${e.message}`
: 'Invalid package file',
}
}
} else {
return {
invalid: true,
message: 'Invalid package file',
}
}
}
clearToUpload() {
this.toUpload.file = null
this.toUpload.manifest = null
this.toUpload.icon = null
}
async handleUpload() {
const loader = this.loader.open('Starting upload').subscribe()
try {
const res = await this.api.sideloadPackage()
this.sideloadService.followProgress(res.progress)
this.api
.uploadPackage(res.upload, this.toUpload.file!)
.catch(e => console.error(e))
await firstValueFrom(this.sideloadService.websocketConnected$)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
this.clearToUpload()
}
}
async parseS9pkV1(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 blobToBuffer(file.slice(99, 103) ?? new Blob()),
).getUint32(0, false)
await getPositions(start, end, file, positions, tocLength as any)
await this.getManifestV1(positions, file)
await this.getIconV1(positions, file)
}
async parseS9pkV2(file: File) {
const s9pk = await S9pk.deserialize(file, null)
this.toUpload.manifest = s9pk.manifest
this.toUpload.icon = await s9pk.icon()
}
private async getManifestV1(positions: Positions, file: Blob) {
const data = await blobToBuffer(
file.slice(
Number(positions['manifest'][0]),
Number(positions['manifest'][0]) + Number(positions['manifest'][1]),
),
)
this.toUpload.manifest = await cbor.decode(data, true)
}
private async getIconV1(positions: Positions, file: Blob) {
const data = file.slice(
Number(positions['icon'][0]),
Number(positions['icon'][0]) + Number(positions['icon'][1]),
'',
)
this.toUpload.icon = await blobToDataURL(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 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 blobToBuffer(file.slice(start, end)),
).getBigUint64(0, false)
start = end
end = start + 8
const chapterLength = new DataView(
await blobToBuffer(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 | File,
): Promise<string | ArrayBuffer | null> {
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.onloadend = () => {
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 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()
return new Promise((resolve, reject) => {
reader.onloadend = () => {
resolve(reader.result)
}
reader.readAsArrayBuffer(f)
reader.onerror = _ => reject(new Error('error reading blob'))
})
}
function compare(a: Uint8Array, b: Uint8Array) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}