v0.3.2 // fencepost fixes
This commit is contained in:
parent
4ce98de0e1
commit
2d429c75b7
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "breeze"
|
name = "breeze"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[profile.dev.package]
|
[profile.dev.package]
|
||||||
|
|
@ -9,22 +9,23 @@ tikv-jemalloc-sys = { opt-level = 3 }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argh = "0.1.12"
|
argh = "0.1.12"
|
||||||
atomic-time = "0.1.4"
|
atomic-time = "0.1.4"
|
||||||
axum = { version = "0.8.1", features = ["macros", "http2"] }
|
axum = { version = "0.8.9", features = ["macros", "http2"] }
|
||||||
axum-extra = { version = "0.10.0", default-features = false, features = [
|
axum-extra = { version = "0.12.6", default-features = false, features = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"typed-header",
|
"typed-header",
|
||||||
] }
|
] }
|
||||||
base64 = "0.21"
|
base64 = "0.22"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
color-eyre = "0.6"
|
color-eyre = "0.6"
|
||||||
dashmap = { version = "6.1.0", features = ["inline"] }
|
dashmap = { version = "6.1.0", features = ["inline"] }
|
||||||
headers = "0.4"
|
headers = "0.4"
|
||||||
|
heed = "0.22.1"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
http = "1.2"
|
http = "1.2"
|
||||||
img-parts = "0.3"
|
img-parts = "0.3"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_with = "3.12"
|
serde_with = "3.19"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
tokio = { version = "1", features = [
|
tokio = { version = "1", features = [
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
|
|
@ -32,6 +33,7 @@ tokio = { version = "1", features = [
|
||||||
"net",
|
"net",
|
||||||
"fs",
|
"fs",
|
||||||
"signal",
|
"signal",
|
||||||
|
"test-util",
|
||||||
] }
|
] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
tokio-util = { version = "0.7", features = ["io"] }
|
tokio-util = { version = "0.7", features = ["io"] }
|
||||||
|
|
|
||||||
|
|
@ -210,9 +210,9 @@ impl Cache {
|
||||||
|
|
||||||
/// Returns if an upload is able to be cached
|
/// Returns if an upload is able to be cached
|
||||||
/// with the current caching rules
|
/// with the current caching rules
|
||||||
#[inline(always)]
|
#[inline]
|
||||||
pub fn will_use(&self, length: u64) -> bool {
|
pub fn will_use(&self, length: u64) -> bool {
|
||||||
length <= self.cfg.max_length
|
length <= (self.cfg.max_length as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The background job that scans through the cache and removes inactive elements.
|
/// The background job that scans through the cache and removes inactive elements.
|
||||||
|
|
@ -221,6 +221,7 @@ impl Cache {
|
||||||
/// letting each entry keep track of expiry with its own task
|
/// letting each entry keep track of expiry with its own task
|
||||||
pub async fn scanner(&self) {
|
pub async fn scanner(&self) {
|
||||||
let mut interval = time::interval(self.cfg.scan_freq);
|
let mut interval = time::interval(self.cfg.scan_freq);
|
||||||
|
interval.tick().await; // Skip first tick
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// We put this first so that it doesn't scan the instant the server starts
|
// We put this first so that it doesn't scan the instant the server starts
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ fn default_motd() -> String {
|
||||||
pub struct EngineConfig {
|
pub struct EngineConfig {
|
||||||
/// The url that the instance of breeze is meant to be accessed from.
|
/// The url that the instance of breeze is meant to be accessed from.
|
||||||
///
|
///
|
||||||
/// ex: https://picture.wtf would generate links like https://picture.wtf/p/abcdef.png
|
/// ex: `https://picture.wtf` would generate links like `https://picture.wtf/p/abcdef.png`
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
|
|
||||||
/// Authentication key for new uploads, will be required if this is specified. (optional)
|
/// Authentication key for new uploads, will be required if this is specified. (optional)
|
||||||
|
|
@ -69,8 +69,8 @@ pub struct DiskConfig {
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct CacheConfig {
|
pub struct CacheConfig {
|
||||||
/// The maximum length in bytes that a file can be
|
/// The maximum length in bytes that a file can be
|
||||||
/// before it skips cache (in seconds)
|
/// before it skips cache (in bytes)
|
||||||
pub max_length: u64,
|
pub max_length: usize,
|
||||||
|
|
||||||
/// The amount of time a file can last inside the cache (in seconds)
|
/// The amount of time a file can last inside the cache (in seconds)
|
||||||
#[serde_as(as = "DurationSeconds")]
|
#[serde_as(as = "DurationSeconds")]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
io::SeekFrom,
|
io::SeekFrom,
|
||||||
ops::Bound,
|
ops::{Bound, RangeBounds},
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
atomic::{AtomicUsize, Ordering},
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
|
@ -95,32 +95,53 @@ pub struct Engine {
|
||||||
|
|
||||||
/// Try to parse a `Range` header into an easier format to work with
|
/// Try to parse a `Range` header into an easier format to work with
|
||||||
fn resolve_range(range: Option<headers::Range>, full_len: u64) -> Option<(u64, u64)> {
|
fn resolve_range(range: Option<headers::Range>, full_len: u64) -> Option<(u64, u64)> {
|
||||||
let last_byte = full_len - 1;
|
// Prepare default range
|
||||||
|
let default = Some((0, full_len));
|
||||||
|
|
||||||
let (start, end) =
|
// Take range, otherwise return
|
||||||
if let Some((start, end)) = range.and_then(|r| r.satisfiable_ranges(full_len).next()) {
|
let Some(range) = range else {
|
||||||
// satisfiable_ranges will never return Excluded so this is ok
|
return default; // unspecified; use default
|
||||||
let start = if let Bound::Included(start_incl) = start {
|
|
||||||
start_incl
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let end = if let Bound::Included(end_incl) = end {
|
|
||||||
end_incl
|
|
||||||
} else {
|
|
||||||
last_byte
|
|
||||||
};
|
};
|
||||||
|
|
||||||
(start, end)
|
// Get iterator of satisfiable ranges
|
||||||
} else {
|
let mut ranges = range.satisfiable_ranges(full_len);
|
||||||
(0, last_byte)
|
|
||||||
|
// Take first range
|
||||||
|
let Some(range) = ranges.next() else {
|
||||||
|
return default; // empty; use default
|
||||||
};
|
};
|
||||||
|
|
||||||
// catch ranges we can't satisfy
|
// If there are multiple ranges, we will
|
||||||
if end > last_byte || start > end {
|
// not process the request
|
||||||
|
if ranges.next().is_some() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert into a..b range
|
||||||
|
let start = match range.start_bound() {
|
||||||
|
Bound::Included(&x) => x,
|
||||||
|
Bound::Excluded(&x) => x.checked_add(1)?,
|
||||||
|
Bound::Unbounded => 0,
|
||||||
|
};
|
||||||
|
let end = match range.end_bound() {
|
||||||
|
Bound::Included(&x) => x.checked_add(1)?,
|
||||||
|
Bound::Excluded(&x) => x,
|
||||||
|
Bound::Unbounded => full_len,
|
||||||
|
};
|
||||||
|
|
||||||
|
// We can't handle bounds
|
||||||
|
// out of order
|
||||||
|
if start > end {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't return more bytes
|
||||||
|
// than we have
|
||||||
|
if end > full_len {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return
|
||||||
Some((start, end))
|
Some((start, end))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,6 +220,7 @@ impl Engine {
|
||||||
return Ok(GetOutcome::NotFound);
|
return Ok(GetOutcome::NotFound);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// read length from disk
|
||||||
let full_len = self.disk.len(&f).await?;
|
let full_len = self.disk.len(&f).await?;
|
||||||
|
|
||||||
// if possible, recache and send a cache response
|
// if possible, recache and send a cache response
|
||||||
|
|
@ -230,11 +252,11 @@ impl Engine {
|
||||||
return Ok(GetOutcome::RangeNotSatisfiable);
|
return Ok(GetOutcome::RangeNotSatisfiable);
|
||||||
};
|
};
|
||||||
|
|
||||||
let range_len = (end - start) + 1;
|
// Set up file handle
|
||||||
|
|
||||||
f.seek(SeekFrom::Start(start)).await?;
|
f.seek(SeekFrom::Start(start)).await?;
|
||||||
let f = f.take(range_len);
|
let f = f.take(end - start);
|
||||||
|
|
||||||
|
// Return
|
||||||
let res = UploadResponse {
|
let res = UploadResponse {
|
||||||
full_len,
|
full_len,
|
||||||
range: (start, end),
|
range: (start, end),
|
||||||
|
|
@ -244,15 +266,27 @@ impl Engine {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve a..b range
|
||||||
let full_len = data.len() as u64;
|
let full_len = data.len() as u64;
|
||||||
let Some((start, end)) = resolve_range(range, full_len) else {
|
let Some((start, end)) = resolve_range(range, full_len) else {
|
||||||
return Ok(GetOutcome::RangeNotSatisfiable);
|
return Ok(GetOutcome::RangeNotSatisfiable);
|
||||||
};
|
};
|
||||||
|
|
||||||
// cut down to range
|
// Cut down to range
|
||||||
let data = data.slice((start as usize)..=(end as usize));
|
let data = {
|
||||||
|
// Convert types.
|
||||||
|
// These should never be greater than usize::MAX
|
||||||
|
// if I recall, because max cache length is a usize.
|
||||||
|
let (start, end): (usize, usize) = (
|
||||||
|
start.try_into().expect("start bound"),
|
||||||
|
end.try_into().expect("end bound"),
|
||||||
|
);
|
||||||
|
|
||||||
// build response
|
// Slice bytes
|
||||||
|
data.slice(start..end)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build response
|
||||||
let res = UploadResponse {
|
let res = UploadResponse {
|
||||||
full_len,
|
full_len,
|
||||||
range: (start, end),
|
range: (start, end),
|
||||||
|
|
@ -286,9 +320,10 @@ impl Engine {
|
||||||
// we found it in cache! take as many bytes as we can
|
// we found it in cache! take as many bytes as we can
|
||||||
let taking = full_data.len().min(SAMPLE_WANTED_BYTES);
|
let taking = full_data.len().min(SAMPLE_WANTED_BYTES);
|
||||||
let data = full_data.slice(0..taking);
|
let data = full_data.slice(0..taking);
|
||||||
|
// get len
|
||||||
let len = full_data.len() as u64;
|
let len = full_data.len() as u64;
|
||||||
|
|
||||||
|
// return
|
||||||
(data, len)
|
(data, len)
|
||||||
} else {
|
} else {
|
||||||
// not in cache, so try disk
|
// not in cache, so try disk
|
||||||
|
|
@ -332,12 +367,12 @@ impl Engine {
|
||||||
|
|
||||||
if !self.has(&saved_name).await {
|
if !self.has(&saved_name).await {
|
||||||
break saved_name;
|
break saved_name;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
// there was a name collision. loop and try again
|
// there was a name collision. loop and try again
|
||||||
info!("name collision! saved_name= {}", saved_name);
|
info!("name collision! saved_name= {}", saved_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Wipe out an upload from all storage.
|
/// Wipe out an upload from all storage.
|
||||||
///
|
///
|
||||||
|
|
@ -483,6 +518,7 @@ impl Engine {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return w/ info for hash calculation
|
||||||
Ok((hash_sample.freeze(), observed_len))
|
Ok((hash_sample.freeze(), observed_len))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,7 +569,7 @@ impl Engine {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
// If anything fails, delete the upload and return the error
|
// If anything fails, delete the upload and return the error
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("failed processing upload!");
|
error!(?err, "failed processing upload!");
|
||||||
|
|
||||||
self.remove(&saved_name).await?;
|
self.remove(&saved_name).await?;
|
||||||
return Err(err);
|
return Err(err);
|
||||||
|
|
|
||||||
19
src/main.rs
19
src/main.rs
|
|
@ -35,6 +35,17 @@ struct Args {
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Instantiates router.
|
||||||
|
fn router(engine: Engine) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/new", post(new::new))
|
||||||
|
.route("/p/{saved_name}", get(view::view))
|
||||||
|
.route("/del", get(delete::delete))
|
||||||
|
.route("/", get(index::index))
|
||||||
|
.route("/robots.txt", get(index::robots_txt))
|
||||||
|
.with_state(Arc::new(engine))
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> eyre::Result<()> {
|
async fn main() -> eyre::Result<()> {
|
||||||
// Install color-eyre
|
// Install color-eyre
|
||||||
|
|
@ -74,13 +85,7 @@ async fn main() -> eyre::Result<()> {
|
||||||
let engine = Engine::with_config(cfg.engine);
|
let engine = Engine::with_config(cfg.engine);
|
||||||
|
|
||||||
// Build main router
|
// Build main router
|
||||||
let app = Router::new()
|
let app = router(engine);
|
||||||
.route("/new", post(new::new))
|
|
||||||
.route("/p/{saved_name}", get(view::view))
|
|
||||||
.route("/del", get(delete::delete))
|
|
||||||
.route("/", get(index::index))
|
|
||||||
.route("/robots.txt", get(index::robots_txt))
|
|
||||||
.with_state(Arc::new(engine));
|
|
||||||
|
|
||||||
// Start web server
|
// Start web server
|
||||||
info!("starting server.");
|
info!("starting server.");
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ impl IntoResponse for ViewError {
|
||||||
impl IntoResponse for UploadResponse {
|
impl IntoResponse for UploadResponse {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let (start, end) = self.range;
|
let (start, end) = self.range;
|
||||||
let range_len = (end - start) + 1;
|
let range_len = end - start;
|
||||||
|
|
||||||
let mut res = match self.data {
|
let mut res = match self.data {
|
||||||
UploadData::Cache(data) => data.into_response(),
|
UploadData::Cache(data) => data.into_response(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue