require 'fileutils'
module TaliaCore
module DataTypes
module FileStore
# the handle for the file
@file_handle = nil
# position of the reading cursors
@position = 0
# Class for data paths
class DataPath < String ; end
module ClassMethods
# Find or create a record for the given location and source_id, then it saves the given file.
def find_or_create_and_assign_file(params)
data_record = self.find_or_create_by_location_and_source_id(extract_filename(params[:file]), params[:source_id])
data_record.file = params[:file]
data_record.save # force attachment save and it also saves type attribute.
end
end
# This will create the data object from a given file. This will simply move the
# given file to the correct location upon save. This will avoid multiple
# read/write operations during import.
#
# The original file must not be touched by external processes until the
# record is saved.
#
# If the delete_original flag is set, the original file will be removed
# on save
def create_from_file(location, file_path, delete_original = false)
close_file
self.location = location
@file_data_to_write = DataPath.new(file_path)
@delete_original_file = delete_original
end
# Add data as string into file
def create_from_data(file_location, data, options = {})
# close file if opened
close_file
# Set the location for the record
self.location = file_location
if(data.respond_to?(:read))
@file_data_to_write = data.read
else
@file_data_to_write = data
end
end
# returns the complete text
def all_text
if(!is_file_open?)
open_file
end
@file_handle.read(self.size)
end
# This is a placeholder in case file is used in a form.
def file() nil; end
# Assign the file data (StringIO or File).
def file=(file_data)
return nil if file_data.nil? || file_data.size == 0
self.assign_type file_data.content_type
self.location = file_data.original_filename
if file_data.is_a?(StringIO)
file_data.rewind
self.temp_data = file_data.read
else
self.temp_path = file_data.path
end
@save_attachment = true
end
def write_file_after_save
# check if there are data to write
return unless(@file_data_to_write)
# check if file already exists
# raise(RuntimeError, "File already exists: #{file_path}") if(File.exists?(file_path))
begin
self.class.benchmark("\033[36m\033[1m\033[4mFileStore\033[0m Saving file for #{self.id}") do
# create data directory path
FileUtils.mkdir_p(data_directory)
if(@file_data_to_write.is_a?(DataPath))
copy_data_file
else
save_cached_data
end
@file_data_to_write = nil
end
rescue Exception => e
assit_fail("Exception on writing file #{self.location}: #{e}")
end
end
# Return true if the specified data file is open, false otherwise
def is_file_open?
(@file_handle != nil)
end
# Assign the STI subclass, perfoming a mime-type lookup.
def assign_type(content_type)
self.type = self.class.class_type_from(content_type).name
end
# private methods ==================================================================
private
# This saves the cached data from the file creation
def save_cached_data
# open file for writing
@file_handle = File.open(file_path, 'w')
# write data string into file
@file_handle << (@file_data_to_write.respond_to?(:read) ? @file_data_to_write.read : @file_data_to_write)
# close file
close_file
end
# This copies the data file with which this object was created to the
# actual storage lcoation
def copy_data_file
copy_or_move(@file_data_to_write, file_path)
end
# Open a specified file name and return a file handle.
# If the file is already opened, return the file handle
def open_file(file_name = file_path, options = 'rb')
# chek if the file name really exists, otherwise raise an exception
if !File.exists?(file_name)
raise(IOError, "File #{file_name} could not be opened.", caller)
end
# try to open the specified file if is not already open
if @file_handle == nil
@file_handle = File.open(file_name, options)
# check and set the initial position of the reading cursors.
# It's necessary to do this thing because we don't know if the user
# has specified the initial reading cursors befort starting working on file
@position ||= @file_handle.pos
@file_handle.pos = @position
end
end
# Close an already opened file
def close_file
if is_file_open?
@file_handle.close
# reset 'flags' variables and position
@file_handle = nil
@position = 0
end
end
# Read all bytes from a file
def read_all_bytes
# 1. Open file with option "r" (reading) and "b" (binary, useful for window system)
open_file
# 2. Read all bytes
begin
bytes = @file_handle.read(self.size).unpack("C*")
return bytes
rescue
# re-raise system the excepiton
raise
return nil
ensure
# 3. Close the file
close_file
end
end
# return the next_byte
def next_byte(close)
if !is_file_open?
open_file
end
begin
current_byte = @file_handle.getc
if current_byte == nil or close
close_file
else
@position += 1
end
return current_byte
rescue
# re-raise system the excepiton
raise
close_file
return nil
end
end
# Copy or move the source file to the target. Working around all the
# things that suck in JRuby. This will honour two environment settings:
#
# * delay_file_copies - will not copy the files, but create a batch file
# so that the copy can be done later. Uses the DelayedCopier class.
# * fast_copies - will use the "normal" copy method from FileUtils that
# is faster. Since it crashed the system for us, the default is to
# use a "safe" workaround. The workaround is probably necessary for
# JRuby only.
def copy_or_move(original, target)
if(@delete_original_file)
FileUtils.move(original, target)
else
# Delay can be enabled through enviroment
if(delay_copies)
DelayedCopier.cp(original, target)
elsif(fast_copies)
FileUtils.copy(original, target)
else
# Call the copy as an external command. This is to work around the
# crashes that occurred using the builtin copy
from_file = File.expand_path(original)
to_file = File.expand_path(target)
system_success = system("cp '#{from_file}' '#{to_file}'")
raise(IOError, "copy error '#{from_file}' '#{to_file}'") unless system_success
end
end
end
# Returns true if the 'delayed write' is enabled in the environment
def delay_copies
ENV['delay_file_copies'] == 'true' || ENV['delay_file_copies'] == 'yes'
end
# Returns true if the 'fast copy' is enabled in the environment.
# Otherwise the class will use a workaround that is less likely to
# crash the whole system using JRuby.
def fast_copies
ENV['fast_copies'] == 'true' || ENV['fast_copies'] == 'yes'
end
# Return the data size
def data_size
File.size(file_path)
end
# set the position of the reading cursor
def set_position(position)
if (position != nil and position =~ /\A\d+\Z/)
if (position < size)
set_position(position)
else
raise(IOError, 'Position out of range', caller)
end
else
raise(IOError, 'Position not valid. It must be an integer')
end
end
# Check if the attachment should be saved.
def save_attachment?
@save_attachment
end
# Save the attachment, copying the file from the temp_path to the data_path.
def save_attachment
return unless save_attachment?
save_file
@save_attachment = false
true
end
# Destroy the attachment
def destroy_attachment
FileUtils.rm(full_filename) if File.exists?(full_filename)
end
# Save the attachment on the data_path directory.
def save_file
FileUtils.mkdir_p(File.dirname(full_filename))
FileUtils.cp(temp_path, full_filename)
FileUtils.chmod(0644, full_filename)
end
end
end
end