Class: Neofiles::File

Inherits:
Object
  • Object
show all
Includes:
Mongoid::Document, Mongoid::Timestamps, DataStore::Mongo::FileHelper
Defined in:
app/models/neofiles/file.rb

Overview

This model stores file metadata like name, size, md5 hash etc. A model ID is essentially what is a “file” in the rest of an application. In some way Neofiles::File may be seen as remote filesystem, where you drop in files and keep their generated IDs to fetch them later, or setup web frontend via Neofiles::Files/ImagesController and request file bytes by ID from there.

When persisting new file to the MongoDB database one must initialize new instance with metadata and set field #file= to IO-like object that holds real bytes. When #save method is called, file metadata are saved into Neofiles::File and file content is read and saved into collection of Neofiles::FileChunk, each of maximum length of #chunk_size bytes:

logo = Neofiles::File.new
logo.description = 'ACME inc logo'
logo.file = '~/my-first-try.png' # or some opened file handle, or IO stream
logo.filename = 'acme.png'
logo.save
logo.chunks.to_a # return an array of Neofiles::FileChunk in order
logo.data # byte string of file contents

# in view.html.slim
- logo = Neofiles::File.find 'xxx'
= neofiles_file_url logo          # 'http://doma.in/neofiles/serve-file/#{logo.id}'
= neofiles_link logo, 'Our logo'  # '<a href="...#{logo.id}">Our logo</a>'

This file/chunks concept is called Mongo GridFS (Grid File System) and is described as a standard way of storing files in MongoDB.

MongoDB collection & client (session) can be changed via Rails.application.config.neofiles.mongo_files_collection and Rails.application.config.neofiles.mongo_client

Model fields:

filename      - real name of file, is guessed when setting #file= but can be changed manually later
content_type  - MIME content type, is guessed when setting #file= but can be changed manually later
length        - file size in bytes
chunk_size    - max Neofiles::FileChunk size in bytes
md5           - md5 hash of file (to find duplicates for example)
description   - arbitrary description
owner_type/id - as in Mongoid polymorphic belongs_to relation, a class name & ID of object this file belongs to
is_deleted    - flag that file was once marked as deleted (just a flag for future use, affects nothing)

There is no sense in deleting a file since space it used to hold is not reallocated by MongoDB, so files are considered forever lasting. But technically it is possible to delete model instance and it’s chunks will be deleted as well.

Direct Known Subclasses

Image, Swf

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#fileObject

Returns the value of attribute file.



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

def file
  @file
end

Class Method Details

.class_by_content_type(content_type) ⇒ Object

Guess descendant class of Neofiles::File by MIME content type to use special purpose class for different file types:

Neofiles::File.file_class_by_content_type('image/jpeg') # -> Neofiles::Image
Neofiles::File.file_class_by_content_type('some/unknown') # -> Neofiles::File

Can be used when persisting new files or loading from database.



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

def self.class_by_content_type(content_type)
  case content_type
  when 'image/svg+xml'
    ::Neofiles::File
  when /\Aimage\//
    ::Neofiles::Image
  when 'application/x-shockwave-flash'
    ::Neofiles::Swf
  else
    self
  end
end

.class_by_file_name(file_name) ⇒ Object

Same as file_class_by_content_type but for file name string.



195
196
197
# File 'app/models/neofiles/file.rb', line 195

def self.class_by_file_name(file_name)
  class_by_content_type(extract_content_type(file_name))
end

.class_by_file_object(file_object) ⇒ Object

Same as file_class_by_content_type but for file-like object.



200
201
202
# File 'app/models/neofiles/file.rb', line 200

def self.class_by_file_object(file_object)
  class_by_file_name(extract_basename(file_object))
end

.cleanname(pathname) ⇒ Object

Extract only file name partion from path.



170
171
172
# File 'app/models/neofiles/file.rb', line 170

def self.cleanname(pathname)
  ::File.basename(pathname.to_s)
