Class: CallRecordSwitchvoxObjectStore

Inherits:
Object
  • Object
show all
Defined in:
app/services/call_record_switchvox_object_store.rb

Overview

R2/S3 object-store adapter for Switchvox call recording files.

Defined Under Namespace

Classes: Error

Constant Summary collapse

ARCHIVE_FOLDERS =
{
  error:     'errors',
  processed: 'processed'
}.freeze
CONFIG_ROOT =
%i[call_recordings object_store].freeze
DELETE_ATTEMPTS =
2
ENV_KEYS =
{
  access_key_id:     'R2_CALL_RECORDINGS_ACCESS_KEY_ID',
  account_id:        'R2_CALL_RECORDINGS_ACCOUNT_ID',
  bucket:            'R2_CALL_RECORDINGS_BUCKET',
  endpoint:          'R2_CALL_RECORDINGS_ENDPOINT',
  prefix:            'R2_CALL_RECORDINGS_PREFIX',
  secret_access_key: 'R2_CALL_RECORDINGS_SECRET_ACCESS_KEY'
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bucket:, prefix:, client:) ⇒ CallRecordSwitchvoxObjectStore

Returns a new instance of CallRecordSwitchvoxObjectStore.



93
94
95
96
97
# File 'app/services/call_record_switchvox_object_store.rb', line 93

def initialize(bucket:, prefix:, client:)
  @bucket = bucket
  @prefix = prefix
  @client = client
end

Instance Attribute Details

#bucketObject (readonly)

Returns the value of attribute bucket.



23
24
25
# File 'app/services/call_record_switchvox_object_store.rb', line 23

def bucket
  @bucket
end

#clientObject (readonly)

Returns the value of attribute client.



23
24
25
# File 'app/services/call_record_switchvox_object_store.rb', line 23

def client
  @client
end

#prefixObject (readonly)

Returns the value of attribute prefix.



23
24
25
# File 'app/services/call_record_switchvox_object_store.rb', line 23

def prefix
  @prefix
end

Class Method Details

.build_client(config) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
# File 'app/services/call_record_switchvox_object_store.rb', line 45

def self.build_client(config)
  Aws::S3::Client.new(
    access_key_id:                 config.fetch(:access_key_id),
    secret_access_key:             config.fetch(:secret_access_key),
    region:                        'auto',
    endpoint:                      config.fetch(:endpoint),
    force_path_style:              true,
    request_checksum_calculation:  'when_required',
    response_checksum_validation:  'when_required'
  )
end

.config_value(key) ⇒ Object



74
75
76
# File 'app/services/call_record_switchvox_object_store.rb', line 74

def self.config_value(key)
  ENV.fetch(ENV_KEYS.fetch(key), nil).presence || Heatwave::Configuration.fetch(*CONFIG_ROOT, key)
end

.configurationObject



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'app/services/call_record_switchvox_object_store.rb', line 57

def self.configuration
  endpoint = config_value(:endpoint)
   = endpoint.present? ? config_value(:account_id) : required_config(:account_id)
  endpoint = endpoint.presence || "https://#{}.r2.cloudflarestorage.com"

  config = {
    access_key_id:     required_config(:access_key_id),
    secret_access_key: required_config(:secret_access_key),
    account_id:        ,
    bucket:            required_config(:bucket),
    endpoint:          endpoint,
    prefix:            configured_prefix
  }
  validate_environment!(config)
  config
end

.configured_prefixString

The bucket prefix the recordings live under (default 'WarmlyYours/'), resolved
the same way configuration resolves it but WITHOUT building a client or
requiring R2 credentials — so a hot path (e.g. the SFTPGo upload webhook) can
map an SFTP virtual_path onto the importer's key form cheaply and safely.

Returns:

  • (String)


40
41
42
43
# File 'app/services/call_record_switchvox_object_store.rb', line 40

def self.configured_prefix
  prefix = config_value(:prefix).presence || 'WarmlyYours/'
  prefix.end_with?('/') ? prefix : "#{prefix}/"
end

.from_configuration(client: nil) ⇒ Object



25
26
27
28
29
30
31
32
33
# File 'app/services/call_record_switchvox_object_store.rb', line 25

def self.from_configuration(client: nil)
  config = configuration

  new(
    bucket: config.fetch(:bucket),
    prefix: config.fetch(:prefix),
    client: client || build_client(config)
  )
end

.required_config(key) ⇒ Object



78
79
80
# File 'app/services/call_record_switchvox_object_store.rb', line 78

def self.required_config(key)
  config_value(key).presence || raise(ArgumentError, "Missing call_recordings.object_store.#{key} configuration")
end

.validate_environment!(config) ⇒ Object

Raises:

  • (ArgumentError)


82
83
84
85
86
87
88
89
90
91
# File 'app/services/call_record_switchvox_object_store.rb', line 82

def self.validate_environment!(config)
  return if Rails.env.local?

  bucket = config.fetch(:bucket).to_s
  expected_environment = Rails.env.to_s
  return if bucket.include?(expected_environment)

  raise ArgumentError,
        "call_recordings.object_store.bucket must be environment-specific for #{expected_environment}; got #{bucket.inspect}"
end

Instance Method Details

#archive_pair(wav_key, xml_key, result) ⇒ Object



135
136
137
138
139
140
141
142
# File 'app/services/call_record_switchvox_object_store.rb', line 135

def archive_pair(wav_key, xml_key, result)
  folder = ARCHIVE_FOLDERS.fetch(result == :error ? :error : :processed)
  move_object(xml_key, archive_key(xml_key, folder)) if object_exists?(xml_key)
  move_object(wav_key, archive_key(wav_key, folder))
rescue StandardError => e
  Rails.logger.error("Failed to archive #{bucket}/#{wav_key} as #{folder}: #{e.message}")
  raise
end

#download_object(key, tmpdir) ⇒ Object



115
116
117
118
119
120
121
122
123
# File 'app/services/call_record_switchvox_object_store.rb', line 115

def download_object(key, tmpdir)
  local_path = File.join(tmpdir, File.basename(key))
  File.open(local_path, 'wb') do |file|
    client.get_object(bucket: bucket, key: key, response_target: file)
  end
  local_path
rescue StandardError => e
  raise Error, "Failed to download #{key} from #{bucket} to #{local_path}: #{e.message}"
end

#importable_wav_keysObject



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'app/services/call_record_switchvox_object_store.rb', line 99

def importable_wav_keys
  keys = []
  continuation_token = nil

  loop do
    response = client.list_objects_v2(**list_params(continuation_token))
    keys.concat(response.contents.filter_map { |object| object.key if importable_wav_key?(object.key) })

    break unless response.is_truncated

    continuation_token = response.next_continuation_token
  end

  keys
end

#object_exists?(key) ⇒ Boolean

Returns:

  • (Boolean)


125
126
127
128
129
130
131
132
133
# File 'app/services/call_record_switchvox_object_store.rb', line 125

def object_exists?(key)
  client.head_object(bucket: bucket, key: key)
  true
rescue Aws::S3::Errors::NotFound
  false
rescue Aws::S3::Errors::Forbidden => e
  Rails.logger.warn("Permission denied checking object existence in #{bucket}/#{key}: #{e.message}")
  raise
end