mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Misc (#3087)
* help ios downlaod .crt and add begin add masked for addresses * only require and show CA for public domain if addSsl * fix type and revert i18n const * feat: add address masking and adjust design (#3088) * feat: add address masking and adjust design * update lockfile * chore: move eye button to actions * chore: refresh notifications and handle action error * static width for health check name --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * hide certificate authorities tab * alpha.17 * add waiting health check status * remove "on" from waiting message * reject on abort in `.watch` * id migration: nostr -> nostr-rs-relay * health check waiting state * use interface type for launch button * better wording for masked * cleaner * sdk improvements * fix type error * fix notification badge issue --------- Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
@@ -224,8 +224,6 @@ export type ListValueSpecObject = {
|
||||
uniqueBy: UniqueBy
|
||||
displayAs: string | null
|
||||
}
|
||||
// TODO Aiden do we really want this expressivity? Why not the below. Also what's with the "readonly" portion?
|
||||
// export type UniqueBy = null | string | { any: string[] } | { all: string[] }
|
||||
|
||||
export type UniqueBy =
|
||||
| null
|
||||
|
||||
9
sdk/base/lib/osBindings/AddPackageSignerParams.ts
Normal file
9
sdk/base/lib/osBindings/AddPackageSignerParams.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Guid } from "./Guid"
|
||||
import type { PackageId } from "./PackageId"
|
||||
|
||||
export type AddPackageSignerParams = {
|
||||
id: PackageId
|
||||
signer: Guid
|
||||
versions: string | null
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export type NamedHealthCheckResult = { name: string } & (
|
||||
| { result: "success"; message: string | null }
|
||||
| { result: "disabled"; message: string | null }
|
||||
| { result: "starting"; message: string | null }
|
||||
| { result: "waiting"; message: string | null }
|
||||
| { result: "loading"; message: string }
|
||||
| { result: "failure"; message: string }
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PackageVersionInfo } from "./PackageVersionInfo"
|
||||
import type { Version } from "./Version"
|
||||
|
||||
export type PackageInfo = {
|
||||
authorized: Array<Guid>
|
||||
authorized: { [key: Guid]: string }
|
||||
versions: { [key: Version]: PackageVersionInfo }
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
import type { Guid } from "./Guid"
|
||||
import type { PackageId } from "./PackageId"
|
||||
|
||||
export type PackageSignerParams = { id: PackageId; signer: Guid }
|
||||
export type RemovePackageSignerParams = { id: PackageId; signer: Guid }
|
||||
@@ -5,6 +5,7 @@ export type SetHealth = { id: HealthCheckId; name: string } & (
|
||||
| { result: "success"; message: string | null }
|
||||
| { result: "disabled"; message: string | null }
|
||||
| { result: "starting"; message: string | null }
|
||||
| { result: "waiting"; message: string | null }
|
||||
| { result: "loading"; message: string }
|
||||
| { result: "failure"; message: string }
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ export { AddAdminParams } from "./AddAdminParams"
|
||||
export { AddAssetParams } from "./AddAssetParams"
|
||||
export { AddCategoryParams } from "./AddCategoryParams"
|
||||
export { AddPackageParams } from "./AddPackageParams"
|
||||
export { AddPackageSignerParams } from "./AddPackageSignerParams"
|
||||
export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams"
|
||||
export { AddressInfo } from "./AddressInfo"
|
||||
export { AddSslOptions } from "./AddSslOptions"
|
||||
@@ -154,7 +155,6 @@ export { PackageId } from "./PackageId"
|
||||
export { PackageIndex } from "./PackageIndex"
|
||||
export { PackageInfoShort } from "./PackageInfoShort"
|
||||
export { PackageInfo } from "./PackageInfo"
|
||||
export { PackageSignerParams } from "./PackageSignerParams"
|
||||
export { PackageState } from "./PackageState"
|
||||
export { PackageVersionInfo } from "./PackageVersionInfo"
|
||||
export { PasswordType } from "./PasswordType"
|
||||
@@ -172,6 +172,7 @@ export { RemoveAssetParams } from "./RemoveAssetParams"
|
||||
export { RemoveCategoryParams } from "./RemoveCategoryParams"
|
||||
export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams"
|
||||
export { RemovePackageParams } from "./RemovePackageParams"
|
||||
export { RemovePackageSignerParams } from "./RemovePackageSignerParams"
|
||||
export { RemoveTunnelParams } from "./RemoveTunnelParams"
|
||||
export { RemoveVersionParams } from "./RemoveVersionParams"
|
||||
export { ReplayId } from "./ReplayId"
|
||||
|
||||
@@ -39,6 +39,7 @@ export class GetSystemSmtp {
|
||||
})
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +47,7 @@ export class GetSystemSmtp {
|
||||
*/
|
||||
watch(
|
||||
abort?: AbortSignal,
|
||||
): AsyncGenerator<T.SmtpValue | null, void, unknown> {
|
||||
): AsyncGenerator<T.SmtpValue | null, never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
|
||||
@@ -393,12 +393,13 @@ export class GetServiceInterface<Mapped = ServiceInterfaceFilled | null> {
|
||||
}
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches the requested service interface. Returns an async iterator that yields whenever the value changes
|
||||
*/
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, void, unknown> {
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
|
||||
@@ -2,11 +2,7 @@ import { Effects } from "../Effects"
|
||||
import { PackageId } from "../osBindings"
|
||||
import { deepEqual } from "./deepEqual"
|
||||
import { DropGenerator, DropPromise } from "./Drop"
|
||||
import {
|
||||
ServiceInterfaceFilled,
|
||||
filledAddress,
|
||||
getHostname,
|
||||
} from "./getServiceInterface"
|
||||
import { ServiceInterfaceFilled, filledAddress } from "./getServiceInterface"
|
||||
|
||||
const makeManyInterfaceFilled = async ({
|
||||
effects,
|
||||
@@ -106,12 +102,13 @@ export class GetServiceInterfaces<Mapped = ServiceInterfaceFilled[]> {
|
||||
}
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches the service interfaces for the package. Returns an async iterator that yields whenever the value changes
|
||||
*/
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, void, unknown> {
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
|
||||
@@ -65,8 +65,9 @@ import {
|
||||
ServiceInterfaceFilled,
|
||||
} from "../../base/lib/util/getServiceInterface"
|
||||
import { getOwnServiceInterfaces } from "../../base/lib/util/getServiceInterfaces"
|
||||
import { Volumes, createVolumes } from "./util/Volume"
|
||||
|
||||
export const OSVersion = testTypeVersion("0.4.0-alpha.16")
|
||||
export const OSVersion = testTypeVersion("0.4.0-alpha.17")
|
||||
|
||||
// prettier-ignore
|
||||
type AnyNeverCond<T extends any[], Then, Else> =
|
||||
@@ -132,6 +133,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
|
||||
return {
|
||||
manifest: this.manifest,
|
||||
volumes: createVolumes(this.manifest),
|
||||
...startSdkEffectWrapper,
|
||||
setDataVersion,
|
||||
getDataVersion,
|
||||
@@ -430,10 +432,10 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
query: Record<string, string>
|
||||
/** (optional) overrides the protocol prefix provided by the bind function.
|
||||
*
|
||||
* @example `ftp://`
|
||||
* @example `{ ssl: 'ftps', noSsl: 'ftp' }`
|
||||
*/
|
||||
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
|
||||
/** TODO Aiden how would someone include a password in the URL? Whether or not to mask the URLs on the screen, for example, when they contain a password */
|
||||
/** mask the url (recommended if it contains credentials such as an API key or password) */
|
||||
masked: boolean
|
||||
},
|
||||
) => new ServiceInterfaceBuilder({ ...options, effects }),
|
||||
|
||||
@@ -14,10 +14,7 @@ export { CommandController } from "./CommandController"
|
||||
import { EXIT_SUCCESS, HealthDaemon } from "./HealthDaemon"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { HealthCheck } from "../health/HealthCheck"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
import { Manifest } from "../test/output.sdk"
|
||||
import { asError } from "../util"
|
||||
|
||||
export const cpExec = promisify(CP.exec)
|
||||
export const cpExecFile = promisify(CP.execFile)
|
||||
@@ -432,7 +429,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
|
||||
async build() {
|
||||
for (const daemon of this.healthDaemons) {
|
||||
await daemon.init()
|
||||
await daemon.updateStatus()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { defaultTrigger } from "../trigger/defaultTrigger"
|
||||
import { Ready } from "./Daemons"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
|
||||
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
||||
import { asError } from "../../../base/lib/util/asError"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
import { SubContainer } from "../util/SubContainer"
|
||||
|
||||
const oncePromise = <T>() => {
|
||||
let resolve: (value: T) => void
|
||||
@@ -26,7 +22,7 @@ export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
|
||||
*
|
||||
*/
|
||||
export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
private _health: HealthCheckResult = { result: "starting", message: null }
|
||||
private _health: HealthCheckResult = { result: "waiting", message: null }
|
||||
private healthWatchers: Array<() => unknown> = []
|
||||
private running = false
|
||||
private started?: number
|
||||
@@ -82,20 +78,18 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
if (newStatus) {
|
||||
console.debug(`Launching ${this.id}...`)
|
||||
this.setupHealthCheck()
|
||||
;(await this.daemon)?.start()
|
||||
this.daemon?.start()
|
||||
this.started = performance.now()
|
||||
} else {
|
||||
console.debug(`Stopping ${this.id}...`)
|
||||
;(await this.daemon)?.term()
|
||||
this.turnOffHealthCheck()
|
||||
|
||||
this.setHealth({ result: "starting", message: null })
|
||||
this.daemon?.term()
|
||||
await this.turnOffHealthCheck()
|
||||
}
|
||||
}
|
||||
|
||||
private healthCheckCleanup: (() => null) | null = null
|
||||
private turnOffHealthCheck() {
|
||||
this.healthCheckCleanup?.()
|
||||
private healthCheckCleanup: (() => Promise<null>) | null = null
|
||||
private async turnOffHealthCheck() {
|
||||
await this.healthCheckCleanup?.()
|
||||
|
||||
this.resolvedReady = false
|
||||
this.readyPromise = new Promise(
|
||||
@@ -107,8 +101,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
)
|
||||
}
|
||||
private async setupHealthCheck() {
|
||||
const daemon = await this.daemon
|
||||
daemon?.onExit((success) => {
|
||||
this.daemon?.onExit((success) => {
|
||||
if (success && this.ready === "EXIT_SUCCESS") {
|
||||
this.setHealth({ result: "success", message: null })
|
||||
} else if (!success) {
|
||||
@@ -116,7 +109,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
result: "failure",
|
||||
message: `${this.id} daemon crashed`,
|
||||
})
|
||||
} else if (!daemon.isOneshot()) {
|
||||
} else if (!this.daemon?.isOneshot()) {
|
||||
this.setHealth({
|
||||
result: "failure",
|
||||
message: `${this.id} daemon exited`,
|
||||
@@ -132,6 +125,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
const { promise: status, resolve: setStatus } = oncePromise<{
|
||||
done: true
|
||||
}>()
|
||||
const { promise: exited, resolve: setExited } = oncePromise<null>()
|
||||
new Promise(async () => {
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
for (
|
||||
@@ -150,10 +144,12 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
|
||||
await this.setHealth(response)
|
||||
}
|
||||
setExited(null)
|
||||
}).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`))
|
||||
|
||||
this.healthCheckCleanup = () => {
|
||||
this.healthCheckCleanup = async () => {
|
||||
setStatus({ done: true })
|
||||
await exited
|
||||
this.healthCheckCleanup = null
|
||||
return null
|
||||
}
|
||||
@@ -201,6 +197,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
const healths = this.dependencies.map((d) => ({
|
||||
health: d.running && d._health,
|
||||
id: d.id,
|
||||
display: typeof d.ready === "object" ? d.ready.display : null,
|
||||
}))
|
||||
const waitingOn = healths.filter(
|
||||
(h) => !h.health || h.health.result !== "success",
|
||||
@@ -209,18 +206,15 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
console.debug(
|
||||
`daemon ${this.id} waiting on ${waitingOn.map((w) => w.id)}`,
|
||||
)
|
||||
this.changeRunning(!waitingOn.length)
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.ready !== "EXIT_SUCCESS" && this.ready.display) {
|
||||
this.effects.setHealth({
|
||||
id: this.id,
|
||||
message: null,
|
||||
name: this.ready.display,
|
||||
result: "starting",
|
||||
})
|
||||
if (waitingOn.length) {
|
||||
const waitingOnNames = waitingOn.flatMap((w) =>
|
||||
w.display ? [w.display] : [],
|
||||
)
|
||||
const message = waitingOnNames.length ? waitingOnNames.join(", ") : null
|
||||
await this.setHealth({ result: "waiting", message })
|
||||
} else {
|
||||
await this.setHealth({ result: "starting", message: null })
|
||||
}
|
||||
await this.updateStatus()
|
||||
await this.changeRunning(!waitingOn.length)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export class GetSslCertificate {
|
||||
})
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +58,7 @@ export class GetSslCertificate {
|
||||
*/
|
||||
watch(
|
||||
abort?: AbortSignal,
|
||||
): AsyncGenerator<[string, string, string], void, unknown> {
|
||||
): AsyncGenerator<[string, string, string], never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
|
||||
@@ -7,6 +7,7 @@ import { once } from "../../../base/lib/util/once"
|
||||
import { Drop } from "../../../base/lib/util/Drop"
|
||||
import { Mounts } from "../mainFn/Mounts"
|
||||
import { BackupEffects } from "../backup/Backups"
|
||||
import { PathBase } from "./Volume"
|
||||
|
||||
export const execFile = promisify(cp.execFile)
|
||||
const False = () => false
|
||||
@@ -71,10 +72,18 @@ async function bind(
|
||||
export interface SubContainer<
|
||||
Manifest extends T.SDKManifest,
|
||||
Effects extends T.Effects = T.Effects,
|
||||
> extends Drop {
|
||||
> extends Drop,
|
||||
PathBase {
|
||||
readonly imageId: keyof Manifest["images"] & T.ImageId
|
||||
readonly rootfs: string
|
||||
readonly guid: T.Guid
|
||||
|
||||
/**
|
||||
* Get the absolute path to a file or directory within this subcontainer's rootfs
|
||||
* @param path Path relative to the rootfs
|
||||
*/
|
||||
subpath(path: string): string
|
||||
|
||||
mount(
|
||||
mounts: Effects extends BackupEffects
|
||||
? Mounts<
|
||||
@@ -137,6 +146,22 @@ export interface SubContainer<
|
||||
options?: CommandOptions & StdioOptions,
|
||||
): Promise<cp.ChildProcess>
|
||||
|
||||
/**
|
||||
* @description Write a file to the subcontainer's filesystem
|
||||
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
|
||||
* @param data The data to write
|
||||
* @param options Optional write options (same as node:fs/promises writeFile)
|
||||
*/
|
||||
writeFile(
|
||||
path: string,
|
||||
data:
|
||||
| string
|
||||
| NodeJS.ArrayBufferView
|
||||
| Iterable<string | NodeJS.ArrayBufferView>
|
||||
| AsyncIterable<string | NodeJS.ArrayBufferView>,
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void>
|
||||
|
||||
rc(): SubContainerRc<Manifest, Effects>
|
||||
|
||||
isOwned(): this is SubContainerOwned<Manifest, Effects>
|
||||
@@ -291,6 +316,12 @@ export class SubContainerOwned<
|
||||
}
|
||||
}
|
||||
|
||||
subpath(path: string): string {
|
||||
return path.startsWith("/")
|
||||
? `${this.rootfs}${path}`
|
||||
: `${this.rootfs}/${path}`
|
||||
}
|
||||
|
||||
async mount(
|
||||
mounts: Effects extends BackupEffects
|
||||
? Mounts<
|
||||
@@ -618,6 +649,27 @@ export class SubContainerOwned<
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Write a file to the subcontainer's filesystem
|
||||
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
|
||||
* @param data The data to write
|
||||
* @param options Optional write options (same as node:fs/promises writeFile)
|
||||
*/
|
||||
async writeFile(
|
||||
path: string,
|
||||
data:
|
||||
| string
|
||||
| NodeJS.ArrayBufferView
|
||||
| Iterable<string | NodeJS.ArrayBufferView>
|
||||
| AsyncIterable<string | NodeJS.ArrayBufferView>,
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
const fullPath = this.subpath(path)
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, "")
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
return fs.writeFile(fullPath, data, options)
|
||||
}
|
||||
|
||||
rc(): SubContainerRc<Manifest, Effects> {
|
||||
return new SubContainerRc(this)
|
||||
}
|
||||
@@ -643,6 +695,9 @@ export class SubContainerRc<
|
||||
get guid() {
|
||||
return this.subcontainer.guid
|
||||
}
|
||||
subpath(path: string): string {
|
||||
return this.subcontainer.subpath(path)
|
||||
}
|
||||
private destroyed = false
|
||||
private destroying: Promise<null> | null = null
|
||||
public constructor(
|
||||
@@ -800,6 +855,24 @@ export class SubContainerRc<
|
||||
return this.subcontainer.spawn(command, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Write a file to the subcontainer's filesystem
|
||||
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
|
||||
* @param data The data to write
|
||||
* @param options Optional write options (same as node:fs/promises writeFile)
|
||||
*/
|
||||
async writeFile(
|
||||
path: string,
|
||||
data:
|
||||
| string
|
||||
| NodeJS.ArrayBufferView
|
||||
| Iterable<string | NodeJS.ArrayBufferView>
|
||||
| AsyncIterable<string | NodeJS.ArrayBufferView>,
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
return this.subcontainer.writeFile(path, data, options)
|
||||
}
|
||||
|
||||
rc(): SubContainerRc<Manifest, Effects> {
|
||||
return this.subcontainer.rc()
|
||||
}
|
||||
|
||||
88
sdk/package/lib/util/Volume.ts
Normal file
88
sdk/package/lib/util/Volume.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
import * as T from "../../../base/lib/types"
|
||||
|
||||
/**
|
||||
* Common interface for objects that have a subpath method (Volume, SubContainer, etc.)
|
||||
*/
|
||||
export interface PathBase {
|
||||
subpath(path: string): string
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Represents a volume in the StartOS filesystem.
|
||||
* Provides utilities for reading and writing files within the volume.
|
||||
*/
|
||||
export class Volume<Id extends string = string> implements PathBase {
|
||||
/**
|
||||
* The absolute path to this volume's root directory
|
||||
*/
|
||||
readonly path: string
|
||||
|
||||
constructor(readonly id: Id) {
|
||||
this.path = `/media/startos/volumes/${id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute path to a file or directory within this volume
|
||||
* @param subpath Path relative to the volume root
|
||||
*/
|
||||
subpath(subpath: string): string {
|
||||
return subpath.startsWith("/")
|
||||
? `${this.path}${subpath}`
|
||||
: `${this.path}/${subpath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Read a file from this volume
|
||||
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
|
||||
* @param options Optional read options (same as node:fs/promises readFile)
|
||||
*/
|
||||
async readFile(
|
||||
subpath: string,
|
||||
options?: Parameters<typeof fs.readFile>[1],
|
||||
): Promise<Buffer | string> {
|
||||
const fullPath = this.subpath(subpath)
|
||||
return fs.readFile(fullPath, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Write a file to this volume
|
||||
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
|
||||
* @param data The data to write
|
||||
* @param options Optional write options (same as node:fs/promises writeFile)
|
||||
*/
|
||||
async writeFile(
|
||||
subpath: string,
|
||||
data:
|
||||
| string
|
||||
| NodeJS.ArrayBufferView
|
||||
| Iterable<string | NodeJS.ArrayBufferView>
|
||||
| AsyncIterable<string | NodeJS.ArrayBufferView>,
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
const fullPath = this.subpath(subpath)
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, "")
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
return fs.writeFile(fullPath, data, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe volumes object that provides Volume instances for each volume defined in the manifest
|
||||
*/
|
||||
export type Volumes<Manifest extends T.SDKManifest> = {
|
||||
[K in Manifest["volumes"][number]]: Volume<K>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a type-safe volumes object from a manifest
|
||||
*/
|
||||
export function createVolumes<Manifest extends T.SDKManifest>(
|
||||
manifest: Manifest,
|
||||
): Volumes<Manifest> {
|
||||
const volumes = {} as Volumes<Manifest>
|
||||
for (const volumeId of manifest.volumes) {
|
||||
;(volumes as any)[volumeId] = new Volume(volumeId)
|
||||
}
|
||||
return volumes
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import * as T from "../../../base/lib/types"
|
||||
import * as fs from "node:fs/promises"
|
||||
import { asError, deepEqual } from "../../../base/lib/util"
|
||||
import { DropGenerator, DropPromise } from "../../../base/lib/util/Drop"
|
||||
import { PathBase } from "./Volume"
|
||||
|
||||
const previousPath = /(.+?)\/([^/]*)$/
|
||||
|
||||
@@ -88,11 +89,12 @@ export type Transformers<Raw = unknown, Transformed = unknown> = {
|
||||
onWrite: (value: Transformed) => Raw
|
||||
}
|
||||
|
||||
type ToPath = string | { volumeId: T.VolumeId; subpath: string }
|
||||
type ToPath = string | { base: PathBase; subpath: string }
|
||||
function toPath(path: ToPath): string {
|
||||
return typeof path === "string"
|
||||
? path
|
||||
: `/media/startos/volumes/${path.volumeId}/${path.subpath}`
|
||||
if (typeof path === "string") {
|
||||
return path
|
||||
}
|
||||
return path.base.subpath(path.subpath)
|
||||
}
|
||||
|
||||
type Validator<T, U> = matches.Validator<T, U> | matches.Validator<unknown, U>
|
||||
@@ -103,7 +105,7 @@ type ReadType<A> = {
|
||||
watch: (
|
||||
effects: T.Effects,
|
||||
abort?: AbortSignal,
|
||||
) => AsyncGenerator<A | null, null, unknown>
|
||||
) => AsyncGenerator<A | null, never, unknown>
|
||||
onChange: (
|
||||
effects: T.Effects,
|
||||
callback: (
|
||||
@@ -270,7 +272,7 @@ export class FileHelper<A> {
|
||||
await onCreated(this.path).catch((e) => console.error(asError(e)))
|
||||
}
|
||||
}
|
||||
return null
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
private readOnChange<B>(
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "../../../base/lib/util"
|
||||
export { GetSslCertificate } from "./GetSslCertificate"
|
||||
|
||||
export { Drop } from "../../../base/lib/util/Drop"
|
||||
export { Volume, Volumes } from "./Volume"
|
||||
|
||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.45",
|
||||
"version": "0.4.0-beta.46",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.45",
|
||||
"version": "0.4.0-beta.46",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.45",
|
||||
"version": "0.4.0-beta.46",
|
||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||
"main": "./package/lib/index.js",
|
||||
"types": "./package/lib/index.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user