From 4548d6526e9319b367ecd3a159b28cdd0f2f1b82 Mon Sep 17 00:00:00 2001 From: Mattia Roccoberton Date: Sun, 27 Feb 2022 10:09:39 +0100 Subject: [PATCH] Improve rake tasks --- README.md | 14 +++- lib/tasks/active_storage_db_tasks.rake | 71 ++++++++++++----- spec/tasks/active_storage_db_tasks_spec.rb | 90 ++++++++++++++-------- 3 files changed, 120 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 564c2e8..1788d04 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ An Active Storage service upload/download plugin that stores files in a PostgreSQL or MySQL database. Main features: -- supports Rails 6.0, 6.1 and 7.0; +- supports Rails _6.0_, _6.1_ and _7.0_; - all service methods implemented; - attachment data stored in a binary field (or blob). @@ -30,10 +30,16 @@ db: ## Misc -Some rake tasks are available: +Some utility tasks are available: -- `asdb:list`: list the stored attachments -- `asdb:get`: download an attachment (ex. `bin/rails "asdb:get[ruby-logo.png,/tmp]"`) +```sh +# list attachments ordered by blob id desc (with limit 100): +bin/rails 'asdb:list' +# search attachments by filename (or part of it) +bin/rails 'asdb:search[some_filename]' +# download attachment by blob id (retrieved with list or search tasks) - the second argument is the destination: +bin/rails 'asdb:download[123,/tmp]' +``` ## Do you like it? Star it! diff --git a/lib/tasks/active_storage_db_tasks.rake b/lib/tasks/active_storage_db_tasks.rake index 396691a..25d60a6 100644 --- a/lib/tasks/active_storage_db_tasks.rake +++ b/lib/tasks/active_storage_db_tasks.rake @@ -1,31 +1,64 @@ # frozen_string_literal: true +module ActiveStorage + module Tasks + module_function + + def print_blob_header(digits: 0) + puts ['Size'.rjust(8), 'Date'.rjust(18), 'Id'.rjust(digits + 2), ' Filename'].join + end + + def print_blob(blob, digits: 0) + size = (blob.byte_size / 1024).to_s.rjust(7) + date = blob.created_at.strftime('%Y-%m-%d %H:%M') + puts "#{size}K #{date} #{blob.id.to_s.rjust(digits)} #{blob.filename}" + end + end +end + namespace :asdb do - desc 'ActiveStorageDB: list attachments' + desc 'ActiveStorageDB: list attachments ordered by blob id desc' task list: [:environment] do |_t, _args| - ::ActiveStorage::Blob.order(:filename).pluck(:byte_size, :created_at, :filename).each do |size, dt, filename| - size_k = (size / 1024).to_s.rjust(7) - date = dt.strftime('%Y-%m-%d %H:%M') - puts "#{size_k}K #{date} #{filename}" + query = ::ActiveStorage::Blob.order(id: :desc).limit(100) + digits = Math.log(query.maximum(:id), 10).to_i + 1 + + ::ActiveStorage::Tasks.print_blob_header(digits: digits) + query.each do |blob| + ::ActiveStorage::Tasks.print_blob(blob, digits: digits) end end - desc 'ActiveStorageDB: download attachment' - task :get, [:src, :dst] => [:environment] do |_t, args| - src = args[:src]&.strip - dst = args[:dst]&.strip - abort('Required arguments: source file, destination file') if src.blank? || dst.blank? - - dst = "#{dst}/#{src}" if Dir.exist?(dst) - dir = File.dirname(dst) - abort("Can't write on: #{dir}") unless File.writable?(dir) + desc 'ActiveStorageDB: download attachment by blob id' + task :download, [:blob_id, :destination] => [:environment] do |_t, args| + blob_id = args[:blob_id]&.strip + destination = args[:destination]&.strip || Dir.pwd + abort('Required arguments: source blob id, destination path') if blob_id.blank? || destination.blank? - blob = ::ActiveStorage::Blob.order(created_at: :desc).find_by(filename: src) + blob = ::ActiveStorage::Blob.find_by(id: blob_id) abort('Source file not found') unless blob - ret = File.binwrite(dst, blob.download) - puts "#{ret} bytes written" - rescue StandardError => e - puts e + destination = "#{destination}/#{blob.filename}" if Dir.exist?(destination) + dir = File.dirname(destination) + abort("Can't write on path: #{dir}") unless File.writable?(dir) + + ret = File.binwrite(destination, blob.download) + puts "#{ret} bytes written - #{destination}" + end + + desc 'ActiveStorageDB: search attachment by filename (or part of it)' + task :search, [:filename] => [:environment] do |_t, args| + filename = args[:filename]&.strip + abort('Required arguments: filename') if filename.blank? + + blobs = ::ActiveStorage::Blob.where('filename LIKE ?', "%#{filename}%").order(id: :desc) + if blobs.any? + digits = Math.log(blobs.first.id, 10).to_i + 1 + ::ActiveStorage::Tasks.print_blob_header(digits: digits) + blobs.each do |blob| + ::ActiveStorage::Tasks.print_blob(blob, digits: digits) + end + else + puts 'No results' + end end end diff --git a/spec/tasks/active_storage_db_tasks_spec.rb b/spec/tasks/active_storage_db_tasks_spec.rb index cd6f3ff..cbbec45 100644 --- a/spec/tasks/active_storage_db_tasks_spec.rb +++ b/spec/tasks/active_storage_db_tasks_spec.rb @@ -6,45 +6,52 @@ describe 'asdb:list' do subject(:task) { execute_task('asdb:list') } - let(:file1) { create(:active_storage_blob, filename: 'file1', created_at: Time.now - 1.hour) } - let(:file2) { create(:active_storage_blob, filename: 'file2', created_at: Time.now - 5.hour) } - let(:file3) { create(:active_storage_blob, filename: 'file3', created_at: Time.now - 3.hour) } + let(:file1) { create(:active_storage_blob, filename: 'some file 1', created_at: Time.now - 1.hour) } + let(:file2) { create(:active_storage_blob, filename: 'some file 2', created_at: Time.now - 5.hour) } + let(:file3) { create(:active_storage_blob, filename: 'some file 3', created_at: Time.now - 3.hour) } before do [file1, file2, file3] end - it { is_expected.to match /file1.+file2.+file3/m } - - it 'prints the list of 3 files' do - expect(task.split("\n").size).to be 3 + it 'prints the columns header + the list of 3 files', :aggregate_failures do + pattern = /#{file3.id} #{file3.filename}.+#{file2.id} #{file2.filename}.+#{file1.id} #{file1.filename}/m + expect(task).to match pattern + expect(task.split("\n").size).to be 4 end end - describe 'asdb:get' do - subject(:task) { execute_task('asdb:get') } + describe 'asdb:download' do + subject(:task) { execute_task('asdb:download') } it 'exits showing the required arguments' do with_captured_stderr do - expect { task }.to raise_exception(SystemExit, /Required arguments/) + expect { task }.to raise_exception(SystemExit, /Required arguments: source blob id, destination path/) end end - context 'with only the source specified' do - subject(:task) { execute_task('asdb:get', src: 'some_file') } + context 'with a missing source' do + subject(:task) { execute_task('asdb:download', blob_id: 'some_file') } + + before do + allow(File).to receive(:writable?).and_return(true) + end - it 'exits showing the required arguments' do + it 'exits showing a not found error' do with_captured_stderr do - expect { task }.to raise_exception(SystemExit, /Required arguments/) + expect { task }.to raise_exception(SystemExit, /Source file not found/) end end end context 'with an invalid destination' do - subject(:task) { execute_task('asdb:get', src: 'some_file', dst: 'some_path') } + subject(:task) { execute_task('asdb:download', blob_id: 'some_file', destination: 'some_path') } + + let(:blob) { build_stubbed(:active_storage_blob) } before do allow(File).to receive(:writable?).and_return(false) + allow(ActiveStorage::Blob).to receive(:find_by).and_return(blob) end it 'exits showing a write error' do @@ -54,35 +61,54 @@ end end - context 'with a missing source' do - subject(:task) { execute_task('asdb:get', src: 'some_file', dst: 'some_path') } + context 'with valid arguments' do + subject(:task) { execute_task('asdb:download', blob_id: 'some_file', destination: 'some_path') } + + let(:blob) { build_stubbed(:active_storage_blob) } before do - allow(File).to receive(:writable?).and_return(true) + allow(File).to receive_messages(binwrite: 1000, writable?: true) + allow(ActiveStorage::Blob).to receive(:find_by).and_return(blob) + allow(blob).to receive(:download).and_return('some data') end - it 'exits showing a not found error' do - with_captured_stderr do - expect { task }.to raise_exception(SystemExit, /Source file not found/) - end + it 'prints the number of bytes written' do + expect(task).to eq "1000 bytes written - some_path\n" end end + end - context 'with valid arguments' do - subject(:task) { execute_task('asdb:get', src: blob.filename.to_s, dst: 'some_path') } + describe 'asdb:search' do + subject(:task) { execute_task('asdb:search') } - let(:blob) { build_stubbed(:active_storage_blob) } - let(:blobs) { instance_double(ActiveRecord::Relation) } + let(:blob) { build_stubbed(:active_storage_blob) } + + it 'exits showing the required arguments' do + with_captured_stderr do + expect { task }.to raise_exception(SystemExit, /Required arguments/) + end + end + + context 'when no files are found' do + subject(:task) { execute_task('asdb:search', filename: 'just ') } + + it 'prints "No results" message' do + expect(task).to eq "No results\n" + end + end + + context 'with there are some results' do + subject(:task) { execute_task('asdb:search', filename: 'just ') } before do - allow(File).to receive_messages(binwrite: 1000, writable?: true) - allow(ActiveStorage::Blob).to receive(:order).and_return(blobs) - allow(blobs).to receive(:find_by).and_return(blob) - allow(blob).to receive(:download).and_return('some data') + create(:active_storage_blob, filename: 'just a file', created_at: Time.now - 1.hour) + create(:active_storage_blob, filename: 'just another file', created_at: Time.now - 5.hour) + create(:active_storage_blob, filename: 'the last file', created_at: Time.now - 3.hour) end - it 'prints the number of bytes written' do - expect(task).to eq "1000 bytes written\n" + it 'prints the files that matches', :aggregate_failures do + expect(task).to match /just another file.+just a file/m + expect(task.split("\n").size).to be 3 end end end