Class: FilePipeline::VersionedFile

Inherits:
Object
  • Object
show all
Includes:
FileOperations::ExifManipulable
Defined in:
lib/file_pipeline/versioned_file.rb

Overview

VersionedFile creates a directory where it stores any versions of file.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FileOperations::ExifManipulable

#delete_tags, file_tags, #missing_exif_fields, #parse_exif_errors, parse_tag_error, #read_exif, strip_path, #write_exif

Constructor Details

#initialize(file, target_suffix: SecureRandom.uuid) ⇒ VersionedFile

Returns a new instance with file as the #original.

Arguments
  • file - Path to the file the instance will be based on. That file should not be touched unless #finalize is called with the :overwrite option set to true.

Caveat it can not be ruled out that buggy or malignant file operations modify the original.

– FIXME: protect the original ++

Options

<tt>target_suffix</ttm> is a string to be appended to the file that will be written by #finalize (the last version) if #finalize is to preserve the original. It is recommended to use a UUID (default) to avoid clashes with other files in the directory.



43
44
45
46
47
48
49
50
51
# File 'lib/file_pipeline/versioned_file.rb', line 43

def initialize(file, target_suffix: SecureRandom.uuid)
  raise Errors::MissingVersionFileError, file: file unless File.exist? file

  @original = file
  @basename = File.basename(file, '.*')
  @history = {}
  @directory = nil
  @target_suffix = target_suffix
end

Instance Attribute Details

#basenameObject (readonly)

The basename of the versioned file.



9
10
11
# File 'lib/file_pipeline/versioned_file.rb', line 9

def basename
  @basename
end

#historyObject (readonly)

A hash with file paths as keys, information on the modifications applied to create the version as values (instances of FileOperations::Results).



13
14
15
# File 'lib/file_pipeline/versioned_file.rb', line 13

def history
  @history
end

#originalObject (readonly)

The path to the original file of self.



16
17
18
# File 'lib/file_pipeline/versioned_file.rb', line 16

def original
  @original
end

#target_suffixObject (readonly)

A String that is appended to the file basename when the file written by #finalize is not replacing the original.



20
21
22
# File 'lib/file_pipeline/versioned_file.rb', line 20

def target_suffix
  @target_suffix
end

Class Method Details

.copy(src, dir, filename) ⇒ Object

Copies the file with path src to /dir/filename.



54
55
56
57
58
# File 'lib/file_pipeline/versioned_file.rb', line 54

def self.copy(src, dir, filename)
  dest = FilePipeline.path(dir, filename)
  FileUtils.cp src, dest
  dest
end

.move(src, dir, filename) ⇒ Object

Moves the file with path src to /dir/filename.



61
62
63
64
65
# File 'lib/file_pipeline/versioned_file.rb', line 61

def self.move(src, dir, filename)
  dest = FilePipeline.path(dir, filename)
  FileUtils.mv src, dest
  dest
end

Instance Method Details

#<<(version_info) ⇒ Object

Adds a new version to #history and returns self.

version_info must be a path to an existing file or an array with the path and optionally a FileOperations::Results instance: ['path/to/file', results_object]. Will move the file to #directory if it is in another directory.



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/file_pipeline/versioned_file.rb', line 73

def <<(version_info)
  file, info = version_info
  raise Errors::FailedModificationError, info: info if info&.failure

  version = validate(file)
  @history[version] = info
  self
rescue StandardError => e
  reset
  raise e
end

#captured_dataObject

Returns a two-dimesnional array, where each nested array has two items; the file operation object and data captured by the operartion (if any).

[[description_object, data_or_nil], ...]



89
90
91
# File 'lib/file_pipeline/versioned_file.rb', line 89

def captured_data
  filter_history :data
end

#captured_data_for(operation_name, **options) ⇒ Object

Returns any data captured by operation_name.

If multiple instances of one operation class have modified the file, pass any options the specific instance of the operation was initialized with as the optional second argument.



98
99
100
101
102
103
104
# File 'lib/file_pipeline/versioned_file.rb', line 98

def captured_data_for(operation_name, **options)
  raw_data = captured_data.filter do |operation, _|
    operation.name == operation_name &&
      options.all? { |k, v| operation.options[k] == v }
  end
  raw_data.map(&:last)
end

#captured_data_with(tag) ⇒ Object

Returns an array with all data captured by operations with tag has.

Tags are defined in FileOperations::CapturedDataTags



