use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum MediaError {
#[error("media.storage.bucket is required when backend = \"s3\"")]
MissingBucket,
#[error(
"partial S3 credentials: media.storage.access_key_id and \
media.storage.secret_access_key must both be set, or neither"
)]
PartialS3Credentials,
#[error(
"media.storage.public_base_url is required for a generic (non-Tigris) \
S3 backend (bucket `{bucket}`): set it to the bucket's public base URL"
)]
MissingPublicBaseUrl {
bucket: String,
},
#[error("invalid storage key `{key}`: path segments must not be `.`, `..`, or empty")]
InvalidKeySegment {
key: String,
},
#[error("failed to read local file `{path}`: {source}")]
LocalRead {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to upload s3://{bucket}/{key}: {message}")]
S3Upload {
bucket: String,
key: String,
message: String,
},
#[error("failed to delete s3://{bucket}/{key}: {message}")]
S3Delete {
bucket: String,
key: String,
message: String,
},
#[error("{message}")]
ReplayRefused {
message: String,
},
#[error("ffmpeg source recording does not exist: {path}")]
FfmpegSourceMissing {
path: String,
},
#[error("failed to start ffmpeg `{bin}`: {source}")]
FfmpegSpawn {
bin: String,
#[source]
source: std::io::Error,
},
#[error("ffmpeg exited with status {code}: {stderr_tail}")]
FfmpegNonZeroExit {
code: String,
stderr_tail: String,
},
#[error("ffmpeg exceeded its deadline")]
FfmpegTimeout,
#[error("failed to write ffmpeg concat list `{path}`: {source}")]
ConcatList {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to access ffmpeg output `{path}`: {source}")]
FfmpegOutputIo {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to write media artifact `{path}`: {source}")]
ArtifactWrite {
path: String,
#[source]
source: std::io::Error,
},
#[error("media encode task did not complete: {message}")]
EncodeTaskJoin {
message: String,
},
}
pub(crate) const STDERR_TAIL_MAX_BYTES: usize = 2048;
#[must_use]
pub(crate) fn stderr_tail(bytes: &[u8]) -> String {
let text = String::from_utf8_lossy(bytes);
if text.len() <= STDERR_TAIL_MAX_BYTES {
return text.into_owned();
}
let mut start = text.len() - STDERR_TAIL_MAX_BYTES;
while start < text.len() && !text.is_char_boundary(start) {
start += 1;
}
text[start..].to_owned()
}
#[cfg(test)]
mod stderr_tail_tests {
use super::{STDERR_TAIL_MAX_BYTES, stderr_tail};
#[test]
fn short_stderr_is_returned_verbatim() {
assert_eq!(stderr_tail(b"boom"), "boom");
}
#[test]
fn invalid_utf8_does_not_panic() {
let out = stderr_tail(&[0xff, b'x']);
assert!(out.contains('x'));
}
#[test]
fn long_stderr_is_bounded_to_the_tail() {
let big = "a".repeat(STDERR_TAIL_MAX_BYTES * 3);
let out = stderr_tail(big.as_bytes());
assert_eq!(out.len(), STDERR_TAIL_MAX_BYTES);
assert!(big.ends_with(&out));
}
#[test]
fn tail_boundary_never_splits_a_codepoint() {
let big = "€".repeat(STDERR_TAIL_MAX_BYTES);
let out = stderr_tail(big.as_bytes());
assert!(out.len() <= STDERR_TAIL_MAX_BYTES);
assert!(big.ends_with(&out));
}
}