Skip to content

Commit

Permalink
runtimes/js: add api.static for serving static files
Browse files Browse the repository at this point in the history
  • Loading branch information
eandre committed Sep 9, 2024
1 parent 87011f6 commit 3fca58e
Show file tree
Hide file tree
Showing 15 changed files with 638 additions and 116 deletions.
42 changes: 42 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

297 changes: 197 additions & 100 deletions proto/encore/parser/meta/v1/meta.pb.go

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion proto/encore/parser/meta/v1/meta.proto
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ message RPC {
bool streaming_response = 17;
optional schema.v1.Type handshake_schema = 18; // handshake schema, or nil

// If the endpoint serves static assets.
optional StaticAssets static_assets = 19;

enum AccessType {
PRIVATE = 0;
PUBLIC = 1;
Expand All @@ -116,6 +119,16 @@ message RPC {
}

message ExposeOptions {}

message StaticAssets {
// dir_rel_path is the slash-separated path to the static files directory,
// relative to the app root.
string dir_rel_path = 1;

// not_found_rel_path is the relative path to the file to serve when the requested
// file is not found. It is relative to the files_rel_path directory.
optional string not_found_rel_path = 2;
}
}

message AuthHandler {
Expand Down Expand Up @@ -324,7 +337,7 @@ message PubSubTopic {
int64 ack_deadline = 3; // How long has a consumer got to process and ack a message in nanoseconds
int64 message_retention = 4; // How long is an undelivered message kept in nanoseconds
RetryPolicy retry_policy = 5; // The retry policy for the subscription

// How many messages each instance can process concurrently.
// If not set, the default is provider-specific.
optional int32 max_concurrency = 6;
Expand Down
1 change: 1 addition & 0 deletions runtimes/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ tokio-retry = "0.3.0"
rsa = { version = "0.9.6", features = ["pem"] }
flate2 = "1.0.30"
urlencoding = "2.1.3"
tower-http = { version = "0.5.2", features = ["fs"] }

[build-dependencies]
prost-build = "0.12.3"
Expand Down
6 changes: 6 additions & 0 deletions runtimes/core/src/api/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ pub struct Endpoint {
/// The maximum size of the request body.
/// If None, no limits are applied.
pub body_limit: Option<u64>,

/// The static assets to serve from this endpoint.
/// Set only for static asset endpoints.
pub static_assets: Option<meta::rpc::StaticAssets>,
}

impl Endpoint {
Expand Down Expand Up @@ -299,6 +303,7 @@ pub fn endpoints_from_meta(
exposed,
requires_auth: !ep.ep.allow_unauthenticated,
body_limit: ep.ep.body_limit,
static_assets: ep.ep.static_assets.clone(),
};

endpoint_map.insert(
Expand Down Expand Up @@ -369,6 +374,7 @@ impl EndpointHandler {
.into_parts();

// Authenticate the request from the platform, if applicable.
// #[allow(clippy::manual_unwrap_or_default)]
let platform_seal_of_approval = match self.authenticate_platform(&parts) {
Ok(seal) => seal,
Err(_err) => None,
Expand Down
1 change: 1 addition & 0 deletions runtimes/core/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod paths;
pub mod reqauth;
pub mod schema;
mod server;
mod static_assets;
pub mod websocket;

pub use endpoint::*;
Expand Down
27 changes: 21 additions & 6 deletions runtimes/core/src/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::api;
use crate::api::endpoint::{EndpointHandler, SharedEndpointData};
use crate::api::paths::Pather;
use crate::api::reqauth::svcauth;
use crate::api::static_assets::StaticAssetsHandler;
use crate::api::{paths, reqauth, schema, BoxedHandler, EndpointMap};
use crate::encore::parser::meta::v1 as meta;
use crate::names::EndpointName;
Expand Down Expand Up @@ -69,13 +70,33 @@ impl Server {
ep: endpoints.get(ep).unwrap().to_owned(),
}));

let shared = Arc::new(SharedEndpointData {
tracer,
platform_auth,
inbound_svc_auth,
});

let mut register = |paths: &[(Arc<api::Endpoint>, Vec<String>)],
mut router: axum::Router|
-> axum::Router {
for (ep, paths) in paths {
match schema::method_filter(ep.methods()) {
Some(filter) => {
let server_handler = ServerHandler::default();

if let Some(assets) = &ep.static_assets {
// For static asset routes, configure the static asset handler directly.
// There's no need to defer it for dynamic runtime registration.
let static_handler = StaticAssetsHandler::new(assets);

let handler = EndpointHandler {
endpoint: ep.clone(),
handler: Arc::new(static_handler),
shared: shared.clone(),
};
server_handler.set(handler);
}

let handler = axum::routing::on(filter, server_handler.clone());
for path in paths {
router = router.route(path, handler.clone());
Expand All @@ -100,12 +121,6 @@ impl Server {
// Register our fallback route.
router = router.fallback_service(fallback_router);

let shared = Arc::new(SharedEndpointData {
tracer,
platform_auth,
inbound_svc_auth,
});

Ok(Self {
endpoints,
hosted_endpoints: Mutex::new(handler_map),
Expand Down
157 changes: 157 additions & 0 deletions runtimes/core/src/api/static_assets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use std::{convert::Infallible, future::Future, path::PathBuf, pin::Pin, sync::Arc};

use http_body_util::Empty;
use std::fmt::Debug;
use std::io;
use tower_http::services::{fs::ServeDir, ServeFile};
use tower_service::Service;

use crate::{encore::parser::meta::v1 as meta, model::RequestData};

use super::{BoxedHandler, Error, HandlerRequest, ResponseData};

#[derive(Clone, Debug)]
pub struct StaticAssetsHandler {
service: Arc<dyn FileServer>,
not_found_handler: bool,
}

impl StaticAssetsHandler {
pub fn new(cfg: &meta::rpc::StaticAssets) -> Self {
let service = ServeDir::new(PathBuf::from(&cfg.dir_rel_path));

let not_found = cfg
.not_found_rel_path
.as_ref()
.map(|p| ServeFile::new(PathBuf::from(p)));
let not_found_handler = not_found.is_some();

let service: Arc<dyn FileServer> = match not_found {
Some(not_found) => Arc::new(service.not_found_service(not_found)),
None => Arc::new(service),
};
StaticAssetsHandler {
service,
not_found_handler,
}
}
}

impl BoxedHandler for StaticAssetsHandler {
fn call(
self: Arc<Self>,
req: HandlerRequest,
) -> Pin<Box<dyn Future<Output = ResponseData> + Send + 'static>> {
Box::pin(async move {
let RequestData::RPC(data) = &req.data else {
return ResponseData::Typed(Err(Error::internal(anyhow::anyhow!(
"invalid request data type"
))));
};

// Find the file path from the request.
let file_path = match &data.path_params {
Some(params) => params
.values()
.next()
.and_then(|v| v.as_str())
.map(|s| format!("/{}", s))
.unwrap_or("/".to_string()),
None => "/".to_string(),
};

let httpreq = {
let mut b = axum::http::request::Request::builder();
{
// Copy headers into request.
let headers = b.headers_mut().unwrap();
for (k, v) in &data.req_headers {
headers.append(k.clone(), v.clone());
}
}
match b
.method(data.method)
.uri(file_path)
.body(Empty::<bytes::Bytes>::new())
{
Ok(req) => req,
Err(e) => {
return ResponseData::Typed(Err(Error::invalid_argument(
"invalid file path",
e,
)));
}
}
};

match self.service.serve(httpreq).await {
Ok(resp) => match resp.status() {
// 1xx, 2xx, 3xx are all considered successful.
code if code.is_informational()
|| code.is_success()
|| code.is_redirection() =>
{
ResponseData::Raw(resp.map(axum::body::Body::new))
}
axum::http::StatusCode::NOT_FOUND => {
// If we have a not found handler, use that directly.
if self.not_found_handler {
ResponseData::Raw(resp.map(axum::body::Body::new))
} else {
// Otherwise return our standard not found error.
ResponseData::Typed(Err(Error::not_found("file not found")))
}
}
axum::http::StatusCode::METHOD_NOT_ALLOWED => ResponseData::Typed(Err(Error {
code: super::ErrCode::InvalidArgument,
internal_message: None,
message: "method not allowed".to_string(),
stack: None,
})),
axum::http::StatusCode::INTERNAL_SERVER_ERROR => {
ResponseData::Typed(Err(Error {
code: super::ErrCode::Internal,
internal_message: None,
message: "failed to serve static asset".to_string(),
stack: None,
}))
}
code => ResponseData::Typed(Err(Error::internal(anyhow::anyhow!(
"failed to serve static asset: {}",
code,
)))),
},
Err(e) => ResponseData::Typed(Err(Error::internal(e))),
}
})
}
}

trait FileServer: Sync + Send + Debug {
fn serve(
&self,
req: axum::http::Request<Empty<bytes::Bytes>>,
) -> Pin<Box<dyn Future<Output = Result<FileRes, io::Error>> + Send + 'static>>;
}

type FileReq = axum::http::Request<Empty<bytes::Bytes>>;
type FileRes = axum::http::Response<tower_http::services::fs::ServeFileSystemResponseBody>;

impl<F> FileServer for ServeDir<F>
where
F: Service<FileReq, Response = FileRes, Error = Infallible>
+ Debug
+ Clone
+ Sync
+ Send
+ 'static,
F::Future: Send + 'static,
{
fn serve(
&self,
req: axum::http::Request<Empty<bytes::Bytes>>,
) -> Pin<Box<dyn Future<Output = Result<FileRes, io::Error>> + Send + 'static>> {
let mut this = self.clone();
Box::pin(async move { this.try_call(req).await })
}
}
2 changes: 1 addition & 1 deletion runtimes/core/src/pubsub/sqs_sns/sub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ impl fetcher::Fetcher for Arc<SqsFetcher> {
let requeue_delay = self
.requeue_policy
.clone()
.nth((attempt - 1).max(0) as usize)
.nth((attempt.max(1) - 1) as usize)
.unwrap_or(Duration::from_secs(1));

let requeue_action = RequeueMessageAction {
Expand Down
Loading

0 comments on commit 3fca58e

Please sign in to comment.