Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions internal/api/arrs_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,17 +337,23 @@ func (s *Server) handleArrsWebhook(c *fiber.Ctx) error {
}

// Redundant Deletion Guard: ensure the file is gone from the local mount
if s.configManager != nil {
if s.configManager != nil && metadataPath != "" && metadataPath != "." && metadataPath != "/" {
cfg := s.configManager.GetConfig()
if cfg.MountPath != "" {
localPath := filepath.Join(cfg.MountPath, metadataPath)

// HARD SAFETY: Never delete the mount root or critical system paths
cleanLocal := filepath.Clean(localPath)
if cleanLocal == "/" || cleanLocal == "." || cleanLocal == filepath.Clean(cfg.MountPath) {
slog.WarnContext(c.Context(), "Safety Guard: Blocked attempt to delete root mount path", "path", cleanLocal)
continue
}
if _, err := os.Stat(localPath); err == nil {
slog.InfoContext(c.Context(), "Redundant Deletion Guard: Manual removal of ghost file from mount", "path", localPath)
_ = os.Remove(localPath)
}
}
}

}

// Process Directory Deletions
Expand Down
3 changes: 2 additions & 1 deletion internal/api/nzb_stremio_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/gofiber/fiber/v2"
"github.com/javi11/altmount/internal/database"
"github.com/javi11/altmount/internal/importer/utils/nzbtrim"
)

