allow mounting files directly (#2931)

* allow mounting files directly

* fixes from testing

* more fixes
This commit is contained in:
Aiden McClelland
2025-05-07 12:47:45 -06:00
committed by GitHub
parent 9bc945f76f
commit a3252f9671
12 changed files with 208 additions and 114 deletions

View File

@@ -3,31 +3,39 @@ import { MountOptions } from "../util/SubContainer"
type MountArray = { mountpoint: string; options: MountOptions }[]
type SharedOptions = {
/** The path within the resource to mount. Use `null` to mount the entire resource */
subpath: string | null
/** Where to mount the resource. e.g. /data */
mountpoint: string
/** Whether to mount this as a file or directory */
type?: "file" | "directory"
}
type VolumeOpts<Manifest extends T.SDKManifest> = {
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest */
volumeId: Manifest["volumes"][number]
/** Whether or not the resource should be readonly for this subcontainer */
readonly: boolean
} & SharedOptions
type DependencyOpts<Manifest extends T.SDKManifest> = {
/** The ID of the dependency */
dependencyId: Manifest["id"]
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest of the dependency */
volumeId: Manifest["volumes"][number]
/** Whether or not the resource should be readonly for this subcontainer */
readonly: boolean
} & SharedOptions
export class Mounts<
Manifest extends T.SDKManifest,
Backups extends {
subpath: string | null
mountpoint: string
} = never,
Backups extends SharedOptions = never,
> {
private constructor(
readonly volumes: {
id: Manifest["volumes"][number]
subpath: string | null
mountpoint: string
readonly: boolean
}[],
readonly assets: {
subpath: string | null
mountpoint: string
}[],
readonly dependencies: {
dependencyId: string
volumeId: string
subpath: string | null
mountpoint: string
readonly: boolean
}[],
readonly volumes: VolumeOpts<Manifest>[],
readonly assets: SharedOptions[],
readonly dependencies: DependencyOpts<T.SDKManifest>[],
readonly backups: Backups[],
) {}
@@ -35,82 +43,36 @@ export class Mounts<
return new Mounts<Manifest>([], [], [], [])
}
addVolume(
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest */
id: Manifest["volumes"][number],
/** The path within the volume to mount. Use `null` to mount the entire volume */
subpath: string | null,
/** Where to mount the volume. e.g. /data */
mountpoint: string,
/** Whether or not the volume should be readonly for this daemon */
readonly: boolean,
) {
addVolume(options: VolumeOpts<Manifest>) {
return new Mounts<Manifest, Backups>(
[
...this.volumes,
{
id,
subpath,
mountpoint,
readonly,
},
],
[...this.volumes, options],
[...this.assets],
[...this.dependencies],
[...this.backups],
)
}
addAssets(
/** The path within the asset directory to mount. Use `null` to mount the entire volume */
subpath: string | null,
/** Where to mount the asset. e.g. /asset */
mountpoint: string,
) {
addAssets(options: SharedOptions) {
return new Mounts<Manifest, Backups>(
[...this.volumes],
[
...this.assets,
{
subpath,
mountpoint,
},
],
[...this.assets, options],
[...this.dependencies],
[...this.backups],
)
}
addDependency<DependencyManifest extends T.SDKManifest>(
/** The ID of the dependency service */
dependencyId: keyof Manifest["dependencies"] & string,
/** The ID of the volume belonging to the dependency service to mount */
volumeId: DependencyManifest["volumes"][number],
/** The path within the dependency's volume to mount. Use `null` to mount the entire volume */
subpath: string | null,
/** Where to mount the dependency's volume. e.g. /service-id */
mountpoint: string,
/** Whether or not the volume should be readonly for this daemon */
readonly: boolean,
options: DependencyOpts<DependencyManifest>,
) {
return new Mounts<Manifest, Backups>(
[...this.volumes],
[...this.assets],
[
...this.dependencies,
{
dependencyId,
volumeId,
subpath,
mountpoint,
readonly,
},
],
[...this.dependencies, options],
[...this.backups],
)
}
addBackups(subpath: string | null, mountpoint: string) {
addBackups(options: SharedOptions) {
return new Mounts<
Manifest,
{
@@ -121,7 +83,7 @@ export class Mounts<
[...this.volumes],
[...this.assets],
[...this.dependencies],
[...this.backups, { subpath, mountpoint }],
[...this.backups, options],
)
}
@@ -144,9 +106,10 @@ export class Mounts<
mountpoint: v.mountpoint,
options: {
type: "volume",
id: v.id,
volumeId: v.volumeId,
subpath: v.subpath,
readonly: v.readonly,
filetype: v.type,
},
})),
)
@@ -156,6 +119,7 @@ export class Mounts<
options: {
type: "assets",
subpath: a.subpath,
filetype: a.type,
},
})),
)
@@ -168,12 +132,13 @@ export class Mounts<
volumeId: d.volumeId,
subpath: d.subpath,
readonly: d.readonly,
filetype: d.type,
},
})),
)
}
}
const a = Mounts.of().addBackups(null, "")
const a = Mounts.of().addBackups({ subpath: null, mountpoint: "" })
// @ts-expect-error
const m: Mounts<T.SDKManifest, never> = a

