activeblob 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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +412 -0
- data/Rakefile +3 -0
- data/app/models/activeblob/attachment.rb +41 -0
- data/app/models/activeblob/blob/image.rb +38 -0
- data/app/models/activeblob/blob/pdf.rb +31 -0
- data/app/models/activeblob/blob/video.rb +44 -0
- data/app/models/activeblob/blob.rb +122 -0
- data/lib/activeblob/blob_helpers.rb +166 -0
- data/lib/activeblob/engine.rb +20 -0
- data/lib/activeblob/model_extensions.rb +114 -0
- data/lib/activeblob/storage/filesystem.rb +60 -0
- data/lib/activeblob/storage/s3.rb +68 -0
- data/lib/activeblob/version.rb +3 -0
- data/lib/activeblob.rb +46 -0
- data/lib/generators/activeblob/install/install_generator.rb +30 -0
- data/lib/generators/activeblob/install/templates/README +41 -0
- data/lib/generators/activeblob/install/templates/create_attachments.rb +20 -0
- data/lib/generators/activeblob/install/templates/create_blobs.rb +16 -0
- data/lib/generators/activeblob/install/templates/initializer.rb +19 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 89d322a2edaa8e9fe38cd46de0bbb3c95413780079b428c6024a4e1d85362708
|
4
|
+
data.tar.gz: 7b0d9320c5eb5311b01a60d0a1db582c01b23b635da5fbefefbb49b5fe60c78e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6aa7e5b2bed692d80735397eba86aef9466f436964d74b9d63b661a017b19610f3fe04ce6ca110c4778693886ea4bab31d5c6f3abe7978d58e5fd2aba9707003
|
7
|
+
data.tar.gz: c09f2e166040761d02fca0846bae64959dacf5257545a6b15fcf66f40b319c74c3fc31f6d17cf07fa8dfa09ecc79e0e2e7ad835b9f8b846deed6edd6556aeb7e
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2025 Ben Ehmke
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,412 @@
|
|
1
|
+
# ActiveBlob
|
2
|
+
|
3
|
+
ActiveBlob is a content-addressable blob storage system for Rails applications. It provides SHA1-based deduplication, polymorphic attachments, and support for multiple storage backends (filesystem and S3).
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Content-addressable storage**: Blobs are identified by SHA1 hash, preventing duplicate storage
|
8
|
+
- **Polymorphic attachments**: Attach blobs to any model with `has_one_blob` or `has_many_blobs`
|
9
|
+
- **Multiple storage backends**: Filesystem or S3-compatible storage
|
10
|
+
- **Type-specific models**: Built-in support for images, videos, and PDFs with automatic metadata extraction
|
11
|
+
- **Flexible input**: Create blobs from files, URLs, base64 data, or raw data
|
12
|
+
- **URL generation**: Generate signed URLs for cloud storage or local paths
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'activeblob'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
```bash
|
25
|
+
bundle install
|
26
|
+
```
|
27
|
+
|
28
|
+
Run the installer to generate migrations and configuration:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
rails generate activeblob:install
|
32
|
+
rails db:migrate
|
33
|
+
```
|
34
|
+
|
35
|
+
## Configuration
|
36
|
+
|
37
|
+
Configure your storage backend in `config/initializers/activeblob.rb`:
|
38
|
+
|
39
|
+
### Filesystem Storage (Default)
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
ActiveBlob.configure do |config|
|
43
|
+
config.storage_config = {
|
44
|
+
storage: 'filesystem',
|
45
|
+
path: Rails.root.join('storage', 'blobs')
|
46
|
+
}
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
### S3 Storage
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
ActiveBlob.configure do |config|
|
54
|
+
config.storage_config = {
|
55
|
+
storage: 's3',
|
56
|
+
bucket: ENV['S3_BUCKET'],
|
57
|
+
access_key_id: ENV['S3_ACCESS_KEY_ID'],
|
58
|
+
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
|
59
|
+
region: ENV['S3_REGION'] || 'us-east-1',
|
60
|
+
endpoint: ENV['S3_ENDPOINT'] # Optional, for S3-compatible services
|
61
|
+
}
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
## Usage
|
66
|
+
|
67
|
+
### Adding Attachments to Models
|
68
|
+
|
69
|
+
#### Single Attachment
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class User < ApplicationRecord
|
73
|
+
has_one_blob :avatar
|
74
|
+
end
|
75
|
+
|
76
|
+
# Usage
|
77
|
+
user = User.new
|
78
|
+
user.avatar = params[:avatar] # File upload
|
79
|
+
user.save
|
80
|
+
```
|
81
|
+
|
82
|
+
#### Multiple Attachments
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
class Post < ApplicationRecord
|
86
|
+
has_many_blobs :images
|
87
|
+
end
|
88
|
+
|
89
|
+
# Usage
|
90
|
+
post = Post.new
|
91
|
+
post.images = [file1, file2, file3]
|
92
|
+
post.save
|
93
|
+
```
|
94
|
+
|
95
|
+
### Creating Blobs
|
96
|
+
|
97
|
+
#### From File Upload
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
blob = ActiveBlob::Blob.new(file: params[:file])
|
101
|
+
blob.save
|
102
|
+
```
|
103
|
+
|
104
|
+
#### From URL
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
blob = ActiveBlob::Blob.new(url: "https://example.com/image.jpg")
|
108
|
+
blob.save
|
109
|
+
```
|
110
|
+
|
111
|
+
#### From Base64
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
blob = ActiveBlob::Blob.new(
|
115
|
+
base64: base64_string,
|
116
|
+
content_type: "image/png",
|
117
|
+
filename: "image.png"
|
118
|
+
)
|
119
|
+
blob.save
|
120
|
+
```
|
121
|
+
|
122
|
+
#### From Raw Data
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
blob = ActiveBlob::Blob.new(data: binary_data)
|
126
|
+
blob.content_type = "image/jpeg"
|
127
|
+
blob.save
|
128
|
+
```
|
129
|
+
|
130
|
+
### Accessing Blobs
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
# Get blob URL
|
134
|
+
user.avatar.url
|
135
|
+
|
136
|
+
# Get blob URL with options
|
137
|
+
blob.url(disposition: 'inline', filename: 'custom.jpg', expires_in: 3600)
|
138
|
+
|
139
|
+
# Get blob metadata
|
140
|
+
blob.size # File size in bytes
|
141
|
+
blob.content_type # MIME type
|
142
|
+
blob.sha1 # Binary SHA1 hash
|
143
|
+
blob.extension # File extension based on content type
|
144
|
+
|
145
|
+
# Open blob for processing
|
146
|
+
blob.open do |file|
|
147
|
+
# Process the file
|
148
|
+
puts file.read
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
### Blob Types
|
153
|
+
|
154
|
+
ActiveBlob automatically selects the appropriate blob type based on content type:
|
155
|
+
|
156
|
+
#### Images (ActiveBlob::Blob::Image)
|
157
|
+
|
158
|
+
Requires `ruby-vips` gem:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
gem 'ruby-vips'
|
162
|
+
```
|
163
|
+
|
164
|
+
Automatically extracts:
|
165
|
+
- Width and height
|
166
|
+
- Dominant color
|
167
|
+
- Background color
|
168
|
+
- Aspect ratio
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
image = ActiveBlob::Blob.new(file: image_file)
|
172
|
+
image.save
|
173
|
+
|
174
|
+
image.width # => 1920
|
175
|
+
image.height # => 1080
|
176
|
+
image.aspect_ratio # => 1.777...
|
177
|
+
image.dominant_color # => "255,128,0"
|
178
|
+
```
|
179
|
+
|
180
|
+
#### Videos (ActiveBlob::Blob::Video)
|
181
|
+
|
182
|
+
Requires `streamio-ffmpeg` gem:
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
gem 'streamio-ffmpeg'
|
186
|
+
```
|
187
|
+
|
188
|
+
Automatically extracts:
|
189
|
+
- Duration
|
190
|
+
- Bitrate
|
191
|
+
- Codec
|
192
|
+
- Width and height
|
193
|
+
- Frame rate
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
video = ActiveBlob::Blob.new(file: video_file)
|
197
|
+
video.save
|
198
|
+
|
199
|
+
video.duration # => 120.5 (seconds)
|
200
|
+
video.width # => 1920
|
201
|
+
video.height # => 1080
|
202
|
+
video.codec # => "h264"
|
203
|
+
video.frame_rate # => 30.0
|
204
|
+
```
|
205
|
+
|
206
|
+
#### PDFs (ActiveBlob::Blob::PDF)
|
207
|
+
|
208
|
+
Requires `pdf-reader` gem:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
gem 'pdf-reader'
|
212
|
+
```
|
213
|
+
|
214
|
+
Automatically extracts:
|
215
|
+
- Page count
|
216
|
+
- Width and height per page
|
217
|
+
- Aspect ratio
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
pdf = ActiveBlob::Blob.new(file: pdf_file)
|
221
|
+
pdf.save
|
222
|
+
|
223
|
+
pdf.page_count # => 10
|
224
|
+
pdf.aspect_ratio # => 0.707 (based on first page)
|
225
|
+
```
|
226
|
+
|
227
|
+
### Deduplication
|
228
|
+
|
229
|
+
ActiveBlob automatically deduplicates blobs based on SHA1 hash:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
# Upload the same file twice
|
233
|
+
blob1 = ActiveBlob::Blob.new(file: file)
|
234
|
+
blob1.save
|
235
|
+
|
236
|
+
blob2 = ActiveBlob::Blob.new(file: file)
|
237
|
+
blob2.save
|
238
|
+
|
239
|
+
# blob1 and blob2 reference the same record
|
240
|
+
blob1.id == blob2.id # => true
|
241
|
+
```
|
242
|
+
|
243
|
+
### Attachments
|
244
|
+
|
245
|
+
Attachments link blobs to your records:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
# Create attachment explicitly
|
249
|
+
attachment = ActiveBlob::Attachment.create(
|
250
|
+
record: user,
|
251
|
+
blob: blob,
|
252
|
+
filename: "avatar.jpg",
|
253
|
+
type: "avatar"
|
254
|
+
)
|
255
|
+
|
256
|
+
# Access attachment properties
|
257
|
+
attachment.filename # => "avatar.jpg"
|
258
|
+
attachment.content_type # => "image/jpeg"
|
259
|
+
attachment.size # => 102400
|
260
|
+
attachment.url # Delegates to blob.url
|
261
|
+
```
|
262
|
+
|
263
|
+
### Nested Attributes
|
264
|
+
|
265
|
+
You can use nested attributes with attachments:
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
class Post < ApplicationRecord
|
269
|
+
has_many_blobs :images
|
270
|
+
accepts_nested_attributes_for :images
|
271
|
+
end
|
272
|
+
|
273
|
+
# In your controller
|
274
|
+
post.update(
|
275
|
+
images_attributes: [
|
276
|
+
{ blob_id: blob.id, filename: "image1.jpg" },
|
277
|
+
{ blob_id: blob2.id, filename: "image2.jpg" }
|
278
|
+
]
|
279
|
+
)
|
280
|
+
```
|
281
|
+
|
282
|
+
## Architecture
|
283
|
+
|
284
|
+
### Models
|
285
|
+
|
286
|
+
- **ActiveBlob::Blob**: Base model for all blobs
|
287
|
+
- `ActiveBlob::Blob::Image`: Image blobs with metadata extraction
|
288
|
+
- `ActiveBlob::Blob::Video`: Video blobs with metadata extraction
|
289
|
+
- `ActiveBlob::Blob::PDF`: PDF blobs with metadata extraction
|
290
|
+
|
291
|
+
- **ActiveBlob::Attachment**: Polymorphic join model linking blobs to records
|
292
|
+
|
293
|
+
### Storage Backends
|
294
|
+
|
295
|
+
- **ActiveBlob::Storage::Filesystem**: Store blobs on local filesystem
|
296
|
+
- **ActiveBlob::Storage::S3**: Store blobs on S3 or S3-compatible services
|
297
|
+
|
298
|
+
### Database Schema
|
299
|
+
|
300
|
+
#### Blobs Table
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
create_table :blobs, id: :uuid do |t|
|
304
|
+
t.string :type # STI type
|
305
|
+
t.bigint :size, null: false # File size in bytes
|
306
|
+
t.string :content_type, null: false # MIME type
|
307
|
+
t.jsonb :metadata, default: {} # Type-specific metadata
|
308
|
+
t.binary :sha1, limit: 20, null: false # SHA1 hash for deduplication
|
309
|
+
t.timestamps
|
310
|
+
end
|
311
|
+
|
312
|
+
add_index :blobs, :sha1
|
313
|
+
add_index :blobs, [:type, :sha1]
|
314
|
+
```
|
315
|
+
|
316
|
+
#### Attachments Table
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
create_table :attachments, id: :uuid do |t|
|
320
|
+
t.string :type # Attachment type (e.g., "avatar", "photo")
|
321
|
+
t.integer :order, default: 0 # Order for has_many_blobs
|
322
|
+
t.string :filename # Original filename
|
323
|
+
t.string :record_type # Polymorphic record type
|
324
|
+
t.uuid :record_id # Polymorphic record ID
|
325
|
+
t.uuid :blob_id, null: false # Reference to blob
|
326
|
+
t.timestamps
|
327
|
+
end
|
328
|
+
|
329
|
+
add_index :attachments, [:record_type, :record_id]
|
330
|
+
add_index :attachments, :blob_id
|
331
|
+
add_index :attachments, [:blob_id, :record_id, :record_type, :type, :filename],
|
332
|
+
unique: true
|
333
|
+
```
|
334
|
+
|
335
|
+
## Advanced Usage
|
336
|
+
|
337
|
+
### Custom Blob Types
|
338
|
+
|
339
|
+
Create custom blob types by subclassing `ActiveBlob::Blob`:
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
class ActiveBlob::Blob::Audio < ActiveBlob::Blob
|
343
|
+
validates :content_type, format: /\Aaudio\/.*\Z/
|
344
|
+
|
345
|
+
def duration
|
346
|
+
metadata['duration']
|
347
|
+
end
|
348
|
+
|
349
|
+
def self.process(record, path)
|
350
|
+
# Extract audio metadata
|
351
|
+
# record.metadata = { 'duration' => ..., 'bitrate' => ... }
|
352
|
+
end
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
### Storage Adapter Interface
|
357
|
+
|
358
|
+
Create custom storage adapters by implementing:
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
class MyStorage
|
362
|
+
def local?
|
363
|
+
# Return true if storage is local, false otherwise
|
364
|
+
end
|
365
|
+
|
366
|
+
def write(id, file, options = {})
|
367
|
+
# Write file to storage
|
368
|
+
end
|
369
|
+
|
370
|
+
def read(id)
|
371
|
+
# Read file from storage
|
372
|
+
end
|
373
|
+
|
374
|
+
def delete(id)
|
375
|
+
# Delete file from storage
|
376
|
+
end
|
377
|
+
|
378
|
+
def exists?(id)
|
379
|
+
# Check if file exists
|
380
|
+
end
|
381
|
+
|
382
|
+
def url(id, **options)
|
383
|
+
# Generate URL for file
|
384
|
+
end
|
385
|
+
|
386
|
+
def copy_to_tempfile(id, basename: nil, &block)
|
387
|
+
# Copy file to temporary location and yield to block
|
388
|
+
end
|
389
|
+
end
|
390
|
+
```
|
391
|
+
|
392
|
+
## Development
|
393
|
+
|
394
|
+
After checking out the repo, run:
|
395
|
+
|
396
|
+
```bash
|
397
|
+
bundle install
|
398
|
+
```
|
399
|
+
|
400
|
+
To run tests:
|
401
|
+
|
402
|
+
```bash
|
403
|
+
rails test
|
404
|
+
```
|
405
|
+
|
406
|
+
## Contributing
|
407
|
+
|
408
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/bemky/activeblob.
|
409
|
+
|
410
|
+
## License
|
411
|
+
|
412
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module ActiveBlob
|
2
|
+
class Attachment < ActiveRecord::Base
|
3
|
+
self.table_name = 'attachments'
|
4
|
+
self.inheritance_column = nil
|
5
|
+
|
6
|
+
belongs_to :blob, class_name: 'ActiveBlob::Blob'
|
7
|
+
belongs_to :record, polymorphic: true
|
8
|
+
|
9
|
+
before_validation { self.order ||= 0 }
|
10
|
+
before_save :default_filename
|
11
|
+
|
12
|
+
validates_uniqueness_of :blob_id, scope: [:record_id, :record_type, :type, :filename], message: " already attached", if: :record_id
|
13
|
+
|
14
|
+
delegate :size, :content_type, :sha1, :extension, to: :blob
|
15
|
+
|
16
|
+
def url(**options)
|
17
|
+
options[:filename] ||= filename
|
18
|
+
blob.url(**options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def file=(file)
|
22
|
+
self.filename ||= ActiveBlob::BlobHelpers.filename_from_file(file)
|
23
|
+
self.blob = ActiveBlob::Blob.new(file: file)
|
24
|
+
end
|
25
|
+
|
26
|
+
def url=(url)
|
27
|
+
file = ActiveBlob::Blob.download_url(url)
|
28
|
+
self.filename = file.original_filename
|
29
|
+
self.blob = ActiveBlob::Blob.create!(file: file)
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_filename
|
33
|
+
self.filename ||= [self.type, SecureRandom.hex(10)].join("-")
|
34
|
+
end
|
35
|
+
|
36
|
+
def open(basename: nil, &block)
|
37
|
+
basename ||= [File.basename(filename), File.extname(filename)]
|
38
|
+
blob.open(basename: basename, &block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActiveBlob
|
2
|
+
class Blob::Image < Blob
|
3
|
+
validates :content_type, presence: true, format: /\Aimage\/\w+\Z/
|
4
|
+
|
5
|
+
def width
|
6
|
+
metadata['width']
|
7
|
+
end
|
8
|
+
|
9
|
+
def height
|
10
|
+
metadata['height']
|
11
|
+
end
|
12
|
+
|
13
|
+
def dominant_color
|
14
|
+
metadata['dominant_color']
|
15
|
+
end
|
16
|
+
|
17
|
+
def background_color
|
18
|
+
metadata['background_color']
|
19
|
+
end
|
20
|
+
|
21
|
+
def aspect_ratio
|
22
|
+
return nil unless self.width && self.height
|
23
|
+
self.width.to_f / self.height.to_f
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.process(record, path)
|
27
|
+
require 'vips' unless defined?(Vips)
|
28
|
+
|
29
|
+
vips = Vips::Image.new_from_file(path)
|
30
|
+
record.metadata = {
|
31
|
+
'width' => vips.width,
|
32
|
+
'height' => vips.height,
|
33
|
+
'dominant_color' => vips.thumbnail_image(1).getpoint(0, 0).join(","),
|
34
|
+
'background_color' => vips.getpoint(0,0).join(",")
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActiveBlob
|
2
|
+
class Blob::PDF < Blob
|
3
|
+
validates :content_type, presence: true, format: /\Aapplication\/pdf\Z/
|
4
|
+
|
5
|
+
def page_count
|
6
|
+
metadata['pages']&.size || 0
|
7
|
+
end
|
8
|
+
|
9
|
+
def aspect_ratio
|
10
|
+
first_page = metadata['pages']&.first
|
11
|
+
return nil unless first_page
|
12
|
+
|
13
|
+
width = first_page['width'].to_f
|
14
|
+
height = first_page['height'].to_f
|
15
|
+
|
16
|
+
return nil if height.zero?
|
17
|
+
|
18
|
+
width / height
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.process(record, path)
|
22
|
+
require 'pdf-reader' unless defined?(PDF::Reader)
|
23
|
+
|
24
|
+
record.metadata = {
|
25
|
+
'pages' => PDF::Reader.new(path).pages.map { |page|
|
26
|
+
{ 'width' => page.width, 'height' => page.height }
|
27
|
+
}
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ActiveBlob
|
2
|
+
class Blob::Video < Blob
|
3
|
+
validates :content_type, presence: true, format: /\Avideo\/.*\Z/
|
4
|
+
|
5
|
+
def width
|
6
|
+
metadata['width']
|
7
|
+
end
|
8
|
+
|
9
|
+
def height
|
10
|
+
metadata['height']
|
11
|
+
end
|
12
|
+
|
13
|
+
def duration
|
14
|
+
metadata['duration']
|
15
|
+
end
|
16
|
+
|
17
|
+
def bitrate
|
18
|
+
metadata['bitrate']
|
19
|
+
end
|
20
|
+
|
21
|
+
def codec
|
22
|
+
metadata['codec']
|
23
|
+
end
|
24
|
+
|
25
|
+
def frame_rate
|
26
|
+
metadata['frame_rate']
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.process(record, path)
|
30
|
+
require 'streamio-ffmpeg' unless defined?(FFMPEG)
|
31
|
+
|
32
|
+
video = FFMPEG::Movie.new(path)
|
33
|
+
|
34
|
+
record.metadata = {
|
35
|
+
'duration' => video.duration,
|
36
|
+
'bitrate' => video.bitrate,
|
37
|
+
'codec' => video.video_codec,
|
38
|
+
'width' => video.width,
|
39
|
+
'height' => video.height,
|
40
|
+
'frame_rate' => video.frame_rate
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|