Skip to content

Commit 5d124c0

Browse files
authored
feat(orphan): add auto cleanup mode (#897)
1 parent aedab87 commit 5d124c0

15 files changed

Lines changed: 489 additions & 261 deletions

File tree

internal/api/handlers/orphan_scan.go

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,13 @@ func (h *OrphanScanHandler) requireLocalAccess(w http.ResponseWriter, r *http.Re
5353

5454
// OrphanScanSettingsPayload is the request body for creating/updating orphan scan settings.
5555
type OrphanScanSettingsPayload struct {
56-
Enabled *bool `json:"enabled"`
57-
GracePeriodMinutes *int `json:"gracePeriodMinutes"`
58-
IgnorePaths []string `json:"ignorePaths"`
59-
ScanIntervalHours *int `json:"scanIntervalHours"`
60-
MaxFilesPerRun *int `json:"maxFilesPerRun"`
56+
Enabled *bool `json:"enabled"`
57+
GracePeriodMinutes *int `json:"gracePeriodMinutes"`
58+
IgnorePaths []string `json:"ignorePaths"`
59+
ScanIntervalHours *int `json:"scanIntervalHours"`
60+
MaxFilesPerRun *int `json:"maxFilesPerRun"`
61+
AutoCleanupEnabled *bool `json:"autoCleanupEnabled"`
62+
AutoCleanupMaxFiles *int `json:"autoCleanupMaxFiles"`
6163
}
6264

6365
// GetSettings returns the orphan scan settings for an instance.
@@ -82,12 +84,14 @@ func (h *OrphanScanHandler) GetSettings(w http.ResponseWriter, r *http.Request)
8284
if settings == nil {
8385
defaults := orphanscan.DefaultSettings()
8486
settings = &models.OrphanScanSettings{
85-
InstanceID: instanceID,
86-
Enabled: defaults.Enabled,
87-
GracePeriodMinutes: defaults.GracePeriodMinutes,
88-
IgnorePaths: defaults.IgnorePaths,
89-
ScanIntervalHours: defaults.ScanIntervalHours,
90-
MaxFilesPerRun: defaults.MaxFilesPerRun,
87+
InstanceID: instanceID,
88+
Enabled: defaults.Enabled,
89+
GracePeriodMinutes: defaults.GracePeriodMinutes,
90+
IgnorePaths: defaults.IgnorePaths,
91+
ScanIntervalHours: defaults.ScanIntervalHours,
92+
MaxFilesPerRun: defaults.MaxFilesPerRun,
93+
AutoCleanupEnabled: defaults.AutoCleanupEnabled,
94+
AutoCleanupMaxFiles: defaults.AutoCleanupMaxFiles,
9195
}
9296
}
9397

@@ -123,12 +127,14 @@ func (h *OrphanScanHandler) UpdateSettings(w http.ResponseWriter, r *http.Reques
123127
if settings == nil {
124128
defaults := orphanscan.DefaultSettings()
125129
settings = &models.OrphanScanSettings{
126-
InstanceID: instanceID,
127-
Enabled: defaults.Enabled,
128-
GracePeriodMinutes: defaults.GracePeriodMinutes,
129-
IgnorePaths: defaults.IgnorePaths,
130-
ScanIntervalHours: defaults.ScanIntervalHours,
131-
MaxFilesPerRun: defaults.MaxFilesPerRun,
130+
InstanceID: instanceID,
131+
Enabled: defaults.Enabled,
132+
GracePeriodMinutes: defaults.GracePeriodMinutes,
133+
IgnorePaths: defaults.IgnorePaths,
134+
ScanIntervalHours: defaults.ScanIntervalHours,
135+
MaxFilesPerRun: defaults.MaxFilesPerRun,
136+
AutoCleanupEnabled: defaults.AutoCleanupEnabled,
137+
AutoCleanupMaxFiles: defaults.AutoCleanupMaxFiles,
132138
}
133139
}
134140

@@ -160,6 +166,16 @@ func (h *OrphanScanHandler) UpdateSettings(w http.ResponseWriter, r *http.Reques
160166
}
161167
settings.MaxFilesPerRun = *payload.MaxFilesPerRun
162168
}
169+
if payload.AutoCleanupEnabled != nil {
170+
settings.AutoCleanupEnabled = *payload.AutoCleanupEnabled
171+
}
172+
if payload.AutoCleanupMaxFiles != nil {
173+
if *payload.AutoCleanupMaxFiles < 1 {
174+
RespondError(w, http.StatusBadRequest, "Auto-cleanup max files threshold must be at least 1")
175+
return
176+
}
177+
settings.AutoCleanupMaxFiles = *payload.AutoCleanupMaxFiles
178+
}
163179

164180
// Validate and normalize ignore paths
165181
if len(settings.IgnorePaths) > 0 {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Copyright (c) 2025, s0up and the autobrr contributors.
2+
-- SPDX-License-Identifier: GPL-2.0-or-later
3+
4+
-- Add auto-cleanup settings to orphan scan
5+
ALTER TABLE orphan_scan_settings ADD COLUMN auto_cleanup_enabled INTEGER NOT NULL DEFAULT 0;
6+
ALTER TABLE orphan_scan_settings ADD COLUMN auto_cleanup_max_files INTEGER NOT NULL DEFAULT 100;

internal/models/orphan_scan.go

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@ import (
1616

1717
// OrphanScanSettings represents orphan scan settings for an instance.
1818
type OrphanScanSettings struct {
19-
ID int64 `json:"id"`
20-
InstanceID int `json:"instanceId"`
21-
Enabled bool `json:"enabled"`
22-
GracePeriodMinutes int `json:"gracePeriodMinutes"`
23-
IgnorePaths []string `json:"ignorePaths"`
24-
ScanIntervalHours int `json:"scanIntervalHours"`
25-
MaxFilesPerRun int `json:"maxFilesPerRun"`
26-
CreatedAt time.Time `json:"createdAt"`
27-
UpdatedAt time.Time `json:"updatedAt"`
19+
ID int64 `json:"id"`
20+
InstanceID int `json:"instanceId"`
21+
Enabled bool `json:"enabled"`
22+
GracePeriodMinutes int `json:"gracePeriodMinutes"`
23+
IgnorePaths []string `json:"ignorePaths"`
24+
ScanIntervalHours int `json:"scanIntervalHours"`
25+
MaxFilesPerRun int `json:"maxFilesPerRun"`
26+
AutoCleanupEnabled bool `json:"autoCleanupEnabled"`
27+
AutoCleanupMaxFiles int `json:"autoCleanupMaxFiles"`
28+
CreatedAt time.Time `json:"createdAt"`
29+
UpdatedAt time.Time `json:"updatedAt"`
2830
}
2931

3032
// OrphanScanRun represents an orphan scan run.
@@ -70,7 +72,8 @@ func NewOrphanScanStore(db dbinterface.Querier) *OrphanScanStore {
7072
func (s *OrphanScanStore) GetSettings(ctx context.Context, instanceID int) (*OrphanScanSettings, error) {
7173
row := s.db.QueryRowContext(ctx, `
7274
SELECT id, instance_id, enabled, grace_period_minutes, ignore_paths,
73-
scan_interval_hours, max_files_per_run, created_at, updated_at
75+
scan_interval_hours, max_files_per_run, auto_cleanup_enabled,
76+
auto_cleanup_max_files, created_at, updated_at
7477
FROM orphan_scan_settings
7578
WHERE instance_id = ?
7679
`, instanceID)
@@ -86,6 +89,8 @@ func (s *OrphanScanStore) GetSettings(ctx context.Context, instanceID int) (*Orp
8689
&ignorePathsJSON,
8790
&settings.ScanIntervalHours,
8891
&settings.MaxFilesPerRun,
92+
&settings.AutoCleanupEnabled,
93+
&settings.AutoCleanupMaxFiles,
8994
&settings.CreatedAt,
9095
&settings.UpdatedAt,
9196
)
@@ -121,16 +126,20 @@ func (s *OrphanScanStore) UpsertSettings(ctx context.Context, settings *OrphanSc
121126

122127
_, err = s.db.ExecContext(ctx, `
123128
INSERT INTO orphan_scan_settings
124-
(instance_id, enabled, grace_period_minutes, ignore_paths, scan_interval_hours, max_files_per_run)
125-
VALUES (?, ?, ?, ?, ?, ?)
129+
(instance_id, enabled, grace_period_minutes, ignore_paths, scan_interval_hours,
130+
max_files_per_run, auto_cleanup_enabled, auto_cleanup_max_files)
131+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
126132
ON CONFLICT(instance_id) DO UPDATE SET
127133
enabled = excluded.enabled,
128134
grace_period_minutes = excluded.grace_period_minutes,
129135
ignore_paths = excluded.ignore_paths,
130136
scan_interval_hours = excluded.scan_interval_hours,
131-
max_files_per_run = excluded.max_files_per_run
137+
max_files_per_run = excluded.max_files_per_run,
138+
auto_cleanup_enabled = excluded.auto_cleanup_enabled,
139+
auto_cleanup_max_files = excluded.auto_cleanup_max_files
132140
`, settings.InstanceID, boolToInt(settings.Enabled), settings.GracePeriodMinutes,
133-
string(ignorePathsJSON), settings.ScanIntervalHours, settings.MaxFilesPerRun)
141+
string(ignorePathsJSON), settings.ScanIntervalHours, settings.MaxFilesPerRun,
142+
boolToInt(settings.AutoCleanupEnabled), settings.AutoCleanupMaxFiles)
134143
if err != nil {
135144
return nil, err
136145
}

internal/services/orphanscan/config.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ func DefaultConfig() Config {
2929
// DefaultSettings returns default settings for a new instance.
3030
func DefaultSettings() Settings {
3131
return Settings{
32-
Enabled: false,
33-
GracePeriodMinutes: 10,
34-
IgnorePaths: []string{},
35-
ScanIntervalHours: 24,
36-
MaxFilesPerRun: 10000,
32+
Enabled: false,
33+
GracePeriodMinutes: 10,
34+
IgnorePaths: []string{},
35+
ScanIntervalHours: 24,
36+
MaxFilesPerRun: 10000,
37+
AutoCleanupEnabled: false,
38+
AutoCleanupMaxFiles: 100,
3739
}
3840
}

internal/services/orphanscan/service.go

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,14 @@ func (s *Service) executeScan(ctx context.Context, instanceID int, runID int64)
317317
if settings == nil {
318318
defaults := DefaultSettings()
319319
settings = &models.OrphanScanSettings{
320-
InstanceID: instanceID,
321-
Enabled: defaults.Enabled,
322-
GracePeriodMinutes: defaults.GracePeriodMinutes,
323-
IgnorePaths: defaults.IgnorePaths,
324-
ScanIntervalHours: defaults.ScanIntervalHours,
325-
MaxFilesPerRun: defaults.MaxFilesPerRun,
320+
InstanceID: instanceID,
321+
Enabled: defaults.Enabled,
322+
GracePeriodMinutes: defaults.GracePeriodMinutes,
323+
IgnorePaths: defaults.IgnorePaths,
324+
ScanIntervalHours: defaults.ScanIntervalHours,
325+
MaxFilesPerRun: defaults.MaxFilesPerRun,
326+
AutoCleanupEnabled: defaults.AutoCleanupEnabled,
327+
AutoCleanupMaxFiles: defaults.AutoCleanupMaxFiles,
326328
}
327329
}
328330

@@ -469,6 +471,57 @@ func (s *Service) executeScan(ctx context.Context, instanceID int, runID int64)
469471
}
470472

471473
log.Info().Int64("run", runID).Int("files", len(allOrphans)).Msg("orphanscan: preview ready")
474+
475+
// Check if auto-cleanup should be triggered for scheduled scans
476+
s.maybeAutoCleanup(ctx, instanceID, runID, settings, len(allOrphans))
477+
}
478+
479+
// maybeAutoCleanup checks if auto-cleanup should be triggered for a scheduled scan.
480+
// Auto-cleanup is only performed when:
481+
// 1. The scan was triggered by the scheduler (not manual)
482+
// 2. AutoCleanupEnabled is true in settings
483+
// 3. The number of files found is <= AutoCleanupMaxFiles threshold
484+
func (s *Service) maybeAutoCleanup(ctx context.Context, instanceID int, runID int64, settings *models.OrphanScanSettings, filesFound int) {
485+
// Get the run to check how it was triggered
486+
run, err := s.store.GetRun(ctx, runID)
487+
if err != nil || run == nil {
488+
log.Error().Err(err).Int64("run", runID).Msg("orphanscan: failed to get run for auto-cleanup check")
489+
return
490+
}
491+
492+
// Only auto-cleanup for scheduled scans (manual scans always show preview)
493+
if run.TriggeredBy != "scheduled" {
494+
return
495+
}
496+
497+
// Check if auto-cleanup is enabled
498+
if settings == nil || !settings.AutoCleanupEnabled {
499+
return
500+
}
501+
502+
// Check file count threshold (safety check for anomalies)
503+
maxFiles := settings.AutoCleanupMaxFiles
504+
if maxFiles <= 0 {
505+
maxFiles = 100 // Default threshold
506+
}
507+
if filesFound > maxFiles {
508+
log.Info().
509+
Int64("run", runID).
510+
Int("filesFound", filesFound).
511+
Int("threshold", maxFiles).
512+
Msg("orphanscan: skipping auto-cleanup (file count exceeds threshold)")
513+
return
514+
}
515+
516+
log.Info().
517+
Int64("run", runID).
518+
Int("filesFound", filesFound).
519+
Msg("orphanscan: triggering auto-cleanup for scheduled scan")
520+
521+
// Trigger deletion - ConfirmDeletion runs in a goroutine
522+
if err := s.ConfirmDeletion(ctx, instanceID, runID); err != nil {
523+
log.Error().Err(err).Int64("run", runID).Msg("orphanscan: auto-cleanup failed to start deletion")
524+
}
472525
}
473526

474527
func (s *Service) executeDeletion(ctx context.Context, instanceID int, runID int64) {

internal/services/orphanscan/types.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,17 @@ type OrphanFile struct {
6767

6868
// Settings represents orphan scan settings for an instance.
6969
type Settings struct {
70-
ID int64
71-
InstanceID int
72-
Enabled bool
73-
GracePeriodMinutes int
74-
IgnorePaths []string
75-
ScanIntervalHours int
76-
MaxFilesPerRun int
77-
CreatedAt time.Time
78-
UpdatedAt time.Time
70+
ID int64
71+
InstanceID int
72+
Enabled bool
73+
GracePeriodMinutes int
74+
IgnorePaths []string
75+
ScanIntervalHours int
76+
MaxFilesPerRun int
77+
AutoCleanupEnabled bool
78+
AutoCleanupMaxFiles int
79+
CreatedAt time.Time
80+
UpdatedAt time.Time
7981
}
8082

8183
// Run represents an orphan scan run.

0 commit comments

Comments
 (0)