Use shrine to upload files instead of ActiveStorage

This commit is contained in:
Eugene Burmakin 2024-04-25 22:28:34 +02:00
parent d99e6d6f50
commit e00f614b9a
14 changed files with 64 additions and 26 deletions

16
Gemfile
View file

@ -5,31 +5,31 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '3.2.3' ruby '3.2.3'
gem 'bootsnap', require: false gem 'bootsnap', require: false
gem 'chartkick'
gem 'devise' gem 'devise'
gem 'geocoder'
gem 'importmap-rails'
gem 'pg' gem 'pg'
gem 'puma' gem 'puma'
gem 'pundit' gem 'pundit'
gem 'rails' gem 'rails'
gem 'shrine', '~> 3.5'
gem 'sidekiq'
gem 'sidekiq-cron'
gem 'sprockets-rails' gem 'sprockets-rails'
gem 'stimulus-rails' gem 'stimulus-rails'
gem 'tailwindcss-rails' gem 'tailwindcss-rails'
gem 'turbo-rails' gem 'turbo-rails'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
gem 'importmap-rails'
gem 'chartkick'
gem 'geocoder'
gem 'sidekiq'
gem 'sidekiq-cron'
group :development, :test do group :development, :test do
gem 'debug', platforms: %i[mri mingw x64_mingw] gem 'debug', platforms: %i[mri mingw x64_mingw]
gem 'dotenv-rails'
gem 'factory_bot_rails' gem 'factory_bot_rails'
gem 'ffaker' gem 'ffaker'
gem 'rspec-rails'
gem 'dotenv-rails'
gem 'pry-byebug' gem 'pry-byebug'
gem 'pry-rails' gem 'pry-rails'
gem 'rspec-rails'
end end
group :test do group :test do

View file

