diff --git a/Gemfile b/Gemfile index 47251781ebcca4f2a51a6ab023e1b24005ad0a4d..cfcb968b1c9b7c34a1f224931644c280ebe7a814 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,9 @@ gem 'bcrypt', '~> 3.1.7' # memcached gem 'dalli' +# dalli multi thread gem +gem 'connection_pool' + # web server gem 'puma' diff --git a/app/controllers/concerns/downloadable_controller.rb b/app/controllers/concerns/downloadable_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..4ab7cfae8ef2728001f78bb147057f308f9c92c8 --- /dev/null +++ b/app/controllers/concerns/downloadable_controller.rb @@ -0,0 +1,16 @@ +module DownloadableController + extend ActiveSupport::Concern + + # GET /learning_objects/1/download + def download + downloadable.download(current_user, request.remote_ip) + redirect_to downloadable.download_link + end + + protected + + def downloadable + raise NotImplementedError + end + +end diff --git a/app/controllers/v1/collections_controller.rb b/app/controllers/v1/collections_controller.rb index b203f6b3164caa7a8eaf3a2cce6f8e2f6cb8c7e5..361c26be05f1eb69f8bfd43029d5479188bb049b 100644 --- a/app/controllers/v1/collections_controller.rb +++ b/app/controllers/v1/collections_controller.rb @@ -1,5 +1,6 @@ class V1::CollectionsController < ApplicationController include ::SociableController + include ::DownloadableController include ::FollowableController include ::TaggableController include ::DeletedObjectsController @@ -11,7 +12,7 @@ class V1::CollectionsController < ApplicationController before_action :authenticate_user!, only: [:create, :update, :destroy] before_action :set_collection, only: [:show, :update, :destroy, :add_object, :delete_object, :subjecting, :unsubjecting, :add_stages, :remove_stages] before_action :set_new_collection, only: :index - before_action :authorize!, except: [:create, :tagging, :untagging, :follow, :unfollow] + before_action :authorize!, except: [:create, :tagging, :untagging, :follow, :unfollow, :download] # GET /v1/collections # GET /v1/collections.json @@ -83,6 +84,7 @@ class V1::CollectionsController < ApplicationController def followable; set_collection; end def taggable; set_collection; end def sociable; set_collection; end + def downloadable; set_collection; end def subjectable; set_collection; end def stageable; set_collection; end diff --git a/app/controllers/v1/learning_objects_controller.rb b/app/controllers/v1/learning_objects_controller.rb index 459ec9a07c4263cf215418ec326424d989b2b2a7..31fcba21dc35e7c653b058e5968b27686f280a96 100644 --- a/app/controllers/v1/learning_objects_controller.rb +++ b/app/controllers/v1/learning_objects_controller.rb @@ -2,6 +2,7 @@ require 'uri' class V1::LearningObjectsController < ApplicationController include ::SociableController + include ::DownloadableController include ::TaggableController include ::Paginator include ::DeletedObjectsController @@ -12,10 +13,9 @@ class V1::LearningObjectsController < ApplicationController before_action :authenticate_user!, only: [:create, :update, :destroy] before_action :set_learning_object, only: [:show, :update, :destroy, :subjecting, :unsubjecting, :add_stages, :remove_stages] before_action :set_new_learning_object, only: :index - before_action :authorize!, except: [:create, :tagging, :untagging] + before_action :authorize!, except: [:create, :tagging, :untagging, :download] before_action :set_paper_trail_whodunnit - def index learning_objects = paginate LearningObject.includes(:tags, :publisher, :language, :license, :subjects, :educational_stages, :reviews) serializer = params[:obaa].nil? ? LearningObjectSerializer : LearningObjectObaaSerializer @@ -72,6 +72,7 @@ class V1::LearningObjectsController < ApplicationController def deleted_resource; LearningObject; end def highlights_resource; LearningObject; end def sociable; set_learning_object; end + def downloadable; set_learning_object; end def taggable; set_learning_object; end def subjectable; set_learning_object; end def stageable; set_learning_object; end diff --git a/app/models/collection.rb b/app/models/collection.rb index f3b7f2745949ffd7685e585600c73272172b3165..9f42147f89f82c1e02f0dad4ef9f56f38c7a59fa 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -25,6 +25,7 @@ class Collection < ApplicationRecord include Reviewable include Sociable + include Downloadable include Followable include Scoreable include Thumbnailable @@ -101,4 +102,8 @@ class Collection < ApplicationRecord def user_category owner.try('user_category') end + + def download_link + '/'+PackageService.link(self) + end end diff --git a/app/models/concerns/downloadable.rb b/app/models/concerns/downloadable.rb new file mode 100644 index 0000000000000000000000000000000000000000..b813ac8371334bafc5a3394cc4cf020be5a17cda --- /dev/null +++ b/app/models/concerns/downloadable.rb @@ -0,0 +1,21 @@ +module Downloadable + extend ActiveSupport::Concern + + included do + has_many :downloads, as: :downloadable, dependent: :destroy + has_many :package_items, as: :packageable, dependent: :destroy + has_many :packages, through: :package_items, dependent: :destroy + end + + def download(user, ip) + Download.where(user: user, ip: ip, downloadable: self).first_or_create + end + + def downloaded?(user, ip) + !Download.where(user: user, ip: ip, downloadable: self).blank? + end + + def download_link + raise NotImplementedError + end +end diff --git a/app/models/concerns/sociable.rb b/app/models/concerns/sociable.rb index b3bf6ab3a954dc09bfb98784e35d25d668dd5cfd..2bc9fcb05f5896e3a1b7e2162afe4a1f52434f2f 100644 --- a/app/models/concerns/sociable.rb +++ b/app/models/concerns/sociable.rb @@ -3,7 +3,6 @@ module Sociable included do has_many :views, as: :viewable, dependent: :destroy - has_many :downloads, as: :downloadable, dependent: :destroy has_many :likes, as: :likeable, dependent: :destroy has_many :shares, as: :shareable, dependent: :destroy end @@ -20,14 +19,6 @@ module Sociable Like.where(user: user, likeable: self).destroy_all end - def download(user) - Download.create(user: user, downloadable: self) - end - - def downloaded?(user) - !Download.where(user: user, downloadable: self).blank? - end - def share(user) Share.create(user: user, shareable: self) end diff --git a/app/models/download.rb b/app/models/download.rb index abebce69951491c8e64051b7841f3bd7995eda59..9812839a856ffcc25eebdacee249a507b5166125 100644 --- a/app/models/download.rb +++ b/app/models/download.rb @@ -8,16 +8,16 @@ # user_id :integer # created_at :datetime not null # updated_at :datetime not null -# +# ip :string not null class Download < ApplicationRecord # *current_user* download *downloadable* include Trackable belongs_to :downloadable, polymorphic: true, counter_cache: true - belongs_to :user + belongs_to :user, optional: true - validates_presence_of :user, :downloadable + validates_presence_of :ip, :downloadable def recipient downloadable diff --git a/app/models/learning_object.rb b/app/models/learning_object.rb index d47b733e0e2c92e8453294fdb55f361c41644ac8..fca93372bb6018ca9f4f50cae448bec9bd0dfc5d 100644 --- a/app/models/learning_object.rb +++ b/app/models/learning_object.rb @@ -34,6 +34,7 @@ class LearningObject < ApplicationRecord include Metadatable include Reviewable include Sociable + include Downloadable include Stateful include Scoreable include Thumbnailable @@ -124,6 +125,10 @@ class LearningObject < ApplicationRecord default_attachment.retrieve_cache_link end + def download_link + object_type.try(:name) == ("VÃdeo" || "Ãudio") ? default_attachment.try(:retrieve_cache_link) : default_attachment.try(:retrieve_url) + end + ## score methods def normalized_collected max = CollectionItem.where(collectionable_type: 'LearningObject').group(:collectionable_id).order('count_all DESC').count diff --git a/app/models/package.rb b/app/models/package.rb new file mode 100644 index 0000000000000000000000000000000000000000..3586b50125a959a323b808a365deadf194e1ba14 --- /dev/null +++ b/app/models/package.rb @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: packages +# +# package_items_id integer +# created_at datetime null: false +# updated_at datetime null: false +# file_path string + +class Package < ApplicationRecord + has_many :package_items, dependent: :destroy + + validates_presence_of :file_path + + def add_items(items) + items.each do |item| + package_item = PackageItem.where(package: self, packageable: item).first_or_create + end + end +end diff --git a/app/models/package_item.rb b/app/models/package_item.rb new file mode 100644 index 0000000000000000000000000000000000000000..30952f09b528b0ffc10c1709ff381d0cf9a773aa --- /dev/null +++ b/app/models/package_item.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: package_items +# +# package_id integer +# packageable_type string +# packageable_id integer +# created_at datetime null: false +# updated_at datetime null: false + +class PackageItem < ApplicationRecord + belongs_to :package + belongs_to :packageable, polymorphic: true + + validates :package, :packageable, presence: true +end diff --git a/app/serializers/learning_object_serializer.rb b/app/serializers/learning_object_serializer.rb index 131f2278481ab30987cfcbac083e4c64c1e93a11..b1cdec145a3a0d84a079128a47c852b616e526e3 100644 --- a/app/serializers/learning_object_serializer.rb +++ b/app/serializers/learning_object_serializer.rb @@ -1,10 +1,6 @@ class LearningObjectSerializer < ActiveModel::Serializer cache key: 'learning_object', expires_in: 4.hours, except: [:likes_count, :liked, :reviewed, :complained] - def default_location - object_type == ("VÃdeo" || "Ãudio") ? object.default_attachment.try(:retrieve_cache_link) : object.default_attachment.try(:retrieve_url) - end - def default_mime_type object.default_attachment.try(:mime_type) end @@ -42,7 +38,6 @@ class LearningObjectSerializer < ActiveModel::Serializer :object_type, :language, :default_attachment_id, - :default_location, :default_mime_type, :score, :state, diff --git a/app/services/package_service.rb b/app/services/package_service.rb index 945b4aaf27a8d05aec89b9d109763d6989baaff1..12928d0ad0c1b7bc1f6a38506de0c25de22247c3 100644 --- a/app/services/package_service.rb +++ b/app/services/package_service.rb @@ -8,7 +8,7 @@ module PackageService end def self.dirname - 'download' + 'packages' end def self.job_key(key) diff --git a/app/services/package_service/generator.rb b/app/services/package_service/generator.rb index ded3003f5f14968f4454c4bdb5e8ec840790f61c..330bc58c7abcc50429c8f747f9fbef69c738694e 100644 --- a/app/services/package_service/generator.rb +++ b/app/services/package_service/generator.rb @@ -1,27 +1,25 @@ module PackageService class Generator + + attr_reader :objects + def initialize(objects) @objects = objects.is_a?(Array) ? objects : [objects] - @cache_key = nil + @filename = nil raise 'Invalid objects for PackageService' unless valid? end def generate - Rails.cache.write(PackageService.job_key(cache_key), 'wait') - PackageWorker.perform_async(objects_map, filename, cache_key) + PackageWorker.perform_async(objects_map, filename) end - def cache_key - return @cache_key unless @cache_key.nil? - - key_name = 'package_service' - cache_list = [] - - @objects.sort_by!(&:id) - @objects.each { |o| cache_list << o.cache_key } - - @cache_key = "#{key_name}[#{cache_list.join(',')}]" + def filename + while @filename.nil? do + name = "#{SecureRandom.hex(12)}.zip" + @filename = name unless File.exist?(PackageService.file_root(name)) + end + @filename end private @@ -36,13 +34,6 @@ module PackageService true end - def filename - loop do - name = "#{SecureRandom.hex(12)}.zip" - return name unless File.exist?(PackageService.file_root(name)) - end - end - def objects_map @objects.map { |o| { class: o.class, id: o.id } } end diff --git a/app/services/package_service/link.rb b/app/services/package_service/link.rb index 897fdc5f8ddcda37b9186ef3c683bbd1d6da25e8..433aeab02c7cf7e81baecc7e54710959edac6ad9 100644 --- a/app/services/package_service/link.rb +++ b/app/services/package_service/link.rb @@ -1,39 +1,50 @@ module PackageService class Link - def initialize(object) - @generator = Generator.new(object) - @cache_key = @generator.cache_key + def initialize(objects) + @generator = Generator.new(objects) end def link - link = retrieve_link - + link = get_package_link if link.nil? @generator.generate link = retrieve_link end - - "#{PackageService.dirname}/#{link}" unless link.nil? + link end private + def get_package_link + package = PackageItem.select("package_id").where(:packageable => @generator.objects).group("package_id") + return nil if package.blank? + link = Package.find(package.first.package_id).file_path + if !File.exist?(Rails.root.join('public',link)) + Package.destroy(package.first.package_id) + return nil + end + link + end + def retrieve_link if wait_job - filename = Rails.cache.fetch(@cache_key) - return filename if !filename.nil? && File.exist?(PackageService.file_root(filename)) + filename = @generator.filename - Rails.cache.delete(@cache_key) if Rails.cache.exist?(@cache_key) + filepath = PackageService.dirname+'/'+filename + package = Package.create(file_path: filepath) + package.add_items(@generator.objects) + return package.file_path end nil end def wait_job - job_key = PackageService.job_key(@cache_key) - unless Rails.cache.fetch(job_key).nil? + unless File.exist?(PackageService.file_root(@generator.filename)) Timeout.timeout(60) do - sleep(1.0) until Rails.cache.fetch(job_key).nil? && !Rails.cache.fetch(@cache_key).nil? + begin + sleep(1.0) + end until File.exist?(PackageService.file_root(@generator.filename)) end end true diff --git a/app/workers/package_worker.rb b/app/workers/package_worker.rb index cfaa9712ba25cdc8ffdc46393ac79453bc5e6fe1..427166f4705152b8cbb99a5b9afc59ac56008300 100644 --- a/app/workers/package_worker.rb +++ b/app/workers/package_worker.rb @@ -1,35 +1,25 @@ +require_dependency 'dspace' +require 'rails' class PackageWorker include Sidekiq::Worker require 'zip' sidekiq_options queue: :package_cache - def perform(objects_ids = nil, filename = nil, cache_key = nil) - return false if objects_ids.blank? || filename.blank? || cache_key.blank? - @cache_key = cache_key + def perform(objects_ids = nil, filename = nil) + return false if objects_ids.blank? || filename.blank? + Bundler.require(*Rails.groups) - return true if file_exist?(PackageService.file_root(filename)) - - Rails.cache.write(PackageService.job_key(cache_key), 'wait') + return true if File.exist?(PackageService.file_root(filename)) files = open_files(objects_ids) create_package(filename, files) ensure close_files(files) unless files.nil? - job_key = PackageService.job_key(cache_key) - Rails.cache.delete(job_key) if Rails.cache.exist?(job_key) end private - def file_exist?(path) - if File.exist?(path) - cache_fetch(path.split('/').last) - return true - end - false - end - def open_files(objects_ids) objects = objects_ids.map { |o| o['class'].constantize.find(o['id']) } @@ -61,7 +51,6 @@ class PackageWorker Zip::File.open(PackageService.file_root(filename), Zip::File::CREATE) do |zipfile| files.each { |file| zipfile.add(File.basename(file.path), file.path) } end - cache_fetch(filename) rescue => e file = PackageService.file_root(filename) FileUtils.rm(file) if File.exist?(file) @@ -69,7 +58,7 @@ class PackageWorker end def open_file(file_path) - path = Rails.root.join('public', file_path) + path = Rails.root.join("public" + file_path) return File.open(path) if File.exist? path open_dspace_file(file_path) end @@ -91,13 +80,4 @@ class PackageWorker FileUtils.rm(path) if path =~ %r{^\/tmp\/.*} && File.exist?(path) end end - - def cache_fetch(value) - filename = Rails.cache.fetch(@cache_key) - file = PackageService.file_root(filename) unless filename.nil? - FileUtils.rm(file) if !file.nil? && File.exist?(file) - - Rails.cache.delete(@cache_key) - Rails.cache.write(@cache_key, value) - end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 9724cd4866a8248ceb1b79d1ec045fd170079787..9a835b03ca5ee8d11c8df3056ae2650ebfcfca35 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -28,7 +28,8 @@ Rails.application.configure do if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true - config.cache_store = :memory_store + config.cache_store = :dalli_store, nil, { :namespace => 'portalmec', :expires_in => 1.day, :compress => true, :pool_size => 5 } + config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=172800' } @@ -51,6 +52,8 @@ Rails.application.configure do config.log_level = :debug + config.public_file_server.enabled = true + # Raises error for missing translations # config.action_view.raise_on_missing_translations = true diff --git a/config/routes.rb b/config/routes.rb index bf6e24c69db14c16ee3a9580e62b5f469d4e1a3d..c314a4533c81a8bfed191612d32c3f98c73169e8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -43,6 +43,12 @@ Rails.application.routes.draw do end end + concern :downloadable do + member do + get 'download', as: :download, action: :download + end + end + concern :reviewable do resources :reviews, only: [:index, :create, :update, :destroy], concerns: :deletable do member do @@ -110,14 +116,14 @@ Rails.application.routes.draw do end end - resources :collections, concerns: [:followable, :sociable, :reviewable, :taggable, :versionable, :deletable, :highlights, :subjectable, :stageable] do + resources :collections, concerns: [:followable, :sociable, :downloadable, :reviewable, :taggable, :versionable, :deletable, :highlights, :subjectable, :stageable] do member do post :items, to: 'collections#add_object' delete :items, to: 'collections#delete_object' end end - resources :learning_objects, concerns: [:sociable, :reviewable, :taggable, :versionable, :deletable, :highlights, :subjectable, :stageable] do + resources :learning_objects, concerns: [:sociable, :downloadable, :reviewable, :taggable, :versionable, :deletable, :highlights, :subjectable, :stageable] do member do resource :chunk, module: 'learning_objects', only: [:create, :show] resource :upload, module: 'learning_objects', only: :create diff --git a/db/migrate/20170215113455_add_ip_to_downloads.rb b/db/migrate/20170215113455_add_ip_to_downloads.rb new file mode 100644 index 0000000000000000000000000000000000000000..8433592385ec31fbdeee87ab2f444958d0bac58a --- /dev/null +++ b/db/migrate/20170215113455_add_ip_to_downloads.rb @@ -0,0 +1,5 @@ +class AddIpToDownloads < ActiveRecord::Migration[5.0] + def change + add_column :downloads, :ip, :string, null: false + end +end diff --git a/db/migrate/20170215121042_add_index_to_downloads.rb b/db/migrate/20170215121042_add_index_to_downloads.rb new file mode 100644 index 0000000000000000000000000000000000000000..62859d2889387624c75b65872e6a469743531f28 --- /dev/null +++ b/db/migrate/20170215121042_add_index_to_downloads.rb @@ -0,0 +1,5 @@ +class AddIndexToDownloads < ActiveRecord::Migration[5.0] + def change + add_index :downloads, [:user_id, :ip, :downloadable_type, :downloadable_id], unique: true, name: 'user_ip_and_downloadable' + end +end diff --git a/db/migrate/20170223132327_create_packages.rb b/db/migrate/20170223132327_create_packages.rb new file mode 100644 index 0000000000000000000000000000000000000000..c2e22f4584b5e4b9fa51e545a59f11e7d4ef8b5f --- /dev/null +++ b/db/migrate/20170223132327_create_packages.rb @@ -0,0 +1,9 @@ +class CreatePackages < ActiveRecord::Migration[5.0] + def change + create_table :packages do |t| + t.references :package_items, index: true + + t.timestamps + end + end +end diff --git a/db/migrate/20170223132838_create_package_items.rb b/db/migrate/20170223132838_create_package_items.rb new file mode 100644 index 0000000000000000000000000000000000000000..14352c19c28f655183b8f548227c2eb391af77d2 --- /dev/null +++ b/db/migrate/20170223132838_create_package_items.rb @@ -0,0 +1,10 @@ +class CreatePackageItems < ActiveRecord::Migration[5.0] + def change + create_table :package_items do |t| + t.references :package, foreign_key: true + t.references :packageable, polymorphic: true + + t.timestamps + end + end +end diff --git a/db/migrate/20170224145548_add_filepath_to_package.rb b/db/migrate/20170224145548_add_filepath_to_package.rb new file mode 100644 index 0000000000000000000000000000000000000000..4ebbf5091493393108594f9a9514897e76480f0f --- /dev/null +++ b/db/migrate/20170224145548_add_filepath_to_package.rb @@ -0,0 +1,5 @@ +class AddFilepathToPackage < ActiveRecord::Migration[5.0] + def change + add_column :packages, :file_path, :string + end +end