end

.extract_basename(object) ⇒ Object

Try different methods to extract file name or path from argument object.



138
139
140
141
142
143
144
145
146
147
# File 'app/models/neofiles/file.rb', line 138

def self.extract_basename(object)
  filename = nil
  %i{ original_path original_filename path filename pathname }.each do |msg|
    if object.respond_to?(msg)
      filename = object.send(msg)
      break
    end
  end
  filename ? cleanname(filename) : nil
end

.extract_content_type(basename) ⇒ Object

Try different methods to extract MIME content type from file name, e.g. jpeg -> image/jpeg



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'app/models/neofiles/file.rb', line 150

def self.extract_content_type(basename)
  if defined?(MIME)
    content_type = MIME::Types.type_for(basename.to_s).first
  else
    ext = ::File.extname(basename.to_s).downcase.sub(/[.]/, '')
    if ext.in? %w{ jpeg jpg gif png }
      content_type = 'image/' + ext.sub(/jpg/, 'jpeg')
    elsif ext == 'swf'
      content_type = 'application/x-shockwave-flash'
    elsif ext == 'svg'
      content_type = 'image/svg+xml'
    else
      content_type = nil
    end
  end

  content_type.to_s if content_type
end

.get_stores_class_name(stores) ⇒ Object

return array with names for each store



213
214
215
216
217
218
219
# File 'app/models/neofiles/file.rb', line 213

def self.get_stores_class_name(stores)
  if stores.is_a?(Array)
    stores.map { |store| Neofiles::DataStore.const_get(store.camelize) }
  else
    get_stores_class_name [stores]
  end
end

.read_data_storesObject



204
205
206
# File 'app/models/neofiles/file.rb', line 204

def self.read_data_stores
  get_stores_class_name Rails.application.config.neofiles.read_data_stores
end

.write_data_storesObject



208
209
210
# File 'app/models/neofiles/file.rb', line 208

def self.write_data_stores
  get_stores_class_name Rails.application.config.neofiles.write_data_stores
end

Instance Method Details

#admin_compact_view(template) ⇒ Object

Representation of file in admin “compact” mode, @see Neofiles::AdminController#file_compact. To be redefined by descendants.



133
134
135
# File 'app/models/neofiles/file.rb', line 133

def admin_compact_view(template)
  template.neofiles_link self, nil, target: '_blank'
end

#base64Object

Encode bytes in base64.



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

def base64
  Array(data).pack('m')
end

#dataObject

Chunks bytes concatenated, that is the whole file content.



64
65
66
67
68
69
70
71
72
# File 'app/models/neofiles/file.rb', line 64

def data
  self.class.read_data_stores.each do |store|
    begin
      return store.find(id).data
    rescue Neofiles::DataStore::NotFoundException
      next
    end
  end
end

#data_uri(options = {}) ⇒ Object

Encode bytes id data uri.



80
81
82
83
# File 'app/models/neofiles/file.rb', line 80

def data_uri(options = {})
  data = base64.chomp
  "data:#{content_type};base64,#{data}"
end

#nullify_unpersisted_fileObject

Reset @file after save.



127
128
129
# File 'app/models/neofiles/file.rb', line 127

def nullify_unpersisted_file
  @file = nil
end

#save_fileObject

Real file saving goes here. File length and md5 hash are computed automatically.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/neofiles/file.rb', line 110

def save_file
  if @file
    self.class.write_data_stores.each do |store|
      begin
        data_store_object = store.new id
        data_store_object.write @file
        self.length = data_store_object.length
        self.md5    = data_store_object.md5
      rescue => ex
        notify_airbrake(ex) if defined? notify_airbrake
        next
      end
    end
  end
end

#unpersisted_file?Boolean

Are we going to save file bytes on next #save?

Returns:

  • (Boolean)


104
105
106
# File 'app/models/neofiles/file.rb', line 104

def unpersisted_file?
  not @file.nil?
end