// mediaExtensions lists common video/media file extensions for Stremio stream filtering.
Expand Down Expand Up @@ -120,7 +121,7 @@ func (s *Server) handleNzbStreams(c *fiber.Ctx) error {
// --- Derive stable names before touching the filesystem ---
uploadDir := filepath.Join(os.TempDir(), "altmount-uploads")
safeFilename := filepath.Base(file.Filename)
nzbName := strings.TrimSuffix(safeFilename, filepath.Ext(safeFilename))
nzbName := nzbtrim.TrimNzbExtension(safeFilename)
tempPath := filepath.Join(uploadDir, safeFilename)

// --- Short-circuit: return cached streams if NZB was already processed ---
Expand Down
40 changes: 27 additions & 13 deletions internal/arrs/instances/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,33 @@ func (m *Manager) RegisterInstance(ctx context.Context, arrURL, apiKey string) (

// Determine category based on ARR type
var category string
switch arrType {
case "radarr":
category = "movies"
case "sonarr":
category = "tv"
case "lidarr":
category = "music"
case "readarr":
category = "books"
case "whisparr":
category = "movies"
default:
return false, fmt.Errorf("unsupported ARR type: %s", arrType)

// Check if instance already exists in config to respect pre-configured category
existingInstances := m.GetAllInstances()
found := false
for _, inst := range existingInstances {
if normalizeURL(inst.URL) == normalizeURL(arrURL) {
category = inst.Category
found = true
break
}
}

if !found {
switch arrType {
case "radarr":
category = "movies"
case "sonarr":
category = "tv"
case "lidarr":
category = "music"
case "readarr":
category = "books"
case "whisparr":
category = "movies"
default:
return false, fmt.Errorf("unsupported ARR type: %s", arrType)
}
}

// Generate instance name
Expand Down
16 changes: 8 additions & 8 deletions internal/arrs/registrar/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ func (m *Manager) EnsureDownloadClientRegistration(ctx context.Context, altmount
slog.InfoContext(ctx, "Updating Radarr download client API key/Host", "instance", instance.Name)
category := instance.Category
if category == "" {
category = "movies"
slog.WarnContext(ctx, "No category found in configuration for instance, using empty string", "instance", instance.Name)
}
dc := &radarr.DownloadClientInput{
ID: existing.ID,
Expand All @@ -442,13 +442,13 @@ func (m *Manager) EnsureDownloadClientRegistration(ctx context.Context, altmount
}
_, err := client.UpdateDownloadClientContext(ctx, dc, true)
if err != nil {
slog.ErrorContext(ctx, "Failed to update Radarr download client", "instance", instance.Name, "error", err)
slog.ErrorContext(ctx, "Failed to update download client", "instance", instance.Name, "error", err)
}
}
} else {
category := instance.Category
if category == "" {
category = "movies"
slog.WarnContext(ctx, "No category found in configuration for instance, using empty string", "instance", instance.Name)
}
dc := &radarr.DownloadClientInput{
Name: clientName,
Expand All @@ -470,9 +470,9 @@ func (m *Manager) EnsureDownloadClientRegistration(ctx context.Context, altmount
}
_, err := client.AddDownloadClientContext(ctx, dc)
if err != nil {
slog.ErrorContext(ctx, "Failed to add Radarr download client", "instance", instance.Name, "error", err)
slog.ErrorContext(ctx, "Failed to add download client to "+instance.Type, "instance", instance.Name, "error", err)
} else {
slog.InfoContext(ctx, "Added AltMount download client to Radarr", "instance", instance.Name)
slog.InfoContext(ctx, "Added AltMount download client to "+instance.Type, "instance", instance.Name, "category", category)
}
}

Expand Down Expand Up @@ -514,7 +514,7 @@ func (m *Manager) EnsureDownloadClientRegistration(ctx context.Context, altmount
slog.InfoContext(ctx, "Updating Sonarr download client API key/Host", "instance", instance.Name)
category := instance.Category
if category == "" {
category = "tv"
slog.WarnContext(ctx, "No category found in configuration for instance, using empty string", "instance", instance.Name)
}
dc := &sonarr.DownloadClientInput{
ID: existing.ID,
Expand All @@ -537,13 +537,13 @@ func (m *Manager) EnsureDownloadClientRegistration(ctx context.Context, altmount
}
_, err := client.UpdateDownloadClientContext(ctx, dc, true)
if err != nil {
slog.ErrorContext(ctx, "Failed to update Sonarr download client", "instance", instance.Name, "error", err)
slog.ErrorContext(ctx, "Failed to update download client", "instance", instance.Name, "error", err)
}
}
} else {
category := instance.Category
if category == "" {
category = "tv"
slog.WarnContext(ctx, "No category found in configuration for instance, using empty string", "instance", instance.Name)
}
dc := &sonarr.DownloadClientInput{
Name: clientName,
Expand Down
2 changes: 1 addition & 1 deletion internal/database/health_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func (r *HealthRepository) GetUnhealthyFiles(ctx context.Context, limit int, str
LIMIT ?
`

// Build the library directory prefix filter (e.g. /mnt/usenet-rclone/%)
// Build the library directory prefix filter (e.g. /my/library/path/%)
libraryPrefix := libraryDir
if !strings.HasSuffix(libraryPrefix, "/") {
libraryPrefix += "/"
Expand Down
37 changes: 37 additions & 0 deletions internal/health/library_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,22 @@ func (lsw *LibrarySyncWorker) SyncLibrary(ctx context.Context, dryRun bool) *Dry
}
}

// HARD SAFETY: Never delete the entire mount root or library root
cleanFile := filepath.Clean(file)
cleanMount := ""
if cfg.MountPath != "" {
cleanMount = filepath.Clean(cfg.MountPath)
}
cleanLibDir := ""
if cfg.Health.LibraryDir != nil && *cfg.Health.LibraryDir != "" {
cleanLibDir = filepath.Clean(*cfg.Health.LibraryDir)
}

if cleanFile == cleanMount || cleanFile == cleanLibDir || cleanFile == "/" || cleanFile == "." {
slog.WarnContext(ctx, "Nuclear Guard: Blocked attempt to delete protected path in sync", "path", cleanFile)
continue
}

err = os.Remove(file)
if err != nil {
if !os.IsNotExist(err) {
Expand Down Expand Up @@ -1148,6 +1164,12 @@ func updateSymlinkForMountChange(
// Create new target path
newTarget := filepath.Join(newMountPath, relativePath)

// HARD SAFETY: Never delete protected paths
cleanSymlink := filepath.Clean(symlinkPath)
if cleanSymlink == "/" || cleanSymlink == "." {
return currentTarget, false, fmt.Errorf("safety block: refusing to remove protected symlink path: %s", cleanSymlink)
}

// Remove old symlink
if err := os.Remove(symlinkPath); err != nil {
slog.WarnContext(ctx, "Failed to remove old symlink during mount path update",
Expand Down Expand Up @@ -1579,6 +1601,21 @@ func (lsw *LibrarySyncWorker) removeEmptyDirectories(ctx context.Context) (int,
default:
}

// HARD SAFETY: Never delete protected paths
cleanDir := filepath.Clean(dir)
cleanMount := ""
if cfg.MountPath != "" {
cleanMount = filepath.Clean(cfg.MountPath)
}
cleanLibDir := ""
if cfg.Health.LibraryDir != nil && *cfg.Health.LibraryDir != "" {
cleanLibDir = filepath.Clean(*cfg.Health.LibraryDir)
}

if cleanDir == cleanMount || cleanDir == cleanLibDir || cleanDir == "/" || cleanDir == "." {
continue
}

// Try to remove the directory (will fail if not empty)
if err := os.Remove(dir); err != nil {
continue
Expand Down
5 changes: 3 additions & 2 deletions internal/importer/archive/rar/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/javi11/altmount/internal/importer/filesystem"
"github.com/javi11/altmount/internal/importer/parser"
"github.com/javi11/altmount/internal/importer/utils"
"github.com/javi11/altmount/internal/importer/utils/nzbtrim"
"github.com/javi11/altmount/internal/importer/validation"
"github.com/javi11/altmount/internal/metadata"
metapb "github.com/javi11/altmount/internal/metadata/proto"
Expand Down Expand Up @@ -240,7 +241,7 @@ func ProcessArchive(ctx context.Context, opts ProcessArchiveOptions) error {
}

nzbName := filepath.Base(nzbPath)
releaseName := strings.TrimSuffix(nzbName, filepath.Ext(nzbName))
releaseName := nzbtrim.TrimNzbExtension(nzbName)
shouldNormalizeName := renameToNzbName && mediaFilesCount == 1

// Count ISO-expanded files so single-file ISOs omit the index suffix.
Expand Down Expand Up @@ -570,7 +571,7 @@ func GroupArchivesByBaseName(files []parser.ParsedFile) [][]parser.ParsedFile {

// normalizeArchiveReleaseFilename aligns the filename to the NZB basename while keeping the original extension.
func normalizeArchiveReleaseFilename(nzbFilename, originalFilename string) string {
releaseName := strings.TrimSuffix(nzbFilename, filepath.Ext(nzbFilename))
releaseName := nzbtrim.TrimNzbExtension(nzbFilename)
fileExt := filepath.Ext(originalFilename)

if fileExt == "" {
Expand Down
5 changes: 3 additions & 2 deletions internal/importer/archive/sevenzip/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/javi11/altmount/internal/importer/filesystem"
"github.com/javi11/altmount/internal/importer/parser"
"github.com/javi11/altmount/internal/importer/utils"
"github.com/javi11/altmount/internal/importer/utils/nzbtrim"
"github.com/javi11/altmount/internal/importer/validation"
"github.com/javi11/altmount/internal/metadata"
metapb "github.com/javi11/altmount/internal/metadata/proto"
Expand Down Expand Up @@ -217,7 +218,7 @@ func ProcessArchive(ctx context.Context, opts ProcessArchiveOptions) error {
}

nzbName := filepath.Base(nzbPath)
releaseName := strings.TrimSuffix(nzbName, filepath.Ext(nzbName))
releaseName := nzbtrim.TrimNzbExtension(nzbName)
shouldNormalizeName := renameToNzbName && mediaFilesCount == 1

// Count ISO-expanded files so single-file ISOs omit the index suffix.
Expand Down Expand Up @@ -521,7 +522,7 @@ func expandISOContents(

// normalizeArchiveReleaseFilename aligns the filename to the NZB basename while keeping the original extension.
func normalizeArchiveReleaseFilename(nzbFilename, originalFilename string) string {
releaseName := strings.TrimSuffix(nzbFilename, filepath.Ext(nzbFilename))
releaseName := nzbtrim.TrimNzbExtension(nzbFilename)
fileExt := filepath.Ext(originalFilename)

if fileExt == "" {
Expand Down
3 changes: 2 additions & 1 deletion internal/importer/filesystem/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/javi11/altmount/internal/importer/parser"
"github.com/javi11/altmount/internal/importer/utils/nzbtrim"
"github.com/javi11/altmount/internal/metadata"
)

Expand Down Expand Up @@ -117,7 +118,7 @@ func EnsureDirectoryExists(virtualDir string, metadataService *metadata.Metadata

// CreateNzbFolder creates a folder named after the NZB file
func CreateNzbFolder(virtualDir, nzbFilename string, metadataService *metadata.MetadataService) (string, error) {
nzbBaseName := strings.TrimSuffix(nzbFilename, filepath.Ext(nzbFilename))
nzbBaseName := nzbtrim.TrimNzbExtension(nzbFilename)
// Now, also strip the media file extension if it exists
// Common media extensions: .mkv, .mp4, .avi, .flv, .wmv, .mov, .webm
// This is not exhaustive, but covers common cases.
Expand Down
3 changes: 2 additions & 1 deletion internal/importer/parser/fileinfo/fileinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/javi11/altmount/internal/importer/parser/par2"
"github.com/javi11/altmount/internal/importer/utils/nzbtrim"
)

var (
Expand All @@ -24,7 +25,7 @@ func GetFileInfos(
nzbFilename string,
) []*FileInfo {
// Strip .nzb extension for use as last-resort filename stem
nzbStem := strings.TrimSuffix(nzbFilename, filepath.Ext(nzbFilename))
nzbStem := nzbtrim.TrimNzbExtension(nzbFilename)

fileInfos := make([]*FileInfo, 0, len(files))
for _, file := range files {
Expand Down
3 changes: 2 additions & 1 deletion internal/importer/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/javi11/altmount/internal/errors"
"github.com/javi11/altmount/internal/importer/parser/fileinfo"
"github.com/javi11/altmount/internal/importer/parser/par2"
"github.com/javi11/altmount/internal/importer/utils/nzbtrim"
metapb "github.com/javi11/altmount/internal/metadata/proto"
"github.com/javi11/altmount/internal/pool"
"github.com/javi11/altmount/internal/progress"
Expand Down Expand Up @@ -354,7 +355,7 @@ func (p *Parser) parseFile(ctx context.Context, meta map[string]string, nzbFilen
if metaFilename, ok := meta["file_name"]; ok && metaFilename != "" {
if fSize, ok := meta["file_size"]; ok {
// This is a usenet-drive nzb with one file
metaFilename = strings.TrimSuffix(nzbFilename, filepath.Ext(nzbFilename))
metaFilename = nzbtrim.TrimNzbExtension(nzbFilename)

if fe, ok := meta["file_extension"]; ok {
metaFilename = metaFilename + fe
Expand Down
5 changes: 3 additions & 2 deletions internal/importer/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/javi11/altmount/internal/importer/multifile"
"github.com/javi11/altmount/internal/importer/parser"
"github.com/javi11/altmount/internal/importer/singlefile"
"github.com/javi11/altmount/internal/importer/utils/nzbtrim"
"github.com/javi11/altmount/internal/metadata"
"github.com/javi11/altmount/internal/pool"
"github.com/javi11/altmount/internal/progress"
Expand Down Expand Up @@ -330,7 +331,7 @@ func (proc *Processor) processSingleFile(
// Normalize virtualDir only for synthetic duplicate folders; skip if the NZB actually lives inside a
// real directory named like the release (e.g. .../Season 01/<file>/<file>.nzb).
nzbName := proc.getCleanNzbName(nzbPath, queueID)
releaseName := strings.TrimSuffix(nzbName, filepath.Ext(nzbName))
releaseName := nzbtrim.TrimNzbExtension(nzbName)
nzbDirBase := filepath.Base(filepath.Dir(nzbPath))
fileDir := filepath.Dir(regularFiles[0].Filename)
if fileDir == "." || fileDir == "" {
Expand Down Expand Up @@ -816,7 +817,7 @@ func applyNzbRename(renameToNzbName bool, nzbName string, files []parser.ParsedF
// normalizeReleaseFilename aligns the filename to the NZB basename while keeping the original extension.
// It avoids generating duplicate extensions like ".mp4.mp4" when the NZB name already contains the suffix.
func normalizeReleaseFilename(nzbFilename, originalFilename string) string {
releaseName := strings.TrimSuffix(nzbFilename, filepath.Ext(nzbFilename))
releaseName := nzbtrim.TrimNzbExtension(nzbFilename)
fileExt := filepath.Ext(originalFilename)

if fileExt == "" {
Expand Down
7 changes: 4 additions & 3 deletions internal/importer/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/javi11/altmount/internal/importer/postprocessor"
"github.com/javi11/altmount/internal/importer/queue"
"github.com/javi11/altmount/internal/importer/scanner"
"github.com/javi11/altmount/internal/importer/utils/nzbtrim"
"github.com/javi11/altmount/internal/metadata"
"github.com/javi11/altmount/internal/pool"
"github.com/javi11/altmount/internal/progress"
Expand Down Expand Up @@ -119,7 +120,7 @@ func isFileAlreadyProcessed(metadataService *metadata.MetadataService, filePath

// Normalize filename (remove .nzb extension)
fileName := filepath.Base(filePath)
baseName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
baseName := nzbtrim.TrimNzbExtension(fileName)

// Check if a directory exists with the release name
releaseDir := filepath.Join(virtualPath, baseName)
Expand Down Expand Up @@ -850,7 +851,7 @@ func (s *Service) ensurePersistentNzb(ctx context.Context, item *database.Import
filename := filepath.Base(item.NzbPath)
newFilename := sanitizeFilename(filename)
if !strings.HasSuffix(strings.ToLower(newFilename), nzbGzExtension) {
newFilename = strings.TrimSuffix(newFilename, filepath.Ext(newFilename)) + nzbGzExtension
newFilename = nzbtrim.TrimNzbExtension(newFilename) + nzbGzExtension
}
newPath := filepath.Join(nzbDir, newFilename)

Expand Down Expand Up @@ -1339,7 +1340,7 @@ func (s *Service) RegenerateMetadata(ctx context.Context, mountRelativePath stri
if releaseName == "" {
// Fallback: use the filename without extension
releaseName = filepath.Base(mountRelativePath)
releaseName = strings.TrimSuffix(releaseName, filepath.Ext(releaseName))
releaseName = nzbtrim.TrimNzbExtension(releaseName)
}

s.log.InfoContext(ctx, "Attempting to regenerate metadata", "path", mountRelativePath, "release_name", releaseName)
Expand Down
Loading
Loading