Sambex.HotFolder (sambex v0.3.0)

View Source

A GenServer that monitors an SMB share directory for new files and processes them automatically.

HotFolder implements the "hot folder" pattern common in printing and document processing industries, where files dropped into a monitored directory trigger automated processing workflows.

Features

  • Sequential Processing: Files are processed one at a time to ensure deterministic behavior
  • Flexible Connection Management: Use existing connections or create new ones
  • Rich File Filtering: Filter files by name patterns, size, and MIME types
  • Automatic Folder Management: Creates and manages processing, success, and error folders
  • Robust Error Handling: Retries failed processing with exponential backoff
  • Efficient Polling: Smart polling with backoff to minimize network overhead

Basic Usage

# Simple configuration with direct connection
{:ok, pid} = Sambex.HotFolder.start_link(%{
  url: "smb://server/print-queue",
  username: "printer",
  password: "secret",
  handler: &MyApp.process_print_job/1
})

# Using an existing named connection
{:ok, pid} = Sambex.HotFolder.start_link(%{
  connection: :print_server,
  handler: &MyApp.process_print_job/1
})

Advanced Configuration

config = %Sambex.HotFolder.Config{
  connection: :print_server,
  base_path: "hot-folders/pdf-processor",
  handler: {MyApp.PDFProcessor, :process, [:high_quality]},

  folders: %{
    incoming: "inbox",
    processing: "working",
    success: "completed",
    errors: "failed"
  },

  filters: %{
    name_patterns: [~r/.pdf$/i],
    min_size: 1024,
    max_size: 100_000_000,  # 100MB
    exclude_patterns: [~r/^./, ~r/~$/]
  },

  poll_interval: %{
    initial: 1_000,
    max: 30_000,
    backoff_factor: 2.0
  },

  handler_timeout: 300_000,  # 5 minutes
  max_retries: 5
}

{:ok, pid} = Sambex.HotFolder.start_link(config)

File Processing Workflow

  1. Discovery: Files are discovered in the incoming folder during polling
  2. Filtering: Files are checked against configured filters
  3. Stability Check: Files must have stable size to ensure complete upload
  4. Processing: File is moved to processing folder and handler is called
  5. Success: On success, file is moved to success folder
  6. Error: On failure, file is moved to errors folder with error report

Handler Interface

Handlers receive a file info map and should return {:ok, result} or {:error, reason}:

def process_file(file_info) do
  # file_info contains: %{path: "...", name: "...", size: ...}
  case do_processing(file_info.path) do
    :ok -> {:ok, %{processed_at: DateTime.utc_now()}}
    {:error, reason} -> {:error, reason}
  end
end

Monitoring and Stats

# Get current statistics
Sambex.HotFolder.stats(pid)
# => %{files_processed: 150, files_failed: 3, uptime: 3600, ...}

# Get current status
Sambex.HotFolder.status(pid)
# => :polling | {:processing, "filename.pdf"} | :error

Summary

Functions

Returns a specification to start this module under a supervisor.

Forces an immediate poll for new files.

Starts a HotFolder GenServer.

Returns the current statistics for the HotFolder.

Returns the current status of the HotFolder.

Stops the HotFolder gracefully.

Types

file_info()

@type file_info() :: %{path: String.t(), name: String.t(), size: non_neg_integer()}

stats()

@type stats() :: %{
  files_processed: non_neg_integer(),
  files_failed: non_neg_integer(),
  current_status: atom(),
  uptime: non_neg_integer(),
  last_poll: DateTime.t() | nil,
  current_interval: pos_integer()
}

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

poll_now(server)

@spec poll_now(GenServer.server()) :: :ok | {:error, atom()}

Forces an immediate poll for new files.

Returns :ok if poll was triggered, or {:error, reason} if not possible.

start_link(config, opts \\ [])

@spec start_link(
  Sambex.HotFolder.Config.t() | map(),
  keyword()
) :: GenServer.on_start()

Starts a HotFolder GenServer.

Options

  • config - A Sambex.HotFolder.Config struct or map of configuration options
  • name - Optional name for the GenServer (for registration)

Examples

{:ok, pid} = Sambex.HotFolder.start_link(%{
  url: "smb://server/share",
  username: "user",
  password: "pass",
  handler: &MyApp.process/1
})

{:ok, pid} = Sambex.HotFolder.start_link(config, name: :pdf_processor)

stats(server)

@spec stats(GenServer.server()) :: stats()

Returns the current statistics for the HotFolder.

Examples

stats = Sambex.HotFolder.stats(pid)
# => %{
#   files_processed: 42,
#   files_failed: 3,
#   current_status: :polling,
#   uptime: 3600,
#   last_poll: ~U[2025-01-15 10:30:00Z],
#   current_interval: 5000
# }

status(server)

@spec status(GenServer.server()) :: atom() | {atom(), String.t()}

Returns the current status of the HotFolder.

Possible statuses:

  • :starting - HotFolder is initializing
  • :polling - Actively polling for files
  • {:processing, filename} - Currently processing a file
  • :error - An error has occurred

stop(server, reason \\ :normal)

@spec stop(GenServer.server(), term()) :: :ok

Stops the HotFolder gracefully.

Any file currently being processed will complete before shutdown.