debug streaming
This commit is contained in:
parent
6be1f80e8a
commit
1ac7b619cf
11 changed files with 178 additions and 87 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
.env
|
.env
|
||||||
|
cache
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1000,6 +1000,7 @@ dependencies = [
|
||||||
"ssri",
|
"ssri",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ sodiumoxide = "0.2"
|
||||||
ssri = "5"
|
ssri = "5"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tokio = { version = "1", features = [ "full", "parking_lot" ] }
|
tokio = { version = "1", features = [ "full", "parking_lot" ] }
|
||||||
|
tokio-util = { version = "0.6", features = [ "codec" ] }
|
||||||
url = { version = "2", features = [ "serde" ] }
|
url = { version = "2", features = [ "serde" ] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
81
src/cache/fs.rs
vendored
81
src/cache/fs.rs
vendored
|
@ -1,20 +1,22 @@
|
||||||
use actix_web::error::PayloadError;
|
use actix_web::error::PayloadError;
|
||||||
use bytes::BytesMut;
|
use futures::{Stream, StreamExt};
|
||||||
use futures::{Future, Stream, StreamExt};
|
use log::debug;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{collections::HashMap, fmt::Display};
|
use tokio::fs::{create_dir_all, remove_file, File};
|
||||||
use tokio::fs::{remove_file, File};
|
|
||||||
use tokio::io::{AsyncRead, AsyncWriteExt, ReadBuf};
|
use tokio::io::{AsyncRead, AsyncWriteExt, ReadBuf};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio::time::Sleep;
|
use tokio::time::Interval;
|
||||||
|
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||||
|
|
||||||
use super::{BoxedImageStream, CacheStreamItem};
|
use super::{BoxedImageStream, CacheStream, CacheStreamItem};
|
||||||
|
|
||||||
/// Keeps track of files that are currently being written to.
|
/// Keeps track of files that are currently being written to.
|
||||||
///
|
///
|
||||||
|
@ -36,15 +38,23 @@ static WRITING_STATUS: Lazy<RwLock<HashMap<PathBuf, Arc<CacheStatus>>>> =
|
||||||
Lazy::new(|| RwLock::new(HashMap::new()));
|
Lazy::new(|| RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
/// Tries to read from the file, returning a byte stream if it exists
|
/// Tries to read from the file, returning a byte stream if it exists
|
||||||
pub async fn read_file(path: &Path) -> Option<Result<FsStream, std::io::Error>> {
|
pub async fn read_file(path: &Path) -> Option<Result<CacheStream, std::io::Error>> {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let status = WRITING_STATUS
|
let status = WRITING_STATUS.read().await.get(path).map(Arc::clone);
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.get(path)
|
|
||||||
.map_or_else(|| Arc::new(CacheStatus::done()), Arc::clone);
|
|
||||||
|
|
||||||
Some(FsStream::new(path, status).await)
|
if let Some(status) = status {
|
||||||
|
Some(
|
||||||
|
ConcurrentFsStream::new(path, status)
|
||||||
|
.await
|
||||||
|
.map(CacheStream::Concurrent),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
File::open(path)
|
||||||
|
.await
|
||||||
|
.map(|f| CacheStream::Completed(FramedRead::new(f, BytesCodec::new()))),
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -55,11 +65,13 @@ pub async fn read_file(path: &Path) -> Option<Result<FsStream, std::io::Error>>
|
||||||
pub async fn write_file(
|
pub async fn write_file(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
mut byte_stream: BoxedImageStream,
|
mut byte_stream: BoxedImageStream,
|
||||||
) -> Result<FsStream, std::io::Error> {
|
) -> Result<CacheStream, std::io::Error> {
|
||||||
let done_writing_flag = Arc::new(CacheStatus::new());
|
let done_writing_flag = Arc::new(CacheStatus::new());
|
||||||
|
|
||||||
let mut file = {
|
let mut file = {
|
||||||
let mut write_lock = WRITING_STATUS.write().await;
|
let mut write_lock = WRITING_STATUS.write().await;
|
||||||
|
let parent = path.parent().unwrap();
|
||||||
|
create_dir_all(parent).await?;
|
||||||
let file = File::create(path).await?; // we need to make sure the file exists and is truncated.
|
let file = File::create(path).await?; // we need to make sure the file exists and is truncated.
|
||||||
write_lock.insert(path.to_path_buf(), Arc::clone(&done_writing_flag));
|
write_lock.insert(path.to_path_buf(), Arc::clone(&done_writing_flag));
|
||||||
file
|
file
|
||||||
|
@ -87,6 +99,7 @@ pub async fn write_file(
|
||||||
} else {
|
} else {
|
||||||
file.flush().await?;
|
file.flush().await?;
|
||||||
file.sync_all().await?; // we need metadata
|
file.sync_all().await?; // we need metadata
|
||||||
|
debug!("writing to file done");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut write_lock = WRITING_STATUS.write().await;
|
let mut write_lock = WRITING_STATUS.write().await;
|
||||||
|
@ -103,21 +116,23 @@ pub async fn write_file(
|
||||||
Ok::<_, std::io::Error>(())
|
Ok::<_, std::io::Error>(())
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(FsStream::new(path, done_writing_flag).await?)
|
Ok(CacheStream::Concurrent(
|
||||||
|
ConcurrentFsStream::new(path, done_writing_flag).await?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FsStream {
|
pub struct ConcurrentFsStream {
|
||||||
file: Pin<Box<File>>,
|
file: Pin<Box<File>>,
|
||||||
sleep: Pin<Box<Sleep>>,
|
sleep: Pin<Box<Interval>>,
|
||||||
is_file_done_writing: Arc<CacheStatus>,
|
is_file_done_writing: Arc<CacheStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FsStream {
|
impl ConcurrentFsStream {
|
||||||
async fn new(path: &Path, is_done: Arc<CacheStatus>) -> Result<Self, std::io::Error> {
|
async fn new(path: &Path, is_done: Arc<CacheStatus>) -> Result<Self, std::io::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
file: Box::pin(File::open(path).await?),
|
file: Box::pin(File::open(path).await?),
|
||||||
// 0.5ms
|
// 0.5ms
|
||||||
sleep: Box::pin(tokio::time::sleep(Duration::from_micros(500))),
|
sleep: Box::pin(tokio::time::interval(Duration::from_micros(500))),
|
||||||
is_file_done_writing: is_done,
|
is_file_done_writing: is_done,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -135,25 +150,35 @@ impl Display for UpstreamError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for FsStream {
|
impl Stream for ConcurrentFsStream {
|
||||||
type Item = CacheStreamItem;
|
type Item = CacheStreamItem;
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
let status = self.is_file_done_writing.load();
|
let status = self.is_file_done_writing.load();
|
||||||
|
|
||||||
let mut bytes = BytesMut::with_capacity(1460);
|
let mut bytes = [0; 1460].to_vec();
|
||||||
let mut buffer = ReadBuf::new(&mut bytes);
|
let mut buffer = ReadBuf::new(&mut bytes);
|
||||||
let polled_result = self.file.as_mut().poll_read(cx, &mut buffer);
|
let polled_result = self.file.as_mut().poll_read(cx, &mut buffer);
|
||||||
|
let filled = buffer.filled().len();
|
||||||
match (status, buffer.filled().len()) {
|
match (status, filled) {
|
||||||
// Prematurely reached EOF, schedule a poll in the future
|
// Prematurely reached EOF, schedule a poll in the future
|
||||||
(WritingStatus::NotDone, 0) => {
|
(WritingStatus::NotDone, 0) => {
|
||||||
let _ = self.sleep.as_mut().poll(cx);
|
let _ = self.sleep.as_mut().poll_tick(cx);
|
||||||
Poll::Pending
|
Poll::Pending
|
||||||
}
|
}
|
||||||
// We got an error, abort the read.
|
// We got an error, abort the read.
|
||||||
(WritingStatus::Error, _) => Poll::Ready(Some(Err(UpstreamError))),
|
(WritingStatus::Error, _) => Poll::Ready(Some(Err(UpstreamError))),
|
||||||
_ => polled_result.map(|_| Some(Ok(bytes.split().into()))),
|
_ => {
|
||||||
|
bytes.truncate(filled);
|
||||||
|
polled_result.map(|_| {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
dbg!(line!());
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Ok(bytes.into()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,11 +198,6 @@ impl CacheStatus {
|
||||||
Self(AtomicU8::new(WritingStatus::NotDone as u8))
|
Self(AtomicU8::new(WritingStatus::NotDone as u8))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
const fn done() -> Self {
|
|
||||||
Self(AtomicU8::new(WritingStatus::Done as u8))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn store(&self, status: WritingStatus) {
|
fn store(&self, status: WritingStatus) {
|
||||||
self.0.store(status as u8, Ordering::Release);
|
self.0.store(status as u8, Ordering::Release);
|
||||||
|
@ -189,6 +209,7 @@ impl CacheStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum WritingStatus {
|
enum WritingStatus {
|
||||||
NotDone = 0,
|
NotDone = 0,
|
||||||
Done,
|
Done,
|
||||||
|
|
8
src/cache/low_mem.rs
vendored
8
src/cache/low_mem.rs
vendored
|
@ -34,10 +34,9 @@ impl Cache for LowMemCache {
|
||||||
) -> Option<Result<(CacheStream, &ImageMetadata), CacheError>> {
|
) -> Option<Result<(CacheStream, &ImageMetadata), CacheError>> {
|
||||||
let metadata = self.on_disk.get(key)?;
|
let metadata = self.on_disk.get(key)?;
|
||||||
let path = self.disk_path.clone().join(PathBuf::from(key.clone()));
|
let path = self.disk_path.clone().join(PathBuf::from(key.clone()));
|
||||||
super::fs::read_file(&path).await.map(|res| {
|
super::fs::read_file(&path)
|
||||||
res.map(|stream| (CacheStream::Fs(stream), metadata))
|
.await
|
||||||
.map_err(Into::into)
|
.map(|res| res.map(|stream| (stream, metadata)).map_err(Into::into))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn put(
|
async fn put(
|
||||||
|
@ -50,7 +49,6 @@ impl Cache for LowMemCache {
|
||||||
self.on_disk.put(key.clone(), metadata);
|
self.on_disk.put(key.clone(), metadata);
|
||||||
super::fs::write_file(&path, image)
|
super::fs::write_file(&path, image)
|
||||||
.await
|
.await
|
||||||
.map(CacheStream::Fs)
|
|
||||||
.map(move |stream| (stream, self.on_disk.get(&key).unwrap()))
|
.map(move |stream| (stream, self.on_disk.get(&key).unwrap()))
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
22
src/cache/mod.rs
vendored
22
src/cache/mod.rs
vendored
|
@ -8,7 +8,7 @@ use actix_web::http::HeaderValue;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use fs::FsStream;
|
use fs::ConcurrentFsStream;
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -16,6 +16,8 @@ use thiserror::Error;
|
||||||
pub use fs::UpstreamError;
|
pub use fs::UpstreamError;
|
||||||
pub use generational::GenerationalCache;
|
pub use generational::GenerationalCache;
|
||||||
pub use low_mem::LowMemCache;
|
pub use low_mem::LowMemCache;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||||
|
|
||||||
mod fs;
|
mod fs;
|
||||||
mod generational;
|
mod generational;
|
||||||
|
@ -163,8 +165,9 @@ pub trait Cache: Send + Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum CacheStream {
|
pub enum CacheStream {
|
||||||
Fs(FsStream),
|
Concurrent(ConcurrentFsStream),
|
||||||
Memory(MemStream),
|
Memory(MemStream),
|
||||||
|
Completed(FramedRead<File, BytesCodec>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CachedImage> for CacheStream {
|
impl From<CachedImage> for CacheStream {
|
||||||
|
@ -180,8 +183,12 @@ impl Stream for CacheStream {
|
||||||
|
|
||||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
match self.get_mut() {
|
match self.get_mut() {
|
||||||
Self::Fs(stream) => stream.poll_next_unpin(cx),
|
Self::Concurrent(stream) => stream.poll_next_unpin(cx),
|
||||||
Self::Memory(stream) => stream.poll_next_unpin(cx),
|
Self::Memory(stream) => stream.poll_next_unpin(cx),
|
||||||
|
Self::Completed(stream) => stream
|
||||||
|
.poll_next_unpin(cx)
|
||||||
|
.map_ok(|v| v.freeze())
|
||||||
|
.map_err(|_| UpstreamError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,9 +199,12 @@ impl Stream for MemStream {
|
||||||
type Item = CacheStreamItem;
|
type Item = CacheStreamItem;
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
let mut new_bytes = Bytes::new();
|
let new_bytes = self.0.split_to(1460);
|
||||||
std::mem::swap(&mut self.0, &mut new_bytes);
|
if new_bytes.is_empty() {
|
||||||
Poll::Ready(Some(Ok(new_bytes)))
|
Poll::Ready(None)
|
||||||
|
} else {
|
||||||
|
Poll::Ready(Some(Ok(new_bytes)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::path::PathBuf;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
use clap::{crate_authors, crate_description, crate_version, Clap};
|
use clap::{crate_authors, crate_description, crate_version, Clap};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
// Validate tokens is an atomic because it's faster than locking on rwlock.
|
// Validate tokens is an atomic because it's faster than locking on rwlock.
|
||||||
pub static VALIDATE_TOKENS: AtomicBool = AtomicBool::new(false);
|
pub static VALIDATE_TOKENS: AtomicBool = AtomicBool::new(false);
|
||||||
|
@ -42,7 +43,20 @@ pub struct CliArgs {
|
||||||
env = "LOW_MEMORY_MODE",
|
env = "LOW_MEMORY_MODE",
|
||||||
takes_value = false
|
takes_value = false
|
||||||
)]
|
)]
|
||||||
|
/// Changes the caching behavior to avoid buffering images in memory, and
|
||||||
|
/// instead use the filesystem as the buffer backing. This is useful for
|
||||||
|
/// clients in low (< 1GB) RAM environments.
|
||||||
pub low_memory: bool,
|
pub low_memory: bool,
|
||||||
|
/// Changes verbosity. Default verbosity is INFO, while increasing counts
|
||||||
|
/// of verbose flags increases to DEBUG and TRACE, respectively.
|
||||||
#[clap(short, long, parse(from_occurrences))]
|
#[clap(short, long, parse(from_occurrences))]
|
||||||
pub verbose: usize,
|
pub verbose: usize,
|
||||||
|
/// Overrides the upstream URL to fetch images from. Don't use this unless
|
||||||
|
/// you know what you're dealing with.
|
||||||
|
#[clap(long)]
|
||||||
|
pub override_upstream: Option<Url>,
|
||||||
|
/// Disables token validation. Don't use this unless you know the
|
||||||
|
/// ramifications of this command.
|
||||||
|
#[clap(long)]
|
||||||
|
pub disable_token_validation: bool,
|
||||||
}
|
}
|
||||||
|
|
64
src/main.rs
64
src/main.rs
|
@ -51,29 +51,6 @@ async fn main() -> Result<(), std::io::Error> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
let cli_args = CliArgs::parse();
|
let cli_args = CliArgs::parse();
|
||||||
|
|
||||||
println!(concat!(
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
" ",
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
" Copyright (C) 2021 ",
|
|
||||||
env!("CARGO_PKG_AUTHORS"),
|
|
||||||
"\n\n",
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
" is free software: you can redistribute it and/or modify\n\
|
|
||||||
it under the terms of the GNU General Public License as published by\n\
|
|
||||||
the Free Software Foundation, either version 3 of the License, or\n\
|
|
||||||
(at your option) any later version.\n\n",
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
" is distributed in the hope that it will be useful,\n\
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of\n\
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n\
|
|
||||||
GNU General Public License for more details.\n\n\
|
|
||||||
You should have received a copy of the GNU General Public License\n\
|
|
||||||
along with ",
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
". If not, see <https://www.gnu.org/licenses/>.\n"
|
|
||||||
));
|
|
||||||
|
|
||||||
let port = cli_args.port;
|
let port = cli_args.port;
|
||||||
let memory_max_size = cli_args.memory_quota.get();
|
let memory_max_size = cli_args.memory_quota.get();
|
||||||
let disk_quota = cli_args.disk_quota;
|
let disk_quota = cli_args.disk_quota;
|
||||||
|
@ -88,6 +65,8 @@ async fn main() -> Result<(), std::io::Error> {
|
||||||
.init()
|
.init()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
print_preamble_and_warnings();
|
||||||
|
|
||||||
let client_secret = if let Ok(v) = env::var("CLIENT_SECRET") {
|
let client_secret = if let Ok(v) = env::var("CLIENT_SECRET") {
|
||||||
v
|
v
|
||||||
} else {
|
} else {
|
||||||
|
@ -111,13 +90,21 @@ async fn main() -> Result<(), std::io::Error> {
|
||||||
|
|
||||||
// Set ctrl+c to send a stop message
|
// Set ctrl+c to send a stop message
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
let r = running.clone();
|
let running_1 = running.clone();
|
||||||
|
let system = System::current();
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
|
let system = &system;
|
||||||
let client_secret = client_secret.clone();
|
let client_secret = client_secret.clone();
|
||||||
|
let running_2 = Arc::clone(&running_1);
|
||||||
System::new().block_on(async move {
|
System::new().block_on(async move {
|
||||||
send_stop(&client_secret).await;
|
if running_2.load(Ordering::SeqCst) {
|
||||||
|
send_stop(&client_secret).await;
|
||||||
|
} else {
|
||||||
|
warn!("Got second ctrl-c, forcefully exiting");
|
||||||
|
system.stop()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
r.store(false, Ordering::SeqCst);
|
running_1.store(false, Ordering::SeqCst);
|
||||||
})
|
})
|
||||||
.expect("Error setting Ctrl-C handler");
|
.expect("Error setting Ctrl-C handler");
|
||||||
|
|
||||||
|
@ -174,3 +161,28 @@ async fn main() -> Result<(), std::io::Error> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_preamble_and_warnings() {
|
||||||
|
println!(concat!(
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
" ",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
" Copyright (C) 2021 ",
|
||||||
|
env!("CARGO_PKG_AUTHORS"),
|
||||||
|
"\n\n",
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
" is free software: you can redistribute it and/or modify\n\
|
||||||
|
it under the terms of the GNU General Public License as published by\n\
|
||||||
|
the Free Software Foundation, either version 3 of the License, or\n\
|
||||||
|
(at your option) any later version.\n\n",
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
" is distributed in the hope that it will be useful,\n\
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of\n\
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n\
|
||||||
|
GNU General Public License for more details.\n\n\
|
||||||
|
You should have received a copy of the GNU General Public License\n\
|
||||||
|
along with ",
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
". If not, see <https://www.gnu.org/licenses/>.\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
15
src/ping.rs
15
src/ping.rs
|
@ -68,7 +68,7 @@ impl<'a> From<(&'a str, &CliArgs)> for Request<'a> {
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
pub image_server: Url,
|
pub image_server: Url,
|
||||||
pub latest_build: usize,
|
pub latest_build: usize,
|
||||||
pub url: String,
|
pub url: Url,
|
||||||
pub token_key: Option<String>,
|
pub token_key: Option<String>,
|
||||||
pub compromised: bool,
|
pub compromised: bool,
|
||||||
pub paused: bool,
|
pub paused: bool,
|
||||||
|
@ -145,8 +145,8 @@ impl std::fmt::Debug for Tls {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_server_state(secret: &str, req: &CliArgs, data: &mut Arc<RwLockServerState>) {
|
pub async fn update_server_state(secret: &str, cli: &CliArgs, data: &mut Arc<RwLockServerState>) {
|
||||||
let req = Request::from_config_and_state(secret, req, data);
|
let req = Request::from_config_and_state(secret, cli, data);
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let resp = client.post(CONTROL_CENTER_PING_URL).json(&req).send().await;
|
let resp = client.post(CONTROL_CENTER_PING_URL).json(&req).send().await;
|
||||||
match resp {
|
match resp {
|
||||||
|
@ -154,7 +154,10 @@ pub async fn update_server_state(secret: &str, req: &CliArgs, data: &mut Arc<RwL
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
let mut write_guard = data.0.write();
|
let mut write_guard = data.0.write();
|
||||||
|
|
||||||
write_guard.image_server = resp.image_server;
|
if !write_guard.url_overridden && write_guard.image_server != resp.image_server {
|
||||||
|
warn!("Ignoring new upstream url!");
|
||||||
|
write_guard.image_server = resp.image_server;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(key) = resp.token_key {
|
if let Some(key) = resp.token_key {
|
||||||
if let Some(key) = base64::decode(&key)
|
if let Some(key) = base64::decode(&key)
|
||||||
|
@ -167,7 +170,9 @@ pub async fn update_server_state(secret: &str, req: &CliArgs, data: &mut Arc<RwL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if VALIDATE_TOKENS.load(Ordering::Acquire) != resp.force_tokens {
|
if !cli.disable_token_validation
|
||||||
|
&& VALIDATE_TOKENS.load(Ordering::Acquire) != resp.force_tokens
|
||||||
|
{
|
||||||
if resp.force_tokens {
|
if resp.force_tokens {
|
||||||
info!("Client received command to enforce token validity.");
|
info!("Client received command to enforce token validity.");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,7 +11,7 @@ use base64::DecodeError;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use futures::{Stream, TryStreamExt};
|
use futures::{Stream, TryStreamExt};
|
||||||
use log::{error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sodiumoxide::crypto::box_::{open_precomputed, Nonce, PrecomputedKey, NONCEBYTES};
|
use sodiumoxide::crypto::box_::{open_precomputed, Nonce, PrecomputedKey, NONCEBYTES};
|
||||||
|
@ -76,6 +76,7 @@ async fn token_data_saver(
|
||||||
return ServerResponse::TokenValidationError(e);
|
return ServerResponse::TokenValidationError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch_image(state, cache, chapter_hash, file_name, true).await
|
fetch_image(state, cache, chapter_hash, file_name, true).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +87,13 @@ pub async fn default(state: Data<RwLockServerState>, req: HttpRequest) -> impl R
|
||||||
req.path().chars().skip(1).collect::<String>()
|
req.path().chars().skip(1).collect::<String>()
|
||||||
);
|
);
|
||||||
info!("Got unknown path, just proxying: {}", path);
|
info!("Got unknown path, just proxying: {}", path);
|
||||||
let resp = reqwest::get(path).await.unwrap();
|
let resp = match reqwest::get(path).await {
|
||||||
|
Ok(resp) => resp,
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e);
|
||||||
|
return ServerResponse::HttpResponse(HttpResponse::BadGateway().finish());
|
||||||
|
}
|
||||||
|
};
|
||||||
let content_type = resp.headers().get(CONTENT_TYPE);
|
let content_type = resp.headers().get(CONTENT_TYPE);
|
||||||
let mut resp_builder = HttpResponseBuilder::new(resp.status());
|
let mut resp_builder = HttpResponseBuilder::new(resp.status());
|
||||||
if let Some(content_type) = content_type {
|
if let Some(content_type) = content_type {
|
||||||
|
@ -153,6 +160,8 @@ fn validate_token(
|
||||||
return Err(TokenValidationError::InvalidChapterHash);
|
return Err(TokenValidationError::InvalidChapterHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("Token validated!");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,15 +203,15 @@ async fn fetch_image(
|
||||||
reqwest::get(format!(
|
reqwest::get(format!(
|
||||||
"{}/data-saver/{}/{}",
|
"{}/data-saver/{}/{}",
|
||||||
state.0.read().image_server,
|
state.0.read().image_server,
|
||||||
&key.1,
|
&key.0,
|
||||||
&key.2
|
&key.1
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
reqwest::get(format!(
|
reqwest::get(format!(
|
||||||
"{}/data/{}/{}",
|
"{}/data/{}/{}",
|
||||||
state.0.read().image_server,
|
state.0.read().image_server,
|
||||||
&key.1,
|
&key.0,
|
||||||
&key.2
|
&key.1
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
@ -214,6 +223,7 @@ async fn fetch_image(
|
||||||
let is_image = content_type
|
let is_image = content_type
|
||||||
.map(|v| String::from_utf8_lossy(v.as_ref()).contains("image/"))
|
.map(|v| String::from_utf8_lossy(v.as_ref()).contains("image/"))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if resp.status() != 200 || !is_image {
|
if resp.status() != 200 || !is_image {
|
||||||
warn!(
|
warn!(
|
||||||
"Got non-OK or non-image response code from upstream, proxying and not caching result.",
|
"Got non-OK or non-image response code from upstream, proxying and not caching result.",
|
||||||
|
@ -241,6 +251,9 @@ async fn fetch_image(
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = resp.bytes_stream().map_err(|e| e.into());
|
let body = resp.bytes_stream().map_err(|e| e.into());
|
||||||
|
|
||||||
|
debug!("Inserting into cache");
|
||||||
|
|
||||||
let metadata = ImageMetadata::new(content_type, length, last_mod).unwrap();
|
let metadata = ImageMetadata::new(content_type, length, last_mod).unwrap();
|
||||||
let (stream, metadata) = {
|
let (stream, metadata) = {
|
||||||
match cache.lock().put(key, Box::new(body), metadata).await {
|
match cache.lock().put(key, Box::new(body), metadata).await {
|
||||||
|
@ -254,6 +267,8 @@ async fn fetch_image(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debug!("Done putting into cache");
|
||||||
|
|
||||||
return construct_response(stream, &metadata);
|
return construct_response(stream, &metadata);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -269,6 +284,8 @@ fn construct_response(
|
||||||
data: impl Stream<Item = Result<Bytes, UpstreamError>> + Unpin + 'static,
|
data: impl Stream<Item = Result<Bytes, UpstreamError>> + Unpin + 'static,
|
||||||
metadata: &ImageMetadata,
|
metadata: &ImageMetadata,
|
||||||
) -> ServerResponse {
|
) -> ServerResponse {
|
||||||
|
debug!("Constructing response");
|
||||||
|
|
||||||
let mut resp = HttpResponse::Ok();
|
let mut resp = HttpResponse::Ok();
|
||||||
if let Some(content_type) = metadata.content_type {
|
if let Some(content_type) = metadata.content_type {
|
||||||
resp.append_header((CONTENT_TYPE, content_type.as_ref()));
|
resp.append_header((CONTENT_TYPE, content_type.as_ref()));
|
||||||
|
|
27
src/state.rs
27
src/state.rs
|
@ -13,7 +13,8 @@ pub struct ServerState {
|
||||||
pub precomputed_key: PrecomputedKey,
|
pub precomputed_key: PrecomputedKey,
|
||||||
pub image_server: Url,
|
pub image_server: Url,
|
||||||
pub tls_config: Tls,
|
pub tls_config: Tls,
|
||||||
pub url: String,
|
pub url: Url,
|
||||||
|
pub url_overridden: bool,
|
||||||
pub log_state: LogState,
|
pub log_state: LogState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +37,7 @@ impl ServerState {
|
||||||
|
|
||||||
match resp {
|
match resp {
|
||||||
Ok(resp) => match resp.json::<Response>().await {
|
Ok(resp) => match resp.json::<Response>().await {
|
||||||
Ok(resp) => {
|
Ok(mut resp) => {
|
||||||
let key = resp
|
let key = resp
|
||||||
.token_key
|
.token_key
|
||||||
.and_then(|key| {
|
.and_then(|key| {
|
||||||
|
@ -60,21 +61,31 @@ impl ServerState {
|
||||||
warn!("Control center has paused this node!");
|
warn!("Control center has paused this node!");
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("This client's URL has been set to {}", resp.url);
|
if let Some(ref override_url) = config.override_upstream {
|
||||||
|
resp.image_server = override_url.clone();
|
||||||
if resp.force_tokens {
|
warn!("Upstream URL overridden to: {}", resp.image_server);
|
||||||
info!("This client will validate tokens.");
|
|
||||||
} else {
|
} else {
|
||||||
info!("This client will not validate tokens.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VALIDATE_TOKENS.store(resp.force_tokens, Ordering::Release);
|
info!("This client's URL has been set to {}", resp.url);
|
||||||
|
|
||||||
|
if config.disable_token_validation {
|
||||||
|
warn!("Token validation is explicitly disabled!");
|
||||||
|
} else {
|
||||||
|
if resp.force_tokens {
|
||||||
|
info!("This client will validate tokens.");
|
||||||
|
} else {
|
||||||
|
info!("This client will not validate tokens.");
|
||||||
|
}
|
||||||
|
VALIDATE_TOKENS.store(resp.force_tokens, Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
precomputed_key: key,
|
precomputed_key: key,
|
||||||
image_server: resp.image_server,
|
image_server: resp.image_server,
|
||||||
tls_config: resp.tls.unwrap(),
|
tls_config: resp.tls.unwrap(),
|
||||||
url: resp.url,
|
url: resp.url,
|
||||||
|
url_overridden: config.override_upstream.is_some(),
|
||||||
log_state: LogState {
|
log_state: LogState {
|
||||||
was_paused_before: resp.paused,
|
was_paused_before: resp.paused,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue