Class: CallRecordSwitchvoxObjectStore
- Inherits:
-
Object
- Object
- CallRecordSwitchvoxObjectStore
- 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
-
#bucket ⇒ Object
readonly
Returns the value of attribute bucket.
-
#client ⇒ Object
readonly
Returns the value of attribute client.
-
#prefix ⇒ Object
readonly
Returns the value of attribute prefix.
Class Method Summary collapse
- .build_client(config) ⇒ Object
- .config_value(key) ⇒ Object
- .configuration ⇒ Object
-
.configured_prefix ⇒ String
The bucket prefix the recordings live under (default 'WarmlyYours/'), resolved the same way CallRecordSwitchvoxObjectStore.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_pathonto the importer's key form cheaply and safely. - .from_configuration(client: nil) ⇒ Object
- .required_config(key) ⇒ Object
- .validate_environment!(config) ⇒ Object
Instance Method Summary collapse
- #archive_pair(wav_key, xml_key, result) ⇒ Object
- #download_object(key, tmpdir) ⇒ Object
- #importable_wav_keys ⇒ Object
-
#initialize(bucket:, prefix:, client:) ⇒ CallRecordSwitchvoxObjectStore
constructor
A new instance of CallRecordSwitchvoxObjectStore.
- #object_exists?(key) ⇒ Boolean
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
#bucket ⇒ Object (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 |
#client ⇒ Object (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 |
#prefix ⇒ Object (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 |
.configuration ⇒ Object
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) account_id = endpoint.present? ? config_value(:account_id) : required_config(:account_id) endpoint = endpoint.presence || "https://#{account_id}.r2.cloudflarestorage.com" config = { access_key_id: required_config(:access_key_id), secret_access_key: required_config(:secret_access_key), account_id: account_id, bucket: required_config(:bucket), endpoint: endpoint, prefix: configured_prefix } validate_environment!(config) config end |
.configured_prefix ⇒ String
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.
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
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.}") 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.}" end |
#importable_wav_keys ⇒ Object
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
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.}") raise end |