From f113d108c97eae297d14ea9b7ae27584a000c171 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 2 May 2019 11:37:35 +0200 Subject: [PATCH 1/3] Save virus scan status to blob metadata --- app/jobs/anti_virus_job.rb | 20 -------- app/jobs/virus_scanner_job.rb | 10 ++++ app/lib/active_storage/virus_scanner.rb | 46 +++++++++++++++++++ app/models/avis.rb | 29 ++++++++---- .../champs/piece_justificative_champ.rb | 32 +++++++------ app/models/concerns/virus_scan_concern.rb | 21 --------- app/models/virus_scan.rb | 8 ---- config/initializers/active_storage.rb | 28 +++++++++++ spec/features/users/brouillon_spec.rb | 4 +- ..._job_spec.rb => virus_scanner_job_spec.rb} | 9 ++-- spec/models/champ_spec.rb | 17 ++++--- .../champs/piece_justificative_champ_spec.rb | 9 ++-- spec/serializers/champ_serializer_spec.rb | 2 +- 13 files changed, 143 insertions(+), 92 deletions(-) delete mode 100644 app/jobs/anti_virus_job.rb create mode 100644 app/jobs/virus_scanner_job.rb create mode 100644 app/lib/active_storage/virus_scanner.rb delete mode 100644 app/models/concerns/virus_scan_concern.rb rename spec/jobs/{anti_virus_job_spec.rb => virus_scanner_job_spec.rb} (58%) diff --git a/app/jobs/anti_virus_job.rb b/app/jobs/anti_virus_job.rb deleted file mode 100644 index 18148ece2..000000000 --- a/app/jobs/anti_virus_job.rb +++ /dev/null @@ -1,20 +0,0 @@ -class AntiVirusJob < ApplicationJob - include ActiveStorage::Downloading - - attr_reader :blob - - def perform(virus_scan) - @blob = ActiveStorage::Blob.find_by(key: virus_scan.blob_key) - - if @blob.present? - download_blob_to_tempfile do |file| - if ClamavService.safe_file?(file.path) - status = VirusScan.statuses.fetch(:safe) - else - status = VirusScan.statuses.fetch(:infected) - end - virus_scan.update(scanned_at: Time.zone.now, status: status) - end - end - end -end diff --git a/app/jobs/virus_scanner_job.rb b/app/jobs/virus_scanner_job.rb new file mode 100644 index 000000000..f20d4bfc1 --- /dev/null +++ b/app/jobs/virus_scanner_job.rb @@ -0,0 +1,10 @@ +class VirusScannerJob < ApplicationJob + def perform(blob) + metadata = extract_metadata_via_virus_scanner(blob) + blob.update!(metadata: blob.metadata.merge(metadata)) + end + + def extract_metadata_via_virus_scanner(blob) + ActiveStorage::VirusScanner.new(blob).metadata + end +end diff --git a/app/lib/active_storage/virus_scanner.rb b/app/lib/active_storage/virus_scanner.rb new file mode 100644 index 000000000..e7ed91761 --- /dev/null +++ b/app/lib/active_storage/virus_scanner.rb @@ -0,0 +1,46 @@ +class ActiveStorage::VirusScanner + include ActiveStorage::Downloading + + def initialize(blob) + @blob = blob + end + + attr_reader :blob + + PENDING = 'pending' + INFECTED = 'infected' + SAFE = 'safe' + + def pending? + blob.metadata[:virus_scan_result] == PENDING + end + + def infected? + blob.metadata[:virus_scan_result] == INFECTED + end + + def safe? + blob.metadata[:virus_scan_result] == SAFE + end + + def analyzed? + blob.metadata[:virus_scan_result].present? + end + + def analyze_later + if !analyzed? + blob.update!(metadata: blob.metadata.merge(virus_scan_result: PENDING)) + VirusScannerJob.perform_later(blob) + end + end + + def metadata + download_blob_to_tempfile do |file| + if ClamavService.safe_file?(file.path) + { virus_scan_result: SAFE, scanned_at: Time.zone.now } + else + { virus_scan_result: INFECTED, scanned_at: Time.zone.now } + end + end + end +end diff --git a/app/models/avis.rb b/app/models/avis.rb index 9a085d11b..4d12a64a1 100644 --- a/app/models/avis.rb +++ b/app/models/avis.rb @@ -1,6 +1,5 @@ class Avis < ApplicationRecord include EmailSanitizableConcern - include VirusScanConcern belongs_to :dossier, touch: true belongs_to :gestionnaire @@ -22,9 +21,6 @@ class Avis < ApplicationRecord scope :by_latest, -> { order(updated_at: :desc) } scope :updated_since?, -> (date) { where('avis.updated_at > ?', date) } - after_commit :create_avis_virus_scan - after_initialize { add_virus_scan_on(self.piece_justificative_file) } - # The form allows subtmitting avis requests to several emails at once, # hence this virtual attribute. attr_accessor :emails @@ -41,6 +37,27 @@ class Avis < ApplicationRecord Avis.find_by(id: avis_id)&.email == email end + # FIXME remove this after migrating virus_scan to blob metadata + def virus_scan + VirusScan.find_by(blob_key: piece_justificative_file.blob.key) + end + + def virus_scan_safe? + virus_scan&.safe? || piece_justificative_file.virus_scanner.safe? + end + + def virus_scan_infected? + virus_scan&.infected? || piece_justificative_file.virus_scanner.infected? + end + + def virus_scan_pending? + virus_scan&.pending? || piece_justificative_file.virus_scanner.pending? + end + + def virus_scan_no_scan? + virus_scan.blank? && !piece_justificative_file.virus_scanner.analyzed? + end + private def notify_gestionnaire @@ -54,8 +71,4 @@ class Avis < ApplicationRecord self.email = nil end end - - def create_avis_virus_scan - create_virus_scan(self.piece_justificative_file) - end end diff --git a/app/models/champs/piece_justificative_champ.rb b/app/models/champs/piece_justificative_champ.rb index 1ec10d2ab..bf9a6b453 100644 --- a/app/models/champs/piece_justificative_champ.rb +++ b/app/models/champs/piece_justificative_champ.rb @@ -1,6 +1,4 @@ class Champs::PieceJustificativeChamp < Champ - after_commit :create_virus_scan - PIECE_JUSTIFICATIVE_FILE_MAX_SIZE = 200.megabytes PIECE_JUSTIFICATIVE_FILE_ACCEPTED_FORMATS = [ @@ -48,20 +46,26 @@ class Champs::PieceJustificativeChamp < Champ errors end + # FIXME remove this after migrating virus_scan to blob metadata + def virus_scan_safe? + virus_scan&.safe? || piece_justificative_file.virus_scanner.safe? + end + + def virus_scan_infected? + virus_scan&.infected? || piece_justificative_file.virus_scanner.infected? + end + + def virus_scan_pending? + virus_scan&.pending? || piece_justificative_file.virus_scanner.pending? + end + + def virus_scan_no_scan? + virus_scan.blank? && !piece_justificative_file.virus_scanner.analyzed? + end + def for_api - if piece_justificative_file.attached? && (virus_scan&.safe? || virus_scan&.pending?) + if piece_justificative_file.attached? && (virus_scan_safe? || virus_scan_pending?) Rails.application.routes.url_helpers.url_for(piece_justificative_file) end end - - private - - def create_virus_scan - if self.piece_justificative_file&.attachment&.blob.present? - VirusScan.where(champ: self).where.not(blob_key: self.piece_justificative_file.blob.key).delete_all - VirusScan.find_or_create_by!(champ: self, blob_key: self.piece_justificative_file.blob.key) do |virus_scan| - virus_scan.status = VirusScan.statuses.fetch(:pending) - end - end - end end diff --git a/app/models/concerns/virus_scan_concern.rb b/app/models/concerns/virus_scan_concern.rb deleted file mode 100644 index c7bfd7013..000000000 --- a/app/models/concerns/virus_scan_concern.rb +++ /dev/null @@ -1,21 +0,0 @@ -module VirusScanConcern - extend ActiveSupport::Concern - - attr_reader :attachment_attribute - - def add_virus_scan_on(piece_justificative) - @attachment_attribute = piece_justificative - end - - def virus_scan - VirusScan.find_by(blob_key: self.attachment_attribute.blob.key) - end - - def create_virus_scan(piece_justificative) - if piece_justificative&.attachment&.blob.present? - VirusScan.find_or_create_by!(blob_key: piece_justificative.blob.key) do |virus_scan| - virus_scan.status = VirusScan.statuses.fetch(:pending) - end - end - end -end diff --git a/app/models/virus_scan.rb b/app/models/virus_scan.rb index e6e0db4c9..c6d108164 100644 --- a/app/models/virus_scan.rb +++ b/app/models/virus_scan.rb @@ -6,12 +6,4 @@ class VirusScan < ApplicationRecord safe: 'safe', infected: 'infected' } - - validates :champ_id, uniqueness: { scope: :blob_key } - - after_create :perform_scan - - def perform_scan - AntiVirusJob.perform_later(self) - end end diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index e1b6e1ecb..88dd9b3d4 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -1 +1,29 @@ ActiveStorage::Service.url_expires_in = 1.hour + +# We want to run the virus scan on every ActiveStorage attachment, +# regardless of its type (user-uploaded document, instructeur-uploaded attestation, form template, etc.) +# +# To do this, the best place to work on is the ActiveStorage::Attachment +# objects themselves. +# +# We have to monkey patch ActiveStorage in order to always run an analyzer. +# The way analyzable blob interface work is by running the first accepted analyzer. +# This is not what we want for the virus scan. Using analyzer interface is still beneficial +# as it takes care of downloading the blob. +ActiveStorage::Attachment.class_eval do + after_create_commit :virus_scan + + private + + def virus_scan + ActiveStorage::VirusScanner.new(blob).analyze_later + end +end + +ActiveStorage::Attached::One.class_eval do + def virus_scanner + if attached? + ActiveStorage::VirusScanner.new(attachment.blob) + end + end +end diff --git a/spec/features/users/brouillon_spec.rb b/spec/features/users/brouillon_spec.rb index c0a495ade..4652c2366 100644 --- a/spec/features/users/brouillon_spec.rb +++ b/spec/features/users/brouillon_spec.rb @@ -167,8 +167,8 @@ feature 'The user' do expect(page).to have_text('analyse antivirus en cours') # Mark file as scanned and clean - virus_scan = VirusScan.last - virus_scan.update(scanned_at: Time.zone.now, status: VirusScan.statuses.fetch(:safe)) + attachment = ActiveStorage::Attachment.last + attachment.blob.update(metadata: attachment.blob.metadata.merge(scanned_at: Time.zone.now, virus_scan_result: ActiveStorage::VirusScanner::SAFE)) within '.piece-justificative' do click_on 'rafraichir' end diff --git a/spec/jobs/anti_virus_job_spec.rb b/spec/jobs/virus_scanner_job_spec.rb similarity index 58% rename from spec/jobs/anti_virus_job_spec.rb rename to spec/jobs/virus_scanner_job_spec.rb index a366ed1cb..3a16ad90f 100644 --- a/spec/jobs/anti_virus_job_spec.rb +++ b/spec/jobs/virus_scanner_job_spec.rb @@ -1,12 +1,11 @@ -RSpec.describe AntiVirusJob, type: :job do +RSpec.describe VirusScannerJob, type: :job do let(:champ) do champ = create(:champ, :piece_justificative) champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") champ end - let(:virus_scan) { create(:virus_scan, status: VirusScan.statuses.fetch(:pending), champ: champ, blob_key: champ.piece_justificative_file.blob.key) } - subject { AntiVirusJob.new.perform(virus_scan) } + subject { VirusScannerJob.new.perform(champ.piece_justificative_file.blob) } context "when no virus is found" do let(:virus_found?) { true } @@ -16,7 +15,7 @@ RSpec.describe AntiVirusJob, type: :job do subject end - it { expect(virus_scan.reload.status).to eq(VirusScan.statuses.fetch(:safe)) } + it { expect(champ.piece_justificative_file.virus_scanner.safe?).to be_truthy } end context "when a virus is found" do @@ -27,6 +26,6 @@ RSpec.describe AntiVirusJob, type: :job do subject end - it { expect(virus_scan.reload.status).to eq(VirusScan.statuses.fetch(:infected)) } + it { expect(champ.piece_justificative_file.virus_scanner.infected?).to be_truthy } end end diff --git a/spec/models/champ_spec.rb b/spec/models/champ_spec.rb index 296310a2b..7c8c895ec 100644 --- a/spec/models/champ_spec.rb +++ b/spec/models/champ_spec.rb @@ -383,21 +383,20 @@ describe Champ do let(:type_de_champ) { create(:type_de_champ_piece_justificative) } context 'and there is a blob' do - before { champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") } + before do + champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") + champ.save + end - it { expect { champ.save }.to change(VirusScan, :count).by(1) } + it { expect(champ.piece_justificative_file.virus_scanner.analyzed?).to be_truthy } end context 'and there is no blob' do - it { expect { champ.save }.to_not change(VirusScan, :count) } + before { champ.save } + + it { expect(champ.piece_justificative_file.virus_scanner).to be_nil } end end - - context 'when type_champ is not type_de_champ_piece_justificative' do - let(:type_de_champ) { create(:type_de_champ_textarea) } - - it { expect { champ.save }.to_not change(VirusScan, :count) } - end end describe "repetition" do diff --git a/spec/models/champs/piece_justificative_champ_spec.rb b/spec/models/champs/piece_justificative_champ_spec.rb index 4c709090d..4b38613d0 100644 --- a/spec/models/champs/piece_justificative_champ_spec.rb +++ b/spec/models/champs/piece_justificative_champ_spec.rb @@ -1,23 +1,24 @@ describe Champs::PieceJustificativeChamp do describe '#for_api' do let(:champ_pj) { create(:champ_piece_justificative) } + let(:metadata) { champ_pj.piece_justificative_file.blob.metadata } - before { champ_pj.virus_scan.update(status: status) } + before { champ_pj.piece_justificative_file.blob.update(metadata: metadata.merge(virus_scan_result: status)) } subject { champ_pj.for_api } context 'when file is safe' do - let(:status) { 'safe' } + let(:status) { ActiveStorage::VirusScanner::SAFE } it { is_expected.to include("/rails/active_storage/blobs/") } end context 'when file is not scanned' do - let(:status) { 'pending' } + let(:status) { ActiveStorage::VirusScanner::PENDING } it { is_expected.to include("/rails/active_storage/blobs/") } end context 'when file is infected' do - let(:status) { 'infected' } + let(:status) { ActiveStorage::VirusScanner::INFECTED } it { is_expected.to be_nil } end end diff --git a/spec/serializers/champ_serializer_spec.rb b/spec/serializers/champ_serializer_spec.rb index af119ca69..d9b24f62e 100644 --- a/spec/serializers/champ_serializer_spec.rb +++ b/spec/serializers/champ_serializer_spec.rb @@ -10,7 +10,7 @@ describe ChampSerializer do before do champ.piece_justificative_file.attach({ filename: __FILE__, io: File.open(__FILE__) }) - champ.reload.virus_scan.safe! + champ.piece_justificative_file.virus_scanner.analyze_later end after { champ.piece_justificative_file.purge } From 6e8b9cf668c8ce5d745cc5ce190146ef51a066b1 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 2 May 2019 11:37:27 +0200 Subject: [PATCH 2/3] Refactor piece_jointe template to avoid repetition --- .../piece_justificative/_pj_link.html.haml | 21 ------------------- .../piece_justificative/_show.html.haml | 2 +- .../_piece_justificative.html.haml | 2 +- .../shared/piece_jointe/_pj_link.html.haml | 10 ++++----- .../_pj_link.html.haml_spec.rb | 20 ++++++++---------- 5 files changed, 16 insertions(+), 39 deletions(-) delete mode 100644 app/views/shared/champs/piece_justificative/_pj_link.html.haml rename spec/views/shared/{champs/piece_justificative => piece_jointe}/_pj_link.html.haml_spec.rb (65%) diff --git a/app/views/shared/champs/piece_justificative/_pj_link.html.haml b/app/views/shared/champs/piece_justificative/_pj_link.html.haml deleted file mode 100644 index 6eadf1fbd..000000000 --- a/app/views/shared/champs/piece_justificative/_pj_link.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -- pj = champ.piece_justificative_file - -.pj-link - - if champ.virus_scan.blank? || champ.virus_scan.safe? - = link_to url_for(pj), target: '_blank', rel: 'noopener', title: "Télécharger la pièce jointe" do - %span.icon.attachment - = pj.filename.to_s - - if champ.virus_scan.blank? - (ce fichier n’a pas été analysé par notre antivirus, téléchargez-le avec précaution) - - - else - = pj.filename.to_s - - if champ.virus_scan.pending? - (analyse antivirus en cours - = link_to "rafraichir", request.path - ) - - elsif champ.virus_scan.infected? - - if user_can_upload - (virus détecté, merci d’envoyer un autre fichier) - - else - (virus détecté, le téléchargement de ce fichier est bloqué) diff --git a/app/views/shared/champs/piece_justificative/_show.html.haml b/app/views/shared/champs/piece_justificative/_show.html.haml index 6c0e1cf7e..2332a8fc7 100644 --- a/app/views/shared/champs/piece_justificative/_show.html.haml +++ b/app/views/shared/champs/piece_justificative/_show.html.haml @@ -1,5 +1,5 @@ - pj = champ.piece_justificative_file - if pj.attached? - = render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: champ, user_can_upload: false } + = render partial: "shared/piece_jointe/pj_link", locals: { object: champ, pj: pj, user_can_upload: false } - else Pièce justificative non fournie diff --git a/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml b/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml index 2906296a5..a20ba19d7 100644 --- a/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml +++ b/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml @@ -9,7 +9,7 @@ - if pj.attached? .piece-justificative-actions{ id: "piece_justificative_#{champ.id}" } .piece-justificative-action - = render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: champ, user_can_upload: true } + = render partial: "shared/piece_jointe/pj_link", locals: { object: champ, pj: champ.piece_justificative_file, user_can_upload: true } .piece-justificative-action - if champ.private? = link_to 'Supprimer', gestionnaire_champ_purge_champ_piece_justificative_path(procedure_id: champ.dossier.procedure_id, dossier_id: champ.dossier_id, champ_id: champ.id), remote: true, method: :delete, class: 'button small danger' diff --git a/app/views/shared/piece_jointe/_pj_link.html.haml b/app/views/shared/piece_jointe/_pj_link.html.haml index 656d1dd4a..e6d804918 100644 --- a/app/views/shared/piece_jointe/_pj_link.html.haml +++ b/app/views/shared/piece_jointe/_pj_link.html.haml @@ -1,19 +1,19 @@ - if pj.attached? .pj-link - - if object.virus_scan.blank? || object.virus_scan.safe? - = link_to url_for(pj), target: '_blank', title: "Télécharger la pièce jointe" do + - if object.virus_scan_safe? || object.virus_scan_no_scan? + = link_to url_for(pj), target: '_blank', rel: 'noopener', title: "Télécharger la pièce jointe" do %span.icon.attachment = pj.filename.to_s - - if object.virus_scan.blank? + - if object.virus_scan_no_scan? (ce fichier n’a pas été analysé par notre antivirus, téléchargez-le avec précaution) - else = pj.filename.to_s - - if object.virus_scan.pending? + - if object.virus_scan_pending? (analyse antivirus en cours = link_to "rafraichir", request.path ) - - elsif object.virus_scan.infected? + - elsif object.virus_scan_infected? - if user_can_upload (virus détecté, merci d’envoyer un autre fichier) - else diff --git a/spec/views/shared/champs/piece_justificative/_pj_link.html.haml_spec.rb b/spec/views/shared/piece_jointe/_pj_link.html.haml_spec.rb similarity index 65% rename from spec/views/shared/champs/piece_justificative/_pj_link.html.haml_spec.rb rename to spec/views/shared/piece_jointe/_pj_link.html.haml_spec.rb index 4eb6c12d4..9bf655204 100644 --- a/spec/views/shared/champs/piece_justificative/_pj_link.html.haml_spec.rb +++ b/spec/views/shared/piece_jointe/_pj_link.html.haml_spec.rb @@ -1,19 +1,17 @@ require 'rails_helper' -describe 'shared/champs/piece_justificative/_pj_link.html.haml', type: :view do - let(:champ) { create(:champ, :piece_justificative, :with_piece_justificative_file) } - let(:virus_scan) { nil } +describe 'shared/piece_jointe/_pj_link.html.haml', type: :view do + let(:champ) { create(:champ_piece_justificative) } + let(:virus_scan_result) { nil } before do - if virus_scan - champ.update(virus_scan: virus_scan) - end + champ.piece_justificative_file.blob.update(metadata: champ.piece_justificative_file.blob.metadata.merge(virus_scan_result: virus_scan_result)) end - subject { render 'shared/champs/piece_justificative/pj_link', champ: champ, user_can_upload: false } + subject { render 'shared/piece_jointe/pj_link', object: champ, pj: champ.piece_justificative_file, user_can_upload: false } context 'when there is no anti-virus scan' do - let(:virus_scan) { nil } + let(:virus_scan_result) { nil } it 'allows to download the file' do expect(subject).to have_link(champ.piece_justificative_file.filename.to_s) @@ -22,7 +20,7 @@ describe 'shared/champs/piece_justificative/_pj_link.html.haml', type: :view do end context 'when the anti-virus scan is pending' do - let(:virus_scan) { create(:virus_scan, :pending) } + let(:virus_scan_result) { ActiveStorage::VirusScanner::PENDING } it 'displays the filename, but doesn’t allow to download the file' do expect(subject).to have_text(champ.piece_justificative_file.filename.to_s) @@ -32,7 +30,7 @@ describe 'shared/champs/piece_justificative/_pj_link.html.haml', type: :view do end context 'when the file is scanned and safe' do - let(:virus_scan) { create(:virus_scan, :safe) } + let(:virus_scan_result) { ActiveStorage::VirusScanner::SAFE } it 'allows to download the file' do expect(subject).to have_link(champ.piece_justificative_file.filename.to_s) @@ -40,7 +38,7 @@ describe 'shared/champs/piece_justificative/_pj_link.html.haml', type: :view do end context 'when the file is scanned and infected' do - let(:virus_scan) { create(:virus_scan, :infected) } + let(:virus_scan_result) { ActiveStorage::VirusScanner::INFECTED } it 'displays the filename, but doesn’t allow to download the file' do expect(subject).to have_text(champ.piece_justificative_file.filename.to_s) From d26f736f50cec6d47d4b7b515caba402c3c94293 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 25 Apr 2019 12:36:33 +0200 Subject: [PATCH 3/3] Migrate virus scans to blob metadata --- .../20190425102459_migrate_virus_scans.rake | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 lib/tasks/deployment/20190425102459_migrate_virus_scans.rake diff --git a/lib/tasks/deployment/20190425102459_migrate_virus_scans.rake b/lib/tasks/deployment/20190425102459_migrate_virus_scans.rake new file mode 100644 index 000000000..e72d6c1ee --- /dev/null +++ b/lib/tasks/deployment/20190425102459_migrate_virus_scans.rake @@ -0,0 +1,22 @@ +namespace :after_party do + desc 'Deployment task: migrate_virus_scans' + task migrate_virus_scans: :environment do + puts "Running deploy task 'migrate_virus_scans'" + + virus_scans = VirusScan.all + progress = ProgressReport.new(virus_scans.count) + virus_scans.find_each do |virus_scan| + blob = ActiveStorage::Blob.find_by(key: virus_scan.blob_key) + if blob + metadata = { virus_scan_result: virus_scan.status, scanned_at: virus_scan.scanned_at } + blob.update_column(:metadata, blob.metadata.merge(metadata)) + end + progress.inc + end + progress.finish + + # Update task as completed. If you remove the line below, the task will + # run with every deploy (or every time you call after_party:run). + AfterParty::TaskRecord.create version: '20190425102459' + end +end