@fdm-monster/server 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/RELEASE_NOTES.MD +14 -0
- 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/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.map +1 -1
- package/dist/controllers/api-key.controller.js +2 -2
- package/dist/controllers/api-key.controller.js.map +1 -1
- package/dist/controllers/auth.controller.js +5 -3
- 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/entities/api-key.entity.js +2 -2
- 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/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.map +1 -1
- 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.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.map +1 -1
- 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.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/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.map +1 -1
- 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 +10 -7
- package/packages/consoles/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.service.js","names":[],"sources":["../../../src/services/orm/settings.service.ts"],"sourcesContent":["import { Settings } from \"@/entities\";\nimport {\n credentialSettingsKey,\n frontendSettingKey,\n getDefaultSettings,\n serverSettingsKey,\n timeoutSettingKey,\n wizardSettingKey,\n} from \"@/constants/server-settings.constants\";\nimport { BaseService } from \"@/services/orm/base.service\";\nimport { SettingsDto } from \"../interfaces/settings.dto\";\nimport type { ISettingsService } from \"@/services/interfaces/settings.service.interface\";\nimport { z } from \"zod\";\nimport {\n credentialCoreSettingUpdateSchema,\n frontendSettingsUpdateSchema,\n jwtSecretCredentialSettingUpdateSchema,\n serverSettingsUpdateSchema,\n timeoutSettingsUpdateSchema,\n wizardUpdateSchema,\n slicerApiKeyUpdateSchema,\n} from \"@/services/validators/settings-service.validation\";\nimport { migrateSettingsRuntime } from \"@/shared/runtime-settings.migration\";\nimport { validateInput } from \"@/handlers/validators\";\n\nexport class SettingsService extends BaseService(Settings, SettingsDto) implements ISettingsService {\n toDto(entity: Settings): SettingsDto {\n return {\n [serverSettingsKey]: entity[serverSettingsKey],\n [frontendSettingKey]: entity[frontendSettingKey],\n [wizardSettingKey]: entity[wizardSettingKey],\n [timeoutSettingKey]: entity[timeoutSettingKey],\n };\n }\n\n async getOrCreate() {\n let settings = await this.getOptional();\n\n if (settings) {\n settings = migrateSettingsRuntime(settings);\n\n const settingsId = settings.id;\n return await this.update(settingsId, settings);\n } else {\n return await this.create(getDefaultSettings());\n }\n }\n\n async updateServerSettings(update: z.infer<typeof serverSettingsUpdateSchema>) {\n const validatedInput = await validateInput(update, serverSettingsUpdateSchema);\n const entity = await this.getOrCreate();\n entity[serverSettingsKey] = validatedInput;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateJwtSecretCredentialSetting(update: z.infer<typeof jwtSecretCredentialSettingUpdateSchema>) {\n const validatedInput = await validateInput(update, jwtSecretCredentialSettingUpdateSchema);\n const entity = await this.getOrCreate();\n entity[credentialSettingsKey].jwtSecret = validatedInput.jwtSecret;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateCoreCredentialSettings(update: z.infer<typeof credentialCoreSettingUpdateSchema>) {\n const validatedInput = await validateInput(update, credentialCoreSettingUpdateSchema);\n const entity = await this.getOrCreate();\n entity[credentialSettingsKey].refreshTokenExpiry = validatedInput.refreshTokenExpiry;\n entity[credentialSettingsKey].refreshTokenAttempts = validatedInput.refreshTokenAttempts;\n entity[credentialSettingsKey].jwtExpiresIn = validatedInput.jwtExpiresIn;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateSlicerApiKey(update: z.infer<typeof slicerApiKeyUpdateSchema>) {\n const validatedInput = await validateInput(update, slicerApiKeyUpdateSchema);\n const entity = await this.getOrCreate();\n entity[credentialSettingsKey].slicerApiKey = validatedInput.slicerApiKey;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateFrontendSettings(update: z.infer<typeof frontendSettingsUpdateSchema>) {\n const validatedInput = await validateInput(update, frontendSettingsUpdateSchema);\n const entity = await this.getOrCreate();\n entity[frontendSettingKey] = validatedInput;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateTimeoutSettings(update: z.infer<typeof timeoutSettingsUpdateSchema>) {\n const validatedInput = await validateInput(update, timeoutSettingsUpdateSchema);\n const entity = await this.getOrCreate();\n entity[timeoutSettingKey] = validatedInput;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateWizardSettings(update: z.infer<typeof wizardUpdateSchema>) {\n const validatedInput = await validateInput(update, wizardUpdateSchema);\n const entity = await this.getOrCreate();\n entity[wizardSettingKey] = validatedInput;\n await this.update(entity.id, entity);\n return entity;\n }\n\n private async getOptional(): Promise<Settings | null> {\n const settingsList = await this.repository.find({ take: 1 });\n return settingsList?.length ? settingsList[0] : null;\n }\n}\n"],"mappings":";;;;;;;;;AAyBA,IAAa,kBAAb,cAAqC,YAAY,UAAU,YAAY,CAA6B;CAClG,MAAM,QAA+B;
|
|
1
|
+
{"version":3,"file":"settings.service.js","names":[],"sources":["../../../src/services/orm/settings.service.ts"],"sourcesContent":["import { Settings } from \"@/entities\";\nimport {\n credentialSettingsKey,\n frontendSettingKey,\n getDefaultSettings,\n serverSettingsKey,\n timeoutSettingKey,\n wizardSettingKey,\n} from \"@/constants/server-settings.constants\";\nimport { BaseService } from \"@/services/orm/base.service\";\nimport { SettingsDto } from \"../interfaces/settings.dto\";\nimport type { ISettingsService } from \"@/services/interfaces/settings.service.interface\";\nimport { z } from \"zod\";\nimport {\n credentialCoreSettingUpdateSchema,\n frontendSettingsUpdateSchema,\n jwtSecretCredentialSettingUpdateSchema,\n serverSettingsUpdateSchema,\n timeoutSettingsUpdateSchema,\n wizardUpdateSchema,\n slicerApiKeyUpdateSchema,\n} from \"@/services/validators/settings-service.validation\";\nimport { migrateSettingsRuntime } from \"@/shared/runtime-settings.migration\";\nimport { validateInput } from \"@/handlers/validators\";\n\nexport class SettingsService extends BaseService(Settings, SettingsDto) implements ISettingsService {\n toDto(entity: Settings): SettingsDto {\n return {\n [serverSettingsKey]: entity[serverSettingsKey],\n [frontendSettingKey]: entity[frontendSettingKey],\n [wizardSettingKey]: entity[wizardSettingKey],\n [timeoutSettingKey]: entity[timeoutSettingKey],\n };\n }\n\n async getOrCreate() {\n let settings = await this.getOptional();\n\n if (settings) {\n settings = migrateSettingsRuntime(settings);\n\n const settingsId = settings.id;\n return await this.update(settingsId, settings);\n } else {\n return await this.create(getDefaultSettings());\n }\n }\n\n async updateServerSettings(update: z.infer<typeof serverSettingsUpdateSchema>) {\n const validatedInput = await validateInput(update, serverSettingsUpdateSchema);\n const entity = await this.getOrCreate();\n entity[serverSettingsKey] = validatedInput;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateJwtSecretCredentialSetting(update: z.infer<typeof jwtSecretCredentialSettingUpdateSchema>) {\n const validatedInput = await validateInput(update, jwtSecretCredentialSettingUpdateSchema);\n const entity = await this.getOrCreate();\n entity[credentialSettingsKey].jwtSecret = validatedInput.jwtSecret;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateCoreCredentialSettings(update: z.infer<typeof credentialCoreSettingUpdateSchema>) {\n const validatedInput = await validateInput(update, credentialCoreSettingUpdateSchema);\n const entity = await this.getOrCreate();\n entity[credentialSettingsKey].refreshTokenExpiry = validatedInput.refreshTokenExpiry;\n entity[credentialSettingsKey].refreshTokenAttempts = validatedInput.refreshTokenAttempts;\n entity[credentialSettingsKey].jwtExpiresIn = validatedInput.jwtExpiresIn;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateSlicerApiKey(update: z.infer<typeof slicerApiKeyUpdateSchema>) {\n const validatedInput = await validateInput(update, slicerApiKeyUpdateSchema);\n const entity = await this.getOrCreate();\n entity[credentialSettingsKey].slicerApiKey = validatedInput.slicerApiKey;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateFrontendSettings(update: z.infer<typeof frontendSettingsUpdateSchema>) {\n const validatedInput = await validateInput(update, frontendSettingsUpdateSchema);\n const entity = await this.getOrCreate();\n entity[frontendSettingKey] = validatedInput;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateTimeoutSettings(update: z.infer<typeof timeoutSettingsUpdateSchema>) {\n const validatedInput = await validateInput(update, timeoutSettingsUpdateSchema);\n const entity = await this.getOrCreate();\n entity[timeoutSettingKey] = validatedInput;\n await this.update(entity.id, entity);\n return entity;\n }\n\n async updateWizardSettings(update: z.infer<typeof wizardUpdateSchema>) {\n const validatedInput = await validateInput(update, wizardUpdateSchema);\n const entity = await this.getOrCreate();\n entity[wizardSettingKey] = validatedInput;\n await this.update(entity.id, entity);\n return entity;\n }\n\n private async getOptional(): Promise<Settings | null> {\n const settingsList = await this.repository.find({ take: 1 });\n return settingsList?.length ? settingsList[0] : null;\n }\n}\n"],"mappings":";;;;;;;;;AAyBA,IAAa,kBAAb,cAAqC,YAAY,UAAU,YAAY,CAA6B;CAClG,MAAM,QAA+B;EACnC,OAAO;IACJ,oBAAoB,OAAO;IAC3B,qBAAqB,OAAO;IAC5B,mBAAmB,OAAO;IAC1B,oBAAoB,OAAO;GAC7B;;CAGH,MAAM,cAAc;EAClB,IAAI,WAAW,MAAM,KAAK,aAAa;EAEvC,IAAI,UAAU;GACZ,WAAW,uBAAuB,SAAS;GAE3C,MAAM,aAAa,SAAS;GAC5B,OAAO,MAAM,KAAK,OAAO,YAAY,SAAS;SAE9C,OAAO,MAAM,KAAK,OAAO,oBAAoB,CAAC;;CAIlD,MAAM,qBAAqB,QAAoD;EAC7E,MAAM,iBAAiB,MAAM,cAAc,QAAQ,2BAA2B;EAC9E,MAAM,SAAS,MAAM,KAAK,aAAa;EACvC,OAAO,qBAAqB;EAC5B,MAAM,KAAK,OAAO,OAAO,IAAI,OAAO;EACpC,OAAO;;CAGT,MAAM,iCAAiC,QAAgE;EACrG,MAAM,iBAAiB,MAAM,cAAc,QAAQ,uCAAuC;EAC1F,MAAM,SAAS,MAAM,KAAK,aAAa;EACvC,OAAO,uBAAuB,YAAY,eAAe;EACzD,MAAM,KAAK,OAAO,OAAO,IAAI,OAAO;EACpC,OAAO;;CAGT,MAAM,6BAA6B,QAA2D;EAC5F,MAAM,iBAAiB,MAAM,cAAc,QAAQ,kCAAkC;EACrF,MAAM,SAAS,MAAM,KAAK,aAAa;EACvC,OAAO,uBAAuB,qBAAqB,eAAe;EAClE,OAAO,uBAAuB,uBAAuB,eAAe;EACpE,OAAO,uBAAuB,eAAe,eAAe;EAC5D,MAAM,KAAK,OAAO,OAAO,IAAI,OAAO;EACpC,OAAO;;CAGT,MAAM,mBAAmB,QAAkD;EACzE,MAAM,iBAAiB,MAAM,cAAc,QAAQ,yBAAyB;EAC5E,MAAM,SAAS,MAAM,KAAK,aAAa;EACvC,OAAO,uBAAuB,eAAe,eAAe;EAC5D,MAAM,KAAK,OAAO,OAAO,IAAI,OAAO;EACpC,OAAO;;CAGT,MAAM,uBAAuB,QAAsD;EACjF,MAAM,iBAAiB,MAAM,cAAc,QAAQ,6BAA6B;EAChF,MAAM,SAAS,MAAM,KAAK,aAAa;EACvC,OAAO,sBAAsB;EAC7B,MAAM,KAAK,OAAO,OAAO,IAAI,OAAO;EACpC,OAAO;;CAGT,MAAM,sBAAsB,QAAqD;EAC/E,MAAM,iBAAiB,MAAM,cAAc,QAAQ,4BAA4B;EAC/E,MAAM,SAAS,MAAM,KAAK,aAAa;EACvC,OAAO,qBAAqB;EAC5B,MAAM,KAAK,OAAO,OAAO,IAAI,OAAO;EACpC,OAAO;;CAGT,MAAM,qBAAqB,QAA4C;EACrE,MAAM,iBAAiB,MAAM,cAAc,QAAQ,mBAAmB;EACtE,MAAM,SAAS,MAAM,KAAK,aAAa;EACvC,OAAO,oBAAoB;EAC3B,MAAM,KAAK,OAAO,OAAO,IAAI,OAAO;EACpC,OAAO;;CAGT,MAAc,cAAwC;EACpD,MAAM,eAAe,MAAM,KAAK,WAAW,KAAK,EAAE,MAAM,GAAG,CAAC;EAC5D,OAAO,cAAc,SAAS,aAAa,KAAK"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-role.service.js","names":[],"sources":["../../../src/services/orm/user-role.service.ts"],"sourcesContent":["import { BaseService } from \"@/services/orm/base.service\";\nimport { UserRole } from \"@/entities/user-role.entity\";\nimport { UserRoleDto } from \"@/services/interfaces/user-role.dto\";\nimport { In } from \"typeorm\";\n\nexport class UserRoleService extends BaseService(UserRole, UserRoleDto) {\n toDto(entity: UserRole): UserRoleDto {\n return {\n id: entity.id,\n roleId: entity.roleId,\n userId: entity.userId,\n createdAt: entity.createdAt,\n };\n }\n\n async setUserRoles(userId: number, roleIds: number[]) {\n const previousRoles = await this.listUserRoles(userId);\n const previousIds = previousRoles.map((p) => p.roleId);\n\n const deletedRoleIds = previousIds.filter((p) => !roleIds.includes(p));\n await this.deleteManyUserRoles(userId, deletedRoleIds);\n const createdRoleIds = roleIds.filter((r) => !previousIds.includes(r));\n for (let roleId of createdRoleIds) {\n await this.create({\n roleId,\n userId,\n });\n }\n }\n\n deleteManyUserRoles(userId: number, roleIds: number[]) {\n return this.repository.delete({\n userId,\n roleId: In(roleIds),\n });\n }\n\n listByRoleId(roleId: number) {\n return this.repository.findBy({ roleId });\n }\n\n async listUserRoles(userId: number) {\n return this.repository.findBy({ userId });\n }\n}\n"],"mappings":";;;;;AAKA,IAAa,kBAAb,cAAqC,YAAY,UAAU,YAAY,CAAC;CACtE,MAAM,QAA+B;
|
|
1
|
+
{"version":3,"file":"user-role.service.js","names":[],"sources":["../../../src/services/orm/user-role.service.ts"],"sourcesContent":["import { BaseService } from \"@/services/orm/base.service\";\nimport { UserRole } from \"@/entities/user-role.entity\";\nimport { UserRoleDto } from \"@/services/interfaces/user-role.dto\";\nimport { In } from \"typeorm\";\n\nexport class UserRoleService extends BaseService(UserRole, UserRoleDto) {\n toDto(entity: UserRole): UserRoleDto {\n return {\n id: entity.id,\n roleId: entity.roleId,\n userId: entity.userId,\n createdAt: entity.createdAt,\n };\n }\n\n async setUserRoles(userId: number, roleIds: number[]) {\n const previousRoles = await this.listUserRoles(userId);\n const previousIds = previousRoles.map((p) => p.roleId);\n\n const deletedRoleIds = previousIds.filter((p) => !roleIds.includes(p));\n await this.deleteManyUserRoles(userId, deletedRoleIds);\n const createdRoleIds = roleIds.filter((r) => !previousIds.includes(r));\n for (let roleId of createdRoleIds) {\n await this.create({\n roleId,\n userId,\n });\n }\n }\n\n deleteManyUserRoles(userId: number, roleIds: number[]) {\n return this.repository.delete({\n userId,\n roleId: In(roleIds),\n });\n }\n\n listByRoleId(roleId: number) {\n return this.repository.findBy({ roleId });\n }\n\n async listUserRoles(userId: number) {\n return this.repository.findBy({ userId });\n }\n}\n"],"mappings":";;;;;AAKA,IAAa,kBAAb,cAAqC,YAAY,UAAU,YAAY,CAAC;CACtE,MAAM,QAA+B;EACnC,OAAO;GACL,IAAI,OAAO;GACX,QAAQ,OAAO;GACf,QAAQ,OAAO;GACf,WAAW,OAAO;GACnB;;CAGH,MAAM,aAAa,QAAgB,SAAmB;EAEpD,MAAM,eAAc,MADQ,KAAK,cAAc,OAAO,EACpB,KAAK,MAAM,EAAE,OAAO;EAEtD,MAAM,iBAAiB,YAAY,QAAQ,MAAM,CAAC,QAAQ,SAAS,EAAE,CAAC;EACtE,MAAM,KAAK,oBAAoB,QAAQ,eAAe;EACtD,MAAM,iBAAiB,QAAQ,QAAQ,MAAM,CAAC,YAAY,SAAS,EAAE,CAAC;EACtE,KAAK,IAAI,UAAU,gBACjB,MAAM,KAAK,OAAO;GAChB;GACA;GACD,CAAC;;CAIN,oBAAoB,QAAgB,SAAmB;EACrD,OAAO,KAAK,WAAW,OAAO;GAC5B;GACA,QAAQ,GAAG,QAAQ;GACpB,CAAC;;CAGJ,aAAa,QAAgB;EAC3B,OAAO,KAAK,WAAW,OAAO,EAAE,QAAQ,CAAC;;CAG3C,MAAM,cAAc,QAAgB;EAClC,OAAO,KAAK,WAAW,OAAO,EAAE,QAAQ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.service.js","names":[],"sources":["../../../src/services/orm/user.service.ts"],"sourcesContent":["import { BaseService } from \"@/services/orm/base.service\";\nimport { RegisterUserDto, UserDto } from \"@/services/interfaces/user.dto\";\nimport { User } from \"@/entities\";\nimport type { IUserService } from \"@/services/interfaces/user-service.interface\";\nimport { In } from \"typeorm\";\nimport { InternalServerException, NotFoundException } from \"@/exceptions/runtime.exceptions\";\nimport { validateInput } from \"@/handlers/validators\";\nimport { newPasswordSchema, registerUserSchema } from \"@/services/validators/user-service.validation\";\nimport { comparePasswordHash, hashPassword } from \"@/utils/crypto.utils\";\nimport { TypeormService } from \"@/services/typeorm/typeorm.service\";\nimport { UserRoleService } from \"@/services/orm/user-role.service\";\nimport { ROLES, type RoleName } from \"@/constants/authorization.constants\";\nimport { RoleService } from \"@/services/orm/role.service\";\n\nexport class UserService extends BaseService(User, UserDto) implements IUserService {\n constructor(\n typeormService: TypeormService,\n private readonly userRoleService: UserRoleService,\n private readonly roleService: RoleService,\n ) {\n super(typeormService);\n }\n\n toDto(user: User): UserDto {\n const roleIds = (user.roles ?? []).map((r) => r.roleId);\n return {\n id: user.id,\n createdAt: user.createdAt,\n isVerified: user.isVerified,\n isDemoUser: user.isDemoUser,\n isRootUser: user.isRootUser,\n username: user.username,\n needsPasswordChange: user.needsPasswordChange,\n roles: this.roleService.roleIdsToRoleNames(roleIds),\n };\n }\n\n listUsers(limit?: number): Promise<User[]> {\n return this.list({ take: limit });\n }\n\n async getUser(userId: number) {\n return this.get(userId);\n }\n\n async getUserRoles(userId: number): Promise<RoleName[]> {\n const userRoles = await this.userRoleService.listUserRoles(userId);\n const roleIds = userRoles.map((ur) => ur.roleId);\n return this.roleService.roleIdsToRoleNames(roleIds);\n }\n\n findRootUsers(): Promise<User[]> {\n return this.repository.findBy({ isRootUser: true });\n }\n\n findVerifiedUsers(): Promise<User[]> {\n return this.repository.findBy({ isVerified: true });\n }\n\n async isUserRootUser(userId: number): Promise<boolean> {\n const entity = await this.get(userId);\n return entity.isRootUser;\n }\n\n async deleteUser(userId: number) {\n // Validate\n const user = await this.getUser(userId);\n\n if (user.isRootUser) {\n throw new InternalServerException(\"Cannot delete a root user\");\n }\n\n if (!user.roles?.length) {\n throw new InternalServerException(\"User:roles relation not loaded, cannot perform deletion role check\");\n }\n\n // Check if the user is the last admin\n const role = this.roleService.getRoleByName(ROLES.ADMIN);\n if (user.roles.find((r) => r.roleId == role.id)) {\n const administrators = await this.findUsersByRoleId(role.id);\n if (administrators?.length === 1 && administrators[0].id === userId) {\n throw new InternalServerException(\"Cannot delete the last user with ADMIN role\");\n }\n }\n\n await this.delete(userId);\n }\n\n findRawByUsername(username: string): Promise<User | null> {\n return this.repository.findOneBy({ username });\n }\n\n async getDemoUserId(): Promise<number | undefined> {\n return (await this.repository.findOneBy({ isDemoUser: true }))?.id;\n }\n\n async setIsRootUserById(userId: number, isRootUser: boolean): Promise<void> {\n const user = await this.getUser(userId);\n if (!user) throw new NotFoundException(\"User not found\");\n\n if (!isRootUser) {\n // Ensure at least one user is root user\n const rootUsers = await this.findRootUsers();\n if (rootUsers.length === 1 && rootUsers[0].id === userId) {\n throw new InternalServerException(\"Cannot set the last root user to non-root user\");\n }\n }\n\n await this.update(userId, { isRootUser });\n }\n\n async setUserRoles(userId: number, roles: RoleName[]): Promise<User> {\n const roleIds = roles.map((roleName) => this.roleService.getRoleByName(roleName).id);\n await this.userRoleService.setUserRoles(userId, roleIds);\n return await this.get(userId);\n }\n\n async setVerifiedById(userId: number, isVerified: boolean): Promise<void> {\n const user = await this.getUser(userId);\n if (!user) throw new NotFoundException(\"User not found\");\n\n if (!isVerified) {\n if (user.isRootUser) {\n throw new InternalServerException(\"Cannot set a owner (root user) to unverified\");\n }\n\n // Ensure at least one user is verified\n const verifiedUsers = await this.findVerifiedUsers();\n if (verifiedUsers.length === 1) {\n throw new InternalServerException(\"Cannot set the last user to unverified\");\n }\n }\n\n await this.update(userId, { isVerified });\n }\n\n async updatePasswordById(userId: number, oldPassword: string, newPassword: string): Promise<User> {\n const user = await this.getUser(userId);\n if (!user) throw new NotFoundException(\"User not found\");\n\n if (!comparePasswordHash(oldPassword, user.passwordHash)) {\n throw new NotFoundException(\"User old password incorrect\");\n }\n\n const { password } = await validateInput({ password: newPassword }, newPasswordSchema);\n const passwordHash = hashPassword(password);\n return await this.update(userId, { passwordHash, needsPasswordChange: false });\n }\n\n async updatePasswordUnsafeByUsername(username: string, newPassword: string): Promise<User> {\n const { password } = await validateInput({ password: newPassword }, newPasswordSchema);\n const passwordHash = hashPassword(password);\n const user = await this.findRawByUsername(username);\n if (!user) throw new NotFoundException(\"User not found\");\n\n return await this.update(user.id, { passwordHash, needsPasswordChange: false });\n }\n\n async updatePasswordHashUnsafeByUsername(username: string, passwordHash: string): Promise<User> {\n const user = await this.findRawByUsername(username);\n if (!user) throw new NotFoundException(\"User not found\");\n\n return await this.update(user.id, { passwordHash, needsPasswordChange: false });\n }\n\n updateUsernameById(userId: number, newUsername: string): Promise<User> {\n return this.update(userId, { username: newUsername });\n }\n\n async register(input: RegisterUserDto): Promise<User> {\n const { username, password, roles, isDemoUser, isRootUser, needsPasswordChange, isVerified } = await validateInput(\n input,\n registerUserSchema,\n );\n\n const passwordHash = hashPassword(password);\n const result = await this.create({\n username,\n passwordHash,\n isVerified: isVerified ?? false,\n isDemoUser: isDemoUser ?? false,\n isRootUser: isRootUser ?? false,\n needsPasswordChange: needsPasswordChange ?? true,\n });\n\n // Convert role names to IDs for database storage\n const roleIds = roles.map((roleName) => this.roleService.getRoleByName(roleName).id);\n await this.userRoleService.setUserRoles(result.id, roleIds);\n\n return this.get(result.id);\n }\n\n private async findUsersByRoleId(roleId: number): Promise<User[]> {\n const userRoles = await this.userRoleService.listByRoleId(roleId);\n const ids = userRoles.map((u) => u.id);\n return this.repository.findBy({ id: In(ids) });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"user.service.js","names":[],"sources":["../../../src/services/orm/user.service.ts"],"sourcesContent":["import { BaseService } from \"@/services/orm/base.service\";\nimport { RegisterUserDto, UserDto } from \"@/services/interfaces/user.dto\";\nimport { User } from \"@/entities\";\nimport type { IUserService } from \"@/services/interfaces/user-service.interface\";\nimport { In } from \"typeorm\";\nimport { InternalServerException, NotFoundException } from \"@/exceptions/runtime.exceptions\";\nimport { validateInput } from \"@/handlers/validators\";\nimport { newPasswordSchema, registerUserSchema } from \"@/services/validators/user-service.validation\";\nimport { comparePasswordHash, hashPassword } from \"@/utils/crypto.utils\";\nimport { TypeormService } from \"@/services/typeorm/typeorm.service\";\nimport { UserRoleService } from \"@/services/orm/user-role.service\";\nimport { ROLES, type RoleName } from \"@/constants/authorization.constants\";\nimport { RoleService } from \"@/services/orm/role.service\";\n\nexport class UserService extends BaseService(User, UserDto) implements IUserService {\n constructor(\n typeormService: TypeormService,\n private readonly userRoleService: UserRoleService,\n private readonly roleService: RoleService,\n ) {\n super(typeormService);\n }\n\n toDto(user: User): UserDto {\n const roleIds = (user.roles ?? []).map((r) => r.roleId);\n return {\n id: user.id,\n createdAt: user.createdAt,\n isVerified: user.isVerified,\n isDemoUser: user.isDemoUser,\n isRootUser: user.isRootUser,\n username: user.username,\n needsPasswordChange: user.needsPasswordChange,\n roles: this.roleService.roleIdsToRoleNames(roleIds),\n };\n }\n\n listUsers(limit?: number): Promise<User[]> {\n return this.list({ take: limit });\n }\n\n async getUser(userId: number) {\n return this.get(userId);\n }\n\n async getUserRoles(userId: number): Promise<RoleName[]> {\n const userRoles = await this.userRoleService.listUserRoles(userId);\n const roleIds = userRoles.map((ur) => ur.roleId);\n return this.roleService.roleIdsToRoleNames(roleIds);\n }\n\n findRootUsers(): Promise<User[]> {\n return this.repository.findBy({ isRootUser: true });\n }\n\n findVerifiedUsers(): Promise<User[]> {\n return this.repository.findBy({ isVerified: true });\n }\n\n async isUserRootUser(userId: number): Promise<boolean> {\n const entity = await this.get(userId);\n return entity.isRootUser;\n }\n\n async deleteUser(userId: number) {\n // Validate\n const user = await this.getUser(userId);\n\n if (user.isRootUser) {\n throw new InternalServerException(\"Cannot delete a root user\");\n }\n\n if (!user.roles?.length) {\n throw new InternalServerException(\"User:roles relation not loaded, cannot perform deletion role check\");\n }\n\n // Check if the user is the last admin\n const role = this.roleService.getRoleByName(ROLES.ADMIN);\n if (user.roles.find((r) => r.roleId == role.id)) {\n const administrators = await this.findUsersByRoleId(role.id);\n if (administrators?.length === 1 && administrators[0].id === userId) {\n throw new InternalServerException(\"Cannot delete the last user with ADMIN role\");\n }\n }\n\n await this.delete(userId);\n }\n\n findRawByUsername(username: string): Promise<User | null> {\n return this.repository.findOneBy({ username });\n }\n\n async getDemoUserId(): Promise<number | undefined> {\n return (await this.repository.findOneBy({ isDemoUser: true }))?.id;\n }\n\n async setIsRootUserById(userId: number, isRootUser: boolean): Promise<void> {\n const user = await this.getUser(userId);\n if (!user) throw new NotFoundException(\"User not found\");\n\n if (!isRootUser) {\n // Ensure at least one user is root user\n const rootUsers = await this.findRootUsers();\n if (rootUsers.length === 1 && rootUsers[0].id === userId) {\n throw new InternalServerException(\"Cannot set the last root user to non-root user\");\n }\n }\n\n await this.update(userId, { isRootUser });\n }\n\n async setUserRoles(userId: number, roles: RoleName[]): Promise<User> {\n const roleIds = roles.map((roleName) => this.roleService.getRoleByName(roleName).id);\n await this.userRoleService.setUserRoles(userId, roleIds);\n return await this.get(userId);\n }\n\n async setVerifiedById(userId: number, isVerified: boolean): Promise<void> {\n const user = await this.getUser(userId);\n if (!user) throw new NotFoundException(\"User not found\");\n\n if (!isVerified) {\n if (user.isRootUser) {\n throw new InternalServerException(\"Cannot set a owner (root user) to unverified\");\n }\n\n // Ensure at least one user is verified\n const verifiedUsers = await this.findVerifiedUsers();\n if (verifiedUsers.length === 1) {\n throw new InternalServerException(\"Cannot set the last user to unverified\");\n }\n }\n\n await this.update(userId, { isVerified });\n }\n\n async updatePasswordById(userId: number, oldPassword: string, newPassword: string): Promise<User> {\n const user = await this.getUser(userId);\n if (!user) throw new NotFoundException(\"User not found\");\n\n if (!comparePasswordHash(oldPassword, user.passwordHash)) {\n throw new NotFoundException(\"User old password incorrect\");\n }\n\n const { password } = await validateInput({ password: newPassword }, newPasswordSchema);\n const passwordHash = hashPassword(password);\n return await this.update(userId, { passwordHash, needsPasswordChange: false });\n }\n\n async updatePasswordUnsafeByUsername(username: string, newPassword: string): Promise<User> {\n const { password } = await validateInput({ password: newPassword }, newPasswordSchema);\n const passwordHash = hashPassword(password);\n const user = await this.findRawByUsername(username);\n if (!user) throw new NotFoundException(\"User not found\");\n\n return await this.update(user.id, { passwordHash, needsPasswordChange: false });\n }\n\n async updatePasswordHashUnsafeByUsername(username: string, passwordHash: string): Promise<User> {\n const user = await this.findRawByUsername(username);\n if (!user) throw new NotFoundException(\"User not found\");\n\n return await this.update(user.id, { passwordHash, needsPasswordChange: false });\n }\n\n updateUsernameById(userId: number, newUsername: string): Promise<User> {\n return this.update(userId, { username: newUsername });\n }\n\n async register(input: RegisterUserDto): Promise<User> {\n const { username, password, roles, isDemoUser, isRootUser, needsPasswordChange, isVerified } = await validateInput(\n input,\n registerUserSchema,\n );\n\n const passwordHash = hashPassword(password);\n const result = await this.create({\n username,\n passwordHash,\n isVerified: isVerified ?? false,\n isDemoUser: isDemoUser ?? false,\n isRootUser: isRootUser ?? false,\n needsPasswordChange: needsPasswordChange ?? true,\n });\n\n // Convert role names to IDs for database storage\n const roleIds = roles.map((roleName) => this.roleService.getRoleByName(roleName).id);\n await this.userRoleService.setUserRoles(result.id, roleIds);\n\n return this.get(result.id);\n }\n\n private async findUsersByRoleId(roleId: number): Promise<User[]> {\n const userRoles = await this.userRoleService.listByRoleId(roleId);\n const ids = userRoles.map((u) => u.id);\n return this.repository.findBy({ id: In(ids) });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"print-file-downloader.service.js","names":[],"sources":["../../src/services/print-file-downloader.service.ts"],"sourcesContent":["import { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport EventEmitter2 from \"eventemitter2\";\nimport { PrintJobService } from \"@/services/orm/print-job.service\";\nimport { FileStorageService } from \"@/services/file-storage.service\";\nimport { FileAnalysisService } from \"@/services/file-analysis.service\";\nimport { PrinterApiFactory } from \"@/services/printer-api.factory\";\nimport { PrinterCache } from \"@/state/printer.cache\";\nimport { captureException } from \"@sentry/node\";\nimport { writeFileSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\n\n/**\n * Service responsible for downloading files from printers for analysis\n * Handles the printJob.needsFileDownload event\n */\nexport class PrintFileDownloaderService {\n logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly eventEmitter2: EventEmitter2,\n private readonly printJobService: PrintJobService,\n private readonly fileStorageService: FileStorageService,\n private readonly fileAnalysisService: FileAnalysisService,\n private readonly printerApiFactory: PrinterApiFactory,\n private readonly printerCache: PrinterCache,\n ) {\n this.logger = loggerFactory(PrintFileDownloaderService.name);\n\n // Register event handler\n this.eventEmitter2.on(\"printJob.needsFileDownload\", (event: { jobId: number }) => {\n this.handleFileDownloadRequest(event.jobId).catch((error) => {\n this.logger.error(`Failed to handle file download for job ${event.jobId}`, error);\n captureException(error);\n });\n });\n\n this.logger.log(\"Print file downloader service initialized\");\n }\n\n private async handleFileDownloadRequest(jobId: number): Promise<void> {\n this.logger.log(`Handling file download request for job ${jobId}`);\n\n try {\n const job = await this.printJobService.getJobByIdOrFail(jobId);\n\n // Check if already has storage ID (race condition protection)\n if (job.fileStorageId) {\n this.logger.log(`Job ${jobId} already has fileStorageId ${job.fileStorageId} - skipping download`);\n return;\n }\n\n if (!job.printerId) {\n this.logger.error(`Job ${jobId} has no printerId - cannot download file`);\n return;\n }\n\n const printer = await this.printerCache.getValue(job.printerId);\n if (!printer) {\n this.logger.error(`Printer ${job.printerId} not found for job ${jobId}`);\n return;\n }\n\n // Get printer API\n const printerApi = this.printerApiFactory.getById(job.printerId);\n\n this.logger.log(`Downloading file ${job.fileName} from printer ${printer.name} (${printer.printerType})`);\n\n // Download file from printer\n let fileBuffer: Buffer;\n try {\n const response = await printerApi.downloadFile(job.fileName);\n\n // Convert stream to buffer\n const chunks: Buffer[] = [];\n const stream = response.data;\n\n await new Promise<void>((resolve, reject) => {\n stream.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n stream.on(\"end\", () => resolve());\n stream.on(\"error\", (err) => reject(err));\n });\n\n fileBuffer = Buffer.concat(chunks);\n } catch (downloadError) {\n this.logger.error(`Failed to download file from printer: ${downloadError}`);\n\n // Mark job as failed analysis\n job.analysisState = \"FAILED\";\n job.statusReason = `File download failed: ${downloadError instanceof Error ? downloadError.message : \"Unknown error\"}`;\n await this.printJobService.printJobRepository.save(job);\n return;\n }\n\n this.logger.log(`Downloaded ${fileBuffer.length} bytes for job ${jobId}`);\n\n // Create temporary file\n const tempPath = join(tmpdir(), `fdm-monster-download-${jobId}-${Date.now()}-${job.fileName}`);\n writeFileSync(tempPath, fileBuffer);\n\n try {\n // Calculate hash\n const fileHash = await this.fileStorageService.calculateFileHash(tempPath);\n this.logger.log(`File hash for job ${jobId}: ${fileHash.substring(0, 12)}...`);\n\n // Check for duplicate\n const existingJob = await this.fileStorageService.findDuplicateByHash(fileHash);\n\n let metadata;\n let fileStorageId: string;\n\n if (existingJob?.fileStorageId) {\n // Found duplicate - reuse existing storage\n const cachedMetadata = await this.fileStorageService.loadMetadata(existingJob.fileStorageId);\n\n if (cachedMetadata) {\n this.logger.log(\n `Duplicate file detected (job ${existingJob.id}, hash match) - reusing storage ${existingJob.fileStorageId}`,\n );\n metadata = {\n ...cachedMetadata,\n fileName: job.fileName,\n };\n fileStorageId = existingJob.fileStorageId;\n } else if (existingJob.analysisState === \"ANALYZED\" && existingJob.metadata) {\n this.logger.log(\n `Duplicate file with DB metadata (job ${existingJob.id}) - reusing storage ${existingJob.fileStorageId}`,\n );\n metadata = {\n ...existingJob.metadata,\n fileName: job.fileName,\n };\n fileStorageId = existingJob.fileStorageId;\n\n // Save metadata JSON for future\n await this.fileStorageService.saveMetadata(fileStorageId, metadata, fileHash, job.fileName);\n } else {\n // Duplicate hash but not analyzed - reuse storage, analyze file\n this.logger.log(\n `Duplicate file not analyzed - reusing storage ${existingJob.fileStorageId}, analyzing now`,\n );\n const existingFilePath = this.fileStorageService.getFilePath(existingJob.fileStorageId);\n const analysisResult = await this.fileAnalysisService.analyzeFile(existingFilePath);\n metadata = analysisResult.metadata;\n fileStorageId = existingJob.fileStorageId;\n await this.fileStorageService.saveMetadata(fileStorageId, metadata, fileHash, job.fileName);\n }\n } else {\n // New file - analyze and store\n this.logger.log(`Analyzing downloaded file: ${job.fileName}`);\n const analysisResult = await this.fileAnalysisService.analyzeFile(tempPath);\n metadata = analysisResult.metadata;\n const thumbnails = analysisResult.thumbnails;\n\n this.logger.log(\n `Analysis complete: format=${metadata.fileFormat}, layers=${metadata.totalLayers}, time=${metadata.gcodePrintTimeSeconds}s, filament=${metadata.filamentUsedGrams}g`,\n );\n\n // Save file to storage (with proper file object simulation)\n const fileObject = {\n path: tempPath,\n originalname: job.fileName,\n mimetype: \"application/octet-stream\",\n size: fileBuffer.length,\n } as Express.Multer.File;\n\n fileStorageId = await this.fileStorageService.saveFile(fileObject, fileHash);\n this.logger.log(`Saved file to storage: ${fileStorageId}`);\n\n let thumbnailMetadata: any[] = [];\n if (thumbnails.length > 0) {\n thumbnailMetadata = await this.fileStorageService.saveThumbnails(fileStorageId, thumbnails);\n this.logger.log(`Saved ${thumbnailMetadata.length} thumbnail(s) for ${fileStorageId}`);\n }\n\n await this.fileStorageService.saveMetadata(\n fileStorageId,\n metadata,\n fileHash,\n job.fileName,\n thumbnailMetadata,\n );\n this.logger.log(`Saved metadata JSON for ${fileStorageId}`);\n }\n\n job.fileStorageId = fileStorageId;\n job.fileHash = fileHash;\n job.fileSize = fileBuffer.length;\n job.fileFormat = metadata.fileFormat;\n job.metadata = metadata;\n job.analysisState = \"ANALYZED\";\n job.analyzedAt = new Date();\n await this.printJobService.printJobRepository.save(job);\n\n this.logger.log(\n `Successfully processed downloaded file for job ${jobId}: storageId=${fileStorageId}, analysisState=${job.analysisState}`,\n );\n } finally {\n try {\n unlinkSync(tempPath);\n } catch (cleanupError) {\n this.logger.warn(`Failed to clean up temp file ${tempPath}: ${cleanupError}`);\n }\n }\n } catch (error) {\n this.logger.error(`Failed to download and analyze file for job ${jobId}`, error);\n captureException(error);\n\n try {\n const job = await this.printJobService.printJobRepository.findOne({ where: { id: jobId } });\n if (job) {\n job.analysisState = \"FAILED\";\n job.statusReason = `File download/analysis failed: ${error instanceof Error ? error.message : \"Unknown error\"}`;\n await this.printJobService.printJobRepository.save(job);\n }\n } catch (updateError) {\n this.logger.error(`Failed to mark job ${jobId} as failed`, updateError);\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"print-file-downloader.service.js","names":[],"sources":["../../src/services/print-file-downloader.service.ts"],"sourcesContent":["import { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport EventEmitter2 from \"eventemitter2\";\nimport { PrintJobService } from \"@/services/orm/print-job.service\";\nimport { FileStorageService } from \"@/services/file-storage.service\";\nimport { FileAnalysisService } from \"@/services/file-analysis.service\";\nimport { PrinterApiFactory } from \"@/services/printer-api.factory\";\nimport { PrinterCache } from \"@/state/printer.cache\";\nimport { captureException } from \"@sentry/node\";\nimport { writeFileSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\n\n/**\n * Service responsible for downloading files from printers for analysis\n * Handles the printJob.needsFileDownload event\n */\nexport class PrintFileDownloaderService {\n logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly eventEmitter2: EventEmitter2,\n private readonly printJobService: PrintJobService,\n private readonly fileStorageService: FileStorageService,\n private readonly fileAnalysisService: FileAnalysisService,\n private readonly printerApiFactory: PrinterApiFactory,\n private readonly printerCache: PrinterCache,\n ) {\n this.logger = loggerFactory(PrintFileDownloaderService.name);\n\n // Register event handler\n this.eventEmitter2.on(\"printJob.needsFileDownload\", (event: { jobId: number }) => {\n this.handleFileDownloadRequest(event.jobId).catch((error) => {\n this.logger.error(`Failed to handle file download for job ${event.jobId}`, error);\n captureException(error);\n });\n });\n\n this.logger.log(\"Print file downloader service initialized\");\n }\n\n private async handleFileDownloadRequest(jobId: number): Promise<void> {\n this.logger.log(`Handling file download request for job ${jobId}`);\n\n try {\n const job = await this.printJobService.getJobByIdOrFail(jobId);\n\n // Check if already has storage ID (race condition protection)\n if (job.fileStorageId) {\n this.logger.log(`Job ${jobId} already has fileStorageId ${job.fileStorageId} - skipping download`);\n return;\n }\n\n if (!job.printerId) {\n this.logger.error(`Job ${jobId} has no printerId - cannot download file`);\n return;\n }\n\n const printer = await this.printerCache.getValue(job.printerId);\n if (!printer) {\n this.logger.error(`Printer ${job.printerId} not found for job ${jobId}`);\n return;\n }\n\n // Get printer API\n const printerApi = this.printerApiFactory.getById(job.printerId);\n\n this.logger.log(`Downloading file ${job.fileName} from printer ${printer.name} (${printer.printerType})`);\n\n // Download file from printer\n let fileBuffer: Buffer;\n try {\n const response = await printerApi.downloadFile(job.fileName);\n\n // Convert stream to buffer\n const chunks: Buffer[] = [];\n const stream = response.data;\n\n await new Promise<void>((resolve, reject) => {\n stream.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n stream.on(\"end\", () => resolve());\n stream.on(\"error\", (err) => reject(err));\n });\n\n fileBuffer = Buffer.concat(chunks);\n } catch (downloadError) {\n this.logger.error(`Failed to download file from printer: ${downloadError}`);\n\n // Mark job as failed analysis\n job.analysisState = \"FAILED\";\n job.statusReason = `File download failed: ${downloadError instanceof Error ? downloadError.message : \"Unknown error\"}`;\n await this.printJobService.printJobRepository.save(job);\n return;\n }\n\n this.logger.log(`Downloaded ${fileBuffer.length} bytes for job ${jobId}`);\n\n // Create temporary file\n const tempPath = join(tmpdir(), `fdm-monster-download-${jobId}-${Date.now()}-${job.fileName}`);\n writeFileSync(tempPath, fileBuffer);\n\n try {\n // Calculate hash\n const fileHash = await this.fileStorageService.calculateFileHash(tempPath);\n this.logger.log(`File hash for job ${jobId}: ${fileHash.substring(0, 12)}...`);\n\n // Check for duplicate\n const existingJob = await this.fileStorageService.findDuplicateByHash(fileHash);\n\n let metadata;\n let fileStorageId: string;\n\n if (existingJob?.fileStorageId) {\n // Found duplicate - reuse existing storage\n const cachedMetadata = await this.fileStorageService.loadMetadata(existingJob.fileStorageId);\n\n if (cachedMetadata) {\n this.logger.log(\n `Duplicate file detected (job ${existingJob.id}, hash match) - reusing storage ${existingJob.fileStorageId}`,\n );\n metadata = {\n ...cachedMetadata,\n fileName: job.fileName,\n };\n fileStorageId = existingJob.fileStorageId;\n } else if (existingJob.analysisState === \"ANALYZED\" && existingJob.metadata) {\n this.logger.log(\n `Duplicate file with DB metadata (job ${existingJob.id}) - reusing storage ${existingJob.fileStorageId}`,\n );\n metadata = {\n ...existingJob.metadata,\n fileName: job.fileName,\n };\n fileStorageId = existingJob.fileStorageId;\n\n // Save metadata JSON for future\n await this.fileStorageService.saveMetadata(fileStorageId, metadata, fileHash, job.fileName);\n } else {\n // Duplicate hash but not analyzed - reuse storage, analyze file\n this.logger.log(\n `Duplicate file not analyzed - reusing storage ${existingJob.fileStorageId}, analyzing now`,\n );\n const existingFilePath = this.fileStorageService.getFilePath(existingJob.fileStorageId);\n const analysisResult = await this.fileAnalysisService.analyzeFile(existingFilePath);\n metadata = analysisResult.metadata;\n fileStorageId = existingJob.fileStorageId;\n await this.fileStorageService.saveMetadata(fileStorageId, metadata, fileHash, job.fileName);\n }\n } else {\n // New file - analyze and store\n this.logger.log(`Analyzing downloaded file: ${job.fileName}`);\n const analysisResult = await this.fileAnalysisService.analyzeFile(tempPath);\n metadata = analysisResult.metadata;\n const thumbnails = analysisResult.thumbnails;\n\n this.logger.log(\n `Analysis complete: format=${metadata.fileFormat}, layers=${metadata.totalLayers}, time=${metadata.gcodePrintTimeSeconds}s, filament=${metadata.filamentUsedGrams}g`,\n );\n\n // Save file to storage (with proper file object simulation)\n const fileObject = {\n path: tempPath,\n originalname: job.fileName,\n mimetype: \"application/octet-stream\",\n size: fileBuffer.length,\n } as Express.Multer.File;\n\n fileStorageId = await this.fileStorageService.saveFile(fileObject, fileHash);\n this.logger.log(`Saved file to storage: ${fileStorageId}`);\n\n let thumbnailMetadata: any[] = [];\n if (thumbnails.length > 0) {\n thumbnailMetadata = await this.fileStorageService.saveThumbnails(fileStorageId, thumbnails);\n this.logger.log(`Saved ${thumbnailMetadata.length} thumbnail(s) for ${fileStorageId}`);\n }\n\n await this.fileStorageService.saveMetadata(\n fileStorageId,\n metadata,\n fileHash,\n job.fileName,\n thumbnailMetadata,\n );\n this.logger.log(`Saved metadata JSON for ${fileStorageId}`);\n }\n\n job.fileStorageId = fileStorageId;\n job.fileHash = fileHash;\n job.fileSize = fileBuffer.length;\n job.fileFormat = metadata.fileFormat;\n job.metadata = metadata;\n job.analysisState = \"ANALYZED\";\n job.analyzedAt = new Date();\n await this.printJobService.printJobRepository.save(job);\n\n this.logger.log(\n `Successfully processed downloaded file for job ${jobId}: storageId=${fileStorageId}, analysisState=${job.analysisState}`,\n );\n } finally {\n try {\n unlinkSync(tempPath);\n } catch (cleanupError) {\n this.logger.warn(`Failed to clean up temp file ${tempPath}: ${cleanupError}`);\n }\n }\n } catch (error) {\n this.logger.error(`Failed to download and analyze file for job ${jobId}`, error);\n captureException(error);\n\n try {\n const job = await this.printJobService.printJobRepository.findOne({ where: { id: jobId } });\n if (job) {\n job.analysisState = \"FAILED\";\n job.statusReason = `File download/analysis failed: ${error instanceof Error ? error.message : \"Unknown error\"}`;\n await this.printJobService.printJobRepository.save(job);\n }\n } catch (updateError) {\n this.logger.error(`Failed to mark job ${jobId} as failed`, updateError);\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"print-queue.service.js","names":[],"sources":["../../src/services/print-queue.service.ts"],"sourcesContent":["import { Repository, Not, IsNull } from \"typeorm\";\nimport { PrintJob } from \"@/entities/print-job.entity\";\nimport { Printer } from \"@/entities/printer.entity\";\nimport EventEmitter2 from \"eventemitter2\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { TypeormService } from \"@/services/typeorm/typeorm.service\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { PrinterApiFactory } from \"@/services/printer-api.factory\";\nimport { FileStorageService } from \"@/services/file-storage.service\";\nimport { PrinterSocketStore } from \"@/state/printer-socket.store\";\nimport { SOCKET_STATE } from \"@/shared/dtos/socket-state.type\";\nimport { API_STATE } from \"@/shared/dtos/api-state.type\";\nimport { captureException } from \"@sentry/node\";\n\nexport interface QueuedJob {\n id: number;\n fileName: string;\n queuePosition: number;\n status: string;\n estimatedTimeSeconds?: number;\n filamentGrams?: number;\n createdAt: Date;\n}\n\nexport interface IPrintQueueService {\n addToQueue(printerId: number, jobId: number, position?: number): Promise<void>;\n\n removeFromQueue(jobId: number): Promise<void>;\n\n getQueue(printerId: number): Promise<QueuedJob[]>;\n\n getGlobalQueuePaged(page: number, pageSize: number): Promise<[PrintJob[], number]>;\n\n getNextInQueue(printerId: number): Promise<PrintJob | null>;\n\n reorderQueue(printerId: number, jobIds: number[]): Promise<void>;\n\n clearQueue(printerId: number): Promise<void>;\n\n processQueue(printerId: number): Promise<PrintJob | null>;\n}\n\n/**\n * Simplified service for managing print job queues per printer\n */\nexport class PrintQueueService implements IPrintQueueService {\n printJobRepository: Repository<PrintJob>;\n printerRepository: Repository<Printer>;\n eventEmitter2: EventEmitter2;\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n typeormService: TypeormService,\n eventEmitter2: EventEmitter2,\n private readonly printerApiFactory: PrinterApiFactory,\n private readonly fileStorageService: FileStorageService,\n private readonly printerSocketStore: PrinterSocketStore,\n ) {\n this.printJobRepository = typeormService.getDataSource().getRepository(PrintJob);\n this.printerRepository = typeormService.getDataSource().getRepository(Printer);\n this.eventEmitter2 = eventEmitter2;\n this.logger = loggerFactory(PrintQueueService.name);\n\n this.eventEmitter2.on(\n \"printQueue.jobSubmitted\",\n (event: {\n printerId: number;\n jobId: number;\n fileName: string;\n fileStorageId?: string;\n queuePosition?: number | null;\n }) => {\n this.handleJobSubmission(\n event.printerId,\n event.jobId,\n event.fileName,\n event.fileStorageId,\n event.queuePosition,\n ).catch((error) => {\n this.logger.error(`Failed to handle job submission for job ${event.jobId}`, error);\n captureException(error);\n });\n },\n );\n }\n\n private isPrinterConnected(printerId: number): { connected: boolean; reason?: string } {\n const socket = this.printerSocketStore.getPrinterSocket(printerId);\n\n if (!socket) {\n return { connected: false, reason: \"No socket connection found\" };\n }\n\n const socketState = socket.socketState;\n const apiState = socket.apiState;\n\n if (socketState !== SOCKET_STATE.opened && socketState !== SOCKET_STATE.authenticated) {\n return { connected: false, reason: `Socket not connected (state: ${socketState})` };\n }\n\n if (apiState !== API_STATE.responding) {\n return { connected: false, reason: `Printer not responding (API state: ${apiState})` };\n }\n\n return { connected: true };\n }\n\n async addToQueue(printerId: number, jobId: number, position?: number): Promise<void> {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n if (!job) {\n throw new Error(`Print job ${jobId} not found`);\n }\n\n this.ensurePrinterAssignment(job, printerId);\n\n if (position === undefined || position === null) {\n const maxPosition = await this.getMaxQueuePosition(printerId);\n job.queuePosition = (maxPosition ?? -1) + 1;\n } else {\n await this.shiftQueuePositions(printerId, position);\n job.queuePosition = position;\n }\n\n job.status = \"QUEUED\";\n await this.printJobRepository.save(job);\n\n this.logger.log(`Added job ${jobId} to printer ${printerId} queue at position ${job.queuePosition}`);\n this.eventEmitter2.emit(\"printQueue.jobAdded\", {\n printerId,\n jobId,\n position: job.queuePosition,\n });\n }\n\n async removeFromQueue(jobId: number): Promise<void> {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n if (!job) {\n throw new Error(`Print job ${jobId} not found`);\n }\n\n const printerId = job.printerId;\n const oldPosition = job.queuePosition;\n\n job.queuePosition = null;\n if (job.status === \"QUEUED\") {\n job.status = \"PENDING\";\n }\n await this.printJobRepository.save(job);\n\n if (oldPosition !== null && printerId) {\n await this.compactQueuePositions(printerId, oldPosition);\n }\n\n this.logger.log(`Removed job ${jobId} from queue`);\n this.eventEmitter2.emit(\"printQueue.jobRemoved\", {\n printerId,\n jobId,\n });\n }\n\n async getQueue(printerId: number): Promise<QueuedJob[]> {\n const jobs = await this.printJobRepository.find({\n where: {\n printerId,\n status: \"QUEUED\",\n queuePosition: Not(IsNull()),\n },\n order: { queuePosition: \"ASC\" },\n });\n\n return jobs.map((j) => ({\n id: j.id,\n fileName: j.fileName,\n queuePosition: j.queuePosition!,\n status: j.status,\n estimatedTimeSeconds: (j.metadata as any)?.gcodePrintTimeSeconds,\n filamentGrams: (j.metadata as any)?.filamentUsedGrams,\n createdAt: j.createdAt,\n }));\n }\n\n async getGlobalQueuePaged(page: number, pageSize: number): Promise<[PrintJob[], number]> {\n const skip = (page - 1) * pageSize;\n\n return await this.printJobRepository.findAndCount({\n where: { status: \"QUEUED\" },\n order: {\n printerId: \"ASC\",\n queuePosition: \"ASC\",\n },\n relations: [\"printer\"],\n take: pageSize,\n skip: skip,\n });\n }\n\n async getNextInQueue(printerId: number): Promise<PrintJob | null> {\n return this.printJobRepository.findOne({\n where: {\n printerId,\n status: \"QUEUED\",\n queuePosition: Not(IsNull()),\n },\n order: { queuePosition: \"ASC\" },\n });\n }\n\n async reorderQueue(printerId: number, jobIds: number[]): Promise<void> {\n for (let i = 0; i < jobIds.length; i++) {\n const job = await this.printJobRepository.findOne({ where: { id: jobIds[i] } });\n if (job?.printerId === printerId) {\n job.queuePosition = i;\n await this.printJobRepository.save(job);\n }\n }\n\n this.logger.log(`Reordered queue for printer ${printerId}`);\n this.eventEmitter2.emit(\"printQueue.reordered\", { printerId });\n }\n\n async clearQueue(printerId: number): Promise<void> {\n const jobs = await this.printJobRepository.find({\n where: {\n printerId,\n status: \"QUEUED\",\n },\n });\n\n for (const job of jobs) {\n job.status = \"PENDING\";\n job.queuePosition = null;\n await this.printJobRepository.save(job);\n }\n\n this.logger.log(`Cleared queue for printer ${printerId} (${jobs.length} jobs)`);\n this.eventEmitter2.emit(\"printQueue.cleared\", { printerId });\n }\n\n async processQueue(printerId: number): Promise<PrintJob | null> {\n const nextJob = await this.getNextInQueue(printerId);\n\n if (!nextJob) {\n this.logger.log(`No jobs in queue for printer ${printerId}`);\n return null;\n }\n\n this.logger.log(`Processing queue: next job is ${nextJob.id} (${nextJob.fileName})`);\n\n this.eventEmitter2.emit(\"printQueue.processNext\", {\n printerId,\n jobId: nextJob.id,\n fileName: nextJob.fileName,\n fileStorageId: nextJob.fileStorageId,\n });\n\n return nextJob;\n }\n\n private ensurePrinterAssignment(job: PrintJob, printerId: number): void {\n if (!job.printerId) {\n job.printerId = printerId;\n } else if (job.printerId !== printerId) {\n throw new Error(`Job ${job.id} belongs to printer ${job.printerId}, cannot submit to printer ${printerId}`);\n }\n }\n\n private async getMaxQueuePosition(printerId: number): Promise<number | null> {\n const result = await this.printJobRepository\n .createQueryBuilder(\"job\")\n .select(\"MAX(job.queuePosition)\", \"max\")\n .where(\"job.printerId = :printerId\", { printerId })\n .getRawOne();\n\n return result?.max ?? null;\n }\n\n private async shiftQueuePositions(printerId: number, fromPosition: number): Promise<void> {\n await this.printJobRepository\n .createQueryBuilder()\n .update(PrintJob)\n .set({ queuePosition: () => \"queuePosition + 1\" })\n .where(\"printerId = :printerId\", { printerId })\n .andWhere(\"queuePosition >= :fromPosition\", { fromPosition })\n .execute();\n }\n\n private async compactQueuePositions(printerId: number, removedPosition: number): Promise<void> {\n await this.printJobRepository\n .createQueryBuilder()\n .update(PrintJob)\n .set({ queuePosition: () => \"queuePosition - 1\" })\n .where(\"printerId = :printerId\", { printerId })\n .andWhere(\"queuePosition > :removedPosition\", { removedPosition })\n .execute();\n }\n\n async submitToPrinter(printerId: number, jobId: number): Promise<void> {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n\n if (!job) {\n throw new Error(`Print job ${jobId} not found`);\n }\n\n this.ensurePrinterAssignment(job, printerId);\n\n const queuePosition = job.queuePosition;\n if (job.queuePosition !== null) {\n const oldPosition = job.queuePosition;\n job.queuePosition = null;\n await this.compactQueuePositions(printerId, oldPosition);\n }\n\n job.status = \"PRINTING\";\n job.startedAt = new Date();\n await this.printJobRepository.save(job);\n\n this.logger.log(`Submitting job ${jobId} (${job.fileName}) to printer ${printerId}`);\n this.eventEmitter2.emit(\"printQueue.jobSubmitted\", {\n printerId,\n jobId: job.id,\n fileName: job.fileName,\n fileStorageId: job.fileStorageId,\n queuePosition,\n });\n }\n\n private async handleJobSubmission(\n printerId: number,\n jobId: number,\n fileName: string,\n fileStorageId?: string,\n queuePosition?: number | null,\n ): Promise<void> {\n this.logger.log(`Handling job submission for job ${jobId} on printer ${printerId}`);\n\n try {\n if (!fileStorageId) {\n throw new Error(`Job ${jobId} has no fileStorageId - cannot submit to printer`);\n }\n const printerApi = this.printerApiFactory.getById(printerId);\n\n const fileSize = this.fileStorageService.getFileSize(fileStorageId);\n const fileStream = this.fileStorageService.readFileStream(fileStorageId);\n\n this.logger.log(`Uploading file ${fileName} to printer ${printerId} and starting print`);\n await printerApi.uploadFile({\n stream: fileStream,\n fileName,\n contentLength: fileSize,\n startPrint: true,\n });\n this.logger.log(`Successfully submitted job ${jobId} to printer ${printerId}`);\n\n if (queuePosition !== null && queuePosition !== undefined) {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n if (job?.queuePosition === queuePosition) {\n job.queuePosition = null;\n await this.printJobRepository.save(job);\n await this.compactQueuePositions(printerId, queuePosition);\n this.logger.log(`Removed job ${jobId} from queue after successful submission`);\n }\n }\n } catch (error) {\n this.logger.error(`Failed to submit job ${jobId} to printer ${printerId}`, error);\n\n try {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n if (job) {\n job.status = \"FAILED\";\n job.statusReason = `Print submission failed: ${error instanceof Error ? error.message : \"Unknown error\"}`;\n job.endedAt = new Date();\n await this.printJobRepository.save(job);\n this.logger.log(`Updated job ${jobId} status to FAILED (still in queue for retry)`);\n }\n } catch (updateError) {\n this.logger.error(`Failed to update job ${jobId} status after submission error`, updateError);\n }\n\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"print-queue.service.js","names":[],"sources":["../../src/services/print-queue.service.ts"],"sourcesContent":["import { Repository, Not, IsNull } from \"typeorm\";\nimport { PrintJob } from \"@/entities/print-job.entity\";\nimport { Printer } from \"@/entities/printer.entity\";\nimport EventEmitter2 from \"eventemitter2\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport { TypeormService } from \"@/services/typeorm/typeorm.service\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport { PrinterApiFactory } from \"@/services/printer-api.factory\";\nimport { FileStorageService } from \"@/services/file-storage.service\";\nimport { PrinterSocketStore } from \"@/state/printer-socket.store\";\nimport { SOCKET_STATE } from \"@/shared/dtos/socket-state.type\";\nimport { API_STATE } from \"@/shared/dtos/api-state.type\";\nimport { captureException } from \"@sentry/node\";\n\nexport interface QueuedJob {\n id: number;\n fileName: string;\n queuePosition: number;\n status: string;\n estimatedTimeSeconds?: number;\n filamentGrams?: number;\n createdAt: Date;\n}\n\nexport interface IPrintQueueService {\n addToQueue(printerId: number, jobId: number, position?: number): Promise<void>;\n\n removeFromQueue(jobId: number): Promise<void>;\n\n getQueue(printerId: number): Promise<QueuedJob[]>;\n\n getGlobalQueuePaged(page: number, pageSize: number): Promise<[PrintJob[], number]>;\n\n getNextInQueue(printerId: number): Promise<PrintJob | null>;\n\n reorderQueue(printerId: number, jobIds: number[]): Promise<void>;\n\n clearQueue(printerId: number): Promise<void>;\n\n processQueue(printerId: number): Promise<PrintJob | null>;\n}\n\n/**\n * Simplified service for managing print job queues per printer\n */\nexport class PrintQueueService implements IPrintQueueService {\n printJobRepository: Repository<PrintJob>;\n printerRepository: Repository<Printer>;\n eventEmitter2: EventEmitter2;\n private readonly logger: LoggerService;\n\n constructor(\n loggerFactory: ILoggerFactory,\n typeormService: TypeormService,\n eventEmitter2: EventEmitter2,\n private readonly printerApiFactory: PrinterApiFactory,\n private readonly fileStorageService: FileStorageService,\n private readonly printerSocketStore: PrinterSocketStore,\n ) {\n this.printJobRepository = typeormService.getDataSource().getRepository(PrintJob);\n this.printerRepository = typeormService.getDataSource().getRepository(Printer);\n this.eventEmitter2 = eventEmitter2;\n this.logger = loggerFactory(PrintQueueService.name);\n\n this.eventEmitter2.on(\n \"printQueue.jobSubmitted\",\n (event: {\n printerId: number;\n jobId: number;\n fileName: string;\n fileStorageId?: string;\n queuePosition?: number | null;\n }) => {\n this.handleJobSubmission(\n event.printerId,\n event.jobId,\n event.fileName,\n event.fileStorageId,\n event.queuePosition,\n ).catch((error) => {\n this.logger.error(`Failed to handle job submission for job ${event.jobId}`, error);\n captureException(error);\n });\n },\n );\n }\n\n private isPrinterConnected(printerId: number): { connected: boolean; reason?: string } {\n const socket = this.printerSocketStore.getPrinterSocket(printerId);\n\n if (!socket) {\n return { connected: false, reason: \"No socket connection found\" };\n }\n\n const socketState = socket.socketState;\n const apiState = socket.apiState;\n\n if (socketState !== SOCKET_STATE.opened && socketState !== SOCKET_STATE.authenticated) {\n return { connected: false, reason: `Socket not connected (state: ${socketState})` };\n }\n\n if (apiState !== API_STATE.responding) {\n return { connected: false, reason: `Printer not responding (API state: ${apiState})` };\n }\n\n return { connected: true };\n }\n\n async addToQueue(printerId: number, jobId: number, position?: number): Promise<void> {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n if (!job) {\n throw new Error(`Print job ${jobId} not found`);\n }\n\n this.ensurePrinterAssignment(job, printerId);\n\n if (position === undefined || position === null) {\n const maxPosition = await this.getMaxQueuePosition(printerId);\n job.queuePosition = (maxPosition ?? -1) + 1;\n } else {\n await this.shiftQueuePositions(printerId, position);\n job.queuePosition = position;\n }\n\n job.status = \"QUEUED\";\n await this.printJobRepository.save(job);\n\n this.logger.log(`Added job ${jobId} to printer ${printerId} queue at position ${job.queuePosition}`);\n this.eventEmitter2.emit(\"printQueue.jobAdded\", {\n printerId,\n jobId,\n position: job.queuePosition,\n });\n }\n\n async removeFromQueue(jobId: number): Promise<void> {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n if (!job) {\n throw new Error(`Print job ${jobId} not found`);\n }\n\n const printerId = job.printerId;\n const oldPosition = job.queuePosition;\n\n job.queuePosition = null;\n if (job.status === \"QUEUED\") {\n job.status = \"PENDING\";\n }\n await this.printJobRepository.save(job);\n\n if (oldPosition !== null && printerId) {\n await this.compactQueuePositions(printerId, oldPosition);\n }\n\n this.logger.log(`Removed job ${jobId} from queue`);\n this.eventEmitter2.emit(\"printQueue.jobRemoved\", {\n printerId,\n jobId,\n });\n }\n\n async getQueue(printerId: number): Promise<QueuedJob[]> {\n const jobs = await this.printJobRepository.find({\n where: {\n printerId,\n status: \"QUEUED\",\n queuePosition: Not(IsNull()),\n },\n order: { queuePosition: \"ASC\" },\n });\n\n return jobs.map((j) => ({\n id: j.id,\n fileName: j.fileName,\n queuePosition: j.queuePosition!,\n status: j.status,\n estimatedTimeSeconds: (j.metadata as any)?.gcodePrintTimeSeconds,\n filamentGrams: (j.metadata as any)?.filamentUsedGrams,\n createdAt: j.createdAt,\n }));\n }\n\n async getGlobalQueuePaged(page: number, pageSize: number): Promise<[PrintJob[], number]> {\n const skip = (page - 1) * pageSize;\n\n return await this.printJobRepository.findAndCount({\n where: { status: \"QUEUED\" },\n order: {\n printerId: \"ASC\",\n queuePosition: \"ASC\",\n },\n relations: [\"printer\"],\n take: pageSize,\n skip: skip,\n });\n }\n\n async getNextInQueue(printerId: number): Promise<PrintJob | null> {\n return this.printJobRepository.findOne({\n where: {\n printerId,\n status: \"QUEUED\",\n queuePosition: Not(IsNull()),\n },\n order: { queuePosition: \"ASC\" },\n });\n }\n\n async reorderQueue(printerId: number, jobIds: number[]): Promise<void> {\n for (let i = 0; i < jobIds.length; i++) {\n const job = await this.printJobRepository.findOne({ where: { id: jobIds[i] } });\n if (job?.printerId === printerId) {\n job.queuePosition = i;\n await this.printJobRepository.save(job);\n }\n }\n\n this.logger.log(`Reordered queue for printer ${printerId}`);\n this.eventEmitter2.emit(\"printQueue.reordered\", { printerId });\n }\n\n async clearQueue(printerId: number): Promise<void> {\n const jobs = await this.printJobRepository.find({\n where: {\n printerId,\n status: \"QUEUED\",\n },\n });\n\n for (const job of jobs) {\n job.status = \"PENDING\";\n job.queuePosition = null;\n await this.printJobRepository.save(job);\n }\n\n this.logger.log(`Cleared queue for printer ${printerId} (${jobs.length} jobs)`);\n this.eventEmitter2.emit(\"printQueue.cleared\", { printerId });\n }\n\n async processQueue(printerId: number): Promise<PrintJob | null> {\n const nextJob = await this.getNextInQueue(printerId);\n\n if (!nextJob) {\n this.logger.log(`No jobs in queue for printer ${printerId}`);\n return null;\n }\n\n this.logger.log(`Processing queue: next job is ${nextJob.id} (${nextJob.fileName})`);\n\n this.eventEmitter2.emit(\"printQueue.processNext\", {\n printerId,\n jobId: nextJob.id,\n fileName: nextJob.fileName,\n fileStorageId: nextJob.fileStorageId,\n });\n\n return nextJob;\n }\n\n private ensurePrinterAssignment(job: PrintJob, printerId: number): void {\n if (!job.printerId) {\n job.printerId = printerId;\n } else if (job.printerId !== printerId) {\n throw new Error(`Job ${job.id} belongs to printer ${job.printerId}, cannot submit to printer ${printerId}`);\n }\n }\n\n private async getMaxQueuePosition(printerId: number): Promise<number | null> {\n const result = await this.printJobRepository\n .createQueryBuilder(\"job\")\n .select(\"MAX(job.queuePosition)\", \"max\")\n .where(\"job.printerId = :printerId\", { printerId })\n .getRawOne();\n\n return result?.max ?? null;\n }\n\n private async shiftQueuePositions(printerId: number, fromPosition: number): Promise<void> {\n await this.printJobRepository\n .createQueryBuilder()\n .update(PrintJob)\n .set({ queuePosition: () => \"queuePosition + 1\" })\n .where(\"printerId = :printerId\", { printerId })\n .andWhere(\"queuePosition >= :fromPosition\", { fromPosition })\n .execute();\n }\n\n private async compactQueuePositions(printerId: number, removedPosition: number): Promise<void> {\n await this.printJobRepository\n .createQueryBuilder()\n .update(PrintJob)\n .set({ queuePosition: () => \"queuePosition - 1\" })\n .where(\"printerId = :printerId\", { printerId })\n .andWhere(\"queuePosition > :removedPosition\", { removedPosition })\n .execute();\n }\n\n async submitToPrinter(printerId: number, jobId: number): Promise<void> {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n\n if (!job) {\n throw new Error(`Print job ${jobId} not found`);\n }\n\n this.ensurePrinterAssignment(job, printerId);\n\n const queuePosition = job.queuePosition;\n if (job.queuePosition !== null) {\n const oldPosition = job.queuePosition;\n job.queuePosition = null;\n await this.compactQueuePositions(printerId, oldPosition);\n }\n\n job.status = \"PRINTING\";\n job.startedAt = new Date();\n await this.printJobRepository.save(job);\n\n this.logger.log(`Submitting job ${jobId} (${job.fileName}) to printer ${printerId}`);\n this.eventEmitter2.emit(\"printQueue.jobSubmitted\", {\n printerId,\n jobId: job.id,\n fileName: job.fileName,\n fileStorageId: job.fileStorageId,\n queuePosition,\n });\n }\n\n private async handleJobSubmission(\n printerId: number,\n jobId: number,\n fileName: string,\n fileStorageId?: string,\n queuePosition?: number | null,\n ): Promise<void> {\n this.logger.log(`Handling job submission for job ${jobId} on printer ${printerId}`);\n\n try {\n if (!fileStorageId) {\n throw new Error(`Job ${jobId} has no fileStorageId - cannot submit to printer`);\n }\n const printerApi = this.printerApiFactory.getById(printerId);\n\n const fileSize = this.fileStorageService.getFileSize(fileStorageId);\n const fileStream = this.fileStorageService.readFileStream(fileStorageId);\n\n this.logger.log(`Uploading file ${fileName} to printer ${printerId} and starting print`);\n await printerApi.uploadFile({\n stream: fileStream,\n fileName,\n contentLength: fileSize,\n startPrint: true,\n });\n this.logger.log(`Successfully submitted job ${jobId} to printer ${printerId}`);\n\n if (queuePosition !== null && queuePosition !== undefined) {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n if (job?.queuePosition === queuePosition) {\n job.queuePosition = null;\n await this.printJobRepository.save(job);\n await this.compactQueuePositions(printerId, queuePosition);\n this.logger.log(`Removed job ${jobId} from queue after successful submission`);\n }\n }\n } catch (error) {\n this.logger.error(`Failed to submit job ${jobId} to printer ${printerId}`, error);\n\n try {\n const job = await this.printJobRepository.findOne({ where: { id: jobId } });\n if (job) {\n job.status = \"FAILED\";\n job.statusReason = `Print submission failed: ${error instanceof Error ? error.message : \"Unknown error\"}`;\n job.endedAt = new Date();\n await this.printJobRepository.save(job);\n this.logger.log(`Updated job ${jobId} status to FAILED (still in queue for retry)`);\n }\n } catch (updateError) {\n this.logger.error(`Failed to update job ${jobId} status after submission error`, updateError);\n }\n\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"printer-api.factory.js","names":[],"sources":["../../src/services/printer-api.factory.ts"],"sourcesContent":["import {\n MoonrakerType,\n OctoprintType,\n PrusaLinkType,\n BambuType,\n type IPrinterApi,\n} from \"@/services/printer-api.interface\";\nimport { DITokens } from \"@/container.tokens\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport { PrinterCache } from \"@/state/printer.cache\";\nimport { CradleService } from \"./core/cradle.service\";\n\nexport class PrinterApiFactory {\n constructor(private readonly cradleService: CradleService) {}\n\n getById(id: number): IPrinterApi {\n const printerCache = this.cradleService.resolve<PrinterCache>(DITokens.printerCache);\n const login = printerCache.getLoginDto(id);\n const printerApi = this.getScopedPrinter(login);\n\n // For BambuApi, set the printer ID so it can access the MQTT adapter from PrinterSocketStore\n if (login.printerType === BambuType && \"setPrinterId\" in printerApi) {\n (printerApi as any).setPrinterId(id);\n }\n\n return printerApi;\n }\n\n getScopedPrinter(login: LoginDto): IPrinterApi {\n let printerApi;\n if (login.printerType === OctoprintType) {\n printerApi = this.cradleService.resolve<IPrinterApi>(DITokens.octoprintApi);\n } else if (login.printerType === MoonrakerType) {\n printerApi = this.cradleService.resolve<IPrinterApi>(DITokens.moonrakerApi);\n } else if (login.printerType === PrusaLinkType) {\n printerApi = this.cradleService.resolve<IPrinterApi>(DITokens.prusaLinkApi);\n } else if (login.printerType === BambuType) {\n printerApi = this.cradleService.resolve<IPrinterApi>(DITokens.bambuApi);\n } else {\n throw new Error(\"PrinterType is unknown, cant pick the right socket adapter\");\n }\n\n printerApi.login = login;\n return printerApi;\n }\n}\n"],"mappings":";;;AAYA,IAAa,oBAAb,MAA+B;CAC7B,YAAY,eAA+C;
|
|
1
|
+
{"version":3,"file":"printer-api.factory.js","names":[],"sources":["../../src/services/printer-api.factory.ts"],"sourcesContent":["import {\n MoonrakerType,\n OctoprintType,\n PrusaLinkType,\n BambuType,\n type IPrinterApi,\n} from \"@/services/printer-api.interface\";\nimport { DITokens } from \"@/container.tokens\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport { PrinterCache } from \"@/state/printer.cache\";\nimport { CradleService } from \"./core/cradle.service\";\n\nexport class PrinterApiFactory {\n constructor(private readonly cradleService: CradleService) {}\n\n getById(id: number): IPrinterApi {\n const printerCache = this.cradleService.resolve<PrinterCache>(DITokens.printerCache);\n const login = printerCache.getLoginDto(id);\n const printerApi = this.getScopedPrinter(login);\n\n // For BambuApi, set the printer ID so it can access the MQTT adapter from PrinterSocketStore\n if (login.printerType === BambuType && \"setPrinterId\" in printerApi) {\n (printerApi as any).setPrinterId(id);\n }\n\n return printerApi;\n }\n\n getScopedPrinter(login: LoginDto): IPrinterApi {\n let printerApi;\n if (login.printerType === OctoprintType) {\n printerApi = this.cradleService.resolve<IPrinterApi>(DITokens.octoprintApi);\n } else if (login.printerType === MoonrakerType) {\n printerApi = this.cradleService.resolve<IPrinterApi>(DITokens.moonrakerApi);\n } else if (login.printerType === PrusaLinkType) {\n printerApi = this.cradleService.resolve<IPrinterApi>(DITokens.prusaLinkApi);\n } else if (login.printerType === BambuType) {\n printerApi = this.cradleService.resolve<IPrinterApi>(DITokens.bambuApi);\n } else {\n throw new Error(\"PrinterType is unknown, cant pick the right socket adapter\");\n }\n\n printerApi.login = login;\n return printerApi;\n }\n}\n"],"mappings":";;;AAYA,IAAa,oBAAb,MAA+B;CAC7B,YAAY,eAA+C;EAA9B,KAAA,gBAAA;;CAE7B,QAAQ,IAAyB;EAE/B,MAAM,QADe,KAAK,cAAc,QAAsB,SAAS,aAC7C,CAAC,YAAY,GAAG;EAC1C,MAAM,aAAa,KAAK,iBAAiB,MAAM;EAG/C,IAAI,MAAM,gBAAA,KAA6B,kBAAkB,YACvD,WAAoB,aAAa,GAAG;EAGtC,OAAO;;CAGT,iBAAiB,OAA8B;EAC7C,IAAI;EACJ,IAAI,MAAM,gBAAA,GACR,aAAa,KAAK,cAAc,QAAqB,SAAS,aAAa;OACtE,IAAI,MAAM,gBAAA,GACf,aAAa,KAAK,cAAc,QAAqB,SAAS,aAAa;OACtE,IAAI,MAAM,gBAAA,GACf,aAAa,KAAK,cAAc,QAAqB,SAAS,aAAa;OACtE,IAAI,MAAM,gBAAA,GACf,aAAa,KAAK,cAAc,QAAqB,SAAS,SAAS;OAEvE,MAAM,IAAI,MAAM,6DAA6D;EAG/E,WAAW,QAAQ;EACnB,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"printer-api.interface.js","names":[],"sources":["../../src/services/printer-api.interface.ts"],"sourcesContent":["import { AxiosPromise } from \"axios\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport type { ServerConfigDto } from \"@/services/moonraker/dto/server/server-config.dto\";\nimport type { SettingsDto } from \"@/services/octoprint/dto/settings/settings.dto\";\nimport { ConnectionState } from \"@/services/octoprint/dto/connection/connection-state.type\";\nimport { Flags } from \"@/services/moonraker/dto/octoprint-compat/api-printer.dto\";\nimport { z } from \"zod\";\nimport { Readable } from \"node:stream\";\n\nexport const uploadFileInputSchema = z.object({\n stream: z.custom<Readable>((val) => {\n return val && typeof val === \"object\" && \"pipe\" in val && typeof val.pipe === \"function\";\n }, \"Must be a readable stream\"),\n fileName: z.string().min(1),\n contentLength: z.number().int().positive(),\n startPrint: z.boolean(),\n uploadToken: z.string().optional(),\n});\n\nexport type UploadFileInput = z.infer<typeof uploadFileInputSchema>;\n\nexport const OctoprintType = 0;\nexport const MoonrakerType = 1;\nexport const PrusaLinkType = 2;\nexport const BambuType = 3;\n\nexport enum PrinterTypesEnum {\n Octoprint = 0,\n Moonraker = 1,\n PrusaLink = 2,\n Bambu = 3,\n}\n\nexport type PrinterType = typeof OctoprintType | typeof MoonrakerType | typeof PrusaLinkType | typeof BambuType;\n\nexport interface FdmCurrentMessageDto {\n progress: {\n printTime: number | null;\n completion: number | null;\n };\n state: {\n text: string;\n error: string;\n flags: Flags;\n };\n job: {\n file: {\n name: string;\n path: string;\n };\n };\n}\n\nexport interface StatusFlags {\n connected: boolean;\n operational: boolean;\n printing: boolean;\n paused: boolean;\n error: boolean;\n finished: boolean;\n}\n\nexport interface FileDto {\n path: string;\n size: number | null;\n date: number | null;\n dir: boolean;\n}\n\nexport interface FilesDto {\n dirs: FileDto[];\n files: FileDto[];\n}\n\nexport enum ReprintState {\n PrinterNotAvailable = 0,\n NoLastPrint = 1,\n LastPrintReady = 2,\n}\n\nexport interface PartialReprintFileDto {\n file?: FileDto;\n reprintState: ReprintState;\n connectionState: ConnectionState | null;\n}\n\nexport interface ReprintFileDto extends PartialReprintFileDto {\n printerId: number;\n}\n\nexport interface IPrinterApi {\n get type(): PrinterType;\n\n set login(login: LoginDto);\n\n getVersion(): Promise<string>;\n\n validateConnection(): Promise<void>;\n\n connect(): Promise<void>;\n\n disconnect(): Promise<void>;\n\n restartServer(): Promise<void>;\n\n restartHost(): Promise<void>;\n\n restartPrinterFirmware(): Promise<void>;\n\n startPrint(path: string): Promise<void>;\n\n pausePrint(): Promise<void>;\n\n resumePrint(): Promise<void>;\n\n cancelPrint(): Promise<void>;\n\n quickStop(): Promise<void>;\n\n sendGcode(script: string): Promise<void>;\n\n movePrintHead(amounts: { x?: number; y?: number; z?: number; speed?: number }): Promise<void>;\n\n homeAxes(axes: { x?: boolean; y?: boolean; z?: boolean }): Promise<void>;\n\n getFile(path: string): Promise<FileDto>;\n\n getFiles(recursive?: boolean, startDir?: string): Promise<FilesDto>;\n\n downloadFile(path: string): AxiosPromise<NodeJS.ReadableStream>;\n\n getFileChunk(path: string, startBytes: number, endBytes: number): AxiosPromise<string>;\n\n uploadFile(input: UploadFileInput): Promise<void>;\n\n deleteFile(path: string): Promise<void>;\n\n deleteFolder(path: string): Promise<void>;\n\n getSettings(): Promise<ServerConfigDto | SettingsDto>;\n\n getReprintState(): Promise<PartialReprintFileDto>;\n}\n"],"mappings":";;AASA,MAAa,wBAAwB,EAAE,OAAO;CAC5C,QAAQ,EAAE,QAAkB,QAAQ;
|
|
1
|
+
{"version":3,"file":"printer-api.interface.js","names":[],"sources":["../../src/services/printer-api.interface.ts"],"sourcesContent":["import { AxiosPromise } from \"axios\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport type { ServerConfigDto } from \"@/services/moonraker/dto/server/server-config.dto\";\nimport type { SettingsDto } from \"@/services/octoprint/dto/settings/settings.dto\";\nimport { ConnectionState } from \"@/services/octoprint/dto/connection/connection-state.type\";\nimport { Flags } from \"@/services/moonraker/dto/octoprint-compat/api-printer.dto\";\nimport { z } from \"zod\";\nimport { Readable } from \"node:stream\";\n\nexport const uploadFileInputSchema = z.object({\n stream: z.custom<Readable>((val) => {\n return val && typeof val === \"object\" && \"pipe\" in val && typeof val.pipe === \"function\";\n }, \"Must be a readable stream\"),\n fileName: z.string().min(1),\n contentLength: z.number().int().positive(),\n startPrint: z.boolean(),\n uploadToken: z.string().optional(),\n});\n\nexport type UploadFileInput = z.infer<typeof uploadFileInputSchema>;\n\nexport const OctoprintType = 0;\nexport const MoonrakerType = 1;\nexport const PrusaLinkType = 2;\nexport const BambuType = 3;\n\nexport enum PrinterTypesEnum {\n Octoprint = 0,\n Moonraker = 1,\n PrusaLink = 2,\n Bambu = 3,\n}\n\nexport type PrinterType = typeof OctoprintType | typeof MoonrakerType | typeof PrusaLinkType | typeof BambuType;\n\nexport interface FdmCurrentMessageDto {\n progress: {\n printTime: number | null;\n completion: number | null;\n };\n state: {\n text: string;\n error: string;\n flags: Flags;\n };\n job: {\n file: {\n name: string;\n path: string;\n };\n };\n}\n\nexport interface StatusFlags {\n connected: boolean;\n operational: boolean;\n printing: boolean;\n paused: boolean;\n error: boolean;\n finished: boolean;\n}\n\nexport interface FileDto {\n path: string;\n size: number | null;\n date: number | null;\n dir: boolean;\n}\n\nexport interface FilesDto {\n dirs: FileDto[];\n files: FileDto[];\n}\n\nexport enum ReprintState {\n PrinterNotAvailable = 0,\n NoLastPrint = 1,\n LastPrintReady = 2,\n}\n\nexport interface PartialReprintFileDto {\n file?: FileDto;\n reprintState: ReprintState;\n connectionState: ConnectionState | null;\n}\n\nexport interface ReprintFileDto extends PartialReprintFileDto {\n printerId: number;\n}\n\nexport interface IPrinterApi {\n get type(): PrinterType;\n\n set login(login: LoginDto);\n\n getVersion(): Promise<string>;\n\n validateConnection(): Promise<void>;\n\n connect(): Promise<void>;\n\n disconnect(): Promise<void>;\n\n restartServer(): Promise<void>;\n\n restartHost(): Promise<void>;\n\n restartPrinterFirmware(): Promise<void>;\n\n startPrint(path: string): Promise<void>;\n\n pausePrint(): Promise<void>;\n\n resumePrint(): Promise<void>;\n\n cancelPrint(): Promise<void>;\n\n quickStop(): Promise<void>;\n\n sendGcode(script: string): Promise<void>;\n\n movePrintHead(amounts: { x?: number; y?: number; z?: number; speed?: number }): Promise<void>;\n\n homeAxes(axes: { x?: boolean; y?: boolean; z?: boolean }): Promise<void>;\n\n getFile(path: string): Promise<FileDto>;\n\n getFiles(recursive?: boolean, startDir?: string): Promise<FilesDto>;\n\n downloadFile(path: string): AxiosPromise<NodeJS.ReadableStream>;\n\n getFileChunk(path: string, startBytes: number, endBytes: number): AxiosPromise<string>;\n\n uploadFile(input: UploadFileInput): Promise<void>;\n\n deleteFile(path: string): Promise<void>;\n\n deleteFolder(path: string): Promise<void>;\n\n getSettings(): Promise<ServerConfigDto | SettingsDto>;\n\n getReprintState(): Promise<PartialReprintFileDto>;\n}\n"],"mappings":";;AASA,MAAa,wBAAwB,EAAE,OAAO;CAC5C,QAAQ,EAAE,QAAkB,QAAQ;EAClC,OAAO,OAAO,OAAO,QAAQ,YAAY,UAAU,OAAO,OAAO,IAAI,SAAS;IAC7E,4BAA4B;CAC/B,UAAU,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC3B,eAAe,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU;CAC1C,YAAY,EAAE,SAAS;CACvB,aAAa,EAAE,QAAQ,CAAC,UAAU;CACnC,CAAC;AAIF,MAAa,gBAAgB;AAC7B,MAAa,gBAAgB;AAC7B,MAAa,gBAAgB;AAC7B,MAAa,YAAY;AAEzB,IAAY,mBAAL,yBAAA,kBAAA;CACL,iBAAA,iBAAA,eAAA,KAAA;CACA,iBAAA,iBAAA,eAAA,KAAA;CACA,iBAAA,iBAAA,eAAA,KAAA;CACA,iBAAA,iBAAA,WAAA,KAAA;;KACD;AA2CD,IAAY,eAAL,yBAAA,cAAA;CACL,aAAA,aAAA,yBAAA,KAAA;CACA,aAAA,aAAA,iBAAA,KAAA;CACA,aAAA,aAAA,oBAAA,KAAA;;KACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prusa-link-http-polling.adapter.js","names":[],"sources":["../../../src/services/prusa-link/prusa-link-http-polling.adapter.ts"],"sourcesContent":["import { PrusaLinkType } from \"@/services/printer-api.interface\";\nimport { PrusaLinkApi } from \"@/services/prusa-link/prusa-link.api\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport EventEmitter2 from \"eventemitter2\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport type { IWebsocketAdapter } from \"@/services/websocket-adapter.interface\";\nimport type { ISocketLogin } from \"@/shared/dtos/socket-login.dto\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport { SOCKET_STATE, SocketState } from \"@/shared/dtos/socket-state.type\";\nimport { API_STATE, ApiState } from \"@/shared/dtos/api-state.type\";\nimport { errorSummary } from \"@/utils/error.utils\";\nimport { prusaLinkEvent } from \"@/services/prusa-link/constants/prusalink.constants\";\nimport type { PrusaLinkEventDto } from \"@/services/prusa-link/constants/prusalink-event.dto\";\nimport { WsMessage } from \"@/services/octoprint/octoprint-websocket.adapter\";\n\nconst defaultLog = { adapter: \"prusa-link\" };\n\nexport class PrusaLinkHttpPollingAdapter implements IWebsocketAdapter {\n public readonly printerType = PrusaLinkType;\n public printerId?: number;\n login: LoginDto;\n socketState: SocketState;\n apiState: ApiState;\n lastMessageReceivedTimestamp: null | number;\n protected logger: LoggerService;\n private refreshPrinterCurrentInterval?: NodeJS.Timeout;\n\n private eventEmittingAllowed: boolean = true;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly prusaLinkApi: PrusaLinkApi,\n private readonly eventEmitter2: EventEmitter2,\n ) {\n this.logger = loggerFactory(PrusaLinkHttpPollingAdapter.name);\n }\n\n public allowEmittingEvents() {\n this.eventEmittingAllowed = true;\n }\n\n public disallowEmittingEvents() {\n this.eventEmittingAllowed = false;\n }\n\n needsReopen(): boolean {\n // TODO this can be standardized\n return !this.refreshPrinterCurrentInterval;\n }\n\n needsSetup(): boolean {\n // TODO this can be standardized\n return !this.refreshPrinterCurrentInterval;\n }\n\n needsReauth(): boolean {\n throw new Error(\"Method not implemented.\");\n }\n\n isClosedOrAborted(): boolean {\n throw new Error(\"Method not implemented.\");\n }\n\n reauthSession(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n registerCredentials(socketLogin: ISocketLogin): void {\n this.login = socketLogin.loginDto;\n this.printerId = socketLogin.printerId;\n }\n\n open(): void {\n this.startPolling();\n }\n\n close(): void {\n this.logger.debug(\"Polling adapter attempting stoppage.\", this.logMeta());\n this.stopPolling();\n }\n\n setupSocketSession(): Promise<void> {\n this.logger.warn(\"SetupSocketSession\", defaultLog);\n return Promise.resolve();\n }\n\n resetSocketState(): void {\n this.logger.warn(\"ResetSocketState\", defaultLog);\n }\n\n startPolling() {\n this.stopPolling(); // Ensure no duplicate intervals exist\n\n this.logger.debug(\"Polling adapter starting, setting interval.\", this.logMeta());\n\n this.refreshPrinterCurrentInterval = setInterval(async () => {\n if (!this.printerId) {\n this.logger.warn(\"Printer ID is not set, skipping status check.\", this.logMeta());\n this.stopPolling();\n return;\n }\n\n this.updateSocketState(SOCKET_STATE.opening);\n try {\n this.prusaLinkApi.login = {\n printerURL: this.login.printerURL,\n username: this.login.username,\n password: this.login.password,\n apiKey: \"\",\n printerType: PrusaLinkType,\n };\n this.updateSocketState(SOCKET_STATE.authenticating);\n const printerState = await this.prusaLinkApi.getPrinterState();\n // Only when PRINTING we avoid appending the flag\n if (printerState.state.flags?.link_state && printerState.state.flags?.link_state !== \"PRINTING\") {\n printerState.state.text = printerState.state.flags.link_state;\n }\n const jobState = await this.prusaLinkApi.getJobState();\n this.updateSocketState(SOCKET_STATE.authenticated);\n this.updateApiState(API_STATE.responding);\n await this.emitEvent(\"current\", {\n ...printerState,\n job: jobState.job,\n progress: {\n printTime: jobState.progress?.printTime,\n printTimeLeft: jobState.progress?.printTimeLeft,\n completion: jobState.progress?.completion * 100,\n },\n });\n } catch (error) {\n this.updateSocketState(SOCKET_STATE.error);\n this.logger.error(`Failed to fetch PrusaLink status ${errorSummary(error)}`, this.logMeta());\n }\n }, 5000);\n }\n\n stopPolling() {\n if (this.refreshPrinterCurrentInterval) {\n this.logger.debug(\"Polling adapter stopping, clearing interval.\", this.logMeta());\n clearInterval(this.refreshPrinterCurrentInterval);\n this.refreshPrinterCurrentInterval = undefined;\n this.updateSocketState(SOCKET_STATE.closed);\n }\n }\n\n private async emitEvent(event: string, payload?: any) {\n if (!this.eventEmittingAllowed) {\n return;\n }\n\n this.logger.debug(`Emitting event ${prusaLinkEvent(event)}`, this.logMeta());\n await this.eventEmitter2.emitAsync(prusaLinkEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: PrusaLinkType,\n } as PrusaLinkEventDto);\n }\n\n private emitEventSync(event: string, payload: any): void {\n if (!this.eventEmittingAllowed) {\n return;\n }\n\n this.eventEmitter2.emit(prusaLinkEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: PrusaLinkType,\n } as PrusaLinkEventDto);\n }\n\n private updateSocketState(state: SocketState): void {\n this.socketState = state;\n this.emitEventSync(WsMessage.WS_STATE_UPDATED, state);\n }\n\n private updateApiState(state: ApiState): void {\n this.apiState = state;\n this.emitEventSync(WsMessage.API_STATE_UPDATED, state);\n }\n\n private logMeta() {\n return defaultLog;\n }\n}\n"],"mappings":";;;;;;;AAeA,MAAM,aAAa,EAAE,SAAS,cAAc;AAE5C,IAAa,8BAAb,MAAa,4BAAyD;CACpE,cAAgB;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,uBAAwC;CAExC,YACE,eACA,cACA,eACA;
|
|
1
|
+
{"version":3,"file":"prusa-link-http-polling.adapter.js","names":[],"sources":["../../../src/services/prusa-link/prusa-link-http-polling.adapter.ts"],"sourcesContent":["import { PrusaLinkType } from \"@/services/printer-api.interface\";\nimport { PrusaLinkApi } from \"@/services/prusa-link/prusa-link.api\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport EventEmitter2 from \"eventemitter2\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport type { IWebsocketAdapter } from \"@/services/websocket-adapter.interface\";\nimport type { ISocketLogin } from \"@/shared/dtos/socket-login.dto\";\nimport type { LoginDto } from \"@/services/interfaces/login.dto\";\nimport { SOCKET_STATE, SocketState } from \"@/shared/dtos/socket-state.type\";\nimport { API_STATE, ApiState } from \"@/shared/dtos/api-state.type\";\nimport { errorSummary } from \"@/utils/error.utils\";\nimport { prusaLinkEvent } from \"@/services/prusa-link/constants/prusalink.constants\";\nimport type { PrusaLinkEventDto } from \"@/services/prusa-link/constants/prusalink-event.dto\";\nimport { WsMessage } from \"@/services/octoprint/octoprint-websocket.adapter\";\n\nconst defaultLog = { adapter: \"prusa-link\" };\n\nexport class PrusaLinkHttpPollingAdapter implements IWebsocketAdapter {\n public readonly printerType = PrusaLinkType;\n public printerId?: number;\n login: LoginDto;\n socketState: SocketState;\n apiState: ApiState;\n lastMessageReceivedTimestamp: null | number;\n protected logger: LoggerService;\n private refreshPrinterCurrentInterval?: NodeJS.Timeout;\n\n private eventEmittingAllowed: boolean = true;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly prusaLinkApi: PrusaLinkApi,\n private readonly eventEmitter2: EventEmitter2,\n ) {\n this.logger = loggerFactory(PrusaLinkHttpPollingAdapter.name);\n }\n\n public allowEmittingEvents() {\n this.eventEmittingAllowed = true;\n }\n\n public disallowEmittingEvents() {\n this.eventEmittingAllowed = false;\n }\n\n needsReopen(): boolean {\n // TODO this can be standardized\n return !this.refreshPrinterCurrentInterval;\n }\n\n needsSetup(): boolean {\n // TODO this can be standardized\n return !this.refreshPrinterCurrentInterval;\n }\n\n needsReauth(): boolean {\n throw new Error(\"Method not implemented.\");\n }\n\n isClosedOrAborted(): boolean {\n throw new Error(\"Method not implemented.\");\n }\n\n reauthSession(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n registerCredentials(socketLogin: ISocketLogin): void {\n this.login = socketLogin.loginDto;\n this.printerId = socketLogin.printerId;\n }\n\n open(): void {\n this.startPolling();\n }\n\n close(): void {\n this.logger.debug(\"Polling adapter attempting stoppage.\", this.logMeta());\n this.stopPolling();\n }\n\n setupSocketSession(): Promise<void> {\n this.logger.warn(\"SetupSocketSession\", defaultLog);\n return Promise.resolve();\n }\n\n resetSocketState(): void {\n this.logger.warn(\"ResetSocketState\", defaultLog);\n }\n\n startPolling() {\n this.stopPolling(); // Ensure no duplicate intervals exist\n\n this.logger.debug(\"Polling adapter starting, setting interval.\", this.logMeta());\n\n this.refreshPrinterCurrentInterval = setInterval(async () => {\n if (!this.printerId) {\n this.logger.warn(\"Printer ID is not set, skipping status check.\", this.logMeta());\n this.stopPolling();\n return;\n }\n\n this.updateSocketState(SOCKET_STATE.opening);\n try {\n this.prusaLinkApi.login = {\n printerURL: this.login.printerURL,\n username: this.login.username,\n password: this.login.password,\n apiKey: \"\",\n printerType: PrusaLinkType,\n };\n this.updateSocketState(SOCKET_STATE.authenticating);\n const printerState = await this.prusaLinkApi.getPrinterState();\n // Only when PRINTING we avoid appending the flag\n if (printerState.state.flags?.link_state && printerState.state.flags?.link_state !== \"PRINTING\") {\n printerState.state.text = printerState.state.flags.link_state;\n }\n const jobState = await this.prusaLinkApi.getJobState();\n this.updateSocketState(SOCKET_STATE.authenticated);\n this.updateApiState(API_STATE.responding);\n await this.emitEvent(\"current\", {\n ...printerState,\n job: jobState.job,\n progress: {\n printTime: jobState.progress?.printTime,\n printTimeLeft: jobState.progress?.printTimeLeft,\n completion: jobState.progress?.completion * 100,\n },\n });\n } catch (error) {\n this.updateSocketState(SOCKET_STATE.error);\n this.logger.error(`Failed to fetch PrusaLink status ${errorSummary(error)}`, this.logMeta());\n }\n }, 5000);\n }\n\n stopPolling() {\n if (this.refreshPrinterCurrentInterval) {\n this.logger.debug(\"Polling adapter stopping, clearing interval.\", this.logMeta());\n clearInterval(this.refreshPrinterCurrentInterval);\n this.refreshPrinterCurrentInterval = undefined;\n this.updateSocketState(SOCKET_STATE.closed);\n }\n }\n\n private async emitEvent(event: string, payload?: any) {\n if (!this.eventEmittingAllowed) {\n return;\n }\n\n this.logger.debug(`Emitting event ${prusaLinkEvent(event)}`, this.logMeta());\n await this.eventEmitter2.emitAsync(prusaLinkEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: PrusaLinkType,\n } as PrusaLinkEventDto);\n }\n\n private emitEventSync(event: string, payload: any): void {\n if (!this.eventEmittingAllowed) {\n return;\n }\n\n this.eventEmitter2.emit(prusaLinkEvent(event), {\n event,\n payload,\n printerId: this.printerId,\n printerType: PrusaLinkType,\n } as PrusaLinkEventDto);\n }\n\n private updateSocketState(state: SocketState): void {\n this.socketState = state;\n this.emitEventSync(WsMessage.WS_STATE_UPDATED, state);\n }\n\n private updateApiState(state: ApiState): void {\n this.apiState = state;\n this.emitEventSync(WsMessage.API_STATE_UPDATED, state);\n }\n\n private logMeta() {\n return defaultLog;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prusa-link.api.js","names":[],"sources":["../../../src/services/prusa-link/prusa-link.api.ts"],"sourcesContent":["import { HttpClientFactory } from \"@/services/core/http-client.factory\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport {\n FileDto,\n IPrinterApi,\n PartialReprintFileDto,\n PrinterType,\n PrusaLinkType,\n UploadFileInput,\n uploadFileInputSchema,\n} from \"@/services/printer-api.interface\";\nimport { AxiosError, AxiosPromise } from \"axios\";\nimport type { LoginDto } from \"../interfaces/login.dto\";\nimport type { ServerConfigDto } from \"../moonraker/dto/server/server-config.dto\";\nimport type { SettingsDto } from \"../octoprint/dto/settings/settings.dto\";\nimport { PrusaLinkHttpClientBuilder } from \"@/services/prusa-link/utils/prusa-link-http-client.builder\";\nimport type { VersionDto } from \"@/services/prusa-link/dto/version.dto\";\nimport type { PL_FileResponseDto } from \"@/services/prusa-link/dto/file-response.dto\";\nimport type { PL_StatusDto } from \"@/services/prusa-link/dto/status.dto\";\nimport type { PL_PrinterStateDto } from \"@/services/prusa-link/dto/printer-state.dto\";\nimport type { PL_JobStateDto } from \"@/services/prusa-link/dto/job-state.dto\";\nimport { uploadDoneEvent, uploadFailedEvent, uploadProgressEvent } from \"@/constants/event.constants\";\nimport { ExternalServiceError } from \"@/exceptions/runtime.exceptions\";\nimport EventEmitter2 from \"eventemitter2\";\nimport type { PL_FileDto } from \"@/services/prusa-link/dto/file.dto\";\nimport { SettingsStore } from \"@/state/settings.store\";\n\nconst defaultLog = { adapter: \"prusa-link\" };\n\n/**\n * Prusa Link OpenAPI spec https://raw.githubusercontent.com/prusa3d/Prusa-Link-Web/master/spec/openapi.yaml\n * Prusa Link https://github.com/prusa3d/Prusa-Link\n * Prusa Link Web https://github.com/prusa3d/Prusa-Link-Web/tree/master\n */\nexport class PrusaLinkApi implements IPrinterApi {\n protected logger: LoggerService;\n private authHeader: string | null = null;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly eventEmitter2: EventEmitter2,\n private readonly httpClientFactory: HttpClientFactory,\n private readonly settingsStore: SettingsStore,\n private printerLogin: LoginDto,\n ) {\n this.logger = loggerFactory(PrusaLinkApi.name);\n this.logger.debug(\"Constructed api client\", this.logMeta());\n }\n\n get type(): PrinterType {\n return PrusaLinkType;\n }\n\n set login(login: LoginDto) {\n this.printerLogin = login;\n }\n\n private get client() {\n return this.createClient();\n }\n\n async getVersion(): Promise<string> {\n const response = await this.client.get<VersionDto>(\"/api/version\");\n return response.data.server;\n }\n\n async validateConnection(): Promise<void> {\n await this.getVersion();\n }\n\n async getFiles(recursive = false, startDir = \"/usb\") {\n if (recursive) {\n throw new Error(\"Recursive listing not supported for PrusaLink printers\");\n }\n\n const response = await this.client.get<PL_FileResponseDto>(\"/api/files\");\n const root = response.data.files.find((dir) => dir.path === startDir);\n\n if (!root || !root.children) {\n return { dirs: [], files: [] };\n }\n\n const items = root.children.map((child) => ({\n path: child.path,\n size: null,\n date: null,\n dir: !child.refs?.download,\n }));\n\n return {\n dirs: items.filter((i) => i.dir),\n files: items.filter((i) => !i.dir),\n };\n }\n\n async getFile(path: string): Promise<FileDto> {\n const response = await this.getFileRaw(path);\n\n return {\n path: response.data.name,\n size: response.data.size,\n date: null,\n dir: false,\n };\n }\n\n async getStatus(): Promise<PL_StatusDto> {\n const response = await this.client.get<PL_StatusDto>(\"/api/v1/status\");\n return response.data;\n }\n\n async getPrinterState(): Promise<PL_PrinterStateDto> {\n // OctoPrint compatibility\n const response = await this.client.get<PL_PrinterStateDto>(\"/api/printer\");\n return response.data;\n }\n\n async getJobState(): Promise<PL_JobStateDto> {\n // OctoPrint compatibility\n const response = await this.client.get<PL_JobStateDto>(\"/api/job\");\n return response.data;\n }\n\n connect(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n disconnect(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n restartServer(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n restartHost(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n restartPrinterFirmware(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n async startPrint(path: string): Promise<void> {\n await this.client.post<void>(`/api/v1/files/usb/${path}`);\n }\n\n async pausePrint(): Promise<void> {\n const jobId = await this.getCurrentJobId();\n if (!jobId) {\n this.logger.warn(\"Job pause command did not complete, job or job id not set\");\n return;\n }\n await this.client.put<void>(`/api/v1/job/${jobId}/pause`);\n }\n\n async resumePrint(): Promise<void> {\n const jobId = await this.getCurrentJobId();\n if (!jobId) {\n this.logger.warn(\"Job resume command did not complete, job or job id not set\");\n return;\n }\n await this.client.put<void>(`/api/v1/job/${jobId}/resume`);\n }\n\n async cancelPrint(): Promise<void> {\n const jobId = await this.getCurrentJobId();\n if (!jobId) {\n this.logger.warn(\"Job cancel command did not complete, job or job id not set\");\n return;\n }\n await this.client.delete<void>(`/api/v1/job/${jobId}`);\n }\n\n quickStop(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n sendGcode(script: string): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n movePrintHead(amounts: { x?: number; y?: number; z?: number; speed?: number }): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n homeAxes(axes: { x?: boolean; y?: boolean; z?: boolean }): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n async downloadFile(path: string): AxiosPromise<NodeJS.ReadableStream> {\n const fileReference = await this.getFileRaw(path);\n const pathUrl = fileReference.data.refs.download;\n\n return await this.client.get(pathUrl, {\n responseType: \"stream\",\n });\n }\n\n async getFileChunk(path: string, startBytes: number, endBytes: number): AxiosPromise<string> {\n const fileReference = await this.getFileRaw(path);\n const pathUrl = fileReference.data.refs.download;\n\n return await this.createClient((o) =>\n o.withHeaders({\n Range: `bytes=${startBytes}-${endBytes}`,\n }),\n ).get<string>(pathUrl);\n }\n\n async uploadFile(input: UploadFileInput): Promise<void> {\n const validated = uploadFileInputSchema.parse(input);\n\n try {\n const response = await this.createClient((b) => {\n b.withHeaders({\n \"Content-Type\": \"application/octet-stream\",\n \"Content-Length\": validated.contentLength.toString(),\n Overwrite: \"?1\",\n \"Print-After-Upload\": validated.startPrint ? \"?1\" : \"?0\",\n })\n .withTimeout(this.settingsStore.getTimeoutSettings().apiUploadTimeout)\n .withOnUploadProgress((p) => {\n if (validated.uploadToken) {\n this.eventEmitter2.emit(`${uploadProgressEvent(validated.uploadToken)}`, validated.uploadToken, p);\n }\n });\n }).put(`/api/v1/files/usb/${encodeURIComponent(validated.fileName)}`, validated.stream);\n\n if (validated.uploadToken) {\n this.eventEmitter2.emit(`${uploadDoneEvent(validated.uploadToken)}`, validated.uploadToken);\n }\n\n return response.data;\n } catch (e: any) {\n if (validated.uploadToken) {\n this.eventEmitter2.emit(\n `${uploadFailedEvent(validated.uploadToken)}`,\n validated.uploadToken,\n (e as AxiosError)?.message,\n );\n }\n\n let data;\n try {\n data = JSON.parse(e.response?.body);\n } catch {\n data = e.response?.body;\n }\n\n throw new ExternalServiceError(\n {\n error: e.message,\n statusCode: e.response?.statusCode,\n data,\n success: false,\n stack: e.stack,\n },\n \"Prusa-Link\",\n );\n }\n }\n\n async deleteFile(path: string): Promise<void> {\n await this.client.delete<void>(`/api/v1/files/usb/${path}`);\n }\n\n async deleteFolder(path: string): Promise<void> {\n await this.client.delete<void>(`/api/v1/files/usb/${path}`);\n }\n\n getSettings(): Promise<ServerConfigDto | SettingsDto> {\n throw new Error(\"Method not implemented.\");\n }\n\n getReprintState(): Promise<PartialReprintFileDto> {\n throw new Error(\"Method not implemented.\");\n }\n\n private getFileRaw(path: string) {\n return this.client.get<PL_FileDto>(`/api/v1/files/usb/${path}`);\n }\n\n private async getCurrentJobId() {\n const status = await this.getStatus();\n return status.job?.id;\n }\n\n private createClient(buildFluentOptions?: (base: PrusaLinkHttpClientBuilder) => void) {\n const builder = new PrusaLinkHttpClientBuilder();\n\n return this.httpClientFactory.createClientWithBaseUrl(builder, this.printerLogin.printerURL, (b) => {\n this.logger.debug(\"Building API client\", this.logMeta());\n\n // Set up digest auth with the credentials and an error handler\n b.withDigestAuth(\n this.printerLogin.username,\n this.printerLogin.password,\n (error) => {\n this.logger.error(\"Authentication error occurred\", error);\n },\n (error, attemptCount) => {\n this.logger.log(\n `Authentication attempt count ${attemptCount} for method ${error.config?.method?.toUpperCase()} path ${error.config?.url}`,\n this.logMeta(),\n );\n },\n (authHeader) => {\n this.logger.debug(\"Authentication successful, saving auth header for later reuse\", this.logMeta());\n this.authHeader = authHeader;\n },\n );\n\n if (this.authHeader) {\n b.withAuthHeader(this.authHeader);\n }\n\n if (buildFluentOptions && typeof buildFluentOptions === \"function\") {\n buildFluentOptions(b);\n }\n });\n }\n\n private logMeta() {\n return defaultLog;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prusa-link.api.js","names":[],"sources":["../../../src/services/prusa-link/prusa-link.api.ts"],"sourcesContent":["import { HttpClientFactory } from \"@/services/core/http-client.factory\";\nimport { LoggerService } from \"@/handlers/logger\";\nimport type { ILoggerFactory } from \"@/handlers/logger-factory\";\nimport {\n FileDto,\n IPrinterApi,\n PartialReprintFileDto,\n PrinterType,\n PrusaLinkType,\n UploadFileInput,\n uploadFileInputSchema,\n} from \"@/services/printer-api.interface\";\nimport { AxiosError, AxiosPromise } from \"axios\";\nimport type { LoginDto } from \"../interfaces/login.dto\";\nimport type { ServerConfigDto } from \"../moonraker/dto/server/server-config.dto\";\nimport type { SettingsDto } from \"../octoprint/dto/settings/settings.dto\";\nimport { PrusaLinkHttpClientBuilder } from \"@/services/prusa-link/utils/prusa-link-http-client.builder\";\nimport type { VersionDto } from \"@/services/prusa-link/dto/version.dto\";\nimport type { PL_FileResponseDto } from \"@/services/prusa-link/dto/file-response.dto\";\nimport type { PL_StatusDto } from \"@/services/prusa-link/dto/status.dto\";\nimport type { PL_PrinterStateDto } from \"@/services/prusa-link/dto/printer-state.dto\";\nimport type { PL_JobStateDto } from \"@/services/prusa-link/dto/job-state.dto\";\nimport { uploadDoneEvent, uploadFailedEvent, uploadProgressEvent } from \"@/constants/event.constants\";\nimport { ExternalServiceError } from \"@/exceptions/runtime.exceptions\";\nimport EventEmitter2 from \"eventemitter2\";\nimport type { PL_FileDto } from \"@/services/prusa-link/dto/file.dto\";\nimport { SettingsStore } from \"@/state/settings.store\";\n\nconst defaultLog = { adapter: \"prusa-link\" };\n\n/**\n * Prusa Link OpenAPI spec https://raw.githubusercontent.com/prusa3d/Prusa-Link-Web/master/spec/openapi.yaml\n * Prusa Link https://github.com/prusa3d/Prusa-Link\n * Prusa Link Web https://github.com/prusa3d/Prusa-Link-Web/tree/master\n */\nexport class PrusaLinkApi implements IPrinterApi {\n protected logger: LoggerService;\n private authHeader: string | null = null;\n\n constructor(\n loggerFactory: ILoggerFactory,\n private readonly eventEmitter2: EventEmitter2,\n private readonly httpClientFactory: HttpClientFactory,\n private readonly settingsStore: SettingsStore,\n private printerLogin: LoginDto,\n ) {\n this.logger = loggerFactory(PrusaLinkApi.name);\n this.logger.debug(\"Constructed api client\", this.logMeta());\n }\n\n get type(): PrinterType {\n return PrusaLinkType;\n }\n\n set login(login: LoginDto) {\n this.printerLogin = login;\n }\n\n private get client() {\n return this.createClient();\n }\n\n async getVersion(): Promise<string> {\n const response = await this.client.get<VersionDto>(\"/api/version\");\n return response.data.server;\n }\n\n async validateConnection(): Promise<void> {\n await this.getVersion();\n }\n\n async getFiles(recursive = false, startDir = \"/usb\") {\n if (recursive) {\n throw new Error(\"Recursive listing not supported for PrusaLink printers\");\n }\n\n const response = await this.client.get<PL_FileResponseDto>(\"/api/files\");\n const root = response.data.files.find((dir) => dir.path === startDir);\n\n if (!root || !root.children) {\n return { dirs: [], files: [] };\n }\n\n const items = root.children.map((child) => ({\n path: child.path,\n size: null,\n date: null,\n dir: !child.refs?.download,\n }));\n\n return {\n dirs: items.filter((i) => i.dir),\n files: items.filter((i) => !i.dir),\n };\n }\n\n async getFile(path: string): Promise<FileDto> {\n const response = await this.getFileRaw(path);\n\n return {\n path: response.data.name,\n size: response.data.size,\n date: null,\n dir: false,\n };\n }\n\n async getStatus(): Promise<PL_StatusDto> {\n const response = await this.client.get<PL_StatusDto>(\"/api/v1/status\");\n return response.data;\n }\n\n async getPrinterState(): Promise<PL_PrinterStateDto> {\n // OctoPrint compatibility\n const response = await this.client.get<PL_PrinterStateDto>(\"/api/printer\");\n return response.data;\n }\n\n async getJobState(): Promise<PL_JobStateDto> {\n // OctoPrint compatibility\n const response = await this.client.get<PL_JobStateDto>(\"/api/job\");\n return response.data;\n }\n\n connect(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n disconnect(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n restartServer(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n restartHost(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n restartPrinterFirmware(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n async startPrint(path: string): Promise<void> {\n await this.client.post<void>(`/api/v1/files/usb/${path}`);\n }\n\n async pausePrint(): Promise<void> {\n const jobId = await this.getCurrentJobId();\n if (!jobId) {\n this.logger.warn(\"Job pause command did not complete, job or job id not set\");\n return;\n }\n await this.client.put<void>(`/api/v1/job/${jobId}/pause`);\n }\n\n async resumePrint(): Promise<void> {\n const jobId = await this.getCurrentJobId();\n if (!jobId) {\n this.logger.warn(\"Job resume command did not complete, job or job id not set\");\n return;\n }\n await this.client.put<void>(`/api/v1/job/${jobId}/resume`);\n }\n\n async cancelPrint(): Promise<void> {\n const jobId = await this.getCurrentJobId();\n if (!jobId) {\n this.logger.warn(\"Job cancel command did not complete, job or job id not set\");\n return;\n }\n await this.client.delete<void>(`/api/v1/job/${jobId}`);\n }\n\n quickStop(): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n sendGcode(script: string): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n movePrintHead(amounts: { x?: number; y?: number; z?: number; speed?: number }): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n homeAxes(axes: { x?: boolean; y?: boolean; z?: boolean }): Promise<void> {\n throw new Error(\"Method not implemented.\");\n }\n\n async downloadFile(path: string): AxiosPromise<NodeJS.ReadableStream> {\n const fileReference = await this.getFileRaw(path);\n const pathUrl = fileReference.data.refs.download;\n\n return await this.client.get(pathUrl, {\n responseType: \"stream\",\n });\n }\n\n async getFileChunk(path: string, startBytes: number, endBytes: number): AxiosPromise<string> {\n const fileReference = await this.getFileRaw(path);\n const pathUrl = fileReference.data.refs.download;\n\n return await this.createClient((o) =>\n o.withHeaders({\n Range: `bytes=${startBytes}-${endBytes}`,\n }),\n ).get<string>(pathUrl);\n }\n\n async uploadFile(input: UploadFileInput): Promise<void> {\n const validated = uploadFileInputSchema.parse(input);\n\n try {\n const response = await this.createClient((b) => {\n b.withHeaders({\n \"Content-Type\": \"application/octet-stream\",\n \"Content-Length\": validated.contentLength.toString(),\n Overwrite: \"?1\",\n \"Print-After-Upload\": validated.startPrint ? \"?1\" : \"?0\",\n })\n .withTimeout(this.settingsStore.getTimeoutSettings().apiUploadTimeout)\n .withOnUploadProgress((p) => {\n if (validated.uploadToken) {\n this.eventEmitter2.emit(`${uploadProgressEvent(validated.uploadToken)}`, validated.uploadToken, p);\n }\n });\n }).put(`/api/v1/files/usb/${encodeURIComponent(validated.fileName)}`, validated.stream);\n\n if (validated.uploadToken) {\n this.eventEmitter2.emit(`${uploadDoneEvent(validated.uploadToken)}`, validated.uploadToken);\n }\n\n return response.data;\n } catch (e: any) {\n if (validated.uploadToken) {\n this.eventEmitter2.emit(\n `${uploadFailedEvent(validated.uploadToken)}`,\n validated.uploadToken,\n (e as AxiosError)?.message,\n );\n }\n\n let data;\n try {\n data = JSON.parse(e.response?.body);\n } catch {\n data = e.response?.body;\n }\n\n throw new ExternalServiceError(\n {\n error: e.message,\n statusCode: e.response?.statusCode,\n data,\n success: false,\n stack: e.stack,\n },\n \"Prusa-Link\",\n );\n }\n }\n\n async deleteFile(path: string): Promise<void> {\n await this.client.delete<void>(`/api/v1/files/usb/${path}`);\n }\n\n async deleteFolder(path: string): Promise<void> {\n await this.client.delete<void>(`/api/v1/files/usb/${path}`);\n }\n\n getSettings(): Promise<ServerConfigDto | SettingsDto> {\n throw new Error(\"Method not implemented.\");\n }\n\n getReprintState(): Promise<PartialReprintFileDto> {\n throw new Error(\"Method not implemented.\");\n }\n\n private getFileRaw(path: string) {\n return this.client.get<PL_FileDto>(`/api/v1/files/usb/${path}`);\n }\n\n private async getCurrentJobId() {\n const status = await this.getStatus();\n return status.job?.id;\n }\n\n private createClient(buildFluentOptions?: (base: PrusaLinkHttpClientBuilder) => void) {\n const builder = new PrusaLinkHttpClientBuilder();\n\n return this.httpClientFactory.createClientWithBaseUrl(builder, this.printerLogin.printerURL, (b) => {\n this.logger.debug(\"Building API client\", this.logMeta());\n\n // Set up digest auth with the credentials and an error handler\n b.withDigestAuth(\n this.printerLogin.username,\n this.printerLogin.password,\n (error) => {\n this.logger.error(\"Authentication error occurred\", error);\n },\n (error, attemptCount) => {\n this.logger.log(\n `Authentication attempt count ${attemptCount} for method ${error.config?.method?.toUpperCase()} path ${error.config?.url}`,\n this.logMeta(),\n );\n },\n (authHeader) => {\n this.logger.debug(\"Authentication successful, saving auth header for later reuse\", this.logMeta());\n this.authHeader = authHeader;\n },\n );\n\n if (this.authHeader) {\n b.withAuthHeader(this.authHeader);\n }\n\n if (buildFluentOptions && typeof buildFluentOptions === \"function\") {\n buildFluentOptions(b);\n }\n });\n }\n\n private logMeta() {\n return defaultLog;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
//#region src/services/prusa-link/utils/digest-auth.util.ts
|
|
3
3
|
function generateDigestAuthHeader(params) {
|
|
4
|
-
const { username, password, method, uri, realm, nonce, qop, nc, cnonce } = params;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (qop) {
|
|
10
|
-
response = createHash("md5").update(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`).digest("hex");
|
|
11
|
-
authHeader = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", qop=${qop}, nc=${nc}, cnonce="${cnonce}", response="${response}"`;
|
|
12
|
-
} else {
|
|
13
|
-
response = createHash("md5").update(`${ha1}:${nonce}:${ha2}`).digest("hex");
|
|
14
|
-
authHeader = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", response="${response}"`;
|
|
4
|
+
const { username, password, method, uri, realm, nonce, qop, nc, cnonce, opaque, algorithm } = params;
|
|
5
|
+
let ha1 = createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
|
|
6
|
+
if (algorithm?.toLowerCase() === "md5-sess") {
|
|
7
|
+
if (!cnonce?.length) throw new Error("cnonce is required for MD5-sess algorithm");
|
|
8
|
+
ha1 = createHash("md5").update(`${ha1}:${nonce}:${cnonce}`).digest("hex");
|
|
15
9
|
}
|
|
16
|
-
|
|
10
|
+
const ha2 = createHash("md5").update(`${method}:${uri}`).digest("hex");
|
|
11
|
+
const response = qop ? createHash("md5").update(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`).digest("hex") : createHash("md5").update(`${ha1}:${nonce}:${ha2}`).digest("hex");
|
|
12
|
+
const headerParts = [
|
|
13
|
+
`Digest username="${username}"`,
|
|
14
|
+
`realm="${realm}"`,
|
|
15
|
+
`nonce="${nonce}"`,
|
|
16
|
+
`uri="${uri}"`
|
|
17
|
+
];
|
|
18
|
+
if (algorithm?.length) headerParts.push(`algorithm=${algorithm}`);
|
|
19
|
+
if (opaque?.length) headerParts.push(`opaque="${opaque}"`);
|
|
20
|
+
if (qop) headerParts.push(`qop=${qop}`, `nc=${nc}`, `cnonce="${cnonce}"`);
|
|
21
|
+
else if (cnonce?.length) headerParts.push(`cnonce="${cnonce}"`);
|
|
22
|
+
headerParts.push(`response="${response}"`);
|
|
23
|
+
return headerParts.join(", ");
|
|
17
24
|
}
|
|
18
25
|
//#endregion
|
|
19
26
|
export { generateDigestAuthHeader };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"digest-auth.util.js","names":[],"sources":["../../../../src/services/prusa-link/utils/digest-auth.util.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { DigestAuthParams } from \"@/services/prusa-link/utils/digest-auth.params\";\n\nexport function generateDigestAuthHeader(params: DigestAuthParams): string {\n const { username, password, method, uri, realm, nonce, qop, nc, cnonce } = params;\n\n
|
|
1
|
+
{"version":3,"file":"digest-auth.util.js","names":[],"sources":["../../../../src/services/prusa-link/utils/digest-auth.util.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { DigestAuthParams } from \"@/services/prusa-link/utils/digest-auth.params\";\n\nexport function generateDigestAuthHeader(params: DigestAuthParams): string {\n const { username, password, method, uri, realm, nonce, qop, nc, cnonce, opaque, algorithm } = params;\n\n let ha1 = createHash(\"md5\").update(`${username}:${realm}:${password}`).digest(\"hex\");\n\n if (algorithm?.toLowerCase() === \"md5-sess\") {\n if (!cnonce?.length) {\n throw new Error(\"cnonce is required for MD5-sess algorithm\");\n }\n ha1 = createHash(\"md5\").update(`${ha1}:${nonce}:${cnonce}`).digest(\"hex\");\n }\n\n const ha2 = createHash(\"md5\").update(`${method}:${uri}`).digest(\"hex\");\n\n const response = qop\n ? createHash(\"md5\").update(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`).digest(\"hex\")\n : createHash(\"md5\").update(`${ha1}:${nonce}:${ha2}`).digest(\"hex\");\n\n const headerParts = [`Digest username=\"${username}\"`, `realm=\"${realm}\"`, `nonce=\"${nonce}\"`, `uri=\"${uri}\"`];\n\n if (algorithm?.length) {\n headerParts.push(`algorithm=${algorithm}`);\n }\n\n if (opaque?.length) {\n headerParts.push(`opaque=\"${opaque}\"`);\n }\n\n if (qop) {\n headerParts.push(`qop=${qop}`, `nc=${nc}`, `cnonce=\"${cnonce}\"`);\n } else if (cnonce?.length) {\n headerParts.push(`cnonce=\"${cnonce}\"`);\n }\n\n headerParts.push(`response=\"${response}\"`);\n\n return headerParts.join(\", \");\n}\n"],"mappings":";;AAGA,SAAgB,yBAAyB,QAAkC;CACzE,MAAM,EAAE,UAAU,UAAU,QAAQ,KAAK,OAAO,OAAO,KAAK,IAAI,QAAQ,QAAQ,cAAc;CAE9F,IAAI,MAAM,WAAW,MAAM,CAAC,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,CAAC,OAAO,MAAM;CAEpF,IAAI,WAAW,aAAa,KAAK,YAAY;EAC3C,IAAI,CAAC,QAAQ,QACX,MAAM,IAAI,MAAM,4CAA4C;EAE9D,MAAM,WAAW,MAAM,CAAC,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC,OAAO,MAAM;;CAG3E,MAAM,MAAM,WAAW,MAAM,CAAC,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC,OAAO,MAAM;CAEtE,MAAM,WAAW,MACb,WAAW,MAAM,CAAC,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,GAAG,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC,OAAO,MAAM,GACvF,WAAW,MAAM,CAAC,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC,OAAO,MAAM;CAEpE,MAAM,cAAc;EAAC,oBAAoB,SAAS;EAAI,UAAU,MAAM;EAAI,UAAU,MAAM;EAAI,QAAQ,IAAI;EAAG;CAE7G,IAAI,WAAW,QACb,YAAY,KAAK,aAAa,YAAY;CAG5C,IAAI,QAAQ,QACV,YAAY,KAAK,WAAW,OAAO,GAAG;CAGxC,IAAI,KACF,YAAY,KAAK,OAAO,OAAO,MAAM,MAAM,WAAW,OAAO,GAAG;MAC3D,IAAI,QAAQ,QACjB,YAAY,KAAK,WAAW,OAAO,GAAG;CAGxC,YAAY,KAAK,aAAa,SAAS,GAAG;CAE1C,OAAO,YAAY,KAAK,KAAK"}
|
|
@@ -17,8 +17,14 @@ var PrusaLinkHttpClientBuilder = class extends DefaultHttpClientBuilder {
|
|
|
17
17
|
if (this.username && this.password) {
|
|
18
18
|
axiosInstance.interceptors.request.use(async (config) => {
|
|
19
19
|
if (this.authHeaderContext) {
|
|
20
|
-
const
|
|
21
|
-
config.
|
|
20
|
+
const method = config.method?.toUpperCase() ?? "GET";
|
|
21
|
+
const rawUrl = config.url ?? "/";
|
|
22
|
+
let uri = rawUrl;
|
|
23
|
+
if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) try {
|
|
24
|
+
const parsed = new URL(rawUrl);
|
|
25
|
+
uri = `${parsed.pathname}${parsed.search ?? ""}`;
|
|
26
|
+
} catch {}
|
|
27
|
+
config.headers[authorizationHeaderKey] = this.generateDigestHeader(method, uri);
|
|
22
28
|
}
|
|
23
29
|
return config;
|
|
24
30
|
});
|
|
@@ -59,22 +65,48 @@ var PrusaLinkHttpClientBuilder = class extends DefaultHttpClientBuilder {
|
|
|
59
65
|
return this;
|
|
60
66
|
}
|
|
61
67
|
saveParsedAuthHeaderContext(authHeader) {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const cleanedHeader = authHeader.trim();
|
|
69
|
+
const headerValue = cleanedHeader.startsWith("Digest ") ? cleanedHeader.substring(7) : cleanedHeader;
|
|
70
|
+
const tokens = [];
|
|
71
|
+
let current = "";
|
|
72
|
+
let inQuotes = false;
|
|
73
|
+
for (const ch of headerValue) {
|
|
74
|
+
if (ch === "\"") {
|
|
75
|
+
inQuotes = !inQuotes;
|
|
76
|
+
current += ch;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ch === "," && !inQuotes) {
|
|
80
|
+
if (current.trim().length) tokens.push(current.trim());
|
|
81
|
+
current = "";
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
current += ch;
|
|
85
|
+
}
|
|
86
|
+
if (current.trim().length) tokens.push(current.trim());
|
|
87
|
+
const authParams = Object.fromEntries(tokens.map((param) => {
|
|
88
|
+
const idx = param.indexOf("=");
|
|
89
|
+
if (idx === -1) return [param.trim(), ""];
|
|
90
|
+
const key = param.slice(0, idx).trim();
|
|
91
|
+
let value = param.slice(idx + 1).trim();
|
|
92
|
+
if (value.startsWith("\"") && value.endsWith("\"")) value = value.slice(1, -1);
|
|
93
|
+
return [key, value];
|
|
67
94
|
}));
|
|
95
|
+
const qopRaw = authParams.qop;
|
|
96
|
+
const qop = qopRaw ? qopRaw.split(",").map((q) => q.trim()).find((q) => q === "auth") ?? qopRaw.split(",")[0].trim() : void 0;
|
|
68
97
|
this.authHeaderContext = {
|
|
69
98
|
realm: authParams.realm,
|
|
70
99
|
nonce: authParams.nonce,
|
|
71
|
-
qop
|
|
72
|
-
hasQop:
|
|
100
|
+
qop,
|
|
101
|
+
hasQop: !!qop,
|
|
102
|
+
opaque: authParams.opaque,
|
|
103
|
+
algorithm: authParams.algorithm
|
|
73
104
|
};
|
|
74
105
|
}
|
|
75
106
|
generateDigestHeader(method, uri) {
|
|
76
107
|
if (!this.authHeaderContext || !this.username || !this.password) throw new Error("Digest auth not properly configured");
|
|
77
|
-
const { realm, nonce, qop, hasQop } = this.authHeaderContext;
|
|
108
|
+
const { realm, nonce, qop, hasQop, opaque, algorithm } = this.authHeaderContext;
|
|
109
|
+
const cnonce = hasQop || algorithm?.toLowerCase() === "md5-sess" ? randomBytes(8).toString("hex") : void 0;
|
|
78
110
|
return generateDigestAuthHeader({
|
|
79
111
|
username: this.username,
|
|
80
112
|
password: this.password,
|
|
@@ -84,7 +116,9 @@ var PrusaLinkHttpClientBuilder = class extends DefaultHttpClientBuilder {
|
|
|
84
116
|
nonce,
|
|
85
117
|
qop: hasQop ? qop : void 0,
|
|
86
118
|
nc: hasQop ? "00000001" : void 0,
|
|
87
|
-
cnonce
|
|
119
|
+
cnonce,
|
|
120
|
+
opaque,
|
|
121
|
+
algorithm
|
|
88
122
|
});
|
|
89
123
|
}
|
|
90
124
|
};
|