@fdm-monster/server 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/.yarn/releases/{yarn-4.13.0.cjs → yarn-4.14.1.cjs} +288 -288
  3. package/.yarnrc.yml +5 -1
  4. package/RELEASE_NOTES.MD +14 -0
  5. package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.129.0}/helpers/decorate.js +1 -1
  6. package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.129.0}/helpers/decorateMetadata.js +1 -1
  7. package/dist/consoles/typeorm-create.js.map +1 -1
  8. package/dist/consoles/typeorm-generate.js.map +1 -1
  9. package/dist/consoles/typeorm-migrate.js.map +1 -1
  10. package/dist/constants/authorization.constants.js.map +1 -1
  11. package/dist/container.js.map +1 -1
  12. package/dist/controllers/api-key.controller.js +2 -2
  13. package/dist/controllers/api-key.controller.js.map +1 -1
  14. package/dist/controllers/auth.controller.js +5 -3
  15. package/dist/controllers/auth.controller.js.map +1 -1
  16. package/dist/controllers/batch-call.controller.js +2 -2
  17. package/dist/controllers/batch-call.controller.js.map +1 -1
  18. package/dist/controllers/camera-stream.controller.js +2 -2
  19. package/dist/controllers/camera-stream.controller.js.map +1 -1
  20. package/dist/controllers/file-storage.controller.js +2 -2
  21. package/dist/controllers/file-storage.controller.js.map +1 -1
  22. package/dist/controllers/first-time-setup.controller.js +2 -2
  23. package/dist/controllers/first-time-setup.controller.js.map +1 -1
  24. package/dist/controllers/floor.controller.js +2 -2
  25. package/dist/controllers/floor.controller.js.map +1 -1
  26. package/dist/controllers/metrics.controller.js +2 -2
  27. package/dist/controllers/metrics.controller.js.map +1 -1
  28. package/dist/controllers/print-job.controller.js +2 -2
  29. package/dist/controllers/print-job.controller.js.map +1 -1
  30. package/dist/controllers/print-queue.controller.js +2 -2
  31. package/dist/controllers/print-queue.controller.js.map +1 -1
  32. package/dist/controllers/printer-files.controller.js +2 -2
  33. package/dist/controllers/printer-files.controller.js.map +1 -1
  34. package/dist/controllers/printer-maintenance-log.controller.js +2 -2
  35. package/dist/controllers/printer-maintenance-log.controller.js.map +1 -1
  36. package/dist/controllers/printer-settings.controller.js +2 -2
  37. package/dist/controllers/printer-settings.controller.js.map +1 -1
  38. package/dist/controllers/printer-tag.controller.js +2 -2
  39. package/dist/controllers/printer-tag.controller.js.map +1 -1
  40. package/dist/controllers/printer.controller.js +2 -2
  41. package/dist/controllers/printer.controller.js.map +1 -1
  42. package/dist/controllers/server-private.controller.js +2 -2
  43. package/dist/controllers/server-private.controller.js.map +1 -1
  44. package/dist/controllers/server-public.controller.js +2 -2
  45. package/dist/controllers/server-public.controller.js.map +1 -1
  46. package/dist/controllers/settings.controller.js +2 -2
  47. package/dist/controllers/settings.controller.js.map +1 -1
  48. package/dist/controllers/slicer-compat.controller.js +2 -2
  49. package/dist/controllers/slicer-compat.controller.js.map +1 -1
  50. package/dist/controllers/user.controller.js +2 -2
  51. package/dist/controllers/user.controller.js.map +1 -1
  52. package/dist/entities/api-key.entity.js +2 -2
  53. package/dist/entities/camera-stream.entity.js +2 -2
  54. package/dist/entities/floor-position.entity.js +2 -2
  55. package/dist/entities/floor.entity.js +2 -2
  56. package/dist/entities/print-job.entity.js +2 -2
  57. package/dist/entities/printer-maintenance-log.entity.js +2 -2
  58. package/dist/entities/printer-tag.entity.js +2 -2
  59. package/dist/entities/printer.entity.js +2 -2
  60. package/dist/entities/refresh-token.entity.js +2 -2
  61. package/dist/entities/role.entity.js +2 -2
  62. package/dist/entities/settings.entity.js +2 -2
  63. package/dist/entities/tag.entity.js +2 -2
  64. package/dist/entities/user-role.entity.js +2 -2
  65. package/dist/entities/user.entity.js +2 -2
  66. package/dist/exceptions/failed-dependency.exception.js.map +1 -1
  67. package/dist/exceptions/job.exceptions.js.map +1 -1
  68. package/dist/exceptions/runtime.exceptions.js.map +1 -1
  69. package/dist/handlers/event-emitter.js.map +1 -1
  70. package/dist/handlers/logger-factory.js.map +1 -1
  71. package/dist/handlers/logger.js.map +1 -1
  72. package/dist/handlers/logging/file-logging.transport.js +1 -2
  73. package/dist/handlers/logging/file-logging.transport.js.map +1 -1
  74. package/dist/handlers/logging/loki-logging.transport.js.map +1 -1
  75. package/dist/handlers/logging/static.logger.js.map +1 -1
  76. package/dist/handlers/validators.js.map +1 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/middleware/api-key.strategy.js.map +1 -1
  79. package/dist/middleware/authenticate.js.map +1 -1
  80. package/dist/middleware/database.js.map +1 -1
  81. package/dist/middleware/demo.middleware.js.map +1 -1
  82. package/dist/middleware/exception.filter.js.map +1 -1
  83. package/dist/middleware/global.middleware.js.map +1 -1
  84. package/dist/middleware/param-converter.middleware.js.map +1 -1
  85. package/dist/middleware/passport.js.map +1 -1
  86. package/dist/middleware/printer-resolver.js.map +1 -1
  87. package/dist/middleware/printer.js.map +1 -1
  88. package/dist/middleware/slicer-api-key.middleware.js.map +1 -1
  89. package/dist/middleware/socketio.middleware.js.map +1 -1
  90. package/dist/migrations/1706829146617-InitSqlite.js.map +1 -1
  91. package/dist/migrations/1707494762198-PrinterGroup.js.map +1 -1
  92. package/dist/migrations/1708465930665-ChangePrintCompletionDeletePrinterCascade.js.map +1 -1
  93. package/dist/migrations/1713300747465-ChangeRoleNameUnique.js.map +1 -1
  94. package/dist/migrations/1713897879622-AddPrinterType.js.map +1 -1
  95. package/dist/migrations/1720338804844-RemovePrinterFile.js.map +1 -1
  96. package/dist/migrations/1745141688926-AddPrinterUsernamePassword.js.map +1 -1
  97. package/dist/migrations/1766576698569-DropPermissions.js.map +1 -1
  98. package/dist/migrations/1767278216516-ChangeCameraPrinterOnDeleteSetNull.js.map +1 -1
  99. package/dist/migrations/1767279607392-DropCustomGcode.js.map +1 -1
  100. package/dist/migrations/1767291804417-DropPrintCompletions.js.map +1 -1
  101. package/dist/migrations/1767352862576-DropSettingsFileClean.js.map +1 -1
  102. package/dist/migrations/1767355639023-ChangeFloorLevelToOrder.js.map +1 -1
  103. package/dist/migrations/1767370191762-ChangeFloorNonUniqueOrder.js.map +1 -1
  104. package/dist/migrations/1767432108916-RenameGroupToTag.js.map +1 -1
  105. package/dist/migrations/1767451444137-AddPrintJob.js.map +1 -1
  106. package/dist/migrations/1767909428129-AddPrinterMaintenanceLog.js.map +1 -1
  107. package/dist/migrations/1778446203015-AddApiKey.js.map +1 -1
  108. package/dist/plugins/controllers-plugin.js.map +1 -1
  109. package/dist/server.constants.js +2 -1
  110. package/dist/server.constants.js.map +1 -1
  111. package/dist/server.core.js.map +1 -1
  112. package/dist/server.env.js.map +1 -1
  113. package/dist/server.host.js.map +1 -1
  114. package/dist/services/authentication/auth.service.js.map +1 -1
  115. package/dist/services/authentication/jwt.service.js.map +1 -1
  116. package/dist/services/bambu/bambu-ftp.adapter.js.map +1 -1
  117. package/dist/services/bambu/bambu-mqtt.adapter.js.map +1 -1
  118. package/dist/services/bambu/bambu.client.js.map +1 -1
  119. package/dist/services/bambu.api.js.map +1 -1
  120. package/dist/services/core/batch-call.service.js.map +1 -1
  121. package/dist/services/core/client-bundle.service.js.map +1 -1
  122. package/dist/services/core/config.service.js +4 -0
  123. package/dist/services/core/config.service.js.map +1 -1
  124. package/dist/services/core/cradle.service.js.map +1 -1
  125. package/dist/services/core/github.service.js.map +1 -1
  126. package/dist/services/core/http-client.factory.js.map +1 -1
  127. package/dist/services/core/logs-manager.service.js.map +1 -1
  128. package/dist/services/core/monsterpi.service.js.map +1 -1
  129. package/dist/services/core/multer.service.js.map +1 -1
  130. package/dist/services/core/server-release.service.js.map +1 -1
  131. package/dist/services/core/yaml.service.js.map +1 -1
  132. package/dist/services/file-analysis.service.js.map +1 -1
  133. package/dist/services/file-storage.service.js.map +1 -1
  134. package/dist/services/moonraker/moonraker-websocket.adapter.js.map +1 -1
  135. package/dist/services/moonraker/moonraker.client.js.map +1 -1
  136. package/dist/services/moonraker.api.js.map +1 -1
  137. package/dist/services/octoprint/octoprint-api.routes.js.map +1 -1
  138. package/dist/services/octoprint/octoprint-websocket.adapter.js.map +1 -1
  139. package/dist/services/octoprint/octoprint.client.js.map +1 -1
  140. package/dist/services/octoprint/utils/api.utils.js.map +1 -1
  141. package/dist/services/octoprint/utils/file.utils.js.map +1 -1
  142. package/dist/services/octoprint/utils/octoprint-http-client.builder.js.map +1 -1
  143. package/dist/services/octoprint.api.js.map +1 -1
  144. package/dist/services/orm/api-key.service.js.map +1 -1
  145. package/dist/services/orm/base.service.js.map +1 -1
  146. package/dist/services/orm/camera-stream.service.js.map +1 -1
  147. package/dist/services/orm/floor-position.service.js.map +1 -1
  148. package/dist/services/orm/floor.service.js.map +1 -1
  149. package/dist/services/orm/permission.service.js.map +1 -1
  150. package/dist/services/orm/print-job.service.js.map +1 -1
  151. package/dist/services/orm/printer-maintenance-log.service.js.map +1 -1
  152. package/dist/services/orm/printer-tag.service.js.map +1 -1
  153. package/dist/services/orm/printer.service.js.map +1 -1
  154. package/dist/services/orm/refresh-token.service.js.map +1 -1
  155. package/dist/services/orm/role.service.js.map +1 -1
  156. package/dist/services/orm/settings.service.js.map +1 -1
  157. package/dist/services/orm/user-role.service.js.map +1 -1
  158. package/dist/services/orm/user.service.js.map +1 -1
  159. package/dist/services/print-file-downloader.service.js.map +1 -1
  160. package/dist/services/print-queue.service.js.map +1 -1
  161. package/dist/services/printer-api.factory.js.map +1 -1
  162. package/dist/services/printer-api.interface.js.map +1 -1
  163. package/dist/services/prusa-link/prusa-link-http-polling.adapter.js.map +1 -1
  164. package/dist/services/prusa-link/prusa-link.api.js.map +1 -1
  165. package/dist/services/prusa-link/utils/digest-auth.util.js +19 -12
  166. package/dist/services/prusa-link/utils/digest-auth.util.js.map +1 -1
  167. package/dist/services/prusa-link/utils/prusa-link-http-client.builder.js +45 -11
  168. package/dist/services/prusa-link/utils/prusa-link-http-client.builder.js.map +1 -1
  169. package/dist/services/socket.factory.js.map +1 -1
  170. package/dist/services/task-manager.service.js.map +1 -1
  171. package/dist/services/typeorm/typeorm.service.js.map +1 -1
  172. package/dist/services/validators/printer-service.validation.js.map +1 -1
  173. package/dist/shared/default-http-client.builder.js.map +1 -1
  174. package/dist/shared/load-controllers.js.map +1 -1
  175. package/dist/shared/runtime-settings.migration.js.map +1 -1
  176. package/dist/shared/websocket-rpc-extended.adapter.js.map +1 -1
  177. package/dist/shared/websocket.adapter.js.map +1 -1
  178. package/dist/state/file-upload-tracker.cache.js.map +1 -1
  179. package/dist/state/floor.store.js.map +1 -1
  180. package/dist/state/printer-events.cache.js.map +1 -1
  181. package/dist/state/printer-socket.store.js.map +1 -1
  182. package/dist/state/printer-thumbnail.cache.js.map +1 -1
  183. package/dist/state/printer.cache.js.map +1 -1
  184. package/dist/state/settings.store.js.map +1 -1
  185. package/dist/state/socket-io.gateway.js.map +1 -1
  186. package/dist/state/test-printer-socket.store.js.map +1 -1
  187. package/dist/tasks/boot.task.js.map +1 -1
  188. package/dist/tasks/client-bundle.task.js.map +1 -1
  189. package/dist/tasks/print-job-analysis.task.js.map +1 -1
  190. package/dist/tasks/printer-websocket-restore.task.js.map +1 -1
  191. package/dist/tasks/printer-websocket.task.js.map +1 -1
  192. package/dist/tasks/socketio.task.js.map +1 -1
  193. package/dist/tasks/software-update.task.js.map +1 -1
  194. package/dist/tasks.js.map +1 -1
  195. package/dist/utils/array.util.js.map +1 -1
  196. package/dist/utils/bgcode/bgcode-thumbnail.parser.js.map +1 -1
  197. package/dist/utils/bgcode/bgcode.utils.js.map +1 -1
  198. package/dist/utils/bgcode/heatshrink-decoder.js.map +1 -1
  199. package/dist/utils/bgcode/png-encoder.js.map +1 -1
  200. package/dist/utils/bgcode/qoi-decoder.js.map +1 -1
  201. package/dist/utils/cache/key-diff.cache.js.map +1 -1
  202. package/dist/utils/correlation-token.util.js.map +1 -1
  203. package/dist/utils/crypto.utils.js.map +1 -1
  204. package/dist/utils/env.utils.js.map +1 -1
  205. package/dist/utils/error.utils.js.map +1 -1
  206. package/dist/utils/fs.utils.js.map +1 -1
  207. package/dist/utils/gcode.utils.js.map +1 -1
  208. package/dist/utils/image-dimensions.js.map +1 -1
  209. package/dist/utils/job-stats.util.js.map +1 -1
  210. package/dist/utils/normalize-url.js.map +1 -1
  211. package/dist/utils/parsers/3mf.parser.js.map +1 -1
  212. package/dist/utils/parsers/bgcode.parser.js.map +1 -1
  213. package/dist/utils/parsers/gcode.parser.js.map +1 -1
  214. package/dist/utils/pretty-print.utils.js.map +1 -1
  215. package/dist/utils/semver.utils.js.map +1 -1
  216. package/dist/utils/swagger/decorators.js.map +1 -1
  217. package/dist/utils/swagger/generator.js.map +1 -1
  218. package/dist/utils/swagger/swagger.js.map +1 -1
  219. package/dist/utils/thumbnail.util.js.map +1 -1
  220. package/dist/utils/time.utils.js.map +1 -1
  221. package/dist/utils/url.utils.js.map +1 -1
  222. package/package.json +10 -7
  223. package/packages/consoles/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"bambu-ftp.adapter.js","names":[],"sources":["../../../src/services/bambu/bambu-ftp.adapter.ts"],"sourcesContent":["import EventEmitter2 from \"eventemitter2\";\nimport { SettingsStore } from \"@/state/settings.store\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { Client, FileInfo } from \"basic-ftp\";\nimport { uploadDoneEvent, uploadFailedEvent, uploadProgressEvent } from \"@/constants/event.constants\";\nimport { unlinkSync, createReadStream, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { AppConstants } from \"@/server.constants\";\nimport { getMediaPath } from \"@/utils/fs.utils\";\nimport { errorSummary } from \"@/utils/error.utils\";\n\nexport class BambuFtpAdapter {\n protected readonly logger: LoggerService;\n\n private ftpClient: Client | null = null;\n private host: string | null = null;\n private accessCode: string | null = null;\n private isConnecting = false;\n\n constructor(\n private readonly settingsStore: SettingsStore,\n loggerFactory: ILoggerFactory,\n private readonly eventEmitter2: EventEmitter2,\n ) {\n this.logger = loggerFactory(BambuFtpAdapter.name);\n }\n\n /**\n * Connect to FTP server\n */\n async connect(host: string, accessCode: string): Promise<void> {\n if (this.ftpClient && !this.ftpClient.closed) {\n this.logger.debug(\"FTP already connected\");\n return;\n }\n\n if (this.isConnecting) {\n throw new Error(\"Connection already in progress\");\n }\n\n // Validate and sanitize inputs\n const sanitizedHost = this.sanitizeHost(host);\n const sanitizedAccessCode = this.sanitizeAccessCode(accessCode);\n\n this.host = sanitizedHost;\n this.accessCode = sanitizedAccessCode;\n this.isConnecting = true;\n\n const timeout = this.settingsStore.getTimeoutSettings().apiTimeout;\n\n this.logger.log(`Connecting to Bambu FTP at ${sanitizedHost}:990`);\n\n try {\n this.ftpClient = new Client(timeout);\n\n // Enable logging if in debug mode\n this.ftpClient.ftp.verbose = false;\n\n await this.ftpClient.access({\n host: sanitizedHost,\n port: 990,\n user: \"bblp\",\n password: sanitizedAccessCode,\n secure: \"implicit\",\n secureOptions: {\n checkServerIdentity: () => {\n return undefined;\n },\n rejectUnauthorized: false,\n },\n });\n\n this.isConnecting = false;\n this.logger.log(\"FTP connected successfully\");\n } catch (error) {\n this.isConnecting = false;\n this.cleanup();\n this.logger.error(\"FTP connection failed:\", error);\n throw error;\n }\n }\n\n async disconnect(): Promise<void> {\n if (!this.ftpClient) {\n return;\n }\n\n this.logger.log(\"Disconnecting FTP\");\n\n try {\n this.ftpClient.close();\n } catch (error) {\n this.logger.error(\"Error closing FTP:\", error);\n } finally {\n this.cleanup();\n }\n }\n\n async listFiles(dirPath: string = \"/\"): Promise<FileInfo[]> {\n this.ensureConnected();\n\n try {\n this.logger.log(`Connecting ftp ${dirPath}`);\n const files = await this.ftpClient!.list(dirPath);\n this.logger.debug(`Listed ${files.length} files in ${dirPath}`);\n return files;\n } catch (error) {\n this.logger.error(`Failed to list files in ${dirPath}: ${errorSummary(error)}`);\n throw error;\n }\n }\n\n private getFileStoragePath(filename: string): string {\n const storagePath = join(getMediaPath(), AppConstants.defaultFileUploadsStorage);\n\n if (!existsSync(storagePath)) {\n mkdirSync(storagePath, { recursive: true });\n }\n\n return join(storagePath, filename);\n }\n\n async uploadFile(stream: Readable, remotePath: string, progressToken?: string): Promise<void> {\n this.ensureConnected();\n\n try {\n if (progressToken) {\n this.ftpClient!.trackProgress((info) => {\n this.eventEmitter2.emit(`${uploadProgressEvent(progressToken)}`, progressToken, {\n loaded: info.bytes,\n total: info.bytesOverall,\n });\n });\n }\n\n this.logger.log(`Uploading stream to ${remotePath}`);\n await this.ftpClient!.uploadFrom(stream, remotePath);\n\n this.ftpClient!.trackProgress();\n\n if (progressToken) {\n this.eventEmitter2.emit(`${uploadDoneEvent(progressToken)}`, progressToken);\n }\n\n this.logger.log(`File uploaded successfully: ${remotePath}`);\n } catch (error) {\n if (progressToken) {\n this.eventEmitter2.emit(`${uploadFailedEvent(progressToken)}`, progressToken, (error as Error)?.message);\n }\n this.logger.error(`Upload failed for ${remotePath}:`, error);\n throw error;\n }\n }\n\n async downloadFile(remotePath: string, localPath: string): Promise<void> {\n this.ensureConnected();\n\n try {\n this.logger.log(`Downloading ${remotePath} to ${localPath}`);\n await this.ftpClient!.downloadTo(localPath, remotePath);\n this.logger.log(`File downloaded successfully: ${remotePath}`);\n } catch (error) {\n this.logger.error(`Download failed for ${remotePath}:`, error);\n throw error;\n }\n }\n\n async downloadFileAsStream(remotePath: string): Promise<{ stream: Readable; tempPath: string; cleanup: () => void }> {\n this.ensureConnected();\n\n const filename = remotePath.split(\"/\").pop() || \"download\";\n const tempPath = this.getFileStoragePath(`bambu-download-${Date.now()}-${filename}`);\n\n try {\n this.logger.log(`Downloading ${remotePath} to temp file for streaming`);\n await this.ftpClient!.downloadTo(tempPath, remotePath);\n this.logger.log(`File downloaded successfully: ${remotePath}`);\n\n const stream = createReadStream(tempPath);\n\n const cleanup = () => {\n try {\n unlinkSync(tempPath);\n this.logger.debug(`Cleaned up temp file: ${tempPath}`);\n } catch (cleanupError) {\n this.logger.warn(`Failed to cleanup temp file ${tempPath}:`, cleanupError);\n }\n };\n\n stream.on(\"close\", cleanup);\n stream.on(\"error\", cleanup);\n\n return { stream, tempPath, cleanup };\n } catch (error) {\n try {\n unlinkSync(tempPath);\n } catch {}\n this.logger.error(`Download failed for ${remotePath}:`, error);\n throw error;\n }\n }\n\n async deleteFile(remotePath: string): Promise<void> {\n this.ensureConnected();\n\n try {\n this.logger.log(`Deleting file: ${remotePath}`);\n await this.ftpClient!.remove(remotePath);\n this.logger.log(`File deleted successfully: ${remotePath}`);\n } catch (error) {\n this.logger.error(`Delete failed for ${remotePath}:`, error);\n throw error;\n }\n }\n\n get isConnected(): boolean {\n return this.ftpClient != null && !this.ftpClient.closed;\n }\n\n private ensureConnected(): void {\n if (!this.isConnected) {\n throw new Error(\"FTP not connected. Call connect() first.\");\n }\n }\n\n private cleanup(): void {\n this.ftpClient = null;\n }\n\n private sanitizeHost(host: string): string {\n if (!host?.length) {\n throw new Error(\"Host must be a non-empty string\");\n }\n\n const trimmed = host.trim();\n if (trimmed.length === 0) {\n throw new Error(\"Host cannot be empty\");\n }\n\n // Validate format: IP address or hostname\n const ipv4Pattern = /^(\\d{1,3}\\.){3}\\d{1,3}$/;\n const hostnamePattern =\n /^[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])?)*$/;\n\n if (!ipv4Pattern.test(trimmed) && !hostnamePattern.test(trimmed)) {\n throw new Error(\"Invalid host format. Must be a valid IP address or hostname\");\n }\n\n // Additional validation for IPv4 address ranges\n if (ipv4Pattern.test(trimmed)) {\n const parts = trimmed.split(\".\").map(Number);\n if (parts.some((part) => part < 0 || part > 255)) {\n throw new Error(\"Invalid IPv4 address. Octets must be between 0 and 255\");\n }\n }\n\n return trimmed;\n }\n\n /**\n * Sanitize and validate access code input\n */\n private sanitizeAccessCode(accessCode: string): string {\n if (!accessCode?.length) {\n throw new Error(\"Access code must be a non-empty string\");\n }\n\n // Trim whitespace\n const trimmed = accessCode.trim();\n\n if (trimmed.length === 0) {\n throw new Error(\"Access code cannot be empty\");\n }\n\n // Bambu Lab access codes are typically 8 characters\n if (trimmed.length < 4 || trimmed.length > 32) {\n throw new Error(\"Access code must be between 4 and 32 characters\");\n }\n\n // Ensure only alphanumeric characters (no special characters that could cause injection)\n const alphanumericPattern = /^[a-zA-Z0-9]+$/;\n if (!alphanumericPattern.test(trimmed)) {\n throw new Error(\"Access code must contain only alphanumeric characters\");\n }\n\n return trimmed;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bambu-ftp.adapter.js","names":[],"sources":["../../../src/services/bambu/bambu-ftp.adapter.ts"],"sourcesContent":["import EventEmitter2 from \"eventemitter2\";\nimport { SettingsStore } from \"@/state/settings.store\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { Client, FileInfo } from \"basic-ftp\";\nimport { uploadDoneEvent, uploadFailedEvent, uploadProgressEvent } from \"@/constants/event.constants\";\nimport { unlinkSync, createReadStream, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { AppConstants } from \"@/server.constants\";\nimport { getMediaPath } from \"@/utils/fs.utils\";\nimport { errorSummary } from \"@/utils/error.utils\";\n\nexport class BambuFtpAdapter {\n protected readonly logger: LoggerService;\n\n private ftpClient: Client | null = null;\n private host: string | null = null;\n private accessCode: string | null = null;\n private isConnecting = false;\n\n constructor(\n private readonly settingsStore: SettingsStore,\n loggerFactory: ILoggerFactory,\n private readonly eventEmitter2: EventEmitter2,\n ) {\n this.logger = loggerFactory(BambuFtpAdapter.name);\n }\n\n /**\n * Connect to FTP server\n */\n async connect(host: string, accessCode: string): Promise<void> {\n if (this.ftpClient && !this.ftpClient.closed) {\n this.logger.debug(\"FTP already connected\");\n return;\n }\n\n if (this.isConnecting) {\n throw new Error(\"Connection already in progress\");\n }\n\n // Validate and sanitize inputs\n const sanitizedHost = this.sanitizeHost(host);\n const sanitizedAccessCode = this.sanitizeAccessCode(accessCode);\n\n this.host = sanitizedHost;\n this.accessCode = sanitizedAccessCode;\n this.isConnecting = true;\n\n const timeout = this.settingsStore.getTimeoutSettings().apiTimeout;\n\n this.logger.log(`Connecting to Bambu FTP at ${sanitizedHost}:990`);\n\n try {\n this.ftpClient = new Client(timeout);\n\n // Enable logging if in debug mode\n this.ftpClient.ftp.verbose = false;\n\n await this.ftpClient.access({\n host: sanitizedHost,\n port: 990,\n user: \"bblp\",\n password: sanitizedAccessCode,\n secure: \"implicit\",\n secureOptions: {\n checkServerIdentity: () => {\n return undefined;\n },\n rejectUnauthorized: false,\n },\n });\n\n this.isConnecting = false;\n this.logger.log(\"FTP connected successfully\");\n } catch (error) {\n this.isConnecting = false;\n this.cleanup();\n this.logger.error(\"FTP connection failed:\", error);\n throw error;\n }\n }\n\n async disconnect(): Promise<void> {\n if (!this.ftpClient) {\n return;\n }\n\n this.logger.log(\"Disconnecting FTP\");\n\n try {\n this.ftpClient.close();\n } catch (error) {\n this.logger.error(\"Error closing FTP:\", error);\n } finally {\n this.cleanup();\n }\n }\n\n async listFiles(dirPath: string = \"/\"): Promise<FileInfo[]> {\n this.ensureConnected();\n\n try {\n this.logger.log(`Connecting ftp ${dirPath}`);\n const files = await this.ftpClient!.list(dirPath);\n this.logger.debug(`Listed ${files.length} files in ${dirPath}`);\n return files;\n } catch (error) {\n this.logger.error(`Failed to list files in ${dirPath}: ${errorSummary(error)}`);\n throw error;\n }\n }\n\n private getFileStoragePath(filename: string): string {\n const storagePath = join(getMediaPath(), AppConstants.defaultFileUploadsStorage);\n\n if (!existsSync(storagePath)) {\n mkdirSync(storagePath, { recursive: true });\n }\n\n return join(storagePath, filename);\n }\n\n async uploadFile(stream: Readable, remotePath: string, progressToken?: string): Promise<void> {\n this.ensureConnected();\n\n try {\n if (progressToken) {\n this.ftpClient!.trackProgress((info) => {\n this.eventEmitter2.emit(`${uploadProgressEvent(progressToken)}`, progressToken, {\n loaded: info.bytes,\n total: info.bytesOverall,\n });\n });\n }\n\n this.logger.log(`Uploading stream to ${remotePath}`);\n await this.ftpClient!.uploadFrom(stream, remotePath);\n\n this.ftpClient!.trackProgress();\n\n if (progressToken) {\n this.eventEmitter2.emit(`${uploadDoneEvent(progressToken)}`, progressToken);\n }\n\n this.logger.log(`File uploaded successfully: ${remotePath}`);\n } catch (error) {\n if (progressToken) {\n this.eventEmitter2.emit(`${uploadFailedEvent(progressToken)}`, progressToken, (error as Error)?.message);\n }\n this.logger.error(`Upload failed for ${remotePath}:`, error);\n throw error;\n }\n }\n\n async downloadFile(remotePath: string, localPath: string): Promise<void> {\n this.ensureConnected();\n\n try {\n this.logger.log(`Downloading ${remotePath} to ${localPath}`);\n await this.ftpClient!.downloadTo(localPath, remotePath);\n this.logger.log(`File downloaded successfully: ${remotePath}`);\n } catch (error) {\n this.logger.error(`Download failed for ${remotePath}:`, error);\n throw error;\n }\n }\n\n async downloadFileAsStream(remotePath: string): Promise<{ stream: Readable; tempPath: string; cleanup: () => void }> {\n this.ensureConnected();\n\n const filename = remotePath.split(\"/\").pop() || \"download\";\n const tempPath = this.getFileStoragePath(`bambu-download-${Date.now()}-${filename}`);\n\n try {\n this.logger.log(`Downloading ${remotePath} to temp file for streaming`);\n await this.ftpClient!.downloadTo(tempPath, remotePath);\n this.logger.log(`File downloaded successfully: ${remotePath}`);\n\n const stream = createReadStream(tempPath);\n\n const cleanup = () => {\n try {\n unlinkSync(tempPath);\n this.logger.debug(`Cleaned up temp file: ${tempPath}`);\n } catch (cleanupError) {\n this.logger.warn(`Failed to cleanup temp file ${tempPath}:`, cleanupError);\n }\n };\n\n stream.on(\"close\", cleanup);\n stream.on(\"error\", cleanup);\n\n return { stream, tempPath, cleanup };\n } catch (error) {\n try {\n unlinkSync(tempPath);\n } catch {}\n this.logger.error(`Download failed for ${remotePath}:`, error);\n throw error;\n }\n }\n\n async deleteFile(remotePath: string): Promise<void> {\n this.ensureConnected();\n\n try {\n this.logger.log(`Deleting file: ${remotePath}`);\n await this.ftpClient!.remove(remotePath);\n this.logger.log(`File deleted successfully: ${remotePath}`);\n } catch (error) {\n this.logger.error(`Delete failed for ${remotePath}:`, error);\n throw error;\n }\n }\n\n get isConnected(): boolean {\n return this.ftpClient != null && !this.ftpClient.closed;\n }\n\n private ensureConnected(): void {\n if (!this.isConnected) {\n throw new Error(\"FTP not connected. Call connect() first.\");\n }\n }\n\n private cleanup(): void {\n this.ftpClient = null;\n }\n\n private sanitizeHost(host: string): string {\n if (!host?.length) {\n throw new Error(\"Host must be a non-empty string\");\n }\n\n const trimmed = host.trim();\n if (trimmed.length === 0) {\n throw new Error(\"Host cannot be empty\");\n }\n\n // Validate format: IP address or hostname\n const ipv4Pattern = /^(\\d{1,3}\\.){3}\\d{1,3}$/;\n const hostnamePattern =\n /^[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])?)*$/;\n\n if (!ipv4Pattern.test(trimmed) && !hostnamePattern.test(trimmed)) {\n throw new Error(\"Invalid host format. Must be a valid IP address or hostname\");\n }\n\n // Additional validation for IPv4 address ranges\n if (ipv4Pattern.test(trimmed)) {\n const parts = trimmed.split(\".\").map(Number);\n if (parts.some((part) => part < 0 || part > 255)) {\n throw new Error(\"Invalid IPv4 address. Octets must be between 0 and 255\");\n }\n }\n\n return trimmed;\n }\n\n /**\n * Sanitize and validate access code input\n */\n private sanitizeAccessCode(accessCode: string): string {\n if (!accessCode?.length) {\n throw new Error(\"Access code must be a non-empty string\");\n }\n\n // Trim whitespace\n const trimmed = accessCode.trim();\n\n if (trimmed.length === 0) {\n throw new Error(\"Access code cannot be empty\");\n }\n\n // Bambu Lab access codes are typically 8 characters\n if (trimmed.length < 4 || trimmed.length > 32) {\n throw new Error(\"Access code must be between 4 and 32 characters\");\n }\n\n // Ensure only alphanumeric characters (no special characters that could cause injection)\n const alphanumericPattern = /^[a-zA-Z0-9]+$/;\n if (!alphanumericPattern.test(trimmed)) {\n throw new Error(\"Access code must contain only alphanumeric characters\");\n }\n\n return trimmed;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"bambu-mqtt.adapter.js","names":[],"sources":["../../../src/services/bambu/bambu-mqtt.adapter.ts"],"sourcesContent":["import type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport EventEmitter2 from \"eventemitter2\";\nimport { SettingsStore } from \"@/state/settings.store\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { PrintData } from \"@/services/bambu/mqtt-message.types\";\nimport mqtt from \"mqtt\";\nimport type { IWebsocketAdapter } from \"@/services/websocket-adapter.interface\";\nimport type { ISocketLogin } from \"@/shared/dtos/socket-login.dto\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport { SOCKET_STATE, SocketState } from \"@/shared/dtos/socket-state.type\";\nimport { API_STATE, ApiState } from \"@/shared/dtos/api-state.type\";\nimport { BambuType } from \"@/services/printer-api.interface\";\nimport { WsMessage } from \"@/services/octoprint/octoprint-websocket.adapter\";\n\nexport const bambuEvent = (event: string) => `bambu.${event}`;\n\nexport interface BambuEventDto {\n event: string;\n payload: any;\n printerId?: number;\n printerType: typeof BambuType;\n}\n\nexport class BambuMqttAdapter implements IWebsocketAdapter {\n protected readonly logger: LoggerService;\n private readonly settingsStore: SettingsStore;\n private readonly eventEmitter2: EventEmitter2;\n\n public readonly printerType = BambuType;\n public printerId?: number;\n public socketState: SocketState = SOCKET_STATE.unopened;\n public apiState: ApiState = API_STATE.unset;\n public login: LoginDto;\n public lastMessageReceivedTimestamp: null | number = null;\n\n private mqttClient: mqtt.MqttClient | null = null;\n private host: string | null = null;\n private accessCode: string | null = null;\n private serial: string | null = null;\n private lastState: PrintData | null = null;\n private isConnecting = false;\n private eventsAllowed = true;\n private sequenceIdCounter = 10;\n private isFirstMessage = true;\n\n constructor(settingsStore: SettingsStore, loggerFactory: ILoggerFactory, eventEmitter2: EventEmitter2) {\n this.settingsStore = settingsStore;\n this.eventEmitter2 = eventEmitter2;\n this.logger = loggerFactory(BambuMqttAdapter.name);\n }\n\n registerCredentials(socketLogin: ISocketLogin): void {\n const { printerId, loginDto } = socketLogin;\n this.printerId = printerId;\n this.login = loginDto;\n\n this.host = loginDto.printerURL?.replace(/^https?:\\/\\//, \"\");\n this.accessCode = loginDto.password || null;\n this.serial = loginDto.username || null;\n }\n\n needsReopen(): boolean {\n const isApiOnline = this.apiState === API_STATE.responding;\n return isApiOnline && (this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.error);\n }\n\n needsSetup(): boolean {\n return this.socketState === SOCKET_STATE.unopened;\n }\n\n needsReauth(): boolean {\n return false;\n }\n\n isClosedOrAborted(): boolean {\n return this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.aborted;\n }\n\n async reauthSession(): Promise<void> {\n this.logger.debug(\"reauthSession called but not needed for Bambu\");\n }\n\n open(): void {\n if (!this.host || !this.accessCode || !this.serial) {\n throw new Error(\"Cannot open connection: credentials not registered\");\n }\n\n this.connect(this.host, this.accessCode, this.serial);\n }\n\n close(): void {\n this.disconnect().catch((err) => {\n this.logger.error(\"Error during MQTT disconnect:\", err);\n });\n }\n\n async setupSocketSession(): Promise<void> {\n // For Bambu, we just need to validate credentials are set\n if (!this.host || !this.accessCode || !this.serial) {\n this.updateSocketState(SOCKET_STATE.aborted);\n this.updateApiState(API_STATE.noResponse);\n throw new Error(\"Credentials not properly registered\");\n }\n\n this.updateSocketState(SOCKET_STATE.opening);\n this.updateApiState(API_STATE.responding);\n }\n\n allowEmittingEvents(): void {\n this.eventsAllowed = true;\n }\n\n disallowEmittingEvents(): void {\n this.eventsAllowed = false;\n }\n\n connect(host: string, accessCode: string, serial: string): void {\n if (this.mqttClient?.connected) {\n this.logger.debug(\"MQTT already connected\");\n this.updateSocketState(SOCKET_STATE.opened);\n return;\n }\n\n if (this.isConnecting) {\n this.logger.warn(\"Connection already in progress\");\n return;\n }\n\n this.host = host;\n this.accessCode = accessCode;\n this.serial = serial;\n this.isConnecting = true;\n this.updateSocketState(SOCKET_STATE.opening);\n\n const mqttUrl = `mqtts://${host}:8883`;\n const timeout = this.settingsStore.getTimeoutSettings().apiTimeout;\n\n this.logger.log(`Connecting to Bambu MQTT at ${mqttUrl}`);\n\n const connectionTimeout = setTimeout(() => {\n this.isConnecting = false;\n this.updateSocketState(SOCKET_STATE.error);\n this.logger.error(\"MQTT connection timeout - will keep trying to reconnect\");\n // Don't force end - let mqtt.js handle reconnection automatically\n // The close event handler will handle cleanup if needed\n }, timeout);\n\n try {\n this.mqttClient = mqtt.connect(mqttUrl, {\n username: \"bblp\",\n password: accessCode,\n reconnectPeriod: 5000,\n rejectUnauthorized: false,\n });\n\n this.mqttClient.on(\"connect\", () => {\n clearTimeout(connectionTimeout);\n this.isConnecting = false;\n this.updateSocketState(SOCKET_STATE.authenticated);\n this.updateApiState(API_STATE.responding);\n this.logger.log(\"MQTT connected successfully\");\n this.logger.debug(`Connected to MQTT broker at mqtts://${host}:8883`);\n\n const reportTopic = `device/${serial}/report`;\n this.mqttClient!.subscribe(reportTopic, { qos: 0 }, (err) => {\n if (err) {\n this.logger.error(`Failed to subscribe to ${reportTopic}:`, err);\n this.updateSocketState(SOCKET_STATE.error);\n } else {\n this.logger.debug(`Subscribed to ${reportTopic}`);\n\n this.sendPushallCommand().catch((err) => {\n this.logger.error(\"Failed to send pushall command:\", err);\n });\n }\n });\n });\n\n this.mqttClient.on(\"error\", (error) => {\n this.isConnecting = false;\n this.updateSocketState(SOCKET_STATE.error);\n this.emitEvent(WsMessage.WS_ERROR, error.message).catch(() => {});\n this.logger.error(\"MQTT error:\", error);\n });\n\n this.mqttClient.on(\"message\", (topic, message) => {\n this.lastMessageReceivedTimestamp = Date.now();\n this.handleMessage(topic, message);\n });\n\n this.mqttClient.on(\"disconnect\", () => {\n this.updateSocketState(SOCKET_STATE.closed);\n this.emitEvent(WsMessage.WS_CLOSED, \"disconnected\").catch(() => {});\n this.logger.warn(\"MQTT disconnected\");\n });\n\n this.mqttClient.on(\"reconnect\", () => {\n this.updateSocketState(SOCKET_STATE.opening);\n this.logger.log(\"MQTT attempting to reconnect...\");\n // Reset first message flag so we log on reconnection\n this.isFirstMessage = true;\n });\n\n this.mqttClient.on(\"close\", () => {\n this.updateSocketState(SOCKET_STATE.closed);\n this.updateApiState(API_STATE.noResponse);\n this.emitEvent(WsMessage.WS_CLOSED, \"connection closed\").catch(() => {});\n this.logger.warn(\"MQTT connection closed - automatic reconnection will be attempted\");\n\n // Emit a \"current\" event with offline state so frontend updates\n if (this.lastState) {\n const offlineMessage = this.transformStateToCurrentMessage(this.lastState);\n this.emitEvent(\"current\", { ...offlineMessage, print: this.lastState }).catch(() => {});\n }\n\n // Don't call cleanup() here - it destroys the client and prevents reconnection\n // cleanup() will be called in disconnect() when we intentionally close\n });\n\n this.mqttClient.on(\"offline\", () => {\n this.updateSocketState(SOCKET_STATE.closed);\n this.updateApiState(API_STATE.noResponse);\n this.logger.warn(\"MQTT client offline - automatic reconnection will be attempted\");\n\n if (this.lastState) {\n const offlineMessage = this.transformStateToCurrentMessage(this.lastState);\n this.emitEvent(\"current\", { ...offlineMessage, print: this.lastState }).catch(() => {});\n }\n });\n } catch (error) {\n clearTimeout(connectionTimeout);\n this.isConnecting = false;\n this.updateSocketState(SOCKET_STATE.error);\n this.logger.error(\"Failed to create MQTT client:\", error);\n this.cleanup();\n }\n }\n\n async disconnect(): Promise<void> {\n if (!this.mqttClient) {\n this.updateSocketState(SOCKET_STATE.closed);\n return;\n }\n\n this.logger.log(\"Disconnecting MQTT\");\n this.updateSocketState(SOCKET_STATE.closed);\n\n return new Promise<void>((resolve) => {\n if (this.mqttClient?.connected) {\n this.mqttClient.end(false, {}, () => {\n this.cleanup();\n resolve();\n });\n } else {\n this.cleanup();\n resolve();\n }\n });\n }\n\n getLastState(): PrintData | null {\n return this.lastState;\n }\n\n private async sendPushallCommand(): Promise<void> {\n const payload = {\n pushing: {\n sequence_id: this.sequenceIdCounter,\n command: \"pushall\",\n version: 1,\n push_target: 1,\n },\n };\n\n this.logger.debug(`Sending command: ${JSON.stringify(payload)} with sequence ID: ${this.sequenceIdCounter}`);\n this.sequenceIdCounter++;\n\n await this.sendCommand(payload);\n this.logger.debug(\"Connected to printer via MQTT\");\n }\n\n async sendCommand(payload: Record<string, any>): Promise<void> {\n if (!this.mqttClient?.connected) {\n throw new Error(\"MQTT not connected\");\n }\n\n if (!this.serial) {\n throw new Error(\"Serial number not set\");\n }\n\n const reportTopic = `device/${this.serial}/report`;\n const message = JSON.stringify(payload);\n\n return new Promise<void>((resolve, reject) => {\n this.mqttClient!.publish(reportTopic, message, { qos: 0 }, (err) => {\n if (err) {\n this.logger.error(\"Failed to send command:\", err);\n reject(err);\n } else {\n this.logger.debug(`Command sent: ${message}`);\n resolve();\n }\n });\n });\n }\n\n async startPrint(\n filename: string,\n subtask_name?: string,\n amsMapping?: number[],\n plateNumber: number = 1,\n ): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"project_file\",\n param: `Metadata/plate_${plateNumber}.gcode`,\n project_id: \"0\", // Always 0 for local prints\n profile_id: \"0\", // Always 0 for local prints\n task_id: \"0\", // Always 0 for local prints\n subtask_id: \"0\", // Always 0 for local prints\n subtask_name: subtask_name ?? filename,\n\n file: \"\", // Filename to print, not needed when \"url\" is specified with filepath\n url: `file:///${filename}`, // URL to print. Root path, protocol can vary. E.g., if sd card, \"ftp:///myfile.3mf\", \"ftp:///cache/myotherfile.3mf\"\n md5: \"\",\n\n timelapse: true,\n bed_type: \"auto\", // Always \"auto\" for local prints\n bed_levelling: true,\n flow_cali: true,\n vibration_cali: true,\n layer_inspect: true,\n\n // AMS mapping: array where each index represents a filament color in your gcode\n // and the value is the AMS slot number (0-3) to use, or -1 to skip\n // Example: [0, 2, -1, 1] means: color 0 uses slot 0, color 1 uses slot 2, color 2 skipped, color 3 uses slot 1\n ams_mapping: amsMapping ? amsMapping.join(\",\") : \"\", // Convert [0, 1, 2, 3] to \"0,1,2,3\"\n use_ams: !!amsMapping && amsMapping.length > 0,\n\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n async pausePrint(): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"pause\",\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n async resumePrint(): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"resume\",\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n async stopPrint(): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"stop\",\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n async sendGcode(gcode: string): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"gcode_line\",\n param: gcode,\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n resetSocketState(): void {\n this.lastState = null;\n }\n\n private updateSocketState(state: SocketState): void {\n this.socketState = state;\n this.emitEventSync(WsMessage.WS_STATE_UPDATED, state);\n }\n\n private updateApiState(state: ApiState): void {\n this.apiState = state;\n this.emitEventSync(WsMessage.API_STATE_UPDATED, state);\n }\n\n private async emitEvent(event: string, payload?: any): Promise<void> {\n if (!this.eventsAllowed) {\n return;\n }\n\n await this.eventEmitter2.emitAsync(bambuEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: BambuType,\n } as BambuEventDto);\n }\n\n private emitEventSync(event: string, payload: any): void {\n if (!this.eventsAllowed) {\n return;\n }\n\n this.eventEmitter2.emit(bambuEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: BambuType,\n } as BambuEventDto);\n }\n\n private transformStateToCurrentMessage(state: PrintData): any {\n const isPrinting = state.gcode_state === \"PRINTING\" || state.mc_print_stage === \"printing\";\n const isPaused = state.mc_print_stage === \"paused\";\n const isFailed = state.gcode_state === \"FAILED\";\n\n // Check if connection is alive\n const isConnected = this.mqttClient?.connected || false;\n // print_error is a side-channel hint that often carries non-zero stale codes when idle;\n // gcode_state is the authoritative print-state machine. Use FAILED for the error flag,\n // and surface print_error via debug logs so anomalies are still discoverable.\n const hasError = !isConnected || isFailed;\n if (state.print_error && state.print_error !== 0) {\n this.logger.debug(\n `Bambu print_error=${state.print_error} gcode_state=${state.gcode_state ?? \"?\"} (informational only)`,\n );\n }\n\n const isPausedText = isPaused ? \"Paused\" : \"Printing\";\n\n const onlineText = isPrinting ? isPausedText : \"Operational\";\n return {\n state: {\n text: isConnected ? (isFailed ? \"Error\" : onlineText) : \"Offline\",\n flags: {\n operational: isConnected && !isFailed,\n printing: isConnected && isPrinting && !isPaused,\n paused: isConnected && isPaused,\n ready: isConnected && !isPrinting && !isFailed,\n error: hasError,\n cancelling: false,\n pausing: false,\n sdReady: isConnected,\n closedOrError: !isConnected || isFailed,\n },\n },\n temps: [\n {\n time: Date.now(),\n tool0: {\n actual: state.nozzle_temper || 0,\n target: state.nozzle_target_temper || 0,\n },\n bed: {\n actual: state.bed_temper || 0,\n target: state.bed_target_temper || 0,\n },\n chamber: {\n actual: state.chamber_temper || 0,\n target: 0,\n },\n },\n ],\n progress: {\n completion: state.mc_percent || 0,\n printTime: null,\n printTimeLeft: state.mc_remaining_time ? state.mc_remaining_time * 60 : null,\n },\n job: {\n file: {\n name: state.gcode_file || state.subtask_name || null,\n },\n },\n currentZ: state.layer_num || null,\n offsets: {},\n resends: { count: 0, transmitted: 0, ratio: 0 },\n logs: [],\n messages: [],\n };\n }\n\n private handleMessage(topic: string, message: Buffer): void {\n try {\n const payload = JSON.parse(message.toString());\n\n if (topic.endsWith(\"/report\") && payload.print) {\n this.lastState = payload.print as PrintData;\n\n if (this.isFirstMessage) {\n this.logger.debug(\"Initial message received\");\n this.isFirstMessage = false;\n }\n\n // Emit combined payload:\n // - Transformed format for UI (state, temps, progress, job)\n // - Raw print object for PrinterEventsCache job tracking\n const currentMessage = this.transformStateToCurrentMessage(this.lastState);\n const combinedPayload = {\n ...currentMessage,\n print: payload.print, // Include raw print data for job tracking\n };\n this.emitEvent(\"current\", combinedPayload).catch((err) => {\n this.logger.error(\"Failed to emit current event:\", err);\n });\n }\n } catch (error) {\n this.logger.error(\"Failed to parse MQTT message:\", error);\n }\n }\n\n private cleanup(): void {\n if (this.mqttClient) {\n this.mqttClient.removeAllListeners();\n this.mqttClient = null;\n }\n this.isConnecting = false;\n this.lastState = null;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bambu-mqtt.adapter.js","names":[],"sources":["../../../src/services/bambu/bambu-mqtt.adapter.ts"],"sourcesContent":["import type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport EventEmitter2 from \"eventemitter2\";\nimport { SettingsStore } from \"@/state/settings.store\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { PrintData } from \"@/services/bambu/mqtt-message.types\";\nimport mqtt from \"mqtt\";\nimport type { IWebsocketAdapter } from \"@/services/websocket-adapter.interface\";\nimport type { ISocketLogin } from \"@/shared/dtos/socket-login.dto\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport { SOCKET_STATE, SocketState } from \"@/shared/dtos/socket-state.type\";\nimport { API_STATE, ApiState } from \"@/shared/dtos/api-state.type\";\nimport { BambuType } from \"@/services/printer-api.interface\";\nimport { WsMessage } from \"@/services/octoprint/octoprint-websocket.adapter\";\n\nexport const bambuEvent = (event: string) => `bambu.${event}`;\n\nexport interface BambuEventDto {\n event: string;\n payload: any;\n printerId?: number;\n printerType: typeof BambuType;\n}\n\nexport class BambuMqttAdapter implements IWebsocketAdapter {\n protected readonly logger: LoggerService;\n private readonly settingsStore: SettingsStore;\n private readonly eventEmitter2: EventEmitter2;\n\n public readonly printerType = BambuType;\n public printerId?: number;\n public socketState: SocketState = SOCKET_STATE.unopened;\n public apiState: ApiState = API_STATE.unset;\n public login: LoginDto;\n public lastMessageReceivedTimestamp: null | number = null;\n\n private mqttClient: mqtt.MqttClient | null = null;\n private host: string | null = null;\n private accessCode: string | null = null;\n private serial: string | null = null;\n private lastState: PrintData | null = null;\n private isConnecting = false;\n private eventsAllowed = true;\n private sequenceIdCounter = 10;\n private isFirstMessage = true;\n\n constructor(settingsStore: SettingsStore, loggerFactory: ILoggerFactory, eventEmitter2: EventEmitter2) {\n this.settingsStore = settingsStore;\n this.eventEmitter2 = eventEmitter2;\n this.logger = loggerFactory(BambuMqttAdapter.name);\n }\n\n registerCredentials(socketLogin: ISocketLogin): void {\n const { printerId, loginDto } = socketLogin;\n this.printerId = printerId;\n this.login = loginDto;\n\n this.host = loginDto.printerURL?.replace(/^https?:\\/\\//, \"\");\n this.accessCode = loginDto.password || null;\n this.serial = loginDto.username || null;\n }\n\n needsReopen(): boolean {\n const isApiOnline = this.apiState === API_STATE.responding;\n return isApiOnline && (this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.error);\n }\n\n needsSetup(): boolean {\n return this.socketState === SOCKET_STATE.unopened;\n }\n\n needsReauth(): boolean {\n return false;\n }\n\n isClosedOrAborted(): boolean {\n return this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.aborted;\n }\n\n async reauthSession(): Promise<void> {\n this.logger.debug(\"reauthSession called but not needed for Bambu\");\n }\n\n open(): void {\n if (!this.host || !this.accessCode || !this.serial) {\n throw new Error(\"Cannot open connection: credentials not registered\");\n }\n\n this.connect(this.host, this.accessCode, this.serial);\n }\n\n close(): void {\n this.disconnect().catch((err) => {\n this.logger.error(\"Error during MQTT disconnect:\", err);\n });\n }\n\n async setupSocketSession(): Promise<void> {\n // For Bambu, we just need to validate credentials are set\n if (!this.host || !this.accessCode || !this.serial) {\n this.updateSocketState(SOCKET_STATE.aborted);\n this.updateApiState(API_STATE.noResponse);\n throw new Error(\"Credentials not properly registered\");\n }\n\n this.updateSocketState(SOCKET_STATE.opening);\n this.updateApiState(API_STATE.responding);\n }\n\n allowEmittingEvents(): void {\n this.eventsAllowed = true;\n }\n\n disallowEmittingEvents(): void {\n this.eventsAllowed = false;\n }\n\n connect(host: string, accessCode: string, serial: string): void {\n if (this.mqttClient?.connected) {\n this.logger.debug(\"MQTT already connected\");\n this.updateSocketState(SOCKET_STATE.opened);\n return;\n }\n\n if (this.isConnecting) {\n this.logger.warn(\"Connection already in progress\");\n return;\n }\n\n this.host = host;\n this.accessCode = accessCode;\n this.serial = serial;\n this.isConnecting = true;\n this.updateSocketState(SOCKET_STATE.opening);\n\n const mqttUrl = `mqtts://${host}:8883`;\n const timeout = this.settingsStore.getTimeoutSettings().apiTimeout;\n\n this.logger.log(`Connecting to Bambu MQTT at ${mqttUrl}`);\n\n const connectionTimeout = setTimeout(() => {\n this.isConnecting = false;\n this.updateSocketState(SOCKET_STATE.error);\n this.logger.error(\"MQTT connection timeout - will keep trying to reconnect\");\n // Don't force end - let mqtt.js handle reconnection automatically\n // The close event handler will handle cleanup if needed\n }, timeout);\n\n try {\n this.mqttClient = mqtt.connect(mqttUrl, {\n username: \"bblp\",\n password: accessCode,\n reconnectPeriod: 5000,\n rejectUnauthorized: false,\n });\n\n this.mqttClient.on(\"connect\", () => {\n clearTimeout(connectionTimeout);\n this.isConnecting = false;\n this.updateSocketState(SOCKET_STATE.authenticated);\n this.updateApiState(API_STATE.responding);\n this.logger.log(\"MQTT connected successfully\");\n this.logger.debug(`Connected to MQTT broker at mqtts://${host}:8883`);\n\n const reportTopic = `device/${serial}/report`;\n this.mqttClient!.subscribe(reportTopic, { qos: 0 }, (err) => {\n if (err) {\n this.logger.error(`Failed to subscribe to ${reportTopic}:`, err);\n this.updateSocketState(SOCKET_STATE.error);\n } else {\n this.logger.debug(`Subscribed to ${reportTopic}`);\n\n this.sendPushallCommand().catch((err) => {\n this.logger.error(\"Failed to send pushall command:\", err);\n });\n }\n });\n });\n\n this.mqttClient.on(\"error\", (error) => {\n this.isConnecting = false;\n this.updateSocketState(SOCKET_STATE.error);\n this.emitEvent(WsMessage.WS_ERROR, error.message).catch(() => {});\n this.logger.error(\"MQTT error:\", error);\n });\n\n this.mqttClient.on(\"message\", (topic, message) => {\n this.lastMessageReceivedTimestamp = Date.now();\n this.handleMessage(topic, message);\n });\n\n this.mqttClient.on(\"disconnect\", () => {\n this.updateSocketState(SOCKET_STATE.closed);\n this.emitEvent(WsMessage.WS_CLOSED, \"disconnected\").catch(() => {});\n this.logger.warn(\"MQTT disconnected\");\n });\n\n this.mqttClient.on(\"reconnect\", () => {\n this.updateSocketState(SOCKET_STATE.opening);\n this.logger.log(\"MQTT attempting to reconnect...\");\n // Reset first message flag so we log on reconnection\n this.isFirstMessage = true;\n });\n\n this.mqttClient.on(\"close\", () => {\n this.updateSocketState(SOCKET_STATE.closed);\n this.updateApiState(API_STATE.noResponse);\n this.emitEvent(WsMessage.WS_CLOSED, \"connection closed\").catch(() => {});\n this.logger.warn(\"MQTT connection closed - automatic reconnection will be attempted\");\n\n // Emit a \"current\" event with offline state so frontend updates\n if (this.lastState) {\n const offlineMessage = this.transformStateToCurrentMessage(this.lastState);\n this.emitEvent(\"current\", { ...offlineMessage, print: this.lastState }).catch(() => {});\n }\n\n // Don't call cleanup() here - it destroys the client and prevents reconnection\n // cleanup() will be called in disconnect() when we intentionally close\n });\n\n this.mqttClient.on(\"offline\", () => {\n this.updateSocketState(SOCKET_STATE.closed);\n this.updateApiState(API_STATE.noResponse);\n this.logger.warn(\"MQTT client offline - automatic reconnection will be attempted\");\n\n if (this.lastState) {\n const offlineMessage = this.transformStateToCurrentMessage(this.lastState);\n this.emitEvent(\"current\", { ...offlineMessage, print: this.lastState }).catch(() => {});\n }\n });\n } catch (error) {\n clearTimeout(connectionTimeout);\n this.isConnecting = false;\n this.updateSocketState(SOCKET_STATE.error);\n this.logger.error(\"Failed to create MQTT client:\", error);\n this.cleanup();\n }\n }\n\n async disconnect(): Promise<void> {\n if (!this.mqttClient) {\n this.updateSocketState(SOCKET_STATE.closed);\n return;\n }\n\n this.logger.log(\"Disconnecting MQTT\");\n this.updateSocketState(SOCKET_STATE.closed);\n\n return new Promise<void>((resolve) => {\n if (this.mqttClient?.connected) {\n this.mqttClient.end(false, {}, () => {\n this.cleanup();\n resolve();\n });\n } else {\n this.cleanup();\n resolve();\n }\n });\n }\n\n getLastState(): PrintData | null {\n return this.lastState;\n }\n\n private async sendPushallCommand(): Promise<void> {\n const payload = {\n pushing: {\n sequence_id: this.sequenceIdCounter,\n command: \"pushall\",\n version: 1,\n push_target: 1,\n },\n };\n\n this.logger.debug(`Sending command: ${JSON.stringify(payload)} with sequence ID: ${this.sequenceIdCounter}`);\n this.sequenceIdCounter++;\n\n await this.sendCommand(payload);\n this.logger.debug(\"Connected to printer via MQTT\");\n }\n\n async sendCommand(payload: Record<string, any>): Promise<void> {\n if (!this.mqttClient?.connected) {\n throw new Error(\"MQTT not connected\");\n }\n\n if (!this.serial) {\n throw new Error(\"Serial number not set\");\n }\n\n const reportTopic = `device/${this.serial}/report`;\n const message = JSON.stringify(payload);\n\n return new Promise<void>((resolve, reject) => {\n this.mqttClient!.publish(reportTopic, message, { qos: 0 }, (err) => {\n if (err) {\n this.logger.error(\"Failed to send command:\", err);\n reject(err);\n } else {\n this.logger.debug(`Command sent: ${message}`);\n resolve();\n }\n });\n });\n }\n\n async startPrint(\n filename: string,\n subtask_name?: string,\n amsMapping?: number[],\n plateNumber: number = 1,\n ): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"project_file\",\n param: `Metadata/plate_${plateNumber}.gcode`,\n project_id: \"0\", // Always 0 for local prints\n profile_id: \"0\", // Always 0 for local prints\n task_id: \"0\", // Always 0 for local prints\n subtask_id: \"0\", // Always 0 for local prints\n subtask_name: subtask_name ?? filename,\n\n file: \"\", // Filename to print, not needed when \"url\" is specified with filepath\n url: `file:///${filename}`, // URL to print. Root path, protocol can vary. E.g., if sd card, \"ftp:///myfile.3mf\", \"ftp:///cache/myotherfile.3mf\"\n md5: \"\",\n\n timelapse: true,\n bed_type: \"auto\", // Always \"auto\" for local prints\n bed_levelling: true,\n flow_cali: true,\n vibration_cali: true,\n layer_inspect: true,\n\n // AMS mapping: array where each index represents a filament color in your gcode\n // and the value is the AMS slot number (0-3) to use, or -1 to skip\n // Example: [0, 2, -1, 1] means: color 0 uses slot 0, color 1 uses slot 2, color 2 skipped, color 3 uses slot 1\n ams_mapping: amsMapping ? amsMapping.join(\",\") : \"\", // Convert [0, 1, 2, 3] to \"0,1,2,3\"\n use_ams: !!amsMapping && amsMapping.length > 0,\n\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n async pausePrint(): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"pause\",\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n async resumePrint(): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"resume\",\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n async stopPrint(): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"stop\",\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n async sendGcode(gcode: string): Promise<void> {\n await this.sendCommand({\n print: {\n command: \"gcode_line\",\n param: gcode,\n sequence_id: this.sequenceIdCounter++,\n },\n });\n }\n\n resetSocketState(): void {\n this.lastState = null;\n }\n\n private updateSocketState(state: SocketState): void {\n this.socketState = state;\n this.emitEventSync(WsMessage.WS_STATE_UPDATED, state);\n }\n\n private updateApiState(state: ApiState): void {\n this.apiState = state;\n this.emitEventSync(WsMessage.API_STATE_UPDATED, state);\n }\n\n private async emitEvent(event: string, payload?: any): Promise<void> {\n if (!this.eventsAllowed) {\n return;\n }\n\n await this.eventEmitter2.emitAsync(bambuEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: BambuType,\n } as BambuEventDto);\n }\n\n private emitEventSync(event: string, payload: any): void {\n if (!this.eventsAllowed) {\n return;\n }\n\n this.eventEmitter2.emit(bambuEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: BambuType,\n } as BambuEventDto);\n }\n\n private transformStateToCurrentMessage(state: PrintData): any {\n const isPrinting = state.gcode_state === \"PRINTING\" || state.mc_print_stage === \"printing\";\n const isPaused = state.mc_print_stage === \"paused\";\n const isFailed = state.gcode_state === \"FAILED\";\n\n // Check if connection is alive\n const isConnected = this.mqttClient?.connected || false;\n // print_error is a side-channel hint that often carries non-zero stale codes when idle;\n // gcode_state is the authoritative print-state machine. Use FAILED for the error flag,\n // and surface print_error via debug logs so anomalies are still discoverable.\n const hasError = !isConnected || isFailed;\n if (state.print_error && state.print_error !== 0) {\n this.logger.debug(\n `Bambu print_error=${state.print_error} gcode_state=${state.gcode_state ?? \"?\"} (informational only)`,\n );\n }\n\n const isPausedText = isPaused ? \"Paused\" : \"Printing\";\n\n const onlineText = isPrinting ? isPausedText : \"Operational\";\n return {\n state: {\n text: isConnected ? (isFailed ? \"Error\" : onlineText) : \"Offline\",\n flags: {\n operational: isConnected && !isFailed,\n printing: isConnected && isPrinting && !isPaused,\n paused: isConnected && isPaused,\n ready: isConnected && !isPrinting && !isFailed,\n error: hasError,\n cancelling: false,\n pausing: false,\n sdReady: isConnected,\n closedOrError: !isConnected || isFailed,\n },\n },\n temps: [\n {\n time: Date.now(),\n tool0: {\n actual: state.nozzle_temper || 0,\n target: state.nozzle_target_temper || 0,\n },\n bed: {\n actual: state.bed_temper || 0,\n target: state.bed_target_temper || 0,\n },\n chamber: {\n actual: state.chamber_temper || 0,\n target: 0,\n },\n },\n ],\n progress: {\n completion: state.mc_percent || 0,\n printTime: null,\n printTimeLeft: state.mc_remaining_time ? state.mc_remaining_time * 60 : null,\n },\n job: {\n file: {\n name: state.gcode_file || state.subtask_name || null,\n },\n },\n currentZ: state.layer_num || null,\n offsets: {},\n resends: { count: 0, transmitted: 0, ratio: 0 },\n logs: [],\n messages: [],\n };\n }\n\n private handleMessage(topic: string, message: Buffer): void {\n try {\n const payload = JSON.parse(message.toString());\n\n if (topic.endsWith(\"/report\") && payload.print) {\n this.lastState = payload.print as PrintData;\n\n if (this.isFirstMessage) {\n this.logger.debug(\"Initial message received\");\n this.isFirstMessage = false;\n }\n\n // Emit combined payload:\n // - Transformed format for UI (state, temps, progress, job)\n // - Raw print object for PrinterEventsCache job tracking\n const currentMessage = this.transformStateToCurrentMessage(this.lastState);\n const combinedPayload = {\n ...currentMessage,\n print: payload.print, // Include raw print data for job tracking\n };\n this.emitEvent(\"current\", combinedPayload).catch((err) => {\n this.logger.error(\"Failed to emit current event:\", err);\n });\n }\n } catch (error) {\n this.logger.error(\"Failed to parse MQTT message:\", error);\n }\n }\n\n private cleanup(): void {\n if (this.mqttClient) {\n this.mqttClient.removeAllListeners();\n this.mqttClient = null;\n }\n this.isConnecting = false;\n this.lastState = null;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"bambu.client.js","names":[],"sources":["../../../src/services/bambu/bambu.client.ts"],"sourcesContent":["import EventEmitter2 from \"eventemitter2\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { SettingsStore } from \"@/state/settings.store\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport { BambuFtpAdapter } from \"@/services/bambu/bambu-ftp.adapter\";\n\n/**\n * Bambu Lab printer client\n * Manages FTP connections for file operations\n *\n * Note: MQTT adapter is managed by PrinterSocketStore, not this client\n *\n * Field mapping from Printer entity:\n * - printerURL: Host IP address\n * - password: Access code (8-character authentication code)\n * - username: Serial number\n */\nexport class BambuClient {\n readonly eventEmitter2: EventEmitter2;\n readonly settingsStore: SettingsStore;\n protected readonly logger: LoggerService;\n private readonly ftpAdapter: BambuFtpAdapter;\n private connected = false;\n\n constructor(\n settingsStore: SettingsStore,\n loggerFactory: ILoggerFactory,\n eventEmitter2: EventEmitter2,\n bambuFtpAdapter: BambuFtpAdapter,\n ) {\n this.settingsStore = settingsStore;\n this.eventEmitter2 = eventEmitter2;\n this.logger = loggerFactory(BambuClient.name);\n this.ftpAdapter = bambuFtpAdapter;\n }\n\n async connect(login: LoginDto): Promise<void> {\n const host = this.extractHost(login.printerURL);\n const accessCode = login.password || \"\";\n\n if (!accessCode) {\n throw new Error(\"Access code (password) is required for Bambu Lab printers\");\n }\n\n this.logger.log(`Connecting to Bambu Lab printer FTP at ${host}`);\n\n try {\n await this.ftpAdapter.connect(host, accessCode);\n this.connected = true;\n this.logger.log(\"Successfully connected to Bambu Lab printer FTP\");\n } catch (error) {\n this.logger.error(\"Failed to connect to Bambu Lab printer FTP:\", error);\n await this.disconnect();\n throw error;\n }\n }\n\n async disconnect(): Promise<void> {\n this.logger.log(\"Disconnecting from Bambu Lab printer FTP\");\n await this.ftpAdapter.disconnect();\n this.connected = false;\n this.logger.log(\"Disconnected from Bambu Lab printer FTP\");\n }\n\n get ftp(): BambuFtpAdapter {\n return this.ftpAdapter;\n }\n\n get isConnected(): boolean {\n return this.connected && this.ftpAdapter.isConnected;\n }\n\n private extractHost(printerURL: string): string {\n try {\n const url = new URL(printerURL);\n return url.hostname;\n } catch {\n // If not a valid URL, assume it's just the hostname/IP\n return printerURL\n .replace(/^https?:\\/\\//, \"\")\n .split(\":\")[0]\n .split(\"/\")[0];\n }\n }\n\n async getApiVersion(_login: LoginDto, _timeout?: number): Promise<{ version: string }> {\n return { version: \"bambu-1.0\" };\n }\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,IAAa,cAAb,MAAa,YAAY;CACvB;CACA;CACA;CACA;CACA,YAAoB;CAEpB,YACE,eACA,eACA,eACA,iBACA;AACA,OAAK,gBAAgB;AACrB,OAAK,gBAAgB;AACrB,OAAK,SAAS,cAAc,YAAY,KAAK;AAC7C,OAAK,aAAa;;CAGpB,MAAM,QAAQ,OAAgC;EAC5C,MAAM,OAAO,KAAK,YAAY,MAAM,WAAW;EAC/C,MAAM,aAAa,MAAM,YAAY;AAErC,MAAI,CAAC,WACH,OAAM,IAAI,MAAM,4DAA4D;AAG9E,OAAK,OAAO,IAAI,0CAA0C,OAAO;AAEjE,MAAI;AACF,SAAM,KAAK,WAAW,QAAQ,MAAM,WAAW;AAC/C,QAAK,YAAY;AACjB,QAAK,OAAO,IAAI,kDAAkD;WAC3D,OAAO;AACd,QAAK,OAAO,MAAM,+CAA+C,MAAM;AACvE,SAAM,KAAK,YAAY;AACvB,SAAM;;;CAIV,MAAM,aAA4B;AAChC,OAAK,OAAO,IAAI,2CAA2C;AAC3D,QAAM,KAAK,WAAW,YAAY;AAClC,OAAK,YAAY;AACjB,OAAK,OAAO,IAAI,0CAA0C;;CAG5D,IAAI,MAAuB;AACzB,SAAO,KAAK;;CAGd,IAAI,cAAuB;AACzB,SAAO,KAAK,aAAa,KAAK,WAAW;;CAG3C,YAAoB,YAA4B;AAC9C,MAAI;AAEF,UAAO,IADS,IAAI,WACV,CAAC;UACL;AAEN,UAAO,WACJ,QAAQ,gBAAgB,GAAG,CAC3B,MAAM,IAAI,CAAC,GACX,MAAM,IAAI,CAAC;;;CAIlB,MAAM,cAAc,QAAkB,UAAiD;AACrF,SAAO,EAAE,SAAS,aAAa"}
1
+ {"version":3,"file":"bambu.client.js","names":[],"sources":["../../../src/services/bambu/bambu.client.ts"],"sourcesContent":["import EventEmitter2 from \"eventemitter2\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { SettingsStore } from \"@/state/settings.store\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport { BambuFtpAdapter } from \"@/services/bambu/bambu-ftp.adapter\";\n\n/**\n * Bambu Lab printer client\n * Manages FTP connections for file operations\n *\n * Note: MQTT adapter is managed by PrinterSocketStore, not this client\n *\n * Field mapping from Printer entity:\n * - printerURL: Host IP address\n * - password: Access code (8-character authentication code)\n * - username: Serial number\n */\nexport class BambuClient {\n readonly eventEmitter2: EventEmitter2;\n readonly settingsStore: SettingsStore;\n protected readonly logger: LoggerService;\n private readonly ftpAdapter: BambuFtpAdapter;\n private connected = false;\n\n constructor(\n settingsStore: SettingsStore,\n loggerFactory: ILoggerFactory,\n eventEmitter2: EventEmitter2,\n bambuFtpAdapter: BambuFtpAdapter,\n ) {\n this.settingsStore = settingsStore;\n this.eventEmitter2 = eventEmitter2;\n this.logger = loggerFactory(BambuClient.name);\n this.ftpAdapter = bambuFtpAdapter;\n }\n\n async connect(login: LoginDto): Promise<void> {\n const host = this.extractHost(login.printerURL);\n const accessCode = login.password || \"\";\n\n if (!accessCode) {\n throw new Error(\"Access code (password) is required for Bambu Lab printers\");\n }\n\n this.logger.log(`Connecting to Bambu Lab printer FTP at ${host}`);\n\n try {\n await this.ftpAdapter.connect(host, accessCode);\n this.connected = true;\n this.logger.log(\"Successfully connected to Bambu Lab printer FTP\");\n } catch (error) {\n this.logger.error(\"Failed to connect to Bambu Lab printer FTP:\", error);\n await this.disconnect();\n throw error;\n }\n }\n\n async disconnect(): Promise<void> {\n this.logger.log(\"Disconnecting from Bambu Lab printer FTP\");\n await this.ftpAdapter.disconnect();\n this.connected = false;\n this.logger.log(\"Disconnected from Bambu Lab printer FTP\");\n }\n\n get ftp(): BambuFtpAdapter {\n return this.ftpAdapter;\n }\n\n get isConnected(): boolean {\n return this.connected && this.ftpAdapter.isConnected;\n }\n\n private extractHost(printerURL: string): string {\n try {\n const url = new URL(printerURL);\n return url.hostname;\n } catch {\n // If not a valid URL, assume it's just the hostname/IP\n return printerURL\n .replace(/^https?:\\/\\//, \"\")\n .split(\":\")[0]\n .split(\"/\")[0];\n }\n }\n\n async getApiVersion(_login: LoginDto, _timeout?: number): Promise<{ version: string }> {\n return { version: \"bambu-1.0\" };\n }\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,IAAa,cAAb,MAAa,YAAY;CACvB;CACA;CACA;CACA;CACA,YAAoB;CAEpB,YACE,eACA,eACA,eACA,iBACA;EACA,KAAK,gBAAgB;EACrB,KAAK,gBAAgB;EACrB,KAAK,SAAS,cAAc,YAAY,KAAK;EAC7C,KAAK,aAAa;;CAGpB,MAAM,QAAQ,OAAgC;EAC5C,MAAM,OAAO,KAAK,YAAY,MAAM,WAAW;EAC/C,MAAM,aAAa,MAAM,YAAY;EAErC,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,4DAA4D;EAG9E,KAAK,OAAO,IAAI,0CAA0C,OAAO;EAEjE,IAAI;GACF,MAAM,KAAK,WAAW,QAAQ,MAAM,WAAW;GAC/C,KAAK,YAAY;GACjB,KAAK,OAAO,IAAI,kDAAkD;WAC3D,OAAO;GACd,KAAK,OAAO,MAAM,+CAA+C,MAAM;GACvE,MAAM,KAAK,YAAY;GACvB,MAAM;;;CAIV,MAAM,aAA4B;EAChC,KAAK,OAAO,IAAI,2CAA2C;EAC3D,MAAM,KAAK,WAAW,YAAY;EAClC,KAAK,YAAY;EACjB,KAAK,OAAO,IAAI,0CAA0C;;CAG5D,IAAI,MAAuB;EACzB,OAAO,KAAK;;CAGd,IAAI,cAAuB;EACzB,OAAO,KAAK,aAAa,KAAK,WAAW;;CAG3C,YAAoB,YAA4B;EAC9C,IAAI;GAEF,OAAO,IADS,IAAI,WACV,CAAC;UACL;GAEN,OAAO,WACJ,QAAQ,gBAAgB,GAAG,CAC3B,MAAM,IAAI,CAAC,GACX,MAAM,IAAI,CAAC;;;CAIlB,MAAM,cAAc,QAAkB,UAAiD;EACrF,OAAO,EAAE,SAAS,aAAa"}
@@ -1 +1 @@
1
- {"version":3,"file":"bambu.api.js","names":[],"sources":["../../src/services/bambu.api.ts"],"sourcesContent":["import {\n BambuType,\n FileDto,\n IPrinterApi,\n PartialReprintFileDto,\n PrinterType,\n ReprintState,\n UploadFileInput,\n uploadFileInputSchema,\n} from \"@/services/printer-api.interface\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { BambuClient } from \"@/services/bambu/bambu.client\";\nimport { BambuMqttAdapter } from \"@/services/bambu/bambu-mqtt.adapter\";\nimport { PrinterSocketStore } from \"@/state/printer-socket.store\";\nimport { AxiosPromise, AxiosResponse } from \"axios\";\nimport type { ServerConfigDto } from \"./moonraker/dto/server/server-config.dto\";\nimport type { SettingsDto } from \"./octoprint/dto/settings/settings.dto\";\nimport { statSync } from \"node:fs\";\n\nconst defaultLog = { adapter: \"bambu-lab\" };\n\n/**\n * Bambu Lab printer API implementation\n * Implements the IPrinterApi interface for fdm-monster integration\n *\n * Note: MQTT adapter is managed by PrinterSocketStore\n *\n * Credentials mapping:\n * - printerURL: Host IP address\n * - password: Access code (8-character authentication code)\n * - username: Serial number\n */\nexport class BambuApi implements IPrinterApi {\n logger: LoggerService;\n client: BambuClient;\n printerLogin: LoginDto;\n private readonly printerSocketStore: PrinterSocketStore;\n private printerId?: number;\n\n constructor(\n bambuClient: BambuClient,\n printerLogin: LoginDto,\n printerSocketStore: PrinterSocketStore,\n loggerFactory: ILoggerFactory,\n ) {\n this.logger = loggerFactory(BambuApi.name);\n this.client = bambuClient;\n this.printerLogin = printerLogin;\n this.printerSocketStore = printerSocketStore;\n this.logger.debug(\"Constructed Bambu API client\", this.logMeta());\n }\n\n /**\n * Set the printer ID for accessing the MQTT adapter\n */\n setPrinterId(printerId: number): void {\n this.printerId = printerId;\n }\n\n /**\n * Get the MQTT adapter from the printer socket store\n */\n private getMqttAdapter(): BambuMqttAdapter {\n if (!this.printerId) {\n throw new Error(\"Printer ID not set. Cannot access MQTT adapter.\");\n }\n\n const adapter = this.printerSocketStore.getPrinterSocket(this.printerId);\n if (!adapter) {\n throw new Error(`MQTT adapter not found for printer ${this.printerId}`);\n }\n\n if (!(adapter instanceof BambuMqttAdapter)) {\n throw new Error(`Adapter for printer ${this.printerId} is not a BambuMqttAdapter`);\n }\n\n return adapter;\n }\n\n /**\n * Ensure FTP is connected, auto-connect if needed\n */\n private async ensureFtpConnected(): Promise<void> {\n if (!this.client.isConnected) {\n this.logger.debug(\"FTP not connected, connecting automatically\");\n await this.client.connect(this.printerLogin);\n }\n }\n\n get type(): PrinterType {\n return BambuType;\n }\n\n set login(login: LoginDto) {\n this.printerLogin = login;\n }\n\n async getVersion(): Promise<string> {\n const response = await this.client.getApiVersion(this.printerLogin);\n return response.version;\n }\n\n async validateConnection(): Promise<void> {\n this.logger.debug(\"Validating Bambu connection\", this.logMeta());\n\n // Test FTP connectivity first\n try {\n await this.ensureFtpConnected();\n const files = await this.client.ftp.listFiles(\"/\");\n this.logger.debug(`FTP connection successful - found ${files.length} directories`, this.logMeta());\n } catch (ftpError) {\n this.logger.debug(`FTP validation failed: ${ftpError}`, this.logMeta());\n throw new Error(`Bambu FTP connection failed: ${ftpError}`);\n }\n }\n\n async connect(): Promise<void> {\n await this.client.connect(this.printerLogin);\n }\n\n async disconnect(): Promise<void> {\n await this.client.disconnect();\n }\n\n restartServer(): Promise<void> {\n this.logger.warn(\"restartServer not supported by Bambu Lab printers\");\n throw new Error(\"Method not supported\");\n }\n\n restartHost(): Promise<void> {\n this.logger.warn(\"restartHost not supported by Bambu Lab printers\");\n throw new Error(\"Method not supported\");\n }\n\n restartPrinterFirmware(): Promise<void> {\n this.logger.warn(\"restartPrinterFirmware not supported by Bambu Lab printers\");\n throw new Error(\"Method not supported\");\n }\n\n async startPrint(path: string): Promise<void> {\n this.logger.log(`Starting print: ${path}`, this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.startPrint(path);\n }\n\n async pausePrint(): Promise<void> {\n this.logger.log(\"Pausing print\", this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.pausePrint();\n }\n\n async resumePrint(): Promise<void> {\n this.logger.log(\"Resuming print\", this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.resumePrint();\n }\n\n async cancelPrint(): Promise<void> {\n this.logger.log(\"Canceling print\", this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.stopPrint();\n }\n\n async quickStop(): Promise<void> {\n this.logger.log(\"Quick stop (same as cancel for Bambu)\", this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.stopPrint();\n }\n\n async sendGcode(script: string): Promise<void> {\n this.logger.log(`Sending GCode: ${script}`, this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.sendGcode(script);\n }\n\n movePrintHead(amounts: { x?: number; y?: number; z?: number; speed?: number }): Promise<void> {\n this.logger.warn(\"movePrintHead not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n homeAxes(axes: { x?: boolean; y?: boolean; z?: boolean }): Promise<void> {\n this.logger.warn(\"homeAxes not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n async getFile(path: string): Promise<FileDto> {\n this.logger.debug(`Getting file info: ${path}`, this.logMeta());\n await this.ensureFtpConnected();\n const files = await this.client.ftp.listFiles(\"/\");\n\n const file = files.find((f) => f.name === path || f.name.endsWith(path));\n if (!file) {\n throw new Error(`File not found: ${path}`);\n }\n\n return {\n path: file.name,\n size: file.size,\n date: file.modifiedAt ? new Date(file.modifiedAt).getTime() : null,\n dir: file.isDirectory,\n };\n }\n\n async getFiles(recursive = false, startDir = \"/\") {\n if (recursive) {\n throw new Error(\"Recursive listing not supported for Bambu Lab printers\");\n }\n\n this.logger.debug(`Listing files from ${startDir}`, this.logMeta());\n await this.ensureFtpConnected();\n\n const items = await this.client.ftp.listFiles(startDir);\n\n const mapped = items.map((item) => {\n const fullPath = startDir === \"/\" ? `/${item.name}` : `${startDir}/${item.name}`;\n\n return {\n path: fullPath,\n size: item.size,\n date: item.modifiedAt ? new Date(item.modifiedAt).getTime() : null,\n dir: item.isDirectory,\n };\n });\n\n return {\n dirs: mapped.filter((i) => i.dir),\n files: mapped.filter((i) => !i.dir),\n };\n }\n\n async downloadFile(path: string): AxiosPromise<NodeJS.ReadableStream> {\n this.logger.log(`Downloading file via FTP: ${path}`, this.logMeta());\n\n await this.ensureFtpConnected();\n\n const { stream, tempPath } = await this.client.ftp.downloadFileAsStream(path);\n\n const stats = statSync(tempPath);\n const filename = path.split(\"/\").pop() || \"download\";\n\n return {\n data: stream,\n status: 200,\n statusText: \"OK\",\n headers: {\n \"content-type\": \"application/octet-stream\",\n \"content-length\": String(stats.size),\n \"content-disposition\": `attachment; filename=\"${filename}\"`,\n },\n config: {\n headers: {} as any,\n },\n };\n }\n\n getFileChunk(path: string, startBytes: number, endBytes: number): AxiosPromise<string> {\n this.logger.warn(\"getFileChunk not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n async uploadFile(input: UploadFileInput): Promise<void> {\n const validated = uploadFileInputSchema.parse(input);\n\n this.logger.log(`Uploading file: ${validated.fileName} (${validated.contentLength} bytes)`, this.logMeta());\n\n try {\n await this.ensureFtpConnected();\n await this.client.ftp.uploadFile(validated.stream, validated.fileName, validated.uploadToken);\n\n if (validated.startPrint) {\n this.logger.log(`Starting print after upload: ${validated.fileName}`, this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.startPrint(validated.fileName);\n }\n } catch (error) {\n this.logger.error(`Upload failed: ${(error as Error).message}`, this.logMeta());\n throw error;\n }\n }\n\n async deleteFile(path: string): Promise<void> {\n this.logger.log(`Deleting file: ${path}`, this.logMeta());\n await this.ensureFtpConnected();\n await this.client.ftp.deleteFile(path);\n }\n\n deleteFolder(path: string): Promise<void> {\n this.logger.warn(\"deleteFolder not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n getSettings(): Promise<ServerConfigDto | SettingsDto> {\n this.logger.warn(\"getSettings not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n async getReprintState(): Promise<PartialReprintFileDto> {\n const mqttAdapter = this.getMqttAdapter();\n const state = mqttAdapter.getLastState();\n\n if (!state) {\n return {\n reprintState: ReprintState.PrinterNotAvailable,\n connectionState: null,\n };\n }\n\n const lastFile = state.gcode_file;\n\n if (!lastFile) {\n return {\n reprintState: ReprintState.NoLastPrint,\n connectionState: null,\n };\n }\n\n return {\n file: {\n path: lastFile,\n size: -1,\n date: null,\n dir: false,\n },\n reprintState: ReprintState.LastPrintReady,\n connectionState: null,\n };\n }\n\n private logMeta() {\n return defaultLog;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bambu.api.js","names":[],"sources":["../../src/services/bambu.api.ts"],"sourcesContent":["import {\n BambuType,\n FileDto,\n IPrinterApi,\n PartialReprintFileDto,\n PrinterType,\n ReprintState,\n UploadFileInput,\n uploadFileInputSchema,\n} from \"@/services/printer-api.interface\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { BambuClient } from \"@/services/bambu/bambu.client\";\nimport { BambuMqttAdapter } from \"@/services/bambu/bambu-mqtt.adapter\";\nimport { PrinterSocketStore } from \"@/state/printer-socket.store\";\nimport { AxiosPromise, AxiosResponse } from \"axios\";\nimport type { ServerConfigDto } from \"./moonraker/dto/server/server-config.dto\";\nimport type { SettingsDto } from \"./octoprint/dto/settings/settings.dto\";\nimport { statSync } from \"node:fs\";\n\nconst defaultLog = { adapter: \"bambu-lab\" };\n\n/**\n * Bambu Lab printer API implementation\n * Implements the IPrinterApi interface for fdm-monster integration\n *\n * Note: MQTT adapter is managed by PrinterSocketStore\n *\n * Credentials mapping:\n * - printerURL: Host IP address\n * - password: Access code (8-character authentication code)\n * - username: Serial number\n */\nexport class BambuApi implements IPrinterApi {\n logger: LoggerService;\n client: BambuClient;\n printerLogin: LoginDto;\n private readonly printerSocketStore: PrinterSocketStore;\n private printerId?: number;\n\n constructor(\n bambuClient: BambuClient,\n printerLogin: LoginDto,\n printerSocketStore: PrinterSocketStore,\n loggerFactory: ILoggerFactory,\n ) {\n this.logger = loggerFactory(BambuApi.name);\n this.client = bambuClient;\n this.printerLogin = printerLogin;\n this.printerSocketStore = printerSocketStore;\n this.logger.debug(\"Constructed Bambu API client\", this.logMeta());\n }\n\n /**\n * Set the printer ID for accessing the MQTT adapter\n */\n setPrinterId(printerId: number): void {\n this.printerId = printerId;\n }\n\n /**\n * Get the MQTT adapter from the printer socket store\n */\n private getMqttAdapter(): BambuMqttAdapter {\n if (!this.printerId) {\n throw new Error(\"Printer ID not set. Cannot access MQTT adapter.\");\n }\n\n const adapter = this.printerSocketStore.getPrinterSocket(this.printerId);\n if (!adapter) {\n throw new Error(`MQTT adapter not found for printer ${this.printerId}`);\n }\n\n if (!(adapter instanceof BambuMqttAdapter)) {\n throw new Error(`Adapter for printer ${this.printerId} is not a BambuMqttAdapter`);\n }\n\n return adapter;\n }\n\n /**\n * Ensure FTP is connected, auto-connect if needed\n */\n private async ensureFtpConnected(): Promise<void> {\n if (!this.client.isConnected) {\n this.logger.debug(\"FTP not connected, connecting automatically\");\n await this.client.connect(this.printerLogin);\n }\n }\n\n get type(): PrinterType {\n return BambuType;\n }\n\n set login(login: LoginDto) {\n this.printerLogin = login;\n }\n\n async getVersion(): Promise<string> {\n const response = await this.client.getApiVersion(this.printerLogin);\n return response.version;\n }\n\n async validateConnection(): Promise<void> {\n this.logger.debug(\"Validating Bambu connection\", this.logMeta());\n\n // Test FTP connectivity first\n try {\n await this.ensureFtpConnected();\n const files = await this.client.ftp.listFiles(\"/\");\n this.logger.debug(`FTP connection successful - found ${files.length} directories`, this.logMeta());\n } catch (ftpError) {\n this.logger.debug(`FTP validation failed: ${ftpError}`, this.logMeta());\n throw new Error(`Bambu FTP connection failed: ${ftpError}`);\n }\n }\n\n async connect(): Promise<void> {\n await this.client.connect(this.printerLogin);\n }\n\n async disconnect(): Promise<void> {\n await this.client.disconnect();\n }\n\n restartServer(): Promise<void> {\n this.logger.warn(\"restartServer not supported by Bambu Lab printers\");\n throw new Error(\"Method not supported\");\n }\n\n restartHost(): Promise<void> {\n this.logger.warn(\"restartHost not supported by Bambu Lab printers\");\n throw new Error(\"Method not supported\");\n }\n\n restartPrinterFirmware(): Promise<void> {\n this.logger.warn(\"restartPrinterFirmware not supported by Bambu Lab printers\");\n throw new Error(\"Method not supported\");\n }\n\n async startPrint(path: string): Promise<void> {\n this.logger.log(`Starting print: ${path}`, this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.startPrint(path);\n }\n\n async pausePrint(): Promise<void> {\n this.logger.log(\"Pausing print\", this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.pausePrint();\n }\n\n async resumePrint(): Promise<void> {\n this.logger.log(\"Resuming print\", this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.resumePrint();\n }\n\n async cancelPrint(): Promise<void> {\n this.logger.log(\"Canceling print\", this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.stopPrint();\n }\n\n async quickStop(): Promise<void> {\n this.logger.log(\"Quick stop (same as cancel for Bambu)\", this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.stopPrint();\n }\n\n async sendGcode(script: string): Promise<void> {\n this.logger.log(`Sending GCode: ${script}`, this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.sendGcode(script);\n }\n\n movePrintHead(amounts: { x?: number; y?: number; z?: number; speed?: number }): Promise<void> {\n this.logger.warn(\"movePrintHead not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n homeAxes(axes: { x?: boolean; y?: boolean; z?: boolean }): Promise<void> {\n this.logger.warn(\"homeAxes not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n async getFile(path: string): Promise<FileDto> {\n this.logger.debug(`Getting file info: ${path}`, this.logMeta());\n await this.ensureFtpConnected();\n const files = await this.client.ftp.listFiles(\"/\");\n\n const file = files.find((f) => f.name === path || f.name.endsWith(path));\n if (!file) {\n throw new Error(`File not found: ${path}`);\n }\n\n return {\n path: file.name,\n size: file.size,\n date: file.modifiedAt ? new Date(file.modifiedAt).getTime() : null,\n dir: file.isDirectory,\n };\n }\n\n async getFiles(recursive = false, startDir = \"/\") {\n if (recursive) {\n throw new Error(\"Recursive listing not supported for Bambu Lab printers\");\n }\n\n this.logger.debug(`Listing files from ${startDir}`, this.logMeta());\n await this.ensureFtpConnected();\n\n const items = await this.client.ftp.listFiles(startDir);\n\n const mapped = items.map((item) => {\n const fullPath = startDir === \"/\" ? `/${item.name}` : `${startDir}/${item.name}`;\n\n return {\n path: fullPath,\n size: item.size,\n date: item.modifiedAt ? new Date(item.modifiedAt).getTime() : null,\n dir: item.isDirectory,\n };\n });\n\n return {\n dirs: mapped.filter((i) => i.dir),\n files: mapped.filter((i) => !i.dir),\n };\n }\n\n async downloadFile(path: string): AxiosPromise<NodeJS.ReadableStream> {\n this.logger.log(`Downloading file via FTP: ${path}`, this.logMeta());\n\n await this.ensureFtpConnected();\n\n const { stream, tempPath } = await this.client.ftp.downloadFileAsStream(path);\n\n const stats = statSync(tempPath);\n const filename = path.split(\"/\").pop() || \"download\";\n\n return {\n data: stream,\n status: 200,\n statusText: \"OK\",\n headers: {\n \"content-type\": \"application/octet-stream\",\n \"content-length\": String(stats.size),\n \"content-disposition\": `attachment; filename=\"${filename}\"`,\n },\n config: {\n headers: {} as any,\n },\n };\n }\n\n getFileChunk(path: string, startBytes: number, endBytes: number): AxiosPromise<string> {\n this.logger.warn(\"getFileChunk not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n async uploadFile(input: UploadFileInput): Promise<void> {\n const validated = uploadFileInputSchema.parse(input);\n\n this.logger.log(`Uploading file: ${validated.fileName} (${validated.contentLength} bytes)`, this.logMeta());\n\n try {\n await this.ensureFtpConnected();\n await this.client.ftp.uploadFile(validated.stream, validated.fileName, validated.uploadToken);\n\n if (validated.startPrint) {\n this.logger.log(`Starting print after upload: ${validated.fileName}`, this.logMeta());\n const mqttAdapter = this.getMqttAdapter();\n await mqttAdapter.startPrint(validated.fileName);\n }\n } catch (error) {\n this.logger.error(`Upload failed: ${(error as Error).message}`, this.logMeta());\n throw error;\n }\n }\n\n async deleteFile(path: string): Promise<void> {\n this.logger.log(`Deleting file: ${path}`, this.logMeta());\n await this.ensureFtpConnected();\n await this.client.ftp.deleteFile(path);\n }\n\n deleteFolder(path: string): Promise<void> {\n this.logger.warn(\"deleteFolder not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n getSettings(): Promise<ServerConfigDto | SettingsDto> {\n this.logger.warn(\"getSettings not implemented for Bambu Lab printers\");\n throw new Error(\"Method not implemented\");\n }\n\n async getReprintState(): Promise<PartialReprintFileDto> {\n const mqttAdapter = this.getMqttAdapter();\n const state = mqttAdapter.getLastState();\n\n if (!state) {\n return {\n reprintState: ReprintState.PrinterNotAvailable,\n connectionState: null,\n };\n }\n\n const lastFile = state.gcode_file;\n\n if (!lastFile) {\n return {\n reprintState: ReprintState.NoLastPrint,\n connectionState: null,\n };\n }\n\n return {\n file: {\n path: lastFile,\n size: -1,\n date: null,\n dir: false,\n },\n reprintState: ReprintState.LastPrintReady,\n connectionState: null,\n };\n }\n\n private logMeta() {\n return defaultLog;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"batch-call.service.js","names":[],"sources":["../../../src/services/core/batch-call.service.ts"],"sourcesContent":["import { PrinterSocketStore } from \"@/state/printer-socket.store\";\nimport { PrinterCache } from \"@/state/printer.cache\";\nimport type { IPrinterService } from \"@/services/interfaces/printer.service.interface\";\nimport { captureException } from \"@sentry/node\";\nimport { errorSummary } from \"@/utils/error.utils\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { PrinterApiFactory } from \"@/services/printer-api.factory\";\nimport { ReprintState, type IPrinterApi, type ReprintFileDto } from \"@/services/printer-api.interface\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\n\ninterface BatchSingletonModel {\n success?: boolean;\n failure?: boolean;\n printerId: number;\n time: number;\n error?: string;\n}\n\ntype BatchModel = Array<BatchSingletonModel>;\n\nexport class BatchCallService {\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly printerApiFactory: PrinterApiFactory,\n private readonly printerCache: PrinterCache,\n private readonly printerSocketStore: PrinterSocketStore,\n private readonly printerService: IPrinterService,\n ) {\n this.logger = loggerFactory(BatchCallService.name);\n }\n\n getPrinter(login: LoginDto): IPrinterApi {\n return this.printerApiFactory.getScopedPrinter(login);\n }\n\n async batchTogglePrintersEnabled(\n printerIds: number[],\n enabled: boolean,\n ): Promise<\n {\n failure?: boolean;\n error?: any;\n success?: boolean;\n printerId: number;\n time: number;\n }[]\n > {\n const promises = [];\n for (const printerId of printerIds) {\n let promise: Promise<any> | undefined = undefined;\n const printerDto = await this.printerCache.getValue(printerId);\n if (!printerDto) continue;\n\n const time = Date.now();\n if (enabled) {\n // If disabled, but not in maintenance, enable the printer\n if (!printerDto.enabled && !printerDto.disabledReason?.length) {\n promise = this.printerService\n .updateEnabled(printerId, true)\n .then(() => {\n return { success: true, printerId, time: Date.now() - time };\n })\n .catch((e) => {\n return { failure: true, error: e.message, printerId, time: Date.now() - time };\n });\n }\n } else if (printerDto.enabled) {\n // If enabled, disable the printer\n promise = this.printerService\n .updateEnabled(printerId, false)\n .then(() => {\n return { success: true, printerId, time: Date.now() - time };\n })\n .catch((e) => {\n return { failure: true, error: e.message, printerId, time: Date.now() - time };\n });\n } else {\n this.logger.warn(\"Did not toggle printer enabled, its in maintenance\");\n }\n\n if (promise) {\n promises.push(promise);\n }\n }\n\n return await Promise.all(promises);\n }\n\n batchConnectSocket(printerIds: number[]): void {\n for (const printerId of printerIds) {\n try {\n this.printerSocketStore.reconnectPrinterAdapter(printerId);\n } catch (e) {\n captureException(e);\n this.logger.error(`Error setting socket to reconnect ${errorSummary(e)}`);\n }\n }\n }\n\n async batchConnectUsb(printerIds: number[]): Promise<Awaited<BatchModel>> {\n const promises = [];\n for (const printerId of printerIds) {\n const login = await this.printerCache.getLoginDtoAsync(printerId);\n const time = Date.now();\n\n const client = this.getPrinter(login);\n const promise = client\n .connect()\n .then(() => {\n return { success: true, printerId, time: Date.now() - time };\n })\n .catch((e) => {\n return { failure: true, error: e.message, printerId, time: Date.now() - time };\n });\n\n promises.push(promise);\n }\n return await Promise.all(promises);\n }\n\n async getBatchPrinterReprintFile(printerIds: number[]): Promise<ReprintFileDto[]> {\n const promises = [];\n for (const printerId of printerIds) {\n const promise = new Promise<ReprintFileDto>(async (resolve, _) => {\n try {\n const login = await this.printerCache.getLoginDtoAsync(printerId);\n const client = this.getPrinter(login);\n const partialReprintState = await client.getReprintState();\n\n return resolve({\n ...partialReprintState,\n printerId,\n });\n } catch (e) {\n captureException(e);\n return resolve({\n connectionState: null,\n printerId,\n reprintState: ReprintState.PrinterNotAvailable,\n });\n }\n });\n promises.push(promise);\n }\n\n return await Promise.all(promises);\n }\n\n async batchReprintCalls(printerIdFileList: { printerId: number; path: string }[]): Promise<Awaited<BatchModel>> {\n const promises = [];\n for (const printerIdFile of printerIdFileList) {\n const { printerId, path } = printerIdFile;\n const login = await this.printerCache.getLoginDtoAsync(printerId);\n\n const time = Date.now();\n const client = this.getPrinter(login);\n const promise = client\n .startPrint(path)\n .then(() => {\n return { success: true, printerId, time: Date.now() - time };\n })\n .catch((e) => {\n captureException(e);\n return { failure: true, error: e.message, printerId, time: Date.now() - time };\n });\n\n promises.push(promise);\n }\n return await Promise.all(promises);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"batch-call.service.js","names":[],"sources":["../../../src/services/core/batch-call.service.ts"],"sourcesContent":["import { PrinterSocketStore } from \"@/state/printer-socket.store\";\nimport { PrinterCache } from \"@/state/printer.cache\";\nimport type { IPrinterService } from \"@/services/interfaces/printer.service.interface\";\nimport { captureException } from \"@sentry/node\";\nimport { errorSummary } from \"@/utils/error.utils\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { PrinterApiFactory } from \"@/services/printer-api.factory\";\nimport { ReprintState, type IPrinterApi, type ReprintFileDto } from \"@/services/printer-api.interface\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\n\ninterface BatchSingletonModel {\n success?: boolean;\n failure?: boolean;\n printerId: number;\n time: number;\n error?: string;\n}\n\ntype BatchModel = Array<BatchSingletonModel>;\n\nexport class BatchCallService {\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly printerApiFactory: PrinterApiFactory,\n private readonly printerCache: PrinterCache,\n private readonly printerSocketStore: PrinterSocketStore,\n private readonly printerService: IPrinterService,\n ) {\n this.logger = loggerFactory(BatchCallService.name);\n }\n\n getPrinter(login: LoginDto): IPrinterApi {\n return this.printerApiFactory.getScopedPrinter(login);\n }\n\n async batchTogglePrintersEnabled(\n printerIds: number[],\n enabled: boolean,\n ): Promise<\n {\n failure?: boolean;\n error?: any;\n success?: boolean;\n printerId: number;\n time: number;\n }[]\n > {\n const promises = [];\n for (const printerId of printerIds) {\n let promise: Promise<any> | undefined = undefined;\n const printerDto = await this.printerCache.getValue(printerId);\n if (!printerDto) continue;\n\n const time = Date.now();\n if (enabled) {\n // If disabled, but not in maintenance, enable the printer\n if (!printerDto.enabled && !printerDto.disabledReason?.length) {\n promise = this.printerService\n .updateEnabled(printerId, true)\n .then(() => {\n return { success: true, printerId, time: Date.now() - time };\n })\n .catch((e) => {\n return { failure: true, error: e.message, printerId, time: Date.now() - time };\n });\n }\n } else if (printerDto.enabled) {\n // If enabled, disable the printer\n promise = this.printerService\n .updateEnabled(printerId, false)\n .then(() => {\n return { success: true, printerId, time: Date.now() - time };\n })\n .catch((e) => {\n return { failure: true, error: e.message, printerId, time: Date.now() - time };\n });\n } else {\n this.logger.warn(\"Did not toggle printer enabled, its in maintenance\");\n }\n\n if (promise) {\n promises.push(promise);\n }\n }\n\n return await Promise.all(promises);\n }\n\n batchConnectSocket(printerIds: number[]): void {\n for (const printerId of printerIds) {\n try {\n this.printerSocketStore.reconnectPrinterAdapter(printerId);\n } catch (e) {\n captureException(e);\n this.logger.error(`Error setting socket to reconnect ${errorSummary(e)}`);\n }\n }\n }\n\n async batchConnectUsb(printerIds: number[]): Promise<Awaited<BatchModel>> {\n const promises = [];\n for (const printerId of printerIds) {\n const login = await this.printerCache.getLoginDtoAsync(printerId);\n const time = Date.now();\n\n const client = this.getPrinter(login);\n const promise = client\n .connect()\n .then(() => {\n return { success: true, printerId, time: Date.now() - time };\n })\n .catch((e) => {\n return { failure: true, error: e.message, printerId, time: Date.now() - time };\n });\n\n promises.push(promise);\n }\n return await Promise.all(promises);\n }\n\n async getBatchPrinterReprintFile(printerIds: number[]): Promise<ReprintFileDto[]> {\n const promises = [];\n for (const printerId of printerIds) {\n const promise = new Promise<ReprintFileDto>(async (resolve, _) => {\n try {\n const login = await this.printerCache.getLoginDtoAsync(printerId);\n const client = this.getPrinter(login);\n const partialReprintState = await client.getReprintState();\n\n return resolve({\n ...partialReprintState,\n printerId,\n });\n } catch (e) {\n captureException(e);\n return resolve({\n connectionState: null,\n printerId,\n reprintState: ReprintState.PrinterNotAvailable,\n });\n }\n });\n promises.push(promise);\n }\n\n return await Promise.all(promises);\n }\n\n async batchReprintCalls(printerIdFileList: { printerId: number; path: string }[]): Promise<Awaited<BatchModel>> {\n const promises = [];\n for (const printerIdFile of printerIdFileList) {\n const { printerId, path } = printerIdFile;\n const login = await this.printerCache.getLoginDtoAsync(printerId);\n\n const time = Date.now();\n const client = this.getPrinter(login);\n const promise = client\n .startPrint(path)\n .then(() => {\n return { success: true, printerId, time: Date.now() - time };\n })\n .catch((e) => {\n captureException(e);\n return { failure: true, error: e.message, printerId, time: Date.now() - time };\n });\n\n promises.push(promise);\n }\n return await Promise.all(promises);\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"client-bundle.service.js","names":[],"sources":["../../../src/services/core/client-bundle.service.ts"],"sourcesContent":["import AdmZip from \"adm-zip\";\nimport { join } from \"node:path\";\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { readdir, rm } from \"node:fs/promises\";\nimport { ensureDirExists, getMediaPath } from \"@/utils/fs.utils\";\nimport { checkVersionSatisfiesMinimum, getMaximumOfVersionsSafe } from \"@/utils/semver.utils\";\nimport { AppConstants } from \"@/server.constants\";\nimport { GithubService } from \"@/services/core/github.service\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { compare } from \"semver\";\nimport { errorSummary } from \"@/utils/error.utils\";\nimport { ExternalServiceError, InternalServerException, NotFoundException } from \"@/exceptions/runtime.exceptions\";\nimport { RequestError } from \"octokit\";\n\ntype UpdateResponse = {\n shouldUpdate: boolean;\n requestedVersion: string | undefined;\n currentVersion: string | null;\n minimumVersion: string;\n targetVersion: string | null;\n reason: string;\n};\n\nexport class ClientBundleService {\n private readonly logger: LoggerService;\n private readonly githubOwner = AppConstants.orgName;\n private readonly githubRepo = AppConstants.clientRepoName;\n private readonly minVersion = AppConstants.defaultClientMinimum;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly githubService: GithubService,\n ) {\n this.logger = loggerFactory(ClientBundleService.name);\n }\n\n get clientPackageJsonPath() {\n return join(getMediaPath(), AppConstants.defaultClientBundleStorage, \"package.json\");\n }\n\n get clientIndexHtmlPath() {\n return join(getMediaPath(), AppConstants.defaultClientBundleStorage, \"dist/index.html\");\n }\n\n async getReleases() {\n try {\n const [releases, latestRelease] = await Promise.all([\n this.githubService.getReleases(this.githubOwner, this.githubRepo),\n this.githubService.getLatestRelease(this.githubOwner, this.githubRepo),\n ]);\n\n return {\n minimum: { tag_name: this.minVersion },\n current: { tag_name: this.getClientVersion() },\n latest: latestRelease.data,\n releases: releases.data,\n };\n } catch (e) {\n if (e instanceof RequestError) {\n this.logger.error(`Github OctoKit error ${errorSummary(e)}`);\n throw new ExternalServiceError({ error: \"Github OctoKit error: \" + e.message }, \"GitHub\");\n }\n throw new InternalServerException(\"Something went wrong with the request to Github\");\n }\n }\n\n async shouldUpdateWithReason(\n overrideAutoUpdate?: boolean,\n minimumVersion?: string,\n requestedVersion?: string,\n allowDowngrade?: boolean,\n ): Promise<UpdateResponse> {\n const clientAutoUpdate = AppConstants.enableClientDistAutoUpdateKey;\n const existingVersion = this.getClientVersion();\n minimumVersion ??= this.minVersion;\n\n // Auto-update check\n if (!clientAutoUpdate && !overrideAutoUpdate) {\n return this.createResponse(\n false,\n existingVersion,\n minimumVersion,\n requestedVersion,\n null,\n \"Client auto-update disabled, skipping\",\n );\n }\n\n // Client files existence check\n if (!existingVersion || !this.doesClientIndexHtmlExist()) {\n const reason = existingVersion ? \"Client index.html could not be found\" : \"Client package.json does not exist\";\n\n return this.createResponse(\n true,\n existingVersion,\n minimumVersion,\n requestedVersion,\n getMaximumOfVersionsSafe(minimumVersion, requestedVersion),\n `${reason}, downloading new release`,\n );\n }\n\n // Minimum version check\n const meetsMinimum = checkVersionSatisfiesMinimum(existingVersion, minimumVersion);\n if (!meetsMinimum) {\n return this.createResponse(\n true,\n existingVersion,\n minimumVersion,\n requestedVersion,\n getMaximumOfVersionsSafe(minimumVersion, requestedVersion),\n `Client version ${existingVersion} below minimum ${minimumVersion}, downloading new release`,\n );\n }\n\n // Handle requested version scenarios\n if (requestedVersion) {\n return this.evaluateRequestedVersion(existingVersion, minimumVersion, requestedVersion, allowDowngrade);\n }\n\n // Default case - already up to date\n return this.createResponse(\n false,\n existingVersion,\n minimumVersion,\n requestedVersion,\n getMaximumOfVersionsSafe(minimumVersion, requestedVersion),\n `Client already satisfies minimum version ${minimumVersion}`,\n );\n }\n\n async downloadClientUpdate(releaseTag: string): Promise<string> {\n // Get release info\n const result = await this.githubService.getReleaseByTag(this.githubOwner, this.githubRepo, releaseTag);\n const release = result.data;\n\n // Find asset\n const assetName = `dist-client-${release.tag_name}.zip`;\n const asset = release.assets.find((a) => a.name === assetName);\n if (!asset) {\n throw new NotFoundException(`Asset ${assetName} not found in release ${release.tag_name}`);\n }\n\n // Download and extract\n const zipPath = await this.downloadZip(asset.id, asset.name);\n await this.extractZip(zipPath);\n\n return release.tag_name;\n }\n\n private evaluateRequestedVersion(\n currentVersion: string,\n minimumVersion: string,\n requestedVersion: string,\n allowDowngrade?: boolean,\n ): UpdateResponse {\n // Check if requested version meets minimum\n if (compare(requestedVersion, minimumVersion) === -1) {\n return this.createResponse(\n false,\n currentVersion,\n minimumVersion,\n requestedVersion,\n requestedVersion,\n `Requested version ${requestedVersion} below minimum ${minimumVersion}, skipping`,\n );\n }\n\n // Compare with current version\n const versionComparison = compare(requestedVersion, currentVersion);\n\n if (versionComparison === 0) {\n // Same version\n return this.createResponse(\n false,\n currentVersion,\n minimumVersion,\n requestedVersion,\n requestedVersion,\n `Requested version ${requestedVersion} same as current, skipping`,\n );\n } else if (versionComparison === -1) {\n // Downgrade\n return this.createResponse(\n !!allowDowngrade,\n currentVersion,\n minimumVersion,\n requestedVersion,\n requestedVersion,\n allowDowngrade\n ? `Downgrading to ${requestedVersion} (above minimum ${minimumVersion})`\n : `Downgrade not allowed, skipping`,\n );\n } else {\n // Upgrade\n return this.createResponse(\n true,\n currentVersion,\n minimumVersion,\n requestedVersion,\n requestedVersion,\n `Upgrading from ${currentVersion} to ${requestedVersion}`,\n );\n }\n }\n\n private createResponse(\n shouldUpdate: boolean,\n currentVersion: string | null,\n minimumVersion: string,\n requestedVersion: string | undefined,\n targetVersion: string | null,\n reason: string,\n ): UpdateResponse {\n return {\n shouldUpdate,\n requestedVersion,\n currentVersion,\n minimumVersion,\n targetVersion,\n reason,\n };\n }\n\n private async downloadZip(assetId: number, assetName: string): Promise<string> {\n const assetResult = await this.githubService.requestAsset(this.githubOwner, this.githubRepo, assetId);\n\n const distZipPath = join(getMediaPath(), AppConstants.defaultClientBundleZipsStorage);\n ensureDirExists(distZipPath);\n\n const path = join(distZipPath, assetName);\n writeFileSync(path, Buffer.from(assetResult.data));\n this.logger.log(`Downloaded client ZIP to ${distZipPath}`);\n\n return path;\n }\n\n private async extractZip(zipPath: string): Promise<void> {\n const distPath = join(getMediaPath(), AppConstants.defaultClientBundleStorage);\n ensureDirExists(distPath);\n\n // Clear the directory\n this.logger.debug(`Clearing contents of ${distPath}`);\n for (const item of await readdir(distPath)) {\n const itemPath = join(distPath, item);\n await rm(itemPath, { force: true, recursive: true }).catch((e) =>\n this.logger.error(`Failed to remove ${itemPath}: ${e.message}`),\n );\n }\n\n // Extract the zip\n try {\n const zip = new AdmZip(zipPath);\n zip.extractAllTo(distPath);\n this.logger.log(`Successfully extracted client to ${distPath}`);\n } catch (e: any) {\n this.logger.error(`Extraction failed: ${e.message}`);\n throw e;\n }\n }\n\n private doesClientIndexHtmlExist(): boolean {\n return existsSync(this.clientIndexHtmlPath);\n }\n\n private getClientVersion(): string | null {\n const path = this.clientPackageJsonPath;\n if (!existsSync(path)) {\n return null;\n }\n\n try {\n const json = JSON.parse(readFileSync(path, \"utf-8\"));\n return json?.version ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client-bundle.service.js","names":[],"sources":["../../../src/services/core/client-bundle.service.ts"],"sourcesContent":["import AdmZip from \"adm-zip\";\nimport { join } from \"node:path\";\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { readdir, rm } from \"node:fs/promises\";\nimport { ensureDirExists, getMediaPath } from \"@/utils/fs.utils\";\nimport { checkVersionSatisfiesMinimum, getMaximumOfVersionsSafe } from \"@/utils/semver.utils\";\nimport { AppConstants } from \"@/server.constants\";\nimport { GithubService } from \"@/services/core/github.service\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { compare } from \"semver\";\nimport { errorSummary } from \"@/utils/error.utils\";\nimport { ExternalServiceError, InternalServerException, NotFoundException } from \"@/exceptions/runtime.exceptions\";\nimport { RequestError } from \"octokit\";\n\ntype UpdateResponse = {\n shouldUpdate: boolean;\n requestedVersion: string | undefined;\n currentVersion: string | null;\n minimumVersion: string;\n targetVersion: string | null;\n reason: string;\n};\n\nexport class ClientBundleService {\n private readonly logger: LoggerService;\n private readonly githubOwner = AppConstants.orgName;\n private readonly githubRepo = AppConstants.clientRepoName;\n private readonly minVersion = AppConstants.defaultClientMinimum;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly githubService: GithubService,\n ) {\n this.logger = loggerFactory(ClientBundleService.name);\n }\n\n get clientPackageJsonPath() {\n return join(getMediaPath(), AppConstants.defaultClientBundleStorage, \"package.json\");\n }\n\n get clientIndexHtmlPath() {\n return join(getMediaPath(), AppConstants.defaultClientBundleStorage, \"dist/index.html\");\n }\n\n async getReleases() {\n try {\n const [releases, latestRelease] = await Promise.all([\n this.githubService.getReleases(this.githubOwner, this.githubRepo),\n this.githubService.getLatestRelease(this.githubOwner, this.githubRepo),\n ]);\n\n return {\n minimum: { tag_name: this.minVersion },\n current: { tag_name: this.getClientVersion() },\n latest: latestRelease.data,\n releases: releases.data,\n };\n } catch (e) {\n if (e instanceof RequestError) {\n this.logger.error(`Github OctoKit error ${errorSummary(e)}`);\n throw new ExternalServiceError({ error: \"Github OctoKit error: \" + e.message }, \"GitHub\");\n }\n throw new InternalServerException(\"Something went wrong with the request to Github\");\n }\n }\n\n async shouldUpdateWithReason(\n overrideAutoUpdate?: boolean,\n minimumVersion?: string,\n requestedVersion?: string,\n allowDowngrade?: boolean,\n ): Promise<UpdateResponse> {\n const clientAutoUpdate = AppConstants.enableClientDistAutoUpdateKey;\n const existingVersion = this.getClientVersion();\n minimumVersion ??= this.minVersion;\n\n // Auto-update check\n if (!clientAutoUpdate && !overrideAutoUpdate) {\n return this.createResponse(\n false,\n existingVersion,\n minimumVersion,\n requestedVersion,\n null,\n \"Client auto-update disabled, skipping\",\n );\n }\n\n // Client files existence check\n if (!existingVersion || !this.doesClientIndexHtmlExist()) {\n const reason = existingVersion ? \"Client index.html could not be found\" : \"Client package.json does not exist\";\n\n return this.createResponse(\n true,\n existingVersion,\n minimumVersion,\n requestedVersion,\n getMaximumOfVersionsSafe(minimumVersion, requestedVersion),\n `${reason}, downloading new release`,\n );\n }\n\n // Minimum version check\n const meetsMinimum = checkVersionSatisfiesMinimum(existingVersion, minimumVersion);\n if (!meetsMinimum) {\n return this.createResponse(\n true,\n existingVersion,\n minimumVersion,\n requestedVersion,\n getMaximumOfVersionsSafe(minimumVersion, requestedVersion),\n `Client version ${existingVersion} below minimum ${minimumVersion}, downloading new release`,\n );\n }\n\n // Handle requested version scenarios\n if (requestedVersion) {\n return this.evaluateRequestedVersion(existingVersion, minimumVersion, requestedVersion, allowDowngrade);\n }\n\n // Default case - already up to date\n return this.createResponse(\n false,\n existingVersion,\n minimumVersion,\n requestedVersion,\n getMaximumOfVersionsSafe(minimumVersion, requestedVersion),\n `Client already satisfies minimum version ${minimumVersion}`,\n );\n }\n\n async downloadClientUpdate(releaseTag: string): Promise<string> {\n // Get release info\n const result = await this.githubService.getReleaseByTag(this.githubOwner, this.githubRepo, releaseTag);\n const release = result.data;\n\n // Find asset\n const assetName = `dist-client-${release.tag_name}.zip`;\n const asset = release.assets.find((a) => a.name === assetName);\n if (!asset) {\n throw new NotFoundException(`Asset ${assetName} not found in release ${release.tag_name}`);\n }\n\n // Download and extract\n const zipPath = await this.downloadZip(asset.id, asset.name);\n await this.extractZip(zipPath);\n\n return release.tag_name;\n }\n\n private evaluateRequestedVersion(\n currentVersion: string,\n minimumVersion: string,\n requestedVersion: string,\n allowDowngrade?: boolean,\n ): UpdateResponse {\n // Check if requested version meets minimum\n if (compare(requestedVersion, minimumVersion) === -1) {\n return this.createResponse(\n false,\n currentVersion,\n minimumVersion,\n requestedVersion,\n requestedVersion,\n `Requested version ${requestedVersion} below minimum ${minimumVersion}, skipping`,\n );\n }\n\n // Compare with current version\n const versionComparison = compare(requestedVersion, currentVersion);\n\n if (versionComparison === 0) {\n // Same version\n return this.createResponse(\n false,\n currentVersion,\n minimumVersion,\n requestedVersion,\n requestedVersion,\n `Requested version ${requestedVersion} same as current, skipping`,\n );\n } else if (versionComparison === -1) {\n // Downgrade\n return this.createResponse(\n !!allowDowngrade,\n currentVersion,\n minimumVersion,\n requestedVersion,\n requestedVersion,\n allowDowngrade\n ? `Downgrading to ${requestedVersion} (above minimum ${minimumVersion})`\n : `Downgrade not allowed, skipping`,\n );\n } else {\n // Upgrade\n return this.createResponse(\n true,\n currentVersion,\n minimumVersion,\n requestedVersion,\n requestedVersion,\n `Upgrading from ${currentVersion} to ${requestedVersion}`,\n );\n }\n }\n\n private createResponse(\n shouldUpdate: boolean,\n currentVersion: string | null,\n minimumVersion: string,\n requestedVersion: string | undefined,\n targetVersion: string | null,\n reason: string,\n ): UpdateResponse {\n return {\n shouldUpdate,\n requestedVersion,\n currentVersion,\n minimumVersion,\n targetVersion,\n reason,\n };\n }\n\n private async downloadZip(assetId: number, assetName: string): Promise<string> {\n const assetResult = await this.githubService.requestAsset(this.githubOwner, this.githubRepo, assetId);\n\n const distZipPath = join(getMediaPath(), AppConstants.defaultClientBundleZipsStorage);\n ensureDirExists(distZipPath);\n\n const path = join(distZipPath, assetName);\n writeFileSync(path, Buffer.from(assetResult.data));\n this.logger.log(`Downloaded client ZIP to ${distZipPath}`);\n\n return path;\n }\n\n private async extractZip(zipPath: string): Promise<void> {\n const distPath = join(getMediaPath(), AppConstants.defaultClientBundleStorage);\n ensureDirExists(distPath);\n\n // Clear the directory\n this.logger.debug(`Clearing contents of ${distPath}`);\n for (const item of await readdir(distPath)) {\n const itemPath = join(distPath, item);\n await rm(itemPath, { force: true, recursive: true }).catch((e) =>\n this.logger.error(`Failed to remove ${itemPath}: ${e.message}`),\n );\n }\n\n // Extract the zip\n try {\n const zip = new AdmZip(zipPath);\n zip.extractAllTo(distPath);\n this.logger.log(`Successfully extracted client to ${distPath}`);\n } catch (e: any) {\n this.logger.error(`Extraction failed: ${e.message}`);\n throw e;\n }\n }\n\n private doesClientIndexHtmlExist(): boolean {\n return existsSync(this.clientIndexHtmlPath);\n }\n\n private getClientVersion(): string | null {\n const path = this.clientPackageJsonPath;\n if (!existsSync(path)) {\n return null;\n }\n\n try {\n const json = JSON.parse(readFileSync(path, \"utf-8\"));\n return json?.version ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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"}
@@ -11,6 +11,10 @@ var ConfigService = class {
11
11
  isDemoMode() {
12
12
  return this.get(AppConstants.OVERRIDE_IS_DEMO_MODE, "false") === "true";
13
13
  }
14
+ instanceLabel() {
15
+ const raw = this.get(AppConstants.INSTANCE_LABEL, "")?.trim();
16
+ return raw?.length ? raw : null;
17
+ }
14
18
  };
15
19
  //#endregion
16
20
  export { ConfigService };
@@ -1 +1 @@
1
- {"version":3,"file":"config.service.js","names":[],"sources":["../../../src/services/core/config.service.ts"],"sourcesContent":["import { AppConstants } from \"@/server.constants\";\nimport { getEnvOrDefault } from \"@/utils/env.utils\";\n\nexport interface IConfigService {\n get<T>(key: string, defaultValue?: T): T | undefined;\n\n getOrThrow(key: string): void;\n\n isDemoMode(): boolean;\n}\n\nexport class ConfigService implements IConfigService {\n get<T>(key: string, defaultValue?: T) {\n return getEnvOrDefault(key, defaultValue);\n }\n\n getOrThrow(key: string) {\n const val = this.get(key);\n if (!val) {\n throw new Error(`Environment variable with key ${key} was not defined.`);\n }\n }\n\n isDemoMode() {\n return this.get<string>(AppConstants.OVERRIDE_IS_DEMO_MODE, \"false\") === \"true\";\n }\n}\n"],"mappings":";;;AAWA,IAAa,gBAAb,MAAqD;CACnD,IAAO,KAAa,cAAkB;AACpC,SAAO,gBAAgB,KAAK,aAAa;;CAG3C,WAAW,KAAa;AAEtB,MAAI,CADQ,KAAK,IAAI,IACb,CACN,OAAM,IAAI,MAAM,iCAAiC,IAAI,mBAAmB;;CAI5E,aAAa;AACX,SAAO,KAAK,IAAY,aAAa,uBAAuB,QAAQ,KAAK"}
1
+ {"version":3,"file":"config.service.js","names":[],"sources":["../../../src/services/core/config.service.ts"],"sourcesContent":["import { AppConstants } from \"@/server.constants\";\nimport { getEnvOrDefault } from \"@/utils/env.utils\";\n\nexport interface IConfigService {\n get<T>(key: string, defaultValue?: T): T | undefined;\n\n getOrThrow(key: string): void;\n\n isDemoMode(): boolean;\n\n instanceLabel(): string | null;\n}\n\nexport class ConfigService implements IConfigService {\n get<T>(key: string, defaultValue?: T) {\n return getEnvOrDefault(key, defaultValue);\n }\n\n getOrThrow(key: string) {\n const val = this.get(key);\n if (!val) {\n throw new Error(`Environment variable with key ${key} was not defined.`);\n }\n }\n\n isDemoMode() {\n return this.get<string>(AppConstants.OVERRIDE_IS_DEMO_MODE, \"false\") === \"true\";\n }\n\n instanceLabel() {\n const raw = this.get<string>(AppConstants.INSTANCE_LABEL, \"\")?.trim();\n return raw?.length ? raw : null;\n }\n}\n"],"mappings":";;;AAaA,IAAa,gBAAb,MAAqD;CACnD,IAAO,KAAa,cAAkB;EACpC,OAAO,gBAAgB,KAAK,aAAa;;CAG3C,WAAW,KAAa;EAEtB,IAAI,CADQ,KAAK,IAAI,IACb,EACN,MAAM,IAAI,MAAM,iCAAiC,IAAI,mBAAmB;;CAI5E,aAAa;EACX,OAAO,KAAK,IAAY,aAAa,uBAAuB,QAAQ,KAAK;;CAG3E,gBAAgB;EACd,MAAM,MAAM,KAAK,IAAY,aAAa,gBAAgB,GAAG,EAAE,MAAM;EACrE,OAAO,KAAK,SAAS,MAAM"}
@@ -1 +1 @@
1
- {"version":3,"file":"cradle.service.js","names":[],"sources":["../../../src/services/core/cradle.service.ts"],"sourcesContent":["import { AwilixContainer } from \"awilix\";\nimport { DITokens } from \"@/container.tokens\";\n\nexport class CradleService {\n constructor(private readonly container: AwilixContainer) {}\n\n resolve<T>(token: keyof typeof DITokens) {\n return this.container.resolve<T>(token);\n }\n}\n"],"mappings":";AAGA,IAAa,gBAAb,MAA2B;CACzB,YAAY,WAA6C;AAA5B,OAAA,YAAA;;CAE7B,QAAW,OAA8B;AACvC,SAAO,KAAK,UAAU,QAAW,MAAM"}
1
+ {"version":3,"file":"cradle.service.js","names":[],"sources":["../../../src/services/core/cradle.service.ts"],"sourcesContent":["import { AwilixContainer } from \"awilix\";\nimport { DITokens } from \"@/container.tokens\";\n\nexport class CradleService {\n constructor(private readonly container: AwilixContainer) {}\n\n resolve<T>(token: keyof typeof DITokens) {\n return this.container.resolve<T>(token);\n }\n}\n"],"mappings":";AAGA,IAAa,gBAAb,MAA2B;CACzB,YAAY,WAA6C;EAA5B,KAAA,YAAA;;CAE7B,QAAW,OAA8B;EACvC,OAAO,KAAK,UAAU,QAAW,MAAM"}
@@ -1 +1 @@
1
- {"version":3,"file":"github.service.js","names":[],"sources":["../../../src/services/core/github.service.ts"],"sourcesContent":["import { NotFoundException } from \"@/exceptions/runtime.exceptions\";\nimport { Octokit, RequestError } from \"octokit\";\n\nexport class GithubService {\n constructor(private readonly octokitService: Octokit) {}\n\n async wasAuthenticated() {\n const result = (await this.octokitService.auth()) as { type: string };\n return result?.type === \"token\";\n }\n\n async getRateLimit() {\n return this.octokitService.rest.rateLimit.get();\n }\n\n async getLatestRelease(owner: string, repo: string) {\n try {\n return await this.octokitService.rest.repos.getLatestRelease({\n owner,\n repo,\n });\n } catch (e: any) {\n if (e instanceof RequestError && e.status === 404) {\n throw new NotFoundException(`Could not find latest release`);\n }\n\n throw e;\n }\n }\n\n async getReleases(owner: string, repo: string) {\n try {\n return await this.octokitService.rest.repos.listReleases({\n owner,\n repo,\n });\n } catch (e: any) {\n if (e instanceof RequestError && e.status === 404) {\n throw new NotFoundException(`Could not find releases`);\n }\n\n throw e;\n }\n }\n\n async getReleaseByTag(owner: string, repo: string, tag: string) {\n return await this.octokitService.rest.repos\n .getReleaseByTag({\n owner,\n repo,\n tag,\n })\n .catch((e: any) => {\n if (e instanceof RequestError && e.status === 404) {\n throw new NotFoundException(`Could not find release with tag ${tag}`);\n }\n\n throw e;\n });\n }\n\n async requestAsset(owner: string, repo: string, assetId: any) {\n return await this.octokitService\n .request(\"GET /repos/:owner/:repo/releases/assets/:asset_id\", {\n headers: {\n Accept: \"application/octet-stream\",\n },\n owner,\n repo,\n asset_id: assetId,\n })\n .catch((e: any) => {\n if (e instanceof RequestError && e.status === 404) {\n throw new NotFoundException(`Could not find asset with id ${assetId}`);\n }\n\n throw e;\n });\n }\n}\n"],"mappings":";;;AAGA,IAAa,gBAAb,MAA2B;CACzB,YAAY,gBAA0C;AAAzB,OAAA,iBAAA;;CAE7B,MAAM,mBAAmB;AAEvB,UAAO,MADe,KAAK,eAAe,MAAM,GACjC,SAAS;;CAG1B,MAAM,eAAe;AACnB,SAAO,KAAK,eAAe,KAAK,UAAU,KAAK;;CAGjD,MAAM,iBAAiB,OAAe,MAAc;AAClD,MAAI;AACF,UAAO,MAAM,KAAK,eAAe,KAAK,MAAM,iBAAiB;IAC3D;IACA;IACD,CAAC;WACK,GAAQ;AACf,OAAI,aAAa,gBAAgB,EAAE,WAAW,IAC5C,OAAM,IAAI,kBAAkB,gCAAgC;AAG9D,SAAM;;;CAIV,MAAM,YAAY,OAAe,MAAc;AAC7C,MAAI;AACF,UAAO,MAAM,KAAK,eAAe,KAAK,MAAM,aAAa;IACvD;IACA;IACD,CAAC;WACK,GAAQ;AACf,OAAI,aAAa,gBAAgB,EAAE,WAAW,IAC5C,OAAM,IAAI,kBAAkB,0BAA0B;AAGxD,SAAM;;;CAIV,MAAM,gBAAgB,OAAe,MAAc,KAAa;AAC9D,SAAO,MAAM,KAAK,eAAe,KAAK,MACnC,gBAAgB;GACf;GACA;GACA;GACD,CAAC,CACD,OAAO,MAAW;AACjB,OAAI,aAAa,gBAAgB,EAAE,WAAW,IAC5C,OAAM,IAAI,kBAAkB,mCAAmC,MAAM;AAGvE,SAAM;IACN;;CAGN,MAAM,aAAa,OAAe,MAAc,SAAc;AAC5D,SAAO,MAAM,KAAK,eACf,QAAQ,qDAAqD;GAC5D,SAAS,EACP,QAAQ,4BACT;GACD;GACA;GACA,UAAU;GACX,CAAC,CACD,OAAO,MAAW;AACjB,OAAI,aAAa,gBAAgB,EAAE,WAAW,IAC5C,OAAM,IAAI,kBAAkB,gCAAgC,UAAU;AAGxE,SAAM;IACN"}
1
+ {"version":3,"file":"github.service.js","names":[],"sources":["../../../src/services/core/github.service.ts"],"sourcesContent":["import { NotFoundException } from \"@/exceptions/runtime.exceptions\";\nimport { Octokit, RequestError } from \"octokit\";\n\nexport class GithubService {\n constructor(private readonly octokitService: Octokit) {}\n\n async wasAuthenticated() {\n const result = (await this.octokitService.auth()) as { type: string };\n return result?.type === \"token\";\n }\n\n async getRateLimit() {\n return this.octokitService.rest.rateLimit.get();\n }\n\n async getLatestRelease(owner: string, repo: string) {\n try {\n return await this.octokitService.rest.repos.getLatestRelease({\n owner,\n repo,\n });\n } catch (e: any) {\n if (e instanceof RequestError && e.status === 404) {\n throw new NotFoundException(`Could not find latest release`);\n }\n\n throw e;\n }\n }\n\n async getReleases(owner: string, repo: string) {\n try {\n return await this.octokitService.rest.repos.listReleases({\n owner,\n repo,\n });\n } catch (e: any) {\n if (e instanceof RequestError && e.status === 404) {\n throw new NotFoundException(`Could not find releases`);\n }\n\n throw e;\n }\n }\n\n async getReleaseByTag(owner: string, repo: string, tag: string) {\n return await this.octokitService.rest.repos\n .getReleaseByTag({\n owner,\n repo,\n tag,\n })\n .catch((e: any) => {\n if (e instanceof RequestError && e.status === 404) {\n throw new NotFoundException(`Could not find release with tag ${tag}`);\n }\n\n throw e;\n });\n }\n\n async requestAsset(owner: string, repo: string, assetId: any) {\n return await this.octokitService\n .request(\"GET /repos/:owner/:repo/releases/assets/:asset_id\", {\n headers: {\n Accept: \"application/octet-stream\",\n },\n owner,\n repo,\n asset_id: assetId,\n })\n .catch((e: any) => {\n if (e instanceof RequestError && e.status === 404) {\n throw new NotFoundException(`Could not find asset with id ${assetId}`);\n }\n\n throw e;\n });\n }\n}\n"],"mappings":";;;AAGA,IAAa,gBAAb,MAA2B;CACzB,YAAY,gBAA0C;EAAzB,KAAA,iBAAA;;CAE7B,MAAM,mBAAmB;EAEvB,QAAO,MADe,KAAK,eAAe,MAAM,GACjC,SAAS;;CAG1B,MAAM,eAAe;EACnB,OAAO,KAAK,eAAe,KAAK,UAAU,KAAK;;CAGjD,MAAM,iBAAiB,OAAe,MAAc;EAClD,IAAI;GACF,OAAO,MAAM,KAAK,eAAe,KAAK,MAAM,iBAAiB;IAC3D;IACA;IACD,CAAC;WACK,GAAQ;GACf,IAAI,aAAa,gBAAgB,EAAE,WAAW,KAC5C,MAAM,IAAI,kBAAkB,gCAAgC;GAG9D,MAAM;;;CAIV,MAAM,YAAY,OAAe,MAAc;EAC7C,IAAI;GACF,OAAO,MAAM,KAAK,eAAe,KAAK,MAAM,aAAa;IACvD;IACA;IACD,CAAC;WACK,GAAQ;GACf,IAAI,aAAa,gBAAgB,EAAE,WAAW,KAC5C,MAAM,IAAI,kBAAkB,0BAA0B;GAGxD,MAAM;;;CAIV,MAAM,gBAAgB,OAAe,MAAc,KAAa;EAC9D,OAAO,MAAM,KAAK,eAAe,KAAK,MACnC,gBAAgB;GACf;GACA;GACA;GACD,CAAC,CACD,OAAO,MAAW;GACjB,IAAI,aAAa,gBAAgB,EAAE,WAAW,KAC5C,MAAM,IAAI,kBAAkB,mCAAmC,MAAM;GAGvE,MAAM;IACN;;CAGN,MAAM,aAAa,OAAe,MAAc,SAAc;EAC5D,OAAO,MAAM,KAAK,eACf,QAAQ,qDAAqD;GAC5D,SAAS,EACP,QAAQ,4BACT;GACD;GACA;GACA,UAAU;GACX,CAAC,CACD,OAAO,MAAW;GACjB,IAAI,aAAa,gBAAgB,EAAE,WAAW,KAC5C,MAAM,IAAI,kBAAkB,gCAAgC,UAAU;GAGxE,MAAM;IACN"}
@@ -1 +1 @@
1
- {"version":3,"file":"http-client.factory.js","names":[],"sources":["../../../src/services/core/http-client.factory.ts"],"sourcesContent":["import { DefaultHttpClientBuilder } from \"@/shared/default-http-client.builder\";\nimport { AxiosInstance } from \"axios\";\nimport { SettingsStore } from \"@/state/settings.store\";\n\nexport class HttpClientFactory {\n constructor(private readonly settingsStore: SettingsStore) {}\n\n createClient<T extends DefaultHttpClientBuilder>(base: T, buildFluentOptions?: (base: T) => void) {\n base.withMaxBodyLength(1000 * 1000 * 1000);\n base.withMaxContentLength(1000 * 1000 * 1000);\n base.withTimeout(this.settingsStore.getTimeoutSettings().apiTimeout);\n\n if (buildFluentOptions) {\n buildFluentOptions(base);\n }\n\n return base.build();\n }\n\n /**\n * Build a default http client with DefaultHttpClientBuilder.\n * @param buildFluentOptions customize the client with builder options of DefaultHttpClientBuilder.\n */\n createDefaultClient(buildFluentOptions?: (base: DefaultHttpClientBuilder) => void) {\n const builder = new DefaultHttpClientBuilder();\n return this.createClient(builder, buildFluentOptions);\n }\n\n createClientWithBaseUrl<T extends DefaultHttpClientBuilder>(\n base: T,\n baseAddress: string,\n buildFluentOptions?: (base: T) => void,\n ): AxiosInstance {\n return this.createClient(base, (builder) => {\n builder.withBaseUrl(baseAddress);\n\n if (buildFluentOptions) {\n buildFluentOptions(base);\n }\n });\n }\n}\n"],"mappings":";;AAIA,IAAa,oBAAb,MAA+B;CAC7B,YAAY,eAA+C;AAA9B,OAAA,gBAAA;;CAE7B,aAAiD,MAAS,oBAAwC;AAChG,OAAK,kBAAkB,MAAO,MAAO,IAAK;AAC1C,OAAK,qBAAqB,MAAO,MAAO,IAAK;AAC7C,OAAK,YAAY,KAAK,cAAc,oBAAoB,CAAC,WAAW;AAEpE,MAAI,mBACF,oBAAmB,KAAK;AAG1B,SAAO,KAAK,OAAO;;;;;;CAOrB,oBAAoB,oBAA+D;EACjF,MAAM,UAAU,IAAI,0BAA0B;AAC9C,SAAO,KAAK,aAAa,SAAS,mBAAmB;;CAGvD,wBACE,MACA,aACA,oBACe;AACf,SAAO,KAAK,aAAa,OAAO,YAAY;AAC1C,WAAQ,YAAY,YAAY;AAEhC,OAAI,mBACF,oBAAmB,KAAK;IAE1B"}
1
+ {"version":3,"file":"http-client.factory.js","names":[],"sources":["../../../src/services/core/http-client.factory.ts"],"sourcesContent":["import { DefaultHttpClientBuilder } from \"@/shared/default-http-client.builder\";\nimport { AxiosInstance } from \"axios\";\nimport { SettingsStore } from \"@/state/settings.store\";\n\nexport class HttpClientFactory {\n constructor(private readonly settingsStore: SettingsStore) {}\n\n createClient<T extends DefaultHttpClientBuilder>(base: T, buildFluentOptions?: (base: T) => void) {\n base.withMaxBodyLength(1000 * 1000 * 1000);\n base.withMaxContentLength(1000 * 1000 * 1000);\n base.withTimeout(this.settingsStore.getTimeoutSettings().apiTimeout);\n\n if (buildFluentOptions) {\n buildFluentOptions(base);\n }\n\n return base.build();\n }\n\n /**\n * Build a default http client with DefaultHttpClientBuilder.\n * @param buildFluentOptions customize the client with builder options of DefaultHttpClientBuilder.\n */\n createDefaultClient(buildFluentOptions?: (base: DefaultHttpClientBuilder) => void) {\n const builder = new DefaultHttpClientBuilder();\n return this.createClient(builder, buildFluentOptions);\n }\n\n createClientWithBaseUrl<T extends DefaultHttpClientBuilder>(\n base: T,\n baseAddress: string,\n buildFluentOptions?: (base: T) => void,\n ): AxiosInstance {\n return this.createClient(base, (builder) => {\n builder.withBaseUrl(baseAddress);\n\n if (buildFluentOptions) {\n buildFluentOptions(base);\n }\n });\n }\n}\n"],"mappings":";;AAIA,IAAa,oBAAb,MAA+B;CAC7B,YAAY,eAA+C;EAA9B,KAAA,gBAAA;;CAE7B,aAAiD,MAAS,oBAAwC;EAChG,KAAK,kBAAkB,MAAO,MAAO,IAAK;EAC1C,KAAK,qBAAqB,MAAO,MAAO,IAAK;EAC7C,KAAK,YAAY,KAAK,cAAc,oBAAoB,CAAC,WAAW;EAEpE,IAAI,oBACF,mBAAmB,KAAK;EAG1B,OAAO,KAAK,OAAO;;;;;;CAOrB,oBAAoB,oBAA+D;EACjF,MAAM,UAAU,IAAI,0BAA0B;EAC9C,OAAO,KAAK,aAAa,SAAS,mBAAmB;;CAGvD,wBACE,MACA,aACA,oBACe;EACf,OAAO,KAAK,aAAa,OAAO,YAAY;GAC1C,QAAQ,YAAY,YAAY;GAEhC,IAAI,oBACF,mBAAmB,KAAK;IAE1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"logs-manager.service.js","names":[],"sources":["../../../src/services/core/logs-manager.service.ts"],"sourcesContent":["import AdmZip from \"adm-zip\";\nimport { join } from \"node:path\";\nimport { rmSync, readdirSync } from \"node:fs\";\nimport { AppConstants } from \"@/server.constants\";\nimport { isParsableDate } from \"@/utils/time.utils\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { getMediaPath } from \"@/utils/fs.utils\";\n\nexport class LogDumpService {\n private readonly logger: LoggerService;\n\n constructor(loggerFactory: ILoggerFactory) {\n this.logger = loggerFactory(LogDumpService.name);\n }\n\n async deleteOlderThanWeekAndMismatchingLogFiles() {\n this.logger.log(\"Cleaning log files\");\n const path = join(getMediaPath(), AppConstants.defaultLogsFolder);\n const dirEntries = readdirSync(path, { withFileTypes: true });\n const files = dirEntries.filter((dirent) => dirent.isFile()).map((dirent) => dirent.name);\n\n // Filter only log files that are not in the format of logs/<app-name>-<date>.log\n const startingFormat = `${AppConstants.logAppName}-`;\n const removedFilesNotInFormat = files.filter((f) => f.endsWith(\".log\") && !f.startsWith(startingFormat));\n const removedFilesOutdated = files.filter((f) => {\n const matchesFormat = f.endsWith(\".log\") && f.startsWith(startingFormat);\n if (!matchesFormat) return false;\n\n const strippedFilename = f.replace(\".log\", \"\").replace(startingFormat, \"\");\n if (!isParsableDate(strippedFilename)) {\n this.logger.warn(\"Failed to parse date from log file, removing it as outdated\");\n return true;\n }\n\n const date = new Date(strippedFilename);\n const now = new Date();\n const diff = now.getTime() - date.getTime();\n const diffDays = diff / (1000 * 3600 * 24);\n return diffDays > 7;\n });\n\n this.logger.log(\n `Removing ${removedFilesNotInFormat.length} files that are not in the format of ${startingFormat}<date>.log, and ${removedFilesOutdated.length} files that are older than 7 days`,\n );\n\n let removedWrongFormatFilesCount = 0;\n for (const file of removedFilesNotInFormat) {\n try {\n rmSync(join(path, file));\n removedWrongFormatFilesCount++;\n } catch (err) {\n this.logger.warn(`Failed to delete log file`);\n }\n }\n\n let removedOutdatedFilesCount = 0;\n for (const file of removedFilesOutdated) {\n try {\n rmSync(join(path, file));\n removedOutdatedFilesCount++;\n } catch (err) {\n this.logger.warn(`Failed to delete log file`);\n }\n }\n\n this.logger.log(`Removed ${removedWrongFormatFilesCount + removedOutdatedFilesCount} log file(s)`);\n return {\n removedWrongFormatFilesCount,\n removedOutdatedFilesCount,\n };\n }\n\n async dumpZip() {\n this.logger.log(\"Dumping logs as ZIP\");\n const zip = new AdmZip();\n const path = join(getMediaPath(), AppConstants.defaultLogsFolder);\n zip.addLocalFolder(path, \"/logs\", (filename) => filename.endsWith(\".log\"));\n\n const outputPath = join(\n getMediaPath(),\n AppConstants.defaultLogZipsFolder,\n `logs-${AppConstants.serverRepoName}.zip`,\n );\n zip.writeZip(outputPath);\n return outputPath;\n }\n}\n"],"mappings":";;;;;;;AASA,IAAa,iBAAb,MAAa,eAAe;CAC1B;CAEA,YAAY,eAA+B;AACzC,OAAK,SAAS,cAAc,eAAe,KAAK;;CAGlD,MAAM,4CAA4C;AAChD,OAAK,OAAO,IAAI,qBAAqB;EACrC,MAAM,OAAO,KAAK,cAAc,EAAE,aAAa,kBAAkB;EAEjE,MAAM,QADa,YAAY,MAAM,EAAE,eAAe,MAAM,CACpC,CAAC,QAAQ,WAAW,OAAO,QAAQ,CAAC,CAAC,KAAK,WAAW,OAAO,KAAK;EAGzF,MAAM,iBAAiB,GAAG,aAAa,WAAW;EAClD,MAAM,0BAA0B,MAAM,QAAQ,MAAM,EAAE,SAAS,OAAO,IAAI,CAAC,EAAE,WAAW,eAAe,CAAC;EACxG,MAAM,uBAAuB,MAAM,QAAQ,MAAM;AAE/C,OAAI,EADkB,EAAE,SAAS,OAAO,IAAI,EAAE,WAAW,eAAe,EACpD,QAAO;GAE3B,MAAM,mBAAmB,EAAE,QAAQ,QAAQ,GAAG,CAAC,QAAQ,gBAAgB,GAAG;AAC1E,OAAI,CAAC,eAAe,iBAAiB,EAAE;AACrC,SAAK,OAAO,KAAK,8DAA8D;AAC/E,WAAO;;GAGT,MAAM,OAAO,IAAI,KAAK,iBAAiB;AAIvC,4BAFa,IADG,MACA,EAAC,SAAS,GAAG,KAAK,SAAS,KAClB,MAAO,OAAO,MACrB;IAClB;AAEF,OAAK,OAAO,IACV,YAAY,wBAAwB,OAAO,uCAAuC,eAAe,kBAAkB,qBAAqB,OAAO,mCAChJ;EAED,IAAI,+BAA+B;AACnC,OAAK,MAAM,QAAQ,wBACjB,KAAI;AACF,UAAO,KAAK,MAAM,KAAK,CAAC;AACxB;WACO,KAAK;AACZ,QAAK,OAAO,KAAK,4BAA4B;;EAIjD,IAAI,4BAA4B;AAChC,OAAK,MAAM,QAAQ,qBACjB,KAAI;AACF,UAAO,KAAK,MAAM,KAAK,CAAC;AACxB;WACO,KAAK;AACZ,QAAK,OAAO,KAAK,4BAA4B;;AAIjD,OAAK,OAAO,IAAI,WAAW,+BAA+B,0BAA0B,cAAc;AAClG,SAAO;GACL;GACA;GACD;;CAGH,MAAM,UAAU;AACd,OAAK,OAAO,IAAI,sBAAsB;EACtC,MAAM,MAAM,IAAI,QAAQ;EACxB,MAAM,OAAO,KAAK,cAAc,EAAE,aAAa,kBAAkB;AACjE,MAAI,eAAe,MAAM,UAAU,aAAa,SAAS,SAAS,OAAO,CAAC;EAE1E,MAAM,aAAa,KACjB,cAAc,EACd,aAAa,sBACb,QAAQ,aAAa,eAAe,MACrC;AACD,MAAI,SAAS,WAAW;AACxB,SAAO"}
1
+ {"version":3,"file":"logs-manager.service.js","names":[],"sources":["../../../src/services/core/logs-manager.service.ts"],"sourcesContent":["import AdmZip from \"adm-zip\";\nimport { join } from \"node:path\";\nimport { rmSync, readdirSync } from \"node:fs\";\nimport { AppConstants } from \"@/server.constants\";\nimport { isParsableDate } from \"@/utils/time.utils\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { getMediaPath } from \"@/utils/fs.utils\";\n\nexport class LogDumpService {\n private readonly logger: LoggerService;\n\n constructor(loggerFactory: ILoggerFactory) {\n this.logger = loggerFactory(LogDumpService.name);\n }\n\n async deleteOlderThanWeekAndMismatchingLogFiles() {\n this.logger.log(\"Cleaning log files\");\n const path = join(getMediaPath(), AppConstants.defaultLogsFolder);\n const dirEntries = readdirSync(path, { withFileTypes: true });\n const files = dirEntries.filter((dirent) => dirent.isFile()).map((dirent) => dirent.name);\n\n // Filter only log files that are not in the format of logs/<app-name>-<date>.log\n const startingFormat = `${AppConstants.logAppName}-`;\n const removedFilesNotInFormat = files.filter((f) => f.endsWith(\".log\") && !f.startsWith(startingFormat));\n const removedFilesOutdated = files.filter((f) => {\n const matchesFormat = f.endsWith(\".log\") && f.startsWith(startingFormat);\n if (!matchesFormat) return false;\n\n const strippedFilename = f.replace(\".log\", \"\").replace(startingFormat, \"\");\n if (!isParsableDate(strippedFilename)) {\n this.logger.warn(\"Failed to parse date from log file, removing it as outdated\");\n return true;\n }\n\n const date = new Date(strippedFilename);\n const now = new Date();\n const diff = now.getTime() - date.getTime();\n const diffDays = diff / (1000 * 3600 * 24);\n return diffDays > 7;\n });\n\n this.logger.log(\n `Removing ${removedFilesNotInFormat.length} files that are not in the format of ${startingFormat}<date>.log, and ${removedFilesOutdated.length} files that are older than 7 days`,\n );\n\n let removedWrongFormatFilesCount = 0;\n for (const file of removedFilesNotInFormat) {\n try {\n rmSync(join(path, file));\n removedWrongFormatFilesCount++;\n } catch (err) {\n this.logger.warn(`Failed to delete log file`);\n }\n }\n\n let removedOutdatedFilesCount = 0;\n for (const file of removedFilesOutdated) {\n try {\n rmSync(join(path, file));\n removedOutdatedFilesCount++;\n } catch (err) {\n this.logger.warn(`Failed to delete log file`);\n }\n }\n\n this.logger.log(`Removed ${removedWrongFormatFilesCount + removedOutdatedFilesCount} log file(s)`);\n return {\n removedWrongFormatFilesCount,\n removedOutdatedFilesCount,\n };\n }\n\n async dumpZip() {\n this.logger.log(\"Dumping logs as ZIP\");\n const zip = new AdmZip();\n const path = join(getMediaPath(), AppConstants.defaultLogsFolder);\n zip.addLocalFolder(path, \"/logs\", (filename) => filename.endsWith(\".log\"));\n\n const outputPath = join(\n getMediaPath(),\n AppConstants.defaultLogZipsFolder,\n `logs-${AppConstants.serverRepoName}.zip`,\n );\n zip.writeZip(outputPath);\n return outputPath;\n }\n}\n"],"mappings":";;;;;;;AASA,IAAa,iBAAb,MAAa,eAAe;CAC1B;CAEA,YAAY,eAA+B;EACzC,KAAK,SAAS,cAAc,eAAe,KAAK;;CAGlD,MAAM,4CAA4C;EAChD,KAAK,OAAO,IAAI,qBAAqB;EACrC,MAAM,OAAO,KAAK,cAAc,EAAE,aAAa,kBAAkB;EAEjE,MAAM,QADa,YAAY,MAAM,EAAE,eAAe,MAAM,CACpC,CAAC,QAAQ,WAAW,OAAO,QAAQ,CAAC,CAAC,KAAK,WAAW,OAAO,KAAK;EAGzF,MAAM,iBAAiB,GAAG,aAAa,WAAW;EAClD,MAAM,0BAA0B,MAAM,QAAQ,MAAM,EAAE,SAAS,OAAO,IAAI,CAAC,EAAE,WAAW,eAAe,CAAC;EACxG,MAAM,uBAAuB,MAAM,QAAQ,MAAM;GAE/C,IAAI,EADkB,EAAE,SAAS,OAAO,IAAI,EAAE,WAAW,eAAe,GACpD,OAAO;GAE3B,MAAM,mBAAmB,EAAE,QAAQ,QAAQ,GAAG,CAAC,QAAQ,gBAAgB,GAAG;GAC1E,IAAI,CAAC,eAAe,iBAAiB,EAAE;IACrC,KAAK,OAAO,KAAK,8DAA8D;IAC/E,OAAO;;GAGT,MAAM,OAAO,IAAI,KAAK,iBAAiB;GAIvC,yBAFa,IADG,MACA,EAAC,SAAS,GAAG,KAAK,SAAS,KAClB,MAAO,OAAO,MACrB;IAClB;EAEF,KAAK,OAAO,IACV,YAAY,wBAAwB,OAAO,uCAAuC,eAAe,kBAAkB,qBAAqB,OAAO,mCAChJ;EAED,IAAI,+BAA+B;EACnC,KAAK,MAAM,QAAQ,yBACjB,IAAI;GACF,OAAO,KAAK,MAAM,KAAK,CAAC;GACxB;WACO,KAAK;GACZ,KAAK,OAAO,KAAK,4BAA4B;;EAIjD,IAAI,4BAA4B;EAChC,KAAK,MAAM,QAAQ,sBACjB,IAAI;GACF,OAAO,KAAK,MAAM,KAAK,CAAC;GACxB;WACO,KAAK;GACZ,KAAK,OAAO,KAAK,4BAA4B;;EAIjD,KAAK,OAAO,IAAI,WAAW,+BAA+B,0BAA0B,cAAc;EAClG,OAAO;GACL;GACA;GACD;;CAGH,MAAM,UAAU;EACd,KAAK,OAAO,IAAI,sBAAsB;EACtC,MAAM,MAAM,IAAI,QAAQ;EACxB,MAAM,OAAO,KAAK,cAAc,EAAE,aAAa,kBAAkB;EACjE,IAAI,eAAe,MAAM,UAAU,aAAa,SAAS,SAAS,OAAO,CAAC;EAE1E,MAAM,aAAa,KACjB,cAAc,EACd,aAAa,sBACb,QAAQ,aAAa,eAAe,MACrC;EACD,IAAI,SAAS,WAAW;EACxB,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"monsterpi.service.js","names":[],"sources":["../../../src/services/core/monsterpi.service.ts"],"sourcesContent":["import { captureException } from \"@sentry/node\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { AppConstants } from \"@/server.constants\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\n\nexport class MonsterPiService {\n private monsterPiVersion: string | null = null;\n private readonly logger;\n private readonly fileLocation = AppConstants.monsterPiFilePath;\n\n constructor(loggerFactory: ILoggerFactory) {\n this.logger = loggerFactory(MonsterPiService.name);\n }\n\n getMonsterPiVersionSafe() {\n const fileExists = existsSync(this.fileLocation);\n if (!fileExists) {\n return null;\n }\n\n try {\n const contents = readFileSync(this.fileLocation);\n this.monsterPiVersion = contents.toString().replaceAll(\" \", \"\");\n return this.monsterPiVersion;\n } catch (e) {\n this.logger.warn(\"Error checking MonsterPi version\");\n captureException(e);\n }\n return null;\n }\n}\n"],"mappings":";;;;AAKA,IAAa,mBAAb,MAAa,iBAAiB;CAC5B,mBAA0C;CAC1C;CACA,eAAgC,aAAa;CAE7C,YAAY,eAA+B;AACzC,OAAK,SAAS,cAAc,iBAAiB,KAAK;;CAGpD,0BAA0B;AAExB,MAAI,CADe,WAAW,KAAK,aACpB,CACb,QAAO;AAGT,MAAI;GACF,MAAM,WAAW,aAAa,KAAK,aAAa;AAChD,QAAK,mBAAmB,SAAS,UAAU,CAAC,WAAW,KAAK,GAAG;AAC/D,UAAO,KAAK;WACL,GAAG;AACV,QAAK,OAAO,KAAK,mCAAmC;AACpD,oBAAiB,EAAE;;AAErB,SAAO"}
1
+ {"version":3,"file":"monsterpi.service.js","names":[],"sources":["../../../src/services/core/monsterpi.service.ts"],"sourcesContent":["import { captureException } from \"@sentry/node\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { AppConstants } from \"@/server.constants\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\n\nexport class MonsterPiService {\n private monsterPiVersion: string | null = null;\n private readonly logger;\n private readonly fileLocation = AppConstants.monsterPiFilePath;\n\n constructor(loggerFactory: ILoggerFactory) {\n this.logger = loggerFactory(MonsterPiService.name);\n }\n\n getMonsterPiVersionSafe() {\n const fileExists = existsSync(this.fileLocation);\n if (!fileExists) {\n return null;\n }\n\n try {\n const contents = readFileSync(this.fileLocation);\n this.monsterPiVersion = contents.toString().replaceAll(\" \", \"\");\n return this.monsterPiVersion;\n } catch (e) {\n this.logger.warn(\"Error checking MonsterPi version\");\n captureException(e);\n }\n return null;\n }\n}\n"],"mappings":";;;;AAKA,IAAa,mBAAb,MAAa,iBAAiB;CAC5B,mBAA0C;CAC1C;CACA,eAAgC,aAAa;CAE7C,YAAY,eAA+B;EACzC,KAAK,SAAS,cAAc,iBAAiB,KAAK;;CAGpD,0BAA0B;EAExB,IAAI,CADe,WAAW,KAAK,aACpB,EACb,OAAO;EAGT,IAAI;GACF,MAAM,WAAW,aAAa,KAAK,aAAa;GAChD,KAAK,mBAAmB,SAAS,UAAU,CAAC,WAAW,KAAK,GAAG;GAC/D,OAAO,KAAK;WACL,GAAG;GACV,KAAK,OAAO,KAAK,mCAAmC;GACpD,iBAAiB,EAAE;;EAErB,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"multer.service.js","names":[],"sources":["../../../src/services/core/multer.service.ts"],"sourcesContent":["import multer, { diskStorage, memoryStorage } from \"multer\";\nimport { extname, join } from \"node:path\";\nimport { existsSync, readdirSync, rmSync } from \"node:fs\";\nimport { getMediaPath } from \"@/utils/fs.utils\";\nimport { AppConstants } from \"@/server.constants\";\nimport { FileUploadTrackerCache } from \"@/state/file-upload-tracker.cache\";\nimport type { Request, Response } from \"express\";\nimport { errorSummary } from \"@/utils/error.utils\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { ValidationException } from \"@/exceptions/runtime.exceptions\";\n\nexport class MulterService {\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly fileUploadTrackerCache: FileUploadTrackerCache,\n ) {\n this.logger = loggerFactory(MulterService.name);\n }\n\n public startTrackingSession(multerFile: Express.Multer.File, printerId: number) {\n return this.fileUploadTrackerCache.addUploadTracker(multerFile, printerId);\n }\n\n public clearUploadsFolder() {\n const fileUploadsPath = join(getMediaPath(), AppConstants.defaultFileUploadsStorage);\n if (!existsSync(fileUploadsPath)) return;\n\n const files = readdirSync(fileUploadsPath, { withFileTypes: true })\n .filter((item) => !item.isDirectory())\n .map((item) => item.name);\n\n for (const file of files) {\n try {\n rmSync(join(fileUploadsPath, file));\n } catch (error) {\n this.logger.error(`Could not clear upload file in temporary folder ${errorSummary(error)}`);\n }\n }\n }\n\n public clearUploadedFile(multerFile: Express.Multer.File) {\n if (existsSync(multerFile.path)) {\n rmSync(multerFile.path);\n } else {\n this.logger.warn(\"Cannot unlink temporarily uploaded file as it was not found\");\n }\n }\n\n getMulterGCodeFileFilter(storeAsFile = true) {\n return this.getMulterFileFilter(storeAsFile);\n }\n\n async multerLoadFileAsync(req: Request, res: Response, fileExtensions: string[], storeAsFile = true) {\n const files = await new Promise<Express.Multer.File[]>((resolve, reject) =>\n this.getMulterFileFilter(storeAsFile)(req, res, (err) => {\n if (err) return reject(err);\n resolve(req.files as Express.Multer.File[]);\n }),\n );\n\n this.validateUploadedFiles(files, fileExtensions, storeAsFile);\n\n return files;\n }\n\n /**\n * Validates uploaded files against allowed extensions.\n * Removes invalid files if stored on disk and throws a ValidationException.\n */\n private validateUploadedFiles(\n files: Express.Multer.File[] | undefined,\n allowedExtensions: string[],\n storeAsFile: boolean,\n ) {\n if (!files?.length || !allowedExtensions?.length) return;\n\n for (const file of files) {\n const ext = extname(file.originalname)?.toLowerCase();\n if (!allowedExtensions.includes(ext)) {\n // Remove invalid file if stored on disk\n if (storeAsFile && file.path && existsSync(file.path)) {\n try {\n rmSync(file.path);\n } catch (e) {\n this.logger.error(`Could not remove invalid file ${errorSummary(e)}`);\n }\n }\n\n throw new ValidationException({\n error: `Only files with extensions ${allowedExtensions.join(\", \")} are allowed`,\n });\n }\n }\n }\n\n getMulterFileFilter(storeAsFile = true) {\n return multer({\n storage: storeAsFile\n ? diskStorage({\n destination: join(getMediaPath(), AppConstants.defaultFileUploadsStorage),\n })\n : memoryStorage(),\n }).any();\n }\n\n getSessions() {\n return this.fileUploadTrackerCache.getUploads();\n }\n}\n"],"mappings":";;;;;;;;AAYA,IAAa,gBAAb,MAAa,cAAc;CACzB;CAEA,YACE,eACA,wBACA;AADiB,OAAA,yBAAA;AAEjB,OAAK,SAAS,cAAc,cAAc,KAAK;;CAGjD,qBAA4B,YAAiC,WAAmB;AAC9E,SAAO,KAAK,uBAAuB,iBAAiB,YAAY,UAAU;;CAG5E,qBAA4B;EAC1B,MAAM,kBAAkB,KAAK,cAAc,EAAE,aAAa,0BAA0B;AACpF,MAAI,CAAC,WAAW,gBAAgB,CAAE;EAElC,MAAM,QAAQ,YAAY,iBAAiB,EAAE,eAAe,MAAM,CAAC,CAChE,QAAQ,SAAS,CAAC,KAAK,aAAa,CAAC,CACrC,KAAK,SAAS,KAAK,KAAK;AAE3B,OAAK,MAAM,QAAQ,MACjB,KAAI;AACF,UAAO,KAAK,iBAAiB,KAAK,CAAC;WAC5B,OAAO;AACd,QAAK,OAAO,MAAM,mDAAmD,aAAa,MAAM,GAAG;;;CAKjG,kBAAyB,YAAiC;AACxD,MAAI,WAAW,WAAW,KAAK,CAC7B,QAAO,WAAW,KAAK;MAEvB,MAAK,OAAO,KAAK,8DAA8D;;CAInF,yBAAyB,cAAc,MAAM;AAC3C,SAAO,KAAK,oBAAoB,YAAY;;CAG9C,MAAM,oBAAoB,KAAc,KAAe,gBAA0B,cAAc,MAAM;EACnG,MAAM,QAAQ,MAAM,IAAI,SAAgC,SAAS,WAC/D,KAAK,oBAAoB,YAAY,CAAC,KAAK,MAAM,QAAQ;AACvD,OAAI,IAAK,QAAO,OAAO,IAAI;AAC3B,WAAQ,IAAI,MAA+B;IAC3C,CACH;AAED,OAAK,sBAAsB,OAAO,gBAAgB,YAAY;AAE9D,SAAO;;;;;;CAOT,sBACE,OACA,mBACA,aACA;AACA,MAAI,CAAC,OAAO,UAAU,CAAC,mBAAmB,OAAQ;AAElD,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,MAAM,QAAQ,KAAK,aAAa,EAAE,aAAa;AACrD,OAAI,CAAC,kBAAkB,SAAS,IAAI,EAAE;AAEpC,QAAI,eAAe,KAAK,QAAQ,WAAW,KAAK,KAAK,CACnD,KAAI;AACF,YAAO,KAAK,KAAK;aACV,GAAG;AACV,UAAK,OAAO,MAAM,iCAAiC,aAAa,EAAE,GAAG;;AAIzE,UAAM,IAAI,oBAAoB,EAC5B,OAAO,8BAA8B,kBAAkB,KAAK,KAAK,CAAC,eACnE,CAAC;;;;CAKR,oBAAoB,cAAc,MAAM;AACtC,SAAO,OAAO,EACZ,SAAS,cACL,YAAY,EACV,aAAa,KAAK,cAAc,EAAE,aAAa,0BAA0B,EAC1E,CAAC,GACF,eAAe,EACpB,CAAC,CAAC,KAAK;;CAGV,cAAc;AACZ,SAAO,KAAK,uBAAuB,YAAY"}
1
+ {"version":3,"file":"multer.service.js","names":[],"sources":["../../../src/services/core/multer.service.ts"],"sourcesContent":["import multer, { diskStorage, memoryStorage } from \"multer\";\nimport { extname, join } from \"node:path\";\nimport { existsSync, readdirSync, rmSync } from \"node:fs\";\nimport { getMediaPath } from \"@/utils/fs.utils\";\nimport { AppConstants } from \"@/server.constants\";\nimport { FileUploadTrackerCache } from \"@/state/file-upload-tracker.cache\";\nimport type { Request, Response } from \"express\";\nimport { errorSummary } from \"@/utils/error.utils\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { ValidationException } from \"@/exceptions/runtime.exceptions\";\n\nexport class MulterService {\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly fileUploadTrackerCache: FileUploadTrackerCache,\n ) {\n this.logger = loggerFactory(MulterService.name);\n }\n\n public startTrackingSession(multerFile: Express.Multer.File, printerId: number) {\n return this.fileUploadTrackerCache.addUploadTracker(multerFile, printerId);\n }\n\n public clearUploadsFolder() {\n const fileUploadsPath = join(getMediaPath(), AppConstants.defaultFileUploadsStorage);\n if (!existsSync(fileUploadsPath)) return;\n\n const files = readdirSync(fileUploadsPath, { withFileTypes: true })\n .filter((item) => !item.isDirectory())\n .map((item) => item.name);\n\n for (const file of files) {\n try {\n rmSync(join(fileUploadsPath, file));\n } catch (error) {\n this.logger.error(`Could not clear upload file in temporary folder ${errorSummary(error)}`);\n }\n }\n }\n\n public clearUploadedFile(multerFile: Express.Multer.File) {\n if (existsSync(multerFile.path)) {\n rmSync(multerFile.path);\n } else {\n this.logger.warn(\"Cannot unlink temporarily uploaded file as it was not found\");\n }\n }\n\n getMulterGCodeFileFilter(storeAsFile = true) {\n return this.getMulterFileFilter(storeAsFile);\n }\n\n async multerLoadFileAsync(req: Request, res: Response, fileExtensions: string[], storeAsFile = true) {\n const files = await new Promise<Express.Multer.File[]>((resolve, reject) =>\n this.getMulterFileFilter(storeAsFile)(req, res, (err) => {\n if (err) return reject(err);\n resolve(req.files as Express.Multer.File[]);\n }),\n );\n\n this.validateUploadedFiles(files, fileExtensions, storeAsFile);\n\n return files;\n }\n\n /**\n * Validates uploaded files against allowed extensions.\n * Removes invalid files if stored on disk and throws a ValidationException.\n */\n private validateUploadedFiles(\n files: Express.Multer.File[] | undefined,\n allowedExtensions: string[],\n storeAsFile: boolean,\n ) {\n if (!files?.length || !allowedExtensions?.length) return;\n\n for (const file of files) {\n const ext = extname(file.originalname)?.toLowerCase();\n if (!allowedExtensions.includes(ext)) {\n // Remove invalid file if stored on disk\n if (storeAsFile && file.path && existsSync(file.path)) {\n try {\n rmSync(file.path);\n } catch (e) {\n this.logger.error(`Could not remove invalid file ${errorSummary(e)}`);\n }\n }\n\n throw new ValidationException({\n error: `Only files with extensions ${allowedExtensions.join(\", \")} are allowed`,\n });\n }\n }\n }\n\n getMulterFileFilter(storeAsFile = true) {\n return multer({\n storage: storeAsFile\n ? diskStorage({\n destination: join(getMediaPath(), AppConstants.defaultFileUploadsStorage),\n })\n : memoryStorage(),\n }).any();\n }\n\n getSessions() {\n return this.fileUploadTrackerCache.getUploads();\n }\n}\n"],"mappings":";;;;;;;;AAYA,IAAa,gBAAb,MAAa,cAAc;CACzB;CAEA,YACE,eACA,wBACA;EADiB,KAAA,yBAAA;EAEjB,KAAK,SAAS,cAAc,cAAc,KAAK;;CAGjD,qBAA4B,YAAiC,WAAmB;EAC9E,OAAO,KAAK,uBAAuB,iBAAiB,YAAY,UAAU;;CAG5E,qBAA4B;EAC1B,MAAM,kBAAkB,KAAK,cAAc,EAAE,aAAa,0BAA0B;EACpF,IAAI,CAAC,WAAW,gBAAgB,EAAE;EAElC,MAAM,QAAQ,YAAY,iBAAiB,EAAE,eAAe,MAAM,CAAC,CAChE,QAAQ,SAAS,CAAC,KAAK,aAAa,CAAC,CACrC,KAAK,SAAS,KAAK,KAAK;EAE3B,KAAK,MAAM,QAAQ,OACjB,IAAI;GACF,OAAO,KAAK,iBAAiB,KAAK,CAAC;WAC5B,OAAO;GACd,KAAK,OAAO,MAAM,mDAAmD,aAAa,MAAM,GAAG;;;CAKjG,kBAAyB,YAAiC;EACxD,IAAI,WAAW,WAAW,KAAK,EAC7B,OAAO,WAAW,KAAK;OAEvB,KAAK,OAAO,KAAK,8DAA8D;;CAInF,yBAAyB,cAAc,MAAM;EAC3C,OAAO,KAAK,oBAAoB,YAAY;;CAG9C,MAAM,oBAAoB,KAAc,KAAe,gBAA0B,cAAc,MAAM;EACnG,MAAM,QAAQ,MAAM,IAAI,SAAgC,SAAS,WAC/D,KAAK,oBAAoB,YAAY,CAAC,KAAK,MAAM,QAAQ;GACvD,IAAI,KAAK,OAAO,OAAO,IAAI;GAC3B,QAAQ,IAAI,MAA+B;IAC3C,CACH;EAED,KAAK,sBAAsB,OAAO,gBAAgB,YAAY;EAE9D,OAAO;;;;;;CAOT,sBACE,OACA,mBACA,aACA;EACA,IAAI,CAAC,OAAO,UAAU,CAAC,mBAAmB,QAAQ;EAElD,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,MAAM,QAAQ,KAAK,aAAa,EAAE,aAAa;GACrD,IAAI,CAAC,kBAAkB,SAAS,IAAI,EAAE;IAEpC,IAAI,eAAe,KAAK,QAAQ,WAAW,KAAK,KAAK,EACnD,IAAI;KACF,OAAO,KAAK,KAAK;aACV,GAAG;KACV,KAAK,OAAO,MAAM,iCAAiC,aAAa,EAAE,GAAG;;IAIzE,MAAM,IAAI,oBAAoB,EAC5B,OAAO,8BAA8B,kBAAkB,KAAK,KAAK,CAAC,eACnE,CAAC;;;;CAKR,oBAAoB,cAAc,MAAM;EACtC,OAAO,OAAO,EACZ,SAAS,cACL,YAAY,EACV,aAAa,KAAK,cAAc,EAAE,aAAa,0BAA0B,EAC1E,CAAC,GACF,eAAe,EACpB,CAAC,CAAC,KAAK;;CAGV,cAAc;EACZ,OAAO,KAAK,uBAAuB,YAAY"}
@@ -1 +1 @@
1
- {"version":3,"file":"server-release.service.js","names":[],"sources":["../../../src/services/core/server-release.service.ts"],"sourcesContent":["import semver from \"semver\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { AppConstants } from \"@/server.constants\";\nimport { GithubService } from \"@/services/core/github.service\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { Octokit } from \"octokit\";\n\nexport class ServerReleaseService {\n airGapped: null | boolean = null; // Connection error\n private synced = false;\n private installedReleaseFound: null | boolean = null;\n private updateAvailable: null | boolean = null;\n private latestRelease: Awaited<ReturnType<Octokit[\"rest\"][\"repos\"][\"listReleases\"]>>[\"data\"][number] | null = null;\n private installedRelease: { tag_name: string } | null = null;\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly serverVersion: string,\n private readonly githubService: GithubService,\n ) {\n this.logger = loggerFactory(ServerReleaseService.name);\n }\n\n getState() {\n return {\n airGapped: this.airGapped,\n latestRelease: this.latestRelease,\n installedRelease: this.installedRelease,\n serverVersion: this.serverVersion,\n installedReleaseFound: this.installedReleaseFound,\n updateAvailable: this.updateAvailable,\n synced: this.synced,\n };\n }\n\n /**\n * Connection-safe acquire data about the installed and latest releases.\n */\n async syncLatestRelease(): Promise<any | null> {\n if (!(await this.githubService.wasAuthenticated())) {\n return;\n }\n const owner = AppConstants.orgName;\n const repo = AppConstants.serverRepoName;\n const response = await this.githubService.getReleases(owner, repo);\n const latestResponse = await this.githubService.getLatestRelease(owner, repo);\n this.synced = true;\n const releases = response.data;\n const latestRelease = latestResponse.data;\n\n // Connection timeout results in airGapped state\n this.airGapped = !releases?.length;\n if (!releases?.length) {\n this.logger.warn(\"Latest release check failed because releases from github empty\");\n return;\n }\n\n const currentlyInstalledRelease = this.serverVersion;\n this.installedRelease = {\n tag_name: currentlyInstalledRelease,\n };\n this.latestRelease = latestRelease;\n\n this.installedReleaseFound = !!currentlyInstalledRelease;\n if (!this.installedReleaseFound) {\n this.updateAvailable = false;\n return;\n }\n\n // If the installed release is unknown/unstable, no update should be triggered\n const lastTagIsNewer = semver.gt(this.latestRelease.tag_name, this.installedRelease.tag_name, true);\n this.updateAvailable = this.installedReleaseFound && lastTagIsNewer;\n }\n\n /**\n * Logs whether a firmware update is ready\n */\n logServerVersionState() {\n const latestReleaseState = this.getState();\n const latestRelease = latestReleaseState?.latestRelease;\n const latestReleaseTag = latestRelease?.tag_name;\n\n if (!latestReleaseTag) {\n // Tests only, silence it\n return;\n }\n\n const packageVersion = this.serverVersion;\n if (this.installedReleaseFound) {\n this.logger.log(\n `\\x1b[36mCurrent release was found in github releases.\\x1b[0m\n Here's github's latest released: \\x1b[32m${latestReleaseTag}\\x1b[0m\n Here's your release tag: \\x1b[32m${packageVersion}\\x1b[0m\n Thanks for using FDM Monster!`,\n );\n } else {\n this.logger.log(\n `\\x1b[36mCurrent release tag not found in github releases.\\x1b[0m\n Here's github's latest released: \\x1b[32m${latestReleaseTag}\\x1b[0m\n Here's your release tag: \\x1b[32m${packageVersion}\\x1b[0m\n Thanks for using FDM Monster!`,\n );\n return;\n }\n\n if (!!packageVersion && latestReleaseState.updateAvailable) {\n if (this.airGapped) {\n this.logger.warn(\n `Installed release: ${packageVersion}. Skipping update check (air-gapped/disconnected from internet)`,\n );\n } else {\n this.logger.log(`Update available! New version: ${latestReleaseTag} (prerelease: ${latestRelease.prerelease})`);\n }\n } else if (packageVersion) {\n return this.logger.log(`Installed release: ${packageVersion}. You are up to date!`);\n } else {\n return this.logger.error(\n \"Cant check release as package.json version environment variable is not set. Make sure FDM Server is run from a 'package.json' or NPM context.\",\n );\n }\n }\n}\n"],"mappings":";;;AAOA,IAAa,uBAAb,MAAa,qBAAqB;CAChC,YAA4B;CAC5B,SAAiB;CACjB,wBAAgD;CAChD,kBAA0C;CAC1C,gBAA8G;CAC9G,mBAAwD;CACxD;CAEA,YACE,eACA,eACA,eACA;AAFiB,OAAA,gBAAA;AACA,OAAA,gBAAA;AAEjB,OAAK,SAAS,cAAc,qBAAqB,KAAK;;CAGxD,WAAW;AACT,SAAO;GACL,WAAW,KAAK;GAChB,eAAe,KAAK;GACpB,kBAAkB,KAAK;GACvB,eAAe,KAAK;GACpB,uBAAuB,KAAK;GAC5B,iBAAiB,KAAK;GACtB,QAAQ,KAAK;GACd;;;;;CAMH,MAAM,oBAAyC;AAC7C,MAAI,CAAE,MAAM,KAAK,cAAc,kBAAkB,CAC/C;EAEF,MAAM,QAAQ,aAAa;EAC3B,MAAM,OAAO,aAAa;EAC1B,MAAM,WAAW,MAAM,KAAK,cAAc,YAAY,OAAO,KAAK;EAClE,MAAM,iBAAiB,MAAM,KAAK,cAAc,iBAAiB,OAAO,KAAK;AAC7E,OAAK,SAAS;EACd,MAAM,WAAW,SAAS;EAC1B,MAAM,gBAAgB,eAAe;AAGrC,OAAK,YAAY,CAAC,UAAU;AAC5B,MAAI,CAAC,UAAU,QAAQ;AACrB,QAAK,OAAO,KAAK,iEAAiE;AAClF;;EAGF,MAAM,4BAA4B,KAAK;AACvC,OAAK,mBAAmB,EACtB,UAAU,2BACX;AACD,OAAK,gBAAgB;AAErB,OAAK,wBAAwB,CAAC,CAAC;AAC/B,MAAI,CAAC,KAAK,uBAAuB;AAC/B,QAAK,kBAAkB;AACvB;;EAIF,MAAM,iBAAiB,OAAO,GAAG,KAAK,cAAc,UAAU,KAAK,iBAAiB,UAAU,KAAK;AACnG,OAAK,kBAAkB,KAAK,yBAAyB;;;;;CAMvD,wBAAwB;EACtB,MAAM,qBAAqB,KAAK,UAAU;EAC1C,MAAM,gBAAgB,oBAAoB;EAC1C,MAAM,mBAAmB,eAAe;AAExC,MAAI,CAAC,iBAEH;EAGF,MAAM,iBAAiB,KAAK;AAC5B,MAAI,KAAK,sBACP,MAAK,OAAO,IACV;+CACuC,iBAAiB;uCACzB,eAAe;mCAE/C;OACI;AACL,QAAK,OAAO,IACV;+CACuC,iBAAiB;uCACzB,eAAe;mCAE/C;AACD;;AAGF,MAAI,CAAC,CAAC,kBAAkB,mBAAmB,gBACzC,KAAI,KAAK,UACP,MAAK,OAAO,KACV,sBAAsB,eAAe,iEACtC;MAED,MAAK,OAAO,IAAI,kCAAkC,iBAAiB,gBAAgB,cAAc,WAAW,GAAG;WAExG,eACT,QAAO,KAAK,OAAO,IAAI,sBAAsB,eAAe,uBAAuB;MAEnF,QAAO,KAAK,OAAO,MACjB,gJACD"}
1
+ {"version":3,"file":"server-release.service.js","names":[],"sources":["../../../src/services/core/server-release.service.ts"],"sourcesContent":["import semver from \"semver\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { AppConstants } from \"@/server.constants\";\nimport { GithubService } from \"@/services/core/github.service\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { Octokit } from \"octokit\";\n\nexport class ServerReleaseService {\n airGapped: null | boolean = null; // Connection error\n private synced = false;\n private installedReleaseFound: null | boolean = null;\n private updateAvailable: null | boolean = null;\n private latestRelease: Awaited<ReturnType<Octokit[\"rest\"][\"repos\"][\"listReleases\"]>>[\"data\"][number] | null = null;\n private installedRelease: { tag_name: string } | null = null;\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly serverVersion: string,\n private readonly githubService: GithubService,\n ) {\n this.logger = loggerFactory(ServerReleaseService.name);\n }\n\n getState() {\n return {\n airGapped: this.airGapped,\n latestRelease: this.latestRelease,\n installedRelease: this.installedRelease,\n serverVersion: this.serverVersion,\n installedReleaseFound: this.installedReleaseFound,\n updateAvailable: this.updateAvailable,\n synced: this.synced,\n };\n }\n\n /**\n * Connection-safe acquire data about the installed and latest releases.\n */\n async syncLatestRelease(): Promise<any | null> {\n if (!(await this.githubService.wasAuthenticated())) {\n return;\n }\n const owner = AppConstants.orgName;\n const repo = AppConstants.serverRepoName;\n const response = await this.githubService.getReleases(owner, repo);\n const latestResponse = await this.githubService.getLatestRelease(owner, repo);\n this.synced = true;\n const releases = response.data;\n const latestRelease = latestResponse.data;\n\n // Connection timeout results in airGapped state\n this.airGapped = !releases?.length;\n if (!releases?.length) {\n this.logger.warn(\"Latest release check failed because releases from github empty\");\n return;\n }\n\n const currentlyInstalledRelease = this.serverVersion;\n this.installedRelease = {\n tag_name: currentlyInstalledRelease,\n };\n this.latestRelease = latestRelease;\n\n this.installedReleaseFound = !!currentlyInstalledRelease;\n if (!this.installedReleaseFound) {\n this.updateAvailable = false;\n return;\n }\n\n // If the installed release is unknown/unstable, no update should be triggered\n const lastTagIsNewer = semver.gt(this.latestRelease.tag_name, this.installedRelease.tag_name, true);\n this.updateAvailable = this.installedReleaseFound && lastTagIsNewer;\n }\n\n /**\n * Logs whether a firmware update is ready\n */\n logServerVersionState() {\n const latestReleaseState = this.getState();\n const latestRelease = latestReleaseState?.latestRelease;\n const latestReleaseTag = latestRelease?.tag_name;\n\n if (!latestReleaseTag) {\n // Tests only, silence it\n return;\n }\n\n const packageVersion = this.serverVersion;\n if (this.installedReleaseFound) {\n this.logger.log(\n `\\x1b[36mCurrent release was found in github releases.\\x1b[0m\n Here's github's latest released: \\x1b[32m${latestReleaseTag}\\x1b[0m\n Here's your release tag: \\x1b[32m${packageVersion}\\x1b[0m\n Thanks for using FDM Monster!`,\n );\n } else {\n this.logger.log(\n `\\x1b[36mCurrent release tag not found in github releases.\\x1b[0m\n Here's github's latest released: \\x1b[32m${latestReleaseTag}\\x1b[0m\n Here's your release tag: \\x1b[32m${packageVersion}\\x1b[0m\n Thanks for using FDM Monster!`,\n );\n return;\n }\n\n if (!!packageVersion && latestReleaseState.updateAvailable) {\n if (this.airGapped) {\n this.logger.warn(\n `Installed release: ${packageVersion}. Skipping update check (air-gapped/disconnected from internet)`,\n );\n } else {\n this.logger.log(`Update available! New version: ${latestReleaseTag} (prerelease: ${latestRelease.prerelease})`);\n }\n } else if (packageVersion) {\n return this.logger.log(`Installed release: ${packageVersion}. You are up to date!`);\n } else {\n return this.logger.error(\n \"Cant check release as package.json version environment variable is not set. Make sure FDM Server is run from a 'package.json' or NPM context.\",\n );\n }\n }\n}\n"],"mappings":";;;AAOA,IAAa,uBAAb,MAAa,qBAAqB;CAChC,YAA4B;CAC5B,SAAiB;CACjB,wBAAgD;CAChD,kBAA0C;CAC1C,gBAA8G;CAC9G,mBAAwD;CACxD;CAEA,YACE,eACA,eACA,eACA;EAFiB,KAAA,gBAAA;EACA,KAAA,gBAAA;EAEjB,KAAK,SAAS,cAAc,qBAAqB,KAAK;;CAGxD,WAAW;EACT,OAAO;GACL,WAAW,KAAK;GAChB,eAAe,KAAK;GACpB,kBAAkB,KAAK;GACvB,eAAe,KAAK;GACpB,uBAAuB,KAAK;GAC5B,iBAAiB,KAAK;GACtB,QAAQ,KAAK;GACd;;;;;CAMH,MAAM,oBAAyC;EAC7C,IAAI,CAAE,MAAM,KAAK,cAAc,kBAAkB,EAC/C;EAEF,MAAM,QAAQ,aAAa;EAC3B,MAAM,OAAO,aAAa;EAC1B,MAAM,WAAW,MAAM,KAAK,cAAc,YAAY,OAAO,KAAK;EAClE,MAAM,iBAAiB,MAAM,KAAK,cAAc,iBAAiB,OAAO,KAAK;EAC7E,KAAK,SAAS;EACd,MAAM,WAAW,SAAS;EAC1B,MAAM,gBAAgB,eAAe;EAGrC,KAAK,YAAY,CAAC,UAAU;EAC5B,IAAI,CAAC,UAAU,QAAQ;GACrB,KAAK,OAAO,KAAK,iEAAiE;GAClF;;EAGF,MAAM,4BAA4B,KAAK;EACvC,KAAK,mBAAmB,EACtB,UAAU,2BACX;EACD,KAAK,gBAAgB;EAErB,KAAK,wBAAwB,CAAC,CAAC;EAC/B,IAAI,CAAC,KAAK,uBAAuB;GAC/B,KAAK,kBAAkB;GACvB;;EAIF,MAAM,iBAAiB,OAAO,GAAG,KAAK,cAAc,UAAU,KAAK,iBAAiB,UAAU,KAAK;EACnG,KAAK,kBAAkB,KAAK,yBAAyB;;;;;CAMvD,wBAAwB;EACtB,MAAM,qBAAqB,KAAK,UAAU;EAC1C,MAAM,gBAAgB,oBAAoB;EAC1C,MAAM,mBAAmB,eAAe;EAExC,IAAI,CAAC,kBAEH;EAGF,MAAM,iBAAiB,KAAK;EAC5B,IAAI,KAAK,uBACP,KAAK,OAAO,IACV;+CACuC,iBAAiB;uCACzB,eAAe;mCAE/C;OACI;GACL,KAAK,OAAO,IACV;+CACuC,iBAAiB;uCACzB,eAAe;mCAE/C;GACD;;EAGF,IAAI,CAAC,CAAC,kBAAkB,mBAAmB,iBACzC,IAAI,KAAK,WACP,KAAK,OAAO,KACV,sBAAsB,eAAe,iEACtC;OAED,KAAK,OAAO,IAAI,kCAAkC,iBAAiB,gBAAgB,cAAc,WAAW,GAAG;OAE5G,IAAI,gBACT,OAAO,KAAK,OAAO,IAAI,sBAAsB,eAAe,uBAAuB;OAEnF,OAAO,KAAK,OAAO,MACjB,gJACD"}