mirror of
https://github.com/manyfold3d/manyfold.git
synced 2026-10-22 19:52:18 -05:00
286 lines
7.8 KiB
Ruby
286 lines
7.8 KiB
Ruby
class Model < ApplicationRecord
|
|
extend Memoist
|
|
include PathBuilder
|
|
include Followable
|
|
include CaberObject
|
|
include Linkable
|
|
include Sluggable
|
|
include PublicIDable
|
|
include Commentable
|
|
include Problematic
|
|
|
|
acts_as_federails_actor(
|
|
username_field: :public_id,
|
|
name_field: :name,
|
|
profile_url_method: :url_for,
|
|
# We use the Service actor type purely so Mastodon doesn't ignore the actor.
|
|
# Actual type is differentiated with f3di:concreteType == "3DModel".
|
|
# Ideally this would be a Document: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document
|
|
# Hopefully at some point this can change, if Mastodon starts allowing other actor types
|
|
# See https://github.com/mastodon/mastodon/issues/22322
|
|
actor_type: "Service"
|
|
)
|
|
|
|
scope :recent, -> { order(created_at: :desc) }
|
|
|
|
belongs_to :library
|
|
belongs_to :creator, optional: true
|
|
belongs_to :collection, optional: true
|
|
belongs_to :preview_file, class_name: "ModelFile", optional: true
|
|
has_many :model_files, dependent: :destroy
|
|
acts_as_taggable_on :tags
|
|
|
|
before_validation :strip_separators_from_path, if: :path_changed?
|
|
|
|
before_validation :normalize_license
|
|
# In Rails 7.1 we will be able to do this instead:
|
|
# normalizes :license, with: -> license { license.blank? ? nil : license }
|
|
|
|
after_create :post_creation_activity
|
|
before_update :move_files, if: :need_to_move_files?
|
|
after_update :post_update_activity
|
|
after_save :write_datapackage_later
|
|
after_commit :check_for_problems_later, on: :update
|
|
|
|
validates :name, presence: true
|
|
validates :path, presence: true, uniqueness: {scope: :library}
|
|
validate :check_for_submodels, on: :update, if: :need_to_move_files?
|
|
validate :destination_is_vacant, on: :update, if: :need_to_move_files?
|
|
validates :license, spdx: true, allow_nil: true
|
|
validates :public_id, multimodel_uniqueness: {case_sensitive: false, check: FederailsCommon::FEDIVERSE_USERNAMES}
|
|
|
|
def parents
|
|
Pathname.new(path).parent.descend.filter_map do |path|
|
|
library.models.find_by(path: path.to_s)
|
|
end
|
|
end
|
|
memoize :parents
|
|
|
|
def merge_into!(target)
|
|
return unless target
|
|
|
|
# Work out path to this model from the target
|
|
relative_path = Pathname.new(path).relative_path_from(Pathname.new(target.path))
|
|
# Remove datapackage
|
|
datapackage&.destroy
|
|
# Move files
|
|
model_files.each do |f|
|
|
new_filename = File.join(relative_path, f.filename)
|
|
if target.model_files.exists?(filename: new_filename)
|
|
f.problems.destroy_all # Remove associated problems for this file manually
|
|
f.delete # Don't run callbacks, just remove the database record
|
|
else
|
|
f.update(
|
|
filename: new_filename,
|
|
model: target
|
|
)
|
|
end
|
|
end
|
|
target.check_for_problems_later
|
|
# Destroy this model
|
|
reload
|
|
destroy
|
|
end
|
|
|
|
def delete_from_disk_and_destroy
|
|
# Remove all presupported_version relationships first, they get in the way
|
|
# This will go away later when we do proper file relationships rather than linking the tables directly
|
|
model_files.update_all(presupported_version_id: nil) # rubocop:disable Rails/SkipsModelValidations
|
|
# Trigger deletion for each file separately, to make sure cleanup happens
|
|
model_files.each { |f| f.delete_from_disk_and_destroy }
|
|
# Remove tags first - sometimes this causes problems if we don't do it beforehand
|
|
update!(tags: [])
|
|
# Delete directory corresponding to model
|
|
library.storage.delete_prefixed(path)
|
|
# Remove from DB
|
|
destroy
|
|
end
|
|
|
|
def contained_models
|
|
previous_library.models.where(
|
|
Model.arel_table[:path].matches(
|
|
Model.sanitize_sql_like(previous_path) + "/%",
|
|
"\\"
|
|
)
|
|
)
|
|
end
|
|
|
|
def contains_other_models?
|
|
contained_models.exists?
|
|
end
|
|
|
|
def needs_organizing?
|
|
formatted_path != path
|
|
end
|
|
|
|
def self.ransackable_attributes(_auth_object = nil)
|
|
["caption", "created_at", "id", "public_id", "library_id", "name", "notes", "path", "slug", "updated_at", "license_cont", "license"]
|
|
end
|
|
|
|
def self.ransackable_associations(auth_object = nil)
|
|
["base_tags", "collection", "creator", "library", "links", "model_files", "preview_file", "problems", "tag_taggings", "taggings", "tags"]
|
|
end
|
|
|
|
def new?
|
|
tags.where(name: SiteSettings.model_tags_auto_tag_new).any?
|
|
end
|
|
|
|
def valid_preview_files
|
|
model_files.select { |it| it.is_image? || it.is_renderable? }
|
|
end
|
|
|
|
def image_files
|
|
model_files.select(&:is_image?)
|
|
end
|
|
|
|
def three_d_files
|
|
model_files.select(&:is_3d_model?)
|
|
end
|
|
|
|
def exists_on_storage?
|
|
library.has_folder?(path)
|
|
end
|
|
|
|
def organize!
|
|
autoupdate_path
|
|
save!
|
|
end
|
|
|
|
def split!(files: [])
|
|
new_model = dup
|
|
new_model.name = "Copy of #{name}"
|
|
new_model.public_id = nil
|
|
new_model.tags = tags
|
|
new_model.organize!
|
|
# Move files
|
|
files.each do |file|
|
|
file.update!(model: new_model)
|
|
file.reattach!
|
|
end
|
|
# Clear preview file appropriately
|
|
if files.include?(preview_file)
|
|
update!(preview_file: nil)
|
|
else
|
|
new_model.update!(preview_file: nil)
|
|
end
|
|
# Done!
|
|
new_model
|
|
end
|
|
|
|
def has_supported_and_unsupported?
|
|
model_files.where(presupported: true).count > 0 &&
|
|
model_files.where(presupported: false).count > 0
|
|
end
|
|
|
|
def file_extensions
|
|
model_files.map(&:extension).uniq
|
|
end
|
|
|
|
def merge_all_children!
|
|
contained_models.each do |child|
|
|
child.merge_into! self
|
|
end
|
|
end
|
|
|
|
def size_on_disk
|
|
model_files.pluck(:size).compact.sum
|
|
end
|
|
|
|
def to_activitypub_object
|
|
ActivityPub::ModelSerializer.new(self).serialize
|
|
end
|
|
|
|
def add_new_files_later(include_all_subfolders: false, delay: 0.seconds)
|
|
Scan::Model::AddNewFilesJob.set(wait: delay).perform_later(id, include_all_subfolders: include_all_subfolders)
|
|
end
|
|
|
|
def check_later(delay: 0.seconds)
|
|
Scan::CheckModelJob.set(wait: delay).perform_later(id)
|
|
end
|
|
|
|
def check_for_problems_later(delay: 5.seconds)
|
|
Scan::Model::CheckForProblemsJob.set(wait: delay).perform_later(id)
|
|
end
|
|
|
|
def organize_later(delay: 5.seconds)
|
|
OrganizeModelJob.set(wait: delay).perform_later(id)
|
|
end
|
|
|
|
def write_datapackage_later(delay: 1.second)
|
|
UpdateDatapackageJob.set(wait: delay).perform_later(id)
|
|
end
|
|
|
|
def parse_metadata_later(delay: 0.seconds)
|
|
Scan::Model::ParseMetadataJob.set(wait: delay).perform_later(id)
|
|
end
|
|
|
|
def datapackage
|
|
model_files.find_by(filename: "datapackage.json")
|
|
end
|
|
|
|
def datapackage_content
|
|
JSON.parse(datapackage.attachment.read) unless datapackage.nil?
|
|
rescue Shrine::FileNotFound
|
|
end
|
|
|
|
private
|
|
|
|
def normalize_license
|
|
self.license = nil if license.blank?
|
|
end
|
|
|
|
def strip_separators_from_path
|
|
self.path = path&.trim_path_separators
|
|
end
|
|
|
|
def previous_library
|
|
library_id_changed? ? Library.find_by(id: library_id_was) : library
|
|
end
|
|
|
|
def previous_path
|
|
path_changed? ? path_was : path
|
|
end
|
|
|
|
def need_to_move_files?
|
|
library_id_changed? ||
|
|
(path_changed? &&
|
|
(previous_path.trim_path_separators != path.trim_path_separators)
|
|
)
|
|
end
|
|
|
|
def autoupdate_path
|
|
self.path = formatted_path
|
|
end
|
|
|
|
def check_for_submodels
|
|
if contains_other_models?
|
|
errors.add(library_id_changed? ? :library : :path, :nested)
|
|
end
|
|
end
|
|
|
|
def destination_is_vacant
|
|
if exists_on_storage? && need_to_move_files?
|
|
errors.add(:path, :destination_exists)
|
|
end
|
|
end
|
|
|
|
def move_files
|
|
# Move all the files
|
|
model_files.each(&:reattach!)
|
|
# Remove the old folder if it's still there
|
|
previous_library.storage.delete_prefixed(previous_path)
|
|
end
|
|
|
|
def post_creation_activity
|
|
if creator.present?
|
|
Activity::CreatorAddedModelJob.set(wait: 5.seconds).perform_later(id)
|
|
end
|
|
end
|
|
|
|
def post_update_activity
|
|
if creator_previously_changed?
|
|
Activity::CreatorAddedModelJob.set(wait: 5.seconds).perform_later(id)
|
|
end
|
|
end
|
|
end
|