Replace all bindings with calls to external binaries

This commit is contained in:
Aode (lion) 2021-08-28 17:15:14 -05:00
parent 7fd707c8df
commit edd6bb4a60
16 changed files with 479 additions and 1283 deletions

377
Cargo.lock generated
View file

@ -261,9 +261,9 @@ dependencies = [
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.15" version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -368,66 +368,12 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bindgen"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239"
dependencies = [
"bitflags",
"cexpr 0.4.0",
"clang-sys",
"lazy_static",
"lazycell",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex 0.1.1",
]
[[package]]
name = "bindgen"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453c49e5950bb0eb63bb3df640e31618846c89d5b7faa54040d76e98e0134375"
dependencies = [
"bitflags",
"cexpr 0.5.0",
"clang-sys",
"clap",
"env_logger",
"lazy_static",
"lazycell",
"log",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex 1.0.0",
"which",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitvec"
version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.9.0" version = "0.9.0"
@ -464,30 +410,6 @@ dependencies = [
"bytes", "bytes",
] ]
[[package]]
name = "cc"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
[[package]]
name = "cexpr"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
dependencies = [
"nom 5.1.2",
]
[[package]]
name = "cexpr"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db507a7679252d2276ed0dd8113c6875ec56d3089f9225b2b42c30cc1f8e5c89"
dependencies = [
"nom 6.2.1",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -506,17 +428,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "clang-sys"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cf2cc85830eae84823884db23c5306442a6c3d5bfd3beb2f2a2c829faa1816"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.33.3" version = "2.33.3"
@ -546,9 +457,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.1.5" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -628,44 +539,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "env_logger"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "ffmpeg-next"
version = "4.4.0-dev"
source = "git+https://github.com/jwiesler/rust-ffmpeg#1966244b303a365d22eca0e33160e58b3d8ce903"
dependencies = [
"bitflags",
"ffmpeg-sys-next",
"libc",
"log",
"vsprintf",
]
[[package]]
name = "ffmpeg-sys-next"
version = "4.3.5"
source = "git+https://github.com/jwiesler/rust-ffmpeg-sys#5d819d172f116a8a6d960df35e4d5bc8b4a82924"
dependencies = [
"bindgen 0.56.0",
"cc",
"libc",
"num_cpus",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -692,12 +565,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.16" version = "0.3.16"
@ -822,22 +689,6 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "gexiv2-sys"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e66ac4aab8b401ca63838116d080b98e12fb5056e1c2a44e35c93da3526d13"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.4" version = "0.3.4"
@ -898,12 +749,6 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.2.3" version = "0.2.3"
@ -952,28 +797,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.101" version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
[[package]]
name = "libloading"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
dependencies = [
"cfg-if",
"winapi",
]
[[package]] [[package]]
name = "local-channel" name = "local-channel"
version = "0.1.2" version = "0.1.2"
@ -994,9 +823,9 @@ checksum = "84f9a2d3e27ce99ce2c3aad0b09b1a7b916293ea9b2bf624c13fe646fadd8da4"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.4" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
] ]
@ -1010,17 +839,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "magick_rust"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13584c9d754ca069ee43d1ee155c4a14a87e7e68faf7b61255f2d3133ee259c4"
dependencies = [
"bindgen 0.59.1",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.0.1" version = "0.0.1"
@ -1038,9 +856,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.4" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
@ -1079,28 +897,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "nom"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
"memchr",
"version_check",
]
[[package]]
name = "nom"
version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6"
dependencies = [
"bitvec",
"funty",
"memchr",
"version_check",
]
[[package]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.3.6" version = "0.3.6"
@ -1120,17 +916,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-rational"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.14" version = "0.2.14"
@ -1164,9 +949,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.1" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [ dependencies = [
"instant", "instant",
"lock_api", "lock_api",
@ -1175,9 +960,9 @@ dependencies = [
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.8.3" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"instant", "instant",
@ -1193,12 +978,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.1.0" version = "2.1.0"
@ -1226,15 +1005,11 @@ dependencies = [
"async-stream", "async-stream",
"awc", "awc",
"base64", "base64",
"ffmpeg-next",
"ffmpeg-sys-next",
"futures", "futures",
"magick_rust",
"mime", "mime",
"num_cpus", "num_cpus",
"once_cell", "once_cell",
"rand", "rand",
"rexiv2",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -1281,12 +1056,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.10" version = "0.2.10"
@ -1347,12 +1116,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "radium"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.4" version = "0.8.4"
@ -1404,9 +1167,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.4.6" version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1428,23 +1191,6 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "rexiv2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab34307334dcf90c2f9afeda08e0c52310f4d9f282cf6e8ec6a6ffb0b09826a5"
dependencies = [
"gexiv2-sys",
"libc",
"num-rational",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.2.3" version = "0.2.3"
@ -1510,18 +1256,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.129" version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1" checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.129" version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57ae87ad533d9a56427558b516d0adac283614e347abf85b0dc0cbbf0a249f3" checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1530,9 +1276,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.66" version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1553,9 +1299,9 @@ dependencies = [
[[package]] [[package]]
name = "sha-1" name = "sha-1"
version = "0.9.7" version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"cfg-if", "cfg-if",
@ -1572,9 +1318,9 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.9.5" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" checksum = "9204c41a1597a8c5af23c82d1c921cb01ec0a4c59e07a9c7306062829a3903f3"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"cfg-if", "cfg-if",
@ -1592,18 +1338,6 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shlex"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2"
[[package]]
name = "shlex"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.0" version = "1.4.0"
@ -1750,21 +1484,6 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.11.0"
@ -1776,18 +1495,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.26" version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" checksum = "e1c319f97498ee34e17e1d7813fcd28a0ec1aaf350a4c44883d2fe741edb1c70"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.26" version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" checksum = "8bf955fbafde33573fd32e90312488fa2ea68f7a220a5faab1809fa90690224f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2067,12 +1786,6 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.2" version = "0.8.2"
@ -2085,16 +1798,6 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "vsprintf"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aec2f81b75ca063294776b4f7e8da71d1d5ae81c2b1b149c8d89969230265d63"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.2+wasi-snapshot-preview1" version = "0.10.2+wasi-snapshot-preview1"
@ -2155,15 +1858,6 @@ version = "0.2.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29"
[[package]]
name = "which"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -2180,23 +1874,8 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"

View file

@ -20,12 +20,10 @@ async-stream = "0.3.0"
awc = { version = "3.0.0-beta.7", default-features = false } awc = { version = "3.0.0-beta.7", default-features = false }
base64 = "0.13.0" base64 = "0.13.0"
futures = "0.3.4" futures = "0.3.4"
magick_rust = { version = "0.15.0" }
mime = "0.3.1" mime = "0.3.1"
num_cpus = "1" num_cpus = "1"
once_cell = "1.4.0" once_cell = "1.4.0"
rand = "0.8.0" rand = "0.8.0"
rexiv2 = "0.9.1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
sha2 = "0.9.0" sha2 = "0.9.0"
@ -33,18 +31,8 @@ sled = { version = "0.34.6" }
structopt = "0.3.14" structopt = "0.3.14"
thiserror = "1.0" thiserror = "1.0"
time = { version = "0.2.23", features = ["serde"] } time = { version = "0.2.23", features = ["serde"] }
tokio = { version = "1", default-features = false, features = ["sync", "process"] } tokio = { version = "1", default-features = false, features = ["io-util", "process", "sync"] }
tracing = "0.1.15" tracing = "0.1.15"
tracing-futures = "0.2.4" tracing-futures = "0.2.4"
tracing-subscriber = { version = "0.2.5", features = ["fmt", "tracing-log"] } tracing-subscriber = { version = "0.2.5", features = ["fmt", "tracing-log"] }
uuid = { version = "0.8", features = ["v4"] } uuid = { version = "0.8", features = ["v4"] }
[dependencies.ffmpeg-next]
version = "4.4.0-dev"
default-features = false
features = ["codec", "filter", "device", "format", "resampling", "postprocessing", "software-resampling", "software-scaling"]
git = "https://github.com/jwiesler/rust-ffmpeg"
[dependencies.ffmpeg-sys-next]
version = "4.3.5"
git = "https://github.com/jwiesler/rust-ffmpeg-sys"

View file

@ -78,7 +78,7 @@ RUN \
--disable-static \ --disable-static \
--disable-docs \ --disable-docs \
--prefix=/usr/local \ --prefix=/usr/local \
--with-utilities=no \ --with-utilities=yes \
--with-magick-plus-plus=no \ --with-magick-plus-plus=no \
--without-perl \ --without-perl \
--with-xml=yes \ --with-xml=yes \
@ -121,7 +121,7 @@ FROM cross-build as pict-rs-builder
RUN \ RUN \
apt-get install -y \ apt-get install -y \
libgexiv2-dev:$ARCH \ exiv2:$ARCH \
libxml2:$ARCH \ libxml2:$ARCH \
libltdl7:$ARCH \ libltdl7:$ARCH \
llvm-dev \ llvm-dev \
@ -133,15 +133,7 @@ RUN \
libwebpdemux2:$ARCH \ libwebpdemux2:$ARCH \
libwebpmux3:$ARCH \ libwebpmux3:$ARCH \
libgomp1:$ARCH \ libgomp1:$ARCH \
libavcodec-dev:$ARCH \ ffmpeg:$ARCH
libavfilter-dev:$ARCH \
libavdevice-dev:$ARCH \
libavformat-dev:$ARCH \
libavresample-dev:$ARCH \
libavutil-dev:$ARCH \
libswscale-dev:$ARCH \
libswresample-dev:$ARCH \
ffmpeg
ENV \ ENV \
PATH=$PATH:/opt/build/.cargo/bin \ PATH=$PATH:/opt/build/.cargo/bin \

View file

@ -78,7 +78,7 @@ RUN \
--disable-static \ --disable-static \
--disable-docs \ --disable-docs \
--prefix=/usr/local \ --prefix=/usr/local \
--with-utilities=no \ --with-utilities=yes \
--with-magick-plus-plus=no \ --with-magick-plus-plus=no \
--without-perl \ --without-perl \
--with-xml=yes \ --with-xml=yes \
@ -121,7 +121,7 @@ FROM cross-build as pict-rs-builder
RUN \ RUN \
apt-get install -y \ apt-get install -y \
libgexiv2-dev:$ARCH \ exiv2:$ARCH \
libxml2:$ARCH \ libxml2:$ARCH \
libltdl7:$ARCH \ libltdl7:$ARCH \
llvm-dev \ llvm-dev \
@ -133,15 +133,7 @@ RUN \
libwebpdemux2:$ARCH \ libwebpdemux2:$ARCH \
libwebpmux3:$ARCH \ libwebpmux3:$ARCH \
libgomp1:$ARCH \ libgomp1:$ARCH \
libavcodec-dev:$ARCH \ ffmpeg:$ARCH && \
libavfilter-dev:$ARCH \
libavdevice-dev:$ARCH \
libavformat-dev:$ARCH \
libavresample-dev:$ARCH \
libavutil-dev:$ARCH \
libswscale-dev:$ARCH \
libswresample-dev:$ARCH \
ffmpeg && \
rm -rf /usr/include/x86_64-linux-gnu rm -rf /usr/include/x86_64-linux-gnu
ENV \ ENV \

View file

@ -78,7 +78,7 @@ RUN \
--disable-static \ --disable-static \
--disable-docs \ --disable-docs \
--prefix=/usr/local \ --prefix=/usr/local \
--with-utilities=no \ --with-utilities=yes \
--with-magick-plus-plus=no \ --with-magick-plus-plus=no \
--without-perl \ --without-perl \
--with-xml=yes \ --with-xml=yes \
@ -121,7 +121,7 @@ FROM cross-build as pict-rs-builder
RUN \ RUN \
apt-get install -y \ apt-get install -y \
libgexiv2-dev:$ARCH \ exiv2:$ARCH \
libxml2:$ARCH \ libxml2:$ARCH \
libltdl7:$ARCH \ libltdl7:$ARCH \
llvm-dev \ llvm-dev \
@ -133,14 +133,6 @@ RUN \
libwebpdemux2:$ARCH \ libwebpdemux2:$ARCH \
libwebpmux3:$ARCH \ libwebpmux3:$ARCH \
libgomp1:$ARCH \ libgomp1:$ARCH \
libavcodec-dev:$ARCH \
libavfilter-dev:$ARCH \
libavdevice-dev:$ARCH \
libavformat-dev:$ARCH \
libavresample-dev:$ARCH \
libavutil-dev:$ARCH \
libswscale-dev:$ARCH \
libswresample-dev:$ARCH \
ffmpeg && \ ffmpeg && \
rm -rf /usr/include/x86_64-linux-gnu rm -rf /usr/include/x86_64-linux-gnu

View file

@ -1,4 +1,4 @@
use crate::validate::GifError; use crate::{exiv2::Exvi2Error, ffmpeg::VideoError, magick::MagickError};
use actix_web::{http::StatusCode, HttpResponse, ResponseError}; use actix_web::{http::StatusCode, HttpResponse, ResponseError};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -57,26 +57,23 @@ pub(crate) enum UploadError {
#[error("Tried to save an image with an already-taken name")] #[error("Tried to save an image with an already-taken name")]
DuplicateAlias, DuplicateAlias,
#[error("Error validating Gif file, {0}")]
Gif(#[from] GifError),
#[error("Error transcoding, {0}")]
Transcode(crate::validate::transcode::Error),
#[error("Tried to create file, but file already exists")] #[error("Tried to create file, but file already exists")]
FileExists, FileExists,
#[error("File metadata could not be parsed, {0}")]
Validate(#[from] rexiv2::Rexiv2Error),
#[error("Error in MagickWand, {0}")]
Wand(String),
#[error("{0}")] #[error("{0}")]
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error("Range header not satisfiable")] #[error("Range header not satisfiable")]
Range, Range,
#[error("{0}")]
VideoError(#[from] VideoError),
#[error("{0}")]
Exvi2Error(#[from] Exvi2Error),
#[error("{0}")]
MagickError(#[from] MagickError),
} }
impl From<awc::error::SendRequestError> for UploadError { impl From<awc::error::SendRequestError> for UploadError {
@ -109,7 +106,9 @@ impl From<actix_web::error::BlockingError> for UploadError {
impl ResponseError for UploadError { impl ResponseError for UploadError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
UploadError::Gif(_) UploadError::VideoError(_)
| UploadError::Exvi2Error(_)
| UploadError::MagickError(_)
| UploadError::DuplicateAlias | UploadError::DuplicateAlias
| UploadError::NoFiles | UploadError::NoFiles
| UploadError::Upload(_) | UploadError::Upload(_)

View file

@ -1,10 +1,10 @@
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub(crate) enum FormatError { pub(crate) enum Exvi2Error {
#[error("Failed to interface with exiv2")] #[error("Failed to interface with exiv2")]
IO(#[from] std::io::Error), IO(#[from] std::io::Error),
#[error("Failed to identify file")] #[error("Mime Parse: {0}")]
Status, Mime(#[from] mime::FromStrError),
#[error("Identify semaphore is closed")] #[error("Identify semaphore is closed")]
Closed, Closed,
@ -12,6 +12,9 @@ pub(crate) enum FormatError {
#[error("Requested information is not present")] #[error("Requested information is not present")]
Missing, Missing,
#[error("Exiv2 command failed")]
Status,
#[error("Requested information was present, but not supported")] #[error("Requested information was present, but not supported")]
Unsupported, Unsupported,
} }
@ -21,6 +24,13 @@ pub(crate) enum ValidInputType {
Gif, Gif,
Png, Png,
Jpeg, Jpeg,
Webp,
}
pub(crate) struct Details {
pub(crate) mime_type: mime::Mime,
pub(crate) width: usize,
pub(crate) height: usize,
} }
static MAX_READS: once_cell::sync::OnceCell<tokio::sync::Semaphore> = static MAX_READS: once_cell::sync::OnceCell<tokio::sync::Semaphore> =
@ -30,48 +40,125 @@ fn semaphore() -> &'static tokio::sync::Semaphore {
MAX_READS.get_or_init(|| tokio::sync::Semaphore::new(num_cpus::get() * 4)) MAX_READS.get_or_init(|| tokio::sync::Semaphore::new(num_cpus::get() * 4))
} }
pub(crate) async fn format<P>(file: P) -> Result<ValidInputType, FormatError> pub(crate) async fn clear_metadata<P>(file: P) -> Result<(), Exvi2Error>
where
P: AsRef<std::path::Path>,
{
let permit = semaphore().acquire().await?;
let status = tokio::process::Command::new("exiv2")
.arg(&"rm")
.arg(&file.as_ref())
.spawn()?
.wait()
.await?;
drop(permit);
if !status.success() {
return Err(Exvi2Error::Status);
}
Ok(())
}
pub(crate) async fn input_type<P>(file: P) -> Result<ValidInputType, Exvi2Error>
where where
P: AsRef<std::path::Path>, P: AsRef<std::path::Path>,
{ {
let permit = semaphore().acquire().await?; let permit = semaphore().acquire().await?;
let output = tokio::process::Command::new("exiv2") let output = tokio::process::Command::new("exiv2")
.args([ .arg(&"pr")
&AsRef::<std::ffi::OsStr>::as_ref(&"pr"), .arg(&file.as_ref())
&file.as_ref().as_ref(),
])
.output() .output()
.await?; .await?;
drop(permit); drop(permit);
if !output.status.success() {
return Err(FormatError::Status);
}
let s = String::from_utf8_lossy(&output.stdout); let s = String::from_utf8_lossy(&output.stdout);
let line = s let mime_line = s
.lines() .lines()
.find(|line| line.starts_with("MIME")) .find(|line| line.starts_with("MIME"))
.ok_or_else(|| FormatError::Missing)?; .ok_or_else(|| Exvi2Error::Missing)?;
let mut segments = line.rsplit(':'); let mut segments = mime_line.rsplit(':');
let mime_type = segments.next().ok_or_else(|| FormatError::Missing)?; let mime_type = segments.next().ok_or_else(|| Exvi2Error::Missing)?;
let input_type = match mime_type.trim() { let input_type = match mime_type.trim() {
"video/mp4" => ValidInputType::Mp4, "video/mp4" => ValidInputType::Mp4,
"video/quicktime" => ValidInputType::Mp4,
"image/gif" => ValidInputType::Gif, "image/gif" => ValidInputType::Gif,
"image/png" => ValidInputType::Png, "image/png" => ValidInputType::Png,
"image/jpeg" => ValidInputType::Jpeg, "image/jpeg" => ValidInputType::Jpeg,
_ => return Err(FormatError::Unsupported), "image/webp" => ValidInputType::Webp,
_ => return Err(Exvi2Error::Unsupported),
}; };
Ok(input_type) Ok(input_type)
} }
impl From<tokio::sync::AcquireError> for FormatError { pub(crate) async fn details<P>(file: P) -> Result<Details, Exvi2Error>
fn from(_: tokio::sync::AcquireError) -> FormatError { where
FormatError::Closed P: AsRef<std::path::Path>,
{
let permit = semaphore().acquire().await?;
let output = tokio::process::Command::new("exiv2")
.arg(&"pr")
.arg(&file.as_ref())
.output()
.await?;
drop(permit);
let s = String::from_utf8_lossy(&output.stdout);
parse_output(s)
}
fn parse_output(s: std::borrow::Cow<'_, str>) -> Result<Details, Exvi2Error> {
let mime_line = s
.lines()
.find(|line| line.starts_with("MIME"))
.ok_or_else(|| Exvi2Error::Missing)?;
let mut segments = mime_line.rsplit(':');
let mime_type = segments.next().ok_or_else(|| Exvi2Error::Missing)?.trim();
let resolution_line = s
.lines()
.find(|line| line.starts_with("Image size"))
.ok_or_else(|| Exvi2Error::Missing)?;
let mut segments = resolution_line.rsplit(':');
let resolution = segments.next().ok_or_else(|| Exvi2Error::Missing)?;
let mut resolution_segments = resolution.split('x');
let width_str = resolution_segments
.next()
.ok_or_else(|| Exvi2Error::Missing)?
.trim();
let height_str = resolution_segments
.next()
.ok_or_else(|| Exvi2Error::Missing)?
.trim();
let width = width_str.parse()?;
let height = height_str.parse()?;
Ok(Details {
mime_type: mime_type.parse()?,
width,
height,
})
}
impl From<tokio::sync::AcquireError> for Exvi2Error {
fn from(_: tokio::sync::AcquireError) -> Exvi2Error {
Exvi2Error::Closed
}
}
impl From<std::num::ParseIntError> for Exvi2Error {
fn from(_: std::num::ParseIntError) -> Exvi2Error {
Exvi2Error::Unsupported
} }
} }

View file

@ -10,6 +10,27 @@ pub(crate) enum VideoError {
Closed, Closed,
} }
pub(crate) enum ThumbnailFormat {
Jpeg,
Webp,
}
impl ThumbnailFormat {
fn as_codec(&self) -> &'static str {
match self {
ThumbnailFormat::Jpeg => "mjpeg",
ThumbnailFormat::Webp => "webp",
}
}
fn as_format(&self) -> &'static str {
match self {
ThumbnailFormat::Jpeg => "singlejpeg",
ThumbnailFormat::Webp => "webp",
}
}
}
static MAX_TRANSCODES: once_cell::sync::OnceCell<tokio::sync::Semaphore> = static MAX_TRANSCODES: once_cell::sync::OnceCell<tokio::sync::Semaphore> =
once_cell::sync::OnceCell::new(); once_cell::sync::OnceCell::new();
@ -18,22 +39,6 @@ fn semaphore() -> &'static tokio::sync::Semaphore {
.get_or_init(|| tokio::sync::Semaphore::new(num_cpus::get().saturating_sub(1).max(1))) .get_or_init(|| tokio::sync::Semaphore::new(num_cpus::get().saturating_sub(1).max(1)))
} }
pub(crate) async fn thumbnail_jpeg<P1, P2>(from: P1, to: P2) -> Result<(), VideoError>
where
P1: AsRef<std::path::Path>,
P2: AsRef<std::path::Path>,
{
thumbnail(from, to, "mjpeg").await
}
pub(crate) async fn thumbnail_png<P1, P2>(from: P1, to: P2) -> Result<(), VideoError>
where
P1: AsRef<std::path::Path>,
P2: AsRef<std::path::Path>,
{
thumbnail(from, to, "png").await
}
pub(crate) async fn to_mp4<P1, P2>(from: P1, to: P2) -> Result<(), VideoError> pub(crate) async fn to_mp4<P1, P2>(from: P1, to: P2) -> Result<(), VideoError>
where where
P1: AsRef<std::path::Path>, P1: AsRef<std::path::Path>,
@ -42,20 +47,22 @@ where
let permit = semaphore().acquire().await?; let permit = semaphore().acquire().await?;
let mut child = tokio::process::Command::new("ffmpeg") let mut child = tokio::process::Command::new("ffmpeg")
.arg(&"-i")
.arg(&from.as_ref())
.args([ .args([
&AsRef::<std::ffi::OsStr>::as_ref(&"-i"), &"-movflags",
&from.as_ref().as_ref(), &"faststart",
&"-movflags".as_ref(), &"-pix_fmt",
&"faststart".as_ref(), &"yuv420p",
&"-pix_fmt".as_ref(), &"-vf",
&"yuv420p".as_ref(), &"scale=trunc(iw/2)*2:trunc(ih/2)*2",
&"-vf".as_ref(), &"-an",
&"scale=trunc(iw/2)*2:truc(ih/2)*2".as_ref(), &"-codec",
&"-an".as_ref(), &"h264",
&"-codec".as_ref(), &"-f",
&"h264".as_ref(), &"mp4",
&to.as_ref().as_ref(),
]) ])
.arg(&to.as_ref())
.spawn()?; .spawn()?;
let status = child.wait().await?; let status = child.wait().await?;
@ -68,7 +75,11 @@ where
Ok(()) Ok(())
} }
async fn thumbnail<P1, P2>(from: P1, to: P2, codec: &str) -> Result<(), VideoError> pub(crate) async fn thumbnail<P1, P2>(
from: P1,
to: P2,
format: ThumbnailFormat,
) -> Result<(), VideoError>
where where
P1: AsRef<std::path::Path>, P1: AsRef<std::path::Path>,
P2: AsRef<std::path::Path>, P2: AsRef<std::path::Path>,
@ -76,17 +87,19 @@ where
let permit = semaphore().acquire().await?; let permit = semaphore().acquire().await?;
let mut child = tokio::process::Command::new("ffmpeg") let mut child = tokio::process::Command::new("ffmpeg")
.arg(&"-i")
.arg(&from.as_ref())
.args([ .args([
&AsRef::<std::ffi::OsStr>::as_ref(&"-i"), &"-ss",
&from.as_ref().as_ref(), &"00:00:01.000",
&"-ss".as_ref(), &"-vframes",
&"00:00:01.000".as_ref(), &"1",
&"-vframes".as_ref(), &"-codec",
&"1".as_ref(), &format.as_codec(),
&"-codec".as_ref(), &"-f",
&codec.as_ref(), &format.as_format(),
&to.as_ref().as_ref(),
]) ])
.arg(&to.as_ref())
.spawn()?; .spawn()?;
let status = child.wait().await?; let status = child.wait().await?;

View file

@ -1,28 +1,135 @@
fn thumbnail_args(max_dimension: usize) -> [String; 2] { #[derive(Debug, thiserror::Error)]
[ pub(crate) enum MagickError {
"-sample".to_string(), #[error("{0}")]
format!("{}x{}>", max_dimension, max_dimension), IO(#[from] std::io::Error),
]
#[error("Magick command failed")]
Status,
#[error("Magick semaphore is closed")]
Closed,
#[error("Invalid format")]
Format,
} }
fn resize_args(max_dimension: usize) -> [String; 4] { pub(crate) enum ValidFormat {
[ Jpeg,
"-filter".to_string(), Png,
"Lanczos".to_string(), Webp,
"-resize".to_string(),
format!("{}x{}>", max_dimension, max_dimension),
]
} }
fn crop_args(width: usize, height: usize) -> [String; 4] { impl ValidFormat {
[ fn as_magic_type(&self) -> &'static str {
"-gravity".to_string(), match self {
"center".to_string(), ValidFormat::Jpeg => "JPEG",
"-crop".to_string(), ValidFormat::Png => "PNG",
format!("{}x{}>", width, height), ValidFormat::Webp => "WEBP",
] }
}
} }
fn blur_args(radius: f64) -> [String; 2] { static MAX_CONVERSIONS: once_cell::sync::OnceCell<tokio::sync::Semaphore> =
["-gaussian-blur".to_string(), radius.to_string()] once_cell::sync::OnceCell::new();
fn semaphore() -> &'static tokio::sync::Semaphore {
MAX_CONVERSIONS
.get_or_init(|| tokio::sync::Semaphore::new(num_cpus::get().saturating_sub(1).max(1)))
}
pub(crate) async fn convert_file<P1, P2>(
from: P1,
to: P2,
format: crate::config::Format,
) -> Result<(), MagickError>
where
P1: AsRef<std::path::Path>,
P2: AsRef<std::path::Path>,
{
let mut output_file = std::ffi::OsString::new();
output_file.extend([format.to_magick_format().as_ref(), ":".as_ref()]);
output_file.extend([to.as_ref().as_ref()]);
let permit = semaphore().acquire().await?;
let status = tokio::process::Command::new("magick")
.arg("convert")
.arg(&from.as_ref())
.arg(&output_file)
.spawn()?
.wait()
.await?;
drop(permit);
if !status.success() {
return Err(MagickError::Status);
}
Ok(())
}
pub(crate) async fn validate_format<P>(file: &P, format: ValidFormat) -> Result<(), MagickError>
where
P: AsRef<std::path::Path>,
{
let permit = semaphore().acquire().await?;
let output = tokio::process::Command::new("magick")
.args([&"identify", &"-ping", &"-format", &"%m\n"])
.arg(&file.as_ref())
.output()
.await?;
drop(permit);
let s = String::from_utf8_lossy(&output.stdout);
if s.lines()
.all(|item| item.is_empty() || item == format.as_magic_type())
{
return Ok(());
}
Err(MagickError::Format)
}
pub(crate) async fn process_image<P1, P2>(
input: P1,
output: P2,
args: Vec<String>,
format: crate::config::Format,
) -> Result<(), MagickError>
where
P1: AsRef<std::path::Path>,
P2: AsRef<std::path::Path>,
{
let mut output_file = std::ffi::OsString::new();
output_file.extend([format!("{}:", format.to_magick_format()).as_ref()]);
output_file.extend([output.as_ref().as_ref()]);
let permit = semaphore().acquire().await?;
let status = tokio::process::Command::new("magick")
.arg(&"convert")
.arg(&input.as_ref())
.args(args)
.arg(&output.as_ref())
.spawn()?
.wait()
.await?;
drop(permit);
if !status.success() {
return Err(MagickError::Status);
}
Ok(())
}
impl From<tokio::sync::AcquireError> for MagickError {
fn from(_: tokio::sync::AcquireError) -> MagickError {
MagickError::Closed
}
} }

View file

@ -6,11 +6,9 @@ use actix_web::{
web, App, HttpResponse, HttpResponseBuilder, HttpServer, web, App, HttpResponse, HttpResponseBuilder, HttpServer,
}; };
use awc::Client; use awc::Client;
use futures::stream::{once, Stream, TryStreamExt}; use futures::stream::{Stream, TryStreamExt};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{ use std::{collections::HashSet, path::PathBuf, pin::Pin, time::SystemTime};
collections::HashSet, future::ready, path::PathBuf, pin::Pin, sync::Once, time::SystemTime,
};
use structopt::StructOpt; use structopt::StructOpt;
use tracing::{debug, error, info, instrument, Span}; use tracing::{debug, error, info, instrument, Span};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
@ -19,6 +17,7 @@ mod config;
mod error; mod error;
mod exiv2; mod exiv2;
mod ffmpeg; mod ffmpeg;
mod magick;
mod middleware; mod middleware;
mod migrate; mod migrate;
mod processor; mod processor;
@ -30,7 +29,6 @@ use self::{
config::{Config, Format}, config::{Config, Format},
error::UploadError, error::UploadError,
middleware::{Internal, Tracing}, middleware::{Internal, Tracing},
processor::process_image,
upload_manager::{Details, UploadManager}, upload_manager::{Details, UploadManager},
validate::{image_webp, video_mp4}, validate::{image_webp, video_mp4},
}; };
@ -58,7 +56,6 @@ static TMP_DIR: Lazy<PathBuf> = Lazy::new(|| {
path path
}); });
static CONFIG: Lazy<Config> = Lazy::new(Config::from_args); static CONFIG: Lazy<Config> = Lazy::new(Config::from_args);
static MAGICK_INIT: Once = Once::new();
// try moving a file // try moving a file
#[instrument] #[instrument]
@ -83,8 +80,11 @@ async fn safe_move_file(from: PathBuf, to: PathBuf) -> Result<(), UploadError> {
Ok(()) Ok(())
} }
async fn safe_create_parent(path: PathBuf) -> Result<(), UploadError> { async fn safe_create_parent<P>(path: P) -> Result<(), UploadError>
if let Some(path) = path.parent() { where
P: AsRef<std::path::Path>,
{
if let Some(path) = path.as_ref().parent() {
debug!("Creating directory {:?}", path); debug!("Creating directory {:?}", path);
actix_fs::create_dir_all(path.to_owned()).await?; actix_fs::create_dir_all(path.to_owned()).await?;
} }
@ -285,7 +285,7 @@ async fn prepare_process(
ext: &str, ext: &str,
manager: &UploadManager, manager: &UploadManager,
whitelist: &Option<HashSet<String>>, whitelist: &Option<HashSet<String>>,
) -> Result<(processor::ProcessChain, Format, String, PathBuf), UploadError> { ) -> Result<(Format, String, PathBuf, Vec<String>), UploadError> {
let (alias, operations) = let (alias, operations) =
query query
.into_inner() .into_inner()
@ -322,8 +322,9 @@ async fn prepare_process(
let processed_name = format!("{}.{}", name, ext); let processed_name = format!("{}.{}", name, ext);
let base = manager.image_dir(); let base = manager.image_dir();
let thumbnail_path = self::processor::build_path(base, &chain, processed_name); let thumbnail_path = self::processor::build_path(base, &chain, processed_name);
let thumbnail_args = self::processor::build_args(&chain);
Ok((chain, format, name, thumbnail_path)) Ok((format, name, thumbnail_path, thumbnail_args))
} }
async fn process_details( async fn process_details(
@ -332,7 +333,7 @@ async fn process_details(
manager: web::Data<UploadManager>, manager: web::Data<UploadManager>,
whitelist: web::Data<Option<HashSet<String>>>, whitelist: web::Data<Option<HashSet<String>>>,
) -> Result<HttpResponse, UploadError> { ) -> Result<HttpResponse, UploadError> {
let (_, _, name, thumbnail_path) = let (_, name, thumbnail_path, _) =
prepare_process(query, ext.as_str(), &manager, &whitelist).await?; prepare_process(query, ext.as_str(), &manager, &whitelist).await?;
let details = manager.variant_details(thumbnail_path, name).await?; let details = manager.variant_details(thumbnail_path, name).await?;
@ -351,7 +352,7 @@ async fn process(
manager: web::Data<UploadManager>, manager: web::Data<UploadManager>,
whitelist: web::Data<Option<HashSet<String>>>, whitelist: web::Data<Option<HashSet<String>>>,
) -> Result<HttpResponse, UploadError> { ) -> Result<HttpResponse, UploadError> {
let (chain, format, name, thumbnail_path) = let (format, name, thumbnail_path, thumbnail_args) =
prepare_process(query, ext.as_str(), &manager, &whitelist).await?; prepare_process(query, ext.as_str(), &manager, &whitelist).await?;
// If the thumbnail doesn't exist, we need to create it // If the thumbnail doesn't exist, we need to create it
@ -396,78 +397,25 @@ async fn process(
} }
} }
safe_create_parent(&thumbnail_path).await?;
// apply chain to the provided image // apply chain to the provided image
let img_bytes = process_image(original_path.clone(), chain, format).await?; magick::process_image(&original_path, &thumbnail_path, thumbnail_args, format).await?;
let path2 = thumbnail_path.clone();
let img_bytes2 = img_bytes.clone();
let store_details = details.is_none();
let details = if let Some(details) = details { let details = if let Some(details) = details {
details details
} else { } else {
let details = Details::from_bytes(&img_bytes)?; let details = Details::from_path(&thumbnail_path).await?;
manager manager
.store_variant_details(path2.clone(), name.clone(), &details) .store_variant_details(thumbnail_path.clone(), name.clone(), &details)
.await?;
manager
.store_variant(thumbnail_path.clone(), name.clone())
.await?; .await?;
details details
}; };
// Save the file in another task, we want to return the thumbnail now return ranged_file_resp(thumbnail_path, range, details).await;
debug!("Spawning storage task");
let span = Span::current();
let details2 = details.clone();
actix_rt::spawn(async move {
let entered = span.enter();
if store_details {
debug!("Storing details");
if let Err(e) = manager
.store_variant_details(path2.clone(), name.clone(), &details2)
.await
{
error!("Error storing details, {}", e);
return;
}
}
if let Err(e) = manager.store_variant(path2.clone(), name).await {
error!("Error storing variant, {}", e);
return;
}
if let Err(e) = safe_save_file(path2, img_bytes2).await {
error!("Error saving file, {}", e);
}
drop(entered);
});
let (builder, stream) = match range {
Some(range_header) => {
if !range_header.is_bytes() {
return Err(UploadError::Range);
}
if range_header.is_empty() {
return Err(UploadError::Range);
} else if range_header.len() == 1 {
let range = range_header.ranges().next().unwrap();
let mut builder = HttpResponse::PartialContent();
builder.insert_header(range.to_content_range(img_bytes.len() as u64));
(builder, range.chop_bytes(img_bytes))
} else {
return Err(UploadError::Range);
}
}
None => (HttpResponse::Ok(), once(ready(Ok(img_bytes)))),
};
return Ok(srv_response(
builder,
stream,
details.content_type(),
7 * DAYS,
details.system_time(),
));
} }
let details = if let Some(details) = details { let details = if let Some(details) = details {
@ -670,10 +618,6 @@ async fn filename_by_alias(
#[actix_rt::main] #[actix_rt::main]
async fn main() -> Result<(), anyhow::Error> { async fn main() -> Result<(), anyhow::Error> {
MAGICK_INIT.call_once(|| {
magick_rust::magick_wand_genesis();
});
if std::env::var("RUST_LOG").is_err() { if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info"); std::env::set_var("RUST_LOG", "info");
} }

View file

@ -1,12 +1,10 @@
use crate::{ use crate::{error::UploadError, ffmpeg::ThumbnailFormat};
config::Format,
error::UploadError,
validate::{ptos, Op},
};
use actix_web::web;
use magick_rust::MagickWand;
use std::path::PathBuf; use std::path::PathBuf;
use tracing::{debug, error, instrument, Span}; use tracing::{debug, error, instrument};
fn ptos(path: &PathBuf) -> Result<String, UploadError> {
Ok(path.to_str().ok_or(UploadError::Path)?.to_owned())
}
pub(crate) trait Processor { pub(crate) trait Processor {
fn name() -> &'static str fn name() -> &'static str
@ -22,7 +20,7 @@ pub(crate) trait Processor {
Self: Sized; Self: Sized;
fn path(&self, path: PathBuf) -> PathBuf; fn path(&self, path: PathBuf) -> PathBuf;
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError>; fn command(&self, args: Vec<String>) -> Vec<String>;
} }
pub(crate) struct Identity; pub(crate) struct Identity;
@ -46,7 +44,6 @@ impl Processor for Identity {
where where
Self: Sized, Self: Sized,
{ {
debug!("Identity");
Some(Box::new(Identity)) Some(Box::new(Identity))
} }
@ -54,8 +51,8 @@ impl Processor for Identity {
path path
} }
fn process(&self, _: &mut MagickWand) -> Result<(), UploadError> { fn command(&self, args: Vec<String>) -> Vec<String> {
Ok(()) args
} }
} }
@ -90,25 +87,10 @@ impl Processor for Thumbnail {
path path
} }
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> { fn command(&self, mut args: Vec<String>) -> Vec<String> {
debug!("Thumbnail"); args.extend(["-sample".to_string(), format!("{}x{}>", self.0, self.0)]);
let width = wand.get_image_width();
let height = wand.get_image_height();
if width > self.0 || height > self.0 { args
let width_ratio = width as f64 / self.0 as f64;
let height_ratio = height as f64 / self.0 as f64;
let (new_width, new_height) = if width_ratio < height_ratio {
(width as f64 / height_ratio, self.0 as f64)
} else {
(self.0 as f64, height as f64 / width_ratio)
};
wand.op(|w| w.sample_image(new_width as usize, new_height as usize))?;
}
Ok(())
} }
} }
@ -143,29 +125,15 @@ impl Processor for Resize {
path path
} }
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> { fn command(&self, mut args: Vec<String>) -> Vec<String> {
debug!("Resize"); args.extend([
let width = wand.get_image_width(); "-filter".to_string(),
let height = wand.get_image_height(); "-Lanczos2".to_string(),
"-resize".to_string(),
format!("{}x{}>", self.0, self.0),
]);
if width > self.0 || height > self.0 { args
let width_ratio = width as f64 / self.0 as f64;
let height_ratio = height as f64 / self.0 as f64;
let (new_width, new_height) = if width_ratio < height_ratio {
(width as f64 / height_ratio, self.0 as f64)
} else {
(self.0 as f64, height as f64 / width_ratio)
};
wand.resize_image(
new_width as usize,
new_height as usize,
magick_rust::bindings::FilterType_Lanczos2Filter,
);
}
Ok(())
} }
} }
@ -208,40 +176,15 @@ impl Processor for Crop {
path path
} }
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> { fn command(&self, mut args: Vec<String>) -> Vec<String> {
let width = wand.get_image_width(); args.extend([
let height = wand.get_image_height(); "-gravity".to_string(),
"center".to_string(),
"-crop".to_string(),
format!("{}:{}+0+0", self.0, self.1),
]);
// 16x9 becomes 16/9, which is bigger than 16/10. a bigger number means a wider image args
//
// Crop ratios bigger than Image ratios mean cropping the image's height and leaving the
// width alone.
let img_ratio = width as f64 / height as f64;
let crop_ratio = self.0 as f64 / self.1 as f64;
let final_width;
let final_height;
let x_offset;
let y_offset;
if crop_ratio > img_ratio {
final_height = (width as f64 / self.0 as f64 * self.1 as f64) as usize;
final_width = width;
x_offset = 0;
y_offset = ((height - final_height) as f64 / 2.0) as isize;
} else {
final_height = height;
final_width = (height as f64 / self.1 as f64 * self.0 as f64) as usize;
x_offset = ((width - final_width) as f64 / 2.0) as isize;
y_offset = 0;
}
wand.op(|w| w.crop_image(final_width, final_height, x_offset, y_offset))?;
Ok(())
} }
} }
@ -270,13 +213,10 @@ impl Processor for Blur {
path path
} }
fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> { fn command(&self, mut args: Vec<String>) -> Vec<String> {
debug!("Blur"); args.extend(["-gaussian-blur".to_string(), self.0.to_string()]);
if self.0 > 0.0 {
wand.op(|w| w.gaussian_blur_image(0.0, self.0))?;
}
Ok(()) args
} }
} }
@ -333,6 +273,13 @@ pub(crate) fn build_path(base: PathBuf, chain: &ProcessChain, filename: String)
path path
} }
pub(crate) fn build_args(chain: &ProcessChain) -> Vec<String> {
chain
.inner
.iter()
.fold(Vec::new(), |acc, processor| processor.command(acc))
}
fn is_motion(s: &str) -> bool { fn is_motion(s: &str) -> bool {
s.ends_with(".gif") || s.ends_with(".mp4") s.ends_with(".gif") || s.ends_with(".mp4")
} }
@ -363,23 +310,17 @@ pub(crate) async fn prepare_image(
let orig_path = original_path_str.clone(); let orig_path = original_path_str.clone();
let tmpfile = crate::tmp_file(); let tmpfile = crate::tmp_file();
crate::safe_create_parent(tmpfile.clone()).await?; crate::safe_create_parent(&tmpfile).await?;
let tmpfile2 = tmpfile.clone();
let res = web::block(move || { let res = crate::ffmpeg::thumbnail(orig_path, &tmpfile, ThumbnailFormat::Jpeg).await;
use crate::validate::transcode::{transcode, Target};
transcode(orig_path, tmpfile, Target::Jpeg).map_err(UploadError::Transcode)
})
.await?;
if let Err(e) = res { if let Err(e) = res {
error!("transcode error: {:?}", e); error!("transcode error: {:?}", e);
actix_fs::remove_file(tmpfile2).await?; actix_fs::remove_file(tmpfile.clone()).await?;
return Err(e.into()); return Err(e.into());
} }
return match crate::safe_move_file(tmpfile2, jpg_path.clone()).await { return match crate::safe_move_file(tmpfile, jpg_path.clone()).await {
Err(UploadError::FileExists) => Ok(Some((jpg_path, Exists::Exists))), Err(UploadError::FileExists) => Ok(Some((jpg_path, Exists::Exists))),
Err(e) => Err(e), Err(e) => Err(e),
_ => Ok(Some((jpg_path, Exists::New))), _ => Ok(Some((jpg_path, Exists::New))),
@ -388,34 +329,3 @@ pub(crate) async fn prepare_image(
Ok(None) Ok(None)
} }
#[instrument]
pub(crate) async fn process_image(
original_file: PathBuf,
chain: ProcessChain,
format: Format,
) -> Result<web::Bytes, UploadError> {
let original_path_str = ptos(&original_file)?;
let span = Span::current();
let bytes = web::block(move || {
let entered = span.enter();
let mut wand = MagickWand::new();
debug!("Reading image");
wand.op(|w| w.read_image(&original_path_str))?;
debug!("Processing image");
for processor in chain.inner.into_iter() {
debug!("Step");
processor.process(&mut wand)?;
}
let vec = wand.op(|w| w.write_image_blob(format.to_magick_format()))?;
drop(entered);
Ok(web::Bytes::from(vec)) as Result<web::Bytes, UploadError>
})
.await??;
Ok(bytes)
}

View file

@ -8,12 +8,9 @@ use actix_web::{
web::Bytes, web::Bytes,
FromRequest, HttpRequest, FromRequest, HttpRequest,
}; };
use futures::stream::{once, Once, Stream, StreamExt, TryStreamExt}; use futures::stream::{Stream, StreamExt, TryStreamExt};
use std::{fs, io}; use std::{fs, io};
use std::{ use std::{future::ready, pin::Pin};
future::{ready, Ready},
pin::Pin,
};
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum Range { pub(crate) enum Range {
@ -46,16 +43,6 @@ impl Range {
} }
} }
pub(crate) fn chop_bytes(&self, bytes: Bytes) -> Once<Ready<Result<Bytes, UploadError>>> {
match self {
Range::RangeStart(start) => once(ready(Ok(bytes.slice(*start as usize..)))),
Range::SuffixLength(from_start) => once(ready(Ok(bytes.slice(..*from_start as usize)))),
Range::Segment(start, end) => {
once(ready(Ok(bytes.slice(*start as usize..*end as usize))))
}
}
}
pub(crate) async fn chop_file( pub(crate) async fn chop_file(
&self, &self,
file: fs::File, file: fs::File,

View file

@ -96,47 +96,18 @@ pub(crate) struct Details {
created_at: time::OffsetDateTime, created_at: time::OffsetDateTime,
} }
fn mime_from_media_type(media_type: rexiv2::MediaType) -> mime::Mime {
match media_type {
rexiv2::MediaType::Jpeg => mime::IMAGE_JPEG,
rexiv2::MediaType::Png => mime::IMAGE_PNG,
rexiv2::MediaType::Gif => mime::IMAGE_GIF,
rexiv2::MediaType::Other(s) if s == "image/webp" => s.parse::<mime::Mime>().unwrap(),
rexiv2::MediaType::Other(s) if s == "video/mp4" || s == "video/quicktime" => {
"video/mp4".parse::<mime::Mime>().unwrap()
}
_ => mime::APPLICATION_OCTET_STREAM,
}
}
impl Details { impl Details {
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self, UploadError> { pub(crate) async fn from_path<P>(path: P) -> Result<Self, UploadError>
let metadata = rexiv2::Metadata::new_from_buffer(bytes)?; where
let mime_type = mime_from_media_type(metadata.get_media_type()?); P: AsRef<std::path::Path>,
let width = metadata.get_pixel_width(); {
let height = metadata.get_pixel_height(); let details = crate::exiv2::details(&path).await?;
let details = Details::now(width as usize, height as usize, mime_type);
Ok(details)
}
pub(crate) async fn from_path(path: PathBuf) -> Result<Self, UploadError> { Ok(Details::now(
let (mime_type, width, height) = web::block(move || { details.width,
rexiv2::Metadata::new_from_path(&path).and_then(|metadata| { details.height,
metadata details.mime_type,
.get_media_type() ))
.map(mime_from_media_type)
.map(|mime_type| {
(
mime_type,
metadata.get_pixel_width(),
metadata.get_pixel_height(),
)
})
})
})
.await??;
Ok(Details::now(width as usize, height as usize, mime_type))
} }
fn now(width: usize, height: usize, content_type: mime::Mime) -> Self { fn now(width: usize, height: usize, content_type: mime::Mime) -> Self {

67
src/validate.rs Normal file
View file

@ -0,0 +1,67 @@
use crate::{
config::Format, error::UploadError, exiv2::ValidInputType, magick::ValidFormat, tmp_file,
};
pub(crate) fn image_webp() -> mime::Mime {
"image/webp".parse().unwrap()
}
pub(crate) fn video_mp4() -> mime::Mime {
"video/mp4".parse().unwrap()
}
// import & export image using the image crate
#[tracing::instrument]
pub(crate) async fn validate_image(
tmpfile: std::path::PathBuf,
prescribed_format: Option<Format>,
) -> Result<mime::Mime, UploadError> {
let input_type = crate::exiv2::input_type(&tmpfile).await?;
match (prescribed_format, input_type) {
(_, ValidInputType::Gif) | (_, ValidInputType::Mp4) => {
let newfile = tmp_file();
crate::safe_create_parent(&newfile).await?;
crate::ffmpeg::to_mp4(&tmpfile, &newfile).await?;
actix_fs::rename(newfile, tmpfile).await?;
Ok(video_mp4())
}
(Some(Format::Jpeg), ValidInputType::Jpeg) | (None, ValidInputType::Jpeg) => {
tracing::debug!("Validating format");
crate::magick::validate_format(&tmpfile, ValidFormat::Jpeg).await?;
tracing::debug!("Clearing metadata");
crate::exiv2::clear_metadata(&tmpfile).await?;
tracing::debug!("Validated");
Ok(mime::IMAGE_JPEG)
}
(Some(Format::Png), ValidInputType::Png) | (None, ValidInputType::Png) => {
tracing::debug!("Validating format");
crate::magick::validate_format(&tmpfile, ValidFormat::Png).await?;
tracing::debug!("Clearing metadata");
crate::exiv2::clear_metadata(&tmpfile).await?;
tracing::debug!("Validated");
Ok(mime::IMAGE_PNG)
}
(Some(Format::Webp), ValidInputType::Webp) | (None, ValidInputType::Webp) => {
tracing::debug!("Validating format");
crate::magick::validate_format(&tmpfile, ValidFormat::Webp).await?;
tracing::debug!("Clearing metadata");
crate::exiv2::clear_metadata(&tmpfile).await?;
tracing::debug!("Validated");
Ok(image_webp())
}
(Some(format), _) => {
let newfile = tmp_file();
crate::safe_create_parent(&newfile).await?;
crate::magick::convert_file(&tmpfile, &newfile, format.clone()).await?;
actix_fs::rename(newfile, tmpfile).await?;
Ok(format.to_mime())
}
}
}

View file

@ -1,218 +0,0 @@
use crate::{config::Format, error::UploadError, tmp_file};
use actix_web::web;
use magick_rust::MagickWand;
use rexiv2::{MediaType, Metadata};
use std::path::PathBuf;
use tracing::{debug, error, instrument, warn, Span};
pub(crate) mod transcode;
use self::transcode::{Error as TranscodeError, Target};
pub(crate) trait Op {
fn op<F, T>(&self, f: F) -> Result<T, UploadError>
where
F: Fn(&Self) -> Result<T, &'static str>;
fn op_mut<F, T>(&mut self, f: F) -> Result<T, UploadError>
where
F: Fn(&mut Self) -> Result<T, &'static str>;
}
impl Op for MagickWand {
fn op<F, T>(&self, f: F) -> Result<T, UploadError>
where
F: Fn(&Self) -> Result<T, &'static str>,
{
match f(self) {
Ok(t) => Ok(t),
Err(e) => {
if let Ok(e) = self.get_exception() {
error!("WandError: {}", e.0);
Err(UploadError::Wand(e.0))
} else {
Err(UploadError::Wand(e.to_owned()))
}
}
}
}
fn op_mut<F, T>(&mut self, f: F) -> Result<T, UploadError>
where
F: Fn(&mut Self) -> Result<T, &'static str>,
{
match f(self) {
Ok(t) => Ok(t),
Err(e) => {
if let Ok(e) = self.get_exception() {
error!("WandError: {}", e.0);
Err(UploadError::Wand(e.0))
} else {
Err(UploadError::Wand(e.to_owned()))
}
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum GifError {
#[error("{0}")]
Decode(#[from] TranscodeError),
#[error("{0}")]
Io(#[from] std::io::Error),
}
pub(crate) fn image_webp() -> mime::Mime {
"image/webp".parse().unwrap()
}
pub(crate) fn video_mp4() -> mime::Mime {
"video/mp4".parse().unwrap()
}
pub(crate) fn ptos(p: &PathBuf) -> Result<String, UploadError> {
Ok(p.to_str().ok_or(UploadError::Path)?.to_owned())
}
fn validate_format(file: &str, format: &str) -> Result<(), UploadError> {
let wand = MagickWand::new();
debug!("reading");
wand.op(|w| w.read_image(file))?;
if wand.op(|w| w.get_image_format())? != format {
return Err(UploadError::UnsupportedFormat);
}
Ok(())
}
fn safe_create_parent(path: &PathBuf) -> Result<(), UploadError> {
if let Some(path) = path.parent() {
std::fs::create_dir_all(path)?;
}
Ok(())
}
// import & export image using the image crate
#[instrument]
pub(crate) async fn validate_image(
tmpfile: PathBuf,
prescribed_format: Option<Format>,
) -> Result<mime::Mime, UploadError> {
let tmpfile_str = ptos(&tmpfile)?;
let span = Span::current();
let content_type = web::block(move || {
let entered = span.enter();
let meta = Metadata::new_from_path(&tmpfile)?;
let content_type = match (prescribed_format, meta.get_media_type()?) {
(_, MediaType::Gif) => {
let newfile = tmp_file();
safe_create_parent(&newfile)?;
validate_frames(&tmpfile, &newfile)?;
video_mp4()
}
(Some(Format::Jpeg), MediaType::Jpeg) | (None, MediaType::Jpeg) => {
validate_format(&tmpfile_str, "JPEG")?;
meta.clear();
meta.save_to_file(&tmpfile)?;
mime::IMAGE_JPEG
}
(Some(Format::Png), MediaType::Png) | (None, MediaType::Png) => {
validate_format(&tmpfile_str, "PNG")?;
meta.clear();
meta.save_to_file(&tmpfile)?;
mime::IMAGE_PNG
}
(Some(Format::Webp), MediaType::Other(webp)) | (None, MediaType::Other(webp))
if webp == "image/webp" =>
{
let newfile = tmp_file();
safe_create_parent(&newfile)?;
let newfile_str = ptos(&newfile)?;
// clean metadata by writing new webp, since exiv2 doesn't support webp yet
{
let wand = MagickWand::new();
debug!("reading");
wand.op(|w| w.read_image(&tmpfile_str))?;
if wand.op(|w| w.get_image_format())? != "WEBP" {
return Err(UploadError::UnsupportedFormat);
}
if let Err(e) = wand.op(|w| w.write_image(&newfile_str)) {
std::fs::remove_file(&newfile_str)?;
return Err(e);
}
}
std::fs::rename(&newfile, &tmpfile)?;
image_webp()
}
(Some(format), _) => {
let newfile = tmp_file();
safe_create_parent(&newfile)?;
let newfile_str = ptos(&newfile)?;
{
let mut wand = MagickWand::new();
debug!("reading: {}", tmpfile_str);
wand.op(|w| w.read_image(&tmpfile_str))?;
wand.op_mut(|w| w.set_image_format(format.to_magick_format()))?;
debug!("writing: {}", newfile_str);
if let Err(e) = wand.op(|w| w.write_image(&newfile_str)) {
std::fs::remove_file(&newfile_str)?;
return Err(e);
}
}
std::fs::rename(&newfile, &tmpfile)?;
format.to_mime()
}
(_, MediaType::Other(mp4)) if mp4 == "video/mp4" || mp4 == "video/quicktime" => {
let newfile = tmp_file();
safe_create_parent(&newfile)?;
validate_frames(&tmpfile, &newfile)?;
video_mp4()
}
(_, media_type) => {
warn!("Unsupported media type, {}", media_type);
return Err(UploadError::UnsupportedFormat);
}
};
drop(entered);
Ok(content_type) as Result<mime::Mime, UploadError>
})
.await??;
Ok(content_type)
}
#[instrument]
fn validate_frames(from: &PathBuf, to: &PathBuf) -> Result<(), GifError> {
debug!("Transmuting GIF");
if let Err(e) = self::transcode::transcode(from, to, Target::Mp4) {
std::fs::remove_file(to)?;
return Err(e.into());
}
std::fs::rename(to, from)?;
Ok(())
}

View file

@ -1,314 +0,0 @@
use ffmpeg_next::{
self, codec, filter, format, frame, media,
util::{format::pixel::Pixel, rational::Rational},
};
use std::path::Path;
#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
#[error("No video stream present")]
MissingVideo,
#[error("Input format is not supported")]
UnsupportedFormat,
#[error("No frame-rate present in input video")]
FrameRate,
#[error("Filter {0} should have been set up by now")]
MissingFilter(&'static str),
#[error("{0}")]
Transcode(#[from] ffmpeg_next::Error),
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum Target {
Mp4,
Jpeg,
#[allow(dead_code)]
Png,
}
impl Target {
fn name(&self) -> &'static str {
match self {
Target::Mp4 => "mp4",
Target::Jpeg => "image2",
Target::Png => "image2",
}
}
fn codec(&self) -> codec::id::Id {
match self {
Target::Mp4 => codec::id::Id::H264,
Target::Jpeg => codec::id::Id::MJPEG,
Target::Png => codec::id::Id::PNG,
}
}
fn frames(&self) -> Option<usize> {
match self {
Target::Mp4 => None,
Target::Jpeg => Some(1),
Target::Png => Some(1),
}
}
}
fn pixel_value(pixel: Pixel) -> i32 {
let av_px_fmt: ffmpeg_sys_next::AVPixelFormat = pixel.into();
unsafe { std::mem::transmute::<_, i32>(av_px_fmt) }
}
fn to_even(num: u32) -> u32 {
if num % 2 == 0 {
num
} else {
num - 1
}
}
fn filter(
decoder: &codec::decoder::Video,
encoder: &codec::encoder::Video,
) -> Result<filter::Graph, Error> {
let mut filter = filter::Graph::new();
let aspect = Rational::new(
to_even(decoder.width()) as i32,
to_even(decoder.height()) as i32,
)
.reduce();
let av_px_fmt = pixel_value(decoder.format());
let args = format!(
"video_size={width}x{height}:pix_fmt={pix_fmt}:time_base={time_base_num}/{time_base_den}:pixel_aspect={pix_aspect_num}/{pix_aspect_den}",
width=to_even(decoder.width()),
height=to_even(decoder.height()),
pix_fmt=av_px_fmt,
time_base_num=decoder.time_base().numerator(),
time_base_den=decoder.time_base().denominator(),
pix_aspect_num=aspect.numerator(),
pix_aspect_den=aspect.denominator(),
);
let buffer = filter::find("buffer").ok_or(Error::MissingFilter("buffer"))?;
filter.add(&buffer, "in", &args)?;
let buffersink = filter::find("buffersink").ok_or(Error::MissingFilter("buffersink"))?;
let mut out = filter.add(&buffersink, "out", "")?;
out.set_pixel_format(encoder.format());
filter.output("in", 0)?.input("out", 0)?.parse("null")?;
filter.validate()?;
println!("{}", filter.dump());
if let Some(codec) = encoder.codec() {
if !codec
.capabilities()
.contains(codec::capabilities::Capabilities::VARIABLE_FRAME_SIZE)
{
filter
.get("out")
.ok_or(Error::MissingFilter("out"))?
.sink()
.set_frame_size(encoder.frame_size());
}
}
Ok(filter)
}
struct Transcoder {
stream: usize,
filter: filter::Graph,
decoder: codec::decoder::Video,
encoder: codec::encoder::Video,
}
impl Transcoder {
fn decode(
&mut self,
packet: &ffmpeg_next::Packet,
decoded: &mut frame::Video,
) -> Result<bool, ffmpeg_next::Error> {
self.decoder.decode(packet, decoded)
}
fn add_frame(&mut self, decoded: &frame::Video) -> Result<(), Error> {
self.filter
.get("in")
.ok_or(Error::MissingFilter("out"))?
.source()
.add(decoded)?;
Ok(())
}
fn encode(
&mut self,
decoded: &mut frame::Video,
encoded: &mut ffmpeg_next::Packet,
octx: &mut format::context::Output,
in_time_base: Rational,
out_time_base: Rational,
) -> Result<(), Error> {
while let Ok(()) = self
.filter
.get("out")
.ok_or(Error::MissingFilter("out"))?
.sink()
.frame(decoded)
{
if let Ok(true) = self.encoder.encode(decoded, encoded) {
encoded.set_stream(0);
encoded.rescale_ts(in_time_base, out_time_base);
encoded.write_interleaved(octx)?;
}
}
Ok(())
}
fn flush(
&mut self,
decoded: &mut frame::Video,
encoded: &mut ffmpeg_next::Packet,
octx: &mut format::context::Output,
in_time_base: Rational,
out_time_base: Rational,
) -> Result<(), Error> {
self.filter
.get("in")
.ok_or(Error::MissingFilter("in"))?
.source()
.flush()?;
self.encode(decoded, encoded, octx, in_time_base, out_time_base)?;
while let Ok(true) = self.encoder.flush(encoded) {
encoded.set_stream(0);
encoded.rescale_ts(in_time_base, out_time_base);
encoded.write_interleaved(octx)?;
}
Ok(())
}
}
fn transcoder(
ictx: &mut format::context::Input,
octx: &mut format::context::Output,
target: Target,
) -> Result<Transcoder, Error> {
let input = ictx
.streams()
.best(media::Type::Video)
.ok_or(Error::MissingVideo)?;
let mut decoder = input.codec().decoder().video()?;
let codec_id = target.codec();
let codec = ffmpeg_next::encoder::find(codec_id)
.ok_or(Error::UnsupportedFormat)?
.video()?;
let global = octx
.format()
.flags()
.contains(format::flag::Flags::GLOBAL_HEADER);
decoder.set_parameters(input.parameters())?;
let mut output = octx.add_stream(codec)?;
let mut encoder = output.codec().encoder().video()?;
if global {
encoder.set_flags(codec::flag::Flags::GLOBAL_HEADER);
}
encoder.set_format(
codec
.formats()
.ok_or(Error::UnsupportedFormat)?
.next()
.ok_or(Error::UnsupportedFormat)?,
);
encoder.set_bit_rate(decoder.bit_rate());
encoder.set_max_bit_rate(decoder.max_bit_rate());
encoder.set_width(to_even(decoder.width()));
encoder.set_height(to_even(decoder.height()));
encoder.set_bit_rate(decoder.bit_rate());
encoder.set_max_bit_rate(decoder.max_bit_rate());
encoder.set_time_base(decoder.frame_rate().ok_or(Error::FrameRate)?.invert());
output.set_time_base(decoder.time_base());
let encoder = encoder.open_as(codec)?;
output.set_parameters(&encoder);
let filter = filter(&decoder, &encoder)?;
Ok(Transcoder {
stream: input.index(),
filter,
decoder,
encoder,
})
}
pub(crate) fn transcode<P, Q>(input: P, output: Q, target: Target) -> Result<(), Error>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let mut ictx = format::input(&input)?;
let mut octx = format::output_as(&output, target.name())?;
let mut transcoder = transcoder(&mut ictx, &mut octx, target)?;
octx.write_header()?;
let in_time_base = transcoder.decoder.time_base();
let out_time_base = octx.stream(0).ok_or(Error::MissingVideo)?.time_base();
let mut decoded = frame::Video::empty();
let mut encoded = ffmpeg_next::Packet::empty();
let mut count = 0;
for (stream, mut packet) in ictx.packets() {
if stream.index() == transcoder.stream {
packet.rescale_ts(stream.time_base(), in_time_base);
if let Ok(true) = transcoder.decode(&packet, &mut decoded) {
let timestamp = decoded.timestamp();
decoded.set_pts(timestamp);
transcoder.add_frame(&decoded)?;
transcoder.encode(
&mut decoded,
&mut encoded,
&mut octx,
in_time_base,
out_time_base,
)?;
count += 1;
}
}
if target.frames().map(|f| count >= f).unwrap_or(false) {
break;
}
}
transcoder.flush(
&mut decoded,
&mut encoded,
&mut octx,
in_time_base,
out_time_base,
)?;
octx.write_trailer()?;
Ok(())
}