Current section
Files
Jump to
Current section
Files
lib/mix/tasks/rindle.sweep_orphaned_temp_files.ex
defmodule Mix.Tasks.Rindle.SweepOrphanedTempFiles do
@shortdoc "Preview or sweep orphaned AV temp run directories"
@moduledoc """
Sweeps orphaned AV temp run directories under `Rindle.tmp/`.
This is the explicit on-demand operator lane for temp-run-dir residue. It is
separate from upload-session cleanup and shares the same internal sweep
service contract as the scheduled worker path.
## Usage
mix rindle.sweep_orphaned_temp_files [--dry-run | --no-dry-run | --live] [--threshold-sec SECONDS]
## Options
* `--dry-run` — explicitly request preview mode. This is also the default.
* `--no-dry-run` / `--live` — perform destructive deletions.
* `--threshold-sec` — sweep directories older than this many seconds.
Defaults to `14400` (4 hours).
## Exit codes
* `0` — sweep completed successfully, including dry runs and zero-match runs.
* `1` — one or more filesystem errors were encountered while scanning or deleting.
## Examples
# Safe default: preview what would be swept
mix rindle.sweep_orphaned_temp_files
# Same, made explicit
mix rindle.sweep_orphaned_temp_files --dry-run --threshold-sec 14400
# Perform live deletion
mix rindle.sweep_orphaned_temp_files --no-dry-run
## Safety default
The Mix task, direct service call, and scheduled worker all default to
`dry_run: true`. Live deletion requires explicit opt-in here
(`--no-dry-run`/`--live`) and in Oban cron args (`"dry_run" => false`).
"""
use Mix.Task
alias Rindle.Ops.SweepOrphanedTempFiles
@default_threshold_sec 14_400
@failure_output_limit 5
@requirements ["app.start"]
@impl Mix.Task
def run(argv) do
{opts, _args, _invalid} =
OptionParser.parse(argv,
strict: [dry_run: :boolean, live: :boolean, threshold_sec: :integer]
)
dry_run? =
case Keyword.fetch(opts, :dry_run) do
{:ok, value} -> value
:error -> not Keyword.get(opts, :live, false)
end
threshold_sec = Keyword.get(opts, :threshold_sec, @default_threshold_sec)
Mix.shell().info(
"Rindle: sweeping orphaned temp run directories " <>
"(dry_run=#{dry_run?}, threshold_sec=#{threshold_sec})..."
)
report = SweepOrphanedTempFiles.sweep(threshold_sec: threshold_sec, dry_run: dry_run?)
format_report(report)
|> Enum.each(fn line -> Mix.shell().info(line) end)
Mix.shell().info("Done.")
if report.errors > 0 do
Mix.shell().error("#{report.errors} sweep error(s) occurred.")
System.halt(1)
end
end
@doc false
def format_report(report, limit \\ @failure_output_limit) do
summary_lines = [
" run_dirs_scanned: #{report.run_dirs_scanned}",
" orphan_count: #{report.orphan_count}",
" run_dirs_deleted: #{report.run_dirs_deleted}",
" errors: #{report.errors}"
]
summary_lines ++ failure_lines(report, limit)
end
defp failure_lines(%{errors: 0}, _limit), do: []
defp failure_lines(report, limit) do
failures = List.duplicate(sweep_failure_entry(report), min(report.errors, limit))
rendered =
Enum.map(failures, fn failure ->
" [#{failure.failure_class}] #{failure.reason}: #{failure.message}"
end)
remaining = report.errors - length(failures)
if remaining > 0 do
rendered ++ [" ... #{remaining} additional sweep failure(s) omitted"]
else
rendered
end
end
defp sweep_failure_entry(report) do
%{
failure_class: :filesystem,
reason: :scan_or_delete_failed,
message:
"#{report.errors} filesystem operation(s) failed during sweep; inspect logs for per-path detail."
}
end
end