Class: Model

Inherits:
ApplicationRecord show all
Includes:
CaberObject, Commentable, FaspClient::DataSharing::Lifecycle, Followable, Indexable, Likeable, Linkable, MemoWise, PathBuilder, Problematic, PublicIDable, Relatable, Sluggable, Talkative
Defined in:
app/models/model.rb

Constant Summary

Constants included from Talkative

Talkative::TIMEOUT

Constants included from FederailsCommon

FederailsCommon::FEDIVERSE_USERNAMES

Constants inherited from ApplicationRecord

ApplicationRecord::SAFE_NAME_LENGTH

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Likeable

#update_like_count!

Methods included from Indexable

#ai_indexable?, #indexable?, #inherited_ai_indexable?, #inherited_indexable?, #public_and_indexable?

Methods included from PublicIDable

#to_param

Methods included from CaberObject

#just_became_public?, #matching_permission_preset, #owners, #private?, #public?, #set_default_permission_preset, #set_owner, #set_permissions_from_preset, #will_be_public?

Methods included from Talkative

#owning_actor

Methods included from Followable

#followed_by?, #followers

Methods included from FederailsCommon

#federails_actor, #local?, #remote?

Methods included from PathBuilder

#formatted_path

Methods inherited from ApplicationRecord

find_param

Class Method Details

.common_root(*models) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
# File 'app/models/model.rb', line 105

def self.common_root(*models)
  # If there are different libraries, there is no common root
  return nil unless models.map(&:library_id).uniq.one?
  # Get each path, split, and working from the front, find the common elements

  first, *remainder = models.map { it.path.split(File::SEPARATOR).without(".") }
  parts = first.zip(*remainder)
  common = parts.map { (it.uniq.length == 1) ? it.first : nil }
  common = common.first(common.index(nil) || 99999)
  common.empty? ? nil : File.join(common)
end

.create_from(other, link_preview_file: false, link_entrypoint: false, name: nil, path: nil) ⇒ Object



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'app/models/model.rb', line 235

def self.create_from(other, link_preview_file: false, link_entrypoint: false, name: nil, path: nil)
  new_model = other.dup
  new_model.update(
    path: path,
    name: name || "Copy of #{other.name}",
    public_id: nil,
    tags: other.tags,
    preview_file: link_preview_file ? other.preview_file : nil,
    entrypoint: link_entrypoint ? other.entrypoint : nil,
    collections: other.collections
  )
  path ? new_model.save! : new_model.organize!
  # Wipe permissions and copy from old model
  new_model.caber_relations.delete_all
  new_model.update!(
    caber_relations_attributes: other.caber_relations.all.map { {permission: it.permission, subject: it.subject} }
  )
  new_model
end

Instance Method Details

#add_new_files_later(include_all_subfolders: false, delay: 0.seconds) ⇒ Object



286
287
288
# File 'app/models/model.rb', line 286

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

#adopt_file(file, path_prefix: nil) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'app/models/model.rb', line 125

def adopt_file(file, path_prefix: nil)
  # Work out the new filename
  new_filename = path_prefix ? File.join(path_prefix, file.filename) : file.filename
  # If there's an identical file already there...
  existing_file = model_files.find_by(filename: new_filename)
  if file.digest && file.digest == existing_file&.digest
    file.problems.destroy_all # Remove associated problems for this file manually
    file.delete # Don't run callbacks, just remove the database record
    return
  elsif existing_file
    # Otherwise, make sure the name is distinct by adding the digest to the end if there's a clash
    new_filename = "#{File.basename(new_filename, ".*")}_#{file.digest.first(6)}#{File.extname(new_filename)}"
  end
  # Adopt the file
  file.update(
    filename: new_filename,
    model: self
  )
  file.reattach!
end

#check_for_problems_later(delay: 5.seconds) ⇒ Object



294
295
296
# File 'app/models/model.rb', line 294

def check_for_problems_later(delay: 5.seconds)
  Scan::Model::CheckForProblemsJob.set(wait: delay).perform_later(id)
end

