Class: Dscf::Core::FileStorage::Client

Inherits:
Object
  • Object
show all
Defined in:
app/services/dscf/core/file_storage/client.rb

Overview

Low-level S3 client for object storage (Garage / MinIO / any S3-compatible service). This is an internal class - other engines should NOT use this directly; go through FileStorage, Uploader, or the Attachable concern instead.

Configuration is read from the environment:

S3_ENDPOINT_URL       - e.g. http://garage:3900 (required)
S3_BUCKET             - bucket name (required)
S3_ACCESS_KEY_ID      - access key (required)
S3_SECRET_ACCESS_KEY  - secret key (required)
S3_REGION             - region (optional, defaults to "garage")

File keys are flat UUIDs (no "/") so they stay valid for the files/:file_key route constraint (/[^\/]+/).

Defined Under Namespace

Classes: ConfigurationError, DownloadError, UploadError

Constant Summary collapse

DEFAULT_REGION =
"garage"
DEFAULT_CONTENT_TYPE =
"application/octet-stream"

Instance Method Summary collapse

Constructor Details

#initialize(app_name: nil, record_class: nil) ⇒ Client

Returns a new instance of Client.

Parameters:

  • app_name (String, nil) (defaults to: nil)

    retained for backwards compatibility (unused)

  • record_class (Class, nil) (defaults to: nil)

    retained for backwards compatibility (unused)



32
33
34
35
36
37
38
39
# File 'app/services/dscf/core/file_storage/client.rb', line 32

def initialize(app_name: nil, record_class: nil)
  @endpoint = ENV["S3_ENDPOINT_URL"].presence
  @bucket = ENV["S3_BUCKET"].presence
  @access_key_id = ENV["S3_ACCESS_KEY_ID"].presence
  @secret_access_key = ENV["S3_SECRET_ACCESS_KEY"].presence
  @region = ENV.fetch("S3_REGION", DEFAULT_REGION)
  validate_configuration!
end

Instance Method Details

#delete(file_key) ⇒ Boolean

Delete an object.

Parameters:

  • file_key (String)

    the object key

Returns:

  • (Boolean)


101
102
103
104
105
106
107
# File 'app/services/dscf/core/file_storage/client.rb', line 101

def delete(file_key)
  s3.delete_object(bucket: @bucket, key: file_key)
  true
rescue StandardError => e
  Rails.logger.error("FileStorage::Client delete error: #{e.message}")
  false
end

#download(file_key) ⇒ Hash

Download a file from object storage.

Parameters:

  • file_key (String)

    the object key

Returns:

  • (Hash)

    { data:, filename:, content_type: }



72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'app/services/dscf/core/file_storage/client.rb', line 72

def download(file_key)
  response = s3.get_object(bucket: @bucket, key: file_key)

  {
    data: response.body.read,
    filename: response.["filename"].presence || file_key,
    content_type: response.content_type.presence || DEFAULT_CONTENT_TYPE
  }
rescue Aws::S3::Errors::NoSuchKey => e
  raise DownloadError, "File not found: #{file_key} (#{e.message})"
rescue Aws::Errors::ServiceError => e
  raise DownloadError, "Download failed: #{e.message}"
end

#exists?(file_key) ⇒ Boolean

Check if an object exists.

Parameters:

  • file_key (String)

    the object key

Returns:

  • (Boolean)


89
90
91
92
93
94
95
96
# File 'app/services/dscf/core/file_storage/client.rb', line 89

def exists?(file_key)
  s3.head_object(bucket: @bucket, key: file_key)
  true
rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::NoSuchKey
  false
rescue StandardError
  false
end

#upload(file, filename: nil) ⇒ Hash

Upload a file to object storage.

Parameters:

  • file (ActionDispatch::Http::UploadedFile, File, Hash)

    the file to upload

  • filename (String) (defaults to: nil)

    optional custom filename

Returns:

  • (Hash)

    { file_key:, filename:, size:, content_type: }



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'app/services/dscf/core/file_storage/client.rb', line 45

def upload(file, filename: nil)
  prepared = prepare_file(file, filename)
  file_key = generate_file_key(prepared[:filename])
  io = prepared[:io]
  io.rewind if io.respond_to?(:rewind)

  s3.put_object(
    bucket: @bucket,
    key: file_key,
    body: io,
    content_type: prepared[:content_type] || DEFAULT_CONTENT_TYPE,
    metadata: {"filename" => prepared[:filename].to_s}
  )

  {
    file_key: file_key,
    filename: prepared[:filename],
    size: prepared[:size],
    content_type: prepared[:content_type]
  }
rescue Aws::Errors::ServiceError => e
  raise UploadError, "Upload failed: #{e.message}"
end