@fdm-monster/server 2.0.11 → 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.
- package/.yarn/install-state.gz +0 -0
- package/.yarn/releases/{yarn-4.13.0.cjs → yarn-4.14.1.cjs} +288 -288
- package/.yarnrc.yml +5 -1
- package/README.md +2 -1
- package/RELEASE_NOTES.MD +153 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.129.0}/helpers/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.129.0}/helpers/decorateMetadata.js +1 -1
- package/dist/_virtual/_virtual_controllers.js +2 -0
- package/dist/consoles/typeorm-create.js.map +1 -1
- package/dist/consoles/typeorm-generate.js.map +1 -1
- package/dist/consoles/typeorm-migrate.js.map +1 -1
- package/dist/constants/authorization.constants.js.map +1 -1
- package/dist/container.js +2 -0
- package/dist/container.js.map +1 -1
- package/dist/container.tokens.js +1 -0
- package/dist/container.tokens.js.map +1 -1
- package/dist/controllers/api-key.controller.js +72 -0
- package/dist/controllers/api-key.controller.js.map +1 -0
- package/dist/controllers/auth.controller.js +6 -4
- package/dist/controllers/auth.controller.js.map +1 -1
- package/dist/controllers/batch-call.controller.js +2 -2
- package/dist/controllers/batch-call.controller.js.map +1 -1
- package/dist/controllers/camera-stream.controller.js +2 -2
- package/dist/controllers/camera-stream.controller.js.map +1 -1
- package/dist/controllers/file-storage.controller.js +2 -2
- package/dist/controllers/file-storage.controller.js.map +1 -1
- package/dist/controllers/first-time-setup.controller.js +2 -2
- package/dist/controllers/first-time-setup.controller.js.map +1 -1
- package/dist/controllers/floor.controller.js +2 -2
- package/dist/controllers/floor.controller.js.map +1 -1
- package/dist/controllers/metrics.controller.js +2 -2
- package/dist/controllers/metrics.controller.js.map +1 -1
- package/dist/controllers/print-job.controller.js +2 -2
- package/dist/controllers/print-job.controller.js.map +1 -1
- package/dist/controllers/print-queue.controller.js +2 -2
- package/dist/controllers/print-queue.controller.js.map +1 -1
- package/dist/controllers/printer-files.controller.js +2 -2
- package/dist/controllers/printer-files.controller.js.map +1 -1
- package/dist/controllers/printer-maintenance-log.controller.js +2 -2
- package/dist/controllers/printer-maintenance-log.controller.js.map +1 -1
- package/dist/controllers/printer-settings.controller.js +2 -2
- package/dist/controllers/printer-settings.controller.js.map +1 -1
- package/dist/controllers/printer-tag.controller.js +2 -2
- package/dist/controllers/printer-tag.controller.js.map +1 -1
- package/dist/controllers/printer.controller.js +2 -2
- package/dist/controllers/printer.controller.js.map +1 -1
- package/dist/controllers/server-private.controller.js +2 -2
- package/dist/controllers/server-private.controller.js.map +1 -1
- package/dist/controllers/server-public.controller.js +2 -2
- package/dist/controllers/server-public.controller.js.map +1 -1
- package/dist/controllers/settings.controller.js +2 -2
- package/dist/controllers/settings.controller.js.map +1 -1
- package/dist/controllers/slicer-compat.controller.js +2 -2
- package/dist/controllers/slicer-compat.controller.js.map +1 -1
- package/dist/controllers/user.controller.js +2 -2
- package/dist/controllers/user.controller.js.map +1 -1
- package/dist/controllers/validation/api-key-controller.validation.js +11 -0
- package/dist/controllers/validation/api-key-controller.validation.js.map +1 -0
- package/dist/data-source.js +6 -2
- package/dist/data-source.js.map +1 -1
- package/dist/entities/api-key.entity.js +60 -0
- package/dist/entities/api-key.entity.js.map +1 -0
- package/dist/entities/camera-stream.entity.js +2 -2
- package/dist/entities/floor-position.entity.js +2 -2
- package/dist/entities/floor.entity.js +2 -2
- package/dist/entities/index.js +2 -1
- package/dist/entities/print-job.entity.js +2 -2
- package/dist/entities/printer-maintenance-log.entity.js +2 -2
- package/dist/entities/printer-tag.entity.js +2 -2
- package/dist/entities/printer.entity.js +2 -2
- package/dist/entities/refresh-token.entity.js +2 -2
- package/dist/entities/role.entity.js +2 -2
- package/dist/entities/settings.entity.js +2 -2
- package/dist/entities/tag.entity.js +2 -2
- package/dist/entities/user-role.entity.js +2 -2
- package/dist/entities/user.entity.js +2 -2
- package/dist/exceptions/failed-dependency.exception.js.map +1 -1
- package/dist/exceptions/job.exceptions.js.map +1 -1
- package/dist/exceptions/runtime.exceptions.js.map +1 -1
- package/dist/handlers/event-emitter.js.map +1 -1
- package/dist/handlers/logger-factory.js.map +1 -1
- package/dist/handlers/logger.js.map +1 -1
- package/dist/handlers/logging/file-logging.transport.js +1 -2
- package/dist/handlers/logging/file-logging.transport.js.map +1 -1
- package/dist/handlers/logging/loki-logging.transport.js.map +1 -1
- package/dist/handlers/logging/static.logger.js.map +1 -1
- package/dist/handlers/validators.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/api-key.strategy.js +45 -0
- package/dist/middleware/api-key.strategy.js.map +1 -0
- package/dist/middleware/authenticate.js.map +1 -1
- package/dist/middleware/database.js.map +1 -1
- package/dist/middleware/demo.middleware.js.map +1 -1
- package/dist/middleware/exception.filter.js.map +1 -1
- package/dist/middleware/global.middleware.js.map +1 -1
- package/dist/middleware/param-converter.middleware.js.map +1 -1
- package/dist/middleware/passport.js +3 -0
- package/dist/middleware/passport.js.map +1 -1
- package/dist/middleware/printer-resolver.js.map +1 -1
- package/dist/middleware/printer.js.map +1 -1
- package/dist/middleware/slicer-api-key.middleware.js.map +1 -1
- package/dist/middleware/socketio.middleware.js.map +1 -1
- package/dist/migrations/1706829146617-InitSqlite.js.map +1 -1
- package/dist/migrations/1707494762198-PrinterGroup.js.map +1 -1
- package/dist/migrations/1708465930665-ChangePrintCompletionDeletePrinterCascade.js.map +1 -1
- package/dist/migrations/1713300747465-ChangeRoleNameUnique.js.map +1 -1
- package/dist/migrations/1713897879622-AddPrinterType.js.map +1 -1
- package/dist/migrations/1720338804844-RemovePrinterFile.js.map +1 -1
- package/dist/migrations/1745141688926-AddPrinterUsernamePassword.js.map +1 -1
- package/dist/migrations/1766576698569-DropPermissions.js.map +1 -1
- package/dist/migrations/1767278216516-ChangeCameraPrinterOnDeleteSetNull.js.map +1 -1
- package/dist/migrations/1767279607392-DropCustomGcode.js.map +1 -1
- package/dist/migrations/1767291804417-DropPrintCompletions.js.map +1 -1
- package/dist/migrations/1767352862576-DropSettingsFileClean.js.map +1 -1
- package/dist/migrations/1767355639023-ChangeFloorLevelToOrder.js.map +1 -1
- package/dist/migrations/1767370191762-ChangeFloorNonUniqueOrder.js.map +1 -1
- package/dist/migrations/1767432108916-RenameGroupToTag.js.map +1 -1
- package/dist/migrations/1767451444137-AddPrintJob.js.map +1 -1
- package/dist/migrations/1767909428129-AddPrinterMaintenanceLog.js.map +1 -1
- package/dist/migrations/1778446203015-AddApiKey.js +49 -0
- package/dist/migrations/1778446203015-AddApiKey.js.map +1 -0
- package/dist/plugins/controllers-plugin.js.map +1 -1
- package/dist/server.constants.js +2 -1
- package/dist/server.constants.js.map +1 -1
- package/dist/server.core.js +6 -2
- package/dist/server.core.js.map +1 -1
- package/dist/server.env.js.map +1 -1
- package/dist/server.host.js.map +1 -1
- package/dist/services/authentication/auth.service.js.map +1 -1
- package/dist/services/authentication/jwt.service.js.map +1 -1
- package/dist/services/bambu/bambu-ftp.adapter.js.map +1 -1
- package/dist/services/bambu/bambu-mqtt.adapter.js.map +1 -1
- package/dist/services/bambu/bambu.client.js.map +1 -1
- package/dist/services/bambu.api.js.map +1 -1
- package/dist/services/core/batch-call.service.js.map +1 -1
- package/dist/services/core/client-bundle.service.js.map +1 -1
- package/dist/services/core/config.service.js +4 -0
- package/dist/services/core/config.service.js.map +1 -1
- package/dist/services/core/cradle.service.js.map +1 -1
- package/dist/services/core/github.service.js.map +1 -1
- package/dist/services/core/http-client.factory.js.map +1 -1
- package/dist/services/core/logs-manager.service.js.map +1 -1
- package/dist/services/core/monsterpi.service.js.map +1 -1
- package/dist/services/core/multer.service.js.map +1 -1
- package/dist/services/core/server-release.service.js.map +1 -1
- package/dist/services/core/yaml.service.js.map +1 -1
- package/dist/services/file-analysis.service.js.map +1 -1
- package/dist/services/file-storage.service.js.map +1 -1
- package/dist/services/interfaces/api-key.dto.js +19 -0
- package/dist/services/interfaces/api-key.dto.js.map +1 -0
- package/dist/services/interfaces/api-key.service.interface.js +1 -0
- package/dist/services/interfaces/user.dto.js +2 -0
- package/dist/services/interfaces/user.dto.js.map +1 -1
- package/dist/services/moonraker/moonraker-websocket.adapter.js.map +1 -1
- package/dist/services/moonraker/moonraker.client.js.map +1 -1
- package/dist/services/moonraker.api.js.map +1 -1
- package/dist/services/octoprint/octoprint-api.routes.js.map +1 -1
- package/dist/services/octoprint/octoprint-websocket.adapter.js.map +1 -1
- package/dist/services/octoprint/octoprint.client.js.map +1 -1
- package/dist/services/octoprint/utils/api.utils.js.map +1 -1
- package/dist/services/octoprint/utils/file.utils.js.map +1 -1
- package/dist/services/octoprint/utils/octoprint-http-client.builder.js.map +1 -1
- package/dist/services/octoprint.api.js.map +1 -1
- package/dist/services/orm/api-key.service.js +90 -0
- package/dist/services/orm/api-key.service.js.map +1 -0
- package/dist/services/orm/base.service.js.map +1 -1
- package/dist/services/orm/camera-stream.service.js.map +1 -1
- package/dist/services/orm/floor-position.service.js.map +1 -1
- package/dist/services/orm/floor.service.js.map +1 -1
- package/dist/services/orm/permission.service.js.map +1 -1
- package/dist/services/orm/print-job.service.js.map +1 -1
- package/dist/services/orm/printer-maintenance-log.service.js.map +1 -1
- package/dist/services/orm/printer-tag.service.js.map +1 -1
- package/dist/services/orm/printer.service.js.map +1 -1
- package/dist/services/orm/refresh-token.service.js.map +1 -1
- package/dist/services/orm/role.service.js.map +1 -1
- package/dist/services/orm/settings.service.js.map +1 -1
- package/dist/services/orm/user-role.service.js.map +1 -1
- package/dist/services/orm/user.service.js.map +1 -1
- package/dist/services/print-file-downloader.service.js.map +1 -1
- package/dist/services/print-queue.service.js.map +1 -1
- package/dist/services/printer-api.factory.js.map +1 -1
- package/dist/services/printer-api.interface.js.map +1 -1
- package/dist/services/prusa-link/prusa-link-http-polling.adapter.js.map +1 -1
- package/dist/services/prusa-link/prusa-link.api.js.map +1 -1
- package/dist/services/prusa-link/utils/digest-auth.util.js +19 -12
- package/dist/services/prusa-link/utils/digest-auth.util.js.map +1 -1
- package/dist/services/prusa-link/utils/prusa-link-http-client.builder.js +45 -11
- package/dist/services/prusa-link/utils/prusa-link-http-client.builder.js.map +1 -1
- package/dist/services/socket.factory.js.map +1 -1
- package/dist/services/task-manager.service.js.map +1 -1
- package/dist/services/typeorm/typeorm.service.js.map +1 -1
- package/dist/services/validators/printer-service.validation.js.map +1 -1
- package/dist/shared/default-http-client.builder.js.map +1 -1
- package/dist/shared/load-controllers.js.map +1 -1
- package/dist/shared/runtime-settings.migration.js.map +1 -1
- package/dist/shared/websocket-rpc-extended.adapter.js.map +1 -1
- package/dist/shared/websocket.adapter.js.map +1 -1
- package/dist/state/file-upload-tracker.cache.js.map +1 -1
- package/dist/state/floor.store.js.map +1 -1
- package/dist/state/printer-events.cache.js.map +1 -1
- package/dist/state/printer-socket.store.js.map +1 -1
- package/dist/state/printer-thumbnail.cache.js.map +1 -1
- package/dist/state/printer.cache.js.map +1 -1
- package/dist/state/settings.store.js.map +1 -1
- package/dist/state/socket-io.gateway.js.map +1 -1
- package/dist/state/test-printer-socket.store.js.map +1 -1
- package/dist/tasks/boot.task.js.map +1 -1
- package/dist/tasks/client-bundle.task.js.map +1 -1
- package/dist/tasks/print-job-analysis.task.js.map +1 -1
- package/dist/tasks/printer-websocket-restore.task.js.map +1 -1
- package/dist/tasks/printer-websocket.task.js.map +1 -1
- package/dist/tasks/socketio.task.js.map +1 -1
- package/dist/tasks/software-update.task.js.map +1 -1
- package/dist/tasks.js.map +1 -1
- package/dist/utils/array.util.js.map +1 -1
- package/dist/utils/bgcode/bgcode-thumbnail.parser.js.map +1 -1
- package/dist/utils/bgcode/bgcode.utils.js.map +1 -1
- package/dist/utils/bgcode/heatshrink-decoder.js.map +1 -1
- package/dist/utils/bgcode/png-encoder.js.map +1 -1
- package/dist/utils/bgcode/qoi-decoder.js.map +1 -1
- package/dist/utils/cache/key-diff.cache.js.map +1 -1
- package/dist/utils/correlation-token.util.js.map +1 -1
- package/dist/utils/crypto.utils.js.map +1 -1
- package/dist/utils/env.utils.js.map +1 -1
- package/dist/utils/error.utils.js.map +1 -1
- package/dist/utils/fs.utils.js.map +1 -1
- package/dist/utils/gcode.utils.js.map +1 -1
- package/dist/utils/image-dimensions.js.map +1 -1
- package/dist/utils/job-stats.util.js.map +1 -1
- package/dist/utils/normalize-url.js.map +1 -1
- package/dist/utils/parsers/3mf.parser.js.map +1 -1
- package/dist/utils/parsers/bgcode.parser.js.map +1 -1
- package/dist/utils/parsers/gcode.parser.js.map +1 -1
- package/dist/utils/pretty-print.utils.js.map +1 -1
- package/dist/utils/semver.utils.js.map +1 -1
- package/dist/utils/swagger/decorators.js.map +1 -1
- package/dist/utils/swagger/generator.js.map +1 -1
- package/dist/utils/swagger/swagger.js.map +1 -1
- package/dist/utils/thumbnail.util.js.map +1 -1
- package/dist/utils/time.utils.js.map +1 -1
- package/dist/utils/url.utils.js.map +1 -1
- package/package.json +17 -14
- package/packages/consoles/package.json +4 -4
|
@@ -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;
|
|
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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yaml.service.js","names":[],"sources":["../../../src/services/core/yaml.service.ts"],"sourcesContent":["import { validateInput } from \"@/handlers/validators\";\nimport {\n exportPrintersFloorsYamlSchema,\n importPrinterPositionsSchema,\n importPrintersFloorsYamlSchema,\n YamlExportSchema,\n} from \"../validators/yaml-service.validation\";\nimport { dump, load } from \"js-yaml\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { PrinterCache } from \"@/state/printer.cache\";\nimport { FloorStore } from \"@/state/floor.store\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport type { IPrinterService } from \"@/services/interfaces/printer.service.interface\";\nimport type { IFloorService } from \"@/services/interfaces/floor.service.interface\";\nimport type { IPrinterTagService } from \"@/services/interfaces/printer-tag.service.interface\";\nimport { BambuType, MoonrakerType, OctoprintType, PrusaLinkType } from \"@/services/printer-api.interface\";\nimport { z } from \"zod\";\nimport type { IUserService } from \"@/services/interfaces/user-service.interface\";\nimport type { IRoleService } from \"@/services/interfaces/role-service.interface\";\nimport { SettingsStore } from \"@/state/settings.store\";\n\nexport class YamlService {\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly printerTagService: IPrinterTagService,\n private readonly printerService: IPrinterService,\n private readonly printerCache: PrinterCache,\n private readonly floorStore: FloorStore,\n private readonly floorService: IFloorService,\n private readonly userService: IUserService,\n private readonly roleService: IRoleService,\n private readonly settingsStore: SettingsStore,\n ) {\n this.logger = loggerFactory(YamlService.name);\n }\n\n async importYaml(yamlBuffer: string) {\n const importSpec = (await load(yamlBuffer)) as YamlExportSchema;\n const databaseTypeSqlite = importSpec.databaseType === \"sqlite\";\n const { exportPrinters, exportFloorGrid, exportSettings, exportUsers } = importSpec.config;\n\n // Normalize data before validation\n this.normalizeYamlData(importSpec, databaseTypeSqlite);\n\n const importData = await validateInput(importSpec, importPrintersFloorsYamlSchema);\n\n // Validate that system tables can be imported - always check if system data is present in backup\n const hasSystemData = exportSettings || exportUsers;\n if (hasSystemData) {\n await this.validateSystemTablesEmpty(importSpec);\n\n // Import system tables if present and validation passed\n if (exportSettings && importSpec.settings) {\n this.logger.log(\"Importing settings\");\n await this.importSettings(importSpec.settings);\n }\n\n if (exportUsers && importSpec.users && importSpec.users.length > 0) {\n this.logger.log(`Importing users (${importSpec.users.length} users)`);\n await this.importUsers(importSpec.users, databaseTypeSqlite);\n }\n }\n\n // Nested validation is manual (for now)\n if (exportFloorGrid && importData.floors?.length) {\n for (const floor of importData.floors) {\n await validateInput(floor, importPrinterPositionsSchema);\n }\n }\n\n this.logger.log(\"Analysing printers for import\");\n const { updateByPropertyPrinters, insertPrinters } = await this.analysePrintersUpsert(\n importData.printers ?? [],\n importData.config.printerComparisonStrategiesByPriority,\n );\n\n this.logger.log(\"Analysing floors for import\");\n const { updateByPropertyFloors, insertFloors } = await this.analyseFloorsUpsert(\n importData.floors ?? [],\n importData.config.floorComparisonStrategiesByPriority,\n );\n\n this.logger.log(\"Analysing tags for import\");\n const { updateByNameTags, insertTags } = await this.analyseUpsertTags(importData.tags ?? []);\n\n this.logger.log(`Performing pure insert printers (${insertPrinters.length} printers)`);\n const printerIdMap: { [k: number]: number } = {};\n for (const newPrinter of insertPrinters) {\n try {\n const state = await this.printerService.create({ ...newPrinter });\n if (!newPrinter.id) {\n throw new Error(`Saved ID was empty ${JSON.stringify(newPrinter)}`);\n }\n printerIdMap[newPrinter.id] = state.id;\n } catch (error) {\n this.logger.error(`Failed to create printer ${newPrinter.name}:`, error);\n // Continue with next printer - don't let one failure break the entire import\n }\n }\n\n this.logger.log(`Performing update import printers (${updateByPropertyPrinters.length} printers)`);\n for (const updatePrinterSpec of updateByPropertyPrinters) {\n try {\n const updateId = updatePrinterSpec.printerId;\n const updatedPrinter = updatePrinterSpec.value;\n if (typeof updateId === \"string\") {\n throw new Error(\"Cannot update a printer by string id in sqlite mode\");\n }\n\n const originalPrinterId = updatedPrinter.id;\n delete updatePrinterSpec.value.id;\n\n updatedPrinter.id = updateId;\n const state = await this.printerService.update(updateId, updatedPrinter);\n if (!updatePrinterSpec.printerId) {\n throw new Error(\"Saved ID was empty\");\n }\n\n printerIdMap[originalPrinterId] = state.id;\n } catch (error) {\n this.logger.error(`Failed to update printer ${updatePrinterSpec.value.name}:`, error);\n // Continue with next printer - don't let one failure break the entire import\n }\n }\n\n this.logger.log(`Performing pure create floors (${insertFloors.length} floors)`);\n const floorIdMap: { [k: number]: number } = {};\n for (const newFloor of insertFloors) {\n try {\n const originalFloorId = newFloor.id as number;\n delete newFloor.id;\n\n // Replace printerIds with newly mapped IDs\n const knownPrinterPositions = [];\n\n if (exportFloorGrid && exportPrinters) {\n for (const floorPosition of newFloor.printers) {\n const knownPrinterId = printerIdMap[floorPosition.printerId];\n // If the ID was not mapped, this position is considered discarded\n if (!knownPrinterId) {\n continue;\n }\n\n delete floorPosition.id;\n delete floorPosition.floorId;\n floorPosition.printerId = knownPrinterId;\n knownPrinterPositions.push(floorPosition);\n }\n newFloor.printers = knownPrinterPositions;\n }\n\n const createdFloor = await this.floorStore.create({ ...newFloor });\n floorIdMap[originalFloorId] = createdFloor.id;\n } catch (error) {\n this.logger.error(`Failed to create floor ${newFloor.name}:`, error);\n // Continue with next floor - don't let one failure break the entire import\n }\n }\n\n this.logger.log(`Performing update of floors (${updateByPropertyFloors.length} floors)`);\n for (const updateFloorSpec of updateByPropertyFloors) {\n try {\n const updateId = updateFloorSpec.floorId;\n\n if (typeof updateId === \"string\") {\n throw new Error(\"Cannot update a floor by string id in sqlite mode\");\n }\n\n const updatedFloor = updateFloorSpec.value;\n const originalFloorId = updatedFloor.id;\n delete updatedFloor.id;\n\n const knownPrinters = [];\n if (exportFloorGrid && exportPrinters) {\n for (const floorPosition of updatedFloor?.printers) {\n const knownPrinterId = printerIdMap[floorPosition.printerId];\n // If the ID was not mapped, this position is considered discarded\n if (!knownPrinterId) {\n continue;\n }\n\n // Purge ids that might be of wrong type or format\n delete floorPosition.id;\n delete floorPosition.floorId;\n floorPosition.printerId = knownPrinterId;\n floorPosition.floorId = updateId;\n knownPrinters.push(floorPosition);\n }\n updatedFloor.id = updateId;\n updatedFloor.printers = knownPrinters;\n }\n const newFloor = await this.floorStore.update(updateId, updatedFloor);\n floorIdMap[originalFloorId] = newFloor.id;\n } catch (error) {\n this.logger.error(`Failed to update floor ${updateFloorSpec.value.name}:`, error);\n // Continue with next floor - don't let one failure break the entire import\n }\n }\n\n await this.floorStore.loadStore();\n\n this.logger.log(`Performing pure create tags (${insertTags.length} tags)`);\n for (const tag of insertTags) {\n try {\n const createdTag = await this.printerTagService.createTag({\n name: tag.name,\n color: tag.color,\n });\n for (const printer of tag.printers) {\n const knownPrinterId = printerIdMap[printer.printerId] satisfies number | undefined;\n // If the ID was not mapped, this position is considered discarded\n if (!knownPrinterId) continue;\n try {\n await this.printerTagService.addPrinterToTag(createdTag.id, knownPrinterId);\n } catch (error) {\n this.logger.error(`Failed to add printer ${knownPrinterId} to tag ${tag.name}:`, error);\n // Continue with next printer in tag\n }\n }\n } catch (error) {\n this.logger.error(`Failed to create tag ${tag.name}:`, error);\n // Continue with next tag - don't let one failure break the entire import\n }\n }\n\n this.logger.log(`Performing update of tag printer links (${updateByNameTags.length} tags)`);\n for (const updateTagSpec of updateByNameTags) {\n try {\n const existingTag = await this.printerTagService.getPrintersByTag(updateTagSpec.tagId);\n const existingPrinterIds = existingTag.printers.map((p) => p.printerId);\n const wantedTargetPrinterIds = (updateTagSpec.value.printers as { printerId: number }[])\n .filter((p) => !!printerIdMap[p.printerId])\n .map((p) => printerIdMap[p.printerId]);\n\n for (const unwantedId of existingPrinterIds.filter((eid) => !wantedTargetPrinterIds.includes(eid))) {\n try {\n await this.printerTagService.removePrinterFromTag(existingTag.id, unwantedId);\n } catch (error) {\n this.logger.error(`Failed to remove printer ${unwantedId} from tag ${existingTag.name}:`, error);\n // Continue with next printer\n }\n }\n for (const nonExistingNewId of wantedTargetPrinterIds.filter((eid) => !existingPrinterIds.includes(eid))) {\n try {\n await this.printerTagService.addPrinterToTag(existingTag.id, nonExistingNewId);\n } catch (error) {\n this.logger.error(`Failed to add printer ${nonExistingNewId} to tag ${existingTag.name}:`, error);\n // Continue with next printer\n }\n }\n } catch (error) {\n this.logger.error(`Failed to update tag ${updateTagSpec.value.name}:`, error);\n // Continue with next tag - don't let one failure break the entire import\n }\n }\n\n return {\n updateByPropertyPrinters,\n updateByPropertyFloors,\n insertPrinters,\n insertFloors,\n printerIdMap,\n floorIdMap,\n };\n }\n\n async importSettings(settings: any) {\n // Update the settings store with imported settings using individual update methods\n if (settings.server) {\n await this.settingsStore.updateServerSettings(settings.server);\n }\n if (settings.timeout) {\n await this.settingsStore.updateTimeoutSettings(settings.timeout);\n }\n if (settings.frontend) {\n await this.settingsStore.updateFrontendSettings(settings.frontend);\n }\n\n if (settings.wizard?.wizardCompleted) {\n const importedWizardVersion: number = settings.wizard.wizardVersion;\n this.logger.log(`Marking wizard as completed with version: ${importedWizardVersion}`);\n await this.settingsStore.setWizardCompleted(importedWizardVersion);\n }\n\n if (settings.credential) {\n const { jwtExpiresIn, refreshTokenAttempts, refreshTokenExpiry, slicerApiKey } = settings.credential;\n if (jwtExpiresIn || refreshTokenAttempts || refreshTokenExpiry) {\n await this.settingsStore.updateCoreCredentialSettings({\n jwtExpiresIn: jwtExpiresIn ?? (await this.settingsStore.getCredentialSettings()).jwtExpiresIn,\n refreshTokenAttempts:\n refreshTokenAttempts ?? (await this.settingsStore.getCredentialSettings()).refreshTokenAttempts,\n refreshTokenExpiry:\n refreshTokenExpiry ?? (await this.settingsStore.getCredentialSettings()).refreshTokenExpiry,\n });\n this.logger.log(\"Imported credential settings\");\n }\n\n if (slicerApiKey) {\n await this.settingsStore.setSlicerApiKey(slicerApiKey);\n this.logger.log(\"Imported slicer API key\");\n }\n }\n\n await this.settingsStore.loadSettings();\n this.logger.log(\"Settings imported successfully\");\n }\n\n async importUsers(users: any[], databaseTypeSqlite: boolean) {\n const allRoles = this.roleService.roles;\n\n for (const user of users) {\n // Ensure ID type matches database type\n if (databaseTypeSqlite && typeof user.id === \"string\") {\n user.id = Number.parseInt(user.id);\n }\n\n // Filter to valid role names only\n const roleNames = (user.roles ?? []).filter((roleName: string) => allRoles.some((r) => r.name === roleName));\n\n // Register user with a temporary password (will be replaced with actual hash)\n await this.userService.register({\n username: user.username,\n password: \"temporary-password-to-be-replaced\",\n roles: roleNames,\n isRootUser: user.isRootUser ?? false,\n isDemoUser: user.isDemoUser ?? false,\n isVerified: user.isVerified ?? false,\n needsPasswordChange: user.needsPasswordChange ?? true,\n });\n\n // Update the password hash directly (without re-hashing)\n await this.userService.updatePasswordHashUnsafeByUsername(user.username, user.passwordHash);\n }\n this.logger.log(`Imported ${users.length} users`);\n }\n\n async validateSystemTablesEmpty(importSpec: YamlExportSchema) {\n const errors: string[] = [];\n\n // Only validate if wizard is completed - skip during first-time setup\n const wizardCompleted = this.settingsStore.getWizardSettings()?.wizardCompleted;\n if (!wizardCompleted) {\n return;\n }\n\n if (importSpec.settings && importSpec.config.exportSettings) {\n const existingSettings = this.settingsStore.getSettings();\n if (existingSettings && Object.keys(existingSettings).length > 0) {\n errors.push(\"Settings table is not empty. Cannot import settings when existing settings are present.\");\n }\n }\n\n if (importSpec.users && importSpec.users.length > 0 && importSpec.config.exportUsers) {\n const existingUsers = await this.userService.listUsers(1);\n if (existingUsers.length > 0) {\n errors.push(\"Users table is not empty. Cannot import users when existing users are present.\");\n }\n }\n\n if (errors.length > 0) {\n throw new Error(`Import validation failed:\\n${errors.join(\"\\n\")}`);\n }\n }\n\n async analysePrintersUpsert(upsertPrinters: any[], comparisonStrategies: string[]) {\n const existingPrinters = await this.printerService.list();\n\n const names = existingPrinters.map((p) => p.name.toLowerCase());\n const urls = existingPrinters.map((p) => p.printerURL);\n const ids = existingPrinters.map((p) => p.id.toString());\n\n const insertPrinters = [];\n const updateByPropertyPrinters = [];\n for (const printer of upsertPrinters) {\n for (const strategy of [...comparisonStrategies, \"new\"]) {\n if (strategy === \"name\") {\n const comparedName = printer.name.toLowerCase();\n const foundIndex = names.findIndex((n) => n === comparedName);\n if (foundIndex !== -1) {\n if (!ids[foundIndex]) {\n throw new Error(\"Update ID is undefined\");\n }\n updateByPropertyPrinters.push({\n strategy: \"name\",\n printerId: Number.parseInt(ids[foundIndex]),\n value: printer,\n });\n break;\n }\n } else if (strategy === \"url\") {\n const comparedName = printer.printerURL.toLowerCase();\n const foundIndex = urls.findIndex((n) => n === comparedName);\n if (foundIndex !== -1) {\n if (!ids[foundIndex]) {\n throw new Error(\"Update ID is undefined\");\n }\n updateByPropertyPrinters.push({\n strategy: \"url\",\n printerId: Number.parseInt(ids[foundIndex]),\n value: printer,\n });\n break;\n }\n } else if (strategy === \"new\") {\n if (!printer.id) {\n throw new Error(JSON.stringify(printer));\n }\n insertPrinters.push(printer);\n break;\n }\n }\n }\n\n return {\n updateByPropertyPrinters,\n insertPrinters,\n };\n }\n\n async analyseFloorsUpsert(upsertFloors: any[], comparisonStrategy: string) {\n const existingFloors = await this.floorService.list();\n const names = existingFloors.map((p) => p.name.toLowerCase());\n const floorLevels = existingFloors.map((p) => p.order);\n const ids = existingFloors.map((p) => p.id.toString());\n\n const insertFloors = [];\n const updateByPropertyFloors = [];\n for (const floor of upsertFloors) {\n for (const strategy of [comparisonStrategy, \"new\"]) {\n if (strategy === \"name\") {\n const comparedProperty = floor.name.toLowerCase();\n const foundIndex = names.findIndex((n) => n === comparedProperty);\n if (foundIndex !== -1) {\n if (!ids[foundIndex]) {\n throw new Error(\"IDS not found, floor name\");\n }\n updateByPropertyFloors.push({\n strategy: \"name\",\n floorId: Number.parseInt(ids[foundIndex]),\n value: floor,\n });\n break;\n }\n } else if (strategy === \"floor\") {\n const comparedProperty = floor.order;\n const foundIndex = floorLevels.findIndex((n) => n === comparedProperty);\n if (foundIndex !== -1) {\n if (!ids[foundIndex]) {\n throw new Error(\"IDS not found, floor level\");\n }\n updateByPropertyFloors.push({\n strategy: \"floor\",\n floorId: Number.parseInt(ids[foundIndex]),\n value: floor,\n });\n break;\n }\n } else if (strategy === \"new\") {\n insertFloors.push(floor);\n break;\n }\n }\n }\n\n return {\n updateByPropertyFloors,\n insertFloors,\n };\n }\n\n async analyseUpsertTags(upsertTags: any[]) {\n if (!upsertTags?.length) {\n return {\n updateByNameTags: [],\n insertTags: [],\n };\n }\n\n const existingTags = await this.printerTagService.listTags();\n const names = existingTags.map((p) => p.name.toLowerCase());\n const ids = existingTags.map((p) => p.id.toString());\n\n const insertTags = [];\n const updateByNameTags = [];\n for (const tag of upsertTags) {\n const comparedProperty = tag.name.toLowerCase();\n const foundIndex = names.indexOf(comparedProperty);\n if (foundIndex === -1) {\n insertTags.push(tag);\n } else {\n if (!ids[foundIndex]) {\n throw new Error(\"IDS not found, tag name\");\n }\n updateByNameTags.push({\n strategy: \"name\",\n tagId: Number.parseInt(ids[foundIndex]),\n value: tag,\n });\n break;\n }\n }\n\n return {\n insertTags,\n updateByNameTags,\n };\n }\n\n async exportYaml(options: z.infer<typeof exportPrintersFloorsYamlSchema>) {\n const input = await validateInput(options, exportPrintersFloorsYamlSchema);\n\n const { exportFloors, exportPrinters, exportFloorGrid, exportTags, exportSettings, exportUsers } = input;\n\n const dumpedObject = {\n version: process.env.npm_package_version,\n exportedAt: new Date(),\n databaseType: \"sqlite\",\n config: input,\n printers: undefined as any,\n floors: undefined as any,\n tags: undefined as any,\n settings: undefined as any,\n users: undefined as any,\n user_roles: undefined as any,\n };\n\n if (exportPrinters) {\n const printers = await this.printerService.list();\n dumpedObject.printers = printers.map((p) => {\n const printerId = p.id;\n const { apiKey, username, password } = this.printerCache.getLoginDto(printerId);\n return {\n id: printerId,\n disabledReason: p.disabledReason,\n enabled: p.enabled,\n printerType: p.printerType,\n dateAdded: p.dateAdded,\n name: p.name,\n printerURL: p.printerURL,\n apiKey,\n username,\n password,\n assignee: p.assignee,\n flowRate: p.flowRate,\n feedRate: p.feedRate,\n };\n });\n }\n\n if (exportFloors) {\n const floors = await this.floorStore.listCache();\n dumpedObject.floors = floors.map((f) => {\n const dumpedFloor = {\n id: f.id,\n order: f.order,\n name: f.name,\n printers: undefined as any,\n };\n\n if (exportFloorGrid) {\n dumpedFloor.printers = f.printers.map((p) => {\n const fPrinterId = p.printerId;\n return {\n printerId: fPrinterId,\n x: p.x,\n y: p.y,\n };\n });\n }\n\n return dumpedFloor;\n });\n }\n\n if (exportTags) {\n const tags = await this.printerTagService.listTags();\n dumpedObject.tags = tags.map((t) => {\n return {\n name: t.name,\n id: t.id,\n printers: t.printers.map((p) => {\n return {\n printerId: p.printerId,\n };\n }),\n };\n });\n }\n\n if (exportSettings) {\n dumpedObject.settings = {\n ...this.settingsStore.getSettings(),\n ...this.settingsStore.getSettingsSensitive(),\n };\n }\n\n if (exportUsers) {\n const users = await this.userService.listUsers(1000);\n\n dumpedObject.users = users.map((u) => {\n const userDto = this.userService.toDto(u);\n return {\n id: userDto.id,\n username: userDto.username,\n isDemoUser: userDto.isDemoUser,\n isRootUser: userDto.isRootUser,\n isVerified: userDto.isVerified,\n needsPasswordChange: userDto.needsPasswordChange,\n passwordHash: u.passwordHash,\n createdAt: userDto.createdAt,\n roles: userDto.roles,\n };\n });\n }\n\n return dump(dumpedObject, {});\n }\n\n private normalizeYamlData(importSpec: YamlExportSchema, databaseTypeSqlite: boolean) {\n // Normalize printer data\n for (const printer of importSpec.printers) {\n // old export bug\n if (!printer.name && printer.printerName) {\n printer.name = printer.printerName;\n delete printer.printerName;\n }\n // 1.5.2 schema\n if (printer.settingsAppearance?.name) {\n printer.name = printer.settingsAppearance?.name;\n delete printer.settingsAppearance?.name;\n }\n // Ensure the type matches the database it came from (1.6.0+)\n if (databaseTypeSqlite && typeof printer.id === \"string\") {\n printer.id = Number.parseInt(printer.id);\n }\n\n // 1.7 backwards compatibility\n if (![OctoprintType, MoonrakerType, PrusaLinkType, BambuType].includes(printer.printerType)) {\n printer.printerType = OctoprintType;\n }\n }\n\n // Normalize floor data\n for (const floor of importSpec.floors) {\n // Migrate legacy 'floor' property to 'order'\n if (floor.floor !== undefined && floor.order === undefined) {\n floor.order = floor.floor;\n delete floor.floor;\n }\n\n // Normalize IDs for sqlite\n if (databaseTypeSqlite) {\n if (typeof floor.id === \"string\") {\n floor.id = Number.parseInt(floor.id);\n }\n // Ensure the type matches the database it came from (1.6.0+)\n for (const printer of floor.printers) {\n if (typeof printer.printerId === \"string\") {\n printer.printerId = Number.parseInt(printer.printerId);\n }\n }\n }\n }\n\n // Normalize groups to tags (backwards compatibility)\n if ((importSpec as any).groups && !importSpec.tags) {\n importSpec.tags = (importSpec as any).groups;\n delete (importSpec as any).groups;\n }\n\n // Normalize exportGroups to exportTags in config (backwards compatibility)\n if ((importSpec.config as any).exportGroups !== undefined && importSpec.config.exportTags === undefined) {\n (importSpec.config as any).exportTags = (importSpec.config as any).exportGroups;\n delete (importSpec.config as any).exportGroups;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"yaml.service.js","names":[],"sources":["../../../src/services/core/yaml.service.ts"],"sourcesContent":["import { validateInput } from \"@/handlers/validators\";\nimport {\n exportPrintersFloorsYamlSchema,\n importPrinterPositionsSchema,\n importPrintersFloorsYamlSchema,\n YamlExportSchema,\n} from \"../validators/yaml-service.validation\";\nimport { dump, load } from \"js-yaml\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { PrinterCache } from \"@/state/printer.cache\";\nimport { FloorStore } from \"@/state/floor.store\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport type { IPrinterService } from \"@/services/interfaces/printer.service.interface\";\nimport type { IFloorService } from \"@/services/interfaces/floor.service.interface\";\nimport type { IPrinterTagService } from \"@/services/interfaces/printer-tag.service.interface\";\nimport { BambuType, MoonrakerType, OctoprintType, PrusaLinkType } from \"@/services/printer-api.interface\";\nimport { z } from \"zod\";\nimport type { IUserService } from \"@/services/interfaces/user-service.interface\";\nimport type { IRoleService } from \"@/services/interfaces/role-service.interface\";\nimport { SettingsStore } from \"@/state/settings.store\";\n\nexport class YamlService {\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly printerTagService: IPrinterTagService,\n private readonly printerService: IPrinterService,\n private readonly printerCache: PrinterCache,\n private readonly floorStore: FloorStore,\n private readonly floorService: IFloorService,\n private readonly userService: IUserService,\n private readonly roleService: IRoleService,\n private readonly settingsStore: SettingsStore,\n ) {\n this.logger = loggerFactory(YamlService.name);\n }\n\n async importYaml(yamlBuffer: string) {\n const importSpec = (await load(yamlBuffer)) as YamlExportSchema;\n const databaseTypeSqlite = importSpec.databaseType === \"sqlite\";\n const { exportPrinters, exportFloorGrid, exportSettings, exportUsers } = importSpec.config;\n\n // Normalize data before validation\n this.normalizeYamlData(importSpec, databaseTypeSqlite);\n\n const importData = await validateInput(importSpec, importPrintersFloorsYamlSchema);\n\n // Validate that system tables can be imported - always check if system data is present in backup\n const hasSystemData = exportSettings || exportUsers;\n if (hasSystemData) {\n await this.validateSystemTablesEmpty(importSpec);\n\n // Import system tables if present and validation passed\n if (exportSettings && importSpec.settings) {\n this.logger.log(\"Importing settings\");\n await this.importSettings(importSpec.settings);\n }\n\n if (exportUsers && importSpec.users && importSpec.users.length > 0) {\n this.logger.log(`Importing users (${importSpec.users.length} users)`);\n await this.importUsers(importSpec.users, databaseTypeSqlite);\n }\n }\n\n // Nested validation is manual (for now)\n if (exportFloorGrid && importData.floors?.length) {\n for (const floor of importData.floors) {\n await validateInput(floor, importPrinterPositionsSchema);\n }\n }\n\n this.logger.log(\"Analysing printers for import\");\n const { updateByPropertyPrinters, insertPrinters } = await this.analysePrintersUpsert(\n importData.printers ?? [],\n importData.config.printerComparisonStrategiesByPriority,\n );\n\n this.logger.log(\"Analysing floors for import\");\n const { updateByPropertyFloors, insertFloors } = await this.analyseFloorsUpsert(\n importData.floors ?? [],\n importData.config.floorComparisonStrategiesByPriority,\n );\n\n this.logger.log(\"Analysing tags for import\");\n const { updateByNameTags, insertTags } = await this.analyseUpsertTags(importData.tags ?? []);\n\n this.logger.log(`Performing pure insert printers (${insertPrinters.length} printers)`);\n const printerIdMap: { [k: number]: number } = {};\n for (const newPrinter of insertPrinters) {\n try {\n const state = await this.printerService.create({ ...newPrinter });\n if (!newPrinter.id) {\n throw new Error(`Saved ID was empty ${JSON.stringify(newPrinter)}`);\n }\n printerIdMap[newPrinter.id] = state.id;\n } catch (error) {\n this.logger.error(`Failed to create printer ${newPrinter.name}:`, error);\n // Continue with next printer - don't let one failure break the entire import\n }\n }\n\n this.logger.log(`Performing update import printers (${updateByPropertyPrinters.length} printers)`);\n for (const updatePrinterSpec of updateByPropertyPrinters) {\n try {\n const updateId = updatePrinterSpec.printerId;\n const updatedPrinter = updatePrinterSpec.value;\n if (typeof updateId === \"string\") {\n throw new Error(\"Cannot update a printer by string id in sqlite mode\");\n }\n\n const originalPrinterId = updatedPrinter.id;\n delete updatePrinterSpec.value.id;\n\n updatedPrinter.id = updateId;\n const state = await this.printerService.update(updateId, updatedPrinter);\n if (!updatePrinterSpec.printerId) {\n throw new Error(\"Saved ID was empty\");\n }\n\n printerIdMap[originalPrinterId] = state.id;\n } catch (error) {\n this.logger.error(`Failed to update printer ${updatePrinterSpec.value.name}:`, error);\n // Continue with next printer - don't let one failure break the entire import\n }\n }\n\n this.logger.log(`Performing pure create floors (${insertFloors.length} floors)`);\n const floorIdMap: { [k: number]: number } = {};\n for (const newFloor of insertFloors) {\n try {\n const originalFloorId = newFloor.id as number;\n delete newFloor.id;\n\n // Replace printerIds with newly mapped IDs\n const knownPrinterPositions = [];\n\n if (exportFloorGrid && exportPrinters) {\n for (const floorPosition of newFloor.printers) {\n const knownPrinterId = printerIdMap[floorPosition.printerId];\n // If the ID was not mapped, this position is considered discarded\n if (!knownPrinterId) {\n continue;\n }\n\n delete floorPosition.id;\n delete floorPosition.floorId;\n floorPosition.printerId = knownPrinterId;\n knownPrinterPositions.push(floorPosition);\n }\n newFloor.printers = knownPrinterPositions;\n }\n\n const createdFloor = await this.floorStore.create({ ...newFloor });\n floorIdMap[originalFloorId] = createdFloor.id;\n } catch (error) {\n this.logger.error(`Failed to create floor ${newFloor.name}:`, error);\n // Continue with next floor - don't let one failure break the entire import\n }\n }\n\n this.logger.log(`Performing update of floors (${updateByPropertyFloors.length} floors)`);\n for (const updateFloorSpec of updateByPropertyFloors) {\n try {\n const updateId = updateFloorSpec.floorId;\n\n if (typeof updateId === \"string\") {\n throw new Error(\"Cannot update a floor by string id in sqlite mode\");\n }\n\n const updatedFloor = updateFloorSpec.value;\n const originalFloorId = updatedFloor.id;\n delete updatedFloor.id;\n\n const knownPrinters = [];\n if (exportFloorGrid && exportPrinters) {\n for (const floorPosition of updatedFloor?.printers) {\n const knownPrinterId = printerIdMap[floorPosition.printerId];\n // If the ID was not mapped, this position is considered discarded\n if (!knownPrinterId) {\n continue;\n }\n\n // Purge ids that might be of wrong type or format\n delete floorPosition.id;\n delete floorPosition.floorId;\n floorPosition.printerId = knownPrinterId;\n floorPosition.floorId = updateId;\n knownPrinters.push(floorPosition);\n }\n updatedFloor.id = updateId;\n updatedFloor.printers = knownPrinters;\n }\n const newFloor = await this.floorStore.update(updateId, updatedFloor);\n floorIdMap[originalFloorId] = newFloor.id;\n } catch (error) {\n this.logger.error(`Failed to update floor ${updateFloorSpec.value.name}:`, error);\n // Continue with next floor - don't let one failure break the entire import\n }\n }\n\n await this.floorStore.loadStore();\n\n this.logger.log(`Performing pure create tags (${insertTags.length} tags)`);\n for (const tag of insertTags) {\n try {\n const createdTag = await this.printerTagService.createTag({\n name: tag.name,\n color: tag.color,\n });\n for (const printer of tag.printers) {\n const knownPrinterId = printerIdMap[printer.printerId] satisfies number | undefined;\n // If the ID was not mapped, this position is considered discarded\n if (!knownPrinterId) continue;\n try {\n await this.printerTagService.addPrinterToTag(createdTag.id, knownPrinterId);\n } catch (error) {\n this.logger.error(`Failed to add printer ${knownPrinterId} to tag ${tag.name}:`, error);\n // Continue with next printer in tag\n }\n }\n } catch (error) {\n this.logger.error(`Failed to create tag ${tag.name}:`, error);\n // Continue with next tag - don't let one failure break the entire import\n }\n }\n\n this.logger.log(`Performing update of tag printer links (${updateByNameTags.length} tags)`);\n for (const updateTagSpec of updateByNameTags) {\n try {\n const existingTag = await this.printerTagService.getPrintersByTag(updateTagSpec.tagId);\n const existingPrinterIds = existingTag.printers.map((p) => p.printerId);\n const wantedTargetPrinterIds = (updateTagSpec.value.printers as { printerId: number }[])\n .filter((p) => !!printerIdMap[p.printerId])\n .map((p) => printerIdMap[p.printerId]);\n\n for (const unwantedId of existingPrinterIds.filter((eid) => !wantedTargetPrinterIds.includes(eid))) {\n try {\n await this.printerTagService.removePrinterFromTag(existingTag.id, unwantedId);\n } catch (error) {\n this.logger.error(`Failed to remove printer ${unwantedId} from tag ${existingTag.name}:`, error);\n // Continue with next printer\n }\n }\n for (const nonExistingNewId of wantedTargetPrinterIds.filter((eid) => !existingPrinterIds.includes(eid))) {\n try {\n await this.printerTagService.addPrinterToTag(existingTag.id, nonExistingNewId);\n } catch (error) {\n this.logger.error(`Failed to add printer ${nonExistingNewId} to tag ${existingTag.name}:`, error);\n // Continue with next printer\n }\n }\n } catch (error) {\n this.logger.error(`Failed to update tag ${updateTagSpec.value.name}:`, error);\n // Continue with next tag - don't let one failure break the entire import\n }\n }\n\n return {\n updateByPropertyPrinters,\n updateByPropertyFloors,\n insertPrinters,\n insertFloors,\n printerIdMap,\n floorIdMap,\n };\n }\n\n async importSettings(settings: any) {\n // Update the settings store with imported settings using individual update methods\n if (settings.server) {\n await this.settingsStore.updateServerSettings(settings.server);\n }\n if (settings.timeout) {\n await this.settingsStore.updateTimeoutSettings(settings.timeout);\n }\n if (settings.frontend) {\n await this.settingsStore.updateFrontendSettings(settings.frontend);\n }\n\n if (settings.wizard?.wizardCompleted) {\n const importedWizardVersion: number = settings.wizard.wizardVersion;\n this.logger.log(`Marking wizard as completed with version: ${importedWizardVersion}`);\n await this.settingsStore.setWizardCompleted(importedWizardVersion);\n }\n\n if (settings.credential) {\n const { jwtExpiresIn, refreshTokenAttempts, refreshTokenExpiry, slicerApiKey } = settings.credential;\n if (jwtExpiresIn || refreshTokenAttempts || refreshTokenExpiry) {\n await this.settingsStore.updateCoreCredentialSettings({\n jwtExpiresIn: jwtExpiresIn ?? (await this.settingsStore.getCredentialSettings()).jwtExpiresIn,\n refreshTokenAttempts:\n refreshTokenAttempts ?? (await this.settingsStore.getCredentialSettings()).refreshTokenAttempts,\n refreshTokenExpiry:\n refreshTokenExpiry ?? (await this.settingsStore.getCredentialSettings()).refreshTokenExpiry,\n });\n this.logger.log(\"Imported credential settings\");\n }\n\n if (slicerApiKey) {\n await this.settingsStore.setSlicerApiKey(slicerApiKey);\n this.logger.log(\"Imported slicer API key\");\n }\n }\n\n await this.settingsStore.loadSettings();\n this.logger.log(\"Settings imported successfully\");\n }\n\n async importUsers(users: any[], databaseTypeSqlite: boolean) {\n const allRoles = this.roleService.roles;\n\n for (const user of users) {\n // Ensure ID type matches database type\n if (databaseTypeSqlite && typeof user.id === \"string\") {\n user.id = Number.parseInt(user.id);\n }\n\n // Filter to valid role names only\n const roleNames = (user.roles ?? []).filter((roleName: string) => allRoles.some((r) => r.name === roleName));\n\n // Register user with a temporary password (will be replaced with actual hash)\n await this.userService.register({\n username: user.username,\n password: \"temporary-password-to-be-replaced\",\n roles: roleNames,\n isRootUser: user.isRootUser ?? false,\n isDemoUser: user.isDemoUser ?? false,\n isVerified: user.isVerified ?? false,\n needsPasswordChange: user.needsPasswordChange ?? true,\n });\n\n // Update the password hash directly (without re-hashing)\n await this.userService.updatePasswordHashUnsafeByUsername(user.username, user.passwordHash);\n }\n this.logger.log(`Imported ${users.length} users`);\n }\n\n async validateSystemTablesEmpty(importSpec: YamlExportSchema) {\n const errors: string[] = [];\n\n // Only validate if wizard is completed - skip during first-time setup\n const wizardCompleted = this.settingsStore.getWizardSettings()?.wizardCompleted;\n if (!wizardCompleted) {\n return;\n }\n\n if (importSpec.settings && importSpec.config.exportSettings) {\n const existingSettings = this.settingsStore.getSettings();\n if (existingSettings && Object.keys(existingSettings).length > 0) {\n errors.push(\"Settings table is not empty. Cannot import settings when existing settings are present.\");\n }\n }\n\n if (importSpec.users && importSpec.users.length > 0 && importSpec.config.exportUsers) {\n const existingUsers = await this.userService.listUsers(1);\n if (existingUsers.length > 0) {\n errors.push(\"Users table is not empty. Cannot import users when existing users are present.\");\n }\n }\n\n if (errors.length > 0) {\n throw new Error(`Import validation failed:\\n${errors.join(\"\\n\")}`);\n }\n }\n\n async analysePrintersUpsert(upsertPrinters: any[], comparisonStrategies: string[]) {\n const existingPrinters = await this.printerService.list();\n\n const names = existingPrinters.map((p) => p.name.toLowerCase());\n const urls = existingPrinters.map((p) => p.printerURL);\n const ids = existingPrinters.map((p) => p.id.toString());\n\n const insertPrinters = [];\n const updateByPropertyPrinters = [];\n for (const printer of upsertPrinters) {\n for (const strategy of [...comparisonStrategies, \"new\"]) {\n if (strategy === \"name\") {\n const comparedName = printer.name.toLowerCase();\n const foundIndex = names.findIndex((n) => n === comparedName);\n if (foundIndex !== -1) {\n if (!ids[foundIndex]) {\n throw new Error(\"Update ID is undefined\");\n }\n updateByPropertyPrinters.push({\n strategy: \"name\",\n printerId: Number.parseInt(ids[foundIndex]),\n value: printer,\n });\n break;\n }\n } else if (strategy === \"url\") {\n const comparedName = printer.printerURL.toLowerCase();\n const foundIndex = urls.findIndex((n) => n === comparedName);\n if (foundIndex !== -1) {\n if (!ids[foundIndex]) {\n throw new Error(\"Update ID is undefined\");\n }\n updateByPropertyPrinters.push({\n strategy: \"url\",\n printerId: Number.parseInt(ids[foundIndex]),\n value: printer,\n });\n break;\n }\n } else if (strategy === \"new\") {\n if (!printer.id) {\n throw new Error(JSON.stringify(printer));\n }\n insertPrinters.push(printer);\n break;\n }\n }\n }\n\n return {\n updateByPropertyPrinters,\n insertPrinters,\n };\n }\n\n async analyseFloorsUpsert(upsertFloors: any[], comparisonStrategy: string) {\n const existingFloors = await this.floorService.list();\n const names = existingFloors.map((p) => p.name.toLowerCase());\n const floorLevels = existingFloors.map((p) => p.order);\n const ids = existingFloors.map((p) => p.id.toString());\n\n const insertFloors = [];\n const updateByPropertyFloors = [];\n for (const floor of upsertFloors) {\n for (const strategy of [comparisonStrategy, \"new\"]) {\n if (strategy === \"name\") {\n const comparedProperty = floor.name.toLowerCase();\n const foundIndex = names.findIndex((n) => n === comparedProperty);\n if (foundIndex !== -1) {\n if (!ids[foundIndex]) {\n throw new Error(\"IDS not found, floor name\");\n }\n updateByPropertyFloors.push({\n strategy: \"name\",\n floorId: Number.parseInt(ids[foundIndex]),\n value: floor,\n });\n break;\n }\n } else if (strategy === \"floor\") {\n const comparedProperty = floor.order;\n const foundIndex = floorLevels.findIndex((n) => n === comparedProperty);\n if (foundIndex !== -1) {\n if (!ids[foundIndex]) {\n throw new Error(\"IDS not found, floor level\");\n }\n updateByPropertyFloors.push({\n strategy: \"floor\",\n floorId: Number.parseInt(ids[foundIndex]),\n value: floor,\n });\n break;\n }\n } else if (strategy === \"new\") {\n insertFloors.push(floor);\n break;\n }\n }\n }\n\n return {\n updateByPropertyFloors,\n insertFloors,\n };\n }\n\n async analyseUpsertTags(upsertTags: any[]) {\n if (!upsertTags?.length) {\n return {\n updateByNameTags: [],\n insertTags: [],\n };\n }\n\n const existingTags = await this.printerTagService.listTags();\n const names = existingTags.map((p) => p.name.toLowerCase());\n const ids = existingTags.map((p) => p.id.toString());\n\n const insertTags = [];\n const updateByNameTags = [];\n for (const tag of upsertTags) {\n const comparedProperty = tag.name.toLowerCase();\n const foundIndex = names.indexOf(comparedProperty);\n if (foundIndex === -1) {\n insertTags.push(tag);\n } else {\n if (!ids[foundIndex]) {\n throw new Error(\"IDS not found, tag name\");\n }\n updateByNameTags.push({\n strategy: \"name\",\n tagId: Number.parseInt(ids[foundIndex]),\n value: tag,\n });\n break;\n }\n }\n\n return {\n insertTags,\n updateByNameTags,\n };\n }\n\n async exportYaml(options: z.infer<typeof exportPrintersFloorsYamlSchema>) {\n const input = await validateInput(options, exportPrintersFloorsYamlSchema);\n\n const { exportFloors, exportPrinters, exportFloorGrid, exportTags, exportSettings, exportUsers } = input;\n\n const dumpedObject = {\n version: process.env.npm_package_version,\n exportedAt: new Date(),\n databaseType: \"sqlite\",\n config: input,\n printers: undefined as any,\n floors: undefined as any,\n tags: undefined as any,\n settings: undefined as any,\n users: undefined as any,\n user_roles: undefined as any,\n };\n\n if (exportPrinters) {\n const printers = await this.printerService.list();\n dumpedObject.printers = printers.map((p) => {\n const printerId = p.id;\n const { apiKey, username, password } = this.printerCache.getLoginDto(printerId);\n return {\n id: printerId,\n disabledReason: p.disabledReason,\n enabled: p.enabled,\n printerType: p.printerType,\n dateAdded: p.dateAdded,\n name: p.name,\n printerURL: p.printerURL,\n apiKey,\n username,\n password,\n assignee: p.assignee,\n flowRate: p.flowRate,\n feedRate: p.feedRate,\n };\n });\n }\n\n if (exportFloors) {\n const floors = await this.floorStore.listCache();\n dumpedObject.floors = floors.map((f) => {\n const dumpedFloor = {\n id: f.id,\n order: f.order,\n name: f.name,\n printers: undefined as any,\n };\n\n if (exportFloorGrid) {\n dumpedFloor.printers = f.printers.map((p) => {\n const fPrinterId = p.printerId;\n return {\n printerId: fPrinterId,\n x: p.x,\n y: p.y,\n };\n });\n }\n\n return dumpedFloor;\n });\n }\n\n if (exportTags) {\n const tags = await this.printerTagService.listTags();\n dumpedObject.tags = tags.map((t) => {\n return {\n name: t.name,\n id: t.id,\n printers: t.printers.map((p) => {\n return {\n printerId: p.printerId,\n };\n }),\n };\n });\n }\n\n if (exportSettings) {\n dumpedObject.settings = {\n ...this.settingsStore.getSettings(),\n ...this.settingsStore.getSettingsSensitive(),\n };\n }\n\n if (exportUsers) {\n const users = await this.userService.listUsers(1000);\n\n dumpedObject.users = users.map((u) => {\n const userDto = this.userService.toDto(u);\n return {\n id: userDto.id,\n username: userDto.username,\n isDemoUser: userDto.isDemoUser,\n isRootUser: userDto.isRootUser,\n isVerified: userDto.isVerified,\n needsPasswordChange: userDto.needsPasswordChange,\n passwordHash: u.passwordHash,\n createdAt: userDto.createdAt,\n roles: userDto.roles,\n };\n });\n }\n\n return dump(dumpedObject, {});\n }\n\n private normalizeYamlData(importSpec: YamlExportSchema, databaseTypeSqlite: boolean) {\n // Normalize printer data\n for (const printer of importSpec.printers) {\n // old export bug\n if (!printer.name && printer.printerName) {\n printer.name = printer.printerName;\n delete printer.printerName;\n }\n // 1.5.2 schema\n if (printer.settingsAppearance?.name) {\n printer.name = printer.settingsAppearance?.name;\n delete printer.settingsAppearance?.name;\n }\n // Ensure the type matches the database it came from (1.6.0+)\n if (databaseTypeSqlite && typeof printer.id === \"string\") {\n printer.id = Number.parseInt(printer.id);\n }\n\n // 1.7 backwards compatibility\n if (![OctoprintType, MoonrakerType, PrusaLinkType, BambuType].includes(printer.printerType)) {\n printer.printerType = OctoprintType;\n }\n }\n\n // Normalize floor data\n for (const floor of importSpec.floors) {\n // Migrate legacy 'floor' property to 'order'\n if (floor.floor !== undefined && floor.order === undefined) {\n floor.order = floor.floor;\n delete floor.floor;\n }\n\n // Normalize IDs for sqlite\n if (databaseTypeSqlite) {\n if (typeof floor.id === \"string\") {\n floor.id = Number.parseInt(floor.id);\n }\n // Ensure the type matches the database it came from (1.6.0+)\n for (const printer of floor.printers) {\n if (typeof printer.printerId === \"string\") {\n printer.printerId = Number.parseInt(printer.printerId);\n }\n }\n }\n }\n\n // Normalize groups to tags (backwards compatibility)\n if ((importSpec as any).groups && !importSpec.tags) {\n importSpec.tags = (importSpec as any).groups;\n delete (importSpec as any).groups;\n }\n\n // Normalize exportGroups to exportTags in config (backwards compatibility)\n if ((importSpec.config as any).exportGroups !== undefined && importSpec.config.exportTags === undefined) {\n (importSpec.config as any).exportTags = (importSpec.config as any).exportGroups;\n delete (importSpec.config as any).exportGroups;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-analysis.service.js","names":[],"sources":["../../src/services/file-analysis.service.ts"],"sourcesContent":["import { PrintJobMetadata, FileFormatType, ThumbnailData } from \"@/entities/print-job.entity\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { GCodeParser } from \"@/utils/parsers/gcode.parser\";\nimport { ThreeMFParser } from \"@/utils/parsers/3mf.parser\";\nimport { BGCodeParser } from \"@/utils/parsers/bgcode.parser\";\nimport { basename, extname } from \"node:path\";\nimport { access } from \"node:fs/promises\";\n\ninterface ParserResult {\n raw: any;\n normalized: any;\n plates?: any[];\n}\n\n/**\n * Service for analyzing print files and extracting metadata\n * Supports G-code, 3MF (including multi-plate), and BGCode formats\n */\nexport class FileAnalysisService {\n private readonly logger: LoggerService;\n\n // File format constants\n private readonly FILE_FORMATS = {\n GCODE: \"gcode\" as const,\n THREE_MF: \"3mf\" as const,\n BGCODE: \"bgcode\" as const,\n };\n\n // File extension constants\n private readonly EXTENSIONS = {\n THREE_MF: \".3mf\",\n BGCODE: \".bgcode\",\n GCODE: \".gcode\",\n GCODE_SHORT: \".g\",\n GCODE_ALT: \".gco\",\n GCODE_THREE_MF: \".gcode.3mf\",\n };\n\n // Parser instances\n private readonly gcodeParser: GCodeParser;\n private readonly threemfParser: ThreeMFParser;\n private readonly bgcodeParser: BGCodeParser;\n\n constructor(loggerFactory: ILoggerFactory) {\n this.logger = loggerFactory(FileAnalysisService.name);\n this.gcodeParser = new GCodeParser();\n this.threemfParser = new ThreeMFParser();\n this.bgcodeParser = new BGCodeParser();\n this.logger.log(\"File analysis service initialized with all parsers\");\n }\n\n /**\n * Analyze a file and extract metadata\n */\n async analyzeFile(filePath: string): Promise<{\n metadata: PrintJobMetadata;\n thumbnails: ThumbnailData[];\n }> {\n const ext = extname(filePath).toLowerCase();\n const fileFormat = this.getFileFormat(ext, filePath);\n\n this.logger.log(`Analyzing file: ${basename(filePath)} (format: ${fileFormat}, ext: ${ext})`);\n\n let result: ParserResult;\n\n try {\n switch (fileFormat) {\n case this.FILE_FORMATS.GCODE:\n result = await this.analyzeGCode(filePath);\n break;\n case this.FILE_FORMATS.THREE_MF:\n result = await this.analyze3MF(filePath);\n break;\n case this.FILE_FORMATS.BGCODE:\n result = await this.analyzeBGCode(filePath);\n break;\n default:\n throw new Error(`Unsupported file format: ${fileFormat}`);\n }\n\n // Extract thumbnails\n const thumbnails = this.extractThumbnails(result);\n\n return {\n metadata: result.normalized as PrintJobMetadata,\n thumbnails,\n };\n } catch (error) {\n this.logger.error(`Failed to analyze file ${filePath} (ext: \"${ext}\"): ${error}`);\n throw error;\n }\n }\n\n /**\n * Analyze multiple plates from a 3MF file\n * Returns array of metadata objects, one per plate\n */\n async analyzeMultiPlate3MF(filePath: string): Promise<\n Array<{\n metadata: PrintJobMetadata;\n thumbnails: ThumbnailData[];\n }>\n > {\n const result = await this.analyze3MF(filePath);\n\n if (!result.normalized.isMultiPlate || !result.plates) {\n // Single plate file\n return [\n {\n metadata: result.normalized as PrintJobMetadata,\n thumbnails: this.extractThumbnails(result),\n },\n ];\n }\n\n // Multi-plate file - return metadata for each plate\n return result.plates.map((plate) => ({\n metadata: {\n ...result.normalized,\n plateNumber: plate.plateNumber,\n gcodePrintTimeSeconds: plate.gcodePrintTimeSeconds,\n filamentUsedGrams: plate.filamentUsedGrams,\n totalLayers: plate.totalLayers,\n } as PrintJobMetadata,\n thumbnails:\n plate.thumbnails?.map((t: any) => ({\n width: t.width || 0,\n height: t.height || 0,\n format: t.type || t.format || \"PNG\",\n })) || [],\n }));\n }\n\n private async analyzeGCode(filePath: string): Promise<ParserResult> {\n return await this.gcodeParser.parse(filePath);\n }\n\n private async analyze3MF(filePath: string): Promise<ParserResult> {\n return await this.threemfParser.parse(filePath);\n }\n\n private async analyzeBGCode(filePath: string): Promise<ParserResult> {\n return await this.bgcodeParser.parse(filePath);\n }\n\n private extractThumbnails(result: ParserResult): ThumbnailData[] {\n const thumbnails: ThumbnailData[] = [];\n\n if (result.raw._thumbnails) {\n for (const thumb of result.raw._thumbnails) {\n thumbnails.push({\n width: thumb.width,\n height: thumb.height,\n format: thumb.format,\n data: thumb.data, // Base64 if available\n });\n }\n }\n\n return thumbnails;\n }\n\n private getFileFormat(ext: string, filePath: string): FileFormatType {\n // Check for compound extensions first\n const lowerPath = filePath.toLowerCase();\n if (lowerPath.endsWith(this.EXTENSIONS.GCODE_THREE_MF) || lowerPath.endsWith(this.EXTENSIONS.THREE_MF)) {\n return this.FILE_FORMATS.THREE_MF;\n }\n if (lowerPath.endsWith(this.EXTENSIONS.BGCODE)) {\n return this.FILE_FORMATS.BGCODE;\n }\n if (\n lowerPath.endsWith(this.EXTENSIONS.GCODE) ||\n lowerPath.endsWith(this.EXTENSIONS.GCODE_SHORT) ||\n lowerPath.endsWith(this.EXTENSIONS.GCODE_ALT)\n ) {\n return this.FILE_FORMATS.GCODE;\n }\n\n // Fallback to extension check\n if (ext === this.EXTENSIONS.THREE_MF) return this.FILE_FORMATS.THREE_MF;\n if (ext === this.EXTENSIONS.BGCODE) return this.FILE_FORMATS.BGCODE;\n if (ext === this.EXTENSIONS.GCODE || ext === this.EXTENSIONS.GCODE_SHORT || ext === this.EXTENSIONS.GCODE_ALT) {\n return this.FILE_FORMATS.GCODE;\n }\n\n throw new Error(`Unknown file extension: \"${ext}\" (path: \"${filePath}\")`);\n }\n\n /**\n * Quick check if file needs analysis\n */\n async needsAnalysis(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAmBA,IAAa,sBAAb,MAAa,oBAAoB;CAC/B;CAGA,eAAgC;EAC9B,OAAO;EACP,UAAU;EACV,QAAQ;EACT;CAGD,aAA8B;EAC5B,UAAU;EACV,QAAQ;EACR,OAAO;EACP,aAAa;EACb,WAAW;EACX,gBAAgB;EACjB;CAGD;CACA;CACA;CAEA,YAAY,eAA+B;
|
|
1
|
+
{"version":3,"file":"file-analysis.service.js","names":[],"sources":["../../src/services/file-analysis.service.ts"],"sourcesContent":["import { PrintJobMetadata, FileFormatType, ThumbnailData } from \"@/entities/print-job.entity\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { GCodeParser } from \"@/utils/parsers/gcode.parser\";\nimport { ThreeMFParser } from \"@/utils/parsers/3mf.parser\";\nimport { BGCodeParser } from \"@/utils/parsers/bgcode.parser\";\nimport { basename, extname } from \"node:path\";\nimport { access } from \"node:fs/promises\";\n\ninterface ParserResult {\n raw: any;\n normalized: any;\n plates?: any[];\n}\n\n/**\n * Service for analyzing print files and extracting metadata\n * Supports G-code, 3MF (including multi-plate), and BGCode formats\n */\nexport class FileAnalysisService {\n private readonly logger: LoggerService;\n\n // File format constants\n private readonly FILE_FORMATS = {\n GCODE: \"gcode\" as const,\n THREE_MF: \"3mf\" as const,\n BGCODE: \"bgcode\" as const,\n };\n\n // File extension constants\n private readonly EXTENSIONS = {\n THREE_MF: \".3mf\",\n BGCODE: \".bgcode\",\n GCODE: \".gcode\",\n GCODE_SHORT: \".g\",\n GCODE_ALT: \".gco\",\n GCODE_THREE_MF: \".gcode.3mf\",\n };\n\n // Parser instances\n private readonly gcodeParser: GCodeParser;\n private readonly threemfParser: ThreeMFParser;\n private readonly bgcodeParser: BGCodeParser;\n\n constructor(loggerFactory: ILoggerFactory) {\n this.logger = loggerFactory(FileAnalysisService.name);\n this.gcodeParser = new GCodeParser();\n this.threemfParser = new ThreeMFParser();\n this.bgcodeParser = new BGCodeParser();\n this.logger.log(\"File analysis service initialized with all parsers\");\n }\n\n /**\n * Analyze a file and extract metadata\n */\n async analyzeFile(filePath: string): Promise<{\n metadata: PrintJobMetadata;\n thumbnails: ThumbnailData[];\n }> {\n const ext = extname(filePath).toLowerCase();\n const fileFormat = this.getFileFormat(ext, filePath);\n\n this.logger.log(`Analyzing file: ${basename(filePath)} (format: ${fileFormat}, ext: ${ext})`);\n\n let result: ParserResult;\n\n try {\n switch (fileFormat) {\n case this.FILE_FORMATS.GCODE:\n result = await this.analyzeGCode(filePath);\n break;\n case this.FILE_FORMATS.THREE_MF:\n result = await this.analyze3MF(filePath);\n break;\n case this.FILE_FORMATS.BGCODE:\n result = await this.analyzeBGCode(filePath);\n break;\n default:\n throw new Error(`Unsupported file format: ${fileFormat}`);\n }\n\n // Extract thumbnails\n const thumbnails = this.extractThumbnails(result);\n\n return {\n metadata: result.normalized as PrintJobMetadata,\n thumbnails,\n };\n } catch (error) {\n this.logger.error(`Failed to analyze file ${filePath} (ext: \"${ext}\"): ${error}`);\n throw error;\n }\n }\n\n /**\n * Analyze multiple plates from a 3MF file\n * Returns array of metadata objects, one per plate\n */\n async analyzeMultiPlate3MF(filePath: string): Promise<\n Array<{\n metadata: PrintJobMetadata;\n thumbnails: ThumbnailData[];\n }>\n > {\n const result = await this.analyze3MF(filePath);\n\n if (!result.normalized.isMultiPlate || !result.plates) {\n // Single plate file\n return [\n {\n metadata: result.normalized as PrintJobMetadata,\n thumbnails: this.extractThumbnails(result),\n },\n ];\n }\n\n // Multi-plate file - return metadata for each plate\n return result.plates.map((plate) => ({\n metadata: {\n ...result.normalized,\n plateNumber: plate.plateNumber,\n gcodePrintTimeSeconds: plate.gcodePrintTimeSeconds,\n filamentUsedGrams: plate.filamentUsedGrams,\n totalLayers: plate.totalLayers,\n } as PrintJobMetadata,\n thumbnails:\n plate.thumbnails?.map((t: any) => ({\n width: t.width || 0,\n height: t.height || 0,\n format: t.type || t.format || \"PNG\",\n })) || [],\n }));\n }\n\n private async analyzeGCode(filePath: string): Promise<ParserResult> {\n return await this.gcodeParser.parse(filePath);\n }\n\n private async analyze3MF(filePath: string): Promise<ParserResult> {\n return await this.threemfParser.parse(filePath);\n }\n\n private async analyzeBGCode(filePath: string): Promise<ParserResult> {\n return await this.bgcodeParser.parse(filePath);\n }\n\n private extractThumbnails(result: ParserResult): ThumbnailData[] {\n const thumbnails: ThumbnailData[] = [];\n\n if (result.raw._thumbnails) {\n for (const thumb of result.raw._thumbnails) {\n thumbnails.push({\n width: thumb.width,\n height: thumb.height,\n format: thumb.format,\n data: thumb.data, // Base64 if available\n });\n }\n }\n\n return thumbnails;\n }\n\n private getFileFormat(ext: string, filePath: string): FileFormatType {\n // Check for compound extensions first\n const lowerPath = filePath.toLowerCase();\n if (lowerPath.endsWith(this.EXTENSIONS.GCODE_THREE_MF) || lowerPath.endsWith(this.EXTENSIONS.THREE_MF)) {\n return this.FILE_FORMATS.THREE_MF;\n }\n if (lowerPath.endsWith(this.EXTENSIONS.BGCODE)) {\n return this.FILE_FORMATS.BGCODE;\n }\n if (\n lowerPath.endsWith(this.EXTENSIONS.GCODE) ||\n lowerPath.endsWith(this.EXTENSIONS.GCODE_SHORT) ||\n lowerPath.endsWith(this.EXTENSIONS.GCODE_ALT)\n ) {\n return this.FILE_FORMATS.GCODE;\n }\n\n // Fallback to extension check\n if (ext === this.EXTENSIONS.THREE_MF) return this.FILE_FORMATS.THREE_MF;\n if (ext === this.EXTENSIONS.BGCODE) return this.FILE_FORMATS.BGCODE;\n if (ext === this.EXTENSIONS.GCODE || ext === this.EXTENSIONS.GCODE_SHORT || ext === this.EXTENSIONS.GCODE_ALT) {\n return this.FILE_FORMATS.GCODE;\n }\n\n throw new Error(`Unknown file extension: \"${ext}\" (path: \"${filePath}\")`);\n }\n\n /**\n * Quick check if file needs analysis\n */\n async needsAnalysis(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAmBA,IAAa,sBAAb,MAAa,oBAAoB;CAC/B;CAGA,eAAgC;EAC9B,OAAO;EACP,UAAU;EACV,QAAQ;EACT;CAGD,aAA8B;EAC5B,UAAU;EACV,QAAQ;EACR,OAAO;EACP,aAAa;EACb,WAAW;EACX,gBAAgB;EACjB;CAGD;CACA;CACA;CAEA,YAAY,eAA+B;EACzC,KAAK,SAAS,cAAc,oBAAoB,KAAK;EACrD,KAAK,cAAc,IAAI,aAAa;EACpC,KAAK,gBAAgB,IAAI,eAAe;EACxC,KAAK,eAAe,IAAI,cAAc;EACtC,KAAK,OAAO,IAAI,qDAAqD;;;;;CAMvE,MAAM,YAAY,UAGf;EACD,MAAM,MAAM,QAAQ,SAAS,CAAC,aAAa;EAC3C,MAAM,aAAa,KAAK,cAAc,KAAK,SAAS;EAEpD,KAAK,OAAO,IAAI,mBAAmB,SAAS,SAAS,CAAC,YAAY,WAAW,SAAS,IAAI,GAAG;EAE7F,IAAI;EAEJ,IAAI;GACF,QAAQ,YAAR;IACE,KAAK,KAAK,aAAa;KACrB,SAAS,MAAM,KAAK,aAAa,SAAS;KAC1C;IACF,KAAK,KAAK,aAAa;KACrB,SAAS,MAAM,KAAK,WAAW,SAAS;KACxC;IACF,KAAK,KAAK,aAAa;KACrB,SAAS,MAAM,KAAK,cAAc,SAAS;KAC3C;IACF,SACE,MAAM,IAAI,MAAM,4BAA4B,aAAa;;GAI7D,MAAM,aAAa,KAAK,kBAAkB,OAAO;GAEjD,OAAO;IACL,UAAU,OAAO;IACjB;IACD;WACM,OAAO;GACd,KAAK,OAAO,MAAM,0BAA0B,SAAS,UAAU,IAAI,MAAM,QAAQ;GACjF,MAAM;;;;;;;CAQV,MAAM,qBAAqB,UAKzB;EACA,MAAM,SAAS,MAAM,KAAK,WAAW,SAAS;EAE9C,IAAI,CAAC,OAAO,WAAW,gBAAgB,CAAC,OAAO,QAE7C,OAAO,CACL;GACE,UAAU,OAAO;GACjB,YAAY,KAAK,kBAAkB,OAAO;GAC3C,CACF;EAIH,OAAO,OAAO,OAAO,KAAK,WAAW;GACnC,UAAU;IACR,GAAG,OAAO;IACV,aAAa,MAAM;IACnB,uBAAuB,MAAM;IAC7B,mBAAmB,MAAM;IACzB,aAAa,MAAM;IACpB;GACD,YACE,MAAM,YAAY,KAAK,OAAY;IACjC,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,UAAU;IACpB,QAAQ,EAAE,QAAQ,EAAE,UAAU;IAC/B,EAAE,IAAI,EAAE;GACZ,EAAE;;CAGL,MAAc,aAAa,UAAyC;EAClE,OAAO,MAAM,KAAK,YAAY,MAAM,SAAS;;CAG/C,MAAc,WAAW,UAAyC;EAChE,OAAO,MAAM,KAAK,cAAc,MAAM,SAAS;;CAGjD,MAAc,cAAc,UAAyC;EACnE,OAAO,MAAM,KAAK,aAAa,MAAM,SAAS;;CAGhD,kBAA0B,QAAuC;EAC/D,MAAM,aAA8B,EAAE;EAEtC,IAAI,OAAO,IAAI,aACb,KAAK,MAAM,SAAS,OAAO,IAAI,aAC7B,WAAW,KAAK;GACd,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,MAAM,MAAM;GACb,CAAC;EAIN,OAAO;;CAGT,cAAsB,KAAa,UAAkC;EAEnE,MAAM,YAAY,SAAS,aAAa;EACxC,IAAI,UAAU,SAAS,KAAK,WAAW,eAAe,IAAI,UAAU,SAAS,KAAK,WAAW,SAAS,EACpG,OAAO,KAAK,aAAa;EAE3B,IAAI,UAAU,SAAS,KAAK,WAAW,OAAO,EAC5C,OAAO,KAAK,aAAa;EAE3B,IACE,UAAU,SAAS,KAAK,WAAW,MAAM,IACzC,UAAU,SAAS,KAAK,WAAW,YAAY,IAC/C,UAAU,SAAS,KAAK,WAAW,UAAU,EAE7C,OAAO,KAAK,aAAa;EAI3B,IAAI,QAAQ,KAAK,WAAW,UAAU,OAAO,KAAK,aAAa;EAC/D,IAAI,QAAQ,KAAK,WAAW,QAAQ,OAAO,KAAK,aAAa;EAC7D,IAAI,QAAQ,KAAK,WAAW,SAAS,QAAQ,KAAK,WAAW,eAAe,QAAQ,KAAK,WAAW,WAClG,OAAO,KAAK,aAAa;EAG3B,MAAM,IAAI,MAAM,4BAA4B,IAAI,YAAY,SAAS,IAAI;;;;;CAM3E,MAAM,cAAc,UAAoC;EACtD,IAAI;GACF,MAAM,OAAO,SAAS;GACtB,OAAO;UACD;GACN,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-storage.service.js","names":[],"sources":["../../src/services/file-storage.service.ts"],"sourcesContent":["import { Repository } from \"typeorm\";\nimport { PrintJob } from \"@/entities/print-job.entity\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { TypeormService } from \"@/services/typeorm/typeorm.service\";\nimport { AppConstants } from \"@/server.constants\";\nimport { getMediaPath } from \"@/utils/fs.utils\";\nimport path, { basename, extname, join } from \"node:path\";\nimport { mkdir, readdir, readFile, rename, rm, stat, unlink, writeFile, access } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync, createReadStream, statSync } from \"node:fs\";\nimport { Readable } from \"node:stream\";\nimport { ConflictException } from \"@/exceptions/runtime.exceptions\";\n\nexport interface IFileStorageService {\n saveFile(file: Express.Multer.File, fileHash?: string): Promise<string>;\n getFile(fileStorageId: string): Promise<Buffer>;\n deleteFile(fileStorageId: string): Promise<void>;\n getFilePath(fileStorageId: string): string;\n getFileSize(fileStorageId: string): number;\n calculateFileHash(filePath: string): Promise<string>;\n validateUniqueFilename(fileName: string): Promise<void>;\n saveMetadata(\n fileStorageId: string,\n metadata: any,\n fileHash?: string,\n originalFileName?: string,\n thumbnailMetadata?: any[],\n ): Promise<void>;\n loadMetadata(fileStorageId: string): Promise<any | null>;\n hasMetadata(fileStorageId: string): Promise<boolean>;\n getDeterministicId(fileHash: string, fileName: string): string;\n findDuplicateByOriginalFileName(originalFileName: string): Promise<{ fileStorageId: string; metadata: any } | null>;\n saveThumbnails(\n fileStorageId: string,\n thumbnails: Array<{ data?: string; format?: string; width?: number; height?: number }>,\n ): Promise<\n Array<{\n index: number;\n path: string;\n filename: string;\n width: number;\n height: number;\n format: string;\n size: number;\n }>\n >;\n getThumbnail(fileStorageId: string, index: number): Promise<Buffer | null>;\n listThumbnails(fileStorageId: string): Promise<string[]>;\n}\n\nexport class FileStorageService implements IFileStorageService {\n printJobRepository: Repository<PrintJob>;\n private readonly logger;\n private readonly storageBasePath: string;\n private readonly STORAGE_SUBDIRS = [\"gcode\", \"3mf\", \"bgcode\"] as const;\n\n constructor(loggerFactory: ILoggerFactory, typeormService: TypeormService) {\n this.printJobRepository = typeormService.getDataSource().getRepository(PrintJob);\n this.logger = loggerFactory(FileStorageService.name);\n\n this.storageBasePath = join(getMediaPath(), AppConstants.defaultPrintFilesStorage);\n }\n\n async ensureStorageDirectories() {\n try {\n await mkdir(this.storageBasePath, { recursive: true });\n for (const subdir of this.STORAGE_SUBDIRS) {\n await mkdir(join(this.storageBasePath, subdir), { recursive: true });\n }\n } catch (error) {\n this.logger.error(\"Failed to create storage directories\", error);\n }\n }\n\n readFileStream(fileStorageId: string): Readable {\n const filePath = this.getFilePath(fileStorageId);\n const stream = createReadStream(filePath);\n\n stream.on(\"error\", (err) => {\n this.logger.error(`Failed to read file ${fileStorageId}: ${err.message}`, err);\n });\n\n return stream;\n }\n\n getFileSize(fileStorageId: string): number {\n const filePath = this.getFilePath(fileStorageId);\n const stats = statSync(filePath);\n return stats.size;\n }\n\n async validateUniqueFilename(fileName: string): Promise<void> {\n const existing = await this.findDuplicateByOriginalFileName(fileName);\n if (existing) {\n throw new ConflictException(\n `A file named \"${fileName}\" already exists in storage. Please rename the file, delete the existing file (ID: ${existing.fileStorageId}), or choose a different name.`,\n existing.fileStorageId,\n );\n }\n }\n\n async saveFile(file: Express.Multer.File, fileHash?: string): Promise<string> {\n const fileExt = extname(file.originalname).toLowerCase();\n\n let fileId: string;\n if (fileHash) {\n const nameHash = createHash(\"sha256\")\n .update(fileHash + file.originalname)\n .digest(\"hex\")\n .substring(0, 32);\n fileId = `${nameHash.substring(0, 8)}-${nameHash.substring(8, 12)}-${nameHash.substring(12, 16)}-${nameHash.substring(16, 20)}-${nameHash.substring(20, 32)}`;\n } else {\n fileId = crypto.randomUUID();\n }\n\n let subdir = \"gcode\";\n if (fileExt === \".3mf\" || file.originalname.includes(\".gcode.3mf\")) {\n subdir = \"3mf\";\n } else if (fileExt === \".bgcode\") {\n subdir = \"bgcode\";\n }\n\n const targetDir = join(this.storageBasePath, subdir);\n const targetPath = join(targetDir, `${fileId}${fileExt}`);\n\n if (file.path) {\n await rename(file.path, targetPath);\n } else if (file.buffer) {\n await writeFile(targetPath, file.buffer);\n } else {\n throw new Error(\"File has no path or buffer\");\n }\n\n this.logger.log(`Saved file ${file.originalname} as ${fileId}`);\n return fileId;\n }\n\n async getFile(fileStorageId: string): Promise<Buffer> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n throw new Error(`File ${fileStorageId} not found in storage`);\n }\n\n return readFile(filePath);\n }\n\n async deleteFile(fileStorageId: string): Promise<void> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n this.logger.warn(`File ${fileStorageId} not found, cannot delete`);\n return;\n }\n\n await unlink(filePath);\n\n const metadataPath = filePath + \".json\";\n try {\n await unlink(metadataPath);\n this.logger.debug(`Deleted metadata JSON for ${fileStorageId}`);\n } catch {}\n\n const thumbnailDir = filePath.replace(/\\.(gcode|3mf|bgcode)$/i, \"_thumbnails\");\n try {\n await rm(thumbnailDir, { recursive: true, force: true });\n this.logger.debug(`Deleted thumbnails for ${fileStorageId}`);\n } catch {}\n\n this.logger.log(`Deleted file ${fileStorageId}`);\n }\n\n getFilePath(fileStorageId: string): string {\n for (const subdir of this.STORAGE_SUBDIRS) {\n for (const ext of [\".gcode\", \".3mf\", \".bgcode\", \"\"]) {\n const fullPath = join(this.storageBasePath, subdir, fileStorageId + ext);\n if (existsSync(fullPath)) {\n return fullPath;\n }\n }\n }\n\n return join(this.storageBasePath, \"gcode\", fileStorageId);\n }\n\n async calculateFileHash(filePath: string): Promise<string> {\n const fileBuffer = await readFile(filePath);\n const hashSum = createHash(\"sha256\");\n hashSum.update(fileBuffer);\n return hashSum.digest(\"hex\");\n }\n\n getDeterministicId(fileHash: string, fileName: string): string {\n const nameHash = createHash(\"sha256\")\n .update(fileHash + fileName)\n .digest(\"hex\")\n .substring(0, 32);\n return `${nameHash.substring(0, 8)}-${nameHash.substring(8, 12)}-${nameHash.substring(12, 16)}-${nameHash.substring(16, 20)}-${nameHash.substring(20, 32)}`;\n }\n\n private async findFilePath(fileStorageId: string): Promise<string | null> {\n for (const subdir of this.STORAGE_SUBDIRS) {\n const dirPath = join(this.storageBasePath, subdir);\n try {\n const files = await readdir(dirPath);\n const matchingFile = files.find((f) => f.startsWith(fileStorageId));\n if (matchingFile) {\n return join(dirPath, matchingFile);\n }\n } catch {}\n }\n\n return null;\n }\n\n async fileExists(fileStorageId: string): Promise<boolean> {\n const filePath = await this.findFilePath(fileStorageId);\n return filePath !== null;\n }\n\n async findDuplicateByHash(fileHash: string): Promise<PrintJob | null> {\n return this.printJobRepository.findOne({\n where: { fileHash },\n order: { createdAt: \"DESC\" },\n });\n }\n\n async findDuplicateByOriginalFileName(originalFileName: string): Promise<{\n fileStorageId: string;\n metadata: any;\n } | null> {\n for (const subdir of this.STORAGE_SUBDIRS) {\n const dirPath = join(this.storageBasePath, subdir);\n try {\n const dirFiles = await readdir(dirPath);\n\n for (const file of dirFiles) {\n if (file.endsWith(\"_thumbnails\") || file.endsWith(\".json\")) continue;\n\n const fileId = path.parse(file).name;\n const metadata = await this.loadMetadata(fileId);\n\n if (metadata?._originalFileName === originalFileName) {\n return {\n fileStorageId: fileId,\n metadata,\n };\n }\n }\n } catch (error) {\n this.logger.error(`Error searching for duplicate in ${subdir}`, error);\n }\n }\n\n return null;\n }\n\n async saveMetadata(\n fileStorageId: string,\n metadata: any,\n fileHash?: string,\n originalFileName?: string,\n thumbnailMetadata?: any[],\n ): Promise<void> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n this.logger.warn(`Cannot save metadata - file ${fileStorageId} not found`);\n return;\n }\n\n const metadataPath = filePath + \".json\";\n\n let existingOriginalFileName = originalFileName;\n let existingThumbnails = thumbnailMetadata;\n try {\n const existingContent = await readFile(metadataPath, \"utf8\");\n const existing = JSON.parse(existingContent);\n if (existing._originalFileName && !originalFileName) {\n existingOriginalFileName = existing._originalFileName;\n }\n if (existing._thumbnails && !thumbnailMetadata) {\n existingThumbnails = existing._thumbnails;\n }\n } catch {}\n\n const metadataWithMeta = {\n ...metadata,\n _fileHash: fileHash || null,\n _analyzedAt: new Date().toISOString(),\n _fileStorageId: fileStorageId,\n _originalFileName: existingOriginalFileName || metadata.fileName || null,\n _thumbnails: existingThumbnails || [],\n };\n\n await writeFile(metadataPath, JSON.stringify(metadataWithMeta, null, 2), \"utf8\");\n const thumbnailMeta = thumbnailMetadata ? ` with ${thumbnailMetadata.length} thumbnail(s)` : \"\";\n this.logger.debug(`Saved metadata for ${fileStorageId}${thumbnailMeta}`);\n }\n\n async loadMetadata(fileStorageId: string): Promise<any | null> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n return null;\n }\n\n const metadataPath = filePath + \".json\";\n try {\n const content = await readFile(metadataPath, \"utf8\");\n return JSON.parse(content);\n } catch {\n return null;\n }\n }\n\n async hasMetadata(fileStorageId: string): Promise<boolean> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n return false;\n }\n\n const metadataPath = filePath + \".json\";\n try {\n await access(metadataPath);\n return true;\n } catch {\n return false;\n }\n }\n\n async saveThumbnails(\n fileStorageId: string,\n thumbnails: Array<{ data?: string; format?: string; width?: number; height?: number }>,\n ): Promise<\n Array<{\n index: number;\n path: string;\n filename: string;\n width: number;\n height: number;\n format: string;\n size: number;\n }>\n > {\n const savedThumbnails: Array<any> = [];\n\n if (!thumbnails || thumbnails.length === 0) {\n return savedThumbnails;\n }\n\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n this.logger.warn(`Cannot save thumbnails - file ${fileStorageId} not found`);\n return savedThumbnails;\n }\n\n const thumbnailDir = filePath.replace(/\\.(gcode|3mf|bgcode)$/i, \"_thumbnails\");\n\n try {\n await rm(thumbnailDir, { recursive: true, force: true });\n this.logger.debug(`Cleared old thumbnails for ${fileStorageId}`);\n } catch {}\n\n await mkdir(thumbnailDir, { recursive: true });\n\n for (let i = 0; i < thumbnails.length; i++) {\n const thumb = thumbnails[i];\n if (!thumb.data) continue;\n\n const ext = thumb.format?.toLowerCase() || \"png\";\n const filename = `thumb_${i}.${ext}`;\n const thumbPath = join(thumbnailDir, filename);\n\n try {\n const buffer = Buffer.from(thumb.data, \"base64\");\n await writeFile(thumbPath, buffer);\n\n const relativePath = path.relative(this.storageBasePath, thumbPath);\n\n savedThumbnails.push({\n index: i,\n path: relativePath,\n filename,\n width: thumb.width || 0,\n height: thumb.height || 0,\n format: ext,\n size: buffer.length,\n });\n\n this.logger.debug(\n `Saved thumbnail ${i} for ${fileStorageId} (${thumb.width}x${thumb.height}, ${buffer.length} bytes)`,\n );\n } catch (error) {\n this.logger.warn(`Failed to save thumbnail ${i} for ${fileStorageId}: ${error}`);\n }\n }\n\n return savedThumbnails;\n }\n\n async getThumbnail(fileStorageId: string, index: number): Promise<Buffer | null> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) return null;\n\n const thumbnailDir = filePath.replace(/\\.(gcode|3mf|bgcode)$/i, \"_thumbnails\");\n\n for (const ext of [\"png\", \"jpg\", \"jpeg\", \"qoi\"]) {\n const thumbPath = join(thumbnailDir, `thumb_${index}.${ext}`);\n try {\n return await readFile(thumbPath);\n } catch {}\n }\n\n return null;\n }\n\n async listThumbnails(fileStorageId: string): Promise<string[]> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) return [];\n\n const thumbnailDir = filePath.replace(/\\.(gcode|3mf|bgcode)$/i, \"_thumbnails\");\n\n try {\n const files = await readdir(thumbnailDir);\n return files.filter((f) => f.startsWith(\"thumb_\")).sort((a, b) => a.localeCompare(b));\n } catch {\n return [];\n }\n }\n\n async listAllFiles(): Promise<\n Array<{\n fileStorageId: string;\n fileName: string;\n fileFormat: string;\n fileSize: number;\n fileHash: string;\n createdAt: Date;\n thumbnailCount: number;\n metadata?: any;\n }>\n > {\n const files: any[] = [];\n\n for (const subdir of this.STORAGE_SUBDIRS) {\n const dirPath = join(this.storageBasePath, subdir);\n try {\n const dirFiles = await readdir(dirPath);\n\n for (const file of dirFiles) {\n if (file.endsWith(\"_thumbnails\") || file.endsWith(\".json\")) continue;\n\n const fileId = path.parse(file).name;\n const filePath = join(dirPath, file);\n const stats = await stat(filePath);\n\n const metadata = await this.loadMetadata(fileId);\n\n const thumbnails = await this.listThumbnails(fileId);\n\n files.push({\n fileStorageId: fileId,\n fileName: metadata?._fileName || file,\n fileFormat: subdir,\n fileSize: stats.size,\n fileHash: metadata?._fileHash || \"\",\n createdAt: stats.birthtime,\n thumbnailCount: thumbnails.length,\n metadata: metadata,\n });\n }\n } catch (error) {\n this.logger.error(`Error listing files in ${subdir}`, error);\n }\n }\n\n return files.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n }\n\n async getFileInfo(fileStorageId: string): Promise<{\n fileStorageId: string;\n fileName: string;\n fileFormat: string;\n fileSize: number;\n fileHash: string;\n createdAt: Date;\n thumbnailCount: number;\n metadata?: any;\n } | null> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) return null;\n\n try {\n const stats = await stat(filePath);\n const metadata = await this.loadMetadata(fileStorageId);\n const thumbnails = await this.listThumbnails(fileStorageId);\n const ext = extname(filePath).substring(1);\n\n return {\n fileStorageId,\n fileName: metadata?._originalFileName || basename(filePath),\n fileFormat: ext,\n fileSize: stats.size,\n fileHash: metadata?._fileHash || \"\",\n createdAt: stats.birthtime,\n thumbnailCount: thumbnails.length,\n metadata: metadata,\n };\n } catch (error) {\n this.logger.error(`Error getting file info for ${fileStorageId}`, error);\n return null;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-storage.service.js","names":[],"sources":["../../src/services/file-storage.service.ts"],"sourcesContent":["import { Repository } from \"typeorm\";\nimport { PrintJob } from \"@/entities/print-job.entity\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { TypeormService } from \"@/services/typeorm/typeorm.service\";\nimport { AppConstants } from \"@/server.constants\";\nimport { getMediaPath } from \"@/utils/fs.utils\";\nimport path, { basename, extname, join } from \"node:path\";\nimport { mkdir, readdir, readFile, rename, rm, stat, unlink, writeFile, access } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync, createReadStream, statSync } from \"node:fs\";\nimport { Readable } from \"node:stream\";\nimport { ConflictException } from \"@/exceptions/runtime.exceptions\";\n\nexport interface IFileStorageService {\n saveFile(file: Express.Multer.File, fileHash?: string): Promise<string>;\n getFile(fileStorageId: string): Promise<Buffer>;\n deleteFile(fileStorageId: string): Promise<void>;\n getFilePath(fileStorageId: string): string;\n getFileSize(fileStorageId: string): number;\n calculateFileHash(filePath: string): Promise<string>;\n validateUniqueFilename(fileName: string): Promise<void>;\n saveMetadata(\n fileStorageId: string,\n metadata: any,\n fileHash?: string,\n originalFileName?: string,\n thumbnailMetadata?: any[],\n ): Promise<void>;\n loadMetadata(fileStorageId: string): Promise<any | null>;\n hasMetadata(fileStorageId: string): Promise<boolean>;\n getDeterministicId(fileHash: string, fileName: string): string;\n findDuplicateByOriginalFileName(originalFileName: string): Promise<{ fileStorageId: string; metadata: any } | null>;\n saveThumbnails(\n fileStorageId: string,\n thumbnails: Array<{ data?: string; format?: string; width?: number; height?: number }>,\n ): Promise<\n Array<{\n index: number;\n path: string;\n filename: string;\n width: number;\n height: number;\n format: string;\n size: number;\n }>\n >;\n getThumbnail(fileStorageId: string, index: number): Promise<Buffer | null>;\n listThumbnails(fileStorageId: string): Promise<string[]>;\n}\n\nexport class FileStorageService implements IFileStorageService {\n printJobRepository: Repository<PrintJob>;\n private readonly logger;\n private readonly storageBasePath: string;\n private readonly STORAGE_SUBDIRS = [\"gcode\", \"3mf\", \"bgcode\"] as const;\n\n constructor(loggerFactory: ILoggerFactory, typeormService: TypeormService) {\n this.printJobRepository = typeormService.getDataSource().getRepository(PrintJob);\n this.logger = loggerFactory(FileStorageService.name);\n\n this.storageBasePath = join(getMediaPath(), AppConstants.defaultPrintFilesStorage);\n }\n\n async ensureStorageDirectories() {\n try {\n await mkdir(this.storageBasePath, { recursive: true });\n for (const subdir of this.STORAGE_SUBDIRS) {\n await mkdir(join(this.storageBasePath, subdir), { recursive: true });\n }\n } catch (error) {\n this.logger.error(\"Failed to create storage directories\", error);\n }\n }\n\n readFileStream(fileStorageId: string): Readable {\n const filePath = this.getFilePath(fileStorageId);\n const stream = createReadStream(filePath);\n\n stream.on(\"error\", (err) => {\n this.logger.error(`Failed to read file ${fileStorageId}: ${err.message}`, err);\n });\n\n return stream;\n }\n\n getFileSize(fileStorageId: string): number {\n const filePath = this.getFilePath(fileStorageId);\n const stats = statSync(filePath);\n return stats.size;\n }\n\n async validateUniqueFilename(fileName: string): Promise<void> {\n const existing = await this.findDuplicateByOriginalFileName(fileName);\n if (existing) {\n throw new ConflictException(\n `A file named \"${fileName}\" already exists in storage. Please rename the file, delete the existing file (ID: ${existing.fileStorageId}), or choose a different name.`,\n existing.fileStorageId,\n );\n }\n }\n\n async saveFile(file: Express.Multer.File, fileHash?: string): Promise<string> {\n const fileExt = extname(file.originalname).toLowerCase();\n\n let fileId: string;\n if (fileHash) {\n const nameHash = createHash(\"sha256\")\n .update(fileHash + file.originalname)\n .digest(\"hex\")\n .substring(0, 32);\n fileId = `${nameHash.substring(0, 8)}-${nameHash.substring(8, 12)}-${nameHash.substring(12, 16)}-${nameHash.substring(16, 20)}-${nameHash.substring(20, 32)}`;\n } else {\n fileId = crypto.randomUUID();\n }\n\n let subdir = \"gcode\";\n if (fileExt === \".3mf\" || file.originalname.includes(\".gcode.3mf\")) {\n subdir = \"3mf\";\n } else if (fileExt === \".bgcode\") {\n subdir = \"bgcode\";\n }\n\n const targetDir = join(this.storageBasePath, subdir);\n const targetPath = join(targetDir, `${fileId}${fileExt}`);\n\n if (file.path) {\n await rename(file.path, targetPath);\n } else if (file.buffer) {\n await writeFile(targetPath, file.buffer);\n } else {\n throw new Error(\"File has no path or buffer\");\n }\n\n this.logger.log(`Saved file ${file.originalname} as ${fileId}`);\n return fileId;\n }\n\n async getFile(fileStorageId: string): Promise<Buffer> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n throw new Error(`File ${fileStorageId} not found in storage`);\n }\n\n return readFile(filePath);\n }\n\n async deleteFile(fileStorageId: string): Promise<void> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n this.logger.warn(`File ${fileStorageId} not found, cannot delete`);\n return;\n }\n\n await unlink(filePath);\n\n const metadataPath = filePath + \".json\";\n try {\n await unlink(metadataPath);\n this.logger.debug(`Deleted metadata JSON for ${fileStorageId}`);\n } catch {}\n\n const thumbnailDir = filePath.replace(/\\.(gcode|3mf|bgcode)$/i, \"_thumbnails\");\n try {\n await rm(thumbnailDir, { recursive: true, force: true });\n this.logger.debug(`Deleted thumbnails for ${fileStorageId}`);\n } catch {}\n\n this.logger.log(`Deleted file ${fileStorageId}`);\n }\n\n getFilePath(fileStorageId: string): string {\n for (const subdir of this.STORAGE_SUBDIRS) {\n for (const ext of [\".gcode\", \".3mf\", \".bgcode\", \"\"]) {\n const fullPath = join(this.storageBasePath, subdir, fileStorageId + ext);\n if (existsSync(fullPath)) {\n return fullPath;\n }\n }\n }\n\n return join(this.storageBasePath, \"gcode\", fileStorageId);\n }\n\n async calculateFileHash(filePath: string): Promise<string> {\n const fileBuffer = await readFile(filePath);\n const hashSum = createHash(\"sha256\");\n hashSum.update(fileBuffer);\n return hashSum.digest(\"hex\");\n }\n\n getDeterministicId(fileHash: string, fileName: string): string {\n const nameHash = createHash(\"sha256\")\n .update(fileHash + fileName)\n .digest(\"hex\")\n .substring(0, 32);\n return `${nameHash.substring(0, 8)}-${nameHash.substring(8, 12)}-${nameHash.substring(12, 16)}-${nameHash.substring(16, 20)}-${nameHash.substring(20, 32)}`;\n }\n\n private async findFilePath(fileStorageId: string): Promise<string | null> {\n for (const subdir of this.STORAGE_SUBDIRS) {\n const dirPath = join(this.storageBasePath, subdir);\n try {\n const files = await readdir(dirPath);\n const matchingFile = files.find((f) => f.startsWith(fileStorageId));\n if (matchingFile) {\n return join(dirPath, matchingFile);\n }\n } catch {}\n }\n\n return null;\n }\n\n async fileExists(fileStorageId: string): Promise<boolean> {\n const filePath = await this.findFilePath(fileStorageId);\n return filePath !== null;\n }\n\n async findDuplicateByHash(fileHash: string): Promise<PrintJob | null> {\n return this.printJobRepository.findOne({\n where: { fileHash },\n order: { createdAt: \"DESC\" },\n });\n }\n\n async findDuplicateByOriginalFileName(originalFileName: string): Promise<{\n fileStorageId: string;\n metadata: any;\n } | null> {\n for (const subdir of this.STORAGE_SUBDIRS) {\n const dirPath = join(this.storageBasePath, subdir);\n try {\n const dirFiles = await readdir(dirPath);\n\n for (const file of dirFiles) {\n if (file.endsWith(\"_thumbnails\") || file.endsWith(\".json\")) continue;\n\n const fileId = path.parse(file).name;\n const metadata = await this.loadMetadata(fileId);\n\n if (metadata?._originalFileName === originalFileName) {\n return {\n fileStorageId: fileId,\n metadata,\n };\n }\n }\n } catch (error) {\n this.logger.error(`Error searching for duplicate in ${subdir}`, error);\n }\n }\n\n return null;\n }\n\n async saveMetadata(\n fileStorageId: string,\n metadata: any,\n fileHash?: string,\n originalFileName?: string,\n thumbnailMetadata?: any[],\n ): Promise<void> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n this.logger.warn(`Cannot save metadata - file ${fileStorageId} not found`);\n return;\n }\n\n const metadataPath = filePath + \".json\";\n\n let existingOriginalFileName = originalFileName;\n let existingThumbnails = thumbnailMetadata;\n try {\n const existingContent = await readFile(metadataPath, \"utf8\");\n const existing = JSON.parse(existingContent);\n if (existing._originalFileName && !originalFileName) {\n existingOriginalFileName = existing._originalFileName;\n }\n if (existing._thumbnails && !thumbnailMetadata) {\n existingThumbnails = existing._thumbnails;\n }\n } catch {}\n\n const metadataWithMeta = {\n ...metadata,\n _fileHash: fileHash || null,\n _analyzedAt: new Date().toISOString(),\n _fileStorageId: fileStorageId,\n _originalFileName: existingOriginalFileName || metadata.fileName || null,\n _thumbnails: existingThumbnails || [],\n };\n\n await writeFile(metadataPath, JSON.stringify(metadataWithMeta, null, 2), \"utf8\");\n const thumbnailMeta = thumbnailMetadata ? ` with ${thumbnailMetadata.length} thumbnail(s)` : \"\";\n this.logger.debug(`Saved metadata for ${fileStorageId}${thumbnailMeta}`);\n }\n\n async loadMetadata(fileStorageId: string): Promise<any | null> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n return null;\n }\n\n const metadataPath = filePath + \".json\";\n try {\n const content = await readFile(metadataPath, \"utf8\");\n return JSON.parse(content);\n } catch {\n return null;\n }\n }\n\n async hasMetadata(fileStorageId: string): Promise<boolean> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n return false;\n }\n\n const metadataPath = filePath + \".json\";\n try {\n await access(metadataPath);\n return true;\n } catch {\n return false;\n }\n }\n\n async saveThumbnails(\n fileStorageId: string,\n thumbnails: Array<{ data?: string; format?: string; width?: number; height?: number }>,\n ): Promise<\n Array<{\n index: number;\n path: string;\n filename: string;\n width: number;\n height: number;\n format: string;\n size: number;\n }>\n > {\n const savedThumbnails: Array<any> = [];\n\n if (!thumbnails || thumbnails.length === 0) {\n return savedThumbnails;\n }\n\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) {\n this.logger.warn(`Cannot save thumbnails - file ${fileStorageId} not found`);\n return savedThumbnails;\n }\n\n const thumbnailDir = filePath.replace(/\\.(gcode|3mf|bgcode)$/i, \"_thumbnails\");\n\n try {\n await rm(thumbnailDir, { recursive: true, force: true });\n this.logger.debug(`Cleared old thumbnails for ${fileStorageId}`);\n } catch {}\n\n await mkdir(thumbnailDir, { recursive: true });\n\n for (let i = 0; i < thumbnails.length; i++) {\n const thumb = thumbnails[i];\n if (!thumb.data) continue;\n\n const ext = thumb.format?.toLowerCase() || \"png\";\n const filename = `thumb_${i}.${ext}`;\n const thumbPath = join(thumbnailDir, filename);\n\n try {\n const buffer = Buffer.from(thumb.data, \"base64\");\n await writeFile(thumbPath, buffer);\n\n const relativePath = path.relative(this.storageBasePath, thumbPath);\n\n savedThumbnails.push({\n index: i,\n path: relativePath,\n filename,\n width: thumb.width || 0,\n height: thumb.height || 0,\n format: ext,\n size: buffer.length,\n });\n\n this.logger.debug(\n `Saved thumbnail ${i} for ${fileStorageId} (${thumb.width}x${thumb.height}, ${buffer.length} bytes)`,\n );\n } catch (error) {\n this.logger.warn(`Failed to save thumbnail ${i} for ${fileStorageId}: ${error}`);\n }\n }\n\n return savedThumbnails;\n }\n\n async getThumbnail(fileStorageId: string, index: number): Promise<Buffer | null> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) return null;\n\n const thumbnailDir = filePath.replace(/\\.(gcode|3mf|bgcode)$/i, \"_thumbnails\");\n\n for (const ext of [\"png\", \"jpg\", \"jpeg\", \"qoi\"]) {\n const thumbPath = join(thumbnailDir, `thumb_${index}.${ext}`);\n try {\n return await readFile(thumbPath);\n } catch {}\n }\n\n return null;\n }\n\n async listThumbnails(fileStorageId: string): Promise<string[]> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) return [];\n\n const thumbnailDir = filePath.replace(/\\.(gcode|3mf|bgcode)$/i, \"_thumbnails\");\n\n try {\n const files = await readdir(thumbnailDir);\n return files.filter((f) => f.startsWith(\"thumb_\")).sort((a, b) => a.localeCompare(b));\n } catch {\n return [];\n }\n }\n\n async listAllFiles(): Promise<\n Array<{\n fileStorageId: string;\n fileName: string;\n fileFormat: string;\n fileSize: number;\n fileHash: string;\n createdAt: Date;\n thumbnailCount: number;\n metadata?: any;\n }>\n > {\n const files: any[] = [];\n\n for (const subdir of this.STORAGE_SUBDIRS) {\n const dirPath = join(this.storageBasePath, subdir);\n try {\n const dirFiles = await readdir(dirPath);\n\n for (const file of dirFiles) {\n if (file.endsWith(\"_thumbnails\") || file.endsWith(\".json\")) continue;\n\n const fileId = path.parse(file).name;\n const filePath = join(dirPath, file);\n const stats = await stat(filePath);\n\n const metadata = await this.loadMetadata(fileId);\n\n const thumbnails = await this.listThumbnails(fileId);\n\n files.push({\n fileStorageId: fileId,\n fileName: metadata?._fileName || file,\n fileFormat: subdir,\n fileSize: stats.size,\n fileHash: metadata?._fileHash || \"\",\n createdAt: stats.birthtime,\n thumbnailCount: thumbnails.length,\n metadata: metadata,\n });\n }\n } catch (error) {\n this.logger.error(`Error listing files in ${subdir}`, error);\n }\n }\n\n return files.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n }\n\n async getFileInfo(fileStorageId: string): Promise<{\n fileStorageId: string;\n fileName: string;\n fileFormat: string;\n fileSize: number;\n fileHash: string;\n createdAt: Date;\n thumbnailCount: number;\n metadata?: any;\n } | null> {\n const filePath = await this.findFilePath(fileStorageId);\n if (!filePath) return null;\n\n try {\n const stats = await stat(filePath);\n const metadata = await this.loadMetadata(fileStorageId);\n const thumbnails = await this.listThumbnails(fileStorageId);\n const ext = extname(filePath).substring(1);\n\n return {\n fileStorageId,\n fileName: metadata?._originalFileName || basename(filePath),\n fileFormat: ext,\n fileSize: stats.size,\n fileHash: metadata?._fileHash || \"\",\n createdAt: stats.birthtime,\n thumbnailCount: thumbnails.length,\n metadata: metadata,\n };\n } catch (error) {\n this.logger.error(`Error getting file info for ${fileStorageId}`, error);\n return null;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/services/interfaces/api-key.dto.ts
|
|
2
|
+
/** Public DTO. Never includes the cleartext token or the stored hash. */
|
|
3
|
+
var ApiKeyDto = class {
|
|
4
|
+
id;
|
|
5
|
+
createdByUserId;
|
|
6
|
+
label;
|
|
7
|
+
prefix;
|
|
8
|
+
createdAt;
|
|
9
|
+
lastUsedAt;
|
|
10
|
+
roles;
|
|
11
|
+
};
|
|
12
|
+
/** Returned exactly once at creation. */
|
|
13
|
+
var CreatedApiKeyDto = class extends ApiKeyDto {
|
|
14
|
+
token;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { ApiKeyDto, CreatedApiKeyDto };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=api-key.dto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.dto.js","names":[],"sources":["../../../src/services/interfaces/api-key.dto.ts"],"sourcesContent":["/** Public DTO. Never includes the cleartext token or the stored hash. */\nexport class ApiKeyDto {\n id!: number;\n createdByUserId!: number;\n label!: string;\n prefix!: string;\n createdAt!: Date;\n lastUsedAt!: Date | null;\n roles!: string[];\n}\n\n/** Returned exactly once at creation. */\nexport class CreatedApiKeyDto extends ApiKeyDto {\n token!: string;\n}\n"],"mappings":";;AACA,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CACA;CACA;CACA;CACA;;;AAIF,IAAa,mBAAb,cAAsC,UAAU;CAC9C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.dto.js","names":[],"sources":["../../../src/services/interfaces/user.dto.ts"],"sourcesContent":["import type { RoleName } from \"@/constants/authorization.constants\";\n\nexport class UserDto {\n id: number;\n createdAt: Date;\n username: string;\n isDemoUser: boolean;\n isRootUser: boolean;\n isVerified: boolean;\n needsPasswordChange: boolean;\n roles: RoleName[];\n}\n\nexport class RegisterUserDto {\n username: string;\n password: string;\n roles: RoleName[];\n isDemoUser: boolean;\n isRootUser: boolean;\n isVerified: boolean;\n needsPasswordChange: boolean;\n}\n"],"mappings":";AAEA,IAAa,UAAb,MAAqB;CACnB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;AAGF,IAAa,kBAAb,MAA6B;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA"}
|
|
1
|
+
{"version":3,"file":"user.dto.js","names":[],"sources":["../../../src/services/interfaces/user.dto.ts"],"sourcesContent":["import type { RoleName } from \"@/constants/authorization.constants\";\n\nexport class UserDto {\n id: number;\n createdAt: Date;\n username: string;\n isDemoUser: boolean;\n isRootUser: boolean;\n isVerified: boolean;\n needsPasswordChange: boolean;\n roles: RoleName[];\n /** True when the principal was authenticated via an API key, not a user login. */\n isApiKey?: boolean;\n}\n\nexport class RegisterUserDto {\n username: string;\n password: string;\n roles: RoleName[];\n isDemoUser: boolean;\n isRootUser: boolean;\n isVerified: boolean;\n needsPasswordChange: boolean;\n}\n"],"mappings":";AAEA,IAAa,UAAb,MAAqB;CACnB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;CAEA;;AAGF,IAAa,kBAAb,MAA6B;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moonraker-websocket.adapter.js","names":[],"sources":["../../../src/services/moonraker/moonraker-websocket.adapter.ts"],"sourcesContent":["import { MoonrakerClient } from \"@/services/moonraker/moonraker.client\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport EventEmitter2 from \"eventemitter2\";\nimport { ConfigService } from \"@/services/core/config.service\";\nimport type { ISocketLogin } from \"@/shared/dtos/socket-login.dto\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { AppConstants } from \"@/server.constants\";\nimport { httpToWsUrl } from \"@/utils/url.utils\";\nimport type { OctoPrintEventDto } from \"@/services/octoprint/dto/octoprint-event.dto\";\nimport { WsMessage } from \"@/services/octoprint/octoprint-websocket.adapter\";\nimport { moonrakerEvent } from \"@/services/moonraker/constants/moonraker.constants\";\nimport { SOCKET_STATE, SocketState } from \"@/shared/dtos/socket-state.type\";\nimport { API_STATE, ApiState } from \"@/shared/dtos/api-state.type\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport type { ConnectionIdentifyDto } from \"@/services/moonraker/dto/websocket/connection-identify.dto\";\nimport type { JsonRpcEventDto } from \"@/services/moonraker/dto/websocket/json-rpc-event.dto\";\nimport { KnownPrinterObject } from \"@/services/moonraker/dto/objects/printer-objects-list.dto\";\nimport { NotifyStatusUpdate } from \"@/services/moonraker/dto/websocket/message.types\";\nimport {\n DisplayStatusObject,\n ExtruderObject,\n FanObject,\n GcodeMoveObject,\n HeaterBedObject,\n HeatersObject,\n IdleTimeoutObject,\n MotionReportObject,\n PauseResumeObject,\n PrintStatsObject,\n StepperEnableObject,\n SystemStatsObject,\n VirtualSdCardObject,\n WebhooksObject,\n} from \"@/services/moonraker/dto/objects/printer-object.types\";\nimport { PP } from \"@/utils/pretty-print.utils\";\nimport type { MoonrakerErrorDto } from \"@/services/moonraker/dto/rest/error.dto\";\nimport type { MoonrakerEventDto } from \"@/services/moonraker/constants/moonraker-event.dto\";\nimport type { PrinterObjectsQueryDto } from \"@/services/moonraker/dto/objects/printer-objects-query.dto\";\nimport type { ConnectionIdentifyResponseDto } from \"@/services/moonraker/dto/websocket/connection-identify-response.dto\";\nimport type { FlagsDto } from \"@/services/octoprint/dto/printer/flags.dto\";\nimport { MoonrakerType, type FdmCurrentMessageDto } from \"@/services/printer-api.interface\";\nimport { Event as WsEvent } from \"ws\";\nimport { NotifyServiceStateChangedParams } from \"@/services/moonraker/dto/websocket/notify-service-state-changed.params\";\nimport { WebsocketRpcExtendedAdapter } from \"@/shared/websocket-rpc-extended.adapter\";\nimport type { IWebsocketAdapter } from \"@/services/websocket-adapter.interface\";\nimport { normalizeUrl } from \"@/utils/normalize-url\";\nimport { AxiosError } from \"axios\";\n\nexport type SubscriptionType = IdleTimeoutObject &\n PauseResumeObject &\n PrintStatsObject &\n HeatersObject &\n HeaterBedObject &\n ExtruderObject &\n DisplayStatusObject &\n WebhooksObject &\n VirtualSdCardObject &\n GcodeMoveObject &\n StepperEnableObject &\n FanObject &\n MotionReportObject &\n SystemStatsObject;\n\nexport class MoonrakerWebsocketAdapter extends WebsocketRpcExtendedAdapter implements IWebsocketAdapter {\n readonly printerType = 1;\n socketState: SocketState = SOCKET_STATE.unopened;\n lastMessageReceivedTimestamp: null | number = null;\n stateUpdated = false;\n stateUpdateTimestamp: null | number = null;\n apiStateUpdated = false;\n apiStateUpdateTimestamp: null | number = null;\n apiState: ApiState = API_STATE.unset;\n // Guaranteed to be set and valid by PrinterApiFactory\n login: LoginDto;\n printerId?: number;\n refreshPrinterObjectsInterval?: NodeJS.Timeout;\n printerObjects: PrinterObjectsQueryDto<SubscriptionType | null> = {\n eventtime: null,\n status: null,\n };\n protected logger: LoggerService;\n private socketURL?: URL;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly moonrakerClient: MoonrakerClient,\n private readonly eventEmitter2: EventEmitter2,\n private readonly configService: ConfigService,\n private readonly serverVersion: string,\n ) {\n super(loggerFactory);\n\n this.logger = loggerFactory(MoonrakerWebsocketAdapter.name);\n }\n\n get _debugMode() {\n return this.configService.get(AppConstants.debugSocketStatesKey, AppConstants.defaultDebugSocketStates) === \"true\";\n }\n\n private get subscriptionObjects() {\n return {\n pause_resume: [],\n idle_timeout: [],\n print_stats: [],\n heaters: [],\n heater_bed: [],\n extruder: [],\n display_status: [],\n webhooks: [],\n virtual_sdcard: [],\n gcode_move: [],\n stepper_enable: [],\n fan: [],\n motion_report: [],\n system_stats: [],\n // custom: position, homed_axes, max_velocity, etc\n // toolhead: [],\n } as {\n [k in KnownPrinterObject]: [];\n };\n }\n\n needsReopen() {\n return false;\n }\n\n needsSetup() {\n return this.socketState === SOCKET_STATE.unopened;\n }\n\n needsReauth() {\n return false;\n }\n\n async reauthSession() {}\n\n registerCredentials(socketLogin: ISocketLogin) {\n const { printerId, loginDto } = socketLogin;\n this.printerId = printerId;\n this.login = loginDto;\n\n const httpUrlString = normalizeUrl(this.login.printerURL);\n const httpUrl = new URL(httpUrlString);\n const httpUrlPath = httpUrl.pathname;\n\n const wsUrl = httpToWsUrl(httpUrlString);\n wsUrl.pathname = (httpUrlPath ?? \"/\") + \"websocket\";\n this.socketURL = wsUrl;\n }\n\n open() {\n if (this.socket) {\n throw new Error(`Socket already exists by printerId, ignoring open request`);\n }\n\n super.open(this.socketURL);\n }\n\n close() {\n clearInterval(this.refreshPrinterObjectsInterval);\n super.close();\n }\n\n async setupSocketSession(): Promise<void> {\n this.resetSocketState();\n\n // Can 404 or 503\n // const oneshot = await this.client.getAccessOneshotToken(this.login);\n // this.logger.log(`Oneshot ${oneshot.data.result}`);\n // await this.client.getAccessOneshotToken(this.login);\n\n await this.moonrakerClient.getApiVersion(this.login).catch((e: AxiosError) => {\n this.setSocketState(\"aborted\");\n this.logger.error(`Printer (${this.printerId}) network or transport error, marking it as unreachable; ${e}`);\n this.setApiState(\"noResponse\");\n\n throw e;\n });\n this.setApiState(API_STATE.responding);\n\n await this.updateCurrentStateSafely();\n\n if (this.refreshPrinterObjectsInterval) {\n clearInterval(this.refreshPrinterObjectsInterval);\n }\n this.refreshPrinterObjectsInterval = setInterval(async () => {\n await this.updateCurrentStateSafely();\n }, 15000);\n }\n\n emitEventSync(event: string, payload: any) {\n if (!this.eventEmittingAllowed) {\n return;\n }\n\n this.eventEmitter2.emit(moonrakerEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n } as OctoPrintEventDto);\n }\n\n resetSocketState() {\n this.setSocketState(\"unopened\");\n this.setApiState(\"unset\");\n }\n\n isClosedOrAborted() {\n return this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.aborted;\n }\n\n protected async afterOpened(_: WsEvent): Promise<void> {\n this.setSocketState(SOCKET_STATE.opened);\n\n const response = await this.sendRequest<ConnectionIdentifyResponseDto, ConnectionIdentifyDto>({\n jsonrpc: \"2.0\",\n method: \"server.connection.identify\",\n params: {\n client_name: \"FDM Monster\",\n version: this.serverVersion,\n type: \"other\",\n url: AppConstants.githubUrl,\n },\n id: 1,\n });\n\n try {\n const query: Partial<Record<KnownPrinterObject, []>> = this.subscriptionObjects;\n\n const result = await this.moonrakerClient.postSubscribePrinterObjects<PrinterObjectsQueryDto<SubscriptionType>>(\n this.login,\n response.result.connection_id,\n query,\n );\n this.printerObjects = result.data.result;\n await this.emitCurrentEvent(this.printerObjects as PrinterObjectsQueryDto<SubscriptionType>);\n } catch (e) {\n const ae = e as MoonrakerErrorDto;\n if (ae.isAxiosError) {\n if (ae.response?.status === 503) {\n // shutdown, we should probably mark this host as problematic\n this.logger.warn(`Klipper host issue ${PP(ae.response.data?.error?.message)}`);\n } else if (ae.response?.status === 404) {\n this.logger.error(\"Error while afterOpened (404) - usually this means Moonraker is still starting\");\n }\n this.setApiState(API_STATE.noResponse);\n return;\n }\n\n this.logger.error(\"Unknown error while afterOpened\");\n this.setApiState(API_STATE.noResponse);\n }\n }\n\n protected async onEventMessage(event: JsonRpcEventDto): Promise<void> {\n this.lastMessageReceivedTimestamp = Date.now();\n\n if (this.socketState !== SOCKET_STATE.authenticated) {\n this.setSocketState(\"authenticated\");\n }\n\n const eventName = event.method;\n if (this._debugMode) {\n this.logger.log(`RX Msg ${eventName} ${JSON.stringify(event.params)?.substring(0, 80)}...`);\n }\n\n // Emit the message to the event bus\n const payload = event.params?.length ? event.params[0] : undefined;\n\n if (eventName === \"notify_service_state_changed\") {\n if (!event.params) {\n // We dont understand the service changed...\n this.logger.error(\"Received 'notify_service_state_changed' but service indicators params were undefined\");\n return;\n }\n const serviceChanged = event.params[0] as NotifyServiceStateChangedParams;\n if (\n serviceChanged.klipper?.active_state ||\n serviceChanged.klipper_mcu?.active_state ||\n serviceChanged.moonraker?.active_state\n ) {\n this.logger.log(\"Received notify_service_state_changed, reloading Moonraker printer objects\");\n await this.setupSocketSession();\n }\n return;\n }\n if (eventName === \"notify_klippy_ready\") {\n this.logger.log(\"Received notify_klippy_ready, reloading Moonraker printer objects\");\n return await this.setupSocketSession();\n }\n if (eventName === \"notify_klippy_disconnected\") {\n this.logger.log(\"Received notify_klippy_disconnected, reloading Moonraker printer objects\");\n return await this.setupSocketSession();\n }\n if (eventName === \"notify_klippy_shutdown\") {\n this.logger.log(\"Received notify_klippy_shutdown, reloading Moonraker printer objects\");\n return await this.setupSocketSession();\n }\n\n if (eventName === \"notify_status_update\") {\n if (!event.params) {\n // We dont understand the service changed...\n this.logger.error(\"Received 'notify_status_update' but service indicators params were undefined\");\n return;\n }\n\n const [data, eventtime] = (event as NotifyStatusUpdate<SubscriptionType>).params!;\n\n const subState = Object.keys(data)[0] as keyof SubscriptionType;\n if (Object.keys((this.printerObjects as PrinterObjectsQueryDto<SubscriptionType>).status).includes(subState)) {\n this.printerObjects.status = { ...this.printerObjects.status, ...data };\n this.printerObjects.eventtime = eventtime;\n await this.emitCurrentEvent(this.printerObjects);\n } else {\n this.logger.warn(`Substate ${subState} unknown`);\n }\n return;\n }\n\n await this.emitEvent(eventName, payload);\n }\n\n protected async afterClosed(event: any) {\n this.setSocketState(\"closed\");\n delete this.socket;\n await this.emitEvent(WsMessage.WS_CLOSED, \"connection closed\");\n }\n\n protected async onError(error: any) {\n this.setSocketState(\"error\");\n await this.emitEvent(WsMessage.WS_ERROR, error?.length ? error : \"connection error\");\n }\n\n private async updateCurrentStateSafely() {\n try {\n const query: Partial<Record<KnownPrinterObject, []>> = this.subscriptionObjects;\n const objects = await this.moonrakerClient.getPrinterObjectsQuery<PrinterObjectsQueryDto<SubscriptionType>>(\n this.login,\n query,\n );\n this.printerObjects = objects.data.result;\n this.setApiState(API_STATE.responding);\n return await this.emitCurrentEvent(this.printerObjects as PrinterObjectsQueryDto<SubscriptionType>);\n } catch (e) {\n // Could be network transient error, klippy error, or Moonraker host problem. All scenarios require a different approach.\n // transient: we should retry and if happening regularly report this host as problematic\n // klippy error (503): we should definitely clear up and report this as error\n // Moonraker host error: this means the socket has to be completely shut down\n const castError = e as MoonrakerErrorDto;\n if (castError.isAxiosError) {\n if (castError?.response?.status == 503) {\n this.printerObjects.status = null;\n this.printerObjects.eventtime = Date.now();\n return await this.emitCurrentEvent(this.printerObjects);\n }\n this.logger.error(\"Could not update Moonraker printer objects due to a request error\");\n this.setApiState(API_STATE.noResponse);\n return;\n }\n\n this.logger.error(`Could not update Moonraker current due to an unknown error`);\n this.setApiState(API_STATE.noResponse);\n }\n }\n\n private async emitCurrentEvent(printerObject: PrinterObjectsQueryDto<SubscriptionType | null>) {\n const originalKlipperObjects = printerObject.status;\n\n // When klipper not connected expect lots of 503\n // operational => no gcode or file being processed, but properly connected (klipper connected?)\n // notify_klippy_ready, notify_klippy_shutdown, notify_klippy_disconnected, notify_service_state_changed\n // stop klipper: notify_klippy_disconnected: notify_service_state_changed\n\n // Better to move this to socket and determine operational, error message there\n // https://github.com/Arksine/moonraker/blob/ba9428558adc70ed8cb3cdcea1723358ead23741/moonraker/components/octoprint_compat.py#L303\n const flags = {\n operational: false,\n printing: false,\n cancelling: false,\n pausing: false,\n paused: false,\n resuming: false,\n finishing: false,\n closedOrError: false,\n error: false,\n ready: false,\n sdReady: false,\n } as FlagsDto;\n let filename = \"\";\n let printTime = null;\n\n let stateText = \"Unset\";\n let error = \"\";\n let completion: number | null = null;\n\n if (originalKlipperObjects != null) {\n stateText = originalKlipperObjects.display_status?.message;\n if (originalKlipperObjects.print_stats?.state?.length) {\n const systemState = originalKlipperObjects.webhooks;\n const printState = originalKlipperObjects.print_stats.state;\n\n const idleState = originalKlipperObjects.idle_timeout?.state;\n filename = originalKlipperObjects.print_stats.filename;\n printTime = originalKlipperObjects.print_stats.print_duration;\n\n flags.operational = systemState.state === \"ready\";\n\n if (flags.operational) {\n flags.printing = printState === \"printing\";\n flags.paused = printState === \"paused\";\n flags.ready = printState === \"standby\" && idleState !== \"Printing\";\n flags.sdReady = true;\n } else {\n flags.error = true;\n stateText = \"Klipper reports: \" + (systemState.state ?? \"unknown\")?.toUpperCase();\n }\n }\n\n completion = (originalKlipperObjects.display_status?.progress ?? 0) * 100.0;\n }\n\n const currentMessage: FdmCurrentMessageDto = {\n progress: {\n printTime,\n completion,\n },\n state: {\n text: stateText,\n error,\n flags,\n },\n job: {\n file: {\n name: filename,\n path: filename,\n },\n },\n };\n\n await this.emitEvent(\"notify_status_update\", originalKlipperObjects);\n await this.emitEvent(\"current\", currentMessage);\n }\n\n private async emitEvent(event: string, payload?: any) {\n if (!this.eventEmittingAllowed) {\n return;\n }\n\n await this.eventEmitter2.emitAsync(moonrakerEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: MoonrakerType,\n } as MoonrakerEventDto);\n }\n\n private setSocketState(state: SocketState) {\n this.socketState = state;\n this.stateUpdated = true;\n this.stateUpdateTimestamp = Date.now();\n if (this._debugMode) {\n this.logger.log(`${this.printerId} Socket state updated to: ` + state);\n }\n this.emitEventSync(WsMessage.WS_STATE_UPDATED, state);\n }\n\n private setApiState(state: ApiState) {\n if (state === API_STATE.globalKey) {\n throw new Error(\"GlobalKey is an invalid WS state for Moonraker\");\n }\n this.apiState = state;\n this.apiStateUpdated = true;\n this.apiStateUpdateTimestamp = Date.now();\n if (this._debugMode) {\n this.logger.log(`${this.printerId} API state updated to: ` + state);\n }\n this.emitEventSync(WsMessage.API_STATE_UPDATED, state);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"moonraker-websocket.adapter.js","names":[],"sources":["../../../src/services/moonraker/moonraker-websocket.adapter.ts"],"sourcesContent":["import { MoonrakerClient } from \"@/services/moonraker/moonraker.client\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport EventEmitter2 from \"eventemitter2\";\nimport { ConfigService } from \"@/services/core/config.service\";\nimport type { ISocketLogin } from \"@/shared/dtos/socket-login.dto\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { AppConstants } from \"@/server.constants\";\nimport { httpToWsUrl } from \"@/utils/url.utils\";\nimport type { OctoPrintEventDto } from \"@/services/octoprint/dto/octoprint-event.dto\";\nimport { WsMessage } from \"@/services/octoprint/octoprint-websocket.adapter\";\nimport { moonrakerEvent } from \"@/services/moonraker/constants/moonraker.constants\";\nimport { SOCKET_STATE, SocketState } from \"@/shared/dtos/socket-state.type\";\nimport { API_STATE, ApiState } from \"@/shared/dtos/api-state.type\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport type { ConnectionIdentifyDto } from \"@/services/moonraker/dto/websocket/connection-identify.dto\";\nimport type { JsonRpcEventDto } from \"@/services/moonraker/dto/websocket/json-rpc-event.dto\";\nimport { KnownPrinterObject } from \"@/services/moonraker/dto/objects/printer-objects-list.dto\";\nimport { NotifyStatusUpdate } from \"@/services/moonraker/dto/websocket/message.types\";\nimport {\n DisplayStatusObject,\n ExtruderObject,\n FanObject,\n GcodeMoveObject,\n HeaterBedObject,\n HeatersObject,\n IdleTimeoutObject,\n MotionReportObject,\n PauseResumeObject,\n PrintStatsObject,\n StepperEnableObject,\n SystemStatsObject,\n VirtualSdCardObject,\n WebhooksObject,\n} from \"@/services/moonraker/dto/objects/printer-object.types\";\nimport { PP } from \"@/utils/pretty-print.utils\";\nimport type { MoonrakerErrorDto } from \"@/services/moonraker/dto/rest/error.dto\";\nimport type { MoonrakerEventDto } from \"@/services/moonraker/constants/moonraker-event.dto\";\nimport type { PrinterObjectsQueryDto } from \"@/services/moonraker/dto/objects/printer-objects-query.dto\";\nimport type { ConnectionIdentifyResponseDto } from \"@/services/moonraker/dto/websocket/connection-identify-response.dto\";\nimport type { FlagsDto } from \"@/services/octoprint/dto/printer/flags.dto\";\nimport { MoonrakerType, type FdmCurrentMessageDto } from \"@/services/printer-api.interface\";\nimport { Event as WsEvent } from \"ws\";\nimport { NotifyServiceStateChangedParams } from \"@/services/moonraker/dto/websocket/notify-service-state-changed.params\";\nimport { WebsocketRpcExtendedAdapter } from \"@/shared/websocket-rpc-extended.adapter\";\nimport type { IWebsocketAdapter } from \"@/services/websocket-adapter.interface\";\nimport { normalizeUrl } from \"@/utils/normalize-url\";\nimport { AxiosError } from \"axios\";\n\nexport type SubscriptionType = IdleTimeoutObject &\n PauseResumeObject &\n PrintStatsObject &\n HeatersObject &\n HeaterBedObject &\n ExtruderObject &\n DisplayStatusObject &\n WebhooksObject &\n VirtualSdCardObject &\n GcodeMoveObject &\n StepperEnableObject &\n FanObject &\n MotionReportObject &\n SystemStatsObject;\n\nexport class MoonrakerWebsocketAdapter extends WebsocketRpcExtendedAdapter implements IWebsocketAdapter {\n readonly printerType = 1;\n socketState: SocketState = SOCKET_STATE.unopened;\n lastMessageReceivedTimestamp: null | number = null;\n stateUpdated = false;\n stateUpdateTimestamp: null | number = null;\n apiStateUpdated = false;\n apiStateUpdateTimestamp: null | number = null;\n apiState: ApiState = API_STATE.unset;\n // Guaranteed to be set and valid by PrinterApiFactory\n login: LoginDto;\n printerId?: number;\n refreshPrinterObjectsInterval?: NodeJS.Timeout;\n printerObjects: PrinterObjectsQueryDto<SubscriptionType | null> = {\n eventtime: null,\n status: null,\n };\n protected logger: LoggerService;\n private socketURL?: URL;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly moonrakerClient: MoonrakerClient,\n private readonly eventEmitter2: EventEmitter2,\n private readonly configService: ConfigService,\n private readonly serverVersion: string,\n ) {\n super(loggerFactory);\n\n this.logger = loggerFactory(MoonrakerWebsocketAdapter.name);\n }\n\n get _debugMode() {\n return this.configService.get(AppConstants.debugSocketStatesKey, AppConstants.defaultDebugSocketStates) === \"true\";\n }\n\n private get subscriptionObjects() {\n return {\n pause_resume: [],\n idle_timeout: [],\n print_stats: [],\n heaters: [],\n heater_bed: [],\n extruder: [],\n display_status: [],\n webhooks: [],\n virtual_sdcard: [],\n gcode_move: [],\n stepper_enable: [],\n fan: [],\n motion_report: [],\n system_stats: [],\n // custom: position, homed_axes, max_velocity, etc\n // toolhead: [],\n } as {\n [k in KnownPrinterObject]: [];\n };\n }\n\n needsReopen() {\n return false;\n }\n\n needsSetup() {\n return this.socketState === SOCKET_STATE.unopened;\n }\n\n needsReauth() {\n return false;\n }\n\n async reauthSession() {}\n\n registerCredentials(socketLogin: ISocketLogin) {\n const { printerId, loginDto } = socketLogin;\n this.printerId = printerId;\n this.login = loginDto;\n\n const httpUrlString = normalizeUrl(this.login.printerURL);\n const httpUrl = new URL(httpUrlString);\n const httpUrlPath = httpUrl.pathname;\n\n const wsUrl = httpToWsUrl(httpUrlString);\n wsUrl.pathname = (httpUrlPath ?? \"/\") + \"websocket\";\n this.socketURL = wsUrl;\n }\n\n open() {\n if (this.socket) {\n throw new Error(`Socket already exists by printerId, ignoring open request`);\n }\n\n super.open(this.socketURL);\n }\n\n close() {\n clearInterval(this.refreshPrinterObjectsInterval);\n super.close();\n }\n\n async setupSocketSession(): Promise<void> {\n this.resetSocketState();\n\n // Can 404 or 503\n // const oneshot = await this.client.getAccessOneshotToken(this.login);\n // this.logger.log(`Oneshot ${oneshot.data.result}`);\n // await this.client.getAccessOneshotToken(this.login);\n\n await this.moonrakerClient.getApiVersion(this.login).catch((e: AxiosError) => {\n this.setSocketState(\"aborted\");\n this.logger.error(`Printer (${this.printerId}) network or transport error, marking it as unreachable; ${e}`);\n this.setApiState(\"noResponse\");\n\n throw e;\n });\n this.setApiState(API_STATE.responding);\n\n await this.updateCurrentStateSafely();\n\n if (this.refreshPrinterObjectsInterval) {\n clearInterval(this.refreshPrinterObjectsInterval);\n }\n this.refreshPrinterObjectsInterval = setInterval(async () => {\n await this.updateCurrentStateSafely();\n }, 15000);\n }\n\n emitEventSync(event: string, payload: any) {\n if (!this.eventEmittingAllowed) {\n return;\n }\n\n this.eventEmitter2.emit(moonrakerEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n } as OctoPrintEventDto);\n }\n\n resetSocketState() {\n this.setSocketState(\"unopened\");\n this.setApiState(\"unset\");\n }\n\n isClosedOrAborted() {\n return this.socketState === SOCKET_STATE.closed || this.socketState === SOCKET_STATE.aborted;\n }\n\n protected async afterOpened(_: WsEvent): Promise<void> {\n this.setSocketState(SOCKET_STATE.opened);\n\n const response = await this.sendRequest<ConnectionIdentifyResponseDto, ConnectionIdentifyDto>({\n jsonrpc: \"2.0\",\n method: \"server.connection.identify\",\n params: {\n client_name: \"FDM Monster\",\n version: this.serverVersion,\n type: \"other\",\n url: AppConstants.githubUrl,\n },\n id: 1,\n });\n\n try {\n const query: Partial<Record<KnownPrinterObject, []>> = this.subscriptionObjects;\n\n const result = await this.moonrakerClient.postSubscribePrinterObjects<PrinterObjectsQueryDto<SubscriptionType>>(\n this.login,\n response.result.connection_id,\n query,\n );\n this.printerObjects = result.data.result;\n await this.emitCurrentEvent(this.printerObjects as PrinterObjectsQueryDto<SubscriptionType>);\n } catch (e) {\n const ae = e as MoonrakerErrorDto;\n if (ae.isAxiosError) {\n if (ae.response?.status === 503) {\n // shutdown, we should probably mark this host as problematic\n this.logger.warn(`Klipper host issue ${PP(ae.response.data?.error?.message)}`);\n } else if (ae.response?.status === 404) {\n this.logger.error(\"Error while afterOpened (404) - usually this means Moonraker is still starting\");\n }\n this.setApiState(API_STATE.noResponse);\n return;\n }\n\n this.logger.error(\"Unknown error while afterOpened\");\n this.setApiState(API_STATE.noResponse);\n }\n }\n\n protected async onEventMessage(event: JsonRpcEventDto): Promise<void> {\n this.lastMessageReceivedTimestamp = Date.now();\n\n if (this.socketState !== SOCKET_STATE.authenticated) {\n this.setSocketState(\"authenticated\");\n }\n\n const eventName = event.method;\n if (this._debugMode) {\n this.logger.log(`RX Msg ${eventName} ${JSON.stringify(event.params)?.substring(0, 80)}...`);\n }\n\n // Emit the message to the event bus\n const payload = event.params?.length ? event.params[0] : undefined;\n\n if (eventName === \"notify_service_state_changed\") {\n if (!event.params) {\n // We dont understand the service changed...\n this.logger.error(\"Received 'notify_service_state_changed' but service indicators params were undefined\");\n return;\n }\n const serviceChanged = event.params[0] as NotifyServiceStateChangedParams;\n if (\n serviceChanged.klipper?.active_state ||\n serviceChanged.klipper_mcu?.active_state ||\n serviceChanged.moonraker?.active_state\n ) {\n this.logger.log(\"Received notify_service_state_changed, reloading Moonraker printer objects\");\n await this.setupSocketSession();\n }\n return;\n }\n if (eventName === \"notify_klippy_ready\") {\n this.logger.log(\"Received notify_klippy_ready, reloading Moonraker printer objects\");\n return await this.setupSocketSession();\n }\n if (eventName === \"notify_klippy_disconnected\") {\n this.logger.log(\"Received notify_klippy_disconnected, reloading Moonraker printer objects\");\n return await this.setupSocketSession();\n }\n if (eventName === \"notify_klippy_shutdown\") {\n this.logger.log(\"Received notify_klippy_shutdown, reloading Moonraker printer objects\");\n return await this.setupSocketSession();\n }\n\n if (eventName === \"notify_status_update\") {\n if (!event.params) {\n // We dont understand the service changed...\n this.logger.error(\"Received 'notify_status_update' but service indicators params were undefined\");\n return;\n }\n\n const [data, eventtime] = (event as NotifyStatusUpdate<SubscriptionType>).params!;\n\n const subState = Object.keys(data)[0] as keyof SubscriptionType;\n if (Object.keys((this.printerObjects as PrinterObjectsQueryDto<SubscriptionType>).status).includes(subState)) {\n this.printerObjects.status = { ...this.printerObjects.status, ...data };\n this.printerObjects.eventtime = eventtime;\n await this.emitCurrentEvent(this.printerObjects);\n } else {\n this.logger.warn(`Substate ${subState} unknown`);\n }\n return;\n }\n\n await this.emitEvent(eventName, payload);\n }\n\n protected async afterClosed(event: any) {\n this.setSocketState(\"closed\");\n delete this.socket;\n await this.emitEvent(WsMessage.WS_CLOSED, \"connection closed\");\n }\n\n protected async onError(error: any) {\n this.setSocketState(\"error\");\n await this.emitEvent(WsMessage.WS_ERROR, error?.length ? error : \"connection error\");\n }\n\n private async updateCurrentStateSafely() {\n try {\n const query: Partial<Record<KnownPrinterObject, []>> = this.subscriptionObjects;\n const objects = await this.moonrakerClient.getPrinterObjectsQuery<PrinterObjectsQueryDto<SubscriptionType>>(\n this.login,\n query,\n );\n this.printerObjects = objects.data.result;\n this.setApiState(API_STATE.responding);\n return await this.emitCurrentEvent(this.printerObjects as PrinterObjectsQueryDto<SubscriptionType>);\n } catch (e) {\n // Could be network transient error, klippy error, or Moonraker host problem. All scenarios require a different approach.\n // transient: we should retry and if happening regularly report this host as problematic\n // klippy error (503): we should definitely clear up and report this as error\n // Moonraker host error: this means the socket has to be completely shut down\n const castError = e as MoonrakerErrorDto;\n if (castError.isAxiosError) {\n if (castError?.response?.status == 503) {\n this.printerObjects.status = null;\n this.printerObjects.eventtime = Date.now();\n return await this.emitCurrentEvent(this.printerObjects);\n }\n this.logger.error(\"Could not update Moonraker printer objects due to a request error\");\n this.setApiState(API_STATE.noResponse);\n return;\n }\n\n this.logger.error(`Could not update Moonraker current due to an unknown error`);\n this.setApiState(API_STATE.noResponse);\n }\n }\n\n private async emitCurrentEvent(printerObject: PrinterObjectsQueryDto<SubscriptionType | null>) {\n const originalKlipperObjects = printerObject.status;\n\n // When klipper not connected expect lots of 503\n // operational => no gcode or file being processed, but properly connected (klipper connected?)\n // notify_klippy_ready, notify_klippy_shutdown, notify_klippy_disconnected, notify_service_state_changed\n // stop klipper: notify_klippy_disconnected: notify_service_state_changed\n\n // Better to move this to socket and determine operational, error message there\n // https://github.com/Arksine/moonraker/blob/ba9428558adc70ed8cb3cdcea1723358ead23741/moonraker/components/octoprint_compat.py#L303\n const flags = {\n operational: false,\n printing: false,\n cancelling: false,\n pausing: false,\n paused: false,\n resuming: false,\n finishing: false,\n closedOrError: false,\n error: false,\n ready: false,\n sdReady: false,\n } as FlagsDto;\n let filename = \"\";\n let printTime = null;\n\n let stateText = \"Unset\";\n let error = \"\";\n let completion: number | null = null;\n\n if (originalKlipperObjects != null) {\n stateText = originalKlipperObjects.display_status?.message;\n if (originalKlipperObjects.print_stats?.state?.length) {\n const systemState = originalKlipperObjects.webhooks;\n const printState = originalKlipperObjects.print_stats.state;\n\n const idleState = originalKlipperObjects.idle_timeout?.state;\n filename = originalKlipperObjects.print_stats.filename;\n printTime = originalKlipperObjects.print_stats.print_duration;\n\n flags.operational = systemState.state === \"ready\";\n\n if (flags.operational) {\n flags.printing = printState === \"printing\";\n flags.paused = printState === \"paused\";\n flags.ready = printState === \"standby\" && idleState !== \"Printing\";\n flags.sdReady = true;\n } else {\n flags.error = true;\n stateText = \"Klipper reports: \" + (systemState.state ?? \"unknown\")?.toUpperCase();\n }\n }\n\n completion = (originalKlipperObjects.display_status?.progress ?? 0) * 100.0;\n }\n\n const currentMessage: FdmCurrentMessageDto = {\n progress: {\n printTime,\n completion,\n },\n state: {\n text: stateText,\n error,\n flags,\n },\n job: {\n file: {\n name: filename,\n path: filename,\n },\n },\n };\n\n await this.emitEvent(\"notify_status_update\", originalKlipperObjects);\n await this.emitEvent(\"current\", currentMessage);\n }\n\n private async emitEvent(event: string, payload?: any) {\n if (!this.eventEmittingAllowed) {\n return;\n }\n\n await this.eventEmitter2.emitAsync(moonrakerEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: MoonrakerType,\n } as MoonrakerEventDto);\n }\n\n private setSocketState(state: SocketState) {\n this.socketState = state;\n this.stateUpdated = true;\n this.stateUpdateTimestamp = Date.now();\n if (this._debugMode) {\n this.logger.log(`${this.printerId} Socket state updated to: ` + state);\n }\n this.emitEventSync(WsMessage.WS_STATE_UPDATED, state);\n }\n\n private setApiState(state: ApiState) {\n if (state === API_STATE.globalKey) {\n throw new Error(\"GlobalKey is an invalid WS state for Moonraker\");\n }\n this.apiState = state;\n this.apiStateUpdated = true;\n this.apiStateUpdateTimestamp = Date.now();\n if (this._debugMode) {\n this.logger.log(`${this.printerId} API state updated to: ` + state);\n }\n this.emitEventSync(WsMessage.API_STATE_UPDATED, state);\n }\n}\n"],"mappings":"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"}
|