329 lines
9.4 KiB
TypeScript
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,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|