diff --git a/app/jobs/analysis/file_conversion_job.rb b/app/jobs/analysis/file_conversion_job.rb new file mode 100644 index 000000000..fb55c195a --- /dev/null +++ b/app/jobs/analysis/file_conversion_job.rb @@ -0,0 +1,41 @@ +require "string/similarity" + +class Analysis::FileConversionJob < ApplicationJob + queue_as :analysis + + def perform(file_id, output_format) + # Can we output this format? + return unless SupportedMimeTypes.can_export?(output_format) + # Get model + begin + file = ModelFile.find(file_id) + rescue ActiveRecord::RecordNotFound + return + end + exporter = nil + extension = nil + case output_format + when :threemf + return unless file.mesh.manifold? + extension = "3mf" + exporter = Mittsu::ThreeMFExporter.new + end + if exporter + new_file = ModelFile.new( + model: file.model, + filename: file.filename.gsub(".#{file.extension}", ".#{extension}") + ) + dedup = 0 + while File.exist?(new_file.pathname) + dedup += 1 + new_file.filename = file.filename.gsub(".#{file.extension}", "-#{dedup}.#{extension}") + end + # Save the actual file in new format + exporter.export(file.mesh, new_file.pathname) + # Store record in database + new_file.save + # Queue up file scan + Analysis::AnalyseModelFileJob.perform_later(new_file.id) + end + end +end diff --git a/app/lib/supported_mime_types.rb b/app/lib/supported_mime_types.rb index 51da15e2a..2f2c7d25a 100644 --- a/app/lib/supported_mime_types.rb +++ b/app/lib/supported_mime_types.rb @@ -15,6 +15,10 @@ module SupportedMimeTypes Mime::EXTENSION_LOOKUP.filter { |k, v| is_model_mime_type?(v) }.keys end + def self.can_export?(type) + [:threemf].include? type + end + class << self private diff --git a/app/models/model_file.rb b/app/models/model_file.rb index 393bc8d7d..e72df82e0 100644 --- a/app/models/model_file.rb +++ b/app/models/model_file.rb @@ -43,8 +43,12 @@ class ModelFile < ApplicationRecord Mime::Type.lookup_by_extension(extension) end + def basename + File.basename(filename, ".*") + end + def name - File.basename(filename, ".*").humanize.titleize + basename.humanize.titleize end def pathname diff --git a/spec/jobs/analysis/file_conversion_job_spec.rb b/spec/jobs/analysis/file_conversion_job_spec.rb new file mode 100644 index 000000000..ee8c84d0b --- /dev/null +++ b/spec/jobs/analysis/file_conversion_job_spec.rb @@ -0,0 +1,83 @@ +require "rails_helper" +require "support/mock_directory" + +RSpec.describe Analysis::FileConversionJob do + around do |ex| + MockDirectory.create([ + "model_one/files/awesome.stl" + ]) do |path| + @library_path = path + ex.run + end + end + + let(:library) { create(:library, path: @library_path) } # rubocop:todo RSpec/InstanceVariable + let(:model) { create(:model, path: "model_one", library: library) } + let(:file) { create(:model_file, model: model, filename: "files/awesome.stl") } + let(:mesh) do + m = Mittsu::Mesh.new(Mittsu::SphereGeometry.new(2.0, 32, 16)) + m.geometry.merge_vertices + m + end + + before do + ActiveJob::Base.queue_adapter = :test + allow(file).to receive(:mesh).and_return(mesh) + allow(ModelFile).to receive(:find).with(file.id).and_return(file) + end + + context "when converting to 3MF" do + it "creates a new file" do + expect { described_class.perform_now(file.id, :threemf) }.to change(ModelFile, :count).from(1).to(2) + end + + it "creates a file with 3mf extension" do + described_class.perform_now(file.id, :threemf) + expect(ModelFile.where.not(id: file.id).first.extension).to eq "3mf" + end + + it "creates a file with the same basename as the original" do + described_class.perform_now(file.id, :threemf) + expect(ModelFile.where.not(id: file.id).first.filename).to eq "files/awesome.3mf" + end + + it "avoids filenames that already exist" do + allow(File).to receive(:exist?).with(file.pathname.gsub(".stl", ".3mf")).and_return(true).once + allow(File).to receive(:exist?).with(file.pathname.gsub(".stl", "-1.3mf")).and_return(true).once + allow(File).to receive(:exist?).with(file.pathname.gsub(".stl", "-2.3mf")).and_return(false).once + described_class.perform_now(file.id, :threemf) + expect(ModelFile.where.not(id: file.id).first.filename).to eq "files/awesome-2.3mf" + end + + it "creates an actual 3MF file on disk" do + described_class.perform_now(file.id, :threemf) + pathname = ModelFile.where.not(id: file.id).first.pathname + expect(File.exist?(pathname)).to be true + end + + it "does not remove the original file" do + described_class.perform_now(file.id, :threemf) + expect { ModelFile.find(file.id) }.not_to raise_error + end + + it "should create a file equivalence with the original file" + + it "does not convert non-manifold meshes" do + allow(mesh).to receive(:manifold?).and_return(false) + expect { described_class.perform_now(file.id, :threemf) }.not_to change(ModelFile, :count) + end + + it "queues up analysis job for new file" do + expect { described_class.perform_now(file.id, :threemf) }.to have_enqueued_job(Analysis::AnalyseModelFileJob) + end + end + + it "does nothing with an invalid file ID" do + allow(ModelFile).to receive(:find).with(nil).and_raise(ActiveRecord::RecordNotFound) + expect { described_class.perform_now(nil, :threemf) }.not_to raise_error + end + + it "does nothing with an invalid output format" do + expect { described_class.perform_now(file.id, :ply) }.not_to raise_error + end +end