file_pipeline 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/file_pipeline/errors/failed_modification_error.rb +23 -0
- data/lib/file_pipeline/errors/missing_version_file_error.rb +18 -0
- data/lib/file_pipeline/errors/source_directory_error.rb +18 -0
- data/lib/file_pipeline/errors/source_file_error.rb +26 -0
- data/lib/file_pipeline/errors.rb +12 -0
- data/lib/file_pipeline/file_operations/captured_data_tags.rb +16 -0
- data/lib/file_pipeline/file_operations/default_operations/exif_redaction.rb +52 -0
- data/lib/file_pipeline/file_operations/default_operations/exif_restoration.rb +56 -0
- data/lib/file_pipeline/file_operations/default_operations/ptiff_conversion.rb +48 -0
- data/lib/file_pipeline/file_operations/default_operations/scale.rb +70 -0
- data/lib/file_pipeline/file_operations/exif_manipulable.rb +90 -0
- data/lib/file_pipeline/file_operations/file_operation.rb +188 -0
- data/lib/file_pipeline/file_operations/log_data_parser.rb +140 -0
- data/lib/file_pipeline/file_operations/results.rb +109 -0
- data/lib/file_pipeline/file_operations.rb +17 -0
- data/lib/file_pipeline/pipeline.rb +104 -0
- data/lib/file_pipeline/versioned_file.rb +284 -0
- data/lib/file_pipeline.rb +92 -0
- metadata +303 -0
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
|