Module: AttachmentSaver::DataStores::FileSystem

Defined in:
lib/datastores/file_system.rb

Constant Summary collapse

RETRIES =

max attempts at finding a unique storage key. very rare to have to retry at all, so if it fails after 100 attempts, something’s seriously wrong.

100

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object



12
13
14
15
16
17
# File 'lib/datastores/file_system.rb', line 12

def self.included(base)
  base.attachment_options[:storage_directory] ||= File.join(Rails.root, 'public') # this is the part of the full filename that _doesn't_ form part of the HTTP path to the files
  base.attachment_options[:storage_path_base] ||= Rails.env == 'production' ? base.table_name : File.join(Rails.env, base.table_name) # and this is the part that does.
  base.attachment_options[:filter_filenames] = Regexp.new(base.attachment_options[:filter_filenames]) if base.attachment_options[:filter_filenames].is_a?(String) # may be nil, in which case the normal randomised-filename scheme is used instead of the filtered-original-filename scheme
  base.attachment_options[:file_permissions] = 0664 unless base.attachment_options.has_key?(:file_permissions) # we don't use || as nil is a meaningful value for this option - it means to not explicitly set the file permissions
end

Instance Method Details

#in_storage?Boolean

Returns:

  • (Boolean)


85
86
87
# File 'lib/datastores/file_system.rb', line 85

def in_storage?
  File.exist?(storage_filename)
end

#public_pathObject



89
90
91
# File 'lib/datastores/file_system.rb', line 89

def public_path
  "/#{storage_key.tr('\\', '/')}" # the tr is just for windows' benefit
end

#reprocess!Object



93
94
95
96
97
# File 'lib/datastores/file_system.rb', line 93

def reprocess!
  raise "this attachment already has a file open to process" unless uploaded_file.nil?
  process_attachment_with_wrapping(storage_filename) if process_attachment?
  save!
end

#save_attachmentObject



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/datastores/file_system.rb', line 19

def save_attachment
  return unless @save_upload # this method is called every time the model is saved, not just when a new file has been uploaded
  
  old_storage_key = storage_key
  @old_filenames ||= []
  @old_filenames << storage_filename unless storage_key.blank?
  self.storage_key = nil
  define_finalizer
  
  # choose a storage key (ie. path/filename) and try it; note that we assign a new 
  # storage key for every new upload, not just every new AR model, so that the URL
  # changes each time, which allows long/infinite cache TTLs & CDN support.
  begin
    if derive_storage_key?
      begin
        # for thumbnail/other derived images, we base the filename on the original 
        # (parent) image + the derived format name
        self.storage_key = derive_storage_key_from(original)
        save_attachment_to(storage_filename)
      rescue Errno::EEXIST # if clobbering pre-existing files (only possible if using filtered_filenames, and even then only if creating new derived images explicitly at some time other than during processing the parent), we still don't want to write into them, we want to use a new file & an atomic rename
        retries = 0
        begin
          self.storage_key = derive_storage_key_from(original, retries + 2) # +2 is arbitrary, I just think it's more human-friendly to go from xyz_thumb.jpg to xyz_thumb2.jpg rather than xyz_thumb0.jpg
          save_attachment_to(storage_filename)
        rescue Errno::EEXIST
          raise if (retries += 1) >= RETRIES # in fact it would be very unusual to ever need to retry at all, let alone multiple times; if you hit this, your operating system is actually broken (or someone's messed with storage_filename)
          retry # pick a new random name and try again
        end
      end
    else
      retries = 0
      begin
        if self.class.attachment_options[:filter_filenames] && respond_to?(:original_filename) && !original_filename.blank?
          # replace all the original_filename characters not included in the keep_filenames character list with underscores, leave the rest; store in randomized directories to avoid naming clashes
          basename = AttachmentSaver::split_filename(original_filename).first.gsub(self.class.attachment_options[:filter_filenames], '_')
          self.storage_key = File.join(self.class.attachment_options[:storage_path_base], random_segment(3), random_segment(3), "#{basename}.#{file_extension}")
        else
          # for new files under this option, we pick a random name (split into 3 parts - 2 directories and a file - to help keep the directories at manageable sizes), and never overwrite
          # this is the default setting, and IMHO the most best choice for most apps; the original filenames are typically pretty meaningless
          self.storage_key = File.join(self.class.attachment_options[:storage_path_base], random_segment(2), random_segment(2), "#{random_segment(6)}.#{file_extension}") # in fact just two random characters in the last part would be ample, since 36^(2+2+2) = billions, but we sacrifice 4 more characters of URL shortness for the benefit of ppl saving the assets to disk without renaming them
        end
        save_attachment_to(storage_filename)
      rescue Errno::EEXIST
        raise if (retries += 1) >= RETRIES # in fact it would be very unusual to ever need to retry at all, let alone multiple times; if you hit this, your operating system is actually broken (or someone's messed with storage_filename)
        retry # pick a new random name and try again
      end
    end

    # successfully written to file; process the attachment
    process_attachment_with_wrapping(storage_filename) if process_attachment?
    # if there's exceptions later (ie. during save itself) that prevent the record from being saved, the finalizer will clean up the file

    @save_upload = nil
  rescue Exception => ex
    FileUtils.rm_f(storage_filename) unless storage_key.blank? || ex.is_a?(Errno::EEXIST)
    self.storage_key = old_storage_key
    @old_filenames.pop unless old_storage_key.blank?
    raise if ex.is_a?(AttachmentSaverError)
    raise FileSystemAttachmentDataStoreError, "#{ex.class}: #{ex.message}", ex.backtrace
  end
end

#storage_filenameObject



81
82
83
# File 'lib/datastores/file_system.rb', line 81

def storage_filename
  File.join(self.class.attachment_options[:storage_directory], storage_key)
end