From 485234d43419951dfa5fb6a75ee108619b78e712 Mon Sep 17 00:00:00 2001 From: drondeseries Date: Wed, 20 May 2026 20:56:49 -0400 Subject: [PATCH 1/2] fix(importer): resolve absolute path leakage and retry path inconsistency --- internal/api/sabnzbd_handlers.go | 12 ++++++++++-- internal/importer/filesystem/utils.go | 8 +++----- internal/importer/scanner/directory.go | 8 +++++++- internal/importer/service.go | 13 +++++++++++++ 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/internal/api/sabnzbd_handlers.go b/internal/api/sabnzbd_handlers.go index d24b92d0..67f8e695 100644 --- a/internal/api/sabnzbd_handlers.go +++ b/internal/api/sabnzbd_handlers.go @@ -1456,6 +1456,12 @@ func (s *Server) calculateHistoryStoragePath(item *database.ImportQueueItem, bas cfg := s.configManager.GetConfig() storagePath := *item.StoragePath + // Sanitize storagePath: strip host-temporary prefixes if present + tempUploadDir := filepath.Join(os.TempDir(), "altmount-uploads") + if strings.HasPrefix(storagePath, tempUploadDir) { + storagePath = strings.TrimPrefix(storagePath, tempUploadDir) + } + // Determine category folder category := config.DefaultCategoryName if item.Category != nil && *item.Category != "" { @@ -1481,7 +1487,6 @@ func (s *Server) calculateHistoryStoragePath(item *database.ImportQueueItem, bas } // 3. Determine the base path for reporting - // For NONE, use MountPath. For others, use ImportDir. finalBasePath := cfg.MountPath if cfg.Import.ImportStrategy != config.ImportStrategyNone { if cfg.Import.ImportDir != nil && *cfg.Import.ImportDir != "" { @@ -1495,7 +1500,10 @@ func (s *Server) calculateHistoryStoragePath(item *database.ImportQueueItem, bas if cfg.SABnzbd.CompleteDir != "" { pathParts = append(pathParts, strings.Trim(cfg.SABnzbd.CompleteDir, "/")) } - pathParts = append(pathParts, category) + // Avoid double-prefixing category if it's already in finalBasePath + if !strings.HasSuffix(filepath.Join(pathParts...), category) { + pathParts = append(pathParts, category) + } pathParts = append(pathParts, relPath) fullStoragePath := filepath.Join(pathParts...) diff --git a/internal/importer/filesystem/utils.go b/internal/importer/filesystem/utils.go index 7db37c81..a43f0bad 100644 --- a/internal/importer/filesystem/utils.go +++ b/internal/importer/filesystem/utils.go @@ -23,16 +23,14 @@ func CalculateVirtualDirectory(nzbPath, relativePath string) string { relPath, err := filepath.Rel(relativePath, nzbPath) if err != nil || strings.HasPrefix(relPath, "..") { - if strings.HasPrefix(relativePath, "/") { - return filepath.Clean(relativePath) - } - return "/" + strings.ReplaceAll(relativePath, string(filepath.Separator), "/") + // If nzbPath is not inside relativePath, we cannot derive a valid relative virtual path. + // Return root as a safe default instead of potentially returning a host-absolute path. + return "/" } relDir := filepath.Dir(relPath) if relDir == "." || relDir == "" { // If the file is at the root, return root - // The processor will handle creating a folder if needed (e.g. for archives or multi-file NZBs) return "/" } diff --git a/internal/importer/scanner/directory.go b/internal/importer/scanner/directory.go index a1cece14..de4e2036 100644 --- a/internal/importer/scanner/directory.go +++ b/internal/importer/scanner/directory.go @@ -191,7 +191,13 @@ func (d *DirectoryScanner) performScan(ctx context.Context, scanPath string) { return nil } - if err := d.queueAdder.AddToQueue(ctx, path, &scanPath, nil); err != nil { + // Calculate path relative to the scan root for accurate queue mapping + relPath, relErr := filepath.Rel(scanPath, path) + if relErr != nil { + relPath = path // Fallback if rel fails, though it shouldn't + } + + if err := d.queueAdder.AddToQueue(ctx, path, &relPath, nil); err != nil { d.log.ErrorContext(ctx, "Failed to add file to queue during scan", "file", path, "error", err) } diff --git a/internal/importer/service.go b/internal/importer/service.go index dc17e259..d747a4f1 100644 --- a/internal/importer/service.go +++ b/internal/importer/service.go @@ -742,6 +742,19 @@ func (s *Service) processNzbItem(ctx context.Context, item *database.ImportQueue } func (s *Service) calculateProcessVirtualDir(item *database.ImportQueueItem, basePath *string) string { + // Sanitize basePath to strip any absolute path prefix that might have leaked from temp dirs + cleanBase := *basePath + // Strip common temp upload prefixes + tempUploadDir := filepath.Join(os.TempDir(), "altmount-uploads") + if strings.HasPrefix(cleanBase, tempUploadDir) { + cleanBase = strings.TrimPrefix(cleanBase, tempUploadDir) + } + // Ensure we are not dealing with a hardcoded absolute path + if strings.HasPrefix(cleanBase, "/") { + cleanBase = filepath.Base(cleanBase) + } + *basePath = cleanBase + // Calculate initial virtual directory from physical/relative path virtualDir := filesystem.CalculateVirtualDirectory(item.NzbPath, *basePath) From 71e1b4b4435983e65c3ce1eb33ae9716cfa8d0e9 Mon Sep 17 00:00:00 2001 From: drondeseries <108965325+drondeseries@users.noreply.github.com> Date: Wed, 20 May 2026 22:04:34 -0400 Subject: [PATCH 2/2] Revert "fix(importer): resolve absolute path leakage and retry path inconsistency" --- internal/api/sabnzbd_handlers.go | 12 ++---------- internal/importer/filesystem/utils.go | 8 +++++--- internal/importer/scanner/directory.go | 8 +------- internal/importer/service.go | 13 ------------- 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/internal/api/sabnzbd_handlers.go b/internal/api/sabnzbd_handlers.go index 67f8e695..d24b92d0 100644 --- a/internal/api/sabnzbd_handlers.go +++ b/internal/api/sabnzbd_handlers.go @@ -1456,12 +1456,6 @@ func (s *Server) calculateHistoryStoragePath(item *database.ImportQueueItem, bas cfg := s.configManager.GetConfig() storagePath := *item.StoragePath - // Sanitize storagePath: strip host-temporary prefixes if present - tempUploadDir := filepath.Join(os.TempDir(), "altmount-uploads") - if strings.HasPrefix(storagePath, tempUploadDir) { - storagePath = strings.TrimPrefix(storagePath, tempUploadDir) - } - // Determine category folder category := config.DefaultCategoryName if item.Category != nil && *item.Category != "" { @@ -1487,6 +1481,7 @@ func (s *Server) calculateHistoryStoragePath(item *database.ImportQueueItem, bas } // 3. Determine the base path for reporting + // For NONE, use MountPath. For others, use ImportDir. finalBasePath := cfg.MountPath if cfg.Import.ImportStrategy != config.ImportStrategyNone { if cfg.Import.ImportDir != nil && *cfg.Import.ImportDir != "" { @@ -1500,10 +1495,7 @@ func (s *Server) calculateHistoryStoragePath(item *database.ImportQueueItem, bas if cfg.SABnzbd.CompleteDir != "" { pathParts = append(pathParts, strings.Trim(cfg.SABnzbd.CompleteDir, "/")) } - // Avoid double-prefixing category if it's already in finalBasePath - if !strings.HasSuffix(filepath.Join(pathParts...), category) { - pathParts = append(pathParts, category) - } + pathParts = append(pathParts, category) pathParts = append(pathParts, relPath) fullStoragePath := filepath.Join(pathParts...) diff --git a/internal/importer/filesystem/utils.go b/internal/importer/filesystem/utils.go index a43f0bad..7db37c81 100644 --- a/internal/importer/filesystem/utils.go +++ b/internal/importer/filesystem/utils.go @@ -23,14 +23,16 @@ func CalculateVirtualDirectory(nzbPath, relativePath string) string { relPath, err := filepath.Rel(relativePath, nzbPath) if err != nil || strings.HasPrefix(relPath, "..") { - // If nzbPath is not inside relativePath, we cannot derive a valid relative virtual path. - // Return root as a safe default instead of potentially returning a host-absolute path. - return "/" + if strings.HasPrefix(relativePath, "/") { + return filepath.Clean(relativePath) + } + return "/" + strings.ReplaceAll(relativePath, string(filepath.Separator), "/") } relDir := filepath.Dir(relPath) if relDir == "." || relDir == "" { // If the file is at the root, return root + // The processor will handle creating a folder if needed (e.g. for archives or multi-file NZBs) return "/" } diff --git a/internal/importer/scanner/directory.go b/internal/importer/scanner/directory.go index de4e2036..a1cece14 100644 --- a/internal/importer/scanner/directory.go +++ b/internal/importer/scanner/directory.go @@ -191,13 +191,7 @@ func (d *DirectoryScanner) performScan(ctx context.Context, scanPath string) { return nil } - // Calculate path relative to the scan root for accurate queue mapping - relPath, relErr := filepath.Rel(scanPath, path) - if relErr != nil { - relPath = path // Fallback if rel fails, though it shouldn't - } - - if err := d.queueAdder.AddToQueue(ctx, path, &relPath, nil); err != nil { + if err := d.queueAdder.AddToQueue(ctx, path, &scanPath, nil); err != nil { d.log.ErrorContext(ctx, "Failed to add file to queue during scan", "file", path, "error", err) } diff --git a/internal/importer/service.go b/internal/importer/service.go index d747a4f1..dc17e259 100644 --- a/internal/importer/service.go +++ b/internal/importer/service.go @@ -742,19 +742,6 @@ func (s *Service) processNzbItem(ctx context.Context, item *database.ImportQueue } func (s *Service) calculateProcessVirtualDir(item *database.ImportQueueItem, basePath *string) string { - // Sanitize basePath to strip any absolute path prefix that might have leaked from temp dirs - cleanBase := *basePath - // Strip common temp upload prefixes - tempUploadDir := filepath.Join(os.TempDir(), "altmount-uploads") - if strings.HasPrefix(cleanBase, tempUploadDir) { - cleanBase = strings.TrimPrefix(cleanBase, tempUploadDir) - } - // Ensure we are not dealing with a hardcoded absolute path - if strings.HasPrefix(cleanBase, "/") { - cleanBase = filepath.Base(cleanBase) - } - *basePath = cleanBase - // Calculate initial virtual directory from physical/relative path virtualDir := filesystem.CalculateVirtualDirectory(item.NzbPath, *basePath)