dscf-core 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca2cb2e351f124278aa1855d908cb84dea04a3842e1216b8485f6d8a73878667
4
- data.tar.gz: 8563fbf9759f19d08c1699cb35d723cd1ee7cec928e7cbcf5d840880a0404491
3
+ metadata.gz: c7a5e4e4e0fa072a549085c2e512fb502b59d3b7547d9265bbc5e969ff3b5225
4
+ data.tar.gz: 246be4bbcbb6660743154695649c2320998b85c890814d5629cc78899a3b3e30
5
5
  SHA512:
6
- metadata.gz: dbf4de0d1af84c42cd807be0c0ee32d1727f1f7fe726d28648c71b174d3d16d4a1a825fd04bcb8480b8c07a4f1841947c2b2d3ca98cc9f65ee59a56373b910b5
7
- data.tar.gz: 775cb2504183bb0579586918e5a11b25e7dfe0d7120bffd02e17fe391e82cdf5dfadd4a2e98f93aff63be4eea6b50a48d5695562ccd508e92dbe075b0fba9c42
6
+ metadata.gz: c60bf6536fecbfff1826fa883c26abf8d5535d5e9ad8a8df96e2aaad29158b9f4d219737932398bfb23e6ec71c02fe511a9b4ffa981d2a4ef4c171da47e42af5
7
+ data.tar.gz: 45400ff88afccceafcf353327a53c787d1a7bb5ddeee7ab101f63e9f48cc6ce573b00a6232dedfca7b0860508c710cd4facbf18397eb842b9238c1f59b7f5695
@@ -154,13 +154,32 @@ module Dscf
154
154
  def create_retailer_from_signup!(user)
155
155
  return unless defined?(Dscf::Marketplace::Retailer)
156
156
 
