mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 04:01:58 +00:00
* 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>
264 lines
7.4 KiB
TypeScript
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
|
|
}
|