mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
allow mounting files directly (#2931)
* allow mounting files directly * fixes from testing * more fixes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
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.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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user