activestorage-hotcell-client 0.0.0 → 0.2.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/MIT-LICENSE +20 -0
- data/README.md +3 -0
- data/lib/active_storage/hot_cell/client/analyzers/analyzing.rb +53 -0
- data/lib/active_storage/hot_cell/client/analyzers/audio/ffprobe.rb +28 -0
- data/lib/active_storage/hot_cell/client/analyzers/audio.rb +28 -0
- data/lib/active_storage/hot_cell/client/analyzers/image/magick.rb +24 -0
- data/lib/active_storage/hot_cell/client/analyzers/image/vips.rb +23 -0
- data/lib/active_storage/hot_cell/client/analyzers/image.rb +34 -0
- data/lib/active_storage/hot_cell/client/analyzers/probing.rb +21 -0
- data/lib/active_storage/hot_cell/client/analyzers/video/ffprobe.rb +29 -0
- data/lib/active_storage/hot_cell/client/analyzers/video.rb +31 -0
- data/lib/active_storage/hot_cell/client/operations.rb +89 -0
- data/lib/active_storage/hot_cell/client/previewers/pdf/mutool.rb +29 -0
- data/lib/active_storage/hot_cell/client/previewers/pdf/poppler.rb +28 -0
- data/lib/active_storage/hot_cell/client/previewers/pdf.rb +16 -0
- data/lib/active_storage/hot_cell/client/previewers/previewing.rb +77 -0
- data/lib/active_storage/hot_cell/client/previewers/video/ffmpeg.rb +38 -0
- data/lib/active_storage/hot_cell/client/railtie.rb +37 -0
- data/lib/active_storage/hot_cell/client/tool_arguments.rb +36 -0
- data/lib/active_storage/hot_cell/client/transformers/image/magick.rb +41 -0
- data/lib/active_storage/hot_cell/client/transformers/image/vips.rb +35 -0
- data/lib/active_storage/hot_cell/client/transformers/transforming.rb +66 -0
- data/lib/active_storage/hot_cell/client/version.rb +9 -0
- data/lib/active_storage/hot_cell/client.rb +72 -0
- data/lib/activestorage-hotcell-client.rb +5 -0
- metadata +69 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 668902d1347b4f118a31b299534d8b8bedfadd568cf3e4e6e872384ee6d6f83f
|
|
4
|
+
data.tar.gz: d55b6e9a965f5327fd2d41c2f94bd7a59e6b2af8083ea2a892a9e24b9db423cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bad8f45d4e713516eb6e0c27a2a0b7ae764f48bfbffe4dd3cf81d73091276f274798a8a72eda5d45212443a120c649645c2f2861de5dad088e903aa77f6f82ae
|
|
7
|
+
data.tar.gz: 359071c3cae9ed347112afe70cd1a45d9dbbda0e1adca966d03fbcb887e16a56bebfc934997c62325b846a2560315c3f617e528cdc779303cf78aab25c0ac187
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 37signals, LLC
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage"
|
|
4
|
+
require "active_storage/analyzer"
|
|
5
|
+
require "active_support/core_ext/class/attribute"
|
|
6
|
+
|
|
7
|
+
module ActiveStorage
|
|
8
|
+
module HotCell
|
|
9
|
+
module Client
|
|
10
|
+
module Analyzers
|
|
11
|
+
# What every analyzer here shares: one round trip to the cell, and the permanent-versus-transient
|
|
12
|
+
# failure split. An analyzer names the client it asks and the KEYS it slices the cell's superset
|
|
13
|
+
# down to — the cell knows more than Rails writes, and an extra key would change the shape of every
|
|
14
|
+
# blob's metadata.
|
|
15
|
+
#
|
|
16
|
+
# **Which way `metadata` fails is the whole design of the method.** The built-in vips analyzer rescues
|
|
17
|
+
# every Vips::Error and returns an empty hash, which Rails then merges with `analyzed: true` — so an
|
|
18
|
+
# undecodable image is recorded as successfully analyzed, forever, and nothing ever re-enqueues
|
|
19
|
+
# AnalyzeJob. That is right for a permanent verdict and catastrophic for a transient one.
|
|
20
|
+
#
|
|
21
|
+
# So a permanent failure follows the built-in behaviour and lets the blob be marked analyzed, with the
|
|
22
|
+
# reason written to the log so it can be re-decided later against a newer library. A transient failure
|
|
23
|
+
# is not rescued at all, which is what leaves the blob `analyzed: false` and eligible to be tried
|
|
24
|
+
# again.
|
|
25
|
+
module Analyzing
|
|
26
|
+
def self.included(analyzer)
|
|
27
|
+
analyzer.class_attribute :client, instance_accessor: false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def metadata
|
|
31
|
+
measured.slice(*self.class::KEYS)
|
|
32
|
+
rescue self.class.client.cell.permanent => error
|
|
33
|
+
logger.warn "hotcell: #{blob.filename} could not be analyzed and will not be retried: #{error.message}"
|
|
34
|
+
{}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
def measured
|
|
39
|
+
download_blob_to_tempfile do |file|
|
|
40
|
+
File.open(file.path, "rb") { |readable| self.class.client.perform_in_hotcell [ readable ], [], payload }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# What travels with the request beyond the descriptor. Nothing, for an analyzer that has no
|
|
45
|
+
# configuration to carry; the media analyzers override it with the application's ffprobe arguments.
|
|
46
|
+
def payload
|
|
47
|
+
{}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage/hot_cell/client/operations"
|
|
4
|
+
require "active_storage/hot_cell/client/analyzers/audio"
|
|
5
|
+
require "active_storage/hot_cell/client/analyzers/probing"
|
|
6
|
+
|
|
7
|
+
module ActiveStorage
|
|
8
|
+
module HotCell
|
|
9
|
+
module Client
|
|
10
|
+
module Analyzers
|
|
11
|
+
class Audio
|
|
12
|
+
# What an application lists in `config.active_storage.analyzers`.
|
|
13
|
+
#
|
|
14
|
+
# config.active_storage.analyzers = [ ActiveStorage::HotCell::Client::Analyzers::Audio::Ffprobe, ... ]
|
|
15
|
+
#
|
|
16
|
+
# Shares the probe operation with the video analyzer; see Analyzers::Video::Ffprobe.
|
|
17
|
+
class Ffprobe < Audio
|
|
18
|
+
include Probing
|
|
19
|
+
|
|
20
|
+
self.client = Operations::Analyzers::Media::Ffprobe
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
FFprobe = Ffprobe
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage"
|
|
4
|
+
require "active_storage/analyzer"
|
|
5
|
+
|
|
6
|
+
require "active_storage/hot_cell/client/analyzers/analyzing"
|
|
7
|
+
|
|
8
|
+
module ActiveStorage
|
|
9
|
+
module HotCell
|
|
10
|
+
module Client
|
|
11
|
+
module Analyzers
|
|
12
|
+
# Rails' AudioAnalyzer, moved to the cell the way the video analyzer is.
|
|
13
|
+
#
|
|
14
|
+
# Rails' audio keys, minus `tags`: Rails writes raw container metadata into the database, and the
|
|
15
|
+
# cell refuses it because those bytes are attacker-controlled.
|
|
16
|
+
class Audio < ActiveStorage::Analyzer
|
|
17
|
+
include Analyzing
|
|
18
|
+
|
|
19
|
+
KEYS = %i[ duration bit_rate sample_rate ].freeze
|
|
20
|
+
|
|
21
|
+
def self.accept?(blob)
|
|
22
|
+
blob.audio?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage/hot_cell/client/operations"
|
|
4
|
+
require "active_storage/hot_cell/client/analyzers/image"
|
|
5
|
+
|
|
6
|
+
module ActiveStorage
|
|
7
|
+
module HotCell
|
|
8
|
+
module Client
|
|
9
|
+
module Analyzers
|
|
10
|
+
class Image
|
|
11
|
+
# What an application on ImageMagick lists in `config.active_storage.analyzers`.
|
|
12
|
+
#
|
|
13
|
+
# config.active_storage.analyzers = [ ActiveStorage::HotCell::Client::Analyzers::Image::Magick, ... ]
|
|
14
|
+
#
|
|
15
|
+
# It shares the base's width-and-height slice and its permanent-versus-transient split, differing
|
|
16
|
+
# only in the client it reaches.
|
|
17
|
+
class Magick < Image
|
|
18
|
+
self.client = Operations::Analyzers::Image::Magick
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage/hot_cell/client/operations"
|
|
4
|
+
require "active_storage/hot_cell/client/analyzers/image"
|
|
5
|
+
|
|
6
|
+
module ActiveStorage
|
|
7
|
+
module HotCell
|
|
8
|
+
module Client
|
|
9
|
+
module Analyzers
|
|
10
|
+
class Image
|
|
11
|
+
# What an application lists in `config.active_storage.analyzers`.
|
|
12
|
+
#
|
|
13
|
+
# config.active_storage.analyzers = [ ActiveStorage::HotCell::Client::Analyzers::Image::Vips, ... ]
|
|
14
|
+
#
|
|
15
|
+
# The library itself is in the cell, so what this names is which operation to ask.
|
|
16
|
+
class Vips < Image
|
|
17
|
+
self.client = Operations::Analyzers::Image::Vips
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage"
|
|
4
|
+
require "active_storage/analyzer"
|
|
5
|
+
|
|
6
|
+
require "active_storage/hot_cell/client/analyzers/analyzing"
|
|
7
|
+
|
|
8
|
+
module ActiveStorage
|
|
9
|
+
module HotCell
|
|
10
|
+
module Client
|
|
11
|
+
module Analyzers
|
|
12
|
+
# Shipping an analyzer is mandatory rather than a nicety. The built-in image analyzers gate `accept?` on
|
|
13
|
+
# `variant_processor` being `:vips` or `:mini_magick`, so a class value makes all of them decline,
|
|
14
|
+
# `analyzer_class` falls through to NullAnalyzer, and the blob is marked analyzed with no dimensions at
|
|
15
|
+
# all. rails/rails#58384 leaves that to us deliberately.
|
|
16
|
+
#
|
|
17
|
+
# This holds the Rails-facing contract — which blobs are accepted, which keys are written. A leaf names
|
|
18
|
+
# the client that reaches its toolchain and differs in nothing else.
|
|
19
|
+
class Image < ActiveStorage::Analyzer
|
|
20
|
+
include Analyzing
|
|
21
|
+
|
|
22
|
+
# Width and height, which is exactly what the built-in analyzer returns. The cell knows more — page
|
|
23
|
+
# count, whether the image is animated — and surfacing it would change the shape of every blob's
|
|
24
|
+
# metadata.
|
|
25
|
+
KEYS = %i[ width height ].freeze
|
|
26
|
+
|
|
27
|
+
def self.accept?(blob)
|
|
28
|
+
blob.image?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage/hot_cell/client/tool_arguments"
|
|
4
|
+
|
|
5
|
+
module ActiveStorage
|
|
6
|
+
module HotCell
|
|
7
|
+
module Client
|
|
8
|
+
module Analyzers
|
|
9
|
+
# What the two ffprobe analyzers add to the shared round trip: `config.active_storage.ffprobe_arguments`,
|
|
10
|
+
# which Rails' own video and audio analyzers splice before the input path, carried to the cell so it
|
|
11
|
+
# can do the same. Split here, the way Rails splits it, and omitted entirely when it is empty.
|
|
12
|
+
module Probing
|
|
13
|
+
private
|
|
14
|
+
def payload
|
|
15
|
+
ToolArguments.payload(:probe_arguments, ActiveStorage.ffprobe_arguments)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage/hot_cell/client/operations"
|
|
4
|
+
require "active_storage/hot_cell/client/analyzers/video"
|
|
5
|
+
require "active_storage/hot_cell/client/analyzers/probing"
|
|
6
|
+
|
|
7
|
+
module ActiveStorage
|
|
8
|
+
module HotCell
|
|
9
|
+
module Client
|
|
10
|
+
module Analyzers
|
|
11
|
+
class Video
|
|
12
|
+
# What an application lists in `config.active_storage.analyzers`.
|
|
13
|
+
#
|
|
14
|
+
# config.active_storage.analyzers = [ ActiveStorage::HotCell::Client::Analyzers::Video::Ffprobe, ... ]
|
|
15
|
+
#
|
|
16
|
+
# One probe operation serves this and the audio analyzer: ffprobe reports both stream kinds in one
|
|
17
|
+
# pass, and each analyzer slices the shared result to its own keys.
|
|
18
|
+
class Ffprobe < Video
|
|
19
|
+
include Probing
|
|
20
|
+
|
|
21
|
+
self.client = Operations::Analyzers::Media::Ffprobe
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
FFprobe = Ffprobe
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage"
|
|
4
|
+
require "active_storage/analyzer"
|
|
5
|
+
|
|
6
|
+
require "active_storage/hot_cell/client/analyzers/analyzing"
|
|
7
|
+
|
|
8
|
+
module ActiveStorage
|
|
9
|
+
module HotCell
|
|
10
|
+
module Client
|
|
11
|
+
module Analyzers
|
|
12
|
+
# Rails' VideoAnalyzer shells out to ffprobe inside the application process. Moving ffprobe into a
|
|
13
|
+
# cell means this has to move with it, or an application that adopts hotcell still analyzes media in
|
|
14
|
+
# its own process and cannot take ffprobe out of its image — the incomplete move for exactly the
|
|
15
|
+
# media type a cell exists to isolate.
|
|
16
|
+
#
|
|
17
|
+
# Rails' exact video keys, sliced from the cell's superset. The cell already applied the rotation
|
|
18
|
+
# swap and the anamorphic correction.
|
|
19
|
+
class Video < ActiveStorage::Analyzer
|
|
20
|
+
include Analyzing
|
|
21
|
+
|
|
22
|
+
KEYS = %i[ width height duration angle display_aspect_ratio audio video ].freeze
|
|
23
|
+
|
|
24
|
+
def self.accept?(blob)
|
|
25
|
+
blob.video?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hot_cell/client"
|
|
4
|
+
|
|
5
|
+
module ActiveStorage
|
|
6
|
+
module HotCell
|
|
7
|
+
module Client
|
|
8
|
+
# The cell these all talk to by default. An application registers it under this name.
|
|
9
|
+
CELL = "active_storage"
|
|
10
|
+
|
|
11
|
+
# One client class per operation, named role, then subject, then tool — so lexical order groups
|
|
12
|
+
# siblings — and the wire name is the snake-cased class path.
|
|
13
|
+
#
|
|
14
|
+
# They are separate classes so that they need not stay together. A cell carries one toolchain by design, so
|
|
15
|
+
# an application that wants video previews in a cell with ffmpeg and variants in a cell with only libvips
|
|
16
|
+
# says so here and changes nothing else:
|
|
17
|
+
#
|
|
18
|
+
# ActiveStorage::HotCell::Client::Operations::Previewers::Video::Ffmpeg.hotcell "video"
|
|
19
|
+
#
|
|
20
|
+
# Routing is a class-level declaration rather than a call-site argument, so call sites carry no deployment
|
|
21
|
+
# detail at all.
|
|
22
|
+
module Operations
|
|
23
|
+
module Transformers
|
|
24
|
+
module Image
|
|
25
|
+
class Vips < ::HotCell::Client
|
|
26
|
+
hotcell CELL
|
|
27
|
+
operation "active_storage.transformers.image.vips"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Magick < ::HotCell::Client
|
|
31
|
+
hotcell CELL
|
|
32
|
+
operation "active_storage.transformers.image.magick"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module Analyzers
|
|
38
|
+
module Image
|
|
39
|
+
class Vips < ::HotCell::Client
|
|
40
|
+
hotcell CELL
|
|
41
|
+
operation "active_storage.analyzers.image.vips"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Magick < ::HotCell::Client
|
|
45
|
+
hotcell CELL
|
|
46
|
+
operation "active_storage.analyzers.image.magick"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# One probe serves both media analyzers: ffprobe reports video and audio streams in one pass, and
|
|
51
|
+
# the video and audio analyzers each slice the shared result to their own keys.
|
|
52
|
+
module Media
|
|
53
|
+
class Ffprobe < ::HotCell::Client
|
|
54
|
+
hotcell CELL
|
|
55
|
+
operation "active_storage.analyzers.media.ffprobe"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
FFprobe = Ffprobe
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
module Previewers
|
|
63
|
+
module Pdf
|
|
64
|
+
class Mutool < ::HotCell::Client
|
|
65
|
+
hotcell CELL
|
|
66
|
+
operation "active_storage.previewers.pdf.mutool"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class Poppler < ::HotCell::Client
|
|
70
|
+
hotcell CELL
|
|
71
|
+
operation "active_storage.previewers.pdf.poppler"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
PDF = Pdf
|
|
76
|
+
|
|
77
|
+
module Video
|
|
78
|
+
class Ffmpeg < ::HotCell::Client
|
|
79
|
+
hotcell CELL
|
|
80
|
+
operation "active_storage.previewers.video.ffmpeg"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
FFmpeg = Ffmpeg
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage/previewer/mupdf_previewer"
|
|
4
|
+
|
|
5
|
+
require "active_storage/hot_cell/client/previewers/previewing"
|
|
6
|
+
require "active_storage/hot_cell/client/previewers/pdf"
|
|
7
|
+
|
|
8
|
+
module ActiveStorage
|
|
9
|
+
module HotCell
|
|
10
|
+
module Client
|
|
11
|
+
module Previewers
|
|
12
|
+
module Pdf
|
|
13
|
+
# What Rails configures in `config.active_storage.previewers`, replacing MuPDFPreviewer.
|
|
14
|
+
class Mutool < ActiveStorage::Previewer::MuPDFPreviewer
|
|
15
|
+
include Previewing
|
|
16
|
+
|
|
17
|
+
self.client = Operations::Previewers::Pdf::Mutool
|
|
18
|
+
|
|
19
|
+
# Delegates to the superclass's content-type predicate rather than restating the list, so the
|
|
20
|
+
# accepted set cannot drift away from the one Rails ships.
|
|
21
|
+
def self.accept?(blob)
|
|
22
|
+
pdf? blob.content_type
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage/previewer/poppler_pdf_previewer"
|
|
4
|
+
|
|
5
|
+
require "active_storage/hot_cell/client/previewers/previewing"
|
|
6
|
+
require "active_storage/hot_cell/client/previewers/pdf"
|
|
7
|
+
|
|
8
|
+
module ActiveStorage
|
|
9
|
+
module HotCell
|
|
10
|
+
module Client
|
|
11
|
+
module Previewers
|
|
12
|
+
module Pdf
|
|
13
|
+
# The Poppler sibling of Mutool, for an application whose image carries pdftoppm rather than
|
|
14
|
+
# mutool — the previewer Rails' default chain reaches first.
|
|
15
|
+
class Poppler < ActiveStorage::Previewer::PopplerPDFPreviewer
|
|
16
|
+
include Previewing
|
|
17
|
+
|
|
18
|
+
self.client = Operations::Previewers::Pdf::Poppler
|
|
19
|
+
|
|
20
|
+
def self.accept?(blob)
|
|
21
|
+
pdf? blob.content_type
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveStorage
|
|
4
|
+
module HotCell
|
|
5
|
+
module Client
|
|
6
|
+
module Previewers
|
|
7
|
+
# The two PDF previewers cannot share a base here: each subclasses the Rails previewer it replaces to
|
|
8
|
+
# keep its content-type predicate, and those are different classes. The namespace is the shared home.
|
|
9
|
+
module Pdf
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
PDF = Pdf
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
require "active_storage"
|
|
5
|
+
require "active_support/core_ext/class/attribute"
|
|
6
|
+
|
|
7
|
+
require "active_storage/hot_cell/client/operations"
|
|
8
|
+
|
|
9
|
+
module ActiveStorage
|
|
10
|
+
module HotCell
|
|
11
|
+
module Client
|
|
12
|
+
module Previewers
|
|
13
|
+
# The preview flow every previewer here shares. A previewer names the client that renders for it and
|
|
14
|
+
# differs in nothing else.
|
|
15
|
+
#
|
|
16
|
+
# **`accept?` must not probe for a binary, and that is the reason these previewers exist as much as the
|
|
17
|
+
# sandboxing is.** `MuPDFPreviewer.accept?` calls `mutool_exists?` and `VideoPreviewer.accept?` calls
|
|
18
|
+
# `ffmpeg_exists?`, both shelling out with `system` from inside a web request. The moment those binaries
|
|
19
|
+
# leave the application image — which is the point of moving the work into a cell — both answer false,
|
|
20
|
+
# `previewable?` goes false with them, and previews stop existing. No exception, no alert, nothing in a
|
|
21
|
+
# log.
|
|
22
|
+
#
|
|
23
|
+
# The sequencing matters as much as the override: these ship and are verified *before* the binary leaves
|
|
24
|
+
# the application image, because the window between those two events fails silently.
|
|
25
|
+
module Previewing
|
|
26
|
+
def self.included(previewer)
|
|
27
|
+
previewer.class_attribute :client, instance_accessor: false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def preview(**options)
|
|
31
|
+
download_blob_to_tempfile do |input|
|
|
32
|
+
render_through(input) do |attachable|
|
|
33
|
+
yield(**attachable, **options)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
# The filename extension and content type come from the cell's own result rather than being
|
|
40
|
+
# restated here, so the operation is the one source of truth for what it produced. The scratch
|
|
41
|
+
# tempfile's name never reaches Rails — only the yielded filename does.
|
|
42
|
+
#
|
|
43
|
+
# **Accepted risk.** A compromised cell chooses both of these values. It does not need to be
|
|
44
|
+
# believed for them to matter, and validating them here buys nothing, because the cell wrote the
|
|
45
|
+
# preview's bytes into this descriptor as its whole job. Active Storage re-identifies the
|
|
46
|
+
# attachment from those bytes and Marcel treats the declared type only as a hint, so bytes that
|
|
47
|
+
# carry script are identified as what they are and served under Rails' own
|
|
48
|
+
# `content_types_allowed_inline` and `content_types_to_serve_as_binary` rules. Lying about the
|
|
49
|
+
# label adds nothing to controlling the bytes. What is left is a preview that renders wrong,
|
|
50
|
+
# which is one of many things a compromised cell can do. The premise is that the bytes are
|
|
51
|
+
# already the cell's to choose; a caller that stops handing the cell an output descriptor would
|
|
52
|
+
# be the thing that changes it.
|
|
53
|
+
def render_through(input)
|
|
54
|
+
Tempfile.create("hotcell-preview", binmode: true) do |output|
|
|
55
|
+
result = File.open(input.path, "rb") do |readable|
|
|
56
|
+
File.open(output.path, "wb") do |writable|
|
|
57
|
+
self.class.client.perform_in_hotcell [ readable ], [ writable ], payload
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
File.open(output.path, "rb") do |io|
|
|
62
|
+
yield io: io, filename: "#{blob.filename.base}.#{result[:format]}",
|
|
63
|
+
content_type: result[:content_type]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# What travels with the request beyond the two descriptors. Nothing, for a previewer that has no
|
|
69
|
+
# configuration to carry; the video previewer overrides it with the application's input arguments.
|
|
70
|
+
def payload
|
|
71
|
+
{}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage/previewer/video_previewer"
|
|
4
|
+
|
|
5
|
+
require "active_storage/hot_cell/client/previewers/previewing"
|
|
6
|
+
require "active_storage/hot_cell/client/tool_arguments"
|
|
7
|
+
|
|
8
|
+
module ActiveStorage
|
|
9
|
+
module HotCell
|
|
10
|
+
module Client
|
|
11
|
+
module Previewers
|
|
12
|
+
module Video
|
|
13
|
+
# What Rails configures in `config.active_storage.previewers`, replacing VideoPreviewer.
|
|
14
|
+
class Ffmpeg < ActiveStorage::Previewer::VideoPreviewer
|
|
15
|
+
include Previewing
|
|
16
|
+
|
|
17
|
+
self.client = Operations::Previewers::Video::Ffmpeg
|
|
18
|
+
|
|
19
|
+
# blob.video? is the content-type predicate the superclass uses; ffmpeg_exists? is the part that
|
|
20
|
+
# has to go.
|
|
21
|
+
def self.accept?(blob)
|
|
22
|
+
blob.video?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
# `config.active_storage.video_preview_input_arguments`, which Rails splices before `-i`, carried
|
|
27
|
+
# to the cell so it can do the same. Split here, the way Rails splits it, and omitted when empty.
|
|
28
|
+
def payload
|
|
29
|
+
ToolArguments.payload(:input_arguments, ActiveStorage.video_preview_input_arguments)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
FFmpeg = Ffmpeg
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module ActiveStorage
|
|
6
|
+
module HotCell
|
|
7
|
+
module Client
|
|
8
|
+
# An application installs the transformer, the analyzer and the previewers itself, because Active Storage
|
|
9
|
+
# reads those from config and there is no way for a gem to set them that the engine does not later
|
|
10
|
+
# overwrite. Everything else this gem needs is done here.
|
|
11
|
+
class Railtie < ::Rails::Railtie
|
|
12
|
+
# to_prepare rather than after_initialize, because it runs again on every code reload and the job
|
|
13
|
+
# classes do not survive one. See retry_transient_failures!.
|
|
14
|
+
initializer "active_storage.hot_cell.client.retry_transient_failures" do |app|
|
|
15
|
+
app.config.to_prepare do
|
|
16
|
+
ActiveStorage::HotCell::Client.retry_transient_failures!
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The two settings rails/rails#58461 adds, copied from `config.active_storage.*` the way Rails' engine
|
|
21
|
+
# copies every other Active Storage setting — in after_initialize, once the environment file has run.
|
|
22
|
+
# On a Rails that already has them, its engine did this already and the same value lands twice.
|
|
23
|
+
#
|
|
24
|
+
# The engine creates `config.active_storage`; an application that has not loaded it has no such
|
|
25
|
+
# namespace, and then there is nothing to copy.
|
|
26
|
+
initializer "active_storage.hot_cell.client.tool_arguments" do
|
|
27
|
+
config.after_initialize do |app|
|
|
28
|
+
next unless app.config.respond_to?(:active_storage)
|
|
29
|
+
|
|
30
|
+
ActiveStorage.ffprobe_arguments = app.config.active_storage.ffprobe_arguments || ""
|
|
31
|
+
ActiveStorage.video_preview_input_arguments = app.config.active_storage.video_preview_input_arguments || ""
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require "active_storage"
|
|
5
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
6
|
+
|
|
7
|
+
module ActiveStorage
|
|
8
|
+
# The two settings rails/rails#58461 adds, defined here when the installed Active Storage predates them so
|
|
9
|
+
# that an application configures the same thing either way. Rails' own definition takes precedence when it
|
|
10
|
+
# exists — a second mattr_accessor would reset the value the engine already copied from config.
|
|
11
|
+
#
|
|
12
|
+
# Both are shell strings, the shape Rails chose for `video_preview_arguments`, and default to nothing.
|
|
13
|
+
mattr_accessor :ffprobe_arguments, default: "" unless respond_to?(:ffprobe_arguments)
|
|
14
|
+
mattr_accessor :video_preview_input_arguments, default: "" unless respond_to?(:video_preview_input_arguments)
|
|
15
|
+
|
|
16
|
+
module HotCell
|
|
17
|
+
module Client
|
|
18
|
+
# Turns one of those shell strings into the argv the cell splices in, split the way Rails splits it —
|
|
19
|
+
# here, in the application, so a malformed string raises against the configuration rather than arriving
|
|
20
|
+
# in the cell as a failed conversion that reads like the document's fault.
|
|
21
|
+
#
|
|
22
|
+
# An empty setting yields nothing at all, so the request carries no key and the operation runs its
|
|
23
|
+
# default argv unchanged.
|
|
24
|
+
module ToolArguments
|
|
25
|
+
def self.split(setting)
|
|
26
|
+
Shellwords.split(setting.to_s)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.payload(key, setting)
|
|
30
|
+
arguments = split(setting)
|
|
31
|
+
arguments.empty? ? {} : { key => arguments }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage"
|
|
4
|
+
require "active_storage/transformers/image_magick"
|
|
5
|
+
|
|
6
|
+
require "active_storage/hot_cell/client/operations"
|
|
7
|
+
require "active_storage/hot_cell/client/transformers/transforming"
|
|
8
|
+
|
|
9
|
+
module ActiveStorage
|
|
10
|
+
module HotCell
|
|
11
|
+
module Client
|
|
12
|
+
module Transformers
|
|
13
|
+
module Image
|
|
14
|
+
# What Rails configures as `config.active_storage.variant_processor` for an application on ImageMagick.
|
|
15
|
+
#
|
|
16
|
+
# config.active_storage.variant_processor = ActiveStorage::HotCell::Client::Transformers::Image::Magick
|
|
17
|
+
#
|
|
18
|
+
# It subclasses Rails' own ImageMagick transformer to keep its transformation allowlist —
|
|
19
|
+
# `supported_image_processing_methods` and the argument blocklist — which runs here, in the
|
|
20
|
+
# application, exactly where Rails runs it today. Only the final step changes: rather than shelling out
|
|
21
|
+
# to `magick` locally, the validated transformations cross to the cell.
|
|
22
|
+
class Magick < ActiveStorage::Transformers::ImageMagick
|
|
23
|
+
include Transforming
|
|
24
|
+
|
|
25
|
+
self.client = Operations::Transformers::Image::Magick
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
# `operations` is Rails' validation: it raises UnsupportedImageProcessingMethod or
|
|
29
|
+
# UnsupportedImageProcessingArgument for anything outside the allowlist. Called for that effect,
|
|
30
|
+
# then the transformations travel to the cell as the application wrote them.
|
|
31
|
+
def process(file, format:)
|
|
32
|
+
operations
|
|
33
|
+
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage"
|
|
4
|
+
require "active_storage/transformers/transformer"
|
|
5
|
+
|
|
6
|
+
require "active_storage/hot_cell/client/operations"
|
|
7
|
+
require "active_storage/hot_cell/client/transformers/transforming"
|
|
8
|
+
|
|
9
|
+
module ActiveStorage
|
|
10
|
+
module HotCell
|
|
11
|
+
module Client
|
|
12
|
+
module Transformers
|
|
13
|
+
module Image
|
|
14
|
+
# What Rails configures as `config.active_storage.variant_processor`.
|
|
15
|
+
#
|
|
16
|
+
# config.active_storage.variant_processor = ActiveStorage::HotCell::Client::Transformers::Image::Vips
|
|
17
|
+
#
|
|
18
|
+
# Note what adopting this does not achieve, because it is easy to assume otherwise: libvips is still loaded
|
|
19
|
+
# into the application process, by `require "active_storage/engine"`, before any configuration is read. The
|
|
20
|
+
# engine builds its default analyzers array by referencing ActiveStorage::Analyzer::ImageAnalyzer::Vips,
|
|
21
|
+
# which requires ruby-vips, which dlopens the library. No `variant_processor` value changes that. Getting
|
|
22
|
+
# the library out of the application means removing ruby-vips from the bundle, which then breaks that
|
|
23
|
+
# default array — an application's call, and not one configuration alone can make.
|
|
24
|
+
#
|
|
25
|
+
# What this does achieve is that no untrusted byte is decoded there.
|
|
26
|
+
class Vips < ActiveStorage::Transformers::Transformer
|
|
27
|
+
include Transforming
|
|
28
|
+
|
|
29
|
+
self.client = Operations::Transformers::Image::Vips
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
require "active_storage"
|
|
5
|
+
require "active_support/core_ext/class/attribute"
|
|
6
|
+
require "active_support/core_ext/hash/deep_transform_values"
|
|
7
|
+
|
|
8
|
+
require "active_storage/hot_cell/client/operations"
|
|
9
|
+
|
|
10
|
+
module ActiveStorage
|
|
11
|
+
module HotCell
|
|
12
|
+
module Client
|
|
13
|
+
module Transformers
|
|
14
|
+
# The conversion both transformers share. A transformer names the client that carries its
|
|
15
|
+
# transformations to the cell, and differs in nothing else.
|
|
16
|
+
module Transforming
|
|
17
|
+
def self.included(transformer)
|
|
18
|
+
transformer.class_attribute :client, instance_accessor: false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
# Returns an open, rewound Tempfile, which is the contract.
|
|
23
|
+
#
|
|
24
|
+
# The transformations travel to the cell almost as they arrived. Rails hands this a symbol-keyed hash
|
|
25
|
+
# with :format already removed — Variation deep-symbolizes on the way in and builds the transformer
|
|
26
|
+
# with `transformations.except(:format)` — so the keys need nothing, and deciding which keys are
|
|
27
|
+
# allowed belongs to the operation rather than to this side of the socket. Values are another matter:
|
|
28
|
+
# Variation symbolizes only keys, so `crop: :attention` reaches here as the application wrote it, and
|
|
29
|
+
# stock Rails accepts it. A Symbol value cannot ride JSON, which is this gem's transport detail
|
|
30
|
+
# rather than the application's problem — so Symbol values go over as the Strings the toolchain
|
|
31
|
+
# would have been handed anyway.
|
|
32
|
+
def process(file, format:)
|
|
33
|
+
output = Tempfile.new([ "hotcell", ".#{format}" ], binmode: true)
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
convert file, output, format: format.to_s, operations: stringified(transformations)
|
|
37
|
+
output.tap(&:rewind)
|
|
38
|
+
rescue StandardError
|
|
39
|
+
output.close!
|
|
40
|
+
raise
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Both handles are reopened by path, narrowly, and that is the protocol rather than fussiness. Rails
|
|
45
|
+
# hands out Tempfiles, which are read-write; an input descriptor must be read-only and an output
|
|
46
|
+
# write-only. An access mode is fixed at open, so a cell handed the wrong one cannot narrow it — it can
|
|
47
|
+
# only decline the request.
|
|
48
|
+
def convert(file, output, **payload)
|
|
49
|
+
File.open(file.path, "rb") do |readable|
|
|
50
|
+
File.open(output.path, "wb") do |writable|
|
|
51
|
+
self.class.client.perform_in_hotcell [ readable ], [ writable ], payload
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Only Symbol values, and only to String. Anything else unserializable is refused by the payload
|
|
57
|
+
# check as ever, because for everything but a Symbol there is no value Rails would have accepted
|
|
58
|
+
# in its place.
|
|
59
|
+
def stringified(transformations)
|
|
60
|
+
transformations.deep_transform_values { |value| value.is_a?(Symbol) ? value.to_s : value }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_storage"
|
|
4
|
+
require "active_support/core_ext/string/inflections"
|
|
5
|
+
|
|
6
|
+
require "active_storage/hot_cell/client/version"
|
|
7
|
+
require "active_storage/hot_cell/client/operations"
|
|
8
|
+
require "active_storage/hot_cell/client/transformers/image/vips"
|
|
9
|
+
require "active_storage/hot_cell/client/transformers/image/magick"
|
|
10
|
+
require "active_storage/hot_cell/client/analyzers/image/vips"
|
|
11
|
+
require "active_storage/hot_cell/client/analyzers/image/magick"
|
|
12
|
+
require "active_storage/hot_cell/client/analyzers/video/ffprobe"
|
|
13
|
+
require "active_storage/hot_cell/client/analyzers/audio/ffprobe"
|
|
14
|
+
require "active_storage/hot_cell/client/previewers/pdf/mutool"
|
|
15
|
+
require "active_storage/hot_cell/client/previewers/pdf/poppler"
|
|
16
|
+
require "active_storage/hot_cell/client/previewers/video/ffmpeg"
|
|
17
|
+
require "active_storage/hot_cell/client/railtie" if defined?(::Rails::Railtie)
|
|
18
|
+
|
|
19
|
+
module ActiveStorage
|
|
20
|
+
module HotCell
|
|
21
|
+
# Everything the application side defines lives under this, and everything the cell side defines lives under
|
|
22
|
+
# ActiveStorage::HotCell::Server. Not a tidying convention: the two gems are never both loaded in production,
|
|
23
|
+
# and a cell is forked from a process that may well have loaded this one — after which a shared name is a
|
|
24
|
+
# superclass mismatch while the cell boots. Two namespaces make that impossible rather than avoided.
|
|
25
|
+
module Client
|
|
26
|
+
# These four jobs declare `retry_on ActiveStorage::IntegrityError` and nothing else, and ActiveJob does
|
|
27
|
+
# not retry by default. They should retry the transient class too: `capacity` most obviously, and every
|
|
28
|
+
# other transient verdict. The policy matches the one they already declare, so a cell failure and an
|
|
29
|
+
# integrity failure back off the same way.
|
|
30
|
+
#
|
|
31
|
+
# Which of these classes exists depends on the Rails version, so a name that is not loaded is skipped.
|
|
32
|
+
JOBS = %w[
|
|
33
|
+
ActiveStorage::AnalyzeJob
|
|
34
|
+
ActiveStorage::CreateVariantsJob
|
|
35
|
+
ActiveStorage::PreviewImageJob
|
|
36
|
+
ActiveStorage::TransformJob
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
RETRY = { wait: :polynomially_longer, attempts: 10 }.freeze
|
|
40
|
+
|
|
41
|
+
# The clients this gem ships, which is where the retry hook learns which cells' transient classes
|
|
42
|
+
# the jobs must retry. Deliberately not HotCell.clients: that records every client the process loaded,
|
|
43
|
+
# including an application's own for unrelated cells, and Active Storage's jobs have no business
|
|
44
|
+
# retrying those.
|
|
45
|
+
CLIENTS = [ Operations::Transformers::Image::Vips, Operations::Transformers::Image::Magick,
|
|
46
|
+
Operations::Analyzers::Image::Vips, Operations::Analyzers::Image::Magick,
|
|
47
|
+
Operations::Analyzers::Media::Ffprobe,
|
|
48
|
+
Operations::Previewers::Pdf::Mutool, Operations::Previewers::Pdf::Poppler,
|
|
49
|
+
Operations::Previewers::Video::Ffmpeg ].freeze
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
# The railtie calls this from a to_prepare block. Applied once at boot it would not survive a code
|
|
53
|
+
# reload: a gem engine's app/jobs is in the reloadable autoloader, so these classes are discarded and
|
|
54
|
+
# redefined, and the retry would silently disappear after the first file save in development.
|
|
55
|
+
#
|
|
56
|
+
# Every registered cell contributes its transient class, because the documented multi-cell
|
|
57
|
+
# arrangement routes PreviewVideo to a cell of its own and each cell names its own class. A cell not
|
|
58
|
+
# yet registered contributes nothing rather than raising: this runs on the boot where the application
|
|
59
|
+
# has bundled the gem and not yet written the initializer, and that boot has to succeed for the
|
|
60
|
+
# rollout to take two deploys rather than one.
|
|
61
|
+
def retry_transient_failures!(jobs: JOBS)
|
|
62
|
+
transients = CLIENTS.filter_map { |client| client.cell.transient if client.registered? }
|
|
63
|
+
.uniq
|
|
64
|
+
return [] if transients.empty?
|
|
65
|
+
|
|
66
|
+
jobs.filter_map(&:safe_constantize)
|
|
67
|
+
.each { |job| job.retry_on(*transients, **RETRY) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bundler auto-requires this gem as "activestorage-hotcell-client", then as "activestorage/hotcell/client".
|
|
4
|
+
# It uses neither path. Without this file, naming it in a Gemfile silently loads nothing.
|
|
5
|
+
require "active_storage/hot_cell/client"
|
metadata
CHANGED
|
@@ -1,24 +1,86 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activestorage-hotcell-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Dalessio
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: hotcell-client
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - '='
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.2.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - '='
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.2.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activestorage
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 8.2.0.alpha
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 8.2.0.alpha
|
|
40
|
+
description: |
|
|
41
|
+
Drop-in replacements for Active Storage's variant processor, analyzers, and previewers. Configure
|
|
42
|
+
Rails with these classes and variants, image and media analysis, and previews run in a HotCell
|
|
43
|
+
container instead of in the application process.
|
|
13
44
|
email:
|
|
14
45
|
- mike@37signals.com
|
|
15
46
|
executables: []
|
|
16
47
|
extensions: []
|
|
17
48
|
extra_rdoc_files: []
|
|
18
|
-
files:
|
|
49
|
+
files:
|
|
50
|
+
- MIT-LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- lib/active_storage/hot_cell/client.rb
|
|
53
|
+
- lib/active_storage/hot_cell/client/analyzers/analyzing.rb
|
|
54
|
+
- lib/active_storage/hot_cell/client/analyzers/audio.rb
|
|
55
|
+
- lib/active_storage/hot_cell/client/analyzers/audio/ffprobe.rb
|
|
56
|
+
- lib/active_storage/hot_cell/client/analyzers/image.rb
|
|
57
|
+
- lib/active_storage/hot_cell/client/analyzers/image/magick.rb
|
|
58
|
+
- lib/active_storage/hot_cell/client/analyzers/image/vips.rb
|
|
59
|
+
- lib/active_storage/hot_cell/client/analyzers/probing.rb
|
|
60
|
+
- lib/active_storage/hot_cell/client/analyzers/video.rb
|
|
61
|
+
- lib/active_storage/hot_cell/client/analyzers/video/ffprobe.rb
|
|
62
|
+
- lib/active_storage/hot_cell/client/operations.rb
|
|
63
|
+
- lib/active_storage/hot_cell/client/previewers/pdf.rb
|
|
64
|
+
- lib/active_storage/hot_cell/client/previewers/pdf/mutool.rb
|
|
65
|
+
- lib/active_storage/hot_cell/client/previewers/pdf/poppler.rb
|
|
66
|
+
- lib/active_storage/hot_cell/client/previewers/previewing.rb
|
|
67
|
+
- lib/active_storage/hot_cell/client/previewers/video/ffmpeg.rb
|
|
68
|
+
- lib/active_storage/hot_cell/client/railtie.rb
|
|
69
|
+
- lib/active_storage/hot_cell/client/tool_arguments.rb
|
|
70
|
+
- lib/active_storage/hot_cell/client/transformers/image/magick.rb
|
|
71
|
+
- lib/active_storage/hot_cell/client/transformers/image/vips.rb
|
|
72
|
+
- lib/active_storage/hot_cell/client/transformers/transforming.rb
|
|
73
|
+
- lib/active_storage/hot_cell/client/version.rb
|
|
74
|
+
- lib/activestorage-hotcell-client.rb
|
|
75
|
+
homepage: https://github.com/basecamp/hotcell
|
|
19
76
|
licenses:
|
|
20
77
|
- MIT
|
|
21
|
-
metadata:
|
|
78
|
+
metadata:
|
|
79
|
+
homepage_uri: https://github.com/basecamp/hotcell
|
|
80
|
+
source_code_uri: https://github.com/basecamp/hotcell/tree/v0.2.0/activestorage-hotcell-client
|
|
81
|
+
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.2.0/CHANGELOG.md
|
|
82
|
+
bug_tracker_uri: https://github.com/basecamp/hotcell/issues
|
|
83
|
+
rubygems_mfa_required: 'true'
|
|
22
84
|
rdoc_options: []
|
|
23
85
|
require_paths:
|
|
24
86
|
- lib
|
|
@@ -26,7 +88,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
26
88
|
requirements:
|
|
27
89
|
- - ">="
|
|
28
90
|
- !ruby/object:Gem::Version
|
|
29
|
-
version: '
|
|
91
|
+
version: '3.3'
|
|
30
92
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
31
93
|
requirements:
|
|
32
94
|
- - ">="
|
|
@@ -35,5 +97,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
35
97
|
requirements: []
|
|
36
98
|
rubygems_version: 4.0.16
|
|
37
99
|
specification_version: 4
|
|
38
|
-
summary:
|
|
100
|
+
summary: Point Active Storage's transformer, analyzer, and previewers at a HotCell.
|
|
39
101
|
test_files: []
|