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 +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
|