157
- Dscf::Marketplace::Retailer.create!(
157
+ retailer = Dscf::Marketplace::Retailer.create!(
158
158
  name: retailer_signup_params[:name],
159
159
  phone: retailer_signup_params[:phone],
160
160
  tin_number: retailer_signup_params[:tin_number],
161
161
  location: retailer_signup_params[:location],
162
162
  user: user
163
163
  )
164
+
165
+ if retailer.location.present? && defined?(Dscf::Core::Address)
166
+ lat_str, lng_str = retailer.location.split(",")
167
+ lat = lat_str.to_f
168
+ lng = lng_str.to_f
169
+
170
+ if lat != 0.0 && lng != 0.0
171
+ Dscf::Core::Address.create!(
172
+ user: user,
173
+ address_type: :shipping,
174
+ country: "Ethiopia",
175
+ city: "Addis Ababa",
176
+ latitude: lat,
177
+ longitude: lng
178
+ )
179
+ end
180
+ end
181
+ rescue => e
182
+ logger.warn "Failed to auto-create address from retailer onboarding location: #{e.message}"
164
183
  end
165
184
 
166
185
  def agent_signup_params
@@ -1,79 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "aws-sdk-s3"
4
+ require "securerandom"
5
+
3
6
  module Dscf
4
7
  module Core
5
8
  module FileStorage
6
- # Low-level HTTP client for MinIO service
7
- # This is an internal class - other engines should NOT use this directly
9
+ # Low-level S3 client for object storage (Garage / MinIO / any S3-compatible service).
10
+ # This is an internal class - other engines should NOT use this directly; go through
11
+ # FileStorage, Uploader, or the Attachable concern instead.
12
+ #
13
+ # Configuration is read from the environment:
14
+ # S3_ENDPOINT_URL - e.g. http://garage:3900 (required)
15
+ # S3_BUCKET - bucket name (required)
16
+ # S3_ACCESS_KEY_ID - access key (required)
17
+ # S3_SECRET_ACCESS_KEY - secret key (required)
18
+ # S3_REGION - region (optional, defaults to "garage")
19
+ #
20
+ # File keys are flat UUIDs (no "/") so they stay valid for the
21
+ # `files/:file_key` route constraint (`/[^\/]+/`).
8
22
  class Client
9
- include HTTParty
10
-
11
23
  class UploadError < StandardError; end
12
24
  class DownloadError < StandardError; end
13
25
  class ConfigurationError < StandardError; end
14
26
 
27
+ DEFAULT_REGION = "garage"
28
+ DEFAULT_CONTENT_TYPE = "application/octet-stream"
29
+
30
+ # @param app_name [String, nil] retained for backwards compatibility (unused)
31
+ # @param record_class [Class, nil] retained for backwards compatibility (unused)
15
32
  def initialize(app_name: nil, record_class: nil)
16
- @base_uri = ENV.fetch("MINIO_SERVICE_URL", "http://localhost:3001")
17
- @record_class = record_class
18
- @api_key = resolve_api_key(app_name)
33
+ @endpoint = ENV["S3_ENDPOINT_URL"].presence
34
+ @bucket = ENV["S3_BUCKET"].presence
35
+ @access_key_id = ENV["S3_ACCESS_KEY_ID"].presence
36
+ @secret_access_key = ENV["S3_SECRET_ACCESS_KEY"].presence
37
+ @region = ENV.fetch("S3_REGION", DEFAULT_REGION)
19
38
  validate_configuration!
20
39
  end
21
40
 
22
- # Upload a file to MinIO
23
- # @param file [ActionDispatch::Http::UploadedFile, File, IO] the file to upload
41
+ # Upload a file to object storage.
42
+ # @param file [ActionDispatch::Http::UploadedFile, File, Hash] the file to upload
24
43
  # @param filename [String] optional custom filename
25
44
  # @return [Hash] { file_key:, filename:, size:, content_type: }
26
45
  def upload(file, filename: nil)
27
46
  prepared = prepare_file(file, filename)
28
-
29
- response = self.class.post(
30
- "#{@base_uri}/upload",
31
- headers: auth_headers,
32
- body: {file: prepared[:io]},
33
- multipart: true,
34
- timeout: 60
47
+ file_key = generate_file_key(prepared[:filename])
48
+ io = prepared[:io]
49
+ io.rewind if io.respond_to?(:rewind)
50
+
51
+ s3.put_object(
52
+ bucket: @bucket,
53
+ key: file_key,
54
+ body: io,
55
+ content_type: prepared[:content_type] || DEFAULT_CONTENT_TYPE,
56
+ metadata: {"filename" => prepared[:filename].to_s}
35
57
  )
36
58
 
37
- handle_upload_response(response, prepared)
59
+ {
60
+ file_key: file_key,
61
+ filename: prepared[:filename],
62
+ size: prepared[:size],
63
+ content_type: prepared[:content_type]
64
+ }
65
+ rescue Aws::Errors::ServiceError => e
66
+ raise UploadError, "Upload failed: #{e.message}"
38
67
  end
39
68
 
40
- # Download a file from MinIO
41
- # @param file_key [String] the file key/name in MinIO
69
+ # Download a file from object storage.
70
+ # @param file_key [String] the object key
42
71
  # @return [Hash] { data:, filename:, content_type: }
43
72
  def download(file_key)
44
- response = self.class.get(
45
- "#{@base_uri}/download/#{URI.encode_www_form_component(file_key)}",
46
- headers: auth_headers,
47
- timeout: 60
48
- )
73
+ response = s3.get_object(bucket: @bucket, key: file_key)
49
74
 
50
- handle_download_response(response, file_key)
75
+ {
76
+ data: response.body.read,
77
+ filename: response.metadata["filename"].presence || file_key,
78
+ content_type: response.content_type.presence || DEFAULT_CONTENT_TYPE
79
+ }
80
+ rescue Aws::S3::Errors::NoSuchKey => e
81
+ raise DownloadError, "File not found: #{file_key} (#{e.message})"
82
+ rescue Aws::Errors::ServiceError => e
83
+ raise DownloadError, "Download failed: #{e.message}"
51
84
  end
52
85
 
53
- # Check if file exists
54
- # @param file_key [String] the file key/name in MinIO
86
+ # Check if an object exists.
87
+ # @param file_key [String] the object key
55
88
  # @return [Boolean]
56
89
  def exists?(file_key)
57
- response = self.class.head(
58
- "#{@base_uri}/download/#{URI.encode_www_form_component(file_key)}",
59
- headers: auth_headers,
60
- timeout: 10
61
- )
62
- response.success?
90
+ s3.head_object(bucket: @bucket, key: file_key)
91
+ true
92
+ rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::NoSuchKey
93
+ false
63
94
  rescue StandardError
64
95
  false
65
96
  end
66
97
 
67
- # Delete a file from MinIO
68
- # @param file_key [String] the file key/name in MinIO
98
+ # Delete an object.
99
+ # @param file_key [String] the object key
69
100
  # @return [Boolean]
70
101
  def delete(file_key)
71
- response = self.class.delete(
72
- "#{@base_uri}/delete/#{URI.encode_www_form_component(file_key)}",
73
- headers: auth_headers,
74
- timeout: 30
75
- )
76
- response.success?
102
+ s3.delete_object(bucket: @bucket, key: file_key)
103
+ true
77
104
  rescue StandardError => e
78
105
  Rails.logger.error("FileStorage::Client delete error: #{e.message}")
79
106
  false
@@ -81,60 +108,32 @@ module Dscf
81
108
 
82
109
  private
83
110
 
84
- def resolve_api_key(app_name)
85
- # Priority: explicit app name -> engine-specific -> default
86
- key_name = app_name&.upcase&.gsub("-", "_") || detect_engine_name
87
-
88
- # Check all possible env var keys
89
- possible_keys = [
90
- "MINIO_API_KEY_DSCF_#{key_name}",
91
- "MINIO_API_KEY_#{key_name}",
92
- "MINIO_API_KEY"
93
- ]
94
-
95
- api_key = possible_keys.find { |k| ENV[k].present? }.then { |k| ENV[k] }
96
-
97
- Rails.logger.debug("MinIO API Key lookup: key_name=#{key_name}, tried=#{possible_keys.inspect}, found=#{api_key.present?}")
98
- Rails.logger.debug("Available MINIO env vars: #{ENV.select { |k, _v| k.start_with?('MINIO_') }.keys.inspect}")
99
-
100
- api_key
101
- end
102
-
103
- def detect_engine_name
104
- # First try to detect from record class namespace
105
- if @record_class
106
- namespace = @record_class.name.split("::")[1] # Dscf::Credit::Bank -> Credit
107
- if namespace && namespace != "Core"
108
- Rails.logger.debug("MinIO engine detection: from record_class=#{@record_class.name}, detected=#{namespace.upcase}")
109
- return namespace.upcase
110
- end
111
- end
112
-
113
- # Fallback: Auto-detect engine based on caller (skip dscf_core since storage is in core)
114
- caller_paths = caller.select { |c| c.include?("/dscf_") && !c.include?("/dscf_core/") }
115
-
116
- caller_path = caller_paths.first
117
- return "CORE" unless caller_path
118
-
119
- match = caller_path.match(%r{/dscf_(\w+)/})
120
- detected = match ? match[1].upcase : "CORE"
121
-
122
- Rails.logger.debug("MinIO engine detection: caller_path=#{caller_path}, detected=#{detected}")
123
- detected
111
+ def s3
112
+ @s3 ||= Aws::S3::Client.new(
113
+ endpoint: @endpoint,
114
+ region: @region,
115
+ access_key_id: @access_key_id,
116
+ secret_access_key: @secret_access_key,
117
+ force_path_style: true
118
+ )
124
119
  end
125
120
 
126
121
  def validate_configuration!
127
- raise ConfigurationError, "MINIO_SERVICE_URL is not configured" if @base_uri.blank?
122
+ missing = []
123
+ missing << "S3_ENDPOINT_URL" if @endpoint.blank?
124
+ missing << "S3_BUCKET" if @bucket.blank?
125
+ missing << "S3_ACCESS_KEY_ID" if @access_key_id.blank?
126
+ missing << "S3_SECRET_ACCESS_KEY" if @secret_access_key.blank?
128
127
 
129
- return unless @api_key.blank?
128
+ return if missing.empty?
130
129
 
131
- detected = detect_engine_name
132
- raise ConfigurationError,
133
- "MINIO_API_KEY is not configured. Looked for: MINIO_API_KEY_DSCF_#{detected}, MINIO_API_KEY_#{detected}, MINIO_API_KEY"
130
+ raise ConfigurationError, "Object storage is not configured. Missing: #{missing.join(', ')}"
134
131
  end
135
132
 
136
- def auth_headers
137
- {"x-api-key" => @api_key}
133
+ # Flat, unique, slash-free key (kept compatible with the files/:file_key route).
134
+ def generate_file_key(filename)
135
+ ext = filename ? File.extname(filename.to_s).downcase : ""
136
+ "#{SecureRandom.uuid}#{ext}"
138
137
  end
139
138
 
140
139
  def prepare_file(file, custom_filename)
@@ -166,44 +165,6 @@ module Dscf
166
165
  raise ArgumentError, "Unsupported file type: #{file.class}"
167
166
  end
168
167
  end
169
-
170
- def handle_upload_response(response, prepared)
171
- unless response.success?
172
- error_message = response.parsed_response&.dig("message") || "Upload failed"
173
- raise UploadError, "#{error_message} (HTTP #{response.code})"
174
- end
175
-
176
- body = response.parsed_response
177
- {
178
- file_key: body["fileName"],
179
- filename: prepared[:filename],
180
- size: body["size"] || prepared[:size],
181
- content_type: body["mimeType"] || prepared[:content_type]
182
- }
183
- end
184
-
185
- def handle_download_response(response, file_key)
186
- unless response.success?
187
- error_message = response.parsed_response&.dig("message") || "Download failed"
188
- raise DownloadError, "#{error_message} (HTTP #{response.code})"
189
- end
190
-
191
- content_disposition = response.headers["content-disposition"]
192
- filename = extract_filename(content_disposition) || file_key
193
-
194
- {
195
- data: response.body,
196
- filename: filename,
197
- content_type: response.headers["content-type"] || "application/octet-stream"
198
- }
199
- end
200
-
201
- def extract_filename(content_disposition)
202
- return nil unless content_disposition
203
-
204
- match = content_disposition.match(/filename="?([^";\s]+)"?/)
205
- match&.[](1)
206
- end
207
168
  end
208
169
  end
209
170
  end
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Core
3
- VERSION = "0.3.8".freeze
3
+ VERSION = "0.3.9".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dscf-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 0.3.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: 4.1.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: aws-sdk-s3
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: bcrypt
56
70
  requirement: !ruby/object:Gem::Requirement