#check_later(delay: 0.seconds) ⇒ Object



290
291
292
# File 'app/models/model.rb', line 290

def check_later(delay: 0.seconds)
  Scan::CheckModelJob.set(wait: delay).perform_later(id)
end

#contained_models ⇒ Object



193
194
195
196
197
198
199
200
# File 'app/models/model.rb', line 193

def contained_models
  previous_library.models.where(
    Model.arel_table[:path].matches(
      Model.sanitize_sql_like(previous_path) + "/%",
      "\\"
    )
  )
end

#contains?(other) ⇒ Boolean

Returns:

  • (Boolean)


121
122
123
# File 'app/models/model.rb', line 121

def contains?(other)
  Model.common_root(self, other) == path
end

#contains_other_models? ⇒ Boolean

Returns:

  • (Boolean)


202
203
204
# File 'app/models/model.rb', line 202

def contains_other_models?
  contained_models.exists?
end

#create_or_update_file_from_url(url:, filename:) ⇒ Object



174
175
176
177
178
179
180
# File 'app/models/model.rb', line 174

def create_or_update_file_from_url(url:, filename:)
  uri = URI.parse(url)
  file = model_files.find_or_create_by(filename: filename)
  file.update_from_url!(url: uri.to_s)
  file
rescue URI::InvalidURIError
end

#creation_comment ⇒ Object



333
334
335
# File 'app/models/model.rb', line 333

def creation_comment
  comments.where(system: true).order(:created_at).first
end

#datapackage ⇒ Object



310
311
312
# File 'app/models/model.rb', line 310

def datapackage
  model_files.find_by(filename: "datapackage.json")
end

#datapackage_content ⇒ Object



314
315
316
317
# File 'app/models/model.rb', line 314

def datapackage_content
  JSON.parse(datapackage.attachment.read) unless datapackage.nil?
rescue Shrine::FileNotFound
end

#delete_from_disk_and_destroy ⇒ Object



182
183
184
185
186
187
188
189
190
191
# File 'app/models/model.rb', line 182

def delete_from_disk_and_destroy
  # 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

#disjoint?(other) ⇒ Boolean

Returns:

  • (Boolean)


117
118
119
# File 'app/models/model.rb', line 117

def disjoint?(other)
  Model.common_root(self, other).nil?
end

#exists_on_storage? ⇒ Boolean

Returns:

  • (Boolean)


226
227
228
# File 'app/models/model.rb', line 226

def exists_on_storage?
  library.has_folder?(path)
end

#fasp_uri ⇒ Object



34
35
36
# File 'app/models/model.rb', line 34

def fasp_uri
  federails_actor&.federated_url
end

#file_extensions ⇒ Object



274
275
276
# File 'app/models/model.rb', line 274

def file_extensions
  model_files.map(&:extension).uniq
end

#has_supported_and_unsupported? ⇒ Boolean

Returns:

  • (Boolean)


269
270
271
272
# File 'app/models/model.rb', line 269

def has_supported_and_unsupported?
  model_files.exists?(presupported: true) &&
    model_files.exists?(presupported: false)
end

#image_files ⇒ Object



218
219
220
# File 'app/models/model.rb', line 218

def image_files
  model_files.select(&:is_image?)
end

#merge!(*models) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'app/models/model.rb', line 146

def merge!(*models)
  # If we've got one argument and it's enumerable, use it directly
  models = models[0] if models.length == 1 && models[0].is_a?(Enumerable)
  # Go through the list
  models.each do |other|
    # Work out path to the other target from here
    relative_path = contains?(other) ? Pathname.new(other.path).relative_path_from(Pathname.new(path)) : nil
    # Remove datapackage
    other.datapackage&.destroy
    # Move files
    other.model_files.each { adopt_file(it, path_prefix: relative_path) }
    check_for_problems_later
    # Merge metadata
    self.creator ||= other.creator
    other.collections.each { |c| collections << c unless c.in?(collections) }
    self.license ||= other.license
    self.caption ||= other.caption
    self.notes ||= other.notes
    self.sensitive ||= other.sensitive
    tag_list.add(*other.tag_list)
    self.links_attributes = other.links.map { {url: it.url} }
    save!
    # Destroy the other model
    other.reload
    other.destroy
  end
