feat: add config for SMB guest mode (#431)

* feat: add config for SMB guest mode

Closes #279

* chore: pr feedbacks
This commit is contained in:
Nico 2026-01-29 23:16:31 +01:00 committed by GitHub
parent f62ae222ca
commit da5ad5c817
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 108 additions and 50 deletions

View file

@ -97,14 +97,15 @@ export type ListVolumesResponses = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";
@ -166,14 +167,15 @@ export type CreateVolumeData = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";
@ -232,14 +234,15 @@ export type CreateVolumeResponses = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";
@ -301,14 +304,15 @@ export type TestConnectionData = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";
@ -420,14 +424,15 @@ export type GetVolumeResponses = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";
@ -491,14 +496,15 @@ export type UpdateVolumeData = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";
@ -566,14 +572,15 @@ export type UpdateVolumeResponses = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";
@ -2103,14 +2110,15 @@ export type ListBackupSchedulesResponses = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";
@ -2485,14 +2493,15 @@ export type GetBackupScheduleResponses = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";
@ -2848,14 +2857,15 @@ export type GetBackupScheduleForVolumeResponses = {
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto";
port?: number;
domain?: string;
guest?: boolean;
password?: string;
readOnly?: boolean;
username?: string;
}
| {
backend: "webdav";

View file

@ -17,6 +17,8 @@ type Props = {
};
export const SMBForm = ({ form }: Props) => {
const guest = form.watch("guest");
return (
<>
<FormField
@ -47,6 +49,33 @@ export const SMBForm = ({ form }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="guest"
defaultValue={false}
render={({ field }) => (
<FormItem>
<FormLabel>Guest 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">Connect as guest (no authentication)</span>
</div>
</FormControl>
<FormDescription>
Connect to the SMB share without credentials. Username and password fields will be disabled.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
@ -54,7 +83,12 @@ export const SMBForm = ({ form }: Props) => {
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="admin" value={field.value} onChange={field.onChange} />
<Input
placeholder="admin"
value={field.value}
onChange={field.onChange}
disabled={guest}
/>
</FormControl>
<FormDescription>Username for SMB authentication.</FormDescription>
<FormMessage />
@ -68,7 +102,12 @@ export const SMBForm = ({ form }: Props) => {
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
<SecretInput
placeholder="••••••••"
value={field.value ?? ""}
onChange={field.onChange}
disabled={guest}
/>
</FormControl>
<FormDescription>Password for SMB authentication.</FormDescription>
<FormMessage />

View file

@ -24,8 +24,9 @@ export const smbConfigSchema = type({
backend: "'smb'",
server: "string",
share: "string",
username: "string",
password: "string",
username: "string?",
password: "string?",
guest: "boolean?",
vers: type("'1.0' | '2.0' | '2.1' | '3.0' | 'auto'").default("auto"),
domain: "string?",
port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(445),

View file

@ -36,36 +36,44 @@ const mount = async (config: BackendConfig, path: string) => {
const run = async () => {
await fs.mkdir(path, { recursive: true });
const password = await cryptoUtils.resolveSecret(config.password);
const source = `//${config.server}/${config.share}`;
const { uid, gid } = os.userInfo();
const options = [`port=${config.port}`, `uid=${uid}`, `gid=${gid}`];
const credentialsDir = await fs.mkdtemp(`${os.tmpdir()}/zerobyte-smb-`);
const credentialsPath = `${credentialsDir}/credentials`;
const credentialsLines = [`username=${config.username}`, `password=${password}`];
if (config.domain) {
credentialsLines.push(`domain=${config.domain}`);
}
await fs.writeFile(credentialsPath, credentialsLines.join("\n"), { mode: 0o600 });
const options = [`credentials=${credentialsPath}`, `port=${config.port}`, `uid=${uid}`, `gid=${gid}`];
if (config.vers && config.vers !== "auto") {
options.push(`vers=${config.vers}`);
}
if (config.readOnly) {
options.push("ro");
}
const args = ["-t", "cifs", "-o", options.join(","), source, path];
logger.debug(`Mounting SMB volume ${path}...`);
logger.info(`Executing mount: mount ${args.join(" ")}`);
try {
const credentialsLines: string[] = [];
if (config.guest) {
credentialsLines.push("username=guest", "password=");
} else {
const password = await cryptoUtils.resolveSecret(config.password ?? "");
credentialsLines.push(`username=${config.username ?? ""}`, `password=${password}`);
}
if (config.domain) {
credentialsLines.push(`domain=${config.domain}`);
}
await fs.writeFile(credentialsPath, credentialsLines.join("\n"), { mode: 0o600 });
options.push(`credentials=${credentialsPath}`);
if (config.vers && config.vers !== "auto") {
options.push(`vers=${config.vers}`);
}
if (config.readOnly) {
options.push("ro");
}
const args = ["-t", "cifs", "-o", options.join(","), source, path];
logger.debug(`Mounting SMB volume ${path}...`);
logger.info(`Executing mount: mount ${args.join(" ")}`);
try {
await executeMount(args);
} catch (error) {
@ -74,7 +82,7 @@ const mount = async (config: BackendConfig, path: string) => {
}
} finally {
await fs.rm(credentialsPath, { force: true });
await fs.rmdir(credentialsDir).catch(() => {});
await fs.rm(credentialsDir, { force: true, recursive: true });
}
logger.info(`SMB volume at ${path} mounted successfully.`);

View file

@ -24,7 +24,7 @@ async function encryptSensitiveFields(config: BackendConfig): Promise<BackendCon
case "smb":
return {
...config,
password: await cryptoUtils.sealSecret(config.password),
password: config.password ? await cryptoUtils.sealSecret(config.password) : undefined,
};
case "webdav":
return {