activestorage-hotcell-server 0.3.1 → 0.4.0
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 +4 -4
- data/lib/active_storage/hot_cell/server/analyzers/image/magick.rb +4 -4
- data/lib/active_storage/hot_cell/server/magick_operation.rb +41 -28
- data/lib/active_storage/hot_cell/server/operation.rb +9 -0
- data/lib/active_storage/hot_cell/server/transformers/image/magick.rb +10 -2
- data/lib/active_storage/hot_cell/server/transforming.rb +24 -7
- data/lib/active_storage/hot_cell/server/version.rb +1 -1
- metadata +9 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8f31eda3f64a83828d024483bd31e733927fdf2d7ea0b02796bbef8ead35509
|
|
4
|
+
data.tar.gz: 0e9975a3262d57ea6caecc8351903297ef6043412ac2bae7ad45a4f5783efaeb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '078512cbc8c4bc0559effd79d63f3e5b30b24c308fe5183bc85e054dea4a5829d34b28ebdc43173503893c94babd16e8de91b9217b76229150fd01c633cad575'
|
|
7
|
+
data.tar.gz: afb813f18f6e85c61389af36c9dc731ca3ff64ac18c24523a0015f8f1f25a5616ebfd307f235e15fbe5038c5fc6d7ebe7776a9d8a44c713630e6ff9b22beff0f
|
|
@@ -25,7 +25,7 @@ module ActiveStorage
|
|
|
25
25
|
|
|
26
26
|
def perform(inputs, _outputs)
|
|
27
27
|
source, = inputs
|
|
28
|
-
frames = identified(source
|
|
28
|
+
frames = identified(source)
|
|
29
29
|
width, height, orientation = frames.first
|
|
30
30
|
|
|
31
31
|
{ **dimensions(width, height, orientation), pages: frames.size, animated: frames.size > 1,
|
|
@@ -36,10 +36,10 @@ module ActiveStorage
|
|
|
36
36
|
# One `identify` run reports every frame's dimensions and orientation, where MiniMagick::Image's
|
|
37
37
|
# accessors (`valid?`, `width`, `pages`) each spawn an identify of their own — four execs per
|
|
38
38
|
# analysis, two of them decoding every frame.
|
|
39
|
-
def identified(
|
|
40
|
-
frames = MiniMagick.identify do |identify|
|
|
39
|
+
def identified(source)
|
|
40
|
+
frames = MiniMagick.identify(inherit_fds: [ source.to_io ]) do |identify|
|
|
41
41
|
identify.format "%w %h %[orientation]\n"
|
|
42
|
-
identify <<
|
|
42
|
+
identify << source.fd_path
|
|
43
43
|
end.lines.map(&:split)
|
|
44
44
|
|
|
45
45
|
raise MiniMagick::Invalid, "ImageMagick does not recognise this as an image" if frames.empty?
|
|
@@ -8,29 +8,6 @@ require "active_storage/hot_cell/server/operation"
|
|
|
8
8
|
# toolchains' loads in the same place is what makes a single cell carrying both legible.
|
|
9
9
|
require "image_processing/mini_magick"
|
|
10
10
|
|
|
11
|
-
# Invariant 9: a tool sees only the environment its operation wrote for it. `Operation#run_tool` holds it
|
|
12
|
-
# with `unsetenv_others: true`, and these operations do not use it — mini_magick spawns `magick` itself.
|
|
13
|
-
# `MiniMagick.restricted_env` is `false` by default, and with it false mini_magick calls
|
|
14
|
-
# `Open3.popen3({}, *command, unsetenv_others: false)`, so `magick` inherited this worker's whole
|
|
15
|
-
# environment. `bin/conformance` did not catch it, because its environment check drives an operation that
|
|
16
|
-
# goes through `run_tool`.
|
|
17
|
-
#
|
|
18
|
-
# `cli_env` names the locale for the reason `tool_environment` does: a tool's output must not shift under
|
|
19
|
-
# it. mini_magick passes `HOME`, `PATH` and `LANG` and does not pass `LC_ALL`, so both go here.
|
|
20
|
-
#
|
|
21
|
-
# Set at require time rather than in `before_worker_boot`, so that the binary lookup this require performs
|
|
22
|
-
# is covered too. Note what this is: mini_magick filters the environment it was given, where `run_tool`
|
|
23
|
-
# writes a fresh one. Driving `magick` directly would make it ours, and that is the separate enhancement
|
|
24
|
-
# this class already names.
|
|
25
|
-
#
|
|
26
|
-
# The OpenMP variables come from the cell's environment rather than being named here: the number belongs
|
|
27
|
-
# to the image, which is configured to match the container's CPU quota. Without them the restriction would
|
|
28
|
-
# hand `magick` the pool the image's bound was meant to take away. `run_tool` carries the same pair.
|
|
29
|
-
MiniMagick.restricted_env = true
|
|
30
|
-
MiniMagick.cli_env = { "LANG" => "C.UTF-8", "LC_ALL" => "C.UTF-8",
|
|
31
|
-
"OMP_NUM_THREADS" => ENV["OMP_NUM_THREADS"],
|
|
32
|
-
"OMP_THREAD_LIMIT" => ENV["OMP_THREAD_LIMIT"] }.compact
|
|
33
|
-
|
|
34
11
|
module ActiveStorage
|
|
35
12
|
module HotCell
|
|
36
13
|
module Server
|
|
@@ -41,14 +18,46 @@ module ActiveStorage
|
|
|
41
18
|
# own output — dimensions from `identify`, an exit status — which is a number and a string, not a
|
|
42
19
|
# decoder. The distinction is the same one probe_media draws for ffprobe.
|
|
43
20
|
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# being staged — which the vips operations shed. Removing it means driving `magick` directly rather
|
|
48
|
-
# than through mini_magick, and is a separate enhancement.
|
|
21
|
+
# Both operations hand mini_magick the input descriptor with `inherit_fds:` and name it by its
|
|
22
|
+
# Descriptor#fd_path, so neither stages its input and neither carries the file_size ceiling the vips
|
|
23
|
+
# operations never had.
|
|
49
24
|
class MagickOperation < Operation
|
|
50
25
|
abstract_operation
|
|
51
26
|
|
|
27
|
+
# Invariant 9: a tool sees only the environment its operation wrote for it. `Operation#run_tool` holds
|
|
28
|
+
# it with `unsetenv_others: true`, and these operations do not use it — mini_magick spawns `magick`
|
|
29
|
+
# itself. `MiniMagick.restricted_env` is `false` by default, and with it false mini_magick calls
|
|
30
|
+
# `Open3.popen3({}, *command, unsetenv_others: false)`, so `magick` inherited this worker's whole
|
|
31
|
+
# environment. `bin/conformance` did not catch it, because its environment check drives an operation
|
|
32
|
+
# that goes through `run_tool`. Note what this is: mini_magick filters the environment it was given,
|
|
33
|
+
# where `run_tool` writes a fresh one. Driving `magick` directly would make it ours, and that is the
|
|
34
|
+
# separate enhancement this class already names.
|
|
35
|
+
#
|
|
36
|
+
# The locale is named for the reason `tool_environment` names it: a tool's output must not shift under
|
|
37
|
+
# it. mini_magick passes `HOME`, `PATH` and `LANG` and does not pass `LC_ALL`, so both go here.
|
|
38
|
+
#
|
|
39
|
+
# Everything else comes from the worker's environment rather than being named here. The OpenMP pair
|
|
40
|
+
# belongs to the image, which is configured to match the container's CPU quota. ImageMagick's own
|
|
41
|
+
# resource ceilings, `MAGICK_DISK_LIMIT` and its siblings, are read from its environment as it loads
|
|
42
|
+
# and the image sets them to a worker's share of the scratch; without them `magick` runs under
|
|
43
|
+
# ImageMagick's defaults, the host's RAM and an unbounded disk. `TMPDIR` and `MAGICK_TMPDIR` are where
|
|
44
|
+
# it spills that cache: the worker sets the first to the request's home after the fork and
|
|
45
|
+
# `Operation#initialize` maps the second from it — which is why this is read per request, after
|
|
46
|
+
# `super`, and not once at require: a hash built then hands `magick` the supervisor's `/tmp`, which is
|
|
47
|
+
# swept only at the next boot.
|
|
48
|
+
def self.cli_env
|
|
49
|
+
{ "LANG" => "C.UTF-8", "LC_ALL" => "C.UTF-8",
|
|
50
|
+
"OMP_NUM_THREADS" => ENV["OMP_NUM_THREADS"], "OMP_THREAD_LIMIT" => ENV["OMP_THREAD_LIMIT"],
|
|
51
|
+
"TMPDIR" => ENV["TMPDIR"], "MAGICK_TMPDIR" => ENV["MAGICK_TMPDIR"] }
|
|
52
|
+
.merge(ENV.select { |name, _| name.match?(/\AMAGICK_\w+_LIMIT\z/) })
|
|
53
|
+
.compact
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize
|
|
57
|
+
super
|
|
58
|
+
MiniMagick.cli_env = self.class.cli_env
|
|
59
|
+
end
|
|
60
|
+
|
|
52
61
|
# MiniMagick::Error is how mini_magick reports a `magick` that exited non-zero — the common shape of an
|
|
53
62
|
# input it cannot decode. MiniMagick::Invalid is an input `identify` rejects outright.
|
|
54
63
|
# ImageProcessing::Error is the pipeline's own verdict on the input against the requested
|
|
@@ -71,3 +80,7 @@ module ActiveStorage
|
|
|
71
80
|
end
|
|
72
81
|
end
|
|
73
82
|
end
|
|
83
|
+
|
|
84
|
+
# Also at require time, so that the binary lookup the require above performs is covered too.
|
|
85
|
+
MiniMagick.restricted_env = true
|
|
86
|
+
MiniMagick.cli_env = ActiveStorage::HotCell::Server::MagickOperation.cli_env
|
|
@@ -30,6 +30,15 @@ module ActiveStorage
|
|
|
30
30
|
"tiff" => "image/tiff",
|
|
31
31
|
}.freeze
|
|
32
32
|
|
|
33
|
+
# The cell describes the request's scratch as `TMPDIR`, and knows nothing about the libraries that run
|
|
34
|
+
# here. ImageMagick reads `MAGICK_TMPDIR` first, so the mapping is this gem's — for the `magick` the
|
|
35
|
+
# magick operations exec and for the `magickload` libvips delegates to in-process. Per request rather
|
|
36
|
+
# than at load: `TMPDIR` is the request's home, which is fresh every time.
|
|
37
|
+
def initialize
|
|
38
|
+
super
|
|
39
|
+
ENV["MAGICK_TMPDIR"] = ENV["TMPDIR"] if ENV.key?("TMPDIR")
|
|
40
|
+
end
|
|
41
|
+
|
|
33
42
|
private
|
|
34
43
|
# A caller breaking the protocol is a caller bug, not a bad document. Raising MessageError is what
|
|
35
44
|
# makes the cell answer `invalid`, which is permanent and which the client raises rather than
|
|
@@ -29,9 +29,17 @@ module ActiveStorage
|
|
|
29
29
|
ImageProcessing::MiniMagick
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
# The staged path rather than /dev/fd; MagickOperation's comment says why.
|
|
33
32
|
def source_path(source)
|
|
34
|
-
source.
|
|
33
|
+
source.fd_path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# `source_path` names the input as the source's `/dev/fd` path, which `magick` can open only if it
|
|
37
|
+
# inherits the descriptor, so name the IO for mini_magick to put in its spawn map. Through the
|
|
38
|
+
# loader options rather than by pre-building a MiniMagick::Tool: a pre-built tool reaches
|
|
39
|
+
# ImageProcessing's `load_image` by its first branch, which drops `page`, `loader` and `geometry`
|
|
40
|
+
# without a word.
|
|
41
|
+
def source_loader_options(source)
|
|
42
|
+
{ inherit_fds: [ source.to_io ] }
|
|
35
43
|
end
|
|
36
44
|
|
|
37
45
|
def describe(path, format)
|
|
@@ -21,23 +21,40 @@ module ActiveStorage
|
|
|
21
21
|
# it to the library, which cannot reproduce those without restating them. Output#post makes the
|
|
22
22
|
# one remaining copy, out through the caller's descriptor.
|
|
23
23
|
encoded = destination.path(extension: format)
|
|
24
|
-
pipeline(
|
|
24
|
+
pipeline(source, format, operations).call(destination: encoded)
|
|
25
25
|
destination.adopt encoded
|
|
26
26
|
|
|
27
27
|
describe destination.path, format
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
private
|
|
31
|
-
# `loader
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# `
|
|
35
|
-
|
|
31
|
+
# Three `loader` calls, and the order is the point: ImageProcessing merges them key by key, so the
|
|
32
|
+
# last one to write a key wins.
|
|
33
|
+
#
|
|
34
|
+
# `page: 0` is first, as a default the caller may replace. It is what Rails does, and what stops a
|
|
35
|
+
# multi-page TIFF or a hundred-frame GIF being decoded in full to produce one thumbnail; a caller
|
|
36
|
+
# who wants every frame sends `loader: { n: -1 }` through `apply` and gets it.
|
|
37
|
+
#
|
|
38
|
+
# `source_loader_options` is last, so the caller cannot replace it.
|
|
39
|
+
def pipeline(source, format, operations)
|
|
36
40
|
processor
|
|
37
|
-
.source(
|
|
41
|
+
.source(source_path(source))
|
|
38
42
|
.loader(page: 0)
|
|
39
43
|
.convert(format)
|
|
40
44
|
.apply(operations_for(operations))
|
|
45
|
+
.loader(**source_loader_options(source))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# What the toolchain needs in order to reach the source at all, which is not the same question as how
|
|
49
|
+
# the image is decoded. Empty here, because `source_path` normally answers a filename and a filename
|
|
50
|
+
# needs no help. An includer whose `source_path` names something the tool cannot open on its own
|
|
51
|
+
# overrides this to supply the rest — `Transformers::Image::Magick` is the one that does.
|
|
52
|
+
#
|
|
53
|
+
# It goes last in `pipeline` because it is not the caller's to change: a payload sending
|
|
54
|
+
# `loader: { inherit_fds: [] }` would otherwise leave the source naming a descriptor the tool has no
|
|
55
|
+
# way to open.
|
|
56
|
+
def source_loader_options(_source)
|
|
57
|
+
{}
|
|
41
58
|
end
|
|
42
59
|
|
|
43
60
|
# What Rails validates, and no more. `combine_options` is refused because it can never be one
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activestorage-hotcell-server
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Dalessio
|
|
@@ -15,42 +15,42 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.4.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.4.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: image_processing
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version:
|
|
32
|
+
version: 2.1.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version:
|
|
39
|
+
version: 2.1.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: mini_magick
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 5.
|
|
46
|
+
version: 5.4.0
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 5.
|
|
53
|
+
version: 5.4.0
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: ruby-vips
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -99,8 +99,8 @@ licenses:
|
|
|
99
99
|
- MIT
|
|
100
100
|
metadata:
|
|
101
101
|
homepage_uri: https://github.com/basecamp/hotcell
|
|
102
|
-
source_code_uri: https://github.com/basecamp/hotcell/tree/v0.
|
|
103
|
-
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.
|
|
102
|
+
source_code_uri: https://github.com/basecamp/hotcell/tree/v0.4.0/activestorage-hotcell-server
|
|
103
|
+
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.4.0/CHANGELOG.md
|
|
104
104
|
bug_tracker_uri: https://github.com/basecamp/hotcell/issues
|
|
105
105
|
rubygems_mfa_required: 'true'
|
|
106
106
|
rdoc_options: []
|