UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

196 lines (195 loc) 6.94 kB
import { uploadDoneEvent, uploadFailedEvent, uploadProgressEvent } from "../../constants/event.constants.js"; import { AppConstants } from "../../server.constants.js"; import { getMediaPath } from "../../utils/fs.utils.js"; import { errorSummary } from "../../utils/error.utils.js"; import { join } from "node:path"; import { createReadStream, existsSync, mkdirSync, unlinkSync } from "node:fs"; import { Client } from "basic-ftp"; //#region src/services/bambu/bambu-ftp.adapter.ts var BambuFtpAdapter = class BambuFtpAdapter { logger; ftpClient = null; host = null; accessCode = null; isConnecting = false; constructor(settingsStore, loggerFactory, eventEmitter2) { this.settingsStore = settingsStore; this.eventEmitter2 = eventEmitter2; this.logger = loggerFactory(BambuFtpAdapter.name); } /** * Connect to FTP server */ async connect(host, accessCode) { if (this.ftpClient && !this.ftpClient.closed) { this.logger.debug("FTP already connected"); return; } if (this.isConnecting) throw new Error("Connection already in progress"); const sanitizedHost = this.sanitizeHost(host); const sanitizedAccessCode = this.sanitizeAccessCode(accessCode); this.host = sanitizedHost; this.accessCode = sanitizedAccessCode; this.isConnecting = true; const timeout = this.settingsStore.getTimeoutSettings().apiTimeout; this.logger.log(`Connecting to Bambu FTP at ${sanitizedHost}:990`); try { this.ftpClient = new Client(timeout); this.ftpClient.ftp.verbose = false; await this.ftpClient.access({ host: sanitizedHost, port: 990, user: "bblp", password: sanitizedAccessCode, secure: "implicit", secureOptions: { checkServerIdentity: () => {}, rejectUnauthorized: false } }); this.isConnecting = false; this.logger.log("FTP connected successfully"); } catch (error) { this.isConnecting = false; this.cleanup(); this.logger.error("FTP connection failed:", error); throw error; } } async disconnect() { if (!this.ftpClient) return; this.logger.log("Disconnecting FTP"); try { this.ftpClient.close(); } catch (error) { this.logger.error("Error closing FTP:", error); } finally { this.cleanup(); } } async listFiles(dirPath = "/") { this.ensureConnected(); try { this.logger.log(`Connecting ftp ${dirPath}`); const files = await this.ftpClient.list(dirPath); this.logger.debug(`Listed ${files.length} files in ${dirPath}`); return files; } catch (error) { this.logger.error(`Failed to list files in ${dirPath}: ${errorSummary(error)}`); throw error; } } getFileStoragePath(filename) { const storagePath = join(getMediaPath(), AppConstants.defaultFileUploadsStorage); if (!existsSync(storagePath)) mkdirSync(storagePath, { recursive: true }); return join(storagePath, filename); } async uploadFile(stream, remotePath, progressToken) { this.ensureConnected(); try { if (progressToken) this.ftpClient.trackProgress((info) => { this.eventEmitter2.emit(`${uploadProgressEvent(progressToken)}`, progressToken, { loaded: info.bytes, total: info.bytesOverall }); }); this.logger.log(`Uploading stream to ${remotePath}`); await this.ftpClient.uploadFrom(stream, remotePath); this.ftpClient.trackProgress(); if (progressToken) this.eventEmitter2.emit(`${uploadDoneEvent(progressToken)}`, progressToken); this.logger.log(`File uploaded successfully: ${remotePath}`); } catch (error) { if (progressToken) this.eventEmitter2.emit(`${uploadFailedEvent(progressToken)}`, progressToken, error?.message); this.logger.error(`Upload failed for ${remotePath}:`, error); throw error; } } async downloadFile(remotePath, localPath) { this.ensureConnected(); try { this.logger.log(`Downloading ${remotePath} to ${localPath}`); await this.ftpClient.downloadTo(localPath, remotePath); this.logger.log(`File downloaded successfully: ${remotePath}`); } catch (error) { this.logger.error(`Download failed for ${remotePath}:`, error); throw error; } } async downloadFileAsStream(remotePath) { this.ensureConnected(); const filename = remotePath.split("/").pop() || "download"; const tempPath = this.getFileStoragePath(`bambu-download-${Date.now()}-${filename}`); try { this.logger.log(`Downloading ${remotePath} to temp file for streaming`); await this.ftpClient.downloadTo(tempPath, remotePath); this.logger.log(`File downloaded successfully: ${remotePath}`); const stream = createReadStream(tempPath); const cleanup = () => { try { unlinkSync(tempPath); this.logger.debug(`Cleaned up temp file: ${tempPath}`); } catch (cleanupError) { this.logger.warn(`Failed to cleanup temp file ${tempPath}:`, cleanupError); } }; stream.on("close", cleanup); stream.on("error", cleanup); return { stream, tempPath, cleanup }; } catch (error) { try { unlinkSync(tempPath); } catch {} this.logger.error(`Download failed for ${remotePath}:`, error); throw error; } } async deleteFile(remotePath) { this.ensureConnected(); try { this.logger.log(`Deleting file: ${remotePath}`); await this.ftpClient.remove(remotePath); this.logger.log(`File deleted successfully: ${remotePath}`); } catch (error) { this.logger.error(`Delete failed for ${remotePath}:`, error); throw error; } } get isConnected() { return this.ftpClient != null && !this.ftpClient.closed; } ensureConnected() { if (!this.isConnected) throw new Error("FTP not connected. Call connect() first."); } cleanup() { this.ftpClient = null; } sanitizeHost(host) { if (!host?.length) throw new Error("Host must be a non-empty string"); const trimmed = host.trim(); if (trimmed.length === 0) throw new Error("Host cannot be empty"); const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; if (!ipv4Pattern.test(trimmed) && !/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(trimmed)) throw new Error("Invalid host format. Must be a valid IP address or hostname"); if (ipv4Pattern.test(trimmed)) { if (trimmed.split(".").map(Number).some((part) => part < 0 || part > 255)) throw new Error("Invalid IPv4 address. Octets must be between 0 and 255"); } return trimmed; } /** * Sanitize and validate access code input */ sanitizeAccessCode(accessCode) { if (!accessCode?.length) throw new Error("Access code must be a non-empty string"); const trimmed = accessCode.trim(); if (trimmed.length === 0) throw new Error("Access code cannot be empty"); if (trimmed.length < 4 || trimmed.length > 32) throw new Error("Access code must be between 4 and 32 characters"); if (!/^[a-zA-Z0-9]+$/.test(trimmed)) throw new Error("Access code must contain only alphanumeric characters"); return trimmed; } }; //#endregion export { BambuFtpAdapter }; //# sourceMappingURL=bambu-ftp.adapter.js.map