View File

@@ -24,6 +24,37 @@ export type ExecOptions = {
const TIMES_TO_WAIT_FOR_PROC = 100
async function prepBind(
from: string | null,
to: string,
type?: "file" | "directory",
) {
const fromMeta = from ? await fs.stat(from).catch((_) => null) : null
const toMeta = await fs.stat(to).catch((_) => null)
if (type === "file" || (!type && from && fromMeta?.isFile())) {
if (toMeta && toMeta.isDirectory()) await fs.rmdir(to, { recursive: false })
if (from && !fromMeta) {
await fs.mkdir(from.replace(/\/[^\/]*\/?$/, ""), { recursive: true })
await fs.writeFile(from, "")
}
if (!toMeta) {
await fs.mkdir(to.replace(/\/[^\/]*\/?$/, ""), { recursive: true })
await fs.writeFile(to, "")
}
} else {
if (toMeta && toMeta.isFile() && !toMeta.size) await fs.rm(to)
if (from && !fromMeta) await fs.mkdir(from, { recursive: true })
if (!toMeta) await fs.mkdir(to, { recursive: true })
}
}
async function bind(from: string, to: string, type?: "file" | "directory") {
await prepBind(from, to, type)
await execFile("mount", ["--bind", from, to])
}
/**
* This is the type that is going to describe what an subcontainer could do. The main point of the
* subcontainer is to have commands that run in a chrooted environment. This is useful for running
@@ -211,11 +242,9 @@ export class SubContainer<
? options.subpath
: `/${options.subpath}`
: "/"
const from = `/media/startos/volumes/${options.id}${subpath}`
const from = `/media/startos/volumes/${options.volumeId}${subpath}`
await fs.mkdir(from, { recursive: true })
await fs.mkdir(path, { recursive: true })
await execFile("mount", ["--bind", from, path])
await bind(from, path, mount.options.filetype)
} else if (options.type === "assets") {
const subpath = options.subpath
? options.subpath.startsWith("/")
@@ -224,10 +253,9 @@ export class SubContainer<
: "/"
const from = `/media/startos/assets/${subpath}`
await fs.mkdir(from, { recursive: true })
await fs.mkdir(path, { recursive: true })
await execFile("mount", ["--bind", from, path])
await bind(from, path, mount.options.filetype)
} else if (options.type === "pointer") {
await prepBind(null, path, options.filetype)
await this.effects.mount({ location: path, target: options })
} else if (options.type === "backup") {
const subpath = options.subpath
@@ -237,9 +265,7 @@ export class SubContainer<
: "/"
const from = `/media/startos/backup${subpath}`
await fs.mkdir(from, { recursive: true })
await fs.mkdir(path, { recursive: true })
await execFile("mount", ["--bind", from, path])
await bind(from, path, mount.options.filetype)
} else {
throw new Error(`unknown type ${(options as any).type}`)
}
@@ -560,14 +586,16 @@ export type MountOptions =
export type MountOptionsVolume = {
type: "volume"
id: string
volumeId: string
subpath: string | null
readonly: boolean
filetype?: "file" | "directory"
}
export type MountOptionsAssets = {
type: "assets"
subpath: string | null
filetype?: "file" | "directory"
}
export type MountOptionsPointer = {
@@ -576,11 +604,13 @@ export type MountOptionsPointer = {
volumeId: string
subpath: string | null
readonly: boolean
filetype?: "file" | "directory"
}
export type MountOptionsBackup = {
type: "backup"
subpath: string | null
filetype?: "file" | "directory"
}
function wait(time: number) {
return new Promise((resolve) => setTimeout(resolve, time))

View File

@@ -329,6 +329,36 @@ export class FileHelper<A> {
)
}
/**
* Create a File Helper for a text file
*/
static string(path: string): FileHelper<string>
static string<A extends string>(
path: string,
shape: Validator<string, A>,
): FileHelper<A>
static string<A extends Transformed, Transformed = string>(
path: string,
shape: Validator<Transformed, A>,
transformers: Transformers<string, Transformed>,
): FileHelper<A>
static string<A extends Transformed, Transformed = string>(
path: string,
shape?: Validator<Transformed, A>,
transformers?: Transformers<string, Transformed>,
) {
return FileHelper.rawTransformed<A, string, Transformed>(
path,
(inData) => inData,
(inString) => inString,
(data) =>
(shape || (matches.string as Validator<Transformed, A>)).unsafeCast(
data,
),
transformers,
)
}
/**
* Create a File Helper for a .json file.
*/

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.17",
"version": "0.4.0-beta.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.17",
"version": "0.4.0-beta.18",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.17",
"version": "0.4.0-beta.18",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",