resourcespace-ruby 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 12ee38b0f2c62de30514d7c97cb16c75869d80b3887e13b6c9a662f949bfbea6
4
+ data.tar.gz: ee59831391f451cb97fe9e4e796349cc0f4fdd4cb67b27ad09fbf82da976e2b2
5
+ SHA512:
6
+ metadata.gz: cf1a67fa696a6535f9743649f485230129bdf36ffaeaeddea73461bdc9202b9ca5dc8ec93d73f5e3f00aef7193b4bbb565a6264e1ca4f0a06c3c907be9692031
7
+ data.tar.gz: 0314fdd3b2d5f6c03a98f9bc121855789efe99c3032d4851844edbd783a45dd26e74cf185c5a8b52c5f99e93d14393ffa2c42051d6d5e7a8b862772ba436947f
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ # Omakase Ruby styling for Rails
2
+ inherit_gem: { rubocop-rails-omakase: rubocop.yml }
3
+
4
+ # Overwrite or add rules to create your own house style
5
+ #
6
+ # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
7
+ # Layout/SpaceInsideArrayLiteralBrackets:
8
+ # Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.4.4
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2024-03-21
9
+
10
+ ### Added
11
+ - Initial release of BrowserMCP wrapper
12
+ - Basic browser automation functionality
13
+ - Navigation (navigate_to, go_back, go_forward)
14
+ - Element interaction (click, hover, type, select_option)
15
+ - Element properties (text, value, attributes, state)
16
+ - Page state management
17
+ - Screenshot and console log capture
18
+ - JavaScript evaluation
19
+ - Robust error handling with automatic retries
20
+ - Comprehensive configuration options
21
+ - Integration tests
22
+ - Documentation
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,355 @@
1
+ # ResourceSpace Ruby Client
2
+
3
+ A comprehensive Ruby client library for the [ResourceSpace](https://www.resourcespace.com/) open-source Digital Asset Management system. This gem provides an easy-to-use interface for managing web assets including images, CSS files, JavaScript files, fonts, and other digital resources.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/resourcespace-ruby.svg)](https://badge.fury.io/rb/resourcespace-ruby)
6
+ [![Build Status](https://github.com/survey-flunkie/resourcespace-ruby/workflows/CI/badge.svg)](https://github.com/survey-flunkie/resourcespace-ruby/actions)
7
+ [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-blue)](https://rubydoc.info/gems/resourcespace-ruby)
8
+
9
+ ## Features
10
+
11
+ - **Complete API Coverage**: Supports all major ResourceSpace API endpoints
12
+ - **Web Asset Focused**: Optimized for managing web development assets (images, CSS, JS, fonts, icons)
13
+ - **Authentication**: Secure SHA256 signature-based authentication
14
+ - **File Operations**: Upload, download, and manage files with ease
15
+ - **Search & Collections**: Powerful search capabilities and collection management
16
+ - **Metadata Management**: Comprehensive metadata and field management
17
+ - **Error Handling**: Robust error handling with specific exception types
18
+ - **Configurable**: Flexible configuration options with global and instance-level settings
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'resourcespace-ruby'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ ```bash
31
+ $ bundle install
32
+ ```
33
+
34
+ Or install it yourself as:
35
+
36
+ ```bash
37
+ $ gem install resourcespace-ruby
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### Basic Configuration
43
+
44
+ ```ruby
45
+ require 'resourcespace'
46
+
47
+ # Configure globally
48
+ ResourceSpace.configure do |config|
49
+ config.url = "https://your-resourcespace.com/api/"
50
+ config.user = "your_username"
51
+ config.private_key = "your_private_key" # Get this from your ResourceSpace profile
52
+ config.timeout = 30
53
+ end
54
+
55
+ # Or configure per instance
56
+ client = ResourceSpace::Client.new(
57
+ url: "https://your-resourcespace.com/api/",
58
+ user: "your_username",
59
+ private_key: "your_private_key"
60
+ )
61
+ ```
62
+
63
+ ### Basic Usage
64
+
65
+ ```ruby
66
+ # Test connection
67
+ status = client.test_connection
68
+ puts "Connected to ResourceSpace #{status['version']}"
69
+
70
+ # Search for web assets
71
+ results = client.search.search_web_assets("images")
72
+ puts "Found #{results.length} image assets"
73
+
74
+ # Upload a web asset
75
+ uploaded = client.resources.upload_file(
76
+ File.open("assets/logo.png"),
77
+ caption: "Company Logo"
78
+ )
79
+
80
+ # Create a collection for web assets
81
+ collection = client.collections.create_web_asset_collection(
82
+ "Website Assets",
83
+ asset_type: "images"
84
+ )
85
+
86
+ # Add resource to collection
87
+ client.collections.add_resource_to_collection(uploaded['ref'], collection['ref'])
88
+ ```
89
+
90
+ ## Web Asset Management
91
+
92
+ This gem is specifically designed to work well with web development assets:
93
+
94
+ ### Upload Web Assets
95
+
96
+ ```ruby
97
+ # Upload different types of web assets
98
+ logo = client.resources.upload_file("assets/logo.png")
99
+ stylesheet = client.resources.upload_file("assets/main.css")
100
+ script = client.resources.upload_file("assets/app.js")
101
+ font = client.resources.upload_file("assets/custom-font.woff2")
102
+ ```
103
+
104
+ ### Search for Specific Asset Types
105
+
106
+ ```ruby
107
+ # Search by asset type
108
+ images = client.search.search_web_assets("images")
109
+ css_files = client.search.search_web_assets("css")
110
+ js_files = client.search.search_web_assets("javascript")
111
+ fonts = client.search.search_web_assets("fonts")
112
+
113
+ # Search by file extension
114
+ svg_files = client.search.search_by_extension(["svg"])
115
+ web_fonts = client.search.search_by_extension(["woff", "woff2", "ttf"])
116
+ ```
117
+
118
+ ### Organize with Collections
119
+
120
+ ```ruby
121
+ # Create collections for different asset types
122
+ image_collection = client.collections.create_web_asset_collection(
123
+ "Website Images",
124
+ asset_type: "images"
125
+ )
126
+
127
+ css_collection = client.collections.create_web_asset_collection(
128
+ "Stylesheets",
129
+ asset_type: "css"
130
+ )
131
+
132
+ # Get all web asset collections
133
+ web_collections = client.collections.get_web_asset_collections
134
+ ```
135
+
136
+ ### Manage Web Asset Metadata
137
+
138
+ ```ruby
139
+ # Set up web asset metadata fields (one-time setup)
140
+ client.metadata.create_web_asset_fields
141
+
142
+ # Update resource with web asset metadata
143
+ client.metadata.update_web_asset_metadata(resource_id, {
144
+ title: "Hero Background Image",
145
+ asset_type: "Image",
146
+ dimensions: "1920x1080",
147
+ usage_rights: "Creative Commons",
148
+ purpose: "Website header background"
149
+ })
150
+ ```
151
+
152
+ ## Advanced Usage
153
+
154
+ ### Resource Management
155
+
156
+ ```ruby
157
+ # Create a new resource
158
+ resource = client.resources.create_resource(
159
+ name: "Company Logo",
160
+ resource_type: 1,
161
+ metadata: {
162
+ 12 => "logo, branding, company", # Keywords field
163
+ 51 => "Image" # Custom asset type field
164
+ }
165
+ )
166
+
167
+ # Get resource details
168
+ details = client.resources.get_resource_data(resource_id)
169
+
170
+ # Update resource metadata
171
+ client.resources.update_field(resource_id, 8, "New Title")
172
+
173
+ # Download resource
174
+ client.resources.download_resource(resource_id, "/local/path/file.jpg")
175
+
176
+ # Get alternative files
177
+ alternatives = client.resources.get_alternative_files(resource_id)
178
+ ```
179
+
180
+ ### Advanced Search
181
+
182
+ ```ruby
183
+ # Advanced search with multiple criteria
184
+ results = client.search.advanced_search({
185
+ title: "logo",
186
+ extensions: ["png", "svg"],
187
+ from_date: "2023-01-01",
188
+ to_date: "2023-12-31"
189
+ }, {
190
+ order_by: "date",
191
+ sort: "desc",
192
+ fetchrows: 20
193
+ })
194
+
195
+ # Search by date range
196
+ recent = client.search.search_by_date_range("2023-01-01", "2023-12-31")
197
+
198
+ # Get recently added resources
199
+ latest = client.search.recent_resources(10)
200
+ ```
201
+
202
+ ### Collection Management
203
+
204
+ ```ruby
205
+ # Create collection
206
+ collection = client.collections.create_collection(
207
+ "Marketing Assets",
208
+ public: false,
209
+ allow_changes: true
210
+ )
211
+
212
+ # Add multiple resources
213
+ resource_ids = [123, 124, 125]
214
+ client.collections.add_resources_to_collection(resource_ids, collection_id)
215
+
216
+ # Search public collections
217
+ public_collections = client.collections.search_public_collections("web")
218
+ ```
219
+
220
+ ### User & Permissions
221
+
222
+ ```ruby
223
+ # Check user capabilities
224
+ capabilities = client.users.capabilities
225
+ puts "Can upload: #{capabilities[:upload]}"
226
+ puts "Can edit: #{capabilities[:edit_resources]}"
227
+
228
+ # Check specific permissions
229
+ can_download = client.users.can_download?
230
+ has_admin = client.users.admin?
231
+
232
+ # Check resource-specific permissions
233
+ can_edit_resource = client.users.can_edit_resource?(resource_id)
234
+ ```
235
+
236
+ ## Configuration Options
237
+
238
+ ```ruby
239
+ ResourceSpace.configure do |config|
240
+ config.url = "https://your-resourcespace.com/api/" # Required
241
+ config.user = "username" # Required
242
+ config.private_key = "your_private_key" # Required
243
+ config.timeout = 30 # Request timeout (seconds)
244
+ config.retries = 3 # Number of retry attempts
245
+ config.verify_ssl = true # Verify SSL certificates
246
+ config.auth_mode = "userkey" # Authentication mode
247
+ config.debug = false # Enable debug logging
248
+ config.logger = Logger.new(STDOUT) # Custom logger
249
+ end
250
+ ```
251
+
252
+ ## Error Handling
253
+
254
+ The gem provides specific exception types for different error conditions:
255
+
256
+ ```ruby
257
+ begin
258
+ resource = client.resources.get_resource_data(999)
259
+ rescue ResourceSpace::NotFoundError => e
260
+ puts "Resource not found: #{e.message}"
261
+ rescue ResourceSpace::AuthenticationError => e
262
+ puts "Authentication failed: #{e.message}"
263
+ rescue ResourceSpace::AuthorizationError => e
264
+ puts "Access denied: #{e.message}"
265
+ rescue ResourceSpace::ValidationError => e
266
+ puts "Invalid data: #{e.message}"
267
+ rescue ResourceSpace::ServerError => e
268
+ puts "Server error: #{e.message}"
269
+ rescue ResourceSpace::NetworkError => e
270
+ puts "Network error: #{e.message}"
271
+ end
272
+ ```
273
+
274
+ ## Rails Integration
275
+
276
+ For Rails applications, add an initializer:
277
+
278
+ ```ruby
279
+ # config/initializers/resourcespace.rb
280
+ ResourceSpace.configure do |config|
281
+ config.url = ENV['RESOURCESPACE_URL']
282
+ config.user = ENV['RESOURCESPACE_USER']
283
+ config.private_key = ENV['RESOURCESPACE_PRIVATE_KEY']
284
+ config.timeout = 30
285
+ end
286
+ ```
287
+
288
+ Then use in your application:
289
+
290
+ ```ruby
291
+ class AssetsController < ApplicationController
292
+ def upload
293
+ client = ResourceSpace::Client.new
294
+ uploaded = client.resources.upload_file(params[:file])
295
+
296
+ # Store reference in your model
297
+ @asset = Asset.create(
298
+ name: uploaded['title'],
299
+ resourcespace_id: uploaded['ref'],
300
+ file_type: uploaded['file_extension']
301
+ )
302
+ end
303
+ end
304
+ ```
305
+
306
+ ## Testing
307
+
308
+ Run the test suite:
309
+
310
+ ```bash
311
+ $ bundle exec rspec
312
+ ```
313
+
314
+ Run tests with coverage:
315
+
316
+ ```bash
317
+ $ COVERAGE=1 bundle exec rspec
318
+ ```
319
+
320
+ ## Development
321
+
322
+ After checking out the repo, run:
323
+
324
+ ```bash
325
+ $ bin/setup
326
+ $ bundle exec rake spec
327
+ ```
328
+
329
+ To install this gem onto your local machine:
330
+
331
+ ```bash
332
+ $ bundle exec rake install
333
+ ```
334
+
335
+ ## Contributing
336
+
337
+ 1. Fork it
338
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
339
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
340
+ 4. Push to the branch (`git push origin my-new-feature`)
341
+ 5. Create new Pull Request
342
+
343
+ ## API Reference
344
+
345
+ For complete API documentation, see the [ResourceSpace API documentation](https://www.resourcespace.com/knowledge-base/api/).
346
+
347
+ ## License
348
+
349
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
350
+
351
+ ## Support
352
+
353
+ - Documentation: [https://rubydoc.info/gems/resourcespace-ruby](https://rubydoc.info/gems/resourcespace-ruby)
354
+ - Issues: [https://github.com/survey-flunkie/resourcespace-ruby/issues](https://github.com/survey-flunkie/resourcespace-ruby/issues)
355
+ - ResourceSpace Documentation: [https://www.resourcespace.com/knowledge-base/](https://www.resourcespace.com/knowledge-base/)
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative "config/application"
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/multipart"
5
+ require "json"
6
+ require "digest"
7
+ require "uri"
8
+
9
+ module ResourceSpace
10
+ # Main client class for interacting with ResourceSpace API
11
+ #
12
+ # @example
13
+ # client = ResourceSpace::Client.new(
14
+ # url: "https://your-resourcespace.com/api/",
15
+ # user: "your_username",
16
+ # private_key: "your_private_key"
17
+ # )
18
+ class Client
19
+ # @return [Configuration] client configuration
20
+ attr_reader :config
21
+
22
+ # @return [Resource] resource management interface
23
+ attr_reader :resources
24
+
25
+ # @return [Collection] collection management interface
26
+ attr_reader :collections
27
+
28
+ # @return [Search] search interface
29
+ attr_reader :search
30
+
31
+ # @return [User] user management interface
32
+ attr_reader :users
33
+
34
+ # @return [Metadata] metadata management interface
35
+ attr_reader :metadata
36
+
37
+ # Initialize a new ResourceSpace client
38
+ #
39
+ # @param url [String] ResourceSpace API URL
40
+ # @param user [String] ResourceSpace username
41
+ # @param private_key [String] ResourceSpace private API key
42
+ # @param config [Configuration] configuration object
43
+ # @param options [Hash] additional configuration options
44
+ def initialize(url: nil, user: nil, private_key: nil, config: nil, **options)
45
+ @config = config || ResourceSpace.config.dup
46
+
47
+ # Set configuration from parameters
48
+ @config.url = url if url
49
+ @config.user = user if user
50
+ @config.private_key = private_key if private_key
51
+
52
+ # Apply additional options
53
+ options.each { |key, value| @config.public_send("#{key}=", value) if @config.respond_to?("#{key}=") }
54
+
55
+ # Validate configuration
56
+ @config.validate!
57
+
58
+ # Initialize API interfaces
59
+ @resources = Resource.new(self)
60
+ @collections = Collection.new(self)
61
+ @search = Search.new(self)
62
+ @users = User.new(self)
63
+ @metadata = Metadata.new(self)
64
+ end
65
+
66
+ # Make a GET request to the ResourceSpace API
67
+ #
68
+ # @param function [String] API function name
69
+ # @param params [Hash] request parameters
70
+ # @return [Hash] parsed JSON response
71
+ def get(function, params = {})
72
+ request(:get, function, params)
73
+ end
74
+
75
+ # Make a POST request to the ResourceSpace API
76
+ #
77
+ # @param function [String] API function name
78
+ # @param params [Hash] request parameters
79
+ # @return [Hash] parsed JSON response
80
+ def post(function, params = {})
81
+ request(:post, function, params)
82
+ end
83
+
84
+ # Upload a file to ResourceSpace
85
+ #
86
+ # @param file [File, String] file object or file path
87
+ # @param params [Hash] additional parameters
88
+ # @return [Hash] parsed JSON response
89
+ def upload_file(file, params = {})
90
+ file_param = if file.is_a?(String)
91
+ Faraday::UploadIO.new(file, mime_type_for_file(file))
92
+ else
93
+ Faraday::UploadIO.new(file, mime_type_for_file(file.path))
94
+ end
95
+
96
+ params = params.merge(filedata: file_param)
97
+ request(:post, "upload_file", params, multipart: true)
98
+ end
99
+
100
+ # Download a file from ResourceSpace
101
+ #
102
+ # @param download_url [String] download URL
103
+ # @param file_path [String] local file path to save to
104
+ # @return [Boolean] true if successful
105
+ def download_file(download_url, file_path)
106
+ response = connection.get(download_url)
107
+
108
+ if response.success?
109
+ File.write(file_path, response.body)
110
+ true
111
+ else
112
+ handle_error_response(response)
113
+ end
114
+ end
115
+
116
+ # Test the API connection
117
+ #
118
+ # @return [Hash] system status information
119
+ def test_connection
120
+ get("get_system_status")
121
+ end
122
+
123
+ private
124
+
125
+ # Make an HTTP request to the ResourceSpace API
126
+ #
127
+ # @param method [Symbol] HTTP method (:get or :post)
128
+ # @param function [String] API function name
129
+ # @param params [Hash] request parameters
130
+ # @param multipart [Boolean] whether to use multipart encoding
131
+ # @return [Hash] parsed JSON response
132
+ def request(method, function, params = {}, multipart: false)
133
+ # Prepare base parameters
134
+ request_params = {
135
+ user: config.user,
136
+ function: function
137
+ }.merge(params)
138
+
139
+ # Build query string for signing
140
+ query_string = URI.encode_www_form(request_params.reject { |_k, v| v.is_a?(Faraday::UploadIO) })
141
+
142
+ # Generate signature
143
+ signature = generate_signature(query_string)
144
+ request_params[:sign] = signature
145
+ request_params[:authmode] = config.auth_mode
146
+
147
+ # Make the request
148
+ response = if method == :get
149
+ connection.get("", request_params)
150
+ else
151
+ if multipart
152
+ connection.post("", request_params)
153
+ else
154
+ connection.post("", URI.encode_www_form(request_params))
155
+ end
156
+ end
157
+
158
+ handle_response(response)
159
+ end
160
+
161
+ # Generate SHA256 signature for API authentication
162
+ #
163
+ # @param query_string [String] URL-encoded query string
164
+ # @return [String] SHA256 hexadecimal signature
165
+ def generate_signature(query_string)
166
+ Digest::SHA256.hexdigest("#{config.private_key}#{query_string}")
167
+ end
168
+
169
+ # Get the Faraday connection instance
170
+ #
171
+ # @return [Faraday::Connection] configured connection
172
+ def connection
173
+ @connection ||= Faraday.new(url: config.url) do |conn|
174
+ conn.request :multipart
175
+ conn.request :url_encoded
176
+ conn.adapter Faraday.default_adapter
177
+
178
+ # Set timeout
179
+ conn.options.timeout = config.timeout
180
+
181
+ # Set headers
182
+ conn.headers["User-Agent"] = config.user_agent
183
+ config.default_headers.each { |key, value| conn.headers[key] = value }
184
+
185
+ # Add response middleware
186
+ conn.response :logger, config.logger if config.debug && config.logger
187
+ end
188
+ end
189
+
190
+ # Handle API response and parse JSON
191
+ #
192
+ # @param response [Faraday::Response] HTTP response
193
+ # @return [Hash] parsed JSON response
194
+ # @raise [Error] if response indicates an error
195
+ def handle_response(response)
196
+ if response.success?
197
+ parse_json_response(response.body)
198
+ else
199
+ handle_error_response(response)
200
+ end
201
+ end
202
+
203
+ # Handle error responses and raise appropriate exceptions
204
+ #
205
+ # @param response [Faraday::Response] HTTP response
206
+ # @raise [Error] appropriate error based on status code
207
+ def handle_error_response(response)
208
+ message = "HTTP #{response.status}"
209
+
210
+ # Try to extract error message from response body
211
+ if response.body && !response.body.empty?
212
+ begin
213
+ parsed_body = JSON.parse(response.body)
214
+ message = parsed_body["error"] || parsed_body["message"] || message
215
+ rescue JSON::ParserError
216
+ message = response.body.length > 200 ? "#{response.body[0..200]}..." : response.body
217
+ end
218
+ end
219
+
220
+ raise ResourceSpace.from_response(response.status, message, response.body)
221
+ end
222
+
223
+ # Parse JSON response body
224
+ #
225
+ # @param body [String] response body
226
+ # @return [Hash] parsed JSON
227
+ # @raise [ParseError] if JSON parsing fails
228
+ def parse_json_response(body)
229
+ return {} if body.nil? || body.empty?
230
+
231
+ JSON.parse(body)
232
+ rescue JSON::ParserError => e
233
+ raise ParseError.new("Failed to parse JSON response: #{e.message}", data: { body: body })
234
+ end
235
+
236
+ # Get MIME type for a file
237
+ #
238
+ # @param file_path [String] file path
239
+ # @return [String] MIME type
240
+ def mime_type_for_file(file_path)
241
+ require "mime/types"
242
+ MIME::Types.type_for(file_path).first&.content_type || "application/octet-stream"
243
+ end
244
+ end
245
+ end