Files
2026-05-03 10:44:02 +00:00

329 lines
9.4 KiB
TypeScript

import * as plugins from './plugins.js';
import * as paths from './paths.js';
export type TSambaShareAccess = 'read' | 'readWrite';
export interface ISambaAuthOptions {
username: string;
password: string;
domain?: string;
}
export interface ISambaClientOptions {
host: string;
port?: number;
auth?: Partial<ISambaAuthOptions>;
timeoutMs?: number;
compression?: boolean;
dfsEnabled?: boolean;
}
export interface ISambaServerUser {
username: string;
password: string;
}
export interface ISambaServerShareUser {
username: string;
access?: TSambaShareAccess;
}
export interface ISambaServerShare {
name: string;
path: string;
readOnly?: boolean;
public?: boolean;
users?: ISambaServerShareUser[];
createIfMissing?: boolean;
}
export interface ISambaServerOptions {
host?: string;
port?: number;
netbiosName?: string;
users?: ISambaServerUser[];
shares: ISambaServerShare[];
}
export interface ISambaServerStartResult {
host: string;
port: number;
address: string;
shares: string[];
}
export interface ISambaServerStatus {
running: boolean;
host?: string;
port?: number;
address?: string;
shares: string[];
}
export interface ISambaDirectoryEntry {
name: string;
size: number;
isDirectory: boolean;
createdFiletime: number;
modifiedFiletime: number;
}
export interface ISambaFileInfo {
size: number;
isDirectory: boolean;
createdFiletime: number;
modifiedFiletime: number;
accessedFiletime: number;
}
export interface ISambaShareInfo {
name: string;
shareType: number;
comment: string;
}
interface IRustSambaConnectionConfig {
host: string;
port?: number;
username?: string;
password?: string;
domain?: string;
timeoutMs?: number;
compression?: boolean;
dfsEnabled?: boolean;
}
type TRustSambaCommands = {
startServer: { params: { config: ISambaServerOptions }; result: ISambaServerStartResult };
stopServer: { params: Record<string, never>; result: Record<string, never> };
getServerStatus: { params: Record<string, never>; result: ISambaServerStatus };
listShares: { params: { connection: IRustSambaConnectionConfig }; result: { shares: ISambaShareInfo[] } };
listDirectory: {
params: { connection: IRustSambaConnectionConfig; share: string; path: string };
result: { entries: ISambaDirectoryEntry[] };
};
readFile: {
params: { connection: IRustSambaConnectionConfig; share: string; path: string };
result: { dataBase64: string; size: number };
};
writeFile: {
params: { connection: IRustSambaConnectionConfig; share: string; path: string; dataBase64: string };
result: { bytesWritten: number };
};
deleteFile: {
params: { connection: IRustSambaConnectionConfig; share: string; path: string };
result: Record<string, never>;
};
createDirectory: {
params: { connection: IRustSambaConnectionConfig; share: string; path: string };
result: Record<string, never>;
};
rename: {
params: { connection: IRustSambaConnectionConfig; share: string; from: string; to: string };
result: Record<string, never>;
};
stat: {
params: { connection: IRustSambaConnectionConfig; share: string; path: string };
result: ISambaFileInfo;
};
};
function getTsrustPlatformSuffix(): string | null {
const archMap: Record<string, string> = { x64: 'amd64', arm64: 'arm64' };
const osMap: Record<string, string> = { linux: 'linux', darwin: 'macos' };
const os = osMap[process.platform];
const arch = archMap[process.arch];
return os && arch ? `${os}_${arch}` : null;
}
function buildLocalRustPaths(): string[] {
const suffix = getTsrustPlatformSuffix();
const localPaths: string[] = [];
if (suffix) {
localPaths.push(plugins.path.join(paths.packageDir, 'dist_rust', `rustsamba_${suffix}`));
}
localPaths.push(plugins.path.join(paths.packageDir, 'dist_rust', 'rustsamba'));
localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'release', 'rustsamba'));
localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'debug', 'rustsamba'));
return localPaths;
}
class SambaBridge {
private bridge = new plugins.smartrust.RustBridge<TRustSambaCommands>({
binaryName: 'rustsamba',
envVarName: 'SMARTSAMBA_RUST_BINARY',
platformPackagePrefix: '@push.rocks/smartsamba',
localPaths: buildLocalRustPaths(),
readyTimeoutMs: 30000,
requestTimeoutMs: 300000,
maxPayloadSize: 128 * 1024 * 1024,
});
public async ensureRunning(): Promise<void> {
if (this.bridge.running) {
return;
}
const spawned = await this.bridge.spawn();
if (!spawned) {
throw new Error('Failed to spawn rustsamba binary. Run pnpm build or pnpm run test:before first.');
}
}
public async sendCommand<K extends string & keyof TRustSambaCommands>(
method: K,
params: TRustSambaCommands[K]['params'],
): Promise<TRustSambaCommands[K]['result']> {
await this.ensureRunning();
return this.bridge.sendCommand(method, params);
}
public kill(): void {
this.bridge.kill();
}
}
function normalizeClientOptions(optionsArg: ISambaClientOptions): IRustSambaConnectionConfig {
return {
host: optionsArg.host,
...(optionsArg.port ? { port: optionsArg.port } : {}),
...(optionsArg.auth?.username ? { username: optionsArg.auth.username } : {}),
...(optionsArg.auth?.password ? { password: optionsArg.auth.password } : {}),
...(optionsArg.auth?.domain ? { domain: optionsArg.auth.domain } : {}),
...(optionsArg.timeoutMs ? { timeoutMs: optionsArg.timeoutMs } : {}),
...(typeof optionsArg.compression === 'boolean' ? { compression: optionsArg.compression } : {}),
...(typeof optionsArg.dfsEnabled === 'boolean' ? { dfsEnabled: optionsArg.dfsEnabled } : {}),
};
}
export class SambaClient {
private bridge = new SambaBridge();
private connection: IRustSambaConnectionConfig;
constructor(optionsArg: ISambaClientOptions) {
this.connection = normalizeClientOptions(optionsArg);
}
public async start(): Promise<void> {
await this.bridge.ensureRunning();
}
public async stop(): Promise<void> {
this.bridge.kill();
}
public async listShares(): Promise<ISambaShareInfo[]> {
const result = await this.bridge.sendCommand('listShares', { connection: this.connection });
return result.shares;
}
public async listDirectory(shareArg: string, pathArg = ''): Promise<ISambaDirectoryEntry[]> {
const result = await this.bridge.sendCommand('listDirectory', {
connection: this.connection,
share: shareArg,
path: pathArg,
});
return result.entries;
}
public async readFile(shareArg: string, pathArg: string): Promise<Buffer> {
const result = await this.bridge.sendCommand('readFile', {
connection: this.connection,
share: shareArg,
path: pathArg,
});
return plugins.buffer.Buffer.from(result.dataBase64, 'base64');
}
public async readFileAsString(shareArg: string, pathArg: string, encoding: BufferEncoding = 'utf8') {
const buffer = await this.readFile(shareArg, pathArg);
return buffer.toString(encoding);
}
public async writeFile(shareArg: string, pathArg: string, dataArg: Buffer | string): Promise<number> {
const buffer = typeof dataArg === 'string' ? plugins.buffer.Buffer.from(dataArg) : dataArg;
const result = await this.bridge.sendCommand('writeFile', {
connection: this.connection,
share: shareArg,
path: pathArg,
dataBase64: buffer.toString('base64'),
});
return result.bytesWritten;
}
public async deleteFile(shareArg: string, pathArg: string): Promise<void> {
await this.bridge.sendCommand('deleteFile', {
connection: this.connection,
share: shareArg,
path: pathArg,
});
}
public async createDirectory(shareArg: string, pathArg: string): Promise<void> {
await this.bridge.sendCommand('createDirectory', {
connection: this.connection,
share: shareArg,
path: pathArg,
});
}
public async rename(shareArg: string, fromArg: string, toArg: string): Promise<void> {
await this.bridge.sendCommand('rename', {
connection: this.connection,
share: shareArg,
from: fromArg,
to: toArg,
});
}
public async stat(shareArg: string, pathArg: string): Promise<ISambaFileInfo> {
return this.bridge.sendCommand('stat', {
connection: this.connection,
share: shareArg,
path: pathArg,
});
}
}
export class SambaServer {
private bridge = new SambaBridge();
private config: ISambaServerOptions;
private startResult?: ISambaServerStartResult;
constructor(optionsArg: ISambaServerOptions) {
this.config = {
host: '127.0.0.1',
port: 445,
...optionsArg,
};
}
public async start(): Promise<ISambaServerStartResult> {
this.startResult = await this.bridge.sendCommand('startServer', { config: this.config });
return this.startResult;
}
public async stop(): Promise<void> {
try {
await this.bridge.sendCommand('stopServer', {} as Record<string, never>);
} finally {
this.bridge.kill();
this.startResult = undefined;
}
}
public async status(): Promise<ISambaServerStatus> {
return this.bridge.sendCommand('getServerStatus', {} as Record<string, never>);
}
public getConnectionOptions(authArg?: Partial<ISambaAuthOptions>): ISambaClientOptions {
if (!this.startResult) {
throw new Error('SambaServer is not started');
}
return {
host: this.startResult.host,
port: this.startResult.port,
auth: authArg,
};
}
}