109
110
111
112
113
114
115
116
117
# File 'lib/file_pipeline/versioned_file.rb', line 109

def captured_data_with(tag)
  return unless changed?

  captured_data.map do |operation, results|
    next unless operation.captured_data_tag == tag

    results
  end
end

#changed?Boolean

Returns true if there are #versions (file has been modified).

Warning: It will also return true if the file has been cloned.

Returns:

  • (Boolean)


122
123
124
# File 'lib/file_pipeline/versioned_file.rb', line 122

def changed?
  current != original
end

#cloneObject Also known as: touch

Creates a new identical version of #current. Will only add the path of the file to history, but no FileOperations::Results.



128
129
130
131
132
# File 'lib/file_pipeline/versioned_file.rb', line 128

def clone
  filename = FilePipeline.new_basename + current_extension
  clone_file = VersionedFile.copy(current, directory, filename)
  self << clone_file
end

#currentObject

Returns the path to the current file or the #original if no versions have been created.



136
137
138
# File 'lib/file_pipeline/versioned_file.rb', line 136

def current
  versions.last || original
end

#current_extensionObject

Returns the file extension for the #current file.



141
142
143
# File 'lib/file_pipeline/versioned_file.rb', line 141

def current_extension
  File.extname current
end

#directoryObject

Returns the path to the directory where the versioned of self are stored. Creates the directory if it does not exist.



147
148
149
# File 'lib/file_pipeline/versioned_file.rb', line 147

def directory
  @directory ||= workdir
end

#finalize(overwrite: false) ⇒ Object

Writes the #current version to #basename, optionally the #target_suffix, and the #current_extension in #original_dir. Deletes all versions and resets the #history to an empty Hash. Returns the path to the written file.

Options
  • overwrite - true or false

    • false (default) - The #target_suffix will be appended to the #basename and the #original will be preserved.

    • true - The finalized version will replace the #original.



162
163
164
165
166
167
168
# File 'lib/file_pipeline/versioned_file.rb', line 162

def finalize(overwrite: false)
  filename = overwrite ? replacing_trarget : preserving_taget
  FileUtils.rm original if overwrite
  @original = VersionedFile.copy(current, original_dir, filename)
ensure
  reset
end

#logObject

Returns an array of triplets (arryas with three items each): the name of the file operation class (a string), options (a hash), and the actual log (an array).



173
174
175
176
# File 'lib/file_pipeline/versioned_file.rb', line 173

def log
  filter_history(:log)
    .map { |operation, info| [operation.name, operation.options, info] }
end

#metadata(for_version: :current) ⇒ Object

Returns the Exif metadata

Options
  • :for_version - current or original

    • current (default) - Metadata for the #current file will be returned.

    • original - Metadata for the #original file will be returned.

– TODO: when file is not an image file, this should return other metadata than exif. TODO: implement the option to return metadata for a specif version index ++



192
193
194
195
# File 'lib/file_pipeline/versioned_file.rb', line 192

def (for_version: :current)
  file = public_send for_version
  read_exif(file).first
end

#modifyObject

Creates a new version. Requires a block that must return a path to an existing file or an array with the path and optionally a FileOperations::Results instance: ['path/to/file', results_object].

The actual file modification logic will be in the block.

The block must take three arguments: for the #current file (from which the modified version will be created), the work #directory (to where the modified file will be written), and the #original file (which will only be used in modifications that need the original file for reference, such as modifications that restore file metadata that was lost in other modifications).



210
211
212
# File 'lib/file_pipeline/versioned_file.rb', line 210

def modify
  self << yield(current, directory, original)
end

#original_dirObject

Returns the directory where #original is stored.



215
216
217
# File 'lib/file_pipeline/versioned_file.rb', line 215

def original_dir
  File.dirname original
end

#recovered_metadataObject

Returns a hash into which all captured data from file operations with the FileOperations::CapturedDataTags::DROPPED_EXIF_DATA has been merged.



221
222
223
224
# File 'lib/file_pipeline/versioned_file.rb', line 221

def 
  captured_data_with(FileOperations::CapturedDataTags::DROPPED_EXIF_DATA)
    &.reduce({}) { |recovered, data| recovered.merge data }
end

#versionsObject

Returns an array with paths to the version files of self (excluding #original).



228
229
230
# File 'lib/file_pipeline/versioned_file.rb', line 228

def versions
  history.keys
end