From 536a714d9b9c5f41d694df9f89733356ea61d167 Mon Sep 17 00:00:00 2001 From: SaelixCode Date: Thu, 5 Mar 2026 10:15:54 -0500 Subject: [PATCH] feat: add dynamic template registry and smart volume path sanitizer --- CHANGELOG.md | 2 ++ backend/src/services/TemplateService.ts | 42 ++++++++++++++++++++----- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc8d9037..1fe9d99b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **Added:** Dynamic Template Registry URL support via global settings, defaulting to LinuxServer.io templates. +- **Fixed:** Smart Volume Sanitizer to automatically rewrite messy Portainer bind mounts into clean, relative paths (Sencho 1:1 path rule). - Git Flow branching strategy and branch protection. - GitHub Actions CI pipeline for automated TypeScript build verification. - **Added:** Automated Docker Hub CI/CD pipeline for the `dev` and `latest` tags. diff --git a/backend/src/services/TemplateService.ts b/backend/src/services/TemplateService.ts index 2769520f..cfe2e0ef 100644 --- a/backend/src/services/TemplateService.ts +++ b/backend/src/services/TemplateService.ts @@ -1,4 +1,6 @@ import axios from 'axios'; +import { DatabaseService } from './DatabaseService'; + export interface TemplateEnv { name: string; @@ -38,7 +40,6 @@ export class TemplateService { private cachedTemplates: Template[] = []; private lastFetchTime: number = 0; private readonly CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours - private readonly TEMPLATES_URL = 'https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json'; public async getTemplates(): Promise { const now = Date.now(); @@ -47,7 +48,11 @@ export class TemplateService { } try { - const response = await axios.get(this.TEMPLATES_URL); + const settings = DatabaseService.getInstance().getGlobalSettings(); + // Default to a reliable LSIO Portainer v2 template registry if not set + const registryUrl = settings.template_registry_url || 'https://raw.githubusercontent.com/technorabilia/portainer-templates/main/lsio/templates/templates-2.0.json'; + + const response = await axios.get(registryUrl); // Filter out templates without images as we are generating compose files from image, ports, etc. this.cachedTemplates = (response.data.templates || []).filter((t: Template) => !!t.image && t.type === 1); this.lastFetchTime = now; @@ -80,14 +85,37 @@ export class TemplateService { if (template.volumes && template.volumes.length > 0) { yaml += ` volumes:\n`; for (const vol of template.volumes) { + let hostPath = ''; + let containerPath = ''; + let options = ''; + if (typeof vol === 'string') { - yaml += ` - ${vol}\n`; + const parts = vol.split(':'); + if (parts.length === 1) { + yaml += ` - ${vol}\n`; + continue; + } + hostPath = parts[0]; + containerPath = parts[1]; + options = parts.slice(2).join(':'); + if (options) options = `:${options}`; } else if (vol.container) { - // If bind is not provided, we extract the folder name from the container path - const containerFolder = vol.container.split('/').pop() || 'data'; - const localBind = vol.bind ? vol.bind.replace(/^\//, './') : `./${containerFolder}`; - yaml += ` - ${localBind}:${vol.container}${vol.readonly ? ':ro' : ''}\n`; + containerPath = vol.container; + const containerFolder = containerPath.split('/').filter(Boolean).pop() || 'data'; + hostPath = vol.bind ? vol.bind : `./${containerFolder}`; + options = vol.readonly ? ':ro' : ''; + } else { + continue; } + + if (hostPath.includes('portainer/Files') || hostPath.includes('/your/') || hostPath.includes('/path/to/')) { + const containerFolder = containerPath.split('/').filter(Boolean).pop() || 'data'; + hostPath = `./${containerFolder}`; + } else if (hostPath.startsWith('/')) { + hostPath = hostPath.replace(/^\//, './'); + } + + yaml += ` - ${hostPath}:${containerPath}${options}\n`; } }