Compare commits
4 commits
7de7f5ede1
...
1f45914cc4
Author | SHA1 | Date | |
---|---|---|---|
1f45914cc4 | |||
3dfb2d7ad5 | |||
9daacaf406 | |||
5af5bb3e95 |
14 changed files with 405 additions and 51 deletions
47
Cargo.lock
generated
47
Cargo.lock
generated
|
@ -76,6 +76,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.74"
|
||||
|
@ -243,6 +249,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"chacha20",
|
||||
"dotenvy",
|
||||
"duration-str",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"hex",
|
||||
|
@ -529,6 +536,20 @@ version = "0.15.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "duration-str"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e172e85f305d6a442b250bf40667ffcb91a24f52c9a1ca59e2fa991ac9b7790"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"nom",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.14"
|
||||
|
@ -1154,6 +1175,12 @@ version = "0.3.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
|
@ -1192,6 +1219,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.4"
|
||||
|
@ -1728,6 +1765,16 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
|
|
34
Cargo.toml
34
Cargo.toml
|
@ -1,28 +1,10 @@
|
|||
[package]
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"server",
|
||||
"cli"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
name = "bin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.33", features = ["full"] }
|
||||
tokio-util = { version="0.7", features = ["io"]}
|
||||
futures-util = "0.3"
|
||||
axum = {version="0.6", features=["macros", "headers", "multipart"]}
|
||||
serde = "1.0"
|
||||
toml = "0.8"
|
||||
render = { git="https://github.com/render-rs/render.rs" }
|
||||
thiserror = "1.0"
|
||||
rand = "0.8"
|
||||
dotenvy = "0.15"
|
||||
markdown = "0.3"
|
||||
axum_oidc = {git="https://git2.zettoit.eu/pfz4/axum_oidc"}
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
|
||||
chacha20 = "0.9"
|
||||
sha3 = "0.10"
|
||||
hex = "0.4"
|
||||
bytes = "1.5"
|
||||
pin-project-lite = "0.2"
|
||||
|
|
19
cli/Cargo.toml
Normal file
19
cli/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "binctl"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version="4.4", features = ["derive"] }
|
||||
reqwest = { version="0.11", features = ["rustls-tls", "stream"], default-features=false}
|
||||
openidconnect = "3.4"
|
||||
thiserror = "1.0"
|
||||
serde = { version="1.0", features = [ "derive" ] }
|
||||
axum = "0.6"
|
||||
tokio = { version = "1.33", features = ["full"] }
|
||||
open = "5.0"
|
||||
tokio-util = { version="0.7.9", features = ["io"]}
|
||||
dirs = "5.0"
|
||||
confy = "0.5"
|
174
cli/src/auth.rs
Normal file
174
cli/src/auth.rs
Normal file
|
@ -0,0 +1,174 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
response::{Html, IntoResponse},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use openidconnect::{
|
||||
core::{CoreAuthenticationFlow, CoreClient, CoreErrorResponseType, CoreProviderMetadata},
|
||||
reqwest::async_http_client,
|
||||
AccessTokenHash, AuthorizationCode, ClaimsVerificationError, ClientId, CsrfToken,
|
||||
DiscoveryError, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, RedirectUrl,
|
||||
RefreshToken, RequestTokenError, Scope, SigningError, StandardErrorResponse, TokenResponse,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("url parse error: {:?}", 0)]
|
||||
UrlParse(#[from] openidconnect::url::ParseError),
|
||||
|
||||
#[error("discovery error: {:?}", 0)]
|
||||
Discovery(#[from] DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>),
|
||||
|
||||
#[error("request token error: {:?}", 0)]
|
||||
RequestToken(
|
||||
#[from]
|
||||
RequestTokenError<
|
||||
openidconnect::reqwest::Error<reqwest::Error>,
|
||||
StandardErrorResponse<CoreErrorResponseType>,
|
||||
>,
|
||||
),
|
||||
|
||||
#[error("claims verification error: {:?}", 0)]
|
||||
ClaimsVerification(#[from] ClaimsVerificationError),
|
||||
|
||||
#[error("signing error: {:?}", 0)]
|
||||
Signing(#[from] SigningError),
|
||||
|
||||
#[error("server did not return an id token")]
|
||||
NoIdToken,
|
||||
|
||||
#[error("invalid access token")]
|
||||
InvalidAccessToken,
|
||||
|
||||
#[error("no response received")]
|
||||
NoResponse,
|
||||
|
||||
#[error("csrf mismatch")]
|
||||
CsrfMismatch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ResponseData {
|
||||
pub code: String,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
pub(crate) async fn login(
|
||||
issuer: &str,
|
||||
client_id: &str,
|
||||
scopes: &[String],
|
||||
refresh_token: &mut Option<String>,
|
||||
) -> Result<String, Error> {
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(
|
||||
IssuerUrl::new(issuer.to_string())?,
|
||||
async_http_client,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create an OpenID Connect client by specifying the client ID, client secret, authorization URL
|
||||
// and token URL.
|
||||
let client = CoreClient::from_provider_metadata(
|
||||
provider_metadata,
|
||||
ClientId::new(client_id.to_string()),
|
||||
None,
|
||||
)
|
||||
// Set the URL the user will be redirected to after the authorization process.
|
||||
.set_redirect_uri(RedirectUrl::new("http://[::1]:8080".to_string())?);
|
||||
|
||||
// Generate a PKCE challenge.
|
||||
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
if let Some(refresh_token) = refresh_token {
|
||||
if let Ok(token_response) = client
|
||||
.exchange_refresh_token(&RefreshToken::new(refresh_token.to_string()))
|
||||
.request_async(async_http_client)
|
||||
.await
|
||||
{
|
||||
eprintln!("authenticated with oidc provider");
|
||||
return Ok(token_response.access_token().secret().clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the full authorization URL.
|
||||
let mut auth = client.authorize_url(
|
||||
CoreAuthenticationFlow::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
);
|
||||
|
||||
for scope in scopes {
|
||||
auth = auth.add_scope(Scope::new(scope.to_string()));
|
||||
}
|
||||
let (auth_url, csrf_token, nonce) = auth
|
||||
// Set the PKCE code challenge.
|
||||
.set_pkce_challenge(pkce_challenge)
|
||||
.url();
|
||||
open::that(auth_url.to_string()).unwrap();
|
||||
eprintln!("a browser should have been opened with the url {auth_url}. please login with your oidc provider.");
|
||||
|
||||
let (fuse_tx, mut fuse_rx) = mpsc::channel::<ResponseData>(1);
|
||||
let app = Router::new()
|
||||
.route("/", get(handle_post))
|
||||
.with_state(Arc::new(fuse_tx));
|
||||
|
||||
let server = axum::Server::bind(&"[::1]:8080".parse().unwrap()).serve(app.into_make_service());
|
||||
|
||||
let data = tokio::select! {
|
||||
x = fuse_rx.recv() => {
|
||||
x
|
||||
}
|
||||
_ = server => {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let data = data.ok_or(Error::NoResponse)?;
|
||||
|
||||
// match csrf_state
|
||||
|
||||
if *csrf_token.secret() != data.state {
|
||||
return Err(Error::CsrfMismatch);
|
||||
}
|
||||
|
||||
let token_response = client
|
||||
.exchange_code(AuthorizationCode::new(data.code))
|
||||
// Set the PKCE code verifier.
|
||||
.set_pkce_verifier(pkce_verifier)
|
||||
.request_async(async_http_client)
|
||||
.await?;
|
||||
|
||||
// Extract the ID token claims after verifying its authenticity and nonce.
|
||||
let id_token = token_response.id_token().ok_or_else(|| Error::NoIdToken)?;
|
||||
let claims = id_token.claims(&client.id_token_verifier(), &nonce)?;
|
||||
|
||||
// Verify the access token hash to ensure that the access token hasn't been substituted for
|
||||
// another user's.
|
||||
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
||||
let actual_access_token_hash =
|
||||
AccessTokenHash::from_token(token_response.access_token(), &id_token.signing_alg()?)?;
|
||||
if actual_access_token_hash != *expected_access_token_hash {
|
||||
return Err(Error::InvalidAccessToken);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_refresh_token) = token_response.refresh_token() {
|
||||
*refresh_token = Some(new_refresh_token.secret().to_string());
|
||||
}
|
||||
|
||||
eprintln!("authenticated with oidc provider");
|
||||
Ok(token_response.access_token().secret().clone())
|
||||
}
|
||||
|
||||
async fn handle_post(
|
||||
State(fuse_tx): State<Arc<mpsc::Sender<ResponseData>>>,
|
||||
Query(data): Query<ResponseData>,
|
||||
) -> impl IntoResponse {
|
||||
fuse_tx.clone().send(data).await;
|
||||
Html("<html><body>Die Anmeldung war erfolgreich. Du kannst dieses Fenster jetzt schließen.<script>window.close()</script></body></html>")
|
||||
}
|
94
cli/src/main.rs
Normal file
94
cli/src/main.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use clap::Parser;
|
||||
use reqwest::{Body, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::stdin;
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::auth::login;
|
||||
|
||||
mod auth;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
pub refresh_token: Option<String>,
|
||||
pub binurl: String,
|
||||
pub issuer: String,
|
||||
pub client_id: String,
|
||||
pub scopes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Args {
|
||||
#[arg(short, long)]
|
||||
content_type: Option<String>,
|
||||
|
||||
#[arg(short, long)]
|
||||
ttl: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
refresh_token: None,
|
||||
binurl: "https://bin.zettoit.eu".to_string(),
|
||||
issuer: "https://auth.zettoit.eu/realms/zettoit".to_string(),
|
||||
client_id: "binctl".to_string(),
|
||||
scopes: vec!["zettoit-bin".to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut cfg: Config = confy::load("binctl", None).unwrap_or_default();
|
||||
|
||||
let args = Args::parse();
|
||||
let access_token = login(
|
||||
&cfg.issuer,
|
||||
&cfg.client_id,
|
||||
cfg.scopes.as_slice(),
|
||||
&mut cfg.refresh_token,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut bin = create_bin(&cfg.binurl, &access_token).await.unwrap();
|
||||
eprintln!("created bin at {}. uploading...", bin);
|
||||
bin.set_query(args.ttl.map(|x| format!("ttl={}", x)).as_deref());
|
||||
|
||||
upload_to_bin(
|
||||
bin.as_ref(),
|
||||
&args
|
||||
.content_type
|
||||
.unwrap_or("application/octet-stream".to_string()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _ = confy::store("binctl", None, cfg);
|
||||
bin.set_query(None);
|
||||
print!("{bin}");
|
||||
}
|
||||
|
||||
async fn create_bin(binurl: &str, access_token: &str) -> Result<Url, reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
Ok(client
|
||||
.get(binurl)
|
||||
.header("Authorization", format!("Bearer {}", access_token))
|
||||
.send()
|
||||
.await?
|
||||
.url()
|
||||
.to_owned())
|
||||
}
|
||||
|
||||
async fn upload_to_bin(url: &str, content_type: &str) -> Result<(), reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post(url)
|
||||
.header("Content-Type", content_type)
|
||||
.body(Body::wrap_stream(ReaderStream::new(stdin())))
|
||||
.send()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
28
flake.nix
28
flake.nix
|
@ -26,20 +26,22 @@
|
|||
nixpkgs.lib.genAttrs [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
] (system: let
|
||||
] (system: function system nixpkgs.legacyPackages.${system});
|
||||
in rec {
|
||||
packages = forAllSystems(system: syspkgs: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default;
|
||||
|
||||
markdownFilter = path: _type: builtins.match ".*md$" path != null;
|
||||
markdownOrCargo = path: type: (markdownFilter path type) || (craneLib.filterCargoSources path type);
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
src = pkgs.lib.cleanSourceWith {
|
||||
src = craneLib.path ./.;
|
||||
filter = markdownOrCargo;
|
||||
filter = path: type:
|
||||
(pkgs.lib.hasSuffix "\.md" path) ||
|
||||
(craneLib.filterCargoSources path type)
|
||||
;
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [ rustToolchain pkg-config ];
|
||||
|
@ -52,18 +54,20 @@
|
|||
|
||||
bin = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
pname = "bin";
|
||||
});
|
||||
in function {
|
||||
inherit bin pkgs;
|
||||
binctl = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
pname = "binctl";
|
||||
});
|
||||
in {
|
||||
packages = forAllSystems({pkgs, bin}: {
|
||||
inherit bin;
|
||||
inherit bin binctl;
|
||||
default = bin;
|
||||
});
|
||||
devShells = forAllSystems({pkgs, bin}: pkgs.mkShell {
|
||||
inputsFrom = bin;
|
||||
devShells = forAllSystems(system: pkgs: pkgs.mkShell {
|
||||
inputsFrom = [packages.${system}.bin packages.${system}.binctl];
|
||||
});
|
||||
hydraJobs."build" = forAllSystems({pkgs, bin}: bin);
|
||||
hydraJobs."bin" = forAllSystems(system: pkgs: packages.${system}.bin);
|
||||
hydraJobs."binctl" = forAllSystems(system: pkgs: packages.${system}.binctl);
|
||||
};
|
||||
}
|
||||
|
|
28
server/Cargo.toml
Normal file
28
server/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "bin"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.33", features = ["full"] }
|
||||
tokio-util = { version="0.7", features = ["io"]}
|
||||
futures-util = "0.3"
|
||||
axum = {version="0.6", features=["macros", "headers", "multipart"]}
|
||||
serde = "1.0"
|
||||
toml = "0.8"
|
||||
render = { git="https://github.com/render-rs/render.rs" }
|
||||
thiserror = "1.0"
|
||||
rand = "0.8"
|
||||
dotenvy = "0.15"
|
||||
markdown = "0.3"
|
||||
axum_oidc = {git="https://git2.zettoit.eu/pfz4/axum_oidc"}
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
|
||||
chacha20 = "0.9"
|
||||
sha3 = "0.10"
|
||||
hex = "0.4"
|
||||
bytes = "1.5"
|
||||
pin-project-lite = "0.2"
|
|
@ -1,9 +1,10 @@
|
|||
#![deny(clippy::unwrap_used)]
|
||||
use duration_str::{deserialize_option_duration, parse_std};
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
env,
|
||||
str::FromStr,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
|
@ -11,7 +12,7 @@ use axum::{
|
|||
body::{HttpBody, StreamBody},
|
||||
debug_handler,
|
||||
extract::{BodyStream, FromRef, FromRequest, Multipart, Path, Query, State},
|
||||
headers::ContentType,
|
||||
headers::{ContentType, Range},
|
||||
http::{
|
||||
header::{self, CONTENT_TYPE},
|
||||
HeaderMap, Request, StatusCode,
|
||||
|
@ -196,7 +197,8 @@ async fn get_index(
|
|||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PostQuery {
|
||||
ttl: Option<u64>,
|
||||
#[serde(deserialize_with = "deserialize_option_duration")]
|
||||
ttl: Option<Duration>,
|
||||
}
|
||||
|
||||
async fn post_item(
|
||||
|
@ -226,7 +228,7 @@ async fn post_item(
|
|||
let mut etag_hasher = Sha3_256::new();
|
||||
let mut size: u64 = 0;
|
||||
|
||||
let mut ttl = params.ttl.unwrap_or(24 * 3600);
|
||||
let mut ttl = params.ttl.unwrap_or(Duration::from_secs(24 * 3600));
|
||||
|
||||
match data {
|
||||
MultipartOrStream::Stream(mut stream) => {
|
||||
|
@ -267,7 +269,7 @@ async fn post_item(
|
|||
}
|
||||
if field.name().unwrap_or_default() == "ttl" {
|
||||
if let Some(mp_ttl) =
|
||||
field.text().await.ok().and_then(|x| x.parse::<u64>().ok())
|
||||
field.text().await.ok().and_then(|x| parse_std(x).ok())
|
||||
{
|
||||
ttl = mp_ttl;
|
||||
}
|
||||
|
@ -277,16 +279,17 @@ async fn post_item(
|
|||
}
|
||||
writer.flush().await?;
|
||||
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
let expires_at = match u64::MAX - ttl > now {
|
||||
true => now + ttl,
|
||||
false => u64::MAX,
|
||||
};
|
||||
if let Some(expires_at) = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)?
|
||||
.checked_add(ttl)
|
||||
.map(|x| x.as_secs())
|
||||
{
|
||||
metadata.expires_at = expires_at;
|
||||
}
|
||||
|
||||
metadata.etag = Some(hex::encode(etag_hasher.finalize()));
|
||||
metadata.size = Some(size);
|
||||
|
||||
metadata.expires_at = expires_at;
|
||||
metadata.to_file(&metadata_path).await?;
|
||||
|
||||
app_state
|
||||
|
@ -355,6 +358,9 @@ async fn get_item(
|
|||
header::CONTENT_LENGTH,
|
||||
metadata.size.unwrap_or_default().into(),
|
||||
);
|
||||
if let Ok(subject) = metadata.subject.parse() {
|
||||
headers.insert("CreatedBy", subject);
|
||||
}
|
||||
|
||||
if let Some(content_type) = metadata.content_type.and_then(|x| x.parse().ok()) {
|
||||
headers.insert(header::CONTENT_TYPE, content_type);
|
Loading…
Reference in a new issue