Initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartsamba',
|
||||
version: '0.1.1',
|
||||
description: 'A TypeScript Samba/SMB client and server module backed by an embedded Rust SMB engine.'
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../',
|
||||
);
|
||||
|
||||
export const nogitDir = plugins.path.join(packageDir, '.nogit');
|
||||
@@ -0,0 +1,11 @@
|
||||
// node native scope
|
||||
import * as buffer from 'node:buffer';
|
||||
import * as path from 'node:path';
|
||||
|
||||
export { buffer, path };
|
||||
|
||||
// @push.rocks scope
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartrust from '@push.rocks/smartrust';
|
||||
|
||||
export { smartpath, smartrust };
|
||||
Reference in New Issue
Block a user