Class: ModelFile

Inherits:
ApplicationRecord show all
Includes:
Listable, MemoWise, Problematic, PublicIDable, Relatable
Defined in:
app/models/model_file.rb

Constant Summary collapse

SPECIAL_FILES =
[
  "datapackage.json"
]
SUPPORT_KEYWORDS =
%w[
  presupported
  presup
  supported
  sup
  wsupports
  withsupports
]

Constants inherited from ApplicationRecord

ApplicationRecord::SAFE_NAME_LENGTH

Instance Method Summary collapse

Methods included from PublicIDable

#to_param

Methods included from Listable

#listers

Methods inherited from ApplicationRecord

find_param

Instance Method Details

#alternative_formats ⇒ Object



289
290
291
# File 'app/models/model_file.rb', line 289

def alternative_formats
  files_related_to_me.where("relationships.predicate" => "alternative_format_of")
end

#analyse_geometry_later(delay: 0.seconds) ⇒ Object



221
222
223
# File 'app/models/model_file.rb', line 221

def analyse_geometry_later(delay: 0.seconds)
  Analysis::GeometricAnalysisJob.set(wait: delay).perform_later(id)
end

#analyse_later(delay: 5.seconds) ⇒ Object



217
218
219
# File 'app/models/model_file.rb', line 217

def analyse_later(delay: 5.seconds)
  Analysis::AnalyseModelFileJob.set(wait: delay).perform_later(id)
end

#attach_existing_file!(refresh: true, skip_validations: false) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'app/models/model_file.rb', line 106

def attach_existing_file!(refresh: true, skip_validations: false)
  return if attachment.present? || !exists_on_storage?
  attachment_attacher.set ModelFileUploader.uploaded_file(
    storage: model.library.storage_key,
    id: path_within_library,
    metadata: {
      filename: basename(include_extension: true),
      size: attributes["size"],
      mime_type: Mime::Type.lookup_by_extension(File.extname(filename).delete(".").downcase).to_s
    }
  )
  attachment_attacher.refresh_metadata! if refresh
  save!(validate: !skip_validations)
end

#basename(include_extension: false) ⇒ Object



87
88
89
# File 'app/models/model_file.rb', line 87

def basename(include_extension: false)
  File.basename(filename, include_extension ? "" : ".*")
end

#cache_key_with_version ⇒ Object



173
174
175
# File 'app/models/model_file.rb', line 173

def cache_key_with_version
  digest
end

#calculate_digest ⇒ Object

TODO: this should move to Shrine metadata processing to be more efficient



137
138
139
140
141
142
143
144
145
# File 'app/models/model_file.rb', line 137

def calculate_digest
  sha = Digest::SHA512.new
  while (chunk = attachment.read(8192))
    sha.update(chunk)
  end
  sha.hexdigest
rescue Errno::ENOENT
  nil
end

#check_derivatives! ⇒ Object



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'app/models/model_file.rb', line 264

def check_derivatives!
  ApplicationRecord.no_touching do
    regen = attachment_derivatives.empty?
    # Clear out missing derivatives
    attachment_derivatives.each do |k, v|
      unless model.library.has_file?(v.id)
        attachment_attacher&.remove_derivative(k)
        regen = true
      end
    end
    if regen
      attachment_attacher&.create_derivatives
      save!(validate: false, touch: false)
    end
  end
end

#convert_later(format, delay: 0.seconds) ⇒ Object



203
204
205
# File 'app/models/model_file.rb', line 203

def convert_later(format, delay: 0.seconds)
  Analysis::FileConversionJob.set(wait: delay).perform_later(id, format.to_sym)
end

#convertable?(to: nil) ⇒ Boolean

Returns:

  • (Boolean)


207
208
209
210
# File 'app/models/model_file.rb', line 207

def convertable?(to: nil)
  return false unless FileHandlers::Assimp.can_load? mime_type
  to.nil? || FileHandlers::Assimp.can_save?(to)
end

#ctime ⇒ Object



239
240
241
242
243
# File 'app/models/model_file.rb', line 239

def ctime
  attachment&.ctime.present? ? Time.zone.parse(attachment.ctime) : updated_at
rescue NoMethodError
  created_at
end

#delete_from_disk_and_destroy ⇒ Object



212
213
214
215
# File 'app/models/model_file.rb', line 212

def delete_from_disk_and_destroy
  model.library.storage.delete path_within_library
  destroy
end

#dimensions ⇒ Object



147
148
149
150
151
152
153
154
155
# File 'app/models/model_file.rb', line 147

def dimensions
  bbox = attachment..dig("object", "bounding_box")
  return nil unless bbox
  bbox = Mittsu::Box3.new.set_from_points([
    Mittsu::Vector3.new(bbox.dig("minimum", "x"), bbox.dig("minimum", "y"), bbox.dig("minimum", "z")),
    Mittsu::Vector3.new(bbox.dig("maximum", "x"), bbox.dig("maximum", "y"), bbox.dig("maximum", "z"))
  ])
  bbox.size
end

#duplicate? ⇒ Boolean

Returns:

  • (Boolean)


167
168
169
# File 'app/models/model_file.rb', line 167

def duplicate?
  size && size > 0 && duplicates.exists? && !is_document?
end

#duplicates ⇒ Object



162
163
164
165
# File 'app/models/model_file.rb', line 162

def duplicates
  return ModelFile.none if digest.nil? # rubocop:todo Pundit/UsePolicyScope
  ModelFile.where(digest: digest).where.not(id: id) # rubocop:todo Pundit/UsePolicyScope
end

#exists_on_storage? ⇒ Boolean

Returns:

  • (Boolean)


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

def exists_on_storage?
  model.library.has_file? path_within_library
end

#extension ⇒ Object



