Class: Download

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/dataset/download.rb

Overview

A generated file, stored for the user to download

A download object is generated when a delayed job needs to send some data back to the user. We keep track of them in the database so that they can be expired and deleted when necessary.

All files are stored in RAILS_ROOT/downloads, not in the public tree, so that it is impossible to get the web server to serve the download files without passing through the RLetters's authentication. This folder is expected to be symlinked over from shared during a Capistrano deployment.

Class Method Summary (collapse)

Instance Method Summary (collapse)

Class Method Details

+ (Download) create_file(basename) {|f| ... }

Creates a download object and file, then passes the file to the block

This function will create the file basename in the downloads folder (do not put a path of any sort on basename). A unique timestamp will be appended to the filename, and the file created. The file handle will then be passed to the provided block. Finally, the function creates a Download model, saves it in the database, and returns it.

Closing the file within the block is optional; it will be closed when the block terminates if it hasn't been already.

Examples:

Create a file

dl = Download.create_file('test.txt') do |f|
  f.write("1234567890")
  f.close
end

Parameters:

  • basename (String)

    the base name of the file to create

Yields:

  • (f)

    a File object, opened for writing

Yield Parameters:

  • f (File)

    the file object created

Returns:



50
51
52
53
54
55
56
57
58
59
60
61
# File 'app/models/dataset/download.rb', line 50

def self.create_file(basename)
  fn = unique_filename basename
      
  # Yield out to the block
  f = File.new(filename_to_path(fn), "w")
  yield f
  
  f.close unless f.closed?
  
  # Build a Download object and return it
  Download.create({ :filename => fn })
end

+ (String) filename_to_path(fn) (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert a filename to an absolute path

Parameters:

  • fn (String)

    relative filename

Returns:

  • (String)

    absolute file path



145
146
147
# File 'app/models/dataset/download.rb', line 145

def self.filename_to_path(fn)
  Rails.root.join('downloads', fn)
end

+ (String) unique_filename(basename) (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get the path to a new download file

This function will fetch a path for the file basename in the downloads folder (do not put a path of any sort on basename). A unique timestamp will be appended to the filename.

Parameters:

  • basename (String)

    the base name of the file to create

Returns:

  • (String)

    the name of the file



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'app/models/dataset/download.rb', line 119

def self.unique_filename(basename)
  ext = File.extname(basename)
  base = File.basename(basename, ext)
  
  # Add a timestamp to the basename
  timestamp = Time.now.utc.strftime('-%Y%m%d%H%M%S')
  ret = base + timestamp + ext
  fn = filename_to_path(ret)
  
  i = 0
  while File.exists? fn
    i = i + 1
    ret = base + timestamp + i.to_s + ext
    fn = filename_to_path(ret)
    
    # Runaway loop counter (DoS?)
    raise StandardError, "Cannot find a filename for download" if i == 100
  end
  
  ret
end

Instance Method Details

- (undefined) delete_file (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Delete the file when the database record is destroyed

Just ignore if the file delete fails and raises an error. We'll have to manually clean the downloads directory in that case.

Returns:

  • (undefined)


156
157
158
# File 'app/models/dataset/download.rb', line 156

def delete_file
  File::delete(filename) rescue nil
end

- (String) filename

Get the filename for this download

We save filenames in the database as relative paths, since the absolute paths may change over time across Capistrano deployments. This function wraps the query of the filename variable and converts it to an absolute path.

Examples:

Open this file for reading

dl = Download.find(...)
File.open(dl.filename) do |f|
  ...
  f.close
end

Returns:

  • (String)

    this download's filename

Raises:

  • (RecordInvalid)

    if the filename is missing (validates :presence)

  • (RecordInvalid)

    if the filename has invalid characters (validates :format, only [A-Za-z.-_] permitted)



81
82
83
84
# File 'app/models/dataset/download.rb', line 81

def filename
  return nil unless filename?
  Download.filename_to_path(read_attribute(:filename))
end

- (undefined) send_file(controller)

Send this download to the user

This function does its best to guess the MIME type by looking at the file extension.

Examples:

Send this file to the user from controller method

f = current_user.downloads.find_by_id('1')
f.send_file(self)

Parameters:

  • controller (ActionController)

    The controller to use

Returns:

  • (undefined)


97
98
99
100
101
102
103
104
105
106
# File 'app/models/dataset/download.rb', line 97

def send_file(controller)
  ext = File.extname(filename)[1..-1]
  mime_type = Mime::Type.lookup_by_extension(ext)
  content_type = mime_type.to_s unless mime_type.nil?
  content_type ||= 'text/plain'
  
  controller.send_file(filename,
    :x_sendfile => true,
    :type => content_type)
end