@ -75,6 +75,8 @@ GEM
minitest (>= 5.1) minitest (>= 5.1)
mutex_m mutex_m
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2) ast (2.4.2)
attr_extras (7.1.0) attr_extras (7.1.0)
base64 (0.2.0) base64 (0.2.0)
@ -88,6 +90,7 @@ GEM
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.2.3) concurrent-ruby (1.2.3)
connection_pool (2.4.1) connection_pool (2.4.1)
content_disposition (1.0.0)
crass (1.0.6) crass (1.0.6)
date (3.3.4) date (3.3.4)
debug (1.9.2) debug (1.9.2)
@ -105,6 +108,8 @@ GEM
dotenv-rails (3.1.0) dotenv-rails (3.1.0)
dotenv (= 3.1.0) dotenv (= 3.1.0)
railties (>= 6.1) railties (>= 6.1)
down (5.4.2)
addressable (~> 2.8)
drb (2.2.1) drb (2.2.1)
erubi (1.12.0) erubi (1.12.0)
et-orbi (1.2.11) et-orbi (1.2.11)
@ -189,6 +194,7 @@ GEM
pry (>= 0.10.4) pry (>= 0.10.4)
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (5.0.5)
puma (6.4.2) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.1) pundit (2.3.1)
@ -285,6 +291,9 @@ GEM
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
shoulda-matchers (6.2.0) shoulda-matchers (6.2.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
shrine (3.5.0)
content_disposition (~> 1.0)
down (~> 5.1)
sidekiq (7.2.2) sidekiq (7.2.2)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
@ -372,6 +381,7 @@ DEPENDENCIES
rspec-rails rspec-rails
rubocop-rails rubocop-rails
shoulda-matchers shoulda-matchers
shrine (~> 3.5)
sidekiq sidekiq
sidekiq-cron sidekiq-cron
simplecov simplecov

File diff suppressed because one or more lines are too long

View file

@ -15,13 +15,14 @@ class ImportsController < ApplicationController
def create def create
files = import_params[:files].reject(&:blank?) files = import_params[:files].reject(&:blank?)
import_ids = files.map do |file| import_ids = files.map do |file|
import = current_user.imports.create( import = current_user.imports.create(
name: file.original_filename, name: file.original_filename,
source: params[:import][:source] source: params[:import][:source]
) )
import.file.attach(file) import.update(raw_data: JSON.parse(File.read(file)))
import.id import.id
end end

View file

@ -1,12 +1,11 @@
# frozen_string_literal: true
class ImportJob < ApplicationJob class ImportJob < ApplicationJob
queue_as :default queue_as :default
def perform(user_id, import_id) def perform(user_id, import_id)
user = User.find(user_id) user = User.find(user_id)
import = user.imports.find(import_id) import = user.imports.find(import_id)
file = import.file
sleep 3 # It takes time to process uploaded file
result = parser(import.source).new(import).call result = parser(import.source).new(import).call

View file

@ -1,8 +1,10 @@
# frozen_string_literal: true
class Import < ApplicationRecord class Import < ApplicationRecord
belongs_to :user belongs_to :user
has_many :points, dependent: :destroy has_many :points, dependent: :destroy
has_one_attached :file include ImportUploader::Attachment(:raw)
enum source: { google: 0, owntracks: 1 } enum source: { google: 0, owntracks: 1 }
end end

View file

@ -1,11 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class GoogleMaps::TimelineParser class GoogleMaps::TimelineParser
attr_reader :import, :json attr_reader :import
def initialize(import) def initialize(import)
@import = import @import = import
@json = JSON.parse(import.file.download)
end end
def call def call
@ -38,7 +37,7 @@ class GoogleMaps::TimelineParser
private private
def parse_json def parse_json
json['timelineObjects'].flat_map do |timeline_object| import.raw_data['timelineObjects'].flat_map do |timeline_object|
if timeline_object['activitySegment'].present? if timeline_object['activitySegment'].present?
if timeline_object['activitySegment']['startLocation'].blank? if timeline_object['activitySegment']['startLocation'].blank?
next if timeline_object['activitySegment']['waypointPath'].blank? next if timeline_object['activitySegment']['waypointPath'].blank?
@ -61,7 +60,7 @@ class GoogleMaps::TimelineParser
end end
elsif timeline_object['placeVisit'].present? elsif timeline_object['placeVisit'].present?
if timeline_object['placeVisit']['location']['latitudeE7'].present? && if timeline_object['placeVisit']['location']['latitudeE7'].present? &&
timeline_object['placeVisit']['location']['longitudeE7'].present? timeline_object['placeVisit']['location']['longitudeE7'].present?
{ {
latitude: timeline_object['placeVisit']['location']['latitudeE7'].to_f / 10**7, latitude: timeline_object['placeVisit']['location']['latitudeE7'].to_f / 10**7,
longitude: timeline_object['placeVisit']['location']['longitudeE7'].to_f / 10**7, longitude: timeline_object['placeVisit']['location']['longitudeE7'].to_f / 10**7,

View file

@ -5,7 +5,7 @@ class OwnTracks::ExportParser
def initialize(import) def initialize(import)
@import = import @import = import
@json = JSON.parse(import.file.download) @json = import.raw_data
end end
def call def call
@ -32,7 +32,7 @@ class OwnTracks::ExportParser
doubles = points_data.size - points doubles = points_data.size - points
processed = points + doubles processed = points + doubles
{ raw_points: points_data.size, points: points, doubles: doubles, processed: processed } { raw_points: points_data.size, points:, doubles:, processed: }
end end
private private
@ -40,8 +40,8 @@ class OwnTracks::ExportParser
def parse_json def parse_json
points = [] points = []
json.keys.each do |user| json.each_key do |user|
json[user].keys.each do |devise| json[user].each_key do |devise|
json[user][devise].each { |point| points << OwnTracks::Params.new(point).call } json[user][devise].each { |point| points << OwnTracks::Params.new(point).call }
end end
end end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class ImportUploader < Shrine
# plugins and uploading logic
end

13
config/shrine.rb Normal file
View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'shrine'
require 'shrine/storage/file_system'
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/cache'), # temporary
store: Shrine::Storage::FileSystem.new('public', prefix: 'uploads') # permanent
}
Shrine.plugin :activerecord # loads Active Record integration
Shrine.plugin :cached_attachment_data # enables retaining cached file across form redisplays
Shrine.plugin :restore_cached_data # extracts metadata for assigned cached files

View file

@ -0,0 +1,5 @@
class AddRawDataToImports < ActiveRecord::Migration[7.1]
def change
add_column :imports, :raw_data, :jsonb
end
end

3
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_04_04_154959) do ActiveRecord::Schema[7.1].define(version: 2024_04_25_200155) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -51,6 +51,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_04_04_154959) do
t.integer "raw_points", default: 0 t.integer "raw_points", default: 0
t.integer "doubles", default: 0 t.integer "doubles", default: 0
t.integer "processed", default: 0 t.integer "processed", default: 0
t.jsonb "raw_data"
t.index ["source"], name: "index_imports_on_source" t.index ["source"], name: "index_imports_on_source"
t.index ["user_id"], name: "index_imports_on_user_id" t.index ["user_id"], name: "index_imports_on_user_id"
end end

View file

@ -1,7 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :import do factory :import do
user user
name { 'APRIL_2013.json' } name { 'APRIL_2013.json' }
source { 1 } source { 1 }
raw_data { JSON.parse(File.read('spec/fixtures/files/owntracks/export.json')) }
end end
end end

View file

@ -1,13 +1,13 @@
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.describe OwnTracks::ExportParser do RSpec.describe OwnTracks::ExportParser do
describe '#call' do describe '#call' do
subject(:parser) { described_class.new(import).call } subject(:parser) { described_class.new(import).call }
let(:file_path) { 'spec/fixtures/files/owntracks/export.json' }
let(:file) { fixture_file_upload(file_path) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:import) { create(:import, user: user, file: file, name: File.basename(file.path)) } let(:import) { create(:import, user:, name: 'owntracks_export.json') }
context 'when file exists' do context 'when file exists' do
it 'creates points' do it 'creates points' do