bambu-js
Version:
Tools to interact with Bambu Lab printers
305 lines • 11.6 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import EventEmitter from "node:events";
import * as ftp from "basic-ftp";
import { FileConnectionError, FileTimeoutError, FileValidationError, FileOperationError, FileNotFoundError, FileUploadError, FileDownloadError, FileDeleteError, FileDirectoryError, FileDisconnectionError, } from "./utilities/file-errors.js";
/**
* Represents the connection state of the file controller.
*/
var ConnectionState;
(function (ConnectionState) {
ConnectionState["DISCONNECTED"] = "disconnected";
ConnectionState["CONNECTING"] = "connecting";
ConnectionState["CONNECTED"] = "connected";
ConnectionState["DISCONNECTING"] = "disconnecting";
})(ConnectionState || (ConnectionState = {}));
/**
* Controller for managing file operations on a Bambu Lab printer's FTP server.
*/
export class FileController extends EventEmitter {
host;
accessCode;
client = null;
connectionState = ConnectionState.DISCONNECTED;
options;
connectionTimeout = null;
constructor(host, accessCode, options = {}) {
super();
// Validate input parameters
if (!host || typeof host !== "string") {
throw new FileValidationError("Host must be a non-empty string");
}
if (!accessCode || typeof accessCode !== "string") {
throw new FileValidationError("Access code must be a non-empty string");
}
this.host = host;
this.accessCode = accessCode;
this.options = {
timeout: options.timeout ?? 15000,
};
}
/**
* Creates a new file controller with the specified configuration.
* @param config - Configuration object containing printer information.
* @returns A new FileController instance
*/
static create(config) {
return new FileController(config.host, config.accessCode, config.options);
}
/**
* Gets the current connection state.
*/
get isConnected() {
return this.connectionState === ConnectionState.CONNECTED;
}
/**
* Gets the printer host.
*/
getHost() {
return this.host;
}
/**
* Gets the printer access code.
*/
getAccessCode() {
return this.accessCode;
}
/**
* Gets the current connection state.
*/
get state() {
return this.connectionState;
}
/**
* Connects to the printer's FTP server.
* @returns A promise that resolves when the connection is established.
*/
async connect() {
return new Promise((resolve, reject) => {
if (this.connectionState === ConnectionState.CONNECTED) {
resolve();
return;
}
if (this.connectionState === ConnectionState.CONNECTING) {
reject(new FileConnectionError("Connection already in progress"));
return;
}
this.connectionState = ConnectionState.CONNECTING;
this.clearTimeouts();
// Set connection timeout
this.connectionTimeout = setTimeout(() => {
this.cleanupConnection();
this.connectionState = ConnectionState.DISCONNECTED;
reject(new FileTimeoutError("Connection timeout"));
}, this.options.timeout);
try {
this.client = new ftp.Client(this.options.timeout);
// Set up event handlers
this.setupEventHandlers();
// Attempt connection
this.client
.access({
host: this.host,
port: 990,
user: "bblp",
password: this.accessCode,
secureOptions: {
rejectUnauthorized: false,
},
secure: "implicit",
})
.then(() => {
this.clearTimeouts();
this.connectionState = ConnectionState.CONNECTED;
this.emit("connect");
resolve();
})
.catch((error) => {
this.clearTimeouts();
this.cleanupConnection();
this.connectionState = ConnectionState.DISCONNECTED;
reject(new FileConnectionError(`Connection failed: ${error.message}`, error));
});
}
catch (error) {
this.clearTimeouts();
this.connectionState = ConnectionState.DISCONNECTED;
reject(error instanceof Error
? new FileConnectionError(error.message, error)
: new FileConnectionError("Unknown connection error"));
}
});
}
/**
* Disconnect from the printer's FTP server.
* @returns A promise that resolves when the disconnection is complete.
*/
async disconnect() {
if (this.connectionState === ConnectionState.DISCONNECTED) {
return;
}
if (this.connectionState === ConnectionState.DISCONNECTING) {
return;
}
this.connectionState = ConnectionState.DISCONNECTING;
this.clearTimeouts();
if (!this.client) {
this.connectionState = ConnectionState.DISCONNECTED;
return;
}
try {
await this.client.close();
}
catch (error) {
// Emit error but continue cleanup
this.emit("error", new FileDisconnectionError(`Error during disconnect: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined));
}
finally {
this.cleanupConnection();
this.connectionState = ConnectionState.DISCONNECTED;
}
}
/**
* List all files within a specified directory on the printer's file system.
* @param dirPath - The absolute path to the directory on the printer's file system.
* @returns A promise that resolves to an array of files in the directory.
*/
async listDir(dirPath) {
if (!this.isConnected || !this.client) {
throw new FileConnectionError("Not connected to the printer");
}
if (!dirPath || typeof dirPath !== "string") {
throw new FileValidationError("Directory path must be a non-empty string");
}
try {
return await this.client.list(dirPath);
}
catch (error) {
throw new FileDirectoryError(`Failed to read directory: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined);
}
}
/**
* Upload a file to the printer's FTP server.
* @param localPath - The local file path to upload.
* @param remotePath - The remote path where the file should be uploaded.
*/
async uploadFile(localPath, remotePath) {
if (!this.isConnected || !this.client) {
throw new FileConnectionError("Not connected to the printer");
}
if (!localPath || typeof localPath !== "string") {
throw new FileValidationError("Local path must be a non-empty string");
}
if (!remotePath || typeof remotePath !== "string") {
throw new FileValidationError("Remote path must be a non-empty string");
}
// Check if local file exists
if (!fs.existsSync(localPath)) {
throw new FileNotFoundError(`Local file does not exist: ${localPath}`);
}
try {
await this.client.uploadFrom(localPath, remotePath);
}
catch (error) {
throw new FileUploadError(`Failed to upload file: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined);
}
}
/**
* Download a file from the printer's FTP server.
* @param remotePath - The remote file path to download.
* @param localPath - The local path where the file should be saved.
*/
async downloadFile(remotePath, localPath) {
if (!this.isConnected || !this.client) {
throw new FileConnectionError("Not connected to the printer");
}
if (!remotePath || typeof remotePath !== "string") {
throw new FileValidationError("Remote path must be a non-empty string");
}
if (!localPath || typeof localPath !== "string") {
throw new FileValidationError("Local path must be a non-empty string");
}
// Ensure local directory exists
const localDir = path.dirname(localPath);
if (!fs.existsSync(localDir)) {
fs.mkdirSync(localDir, { recursive: true });
}
try {
await this.client.downloadTo(localPath, remotePath);
}
catch (error) {
throw new FileDownloadError(`Failed to download file: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined);
}
}
/**
* Delete a file from the printer's FTP server.
* @param remotePath - The remote file path to delete.
*/
async deleteFile(remotePath) {
if (!this.isConnected || !this.client) {
throw new FileConnectionError("Not connected to the printer");
}
if (!remotePath || typeof remotePath !== "string") {
throw new FileValidationError("Remote path must be a non-empty string");
}
try {
await this.client.remove(remotePath);
}
catch (error) {
throw new FileDeleteError(`Failed to delete file: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined);
}
}
/**
* Ensure a given directory exists on the printer's file system.
* @param dirPath - The absolute path to the directory on the printer's file system.
* @returns A promise that resolves when the directory exists.
*/
async ensureDir(dirPath) {
if (!this.isConnected || !this.client) {
throw new FileConnectionError("Not connected to the printer");
}
if (!dirPath || typeof dirPath !== "string") {
throw new FileValidationError("Directory path must be a non-empty string");
}
try {
await this.client.ensureDir(dirPath);
}
catch (error) {
throw new FileDirectoryError(`Failed to ensure directory exists: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : undefined);
}
}
/**
* Sets up event handlers for the FTP client.
*/
setupEventHandlers() {
if (!this.client)
return;
// Handle progress events
this.client.trackProgress((info) => {
this.emit("progress", {
bytesOverall: info.bytesOverall,
bytesTransferred: info.bytes,
});
});
}
/**
* Clears all active timeouts.
*/
clearTimeouts() {
if (this.connectionTimeout) {
clearTimeout(this.connectionTimeout);
this.connectionTimeout = null;
}
}
/**
* Cleans up the FTP connection and removes all event listeners.
*/
cleanupConnection() {
if (this.client) {
this.client.close();
this.client = null;
}
this.clearTimeouts();
}
}
//# sourceMappingURL=file-controller.js.map