Initial commit
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
.nogit/
|
||||
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
|
||||
# caches
|
||||
.yarn/
|
||||
.cache/
|
||||
.rpt2_cache
|
||||
|
||||
# builds
|
||||
dist/
|
||||
dist_*/
|
||||
rust/target/
|
||||
|
||||
# custom
|
||||
.claude/*
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"@git.zone/cli": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "smartsamba",
|
||||
"description": "A TypeScript Samba/SMB client and server module backed by an embedded Rust SMB engine.",
|
||||
"npmPackagename": "@push.rocks/smartsamba",
|
||||
"license": "MIT",
|
||||
"projectDomain": "push.rocks",
|
||||
"keywords": [
|
||||
"samba",
|
||||
"smb",
|
||||
"cifs",
|
||||
"network share",
|
||||
"file sharing",
|
||||
"typescript",
|
||||
"rust",
|
||||
"client",
|
||||
"server"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-05-03 - 0.1.1 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-05-03 - 0.1.0 - project
|
||||
Initial project setup and repository cleanup.
|
||||
|
||||
- Added the initial project structure
|
||||
- Removed committed Rust build artifacts
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Task Venture Capital GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "smartsamba",
|
||||
"description": "A TypeScript Samba/SMB client and server module backed by an embedded Rust SMB engine.",
|
||||
"npmPackagename": "@push.rocks/smartsamba",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"samba",
|
||||
"smb",
|
||||
"cifs",
|
||||
"network share",
|
||||
"file sharing",
|
||||
"typescript",
|
||||
"rust",
|
||||
"client",
|
||||
"server"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@push.rocks/smartsamba",
|
||||
"version": "0.1.1",
|
||||
"private": false,
|
||||
"description": "A TypeScript Samba/SMB client and server module backed by an embedded Rust SMB engine.",
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js"
|
||||
},
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"author": "Task Venture Capital GmbH <hello@task.vc>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test:before": "tsrust",
|
||||
"pretest": "tsrust",
|
||||
"test": "tstest test/ --verbose --timeout 60 --logfile",
|
||||
"build": "tsbuild tsfolders && tsrust",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsrust": "^1.3.3",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@types/node": "^25.6.0",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartrust": "^1.4.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://code.foss.global/push.rocks/smartsamba.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://code.foss.global/push.rocks/smartsamba/issues"
|
||||
},
|
||||
"homepage": "https://code.foss.global/push.rocks/smartsamba",
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_rust/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
".smartconfig.json",
|
||||
"license",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"keywords": [
|
||||
"samba",
|
||||
"smb",
|
||||
"cifs",
|
||||
"network share",
|
||||
"file sharing",
|
||||
"typescript",
|
||||
"rust",
|
||||
"client",
|
||||
"server"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
],
|
||||
"packageManager": "pnpm@10.28.2"
|
||||
}
|
||||
Generated
+7516
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
# @push.rocks/smartsamba
|
||||
|
||||
A TypeScript Samba/SMB client and server module backed by a bundled Rust SMB engine.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
pnpm add @push.rocks/smartsamba
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { SambaServer, SambaClient } from '@push.rocks/smartsamba';
|
||||
|
||||
const server = new SambaServer({
|
||||
port: 0,
|
||||
users: [{ username: 'alice', password: 'secret' }],
|
||||
shares: [{ name: 'files', path: './shared' }],
|
||||
});
|
||||
|
||||
const started = await server.start();
|
||||
|
||||
const client = new SambaClient({
|
||||
host: '127.0.0.1',
|
||||
port: started.port,
|
||||
auth: { username: 'alice', password: 'secret' },
|
||||
});
|
||||
|
||||
await client.writeFile('files', 'hello.txt', 'hello samba');
|
||||
const data = await client.readFileAsString('files', 'hello.txt');
|
||||
|
||||
await client.stop();
|
||||
await server.stop();
|
||||
```
|
||||
|
||||
The TypeScript API uses `@push.rocks/smartrust` to communicate with the bundled `rustsamba` binary built by `@git.zone/tsrust`. It does not wrap system `smbd` or `smbclient`.
|
||||
@@ -0,0 +1,2 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
Generated
+1662
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "rustsamba"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "rustsamba"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
base64 = "0.22"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
futures-util = "0.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
smolder-smb-core = "0.3.0"
|
||||
smb-server = "0.4.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
@@ -0,0 +1,34 @@
|
||||
mod management;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "rustsamba", about = "Embedded SMB client/server engine for smartsamba")]
|
||||
struct Cli {
|
||||
#[arg(long)]
|
||||
management: bool,
|
||||
|
||||
#[arg(long, default_value = "info")]
|
||||
log_level: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_new(&cli.log_level)
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
if cli.management {
|
||||
management::management_loop().await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
eprintln!("rustsamba: use --management for IPC mode");
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::io::Write;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct IpcRequest {
|
||||
id: String,
|
||||
method: String,
|
||||
#[serde(default)]
|
||||
params: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct IpcResponse {
|
||||
id: String,
|
||||
success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
result: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct IpcEvent {
|
||||
event: String,
|
||||
data: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StartServerParams {
|
||||
config: SambaServerConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SambaServerConfig {
|
||||
host: Option<String>,
|
||||
port: Option<u16>,
|
||||
netbios_name: Option<String>,
|
||||
#[serde(default)]
|
||||
users: Vec<SambaServerUser>,
|
||||
shares: Vec<SambaServerShare>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SambaServerUser {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SambaServerShare {
|
||||
name: String,
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
read_only: bool,
|
||||
#[serde(default)]
|
||||
public: bool,
|
||||
users: Option<Vec<SambaServerShareUser>>,
|
||||
create_if_missing: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SambaServerShareUser {
|
||||
username: String,
|
||||
access: Option<SambaShareAccess>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum SambaShareAccess {
|
||||
Read,
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SambaConnectionConfig {
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
domain: Option<String>,
|
||||
timeout_ms: Option<u64>,
|
||||
compression: Option<bool>,
|
||||
dfs_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConnectionParams {
|
||||
connection: SambaConnectionConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SharePathParams {
|
||||
connection: SambaConnectionConfig,
|
||||
share: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WriteFileParams {
|
||||
connection: SambaConnectionConfig,
|
||||
share: String,
|
||||
path: String,
|
||||
data_base64: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RenameParams {
|
||||
connection: SambaConnectionConfig,
|
||||
share: String,
|
||||
from: String,
|
||||
to: String,
|
||||
}
|
||||
|
||||
struct RunningServer {
|
||||
local_addr: SocketAddr,
|
||||
shares: Vec<String>,
|
||||
join: tokio::task::JoinHandle<std::io::Result<()>>,
|
||||
}
|
||||
|
||||
impl RunningServer {
|
||||
fn status(&self) -> Value {
|
||||
serde_json::json!({
|
||||
"running": true,
|
||||
"host": self.local_addr.ip().to_string(),
|
||||
"port": self.local_addr.port(),
|
||||
"address": self.local_addr.to_string(),
|
||||
"shares": self.shares,
|
||||
})
|
||||
}
|
||||
|
||||
async fn stop(self) {
|
||||
self.join.abort();
|
||||
let _ = self.join.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn management_loop() -> Result<()> {
|
||||
send_event("ready", serde_json::json!({}));
|
||||
|
||||
let stdin = BufReader::new(tokio::io::stdin());
|
||||
let mut lines = stdin.lines();
|
||||
let mut server: Option<RunningServer> = None;
|
||||
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let request: IpcRequest = match serde_json::from_str(line) {
|
||||
Ok(request) => request,
|
||||
Err(error) => {
|
||||
send_response(IpcResponse::err(
|
||||
"unknown".to_string(),
|
||||
format!("Invalid IPC request: {error}"),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let id = request.id.clone();
|
||||
let response = handle_request(request, &mut server).await;
|
||||
send_response(match response {
|
||||
Ok(value) => IpcResponse::ok(id, value),
|
||||
Err(error) => IpcResponse::err(id, error.to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(server) = server.take() {
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_request(request: IpcRequest, server: &mut Option<RunningServer>) -> Result<Value> {
|
||||
match request.method.as_str() {
|
||||
"startServer" => {
|
||||
let params: StartServerParams = serde_json::from_value(request.params)?;
|
||||
start_server(params.config, server).await
|
||||
}
|
||||
"stopServer" => {
|
||||
if let Some(running) = server.take() {
|
||||
running.stop().await;
|
||||
}
|
||||
Ok(serde_json::json!({}))
|
||||
}
|
||||
"getServerStatus" => Ok(match server.as_ref() {
|
||||
Some(running) => running.status(),
|
||||
None => serde_json::json!({ "running": false, "shares": [] }),
|
||||
}),
|
||||
"listShares" => {
|
||||
let params: ConnectionParams = serde_json::from_value(request.params)?;
|
||||
list_shares(params.connection).await
|
||||
}
|
||||
"listDirectory" => {
|
||||
let params: SharePathParams = serde_json::from_value(request.params)?;
|
||||
list_directory(params).await
|
||||
}
|
||||
"readFile" => {
|
||||
let params: SharePathParams = serde_json::from_value(request.params)?;
|
||||
read_file(params).await
|
||||
}
|
||||
"writeFile" => {
|
||||
let params: WriteFileParams = serde_json::from_value(request.params)?;
|
||||
write_file(params).await
|
||||
}
|
||||
"deleteFile" => {
|
||||
let params: SharePathParams = serde_json::from_value(request.params)?;
|
||||
delete_file(params).await
|
||||
}
|
||||
"createDirectory" => {
|
||||
let params: SharePathParams = serde_json::from_value(request.params)?;
|
||||
create_directory(params).await
|
||||
}
|
||||
"rename" => {
|
||||
let params: RenameParams = serde_json::from_value(request.params)?;
|
||||
rename_path(params).await
|
||||
}
|
||||
"stat" => {
|
||||
let params: SharePathParams = serde_json::from_value(request.params)?;
|
||||
stat_path(params).await
|
||||
}
|
||||
unknown => Err(anyhow!("Unknown method: {unknown}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_server(config: SambaServerConfig, server: &mut Option<RunningServer>) -> Result<Value> {
|
||||
if server.is_some() {
|
||||
return Err(anyhow!("Samba server is already running"));
|
||||
}
|
||||
if config.shares.is_empty() {
|
||||
return Err(anyhow!("At least one share must be configured"));
|
||||
}
|
||||
|
||||
let host = config.host.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||
let port = config.port.unwrap_or(445);
|
||||
let ip: IpAddr = host.parse().with_context(|| format!("Invalid server host: {host}"))?;
|
||||
let listen_addr = SocketAddr::new(ip, port);
|
||||
|
||||
let mut builder = smb_server::SmbServer::builder().listen(listen_addr);
|
||||
if let Some(netbios_name) = config.netbios_name {
|
||||
builder = builder.netbios_name(netbios_name);
|
||||
}
|
||||
for user in &config.users {
|
||||
builder = builder.user(user.username.clone(), user.password.clone());
|
||||
}
|
||||
|
||||
let mut share_names = Vec::new();
|
||||
for share_config in &config.shares {
|
||||
if share_config.create_if_missing.unwrap_or(true) {
|
||||
std::fs::create_dir_all(&share_config.path)
|
||||
.with_context(|| format!("Failed to create share path {}", share_config.path))?;
|
||||
}
|
||||
|
||||
let backend = smb_server::LocalFsBackend::new(Path::new(&share_config.path))
|
||||
.with_context(|| format!("Failed to open share path {}", share_config.path))?;
|
||||
let backend = if share_config.read_only {
|
||||
backend.read_only()
|
||||
} else {
|
||||
backend
|
||||
};
|
||||
let mut share = smb_server::Share::new(share_config.name.clone(), backend);
|
||||
|
||||
if share_config.public {
|
||||
share = if share_config.read_only {
|
||||
share.public_read_only()
|
||||
} else {
|
||||
share.public()
|
||||
};
|
||||
} else {
|
||||
let configured_users = share_config.users.clone().unwrap_or_else(|| {
|
||||
config
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| SambaServerShareUser {
|
||||
username: user.username.clone(),
|
||||
access: Some(SambaShareAccess::ReadWrite),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
if configured_users.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"Share {} needs either public=true or at least one configured user",
|
||||
share_config.name
|
||||
));
|
||||
}
|
||||
for share_user in configured_users {
|
||||
share = share.user(share_user.username, access_to_smb(share_user.access));
|
||||
}
|
||||
}
|
||||
|
||||
share_names.push(share_config.name.clone());
|
||||
builder = builder.share(share);
|
||||
}
|
||||
|
||||
let smb_server = builder.build().map_err(|error| anyhow!(error.to_string()))?;
|
||||
let local_addr = smb_server.bind().await?;
|
||||
let join = tokio::spawn(async move { smb_server.serve().await });
|
||||
|
||||
let running = RunningServer {
|
||||
local_addr,
|
||||
shares: share_names.clone(),
|
||||
join,
|
||||
};
|
||||
let result = serde_json::json!({
|
||||
"host": local_addr.ip().to_string(),
|
||||
"port": local_addr.port(),
|
||||
"address": local_addr.to_string(),
|
||||
"shares": share_names,
|
||||
});
|
||||
*server = Some(running);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn access_to_smb(access: Option<SambaShareAccess>) -> smb_server::Access {
|
||||
match access.unwrap_or(SambaShareAccess::ReadWrite) {
|
||||
SambaShareAccess::Read => smb_server::Access::Read,
|
||||
SambaShareAccess::ReadWrite => smb_server::Access::ReadWrite,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_smb_path(path: &str) -> String {
|
||||
path.trim_matches(['/', '\\']).replace('/', "\\")
|
||||
}
|
||||
|
||||
fn directory_smb_path(path: &str) -> String {
|
||||
let normalized = normalize_smb_path(path);
|
||||
if normalized.is_empty() {
|
||||
"\\".to_string()
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
}
|
||||
|
||||
fn smolder_error(error: smolder_core::error::CoreError) -> anyhow::Error {
|
||||
anyhow!(error.to_string())
|
||||
}
|
||||
|
||||
fn finish_smolder<T>(
|
||||
operation_result: std::result::Result<T, smolder_core::error::CoreError>,
|
||||
logoff_result: std::result::Result<(), smolder_core::error::CoreError>,
|
||||
) -> Result<T> {
|
||||
match (operation_result, logoff_result) {
|
||||
(Ok(value), Ok(())) => Ok(value),
|
||||
(Err(error), _) => Err(smolder_error(error)),
|
||||
(Ok(_), Err(error)) => Err(smolder_error(error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn system_time_to_filetime(time: Option<SystemTime>) -> u64 {
|
||||
const WINDOWS_TICK: u64 = 10_000_000;
|
||||
const SEC_TO_UNIX_EPOCH: u64 = 11_644_473_600;
|
||||
|
||||
match time.and_then(|value| value.duration_since(UNIX_EPOCH).ok()) {
|
||||
Some(duration) => {
|
||||
(duration.as_secs() + SEC_TO_UNIX_EPOCH) * WINDOWS_TICK
|
||||
+ u64::from(duration.subsec_nanos()) / 100
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata_to_json(metadata: &smolder_core::facade::FileMetadata) -> Value {
|
||||
serde_json::json!({
|
||||
"size": metadata.size,
|
||||
"isDirectory": metadata.is_directory(),
|
||||
"createdFiletime": system_time_to_filetime(metadata.created),
|
||||
"modifiedFiletime": system_time_to_filetime(metadata.written),
|
||||
"accessedFiletime": system_time_to_filetime(metadata.accessed),
|
||||
})
|
||||
}
|
||||
|
||||
fn directory_entry_to_json(entry: smolder_core::facade::DirectoryEntry) -> Value {
|
||||
let smolder_core::facade::DirectoryEntry { name, metadata, .. } = entry;
|
||||
serde_json::json!({
|
||||
"name": name,
|
||||
"size": metadata.size,
|
||||
"isDirectory": metadata.is_directory(),
|
||||
"createdFiletime": system_time_to_filetime(metadata.created),
|
||||
"modifiedFiletime": system_time_to_filetime(metadata.written),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_smb_client(config: &SambaConnectionConfig) -> Result<smolder_core::facade::Client> {
|
||||
let _unsupported_options = (config.timeout_ms, config.compression, config.dfs_enabled);
|
||||
let mut credentials = smolder_core::auth::NtlmCredentials::new(
|
||||
config.username.clone().unwrap_or_default(),
|
||||
config.password.clone().unwrap_or_default(),
|
||||
);
|
||||
if let Some(domain) = &config.domain {
|
||||
if !domain.is_empty() {
|
||||
credentials = credentials.with_domain(domain.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut builder = smolder_core::facade::Client::builder(config.host.clone())
|
||||
.with_ntlm_credentials(credentials);
|
||||
if let Some(port) = config.port {
|
||||
builder = builder.with_port(port);
|
||||
}
|
||||
|
||||
builder.build().map_err(smolder_error)
|
||||
}
|
||||
|
||||
async fn connect_share_client(
|
||||
config: &SambaConnectionConfig,
|
||||
share: &str,
|
||||
) -> Result<smolder_core::facade::Share> {
|
||||
create_smb_client(config)?
|
||||
.connect_share(share)
|
||||
.await
|
||||
.map_err(smolder_error)
|
||||
.with_context(|| format!("Failed to connect to share {share}"))
|
||||
}
|
||||
|
||||
async fn list_shares(connection: SambaConnectionConfig) -> Result<Value> {
|
||||
let mut srvsvc = create_smb_client(&connection)?
|
||||
.connect_srvsvc()
|
||||
.await
|
||||
.map_err(smolder_error)?;
|
||||
let shares = srvsvc
|
||||
.share_enum_level1()
|
||||
.await
|
||||
.map_err(smolder_error)?
|
||||
.into_iter()
|
||||
.map(|share| {
|
||||
serde_json::json!({
|
||||
"name": share.name,
|
||||
"shareType": share.share_type,
|
||||
"comment": share.remark.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(serde_json::json!({ "shares": shares }))
|
||||
}
|
||||
|
||||
async fn list_directory(params: SharePathParams) -> Result<Value> {
|
||||
let mut share = connect_share_client(¶ms.connection, ¶ms.share).await?;
|
||||
let path = directory_smb_path(¶ms.path);
|
||||
let entries_result = share.list(&path).await;
|
||||
let logoff_result = share.logoff().await;
|
||||
let entries = finish_smolder(entries_result, logoff_result)?
|
||||
.into_iter()
|
||||
.map(directory_entry_to_json)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(serde_json::json!({ "entries": entries }))
|
||||
}
|
||||
|
||||
async fn read_file(params: SharePathParams) -> Result<Value> {
|
||||
let mut share = connect_share_client(¶ms.connection, ¶ms.share).await?;
|
||||
let path = normalize_smb_path(¶ms.path);
|
||||
let data_result = share.get(&path).await;
|
||||
let logoff_result = share.logoff().await;
|
||||
let data = finish_smolder(data_result, logoff_result)?;
|
||||
let size = data.len();
|
||||
Ok(serde_json::json!({
|
||||
"dataBase64": general_purpose::STANDARD.encode(data),
|
||||
"size": size,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn write_file(params: WriteFileParams) -> Result<Value> {
|
||||
let data = general_purpose::STANDARD
|
||||
.decode(params.data_base64.as_bytes())
|
||||
.context("Invalid base64 file data")?;
|
||||
let bytes_written = data.len();
|
||||
let mut share = connect_share_client(¶ms.connection, ¶ms.share).await?;
|
||||
let path = normalize_smb_path(¶ms.path);
|
||||
let write_result = share.put(&path, &data).await;
|
||||
let logoff_result = share.logoff().await;
|
||||
finish_smolder(write_result, logoff_result)?;
|
||||
Ok(serde_json::json!({ "bytesWritten": bytes_written }))
|
||||
}
|
||||
|
||||
async fn delete_file(params: SharePathParams) -> Result<Value> {
|
||||
let mut share = connect_share_client(¶ms.connection, ¶ms.share).await?;
|
||||
let path = normalize_smb_path(¶ms.path);
|
||||
let remove_result = share.remove(&path).await;
|
||||
let logoff_result = share.logoff().await;
|
||||
finish_smolder(remove_result, logoff_result)?;
|
||||
Ok(serde_json::json!({}))
|
||||
}
|
||||
|
||||
async fn create_directory(params: SharePathParams) -> Result<Value> {
|
||||
let mut share = connect_share_client(¶ms.connection, ¶ms.share).await?;
|
||||
let path = normalize_smb_path(¶ms.path);
|
||||
let create_result = share.create_dir(&path).await;
|
||||
let logoff_result = share.logoff().await;
|
||||
finish_smolder(create_result, logoff_result)?;
|
||||
Ok(serde_json::json!({}))
|
||||
}
|
||||
|
||||
async fn rename_path(params: RenameParams) -> Result<Value> {
|
||||
let mut share = connect_share_client(¶ms.connection, ¶ms.share).await?;
|
||||
let from_path = normalize_smb_path(¶ms.from);
|
||||
let to_path = normalize_smb_path(¶ms.to);
|
||||
let rename_result = share.rename(&from_path, &to_path).await;
|
||||
let logoff_result = share.logoff().await;
|
||||
finish_smolder(rename_result, logoff_result)?;
|
||||
Ok(serde_json::json!({}))
|
||||
}
|
||||
|
||||
async fn stat_path(params: SharePathParams) -> Result<Value> {
|
||||
let mut share = connect_share_client(¶ms.connection, ¶ms.share).await?;
|
||||
let path = normalize_smb_path(¶ms.path);
|
||||
let stat_result = share.stat(&path).await;
|
||||
let logoff_result = share.logoff().await;
|
||||
let metadata = finish_smolder(stat_result, logoff_result)?;
|
||||
Ok(metadata_to_json(&metadata))
|
||||
}
|
||||
|
||||
fn send_response(response: IpcResponse) {
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
if serde_json::to_writer(&mut stdout, &response).is_ok() {
|
||||
let _ = stdout.write_all(b"\n");
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn send_event(event: &str, data: Value) {
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
let payload = IpcEvent {
|
||||
event: event.to_string(),
|
||||
data,
|
||||
};
|
||||
if serde_json::to_writer(&mut stdout, &payload).is_ok() {
|
||||
let _ = stdout.write_all(b"\n");
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcResponse {
|
||||
fn ok(id: String, result: Value) -> Self {
|
||||
Self {
|
||||
id,
|
||||
success: true,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn err(id: String, message: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
success: false,
|
||||
result: None,
|
||||
error: Some(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { SambaClient, SambaServer } from '../ts/index.js';
|
||||
|
||||
tap.test('should serve and access a local SMB share', async () => {
|
||||
const sharePath = path.join(process.cwd(), '.nogit', `smartsamba-${Date.now()}`);
|
||||
await fs.mkdir(sharePath, { recursive: true });
|
||||
|
||||
const server = new SambaServer({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
users: [{ username: 'alice', password: 'secret' }],
|
||||
shares: [{ name: 'files', path: sharePath }],
|
||||
});
|
||||
|
||||
const started = await server.start();
|
||||
const client = new SambaClient({
|
||||
host: '127.0.0.1',
|
||||
port: started.port,
|
||||
auth: { username: 'alice', password: 'secret' },
|
||||
timeoutMs: 10000,
|
||||
});
|
||||
|
||||
try {
|
||||
await client.writeFile('files', 'hello.txt', 'hello samba');
|
||||
const content = await client.readFileAsString('files', 'hello.txt');
|
||||
expect(content).toEqual('hello samba');
|
||||
|
||||
const entries = await client.listDirectory('files');
|
||||
expect(entries.some((entry) => entry.name === 'hello.txt')).toBeTrue();
|
||||
|
||||
const info = await client.stat('files', 'hello.txt');
|
||||
expect(info.size).toEqual('hello samba'.length);
|
||||
expect(info.isDirectory).toBeFalse();
|
||||
|
||||
await client.createDirectory('files', 'nested');
|
||||
await client.rename('files', 'hello.txt', 'nested/renamed.txt');
|
||||
const renamed = await client.readFileAsString('files', 'nested/renamed.txt');
|
||||
expect(renamed).toEqual('hello samba');
|
||||
|
||||
await client.deleteFile('files', 'nested/renamed.txt');
|
||||
} finally {
|
||||
await client.stop();
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user