file_pipeline 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fe1107c14a49c1372b1d3f62c76db36b262311113d3661b4be3ac3b7275f97b8
4
+ data.tar.gz: f01ab4be7e0eb246a7da41fa734a2e88491dc90ce01033b3b7a125c012029525
5
+ SHA512:
6
+ metadata.gz: bc53dff7ae7c5a9955f72e83ee2be935f59db398093b555531b5bdbd619faf4637d6bf5effa1842fcf8b3906a90c1626394b892aa6fd24042063b7e78c2f86fb
7
+ data.tar.gz: 14f493a7bf76fc55c4e60df8d2f999ae66b5770a064c3cbf03b46e52b8a03602629e399cfb6d395109cbf87d89f7dfc459d3f9911c7b1bdc959fc8b9e49c7881
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Martin Stein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module Errors
5
+ # Error class for exceptions that are raised when a a FileOperation in a
6
+ # Pipeline returns failure.
7
+ class FailedModificationError < StandardError
8
+ # The file opration that caused the error.
9
+ attr_reader :info
10
+
11
+ def initialize(msg = nil, info: nil)
12
+ @info = info
13
+ if info.respond_to?(:operation) && info.respond_to?(:log)
14
+ msg ||= "#{@info.operation&.name} with options"\
15
+ " #{@info.operation&.options} failed, log: #{@info.log}"
16
+ else
17
+ msg ||= 'Operation failed' unless info
18
+ end
19
+ super msg
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module Errors
5
+ # Error class for exceptions that are raised when a new version is added,
6
+ # but no actual file is associated with it.
7
+ class MissingVersionFileError < StandardError
8
+ # The file that could not be found.
9
+ attr_reader :file
10
+
11
+ def initialize(msg = nil, file: nil)
12
+ @file = file
13
+ msg ||= "File missing for version '#{@file}'"
14
+ super msg
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module Errors
5
+ # Error class for exceptions that are raised when a specified source
6
+ # directory does not exist (or is not a directory).
7
+ class SourceDirectoryError < StandardError
8
+ # The directory that could not be found.
9
+ attr_reader :directory
10
+
11
+ def initialize(msg = nil, dir: nil)
12
+ @directory = dir
13
+ msg ||= "The source directory #{@directory} does not exist"
14
+ super msg
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module Errors
5
+ # Error class for exceptions that are raised when a specified source
6
+ # directory does not exist (or is not a directory).
7
+ class SourceFileError < StandardError
8
+ # The source file that could not be located.
9
+ attr_reader :file
10
+
11
+ # The directories for source files that were registered with FilePipeline
12
+ # and searched at the time the error was raises.
13
+ attr_reader :directories
14
+
15
+ def initialize(msg = nil, file: nil, directories: nil)
16
+ @file = file
17
+ @directories = directories
18
+ default_msg = "The source file #{@file} was not found. Searched in:\n"
19
+ msg ||= @directories.inject(default_msg) do |str, dir|
20
+ str + "\t- #{dir}\n"
21
+ end
22
+ super msg
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors/failed_modification_error'
4
+ require_relative 'errors/missing_version_file_error'
5
+ require_relative 'errors/source_directory_error'
6
+ require_relative 'errors/source_file_error'
7
+
8
+ module FilePipeline
9
+ # Contains error classes for FilePipeline.
10
+ module Errors
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module FileOperations
5
+ # Module that contains constants used as tags for the kinds of data captured
6
+ # by file operations.
7
+ module CapturedDataTags
8
+ # Tag for operations that do not return data
9
+ NO_DATA = :no_data
10
+
11
+ # Tag for operations that return _Exif_ metadata that has not been
12
+ # preserved (by accident or intention) in a file.
13
+ DROPPED_EXIF_DATA = :dropped_exif_data
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module FileOperations
5
+ # A FileOperation that will redact (delete) unwanted _Exif_ tags from a
6
+ # file's metadata.
7
+ #
8
+ # This could be tags containing sensitive data, such as e.g. _GPS_ location
9
+ # data.
10
+ #
11
+ # *Caveat:* if this operation is applied to a file together with
12
+ # ExifRestoration, it should be applied _after_ the latter, to avoid
13
+ # redacted tags being restored.
14
+ class ExifRedaction < FileOperation
15
+ include ExifManipulable
16
+
17
+ # :args: options
18
+ #
19
+ # Returns a new instance.
20
+ #
21
+ # ===== Options
22
+ #
23
+ # * <tt>redact_tags</tt> - _Exif_ tags to be deleted.
24
+ #
25
+ def initialize(**opts)
26
+ defaults = { redact_tags: [] }
27
+ super(opts, defaults)
28
+ end
29
+
30
+ # Returns the DROPPED_EXIF_DATA tag defined in CapturedDataTags.
31
+ #
32
+ # This operation will capture all _Exif_ tags and their values are
33
+ # declared in #options <tt>redact_tags</tt> that are redacted from the
34
+ # file created by the operation.
35
+ def captured_data_tag
36
+ CapturedDataTags::DROPPED_EXIF_DATA
37
+ end
38
+
39
+ # :args: src_file, out_file
40
+ #
41
+ # Writes a new version of <tt>src_file</tt> to <tt>out_file</tt> with all
42
+ # _Exif_ tags provided in the +redact_tags+ option deleted.
43
+ #
44
+ # Will return all deleted _Exif_ tags and their values as data.
45
+ def operation(*args)
46
+ src_file, out_file = args
47
+ FileUtils.cp src_file, out_file
48
+ delete_tags out_file, options[:redact_tags]
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module FileOperations
5
+ # A FileOperation that compares Exif Metadata in two files and copies tags
6
+ # missing in one from the other. Used to restore Exif tags that were not
7
+ # preserved during e.g. a file conversion.
8
+ #
9
+ # *Caveat:* if this operation is applied to a file together with
10
+ # ExifRedaction, it should be applied _before_ the latter, to avoid
11
+ # redacted tags being restored.
12
+ class ExifRestoration < FileOperation
13
+ include ExifManipulable
14
+
15
+ # :args: options
16
+ #
17
+ # Returns a new instance.
18
+ #
19
+ # ===== Options
20
+ #
21
+ # * <tt>skip_tags</tt> - _Exif_ tags to be ignored during restoration.
22
+ #
23
+ # The ExifManipulable mixin defines a set of _Exif_ tags that will always
24
+ # be ignored. These are tags relating to the file properties (e.g.
25
+ # filesize, MIME-type) that will have been altered by any prior operation,
26
+ # such as file format conversions.
27
+ def initialize(**opts)
28
+ defaults = { skip_tags: [] }
29
+ super(opts, defaults)
30
+ @options[:skip_tags] += ExifManipulable.file_tags
31
+ end
32
+
33
+ # Returns the DROPPED_EXIF_DATA tag defined in CapturedDataTags.
34
+ #
35
+ # This operation will capture any _Exif_ tags and their values that could
36
+ # not be written to the file created by the operation.
37
+ def captured_data_tag
38
+ CapturedDataTags::DROPPED_EXIF_DATA
39
+ end
40
+
41
+ # :args: src_file, out_file
42
+ #
43
+ # Writes a new version of <tt>src_file</tt> to <tt>out_file</tt> with all
44
+ # writable _Exif_ tags from +original+ restored.
45
+ #
46
+ # Will return any _Exif_ tags that could not be written and their values
47
+ # from the +original+ file as data.
48
+ def operation(src_file, out_file, original)
49
+ original_exif, src_file_exif = read_exif original, src_file
50
+ values = missing_exif_fields(src_file_exif, original_exif)
51
+ FileUtils.cp src_file, out_file
52
+ write_exif out_file, values
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module FileOperations
5
+ # Saves a file to a <em>tiled multi-resolution TIFF</em> ('pyramid'), as
6
+ # required by e.g. the IIP image server.
7
+ #
8
+ # See https://iipimage.sourceforge.io/documentation/images/ or
9
+ # https://www.loc.gov/preservation/digital/formats/fdd/fdd000237.shtml
10
+ # for more information on the format.
11
+ class PtiffConversion < FileOperation
12
+ # :args: options
13
+ #
14
+ # Returns a new instance.
15
+ #
16
+ # ===== Options
17
+ #
18
+ # * +:tile+ - Writes a tiled _TIFF_ (_default_ +true+)
19
+ # * +:tile_width+: Tile width in pixels (_default_ +256+)
20
+ # * +:tile_height+: Tile height in pixels (_default_ +256+)
21
+ def initialize(**opts)
22
+ defaults = {
23
+ tile: true,
24
+ tile_width: 256,
25
+ tile_height: 256
26
+ }
27
+ super(opts, defaults)
28
+ @options[:pyramid] = true
29
+ end
30
+
31
+ # :args: src_file, out_file
32
+ #
33
+ # Writes a pyramid tiff version of <tt>src_file</tt> to <tt>out_file</tt>.
34
+ def operation(*args)
35
+ src_file, out_file = args
36
+ image = Vips::Image.new_from_file src_file
37
+ image.tiffsave(out_file, options)
38
+ # Return lof if any
39
+ end
40
+
41
+ # Returns <tt>'.tiff'</tt> (all files created by #operation will be _TIFF_
42
+ # files).
43
+ def target_extension
44
+ '.tiff'
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module FileOperations
5
+ # Scale instances are FileOperations that will scale an image to a given
6
+ # resolution.
7
+ #
8
+ # ==== Caveats
9
+ #
10
+ # This will scale images smaller than the given width and height up.
11
+ class Scale < FileOperation
12
+ include Math
13
+
14
+ # :args: options
15
+ #
16
+ # Returns a new instance.
17
+ #
18
+ # ===== Options
19
+ #
20
+ # * +:width+ - The target image width in pixels (_default_ 1024).
21
+ # * +:height+ - The target image height in pixels (_default_ 768).
22
+ # * +:method+ - A symbol for the method used to calculate the scale:
23
+ # factor.
24
+ # * +:scale_by_bounds+ (_default_) - see #scale_by_bounds.
25
+ # * +:scale_by_pixels+ - See #scale_by_pixels.
26
+ def initialize(**opts)
27
+ defaults = {
28
+ width: 1024,
29
+ height: 768,
30
+ method: :scale_by_bounds
31
+ }
32
+ super(opts, defaults)
33
+ end
34
+
35
+ # :args: src_file, out_file
36
+ #
37
+ # Writes a scaled version of <tt>src_file</tt> to <tt>out_file</tt>.
38
+ def operation(*args)
39
+ src_file, out_file = args
40
+ image = Vips::Image.new_from_file src_file
41
+ factor = public_send options[:method], image.size
42
+ image.resize(factor).write_to_file out_file
43
+ end
44
+
45
+ # Calculatees the scale factor to scale +dimensions+ (an array with image
46
+ # width and height in pixels) so that it will match the same total pixel
47
+ # count as +:width+ multiplied by +:height+ given in #options.
48
+ #
49
+ # *Warning*: rounding errors may occur.
50
+ #
51
+ #--
52
+ # FIXME: avoid rounding errors.
53
+ #++
54
+ def scale_by_pixels(dimensions)
55
+ out_pixels = sqrt(options[:width] * options[:height]).truncate
56
+ src_pixels = sqrt(dimensions[0] * dimensions[1]).truncate
57
+ out_pixels / src_pixels.to_f
58
+ end
59
+
60
+ # Calculates the scale factor to scale +dimensions+ (an array with image
61
+ # width and height in pixels) so that it will fit inside the bounds
62
+ # defined by +:width+ and +:height+ given in #options.
63
+ def scale_by_bounds(dimensions)
64
+ x = options[:width] / dimensions[0].to_f
65
+ y = options[:height] / dimensions[1].to_f
66
+ x * dimensions[1] > options[:height] ? y : x
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_exiftool'
4
+
5
+ module FilePipeline
6
+ module FileOperations
7
+ # Mixin with methods to facilitate work with _Exif_ metadata.
8
+ module ExifManipulable
9
+ # Returns an Array of tags to be ignored during comparison. These can
10
+ # be merged with an ExifManipulable including FileOperation's options
11
+ # to skip tags (e.g. the <tt>skip_tags</tt> option in ExifRestoration).
12
+ def self.file_tags
13
+ %w[FileSize FileModifyDate FileAccessDate FileInodeChangeDate
14
+ FilePermissions FileType FileTypeExtension MIMEType]
15
+ end
16
+
17
+ def self.parse_tag_error(message) # :nodoc:
18
+ /Warning: Sorry, (?<tag>\w+) is not writable/
19
+ .match(message) { |match| match[:tag] }
20
+ end
21
+
22
+ def self.strip_path(str) # :nodoc:
23
+ str.sub(%r{ - \/?(\/|[-:.]+|\w+)+\.\w+$}, '')
24
+ end
25
+
26
+ # Redacts (deletes) all <tt>tags_to_delete</tt> in <tt>out_file</tt>.
27
+ def delete_tags(out_file, tags_to_delete)
28
+ exif, = read_exif out_file
29
+ values = exif.select { |tag| tags_to_delete.include? tag }
30
+ values_to_delete = values.transform_values { nil }
31
+ log, = write_exif out_file, values_to_delete
32
+ [log, values]
33
+ end
34
+
35
+ # Compares to hashes with exif tags and values and returns a hash with
36
+ # the tags that are present in <tt>other_exif</tt> but absent in
37
+ # <tt>this_exif</tt>.
38
+ def missing_exif_fields(this_exif, other_exif)
39
+ other_exif.delete_if do |tag, _|
40
+ this_exif.key?(tag) || options[:skip_tags].include?(tag)
41
+ end
42
+ end
43
+
44
+ # :args: error_messages, exif
45
+ #
46
+ # Takes an array of <tt>error_messages</tt> and a hash (+exif+) with tags
47
+ # and their values and parses errors where tags could not be written.
48
+ #
49
+ # Returns an array with a log (any messages that were not errors where a
50
+ # tag could not be written) and data (a hash with any tags that could not
51
+ # be written, and the associated values from +exif+)
52
+ def parse_exif_errors(errs, values)
53
+ errs.each_with_object(LogDataParser.template) do |message, info|
54
+ errors, data = info
55
+ tag = ExifManipulable.parse_tag_error(message)
56
+ if tag
57
+ data.store tag, values[tag]
58
+ next info
59
+ end
60
+ errors << ExifManipulable.strip_path(message)
61
+ info
62
+ end
63
+ end
64
+
65
+ # Reads exif information for one or more +files+. Returns an array of
66
+ # hashes, one for each file, with tags and their values.
67
+ def read_exif(*files)
68
+ file_paths = files.map { |f| File.expand_path(f) }
69
+ results, errors = MultiExiftool.read file_paths
70
+ raise 'Error reading Exif' unless errors.empty?
71
+
72
+ results.map(&:to_h)
73
+ end
74
+
75
+ # Writes +values+ (a hash with exif tags as keys) to +out_file+.
76
+ #
77
+ # Returns an array with a log (an array of messages - strings) and a
78
+ # hash with all tags/values that could not be written.
79
+ def write_exif(out_file, values)
80
+ writer = MultiExiftool::Writer.new
81
+ writer.filenames = Dir[File.expand_path(out_file)]
82
+ writer.overwrite_original = true
83
+ writer.values = values
84
+ return if writer.write
85
+
86
+ parse_exif_errors(writer.errors, values)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FilePipeline
4
+ module FileOperations
5
+ # This is an abstract class to be subclassed when file operations are
6
+ # implemented.
7
+ #
8
+ # For the autoloading mechanism in FilePipeline.load to work, it is required
9
+ # that subclasses are in defined in the module FilePipeline::FileOperations.
10
+ #
11
+ # The constructor (_#initialze_) must accept a doublesplat argument as the
12
+ # only argument and should call +super+.
13
+ #
14
+ # Subclasses must implement an #operation method or override the #run
15
+ # method.
16
+ #
17
+ # If the operation results in file type different than that of the file
18
+ # that is passed to #run or #operation as <tt>src_file</tt>, the subclass
19
+ # must have a #target_extension method that returns the appropriate
20
+ # extension.
21
+ class FileOperation
22
+ # A Hash; any options used when performing #operation.
23
+ attr_reader :options
24
+
25
+ # Returns a new instance and sets #options.
26
+ #
27
+ # This can be called from subclasses.
28
+ #
29
+ # ===== Arguments
30
+ #
31
+ # * +defaults+ - Default options for the subclass (hash).
32
+ # * +opts+ - Options passed to the sublass initializer (hash).
33
+ def initialize(opts, defaults = {})
34
+ @options = defaults.update(opts)
35
+ end
36
+
37
+ # Returns the NO_DATA tag.
38
+ #
39
+ # If the results returned by a subclass contain data, override this methos
40
+ # to return the appropriate tag for the data. This tag can be used to
41
+ # filter data captured by operations.
42
+ #
43
+ # Tags are defined in CapturedDataTags.
44
+ def captured_data_tag
45
+ CapturedDataTags::NO_DATA
46
+ end
47
+
48
+ # Returns the extension for +file+ (a string). This should be the
49
+ # extension for the type the file created by #operation will have.
50
+ #
51
+ # If the #operation of a subclass will result in a different extension of
52
+ # predictable type, define a #target_extension method.
53
+ def extension(file)
54
+ target_extension || File.extname(file)
55
+ end
56
+
57
+ # Returns a Results object with the Results#success set to +false+ and
58
+ # any information returned by the operation in <tt>log_data</tt> (a string
59
+ # error, array, or hash).
60
+ def failure(log_data = nil)
61
+ results false, log_data
62
+ end
63
+
64
+ # Returns the class name (string) of +self+ _without_ the names of the
65
+ # modules that the class is nested in.
66
+ def name
67
+ self.class.name.split('::').last
68
+ end
69
+
70
+ # :args: src_file, out_file, original = nil
71
+ #
72
+ # To be implemented in subclasses. Should return any logged errors or data
73
+ # produced (a string, error, array, or hash) or +nil+.
74
+ #
75
+ # ===== Arguments
76
+ #
77
+ # * <tt>src_file</tt> - Path for the file the operation will use as the
78
+ # basis for the new version it will create.
79
+ # * <tt>out_file</tt> - Path the file created by the operation will be
80
+ # written to.
81
+ # * +original+ - Path to the original, unmodified, file (optional).
82
+ def operation(*_)
83
+ raise 'not implemented'
84
+ end
85
+
86
+ # Returns a new Results object with the #descrip1tion of +self+,
87
+ # +success+, and any information returned by the operation as
88
+ # <tt>log_data</tt> (a string, error, array, or hash.)
89
+ #
90
+ # ===== Examples
91
+ #
92
+ # error = StandardError.new
93
+ # warning = 'a warning occurred'
94
+ # log = [error, warning]
95
+ # data = { mime_type: 'image/jpeg' }
96
+ #
97
+ # my_op = MyOperation.new
98
+ #
99
+ # my_op.results(false, error)
100
+ # # => <Results @data=nil, @log=[error], ..., @success=false>
101
+ #
102
+ # my_op.results(true, warning)
103
+ # # => <Results @data=nil, @log=[warning], ..., @success=true>
104
+ #
105
+ # my_op.results(true, data)
106
+ # # => <Results @data=data, @log=[], ..., @success=true>
107
+ #
108
+ # my_op.results(true, [warning, data])
109
+ # # => <Results @data=data, @log=[warning], ..., @success=true>
110
+ #
111
+ # my_op.results(false, log)
112
+ # # => <Results @data=nil, @log=[error, warning], ..., @success=false>
113
+ #
114
+ # my_op.results(false, [log, data])
115
+ # # => <Results @data=data, @log=[error, warning], ..., @success=false>
116
+ #
117
+ def results(success, log_data = nil)
118
+ Results.new(self, success, log_data)
119
+ end
120
+
121
+ # Runs the operation on <tt>src_file</tt> and retunes an array with a
122
+ # path for the file created by the operation and a Results object.
123
+ #
124
+ # Subclasses of FileOperation must either implement an #operation method,
125
+ # or override the #run method, making sure it has the same signature and
126
+ # kind of return value.
127
+ #
128
+ # The method will create a new path for the file produced by #operation to
129
+ # be written to. This path will consist of +directory+ and a new basename.
130
+ #
131
+ # The optional +original+ argument can be used to reference another file,
132
+ # e.g. when exif metadata tags missing in the <tt>src_file</tt> are to
133
+ # be copied over from another file.
134
+ def run(src_file, directory, original = nil)
135
+ out_file = target directory, extension(src_file)
136
+ log_data = operation src_file, out_file, original
137
+ [out_file, success(log_data)]
138
+ rescue StandardError => e
139
+ FileUtils.rm out_file if File.exist? out_file
140
+ [out_file, failure(e)]
141
+ end
142
+
143
+ # Returns a Results object with the Results#success set to +true+ and
144
+ # any information returned by the operation in <tt>log_data</tt> (a string
145
+ # error, array, or hash).
146
+ def success(log_data = nil)
147
+ results true, log_data
148
+ end
149
+
150
+ # Returns a new path to which the file created by the operation can be
151
+ # written. The path will be in +directory+, with a new basename determined
152
+ # by +kind+ and have the specified file +extension+.
153
+ #
154
+ # There are two options for the +kind+ of basename to be created:
155
+ # * +:timestamp+ (_default_) - Creates a timestamp basename.
156
+ # * +:random+ - Creates a UUID basename.
157
+ #
158
+ # The timestamp format is <tt>YYYY-MM-DDTHH:MM:SS.NNNNNNNNN</TT>.
159
+ #
160
+ # ===== Examples
161
+ #
162
+ # file_operation.target('path/to/dir', '.png', :timestamp)
163
+ # # => 'path/to/dir/2019-07-24T09:30:12:638935761.png'
164
+ #
165
+ # file_operation.target('path/to/dir', '.png', :random)
166
+ # # => 'path/to/dir/123e4567-e89b-12d3-a456-426655440000.png'
167
+ def target(directory, extension, kind = :timestamp)
168
+ filename = FilePipeline.new_basename(kind) + extension
169
+ File.join directory, filename
170
+ end
171
+
172
+ # Returns +nil+.
173
+ #
174
+ # If the #operation of a subclass will result in a different extension of
175
+ # predictable type, override this method to return the appropriate type.
176
+ #
177
+ # If, for instance, the operation will always create a <em>TIFF</em> file,
178
+ # the implementation could be:
179
+ #
180
+ # # Returns '.tiff'
181
+ # def target_extension
182
+ # '.tiff'
183
+ # end
184
+ #
185
+ def target_extension; end
186
+ end
187
+ end
188
+ end