end

#needs_organizing? ⇒ Boolean

Returns:

  • (Boolean)


206
207
208
# File 'app/models/model.rb', line 206

def needs_organizing?
  formatted_path != path
end

#new? ⇒ Boolean

Returns:

  • (Boolean)


210
211
212
# File 'app/models/model.rb', line 210

def new?
  tags.where(name: SiteSettings.model_tags_auto_tag_new).any?
end

#organize! ⇒ Object



230
231
232
233
# File 'app/models/model.rb', line 230

def organize!
  autoupdate_path
  save!
end

#organize_later(delay: 5.seconds) ⇒ Object



298
299
300
# File 'app/models/model.rb', line 298

def organize_later(delay: 5.seconds)
  OrganizeModelJob.set(wait: delay).perform_later(id)
end

#parents ⇒ Object



94
95
96
97
98
# File 'app/models/model.rb', line 94

def parents
  Pathname.new(path).parent.descend.filter_map do |path|
    library.models.find_by(path: path.to_s)
  end
end

#parse_metadata_later(delay: 0.seconds) ⇒ Object



306
307
308
# File 'app/models/model.rb', line 306

def (delay: 0.seconds)
  Scan::Model::ParseMetadataJob.set(wait: delay).perform_later(id)
end

#pregenerate_downloads(delay: 10.minutes, queue: nil) ⇒ Object



319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'app/models/model.rb', line 319

def pregenerate_downloads(delay: 10.minutes, queue: nil)
  # By default, give 10 minutes' grace for followup changes before we pregenerate the download
  # Other scan jobs could be running, which might take some time.
  # This is brittle, and we need a better way to say "this model is done changing for a while"
  return unless SiteSettings.pregenerate_downloads

  download_types = [nil]
  download_types += ["supported", "unsupported"] if has_supported_and_unsupported?
  download_types += file_extensions.excluding("json")
  download_types.each do |selection|
    ArchiveDownloadService.new(model: self, selection: selection).prepare(delay: delay, queue: queue)
  end
end

#size_on_disk ⇒ Object



278
279
280
# File 'app/models/model.rb', line 278

def size_on_disk
  model_files.pluck(:size).compact.sum
end

#split!(files: []) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'app/models/model.rb', line 255

def split!(files: [])
  preview_file_will_move = files.include?(preview_file)
  entrypoint_will_move = files.include?(entrypoint)
  new_model = Model.create_from(self, link_preview_file: preview_file_will_move, link_entrypoint: entrypoint_will_move)
  # Clear preview file if it was moved
  update!(preview_file: nil) if preview_file_will_move
  # Clear entrypoint file if it was moved
  update!(entrypoint: nil) if entrypoint_will_move
  # Move files
  files.each { new_model.adopt_file(it) }
  # Done!
  new_model
end

#three_d_files ⇒ Object



222
223
224
# File 'app/models/model.rb', line 222

def three_d_files
  model_files.select(&:is_3d_model?)
end

#to_activitypub_object ⇒ Object



282
283
284
# File 'app/models/model.rb', line 282

def to_activitypub_object
  ActivityPub::ModelSerializer.new(self).serialize
end

#valid_preview_files ⇒ Object



214
215
216
# File 'app/models/model.rb', line 214

def valid_preview_files
  model_files.select { it.is_image? || it.is_video? || FileHandlers::F3d.can_load?(it.mime_type) || FileHandlers::Three.can_load?(it.mime_type) }
end

#was_changed? ⇒ Boolean

Returns:

  • (Boolean)


101
102
103
# File 'app/models/model.rb', line 101

def was_changed?
  !previous_changes.empty?
end

#write_datapackage_later(delay: 1.second) ⇒ Object



302
303
304
# File 'app/models/model.rb', line 302

def write_datapackage_later(delay: 1.second)
  UpdateDatapackageJob.set(wait: delay).perform_later(id)
end