54
55
56
57
58
59
60
61
# File 'app/models/model_file.rb', line 54

def extension
  if has_attribute? :attachment_data
    attachment&.extension
  else
    # DEPRECATED: for Pre-shrine migration
    File.extname(filename).delete(".").downcase
  end
end

#file_last_modified ⇒ Object



125
126
127
# File 'app/models/model_file.rb', line 125

def file_last_modified
  model.library.file_last_modified path_within_library
end

#filename_without_extension ⇒ Object



91
92
93
94
# File 'app/models/model_file.rb', line 91

def filename_without_extension
  dirname = File.dirname(filename)
  File.join([dirname, basename(include_extension: false)].reject { it == "." })
end

#has_render? ⇒ Boolean

Returns:

  • (Boolean)


79
80
81
# File 'app/models/model_file.rb', line 79

def has_render?
  is_3d_model? && attachment_attacher.derivatives.key?(:render)
end

#head(bytes) ⇒ Object



129
130
131
132
133
134
# File 'app/models/model_file.rb', line 129

def head(bytes)
  io = attachment.open
  result = io.read(bytes)
  io.close
  result
end

#is_3d_model? ⇒ Boolean

Returns:

  • (Boolean)


75
76
77
# File 'app/models/model_file.rb', line 75

def is_3d_model?
  MediaType.model_extensions.include? extension
end

#is_document? ⇒ Boolean

Returns:

  • (Boolean)


71
72
73
# File 'app/models/model_file.rb', line 71

def is_document?
  MediaType.document_extensions.include? extension
end

#is_image? ⇒ Boolean

Returns:

  • (Boolean)


63
64
65
# File 'app/models/model_file.rb', line 63

def is_image?
  MediaType.image_extensions.include? extension
end

#is_video? ⇒ Boolean

Returns:

  • (Boolean)


67
68
69
# File 'app/models/model_file.rb', line 67

def is_video?
  MediaType.video_extensions.include? extension
end

#mime_type ⇒ Object



83
84
85
# File 'app/models/model_file.rb', line 83

def mime_type
  Mime::Type.lookup_by_extension(extension)
end

#mtime ⇒ Object



233
234
235
236
237
# File 'app/models/model_file.rb', line 233

def mtime
  attachment&.mtime.present? ? Time.zone.parse(attachment.mtime) : updated_at
rescue NoMethodError
  updated_at
end

#name ⇒ Object



96
97
98
# File 'app/models/model_file.rb', line 96

def name
  basename.humanize.careful_titleize
end

#name_and_filename ⇒ Object



199
200
201
# File 'app/models/model_file.rb', line 199

def name_and_filename
  "#{name} (#{filename})"
end

#parse_metadata_later(delay: 0.seconds) ⇒ Object



225
226
227
# File 'app/models/model_file.rb', line 225

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

#path_within_library(derivative: nil) ⇒ Object



100
101
102
103
104
# File 'app/models/model_file.rb', line 100

def path_within_library(derivative: nil)
  derivative ?
    File.join(model.path, ".manyfold", "derivatives", filename, "#{derivative}.#{extension}") :
    File.join(model.path, filename)
end

#planar? ⇒ Boolean

Returns:

  • (Boolean)


157
158
159
160
# File 'app/models/model_file.rb', line 157

def planar?
  i = dimensions.to_a.index { it.zero? }
  i ? [:x, :y, :z].at(i) : nil
end

#presupported_version ⇒ Object



285
286
287
# File 'app/models/model_file.rb', line 285

def presupported_version
  reverse_relationships.where(predicate: "supported_version_of").first&.subject
end

#presupported_version=(file) ⇒ Object



293
294
295
296
297
298
299
# File 'app/models/model_file.rb', line 293

def presupported_version=(file)
  clear_presupported_relation and return if file.nil?
  return unless !presupported && file.presupported
  if file != presupported_version
    reverse_relationships.where(predicate: "supported_version_of").create_with(subject: file).first_or_create!.update(subject: file)
  end
end

#reattach! ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
# File 'app/models/model_file.rb', line 187

def reattach!
  if attachment.id != path_within_library || attachment.storage_key != model.library.storage_key
    old_path = attachment.id
    old_storage = attachment.storage
    # Reattach
    attachment_attacher.attach attachment, storage: model.library.storage_key
    # Remove previous file
    old_storage.delete old_path
    save!
  end
end

#refresh_metadata! ⇒ Object



257
258
259
260
261
262
# File 'app/models/model_file.rb', line 257

def refresh_metadata!
  ApplicationRecord.no_touching do
    attachment_attacher&.refresh_metadata!
    save(touch: false, validate: false)
  end
end

#scene ⇒ Object



177
178
179
180
181
182
183
184
185
# File 'app/models/model_file.rb', line 177

def scene
  Shrine.with_file(attachment.open) do
    scene = Assimp.import_file(it.path)
    scene.apply_post_processing(Assimp::PostProcessSteps[
      :JoinIdenticalVertices,
      :Triangulate
    ])
  end
end

#unsupported_version ⇒ Object



281
282
283
# File 'app/models/model_file.rb', line 281

def unsupported_version
  relationships.where(predicate: "supported_version_of").first&.objekt
end

#up_direction ⇒ Object



229
230
231
# File 'app/models/model_file.rb', line 229

def up_direction
  y_up ? "+y" : "+z"
end

#update_from_url!(url:) ⇒ Object



245
246
247
248
249
250
251
252
253
254
255
# File 'app/models/model_file.rb', line 245

def update_from_url!(url:)
  save! if attachment_attacher.assign_remote_url(
    url,
    downloader: {
      headers: {
        "If-None-Match" => attachment&.&.dig("remote_etag"),
        "If-Modified-Since" => attachment&.&.dig("remote_last_modified")
      }.compact
    }
  )
end