mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat(volumes): read only mount mode
This commit is contained in:
@@ -4,10 +4,8 @@ import type { Options as ClientOptions, TDataShape, Client } from "./client";
|
|||||||
import type {
|
import type {
|
||||||
RegisterData,
|
RegisterData,
|
||||||
RegisterResponses,
|
RegisterResponses,
|
||||||
RegisterErrors,
|
|
||||||
LoginData,
|
LoginData,
|
||||||
LoginResponses,
|
LoginResponses,
|
||||||
LoginErrors,
|
|
||||||
LogoutData,
|
LogoutData,
|
||||||
LogoutResponses,
|
LogoutResponses,
|
||||||
GetMeData,
|
GetMeData,
|
||||||
@@ -16,7 +14,6 @@ import type {
|
|||||||
GetStatusResponses,
|
GetStatusResponses,
|
||||||
ChangePasswordData,
|
ChangePasswordData,
|
||||||
ChangePasswordResponses,
|
ChangePasswordResponses,
|
||||||
ChangePasswordErrors,
|
|
||||||
ListVolumesData,
|
ListVolumesData,
|
||||||
ListVolumesResponses,
|
ListVolumesResponses,
|
||||||
CreateVolumeData,
|
CreateVolumeData,
|
||||||
@@ -97,7 +94,7 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
|
|||||||
* Register a new user
|
* Register a new user
|
||||||
*/
|
*/
|
||||||
export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => {
|
export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => {
|
||||||
return (options?.client ?? _heyApiClient).post<RegisterResponses, RegisterErrors, ThrowOnError>({
|
return (options?.client ?? _heyApiClient).post<RegisterResponses, unknown, ThrowOnError>({
|
||||||
url: "/api/v1/auth/register",
|
url: "/api/v1/auth/register",
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -111,7 +108,7 @@ export const register = <ThrowOnError extends boolean = false>(options?: Options
|
|||||||
* Login with username and password
|
* Login with username and password
|
||||||
*/
|
*/
|
||||||
export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => {
|
export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => {
|
||||||
return (options?.client ?? _heyApiClient).post<LoginResponses, LoginErrors, ThrowOnError>({
|
return (options?.client ?? _heyApiClient).post<LoginResponses, unknown, ThrowOnError>({
|
||||||
url: "/api/v1/auth/login",
|
url: "/api/v1/auth/login",
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -157,7 +154,7 @@ export const getStatus = <ThrowOnError extends boolean = false>(options?: Option
|
|||||||
export const changePassword = <ThrowOnError extends boolean = false>(
|
export const changePassword = <ThrowOnError extends boolean = false>(
|
||||||
options?: Options<ChangePasswordData, ThrowOnError>,
|
options?: Options<ChangePasswordData, ThrowOnError>,
|
||||||
) => {
|
) => {
|
||||||
return (options?.client ?? _heyApiClient).post<ChangePasswordResponses, ChangePasswordErrors, ThrowOnError>({
|
return (options?.client ?? _heyApiClient).post<ChangePasswordResponses, unknown, ThrowOnError>({
|
||||||
url: "/api/v1/auth/change-password",
|
url: "/api/v1/auth/change-password",
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -10,13 +10,6 @@ export type RegisterData = {
|
|||||||
url: "/api/v1/auth/register";
|
url: "/api/v1/auth/register";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RegisterErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid request or username already exists
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RegisterResponses = {
|
export type RegisterResponses = {
|
||||||
/**
|
/**
|
||||||
* User created successfully
|
* User created successfully
|
||||||
@@ -43,13 +36,6 @@ export type LoginData = {
|
|||||||
url: "/api/v1/auth/login";
|
url: "/api/v1/auth/login";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoginErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid credentials
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LoginResponses = {
|
export type LoginResponses = {
|
||||||
/**
|
/**
|
||||||
* Login successful
|
* Login successful
|
||||||
@@ -135,17 +121,6 @@ export type ChangePasswordData = {
|
|||||||
url: "/api/v1/auth/change-password";
|
url: "/api/v1/auth/change-password";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChangePasswordErrors = {
|
|
||||||
/**
|
|
||||||
* Invalid current password or validation error
|
|
||||||
*/
|
|
||||||
400: unknown;
|
|
||||||
/**
|
|
||||||
* Not authenticated
|
|
||||||
*/
|
|
||||||
401: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ChangePasswordResponses = {
|
export type ChangePasswordResponses = {
|
||||||
/**
|
/**
|
||||||
* Password changed successfully
|
* Password changed successfully
|
||||||
|
|||||||
@@ -202,6 +202,31 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="readOnly"
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Read-only Mode</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={field.value ?? false}
|
||||||
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Mount volume as read-only</span>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -301,6 +326,31 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="readOnly"
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Read-only Mode</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={field.value ?? false}
|
||||||
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Mount volume as read-only</span>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -422,6 +472,31 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="readOnly"
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Read-only Mode</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={field.value ?? false}
|
||||||
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Mount volume as read-only</span>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Prevent any modifications to the volume. Recommended for backup sources and sensitive data.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,20 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "~/components/ui/alert-dialog";
|
} from "~/components/ui/alert-dialog";
|
||||||
import type { Snapshot } from "~/lib/types";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
|
||||||
|
import type { Snapshot, Volume } from "~/lib/types";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
snapshot: Snapshot;
|
snapshot: Snapshot;
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
|
volume?: Volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SnapshotFileBrowser = (props: Props) => {
|
export const SnapshotFileBrowser = (props: Props) => {
|
||||||
const { snapshot, repositoryName } = props;
|
const { snapshot, repositoryName, volume } = props;
|
||||||
|
|
||||||
|
const isReadOnly = volume?.config && "readOnly" in volume.config && volume.config.readOnly === true;
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||||
@@ -195,11 +199,28 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
<CardDescription>{`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}</CardDescription>
|
<CardDescription>{`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{selectedPaths.size > 0 && (
|
{selectedPaths.size > 0 && (
|
||||||
<Button onClick={handleRestoreClick} variant="primary" size="sm" disabled={isRestoring}>
|
<Tooltip>
|
||||||
{isRestoring
|
<TooltipTrigger asChild>
|
||||||
? "Restoring..."
|
<span tabIndex={isReadOnly ? 0 : undefined}>
|
||||||
: `Restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"}`}
|
<Button
|
||||||
</Button>
|
onClick={handleRestoreClick}
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
disabled={isRestoring || isReadOnly}
|
||||||
|
>
|
||||||
|
{isRestoring
|
||||||
|
? "Restoring..."
|
||||||
|
: `Restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"}`}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{isReadOnly && (
|
||||||
|
<TooltipContent className="text-center">
|
||||||
|
<p>Volume is mounted as read-only.</p>
|
||||||
|
<p>Please remount with read-only disabled to restore files.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||||||
key={selectedSnapshot?.short_id}
|
key={selectedSnapshot?.short_id}
|
||||||
snapshot={selectedSnapshot}
|
snapshot={selectedSnapshot}
|
||||||
repositoryName={schedule.repository.name}
|
repositoryName={schedule.repository.name}
|
||||||
|
volume={schedule.volume}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||||||
return { status: BACKEND_STATUS.error, error: "NFS mounting is only supported on Linux hosts." };
|
return { status: BACKEND_STATUS.error, error: "NFS mounting is only supported on Linux hosts." };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status } = await checkHealth(path);
|
const { status } = await checkHealth(path, config.readOnly);
|
||||||
if (status === "mounted") {
|
if (status === "mounted") {
|
||||||
return { status: BACKEND_STATUS.mounted };
|
return { status: BACKEND_STATUS.mounted };
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,9 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||||||
|
|
||||||
const source = `${config.server}:${config.exportPath}`;
|
const source = `${config.server}:${config.exportPath}`;
|
||||||
const options = [`vers=${config.version}`, `port=${config.port}`];
|
const options = [`vers=${config.version}`, `port=${config.port}`];
|
||||||
|
if (config.readOnly) {
|
||||||
|
options.push("ro");
|
||||||
|
}
|
||||||
const args = ["-t", "nfs", "-o", options.join(","), source, path];
|
const args = ["-t", "nfs", "-o", options.join(","), source, path];
|
||||||
|
|
||||||
logger.debug(`Mounting volume ${path}...`);
|
logger.debug(`Mounting volume ${path}...`);
|
||||||
@@ -84,7 +87,7 @@ const unmount = async (path: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkHealth = async (path: string) => {
|
const checkHealth = async (path: string, readOnly: boolean) => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
logger.debug(`Checking health of NFS volume at ${path}...`);
|
logger.debug(`Checking health of NFS volume at ${path}...`);
|
||||||
await fs.access(path);
|
await fs.access(path);
|
||||||
@@ -95,7 +98,9 @@ const checkHealth = async (path: string) => {
|
|||||||
throw new Error(`Path ${path} is not mounted as NFS.`);
|
throw new Error(`Path ${path} is not mounted as NFS.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await createTestFile(path);
|
if (!readOnly) {
|
||||||
|
await createTestFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(`NFS volume at ${path} is healthy and mounted.`);
|
logger.debug(`NFS volume at ${path} is healthy and mounted.`);
|
||||||
return { status: BACKEND_STATUS.mounted };
|
return { status: BACKEND_STATUS.mounted };
|
||||||
@@ -112,5 +117,5 @@ const checkHealth = async (path: string) => {
|
|||||||
export const makeNfsBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
export const makeNfsBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||||
mount: () => mount(config, path),
|
mount: () => mount(config, path),
|
||||||
unmount: () => unmount(path),
|
unmount: () => unmount(path),
|
||||||
checkHealth: () => checkHealth(path),
|
checkHealth: () => checkHealth(path, config.readOnly),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||||||
return { status: BACKEND_STATUS.error, error: "SMB mounting is only supported on Linux hosts." };
|
return { status: BACKEND_STATUS.error, error: "SMB mounting is only supported on Linux hosts." };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status } = await checkHealth(path);
|
const { status } = await checkHealth(path, config.readOnly);
|
||||||
if (status === "mounted") {
|
if (status === "mounted") {
|
||||||
return { status: BACKEND_STATUS.mounted };
|
return { status: BACKEND_STATUS.mounted };
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,10 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||||||
options.push(`domain=${config.domain}`);
|
options.push(`domain=${config.domain}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.readOnly) {
|
||||||
|
options.push("ro");
|
||||||
|
}
|
||||||
|
|
||||||
const args = ["-t", "cifs", "-o", options.join(","), source, path];
|
const args = ["-t", "cifs", "-o", options.join(","), source, path];
|
||||||
|
|
||||||
logger.debug(`Mounting SMB volume ${path}...`);
|
logger.debug(`Mounting SMB volume ${path}...`);
|
||||||
@@ -96,7 +100,7 @@ const unmount = async (path: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkHealth = async (path: string) => {
|
const checkHealth = async (path: string, readOnly: boolean) => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
logger.debug(`Checking health of SMB volume at ${path}...`);
|
logger.debug(`Checking health of SMB volume at ${path}...`);
|
||||||
await fs.access(path);
|
await fs.access(path);
|
||||||
@@ -107,7 +111,9 @@ const checkHealth = async (path: string) => {
|
|||||||
throw new Error(`Path ${path} is not mounted as CIFS/SMB.`);
|
throw new Error(`Path ${path} is not mounted as CIFS/SMB.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await createTestFile(path);
|
if (!readOnly) {
|
||||||
|
await createTestFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(`SMB volume at ${path} is healthy and mounted.`);
|
logger.debug(`SMB volume at ${path} is healthy and mounted.`);
|
||||||
return { status: BACKEND_STATUS.mounted };
|
return { status: BACKEND_STATUS.mounted };
|
||||||
@@ -124,5 +130,5 @@ const checkHealth = async (path: string) => {
|
|||||||
export const makeSmbBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
export const makeSmbBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||||
mount: () => mount(config, path),
|
mount: () => mount(config, path),
|
||||||
unmount: () => unmount(path),
|
unmount: () => unmount(path),
|
||||||
checkHealth: () => checkHealth(path),
|
checkHealth: () => checkHealth(path, config.readOnly),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||||||
return { status: BACKEND_STATUS.error, error: "WebDAV mounting is only supported on Linux hosts." };
|
return { status: BACKEND_STATUS.error, error: "WebDAV mounting is only supported on Linux hosts." };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status } = await checkHealth(path);
|
const { status } = await checkHealth(path, config.readOnly);
|
||||||
if (status === "mounted") {
|
if (status === "mounted") {
|
||||||
return { status: BACKEND_STATUS.mounted };
|
return { status: BACKEND_STATUS.mounted };
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,9 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||||||
const port = config.port !== defaultPort ? `:${config.port}` : "";
|
const port = config.port !== defaultPort ? `:${config.port}` : "";
|
||||||
const source = `${protocol}://${config.server}${port}${config.path}`;
|
const source = `${protocol}://${config.server}${port}${config.path}`;
|
||||||
|
|
||||||
const options = ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
|
const options = config.readOnly
|
||||||
|
? ["uid=1000", "gid=1000", "file_mode=0444", "dir_mode=0555", "ro"]
|
||||||
|
: ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
|
||||||
|
|
||||||
if (config.username && config.password) {
|
if (config.username && config.password) {
|
||||||
const secretsFile = "/etc/davfs2/secrets";
|
const secretsFile = "/etc/davfs2/secrets";
|
||||||
@@ -132,7 +134,7 @@ const unmount = async (path: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkHealth = async (path: string) => {
|
const checkHealth = async (path: string, readOnly: boolean) => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
logger.debug(`Checking health of WebDAV volume at ${path}...`);
|
logger.debug(`Checking health of WebDAV volume at ${path}...`);
|
||||||
await fs.access(path);
|
await fs.access(path);
|
||||||
@@ -143,7 +145,9 @@ const checkHealth = async (path: string) => {
|
|||||||
throw new Error(`Path ${path} is not mounted as WebDAV.`);
|
throw new Error(`Path ${path} is not mounted as WebDAV.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await createTestFile(path);
|
if (!readOnly) {
|
||||||
|
await createTestFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(`WebDAV volume at ${path} is healthy and mounted.`);
|
logger.debug(`WebDAV volume at ${path} is healthy and mounted.`);
|
||||||
return { status: BACKEND_STATUS.mounted };
|
return { status: BACKEND_STATUS.mounted };
|
||||||
@@ -160,5 +164,5 @@ const checkHealth = async (path: string) => {
|
|||||||
export const makeWebdavBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
export const makeWebdavBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||||
mount: () => mount(config, path),
|
mount: () => mount(config, path),
|
||||||
unmount: () => unmount(path),
|
unmount: () => unmount(path),
|
||||||
checkHealth: () => checkHealth(path),
|
checkHealth: () => checkHealth(path, config.readOnly),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const nfsConfigSchema = type({
|
|||||||
exportPath: "string",
|
exportPath: "string",
|
||||||
port: type("string.integer").or(type("number")).to("1 <= number <= 65536").default(2049),
|
port: type("string.integer").or(type("number")).to("1 <= number <= 65536").default(2049),
|
||||||
version: "'3' | '4' | '4.1'",
|
version: "'3' | '4' | '4.1'",
|
||||||
|
readOnly: "boolean?",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const smbConfigSchema = type({
|
export const smbConfigSchema = type({
|
||||||
@@ -26,6 +27,7 @@ export const smbConfigSchema = type({
|
|||||||
vers: type("'1.0' | '2.0' | '2.1' | '3.0'").default("3.0"),
|
vers: type("'1.0' | '2.0' | '2.1' | '3.0'").default("3.0"),
|
||||||
domain: "string?",
|
domain: "string?",
|
||||||
port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(445),
|
port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(445),
|
||||||
|
readOnly: "boolean?",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const directoryConfigSchema = type({
|
export const directoryConfigSchema = type({
|
||||||
|
|||||||
Reference in New Issue
Block a user