diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..ad63f70273b447516de6a05e6cb41ce3b8283663 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +log/ +tmp/ +public/system/ +public/assets/ +vendor/ +shared/ diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000000000000000000000000000000000000..75723cce1ef9a38e5340957170cf567c4b46346f --- /dev/null +++ b/.env.dev @@ -0,0 +1,41 @@ +# '.env' file example for local development. +# Copy this file to a file named '.env' and make changes there. +# The final '.env' file should not be commited to version control. +# Set the *_PASSWORD variables with actual passwords. +# 'host.docker.internal' resolves to the IP of the host machine inside the docker network. + +PORTALMEC_API_URL=localhost + +PORTALMEC_DB_HOST=host.docker.internal +PORTALMEC_DB_POOL=25 +PORTALMEC_DB_NAME=portalmec +PORTALMEC_DB_USERNAME=portalmec +PORTALMEC_DB_PASSWORD= + +PORTALMEC_DSPACE_LOGIN=admin@mecdb3.c3sl.ufpr.br +PORTALMEC_DSPACE_PASSWORD= +PORTALMEC_DSPACE_PORT=8443 +PORTALMEC_DSPACE_HOST=mecdb4.c3sl.ufpr.br + +PORTALMEC_ELASTICSEARCH_PORT=9200 +PORTALMEC_ELASTICSEARCH_HOST=host.docker.internal +PORTALMEC_ELASTICSEARCH_LOGIN=elastic +PORTALMEC_ELASTICSEARCH_PASSWORD= +PORTALMEC_ELASTICSEARCH_INDEX_PREFIX= + +MEMCACHE_SERVERS=localhost +ACTION_MAILER_HOST=api.portalmectest.c3sl.ufpr.br +SMTP_ADDRESS=urquell.c3sl.ufpr.br +SMTP_PORT=587 +GITLAB_PORTALMEC_PRIVATE_TOKEN= + +RAILS_SERVE_STATIC_FILES=FALSE +RAILS_ENV=development +PORT=3000 +WEB_CONCURRENCY=8 +RAILS_MAX_THREADS=8 + +REDIS_HOST=redis + +GOOGLE_KEY= +GOOGLE_SECRET= diff --git a/.gitignore b/.gitignore index 5767d2bf65adf0dc83b3068f876ec7a1f4384a70..62cdd175ab578c5c6805ac4acfc91d8f89e36b80 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ autocomplete-server.service # ignore configs /config/database.yml /config/sidekiq.yml +.env +.env.prod diff --git a/Dockerfile b/Dockerfile index 0f5a273dd3aff2a1278261d9d2f99ffdbf727d0c..bcb62297504ce90e95fa7d63dd9b198929633097 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,32 @@ -FROM ruby:2.2.0 -RUN apt-get update -qq && apt-get install -y build-essential nodejs +# syntax=docker/dockerfile:1 -RUN mkdir /app +FROM ruby:3.1.0-alpine -WORKDIR /tmp -COPY Gemfile Gemfile -COPY Gemfile.lock Gemfile.lock -RUN bundle install -j 3 +ENV BUNDLER_VERSION=2.3.26 + +RUN gem install bundler -v 2.3.26 WORKDIR /app -CMD ["rails", "server", "-b", "0.0.0.0"] +COPY Gemfile Gemfile.lock ./ + +RUN apk add --update --no-cache \ + build-base \ + curl-dev \ + shared-mime-info \ + postgresql-dev \ + file \ + imagemagick-dev \ + nodejs-current \ + libarchive \ + git + +# Prevent libxml2 and libxslt conflicts +RUN bundle config build.nokogiri --use-system-libraries + +# Only install if Gemfile not satisfied +RUN bundle check || bundle install + +COPY . ./ + +ENTRYPOINT ["./entrypoints/docker-entrypoint.sh"] diff --git a/Gemfile b/Gemfile index b60365f7d8520effe74d2f1f2ffa1f0f15a7de52..bb639a56af932a488291be40fbbde06ba813d90a 100644 --- a/Gemfile +++ b/Gemfile @@ -185,4 +185,6 @@ gem 'elasticsearch', '~> 8.6' gem 'multipart-post', '~> 2.0' gem 'faraday-multipart', '~> 1.0', '>= 1.0.4' +gem 'tzinfo-data' + diff --git a/app/models/collection.rb b/app/models/collection.rb index b88f3f1b9e9372652702becf86334b376dedb825..5a05ed7b1eb011a6edb784bcf23ce31e83846eb1 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -73,7 +73,7 @@ class Collection < ApplicationRecord scope :from_user, ->(user) { where(owner: user) } - searchkick language: 'brazilian', match: :word_start, searchable: [:name, :description, :author], callbacks: :async, index_prefix: Socket.gethostname.downcase, merge_mappings: true, mappings: { + searchkick language: 'brazilian', match: :word_start, searchable: [:name, :description, :author], callbacks: :async, merge_mappings: true, mappings: { "properties": { "created_at": { "type": "date" diff --git a/app/models/learning_object.rb b/app/models/learning_object.rb index 0ba7335aa6e8cff9e97715229be9a12e210fcd55..5273abbd61509ddefe67f55afd5868e95709c944 100644 --- a/app/models/learning_object.rb +++ b/app/models/learning_object.rb @@ -99,7 +99,7 @@ class LearningObject < ApplicationRecord default_scope { includes(:object_type, :attachment, :attachments) } scope :missing_thumbnail, ->() { where(thumbnail_file_name: nil) } - searchkick language: 'brazilian', match: :word_start, searchable: [:name, :description, :author, :object_type, :tags, :subjects, :educational_stages, :languages], callbacks: :async, index_prefix: Socket.gethostname.downcase, merge_mappings: true, mappings: { + searchkick language: 'brazilian', match: :word_start, searchable: [:name, :description, :author, :object_type, :tags, :subjects, :educational_stages, :languages], callbacks: :async, merge_mappings: true, mappings: { "properties": { "published_at": { "type": "date" diff --git a/app/models/user.rb b/app/models/user.rb index f01650244285960f9e5635aab7a35db1a2cd1d68..e2c2ec53d1fa0a77af31a11b282549d468e502f9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -164,7 +164,7 @@ class User < ApplicationRecord enum state: { active: 0, blocked: 1, banished: 2} - searchkick language: 'brazilian', match: :word_start, searchable: [:name], callbacks: :async, index_prefix: Socket.gethostname.downcase + searchkick language: 'brazilian', match: :word_start, searchable: [:name], callbacks: :async acts_as_paranoid diff --git a/config/application.rb b/config/application.rb index 552d3f5ce92635b6964b3a4a67b6b29456d2e7b4..4751209cd33b95ee9706afa6dd991798f54890d0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -72,7 +72,7 @@ module Portalmec # end # end - config.middleware.use Rack::Cors do + config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '*', @@ -101,6 +101,11 @@ module Portalmec config.middleware.use ActionDispatch::Cookies config.middleware.use ActionDispatch::Session::CookieStore + # host authorization + if ENV.key?("PORTALMEC_API_URL") + config.hosts << ENV["PORTALMEC_API_URL"] + end + config.hosts << "localhost" end end diff --git a/config/database.yml b/config/database.yml index 20ae345b07dcaf8cdb281acd0c434e0c99bb7777..eec5db4fafd799df870084e9c5ff70c8ac6a1796 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,24 +1,4 @@ -# default: &defaults -# adapter: postgresql -# encoding: unicode - -development: - adapter: postgresql - encoding: unicode - database: portalmecapi - username: postgres - password: 123mudar - host: localhost - -test: - adapter: postgresql - encoding: unicode - database: portalmecapi - username: postgres - password: 123mudar - host: localhost - -production: +default: &default adapter: postgresql encoding: unicode timeout: 30000 @@ -27,4 +7,14 @@ production: database: <%= ENV['PORTALMEC_DB_NAME'] %> username: <%= ENV['PORTALMEC_DB_USERNAME'] %> password: <%= ENV['PORTALMEC_DB_PASSWORD'] %> + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default reaping_frequency: 30 + diff --git a/config/initializers/eager_load.rb b/config/initializers/eager_load.rb index ef15c6e4573b27da6266b2b263d057dd9db31f5c..00abea411784a180a3e9d4b2b9c08c60e1d68d77 100644 --- a/config/initializers/eager_load.rb +++ b/config/initializers/eager_load.rb @@ -17,4 +17,5 @@ # You should have received a copy of the GNU Affero General Public License # along with portalmec. If not, see <http://www.gnu.org/licenses/>. -Rails.application.eager_load! unless Rails.env.test? +# Rails.application.eager_load! unless Rails.env.test? + diff --git a/config/initializers/elasticsearch.rb b/config/initializers/elasticsearch.rb index 9a3b628ece9d05b53e0057027bb5652e122d92df..fef4adf01a3de0f4c99e50272e74a6cb1909f4ba 100644 --- a/config/initializers/elasticsearch.rb +++ b/config/initializers/elasticsearch.rb @@ -16,38 +16,28 @@ # # You should have received a copy of the GNU Affero General Public License # along with portalmec. If not, see <http://www.gnu.org/licenses/>. -if Rails.env.production? - login = ENV['PORTALMEC_ELASTICSEARCH_LOGIN'] - pass = ENV['PORTALMEC_ELASTICSEARCH_PASSWORD'] - host = ENV['PORTALMEC_ELASTICSEARCH_HOST'] - port = ENV['PORTALMEC_ELASTICSEARCH_PORT'] - elasticsearch_client = Elasticsearch::Client.new( - url: "https://" + login + ":" + pass + "@" + host + ":" + port, - transport_options: { - request: { - timeout: 550 - }, - ssl: { - verify: false - } - #, - #headers: { - # Authorization: "Basic " + Base64.strict_encode64(login + ":" + pass) - #} + +login = ENV['PORTALMEC_ELASTICSEARCH_LOGIN'] || "elastic" +pass = ENV['PORTALMEC_ELASTICSEARCH_PASSWORD'] || "" +host = ENV['PORTALMEC_ELASTICSEARCH_HOST'] || "elasticsearch" +port = ENV['PORTALMEC_ELASTICSEARCH_PORT'] || "9200" +elasticsearch_client = Elasticsearch::Client.new( + url: "https://" + login + ":" + pass + "@" + host + ":" + port, + transport_options: { + request: { + timeout: 550 + }, + ssl: { + verify: false } - ) -elsif Rails.env.test? - elasticsearch_client = Elasticsearch::Client.new( - url: 'localhost:9200', - transport_options: {request: {timeout: 550}} - ) -else - elasticsearch_client = Elasticsearch::Client.new( - url: 'localhost:9200', - transport_options: {request: {timeout: 550}} - ) -end + #, + #headers: { + # Authorization: "Basic " + Base64.strict_encode64(login + ":" + pass) + #} + } +) Searchkick.client = elasticsearch_client +Searchkick.index_prefix = ENV["PORTALMEC_ELASTICSEARCH_INDEX_PREFIX"] #Searchkick.redis = ConnectionPool.new { Redis.new } #Searchkick::ProcessQueueJob.perform_later(class_name: "LearningObject") diff --git a/docker-compose.yml b/docker-compose.yml index 107fbac4bc8028096540df9356344d956862b8a5..fd7c58684c63779cf8fbd73fdeb8630c169dd2dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,22 +17,42 @@ # You should have received a copy of the GNU Affero General Public License # along with portalmec. If not, see <http://www.gnu.org/licenses/>. -web: - build: . - volumes: - - .:/app - ports: - - "3000:3000" - links: - - db - - redis - - elasticsearch +version: '3.4' -db: - image: postgres +services: + app: + build: + context: . + dockerfile: Dockerfile + depends_on: + - redis + ports: + - "3000:3000" + volumes: + - .:/app + - gem_cache:/usr/local/bundle/gems + env_file: .env + extra_hosts: + - "host.docker.internal:host-gateway" -redis: - image: redis + redis: + image: redis:7.0.11-alpine -elasticsearch: - image: elasticsearch \ No newline at end of file + sidekiq: + build: + context: . + dockerfile: Dockerfile + depends_on: + - app + - redis + volumes: + - .:/app + - gem_cache:/usr/local/bundle/gems + env_file: .env + entrypoint: ./entrypoints/sidekiq-entrypoint.sh + extra_hosts: + - "host.docker.internal:host-gateway" + +volumes: + gem_cache: + db_data: diff --git a/entrypoints/docker-entrypoint.sh b/entrypoints/docker-entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..8ec86f8bd5566bfa58c25e58ff3f41c79993cbb3 --- /dev/null +++ b/entrypoints/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +set -e + +# If the container closes unexpectably the lockfile will not be removed and +# rails will refuse to start. So we remove at the start if it already exists. +if [ -f tmp/pids/server.pid ]; then + rm tmp/pids/server.pid +fi + +exec bundle exec puma -C config/puma.rb + diff --git a/entrypoints/init.sql b/entrypoints/init.sql new file mode 100755 index 0000000000000000000000000000000000000000..bd6b94778ccb5bac4d41ac4cb6a5213e4a9b9f8e --- /dev/null +++ b/entrypoints/init.sql @@ -0,0 +1,2 @@ +CREATE USER portalmec; +ALTER USER portalmec WITH SUPERUSER; diff --git a/entrypoints/sidekiq-entrypoint.sh b/entrypoints/sidekiq-entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..4f81ae2c56a8e1ab41e76ab115f50e508571867c --- /dev/null +++ b/entrypoints/sidekiq-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +if [ -f tmp/pids/server.pid ]; then + rm tmp/pids/server.pid +fi + +exec bundle exec sidekiq + diff --git a/env b/env deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/lib/log/database_logger.rb b/lib/log/database_logger.rb index dcb093a6af7513d4d333ca0d6838ea551d663c39..ba37bea425f5d492f29606b1926802a00c12b4db 100644 --- a/lib/log/database_logger.rb +++ b/lib/log/database_logger.rb @@ -17,39 +17,37 @@ # You should have received a copy of the GNU Affero General Public License # along with portalmec. If not, see <http://www.gnu.org/licenses/>. -module Log - class DatabaseLogger < Logger +class DatabaseLogger < Logger - CREATE = 5 - SELECT = 6 - UPDATE = 7 - DELETE = 8 + CREATE = 5 + SELECT = 6 + UPDATE = 7 + DELETE = 8 - SEVS = %w(DEBUG INFO WARN ERROR FATAL CREATE SELECT UPDATE DELETE) - def format_severity(severity) - SEVS[severity] || 'ANY' - end - - def create(progname = nil, &block) - add(5, nil, progname, &block) unless @db_lvl < 5 - end + SEVS = %w(DEBUG INFO WARN ERROR FATAL CREATE SELECT UPDATE DELETE) + def format_severity(severity) + SEVS[severity] || 'ANY' + end - def select(progname = nil, &block) - add(6, nil, progname, &block) unless @db_lvl < 6 - end + def create(progname = nil, &block) + add(5, nil, progname, &block) unless @db_lvl < 5 + end - def update(progname = nil, &block) - add(7, nil, progname, &block) unless @db_lvl < 7 - end + def select(progname = nil, &block) + add(6, nil, progname, &block) unless @db_lvl < 6 + end - def delete(progname = nil, &block) - add(8, nil, progname, &block) unless @db_lvl < 8 - end + def update(progname = nil, &block) + add(7, nil, progname, &block) unless @db_lvl < 7 + end - def level=(lvl) - @db_lvl = lvl - @level = lvl >= 5 ? 1 : lvl - end + def delete(progname = nil, &block) + add(8, nil, progname, &block) unless @db_lvl < 8 + end + def level=(lvl) + @db_lvl = lvl + @level = lvl >= 5 ? 1 : lvl end -end \ No newline at end of file + +end diff --git a/lib/log/logging.rb b/lib/log/logging.rb index 80d22c5e8e5ea3a61e6e7451fe03c909ebe54d2e..cff42f876402ef974b40c1a43375e411289f15ae 100644 --- a/lib/log/logging.rb +++ b/lib/log/logging.rb @@ -17,27 +17,25 @@ # You should have received a copy of the GNU Affero General Public License # along with portalmec. If not, see <http://www.gnu.org/licenses/>. -module Log - module Logging - def logger - Logging.logger - end - - def self.logger - if @logger.nil? - @logger = DatabaseLogger.new(nil) - @logger.level=-1 - end - @logger - end - - def self.logger=(logger) - @logger = logger - end +module Logging + def logger + Logging.logger + end - def self.disable_logging + def self.logger + if @logger.nil? @logger = DatabaseLogger.new(nil) + @logger.level=-1 end + @logger + end + def self.logger=(logger) + @logger = logger end -end \ No newline at end of file + + def self.disable_logging + @logger = DatabaseLogger.new(nil) + end + +end diff --git a/lib/portalmec/sociable_tests.rb b/lib/portalmec/sociable_tests.rb index d14964562eaf9e40428ae5a5411da66367bc2c09..d5c6d6aa13fdde3c1051feb91162635641684653 100644 --- a/lib/portalmec/sociable_tests.rb +++ b/lib/portalmec/sociable_tests.rb @@ -19,6 +19,8 @@ # require 'active_support' +SociableTests = 3 + # module Portalmec::SociableTests # extend ActiveSupport::Testing::Declarative @@ -88,4 +90,4 @@ # def sociable_user # users(:john) # end -# end \ No newline at end of file +# end diff --git a/lib/thumbnail/generatable_strategy.rb b/lib/thumbnail/generatable_strategy.rb index 185ba4e4653c522e51c72e9bb49f5469c1382e72..7904ece4046bec85b90ac78d92ae2f30f8c1c39e 100644 --- a/lib/thumbnail/generatable_strategy.rb +++ b/lib/thumbnail/generatable_strategy.rb @@ -21,25 +21,23 @@ require 'mimemagic' ## # This abstract class representes the interface to generate thumbnails -module Thumbnail - class GeneratableStrategy +class GeneratableStrategy - ## - # Generate a thumbnail based on the +media+ - # +media+ needs to be a pointer to a file - def generate(media) - raise NotImplementedError - end + ## + # Generate a thumbnail based on the +media+ + # +media+ needs to be a pointer to a file + def generate(media) + raise NotImplementedError + end - ## - # Checks if the strategy can generate a thumbnail of that +media+ - def can_generate?(media) - if File == media.class - mime = MimeMagic.by_magic(media) - return mime.type.include? yield unless mime.blank? - end - false + ## + # Checks if the strategy can generate a thumbnail of that +media+ + def can_generate?(media) + if File == media.class + mime = MimeMagic.by_magic(media) + return mime.type.include? yield unless mime.blank? end - + false end + end diff --git a/lib/thumbnail/strategies/image_thumbnail_generator.rb b/lib/thumbnail/strategies/image_thumbnail_generator.rb index a5f7533d85e27ef31b1902d66a88a26ee450da6c..a5bad89bbc1589fd7c0886b415dfa36cad1eab68 100644 --- a/lib/thumbnail/strategies/image_thumbnail_generator.rb +++ b/lib/thumbnail/strategies/image_thumbnail_generator.rb @@ -17,18 +17,14 @@ # You should have received a copy of the GNU Affero General Public License # along with portalmec. If not, see <http://www.gnu.org/licenses/>. -module Thumbnail - module Strategies - class ImageThumbnailGenerator < ::Thumbnail::GeneratableStrategy +class ImageThumbnailGenerator < ::GeneratableStrategy - def generate(media) - media - end - - def can_generate?(media) - super { 'image' } - end + def generate(media) + media + end - end + def can_generate?(media) + super { 'image' } end + end diff --git a/lib/thumbnail/strategies/pdf_thumbnail_generator.rb b/lib/thumbnail/strategies/pdf_thumbnail_generator.rb index 624bb3e26a3d15bf4dc23ebd3bdf85293b412d97..52b812c520da79954f70a4b73d7b8b57188760d0 100644 --- a/lib/thumbnail/strategies/pdf_thumbnail_generator.rb +++ b/lib/thumbnail/strategies/pdf_thumbnail_generator.rb @@ -17,31 +17,27 @@ # You should have received a copy of the GNU Affero General Public License # along with portalmec. If not, see <http://www.gnu.org/licenses/>. -module Thumbnail - module Strategies - class PdfThumbnailGenerator < ::Thumbnail::GeneratableStrategy +class PdfThumbnailGenerator < GeneratableStrategy - def generate(media) - pdf = first_page(media.path) - return nil if pdf.blank? + def generate(media) + pdf = first_page(media.path) + return nil if pdf.blank? - hash = SecureRandom.hex(10) - output = "/tmp/#{hash}.png" - pdf.write(output) - File.open output - end - - def can_generate?(media) - super { 'pdf' } - end + hash = SecureRandom.hex(10) + output = "/tmp/#{hash}.png" + pdf.write(output) + File.open output + end - private + def can_generate?(media) + super { 'pdf' } + end - def first_page(pdf_path) - first_page_path = pdf_path + "[0]" - Magick::Image.read(first_page_path).first - end + private - end + def first_page(pdf_path) + first_page_path = pdf_path + "[0]" + Magick::Image.read(first_page_path).first end + end diff --git a/lib/thumbnail/strategies/url_thumbnail_generator.rb b/lib/thumbnail/strategies/url_thumbnail_generator.rb index e8a9dbb961e310d4df6408570b8e57ff21710b30..ee7d90d2549e6271627b9662ee990ebd0caf7472 100644 --- a/lib/thumbnail/strategies/url_thumbnail_generator.rb +++ b/lib/thumbnail/strategies/url_thumbnail_generator.rb @@ -19,21 +19,17 @@ require 'screencap' -module Thumbnail - module Strategies - class UrlThumbnailGenerator < ::Thumbnail::GeneratableStrategy +class UrlThumbnailGenerator < GeneratableStrategy - def generate(screen) - screen.fetch( - width: 1024, - height: 768 - ) - end - - def can_generate?(screen) - ::Screencap::Fetcher == screen.class - end + def generate(screen) + screen.fetch( + width: 1024, + height: 768 + ) + end - end + def can_generate?(screen) + ::Screencap::Fetcher == screen.class end + end diff --git a/lib/thumbnail/strategies/video_thumbnail_generator.rb b/lib/thumbnail/strategies/video_thumbnail_generator.rb index 4f0ea0bf2115c6a5208c2d63266e6ddb7d212e8d..529d57e1758ad1b3f97e9a327e8942af6fcbf652 100644 --- a/lib/thumbnail/strategies/video_thumbnail_generator.rb +++ b/lib/thumbnail/strategies/video_thumbnail_generator.rb @@ -19,39 +19,35 @@ require 'streamio-ffmpeg' -module Thumbnail - module Strategies - class VideoThumbnailGenerator < ::Thumbnail::GeneratableStrategy - - def generate(media) - movie = build_ffmpeg_movie media.path - hash = SecureRandom.hex(10) - output = "/tmp/#{hash}.jpg" - - if movie.valid? and !movie.video_codec.nil? - frame = (movie.duration * 25/100).floor - movie.screenshot(output, - {seek_time: frame, resolution: size}, - preserve_aspect_ratio: :width) - File.open output - else - raise "ERROR: Video's thumbnail was not generated. (NULL_CODEC)" - end - end - - def can_generate?(media) - super { 'video' } - end - private - - def size - '530x300' - end - - def build_ffmpeg_movie(path) - ::FFMPEG::Movie.new(path) - end - +class VideoThumbnailGenerator < GeneratableStrategy + + def generate(media) + movie = build_ffmpeg_movie media.path + hash = SecureRandom.hex(10) + output = "/tmp/#{hash}.jpg" + + if movie.valid? and !movie.video_codec.nil? + frame = (movie.duration * 25/100).floor + movie.screenshot(output, + {seek_time: frame, resolution: size}, + preserve_aspect_ratio: :width) + File.open output + else + raise "ERROR: Video's thumbnail was not generated. (NULL_CODEC)" end end + + def can_generate?(media) + super { 'video' } + end + private + + def size + '530x300' + end + + def build_ffmpeg_movie(path) + ::FFMPEG::Movie.new(path) + end + end