Compare commits

...

6 Commits

7 changed files with 498 additions and 80 deletions

View File

@ -29,3 +29,7 @@ features = ["tls"]
version = "0.24.3"
default-features = false
features = ["gif", "jpeg", "ico", "png", "tiff", "webp", "bmp", "hdr"]
[profile.release]
lto = true
codegen-units = 1

View File

@ -1,8 +1,17 @@
syntax = "proto3";
enum Format {
WEBP = 0;
AVIF = 1;
JPG = 2;
PNG = 3;
GIF = 4;
}
message Image {
bytes key = 1;
string full_url = 2;
string thumb_url = 3;
string blurhash = 4;
Format format = 5;
}

View File

@ -2,7 +2,7 @@ use core::fmt;
use std::{path::{PathBuf, Path}, io};
#[derive(Debug)]
pub enum AviaryError {
pub enum AviaryUploadError {
/// One of the files passed by the user simply did not exist
///
/// The attached path is the file which didn't exist
@ -52,11 +52,11 @@ pub enum AviaryError {
BadServerParameter,
}
impl AviaryError {
pub fn from_open_error(e: io::ErrorKind, location: &Path) -> AviaryError {
impl AviaryUploadError {
pub fn from_open_error(e: io::ErrorKind, location: &Path) -> AviaryUploadError {
match e {
io::ErrorKind::NotFound => AviaryError::FileDNE(location.to_owned()),
io::ErrorKind::PermissionDenied => AviaryError::ReadPermissionDenied(location.to_owned()),
io::ErrorKind::NotFound => AviaryUploadError::FileDNE(location.to_owned()),
io::ErrorKind::PermissionDenied => AviaryUploadError::ReadPermissionDenied(location.to_owned()),
_ => panic!(
"Received an error kind that should be impossible for {}: {:?}",
location.display(),
@ -65,36 +65,36 @@ impl AviaryError {
}
}
pub fn from_upload_error(err: ureq::Error) -> AviaryError {
pub fn from_upload_error(err: ureq::Error) -> AviaryUploadError {
match err {
ureq::Error::Status(code, msg) => AviaryError::ServerError(
ureq::Error::Status(code, msg) => AviaryUploadError::ServerError(
format!("Error code {} received from server: {}", code,
msg.into_string().unwrap_or(String::new()))),
ureq::Error::Transport(transport) => match transport.kind() {
ureq::ErrorKind::InvalidUrl =>
AviaryError::BadServerParameter,
AviaryUploadError::BadServerParameter,
ureq::ErrorKind::Dns =>
AviaryError::ConnectionError(
AviaryUploadError::ConnectionError(
format!("DNS issue: {}", transport.message().unwrap_or(""))),
ureq::ErrorKind::Io =>
AviaryError::ConnectionError(
AviaryUploadError::ConnectionError(
format!("IO issue: {}", transport.message().unwrap_or(""))),
ureq::ErrorKind::ConnectionFailed =>
AviaryError::ConnectionError(
AviaryUploadError::ConnectionError(
format!("Connection issue: {}", transport.message().unwrap_or(""))),
ureq::ErrorKind::TooManyRedirects =>
AviaryError::ServerError("Too many redirects".to_owned()),
AviaryUploadError::ServerError("Too many redirects".to_owned()),
ureq::ErrorKind::BadHeader =>
AviaryError::ServerError("Invalid header from server".to_owned()),
AviaryUploadError::ServerError("Invalid header from server".to_owned()),
ureq::ErrorKind::BadStatus =>
AviaryError::ServerError("Invalid status from server".to_owned()),
AviaryUploadError::ServerError("Invalid status from server".to_owned()),
unk => panic!("Unexpected transport error kind {unk}:\n{transport}")
},
}
}
}
impl fmt::Display for AviaryError {
impl fmt::Display for AviaryUploadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\x1b[31mError!\x1b[0m\n")?;
match self {
@ -171,3 +171,208 @@ impl fmt::Display for AviaryError {
}
}
}
#[derive(Debug)]
pub enum AviaryDownloadError {
/// Indicates that the key passed to the program was malformed
///
/// There are two possibities here:
/// - The key was not valid base 64
/// - The key was valid, but was not 32 bytes
///
/// Which of these two was the case is designated by the associated boolean. If
/// `true`, the key was invalid base64. If `false`, it was the wrong length.
///
/// Note that this error should only be used for keys passed to the program by the user. If a
/// key of the wrong length is listed in the index file, this should be considered a bad
///
/// Handling: Sugguest that the user check to make sure that they copied the base64
/// key correctly, and that it is encoded in base64.
MalformedKey(bool),
/// The server encountered an error or misbehaved
///
/// If this was due to a bad code, the associated data will be the error code, along
/// with the response from the server, as a String.
///
/// If this was due to misbehavior, there will be no code, and the String will be a
/// short explaination of what went wrong. Note that misbehavior includes any *unexpected*
/// behavior, not necessarily indicating that the server is broken.
///
/// Handling: Inform the user that something went wrong with the server, and that
/// they should try another instance and/or contact the instance admin.
ServerError(Option<u16>, String),
/// The filesystem denied access to a path with a permission denied error
///
/// This will either be permission denied to create a directory or permission denied to
/// create/write to a file
///
/// The associated path is the path for which permission was denied
///
/// Handling: Ask the user to check that they own the output directory and have
/// permission for it. Sugguest trying a different directory.
PermissionDenied(PathBuf),
/// Some uncommon filesystem error occured
///
/// This is a sort of wildcard error for filesystem errors which aren't likely to happen under
/// normal circumstances. Examples include:
/// - The disk is ejected while a write operation is ongoing
/// - The filesystem does not support creating directories
/// - The filesystem operation timed out
///
/// The associated error and path are the error that triggered this and the path the operation
/// occured for.
///
/// Handling: Assume the user is a more technical user and state as much. Give the error
/// details as well as the path. In case the user is not a technical user, provide them with a
/// link to get help / report a bug.
FilesystemError(io::Error, PathBuf),
/// The server URL provided by the user was invalid
///
/// Handling: Halt execution and alert the user that the server url they provided was
/// invalid, including a couple examples
BadServerParameter,
/// Some network error prevented us from connecting to the server
///
/// No evidence exists to indicate that this was the fault of the server rather than
/// us.
///
/// Handling: Ask the user to validate that they can connect to the internet, and
/// sugguest that they try manually entering the URL into the browser to see if it
/// works.
ConnectionError(String),
/// Missing or expired index
///
/// Indicates that the server reported that the index file did not exist (404). This could be
/// due to using the wrong server URL or miscopying the file/index ID. A similar error can
/// also exist when the server has expired the index file and it has not yet been overwritten
/// by a new file.
///
/// Handling: Ask that the user check to make sure they copied the URL correctly and that
/// they've specified the right backend server URL. If they have, alert them that it's likely
/// that the gallery has expired.
MissingIndex,
/// One (or more) of the images in the gallery has expired
///
/// Occurs when the index points to a URL which the server claims does not exist. This almost
/// always indicates that the image has expired. Other possibilities include:
/// - The index was incorrect even *before* it was encrypted and uploaded
/// - The key was compramised, and the server took advantage of this to substitute its own
/// index file, but then gave faulty urls?? don't know why they'd do that
/// - ???
///
/// Handling: Ask that the user check to make sure they copied the URL correctly and that
/// they've specified the right backend server URL. If they have, alert them that it's likely
/// that the gallery has expired.
ExpiredImage,
/// Indicates that there was a mismatch between the key and the cyphertext
///
/// This could mean that the key was correct, but the cyphertext was invalid (like if it wasn't
/// encrypted data at all), or it could mean that the key (although still 32 bytes of base64)
/// didn't match up with the cyphertext.
///
/// An important reason this might occur is if the gallery expired and the server re-assigned
/// the ID to another file, and both used the .bin extension.
///
/// Handling: If this occurs for an image where the URL and key are both provided by the
/// index, it should always be interpreted as an expiration. If it occurs for an index, ask
/// the user to verify that they key and file ID were copied correctly, and if they are
/// correct, then the gallery expired.
KeyMismatch,
/// Indicates that the index did not conform to the protobuf spec
///
/// This is perhaps one of the least likely errors to occur in typical usage. This indicates
/// that the index violated some basic guarantee in some way. This typically corresponds to a
/// parse error. However, this isn't simple corruption, as the index was
/// still correctly encrypted and the encryption tag matched up.
///
/// Some examples for events which could trigger this error:
/// - The index is not valid protobuf
/// - The index specifies a key which is not 32 bytes in length
/// - The index specified a file ID which contains illegal characters
///
/// The most likely explanations for this are as follows:
/// - The key/file id pair provided to program are valid, but point to an image rather than a
/// gallery. This is unlikely because this information is never exposed to the user.
/// - These are valid key/file id pairs, but are intended for a different project which uses
/// the same encryption scheme, stores different data / doesn't use a protobuf index.
/// - A very poorly behaved client generated this index
///
/// The associated str will provide a more specific explanation for exactly what went wrong.
///
/// Handling: This is incredibly unlikely to happen to an unsuspecting user. Provide the user
/// with a detailed technical description of what happened, as well as a link to report a bug,
/// in the event that they are a non-technical user who, against all odds, encountered this
/// problem.
CorruptIndex(&'static str),
/// A file already exists in the destination folder and we don't want to overwrite it
///
/// Handling: Alert the user that the file exists and that we won't overwrite it. Their
/// options are:
/// - Choose a new destination directory
/// - Remove the existing files
/// - Add the `--force/-f` option to force the program to overwrite the files
NotOverwriting(PathBuf),
/// Cancelled
///
/// The user cancelled the operation, e.g. C^c
///
/// Handling: Exit politely and state the reason
Cancelled,
}
impl AviaryDownloadError {
pub fn from_download_error(
is_index: bool
) -> impl Fn(ureq::Error) -> AviaryDownloadError {
move|err: ureq::Error| {
match err {
ureq::Error::Status(code, msg) => match (code/100, code%100) {
(4, 04) =>
if is_index {
AviaryDownloadError::MissingIndex
} else {
AviaryDownloadError::ExpiredImage
}
_ => AviaryDownloadError::ServerError(
Some(code),
msg.into_string().unwrap_or(String::new())
),
},
ureq::Error::Transport(transport) => match transport.kind() {
ureq::ErrorKind::InvalidUrl =>
if is_index {
AviaryDownloadError::BadServerParameter
} else {
AviaryDownloadError::CorruptIndex("Index lists bad characters in file ID")
},
ureq::ErrorKind::Dns =>
AviaryDownloadError::ConnectionError(
format!("DNS issue: {}", transport.message().unwrap_or(""))),
ureq::ErrorKind::Io =>
AviaryDownloadError::ConnectionError(
format!("IO issue: {}", transport.message().unwrap_or(""))),
ureq::ErrorKind::ConnectionFailed =>
AviaryDownloadError::ConnectionError(
format!("Connection issue: {}", transport.message().unwrap_or(""))),
ureq::ErrorKind::TooManyRedirects =>
AviaryDownloadError::ServerError(Some(302), "Too many redirects".to_owned()),
ureq::ErrorKind::BadHeader =>
AviaryDownloadError::ServerError(None, "Invalid header from server".to_owned()),
ureq::ErrorKind::BadStatus =>
AviaryDownloadError::ServerError(None, "Invalid status from server".to_owned()),
unk => panic!("Unexpected transport error kind {unk}:\n{transport}")
},
}
}}
}

View File

@ -6,16 +6,19 @@ mod protobuf;
mod errors;
use std::{borrow::Cow, io::stdout};
use std::io::{Read, Write};
use std::io::{Read, Write, self};
use std::fs::{File, self, OpenOptions};
use std::path::Path;
use errors::AviaryError;
use errors::AviaryUploadError;
use itertools::{Itertools, Either};
use parse::{CreateArgs, Command, DownloadArgs};
use ::protobuf::Message;
use sanitise_file_name::sanitise_with_options;
use crate::errors::AviaryDownloadError;
use crate::protobuf::image::Format;
fn trim_url<'a>(base_url: &str, url: &'a str) -> Option<&'a str> {
if url.starts_with(base_url) {
let shortened = url
@ -43,7 +46,7 @@ fn main() {
};
match args.command {
Command::Create(create_args) => create(&*full_server, create_args),
Command::Download(download_args) => download(&*full_server, download_args),
Command::Download(download_args) => download(&*full_server, download_args).unwrap(),
}
}
@ -53,7 +56,7 @@ fn create(server: &str, args: CreateArgs) {
.map(Path::new)
.map(|path| File::open(path)
.map(|file| (path, file))
.map_err(|e| AviaryError::from_open_error(e.kind(), &path))
.map_err(|e| AviaryUploadError::from_open_error(e.kind(), &path))
)
.partition_result();
@ -61,8 +64,8 @@ fn create(server: &str, args: CreateArgs) {
println!(" \x1b[31mError!\x1b[0m");
let (nonexistant, noread): (Vec<_>, Vec<_>) = errors.iter()
.partition_map(|e| match e {
AviaryError::FileDNE(path) => Either::Left(path),
AviaryError::ReadPermissionDenied(path) => Either::Right(path),
AviaryUploadError::FileDNE(path) => Either::Left(path),
AviaryUploadError::ReadPermissionDenied(path) => Either::Right(path),
other => panic!("This error should not be possible! {other:?}")
});
if !nonexistant.is_empty() {
@ -84,47 +87,49 @@ fn create(server: &str, args: CreateArgs) {
let mut buff = Vec::with_capacity(file.metadata()?.len() as usize);
file.read_to_end(&mut buff)?;
Ok((path, buff))
})().map_err(|e| AviaryError::StreamReadError(path.to_owned(), e))
})().map_err(|e| AviaryUploadError::StreamReadError(path.to_owned(), e))
)
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Thumbnailing... ") })
.inspect(|_| drop(stdout().flush()))
.map(|r| r.and_then(|(path, raw_dat)| {
let (thumbnail, blurhash) = thumbnailing::thumbnail(&raw_dat)
.map_err(|_| AviaryError::ImageFormatError(path.to_owned()))?;
Ok((raw_dat, thumbnail, blurhash))
let (full, thumbnail, blurhash, format) = thumbnailing::thumbnail(&raw_dat)
.map_err(|_| AviaryUploadError::ImageFormatError(path.to_owned()))?;
Ok((full.map(Either::Right).unwrap_or(Either::Left(raw_dat)), thumbnail, blurhash, format))
}))
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Encrypting... ")})
.inspect(|_| drop(stdout().flush()))
.map_ok(|(raw_dat, thumbnail, blurhash)| {
.map_ok(|(raw_dat, thumbnail, blurhash, format)| {
let key = crypto::make_key();
(
key,
crypto::encrypt(&key, &raw_dat),
crypto::encrypt(&key, &thumbnail),
blurhash
blurhash,
format
)
})
.inspect(|r| if r.is_ok() { print!("\x1b[32mDone!\n\x1b[37m└─\x1b[0m Uploading... ")})
.inspect(|_| drop(stdout().flush()))
.map(|r| r.and_then(|(key, full_img, thumb, blurhash)|
.map(|r| r.and_then(|(key, full_img, thumb, blurhash, format)|
upload::put_data(&agent, server, &thumb)
.and_then(|thumb_url|
upload::put_data(&agent, server, &full_img)
.map(|full_url| (key, full_url, thumb_url, blurhash)))
.map_err(AviaryError::from_upload_error)
.map(|full_url| (key, full_url, thumb_url, blurhash, format)))
.map_err(AviaryUploadError::from_upload_error)
))
.map(|r| r.and_then(|(key, full_url, thumb_url, blurhash)| {
.map(|r| r.and_then(|(key, full_url, thumb_url, blurhash, format)| {
let full_trimmed = trim_url(server, &full_url);
let thmb_trimmed = trim_url(server, &thumb_url);
if let (Some(full_url), Some(thmb_url)) = (full_trimmed, thmb_trimmed) {
Ok((key, full_url.to_owned(), thmb_url.to_owned(), blurhash))
Ok((key, full_url.to_owned(), thmb_url.to_owned(), blurhash, format))
} else {
Err(AviaryError::ServerError(format!("Received bad response from server: {}", full_url)))
Err(AviaryUploadError::ServerError(format!("Received bad response from server: {}", full_url)))
}
}))
.inspect(|r| if r.is_ok() { println!("\x1b[32mDone!\n")})
.map_ok(|(key, full_url, thumb_url, blurhash)| protobuf::image::Image {
.map_ok(|(key, full_url, thumb_url, blurhash, format)| protobuf::image::Image {
key: key.into(),
format: format.into(),
full_url, thumb_url, blurhash,
special_fields: Default::default()
})
@ -134,7 +139,7 @@ fn create(server: &str, args: CreateArgs) {
images: image_info,
title: args.title,
desc: None,
special_fields: Default::default()
special_fields: Default::default(),
};
let index_key = crypto::make_key();
let encrypted_index = crypto::encrypt(
@ -145,7 +150,7 @@ fn create(server: &str, args: CreateArgs) {
let encoded_key = base64::encode(index_key);
print!("\x1b[0mUploading index... ");
upload::put_data(&agent, server, &encrypted_index)
.map_err(|e| AviaryError::from_upload_error(e))
.map_err(|e| AviaryUploadError::from_upload_error(e))
.map(|url| format!("{}#{}", url.trim().trim_end_matches(".bin"), &encoded_key))
});
match index_url {
@ -174,48 +179,118 @@ const SANITIZATION_OPTIONS: sanitise_file_name::Options<fn(char) -> Option<char>
six_measures_of_barley: "Unnamed-Album"
};
fn download(server: &str, args: DownloadArgs) {
fn download(server: &str, args: DownloadArgs) -> Result<(), AviaryDownloadError> {
let mut download_buffer = Vec::with_capacity(5_000_000);
let mut decrypt_buffer = Vec::with_capacity(5_000_000);
let index_url = format!("{}/{}.bin", server, args.id);
let key: [u8; 32] = base64::decode(args.key)
.expect("key is malformed b64")
.map_err(|_| AviaryDownloadError::MalformedKey(true))?
.try_into()
.expect("key is the wrong size");
.map_err(|_| AviaryDownloadError::MalformedKey(false))?;
let download_agent = upload::get_agent();
print!("Downloading index... ");
let encrypted_index = upload::get_data(&download_agent, &index_url, 500_000, &mut download_buffer)
.expect("GET failed");
.map_err(AviaryDownloadError::from_download_error(true))?;
let serialized_index = crypto::decrypt(&key, &encrypted_index, &mut decrypt_buffer)
.expect("wrong key or corrupt index");
.ok_or(AviaryDownloadError::KeyMismatch)?;
let index = protobuf::index::Index::parse_from_bytes(&serialized_index)
.expect("malformed index");
.map_err(|_| AviaryDownloadError::CorruptIndex("Index was not a valid protobuf structure"))?;
let image_count = index.images.len();
println!(
"\x1b[32mDone!\x1b[0m\n\nGallery Name: {}\n Image Count: {}\n\n(desc not implemented)\n",
index.title.as_ref().map(String::as_str).unwrap_or("<not set>"),
image_count
);
let dest_dir: Cow<Path> = args.output.map(Cow::Owned)
.unwrap_or_else(||
index.title.map(|title|
Cow::Owned(sanitise_with_options(&title, &SANITIZATION_OPTIONS).into()))
.unwrap_or(Cow::Borrowed("Unnamed-Album".as_ref())));
fs::create_dir_all(&dest_dir).expect("Failed to create destination directory");
fs::create_dir_all(&dest_dir).map_err(|e| match e.kind() {
io::ErrorKind::PermissionDenied =>
AviaryDownloadError::PermissionDenied(dest_dir.as_ref().to_owned()),
_ => AviaryDownloadError::FilesystemError(e, dest_dir.as_ref().to_owned()),
})?;
for (indx, image) in index.images.into_iter().enumerate() {
let path = dest_dir.join(format!("{indx:03}.webp"));
let extension = image.format.enum_value().map(|f| match f {
Format::PNG => "png",
Format::WEBP => "webp",
Format::AVIF => "avif",
Format::JPG => "jpg",
Format::GIF => "gif",
}).unwrap_or("unk");
// Print a little blurhash
const BH_WIDTH: u32 = 12;
const BH_HEIGHT: u32 = 5;
let blur = blurhash::decode(&image.blurhash, BH_WIDTH, BH_HEIGHT, 1.0);
let mut blur = blur.iter();
for y in 0..BH_HEIGHT {
for _ in 0..BH_WIDTH {
let r = blur.next().unwrap();
let g = blur.next().unwrap();
let b = blur.next().unwrap();
let _ = blur.next().unwrap();
print!("\x1b[38;2;{r};{g};{b}m█");
}
match y {
1 => println!("\x1b[0m Image {}/{}", indx + 1, image_count),
2 => println!("\x1b[0m Format: {}", extension),
_ => println!(""),
}
}
print!("\x1b[37m├─\x1b[0m Reserving file... ");
stdout().flush().unwrap();
let path = dest_dir.join(format!("{indx:03}.{extension}"));
let mut dest_file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)
.expect("Failed to open");
let key = image.key.try_into().expect("Invalid key size");
.open(&path)
.map_err(|e| match e.kind() {
io::ErrorKind::PermissionDenied =>
AviaryDownloadError::PermissionDenied(path.clone()),
io::ErrorKind::AlreadyExists =>
AviaryDownloadError::NotOverwriting(path.clone()),
io::ErrorKind::Interrupted =>
AviaryDownloadError::Cancelled,
_ => AviaryDownloadError::FilesystemError(e, path.clone())
})?;
let key = image.key.try_into()
.map_err(|_|
AviaryDownloadError::CorruptIndex("Key of invalid length specified in index")
)?;
print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Dowloading... ");
stdout().flush().unwrap();
let encrypted_thumbnail = upload::get_data(
&download_agent,
&format!("{}/{}.bin", server, image.thumb_url),
&format!("{}/{}.bin", server, image.full_url),
7680_000,
&mut download_buffer
).expect("Failed to retrieve image data");
).map_err(AviaryDownloadError::from_download_error(false))?;
print!("\x1b[32mDone!\n\x1b[37m├─\x1b[0m Decrypting... ");
stdout().flush().unwrap();
let decrypted_thumb = crypto::decrypt(
&key,
encrypted_thumbnail,
&mut decrypt_buffer
).expect("Invalid image data referenced by index or bad key in index");
dest_file.write_all(&decrypted_thumb).expect("Failed to write to disk");
).ok_or(AviaryDownloadError::ExpiredImage)?;
print!("\x1b[32mDone!\n\x1b[37m└─\x1b[0m Saving... ");
stdout().flush().unwrap();
dest_file.write_all(&decrypted_thumb)
.map_err(|e| match e.kind() {
io::ErrorKind::PermissionDenied =>
AviaryDownloadError::PermissionDenied(path.clone()),
io::ErrorKind::Interrupted =>
AviaryDownloadError::Cancelled,
_ => AviaryDownloadError::FilesystemError(e, path.clone())
})?;
println!("\x1b[32mDone!\n");
}
Ok(())
}

View File

@ -37,6 +37,8 @@ pub struct Image {
pub thumb_url: ::std::string::String,
// @@protoc_insertion_point(field:Image.blurhash)
pub blurhash: ::std::string::String,
// @@protoc_insertion_point(field:Image.format)
pub format: ::protobuf::EnumOrUnknown<Format>,
// special fields
// @@protoc_insertion_point(special_field:Image.special_fields)
pub special_fields: ::protobuf::SpecialFields,
@ -54,7 +56,7 @@ impl Image {
}
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
let mut fields = ::std::vec::Vec::with_capacity(4);
let mut fields = ::std::vec::Vec::with_capacity(5);
let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>(
"key",
@ -76,6 +78,11 @@ impl Image {
|m: &Image| { &m.blurhash },
|m: &mut Image| { &mut m.blurhash },
));
fields.push(::protobuf::reflect::rt::v2::make_simpler_field_accessor::<_, _>(
"format",
|m: &Image| { &m.format },
|m: &mut Image| { &mut m.format },
));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<Image>(
"Image",
fields,
@ -106,6 +113,9 @@ impl ::protobuf::Message for Image {
34 => {
self.blurhash = is.read_string()?;
},
40 => {
self.format = is.read_enum_or_unknown()?;
},
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
@ -130,6 +140,9 @@ impl ::protobuf::Message for Image {
if !self.blurhash.is_empty() {
my_size += ::protobuf::rt::string_size(4, &self.blurhash);
}
if self.format != ::protobuf::EnumOrUnknown::new(Format::WEBP) {
my_size += ::protobuf::rt::int32_size(5, self.format.value());
}
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
@ -148,6 +161,9 @@ impl ::protobuf::Message for Image {
if !self.blurhash.is_empty() {
os.write_string(4, &self.blurhash)?;
}
if self.format != ::protobuf::EnumOrUnknown::new(Format::WEBP) {
os.write_enum(5, ::protobuf::EnumOrUnknown::value(&self.format))?;
}
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
@ -169,6 +185,7 @@ impl ::protobuf::Message for Image {
self.full_url.clear();
self.thumb_url.clear();
self.blurhash.clear();
self.format = ::protobuf::EnumOrUnknown::new(Format::WEBP);
self.special_fields.clear();
}
@ -178,6 +195,7 @@ impl ::protobuf::Message for Image {
full_url: ::std::string::String::new(),
thumb_url: ::std::string::String::new(),
blurhash: ::std::string::String::new(),
format: ::protobuf::EnumOrUnknown::from_i32(0),
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
@ -201,24 +219,107 @@ impl ::protobuf::reflect::ProtobufValue for Image {
type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage<Self>;
}
#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)]
// @@protoc_insertion_point(enum:Format)
pub enum Format {
// @@protoc_insertion_point(enum_value:Format.WEBP)
WEBP = 0,
// @@protoc_insertion_point(enum_value:Format.AVIF)
AVIF = 1,
// @@protoc_insertion_point(enum_value:Format.JPG)
JPG = 2,
// @@protoc_insertion_point(enum_value:Format.PNG)
PNG = 3,
// @@protoc_insertion_point(enum_value:Format.GIF)
GIF = 4,
}
impl ::protobuf::Enum for Format {
const NAME: &'static str = "Format";
fn value(&self) -> i32 {
*self as i32
}
fn from_i32(value: i32) -> ::std::option::Option<Format> {
match value {
0 => ::std::option::Option::Some(Format::WEBP),
1 => ::std::option::Option::Some(Format::AVIF),
2 => ::std::option::Option::Some(Format::JPG),
3 => ::std::option::Option::Some(Format::PNG),
4 => ::std::option::Option::Some(Format::GIF),
_ => ::std::option::Option::None
}
}
const VALUES: &'static [Format] = &[
Format::WEBP,
Format::AVIF,
Format::JPG,
Format::PNG,
Format::GIF,
];
}
impl ::protobuf::EnumFull for Format {
fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor {
static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new();
descriptor.get(|| file_descriptor().enum_by_package_relative_name("Format").unwrap()).clone()
}
fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor {
let index = *self as usize;
Self::enum_descriptor().value_by_index(index)
}
}
impl ::std::default::Default for Format {
fn default() -> Self {
Format::WEBP
}
}
impl Format {
fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData {
::protobuf::reflect::GeneratedEnumDescriptorData::new::<Format>("Format")
}
}
static file_descriptor_proto_data: &'static [u8] = b"\
\n\x0bimage.proto\"m\n\x05Image\x12\x10\n\x03key\x18\x01\x20\x01(\x0cR\
\x03key\x12\x19\n\x08full_url\x18\x02\x20\x01(\tR\x07fullUrl\x12\x1b\n\t\
thumb_url\x18\x03\x20\x01(\tR\x08thumbUrl\x12\x1a\n\x08blurhash\x18\x04\
\x20\x01(\tR\x08blurhashJ\x86\x02\n\x06\x12\x04\0\0\x07\x01\n\x08\n\x01\
\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x07\x01\n\n\n\x03\x04\
\0\x01\x12\x03\x02\x08\r\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x08\x16\n\
\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x08\r\n\x0c\n\x05\x04\0\x02\0\x01\
\x12\x03\x03\x0e\x11\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x14\x15\n\
\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x08\x1c\n\x0c\n\x05\x04\0\x02\x01\
\x05\x12\x03\x04\x08\x0e\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\x0f\
\x17\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04\x1a\x1b\n\x0b\n\x04\x04\0\
\x02\x02\x12\x03\x05\x08\x1d\n\x0c\n\x05\x04\0\x02\x02\x05\x12\x03\x05\
\x08\x0e\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\x05\x0f\x18\n\x0c\n\x05\
\x04\0\x02\x02\x03\x12\x03\x05\x1b\x1c\n\x0b\n\x04\x04\0\x02\x03\x12\x03\
\x06\x08\x1c\n\x0c\n\x05\x04\0\x02\x03\x05\x12\x03\x06\x08\x0e\n\x0c\n\
\x05\x04\0\x02\x03\x01\x12\x03\x06\x0f\x17\n\x0c\n\x05\x04\0\x02\x03\x03\
\x12\x03\x06\x1a\x1bb\x06proto3\
\n\x0bimage.proto\"\x8e\x01\n\x05Image\x12\x10\n\x03key\x18\x01\x20\x01(\
\x0cR\x03key\x12\x19\n\x08full_url\x18\x02\x20\x01(\tR\x07fullUrl\x12\
\x1b\n\tthumb_url\x18\x03\x20\x01(\tR\x08thumbUrl\x12\x1a\n\x08blurhash\
\x18\x04\x20\x01(\tR\x08blurhash\x12\x1f\n\x06format\x18\x05\x20\x01(\
\x0e2\x07.FormatR\x06format*7\n\x06Format\x12\x08\n\x04WEBP\x10\0\x12\
\x08\n\x04AVIF\x10\x01\x12\x07\n\x03JPG\x10\x02\x12\x07\n\x03PNG\x10\x03\
\x12\x07\n\x03GIF\x10\x04J\xa2\x04\n\x06\x12\x04\0\0\x10\x01\n\x08\n\x01\
\x0c\x12\x03\0\0\x12\n\n\n\x02\x05\0\x12\x04\x02\0\x08\x01\n\n\n\x03\x05\
\0\x01\x12\x03\x02\x05\x0b\n\x0b\n\x04\x05\0\x02\0\x12\x03\x03\x08\x11\n\
\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x03\x08\x0c\n\x0c\n\x05\x05\0\x02\0\
\x02\x12\x03\x03\x0f\x10\n\x0b\n\x04\x05\0\x02\x01\x12\x03\x04\x08\x11\n\
\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x04\x08\x0c\n\x0c\n\x05\x05\0\x02\
\x01\x02\x12\x03\x04\x0f\x10\n\x0b\n\x04\x05\0\x02\x02\x12\x03\x05\x08\
\x10\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\x05\x08\x0b\n\x0c\n\x05\x05\0\
\x02\x02\x02\x12\x03\x05\x0e\x0f\n\x0b\n\x04\x05\0\x02\x03\x12\x03\x06\
\x08\x10\n\x0c\n\x05\x05\0\x02\x03\x01\x12\x03\x06\x08\x0b\n\x0c\n\x05\
\x05\0\x02\x03\x02\x12\x03\x06\x0e\x0f\n\x0b\n\x04\x05\0\x02\x04\x12\x03\
\x07\x08\x10\n\x0c\n\x05\x05\0\x02\x04\x01\x12\x03\x07\x08\x0b\n\x0c\n\
\x05\x05\0\x02\x04\x02\x12\x03\x07\x0e\x0f\n\n\n\x02\x04\0\x12\x04\n\0\
\x10\x01\n\n\n\x03\x04\0\x01\x12\x03\n\x08\r\n\x0b\n\x04\x04\0\x02\0\x12\
\x03\x0b\x08\x16\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x0b\x08\r\n\x0c\n\
\x05\x04\0\x02\0\x01\x12\x03\x0b\x0e\x11\n\x0c\n\x05\x04\0\x02\0\x03\x12\
\x03\x0b\x14\x15\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x0c\x08\x1c\n\x0c\n\
\x05\x04\0\x02\x01\x05\x12\x03\x0c\x08\x0e\n\x0c\n\x05\x04\0\x02\x01\x01\
\x12\x03\x0c\x0f\x17\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x0c\x1a\x1b\n\
\x0b\n\x04\x04\0\x02\x02\x12\x03\r\x08\x1d\n\x0c\n\x05\x04\0\x02\x02\x05\
\x12\x03\r\x08\x0e\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x03\r\x0f\x18\n\x0c\
\n\x05\x04\0\x02\x02\x03\x12\x03\r\x1b\x1c\n\x0b\n\x04\x04\0\x02\x03\x12\
\x03\x0e\x08\x1c\n\x0c\n\x05\x04\0\x02\x03\x05\x12\x03\x0e\x08\x0e\n\x0c\
\n\x05\x04\0\x02\x03\x01\x12\x03\x0e\x0f\x17\n\x0c\n\x05\x04\0\x02\x03\
\x03\x12\x03\x0e\x1a\x1b\n\x0b\n\x04\x04\0\x02\x04\x12\x03\x0f\x08\x1a\n\
\x0c\n\x05\x04\0\x02\x04\x06\x12\x03\x0f\x08\x0e\n\x0c\n\x05\x04\0\x02\
\x04\x01\x12\x03\x0f\x0f\x15\n\x0c\n\x05\x04\0\x02\x04\x03\x12\x03\x0f\
\x18\x19b\x06proto3\
";
/// `FileDescriptorProto` object which was a source for this generated file
@ -238,7 +339,8 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor {
let mut deps = ::std::vec::Vec::with_capacity(0);
let mut messages = ::std::vec::Vec::with_capacity(1);
messages.push(Image::generated_message_descriptor_data());
let mut enums = ::std::vec::Vec::with_capacity(0);
let mut enums = ::std::vec::Vec::with_capacity(1);
enums.push(Format::generated_enum_descriptor_data());
::protobuf::reflect::GeneratedFileDescriptor::new_generated(
file_descriptor_proto(),
deps,

View File

@ -1,11 +1,15 @@
use std::io::Cursor;
use image::{io::Reader, DynamicImage, ImageResult};
use image::{io::Reader, DynamicImage, ImageResult, ImageFormat};
use webp::{Encoder, WebPMemory};
pub fn thumbnail(bytes: &[u8]) -> ImageResult<(WebPMemory, String)> {
let original_image = Reader::new(Cursor::new(bytes))
.with_guessed_format().expect("IO errors impossible with Cursor")
.decode()?;
use crate::protobuf::image::Format;
pub fn thumbnail(bytes: &[u8]) -> ImageResult<(Option<WebPMemory>, WebPMemory, String, Format)> {
let original_image_encoded = Reader::new(Cursor::new(bytes))
.with_guessed_format().expect("IO errors impossible with Cursor");
let original_format = original_image_encoded.format();
let original_image = original_image_encoded.decode()?;
let new_dimm = u32::min(original_image.height(), original_image.width());
let crop_x = (original_image.width() - new_dimm) / 2;
let crop_y = (original_image.height() - new_dimm) / 2;
@ -15,11 +19,32 @@ pub fn thumbnail(bytes: &[u8]) -> ImageResult<(WebPMemory, String)> {
} else {
original_image.thumbnail_exact(400, 400)
}.into_rgba8();
let blurhash = blurhash::encode(4, 4, 400, 400, scaled.as_raw());
let scale = scaled.width();
let blurhash = blurhash::encode(4, 4, scale, scale, scaled.as_raw());
let (recoded_original, format) = match original_format {
Some(ImageFormat::WebP) => (None, Format::WEBP),
Some(ImageFormat::Avif) => (None, Format::AVIF),
Some(ImageFormat::Jpeg) => (None, Format::JPG),
Some(ImageFormat::Png) => (None, Format::PNG),
Some(ImageFormat::Gif) => (None, Format::GIF),
_ => ( // Unrecognized or format which isn't spec-supported
Some(
Encoder::from_image(&original_image)
.expect("Error transcoding image as webp")
.encode_lossless(),
),
Format::WEBP
)
};
Ok((
recoded_original,
Encoder::from_image(&DynamicImage::ImageRgba8(scaled))
.expect("Unexpected difficulty interpretting thumbnail")
.encode(50.0),
blurhash
blurhash,
format,
))
}

View File

@ -21,12 +21,10 @@ pub fn put_data(agent: &Agent, server: &str, data: &[u8]) -> Result<String, ureq
pub fn get_data<'b>(agent: &Agent, location: &str, size_limit: u64, buffer: &'b mut Vec<u8>) -> Result<&'b mut Vec<u8>, ureq::Error> {
buffer.clear();
println!("{}", location);
let read = agent.get(location)
agent.get(location)
.call()?
.into_reader()
.take(size_limit)
.read_to_end(buffer)?;
println!("{}", read);
Ok(buffer)
}