Class: Webhooks::V1::SftpgoController

Inherits:
BaseController
  • Object
show all
Defined in:
app/controllers/webhooks/v1/sftpgo_controller.rb

Overview

Webhook endpoint for SFTPGo's upload action hook — fires the moment the
PBX finishes uploading a call recording to the heatwave-sftp accessory, so
the recording imports in near-real-time instead of waiting for the hourly
poll.

SFTPGo (config/deploy.yml sftp accessory) is configured with
SFTPGO_COMMON__ACTIONS__EXECUTE_ON=upload
SFTPGO_COMMON__ACTIONS__HOOK=https://api.warmlyyours.com/webhooks/v1/sftpgo?token=…
and POSTs a JSON FsActionNotification on every completed upload:

{ "action": "upload", "username": "pbx", "status": 1,
"virtual_path": "/2026-06-19-…-2125551234.xml", "path": "…",
"file_size": 1234, "ip": "…", "timestamp": 1718… }

We trigger on the .xml sidecar, NOT the .wav: Switchvox writes a paired
<name>.wav + <name>.xml, and keying off the metadata file means the
importer always has both halves (if the wav lands fractionally later the
worker's retry covers it). The hook fires for every file/user, so we filter
hard to the pbx recordings user's metadata uploads and ACK everything else.

The derived .wav key is the SAME key form
CallRecordSwitchvoxObjectStore#importable_wav_keys yields, so the enqueued
CallRecordImporterWorker job shares the poll's :until_executed lock —
webhook and poll can never double-import the same recording. The hourly poll
stays in place as the backstop for anything the hook misses (xml-less wavs,
a dropped notification, SFTPGo downtime).

Authentication: shared-secret token query param (fails closed in
production when unconfigured). Test with:
curl -X POST "https://api.warmlyyours.com/webhooks/v1/sftpgo?token=TOKEN"
-H 'Content-Type: application/json'
-d '"action":"upload","username":"pbx","status":1,"virtual_path":"/x"action":"upload","username":"pbx","status":1,"virtual_path":"/x.xml"'

Constant Summary collapse

RECORDINGS_USER =

The SFTPGo user the call-recording importer reads from (the paired
pbx-backup user's system-backup uploads must NOT trigger an import).

'pbx'

Instance Method Summary collapse

Instance Method Details

#createObject

POST /webhooks/v1/sftpgo



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/controllers/webhooks/v1/sftpgo_controller.rb', line 44

def create
  unless valid_request?
    Rails.logger.warn "[Webhooks::V1::Sftpgo] Unauthorized request from #{request.remote_ip}"
    return head :unauthorized
  end

  payload = json_payload
  return head :bad_request if payload.blank?

  wav_key = recordable_wav_key(payload)
  unless wav_key
    # A non-recording upload (a .wav sidecar event, a pbx-backup archive, an
    # archive move, an errored transfer): ACK so SFTPGo doesn't retry.
    Rails.logger.debug { "[Webhooks::V1::Sftpgo] Ignoring #{payload['action']} #{payload['username']}:#{payload['virtual_path'] || payload['path']}" }
    return head :ok
  end

  webhook_log = WebhookLog.ingest!(
    provider: 'sftpgo',
    category: 'recording_uploaded',
    external_id: wav_key,
    data: audit_data(payload, wav_key)
  )
  WebhookProcessorWorker.perform_async(webhook_log.id)

  Rails.logger.info "[Webhooks::V1::Sftpgo] Queued import for #{wav_key} (WebhookLog #{webhook_log.id})"
  head :ok
end