file_pipeline 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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