feat: add session-owned uploaded file attachments
This commit is contained in:
@@ -40,6 +40,7 @@ use worker_runtime::fs_store::FsRuntimeStoreOptions;
|
||||
use worker_runtime::http_server::{
|
||||
RuntimeHttpConfigBundleAvailabilityResponse, RuntimeHttpConfigBundleSyncRequest,
|
||||
RuntimeHttpErrorResponse, RuntimeHttpRepositoryAccessResponse, RuntimeHttpSummaryResponse,
|
||||
RuntimeHttpUploadedFileDeleteResponse, RuntimeHttpUploadedFileResponse,
|
||||
RuntimeHttpWorkerCompletionsRequest, RuntimeHttpWorkerCompletionsResponse,
|
||||
RuntimeHttpWorkerDeleteResponse, RuntimeHttpWorkerInputResponse,
|
||||
RuntimeHttpWorkerLifecycleRequest, RuntimeHttpWorkerLifecycleResponse,
|
||||
@@ -1041,6 +1042,34 @@ pub trait WorkspaceWorkerRuntime: Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_worker_file(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
_file_name: &str,
|
||||
_media_type: &str,
|
||||
_content: &[u8],
|
||||
) -> Result<protocol::UploadedFileRef, RuntimeRegistryError> {
|
||||
Err(RuntimeRegistryError::RuntimeOperationFailed {
|
||||
runtime_id: self.runtime_id().to_string(),
|
||||
code: "worker_file_upload_unsupported".to_string(),
|
||||
message: format!("runtime does not support file upload for worker `{worker_id}`"),
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_worker_uploaded_file(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
_artifact_id: &str,
|
||||
) -> Result<(), RuntimeRegistryError> {
|
||||
Err(RuntimeRegistryError::RuntimeOperationFailed {
|
||||
runtime_id: self.runtime_id().to_string(),
|
||||
code: "worker_file_delete_unsupported".to_string(),
|
||||
message: format!(
|
||||
"runtime does not support uploaded-file deletion for worker `{worker_id}`"
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn worker_completions(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
@@ -1543,6 +1572,50 @@ impl RuntimeRegistry {
|
||||
Ok(runtime.send_input(worker_id, request))
|
||||
}
|
||||
|
||||
pub fn upload_worker_file(
|
||||
&self,
|
||||
worker: &RuntimeWorkerRef,
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
content: &[u8],
|
||||
) -> Result<protocol::UploadedFileRef, RuntimeRegistryError> {
|
||||
let runtime_id = worker.runtime_id.as_str();
|
||||
let worker_id = worker.worker_id.as_str();
|
||||
validate_backend_identifier("runtime_id", runtime_id)?;
|
||||
validate_backend_identifier("worker_id", worker_id)?;
|
||||
let runtime = self.runtime(runtime_id)?;
|
||||
let lookup = runtime.worker(worker_id);
|
||||
if lookup.worker.is_none() {
|
||||
return Err(operation_failed_or_unknown_worker(
|
||||
runtime_id,
|
||||
worker_id,
|
||||
lookup.diagnostics,
|
||||
));
|
||||
}
|
||||
runtime.upload_worker_file(worker_id, file_name, media_type, content)
|
||||
}
|
||||
|
||||
pub fn delete_worker_uploaded_file(
|
||||
&self,
|
||||
worker: &RuntimeWorkerRef,
|
||||
artifact_id: &str,
|
||||
) -> Result<(), RuntimeRegistryError> {
|
||||
let runtime_id = worker.runtime_id.as_str();
|
||||
let worker_id = worker.worker_id.as_str();
|
||||
validate_backend_identifier("runtime_id", runtime_id)?;
|
||||
validate_backend_identifier("worker_id", worker_id)?;
|
||||
let runtime = self.runtime(runtime_id)?;
|
||||
let lookup = runtime.worker(worker_id);
|
||||
if lookup.worker.is_none() {
|
||||
return Err(operation_failed_or_unknown_worker(
|
||||
runtime_id,
|
||||
worker_id,
|
||||
lookup.diagnostics,
|
||||
));
|
||||
}
|
||||
runtime.delete_worker_uploaded_file(worker_id, artifact_id)
|
||||
}
|
||||
|
||||
pub fn worker_completions(
|
||||
&self,
|
||||
worker: &RuntimeWorkerRef,
|
||||
@@ -2509,6 +2582,46 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_worker_file(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
content: &[u8],
|
||||
) -> Result<protocol::UploadedFileRef, RuntimeRegistryError> {
|
||||
let worker_ref =
|
||||
self.worker_ref(worker_id)
|
||||
.ok_or_else(|| RuntimeRegistryError::UnknownWorker {
|
||||
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id),
|
||||
})?;
|
||||
self.runtime
|
||||
.upload_worker_file(&worker_ref, file_name, media_type, content)
|
||||
.map_err(|error| RuntimeRegistryError::RuntimeOperationFailed {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
code: "embedded_worker_file_upload_failed".to_string(),
|
||||
message: error.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_worker_uploaded_file(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
artifact_id: &str,
|
||||
) -> Result<(), RuntimeRegistryError> {
|
||||
let worker_ref =
|
||||
self.worker_ref(worker_id)
|
||||
.ok_or_else(|| RuntimeRegistryError::UnknownWorker {
|
||||
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id),
|
||||
})?;
|
||||
self.runtime
|
||||
.delete_worker_uploaded_file(&worker_ref, artifact_id)
|
||||
.map_err(|error| RuntimeRegistryError::RuntimeOperationFailed {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
code: "embedded_worker_file_delete_failed".to_string(),
|
||||
message: error.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn worker_completions(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
@@ -2848,6 +2961,16 @@ impl RemoteWorkerRuntime {
|
||||
self.send_json(path, self.http.post(self.endpoint(path)).json(body))
|
||||
}
|
||||
|
||||
fn post_bytes<T>(&self, path: &str, body: &[u8]) -> Result<T, RuntimeDiagnostic>
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
{
|
||||
self.send_json(
|
||||
path,
|
||||
self.http.post(self.endpoint(path)).body(body.to_vec()),
|
||||
)
|
||||
}
|
||||
|
||||
fn delete_json<T>(&self, path: &str) -> Result<T, RuntimeDiagnostic>
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
@@ -3536,6 +3659,47 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_worker_file(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
content: &[u8],
|
||||
) -> Result<protocol::UploadedFileRef, RuntimeRegistryError> {
|
||||
let path = format!(
|
||||
"/v1/workers/{}/attachments?file_name={}&media_type={}",
|
||||
url_path_segment_encode(worker_id),
|
||||
url_query_value_encode(file_name),
|
||||
url_query_value_encode(media_type),
|
||||
);
|
||||
self.post_bytes::<RuntimeHttpUploadedFileResponse>(&path, content)
|
||||
.map(|response| response.file)
|
||||
.map_err(|diagnostic| RuntimeRegistryError::RuntimeOperationFailed {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
code: diagnostic.code,
|
||||
message: diagnostic.message,
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_worker_uploaded_file(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
artifact_id: &str,
|
||||
) -> Result<(), RuntimeRegistryError> {
|
||||
let path = format!(
|
||||
"/v1/workers/{}/attachments/{}",
|
||||
url_path_segment_encode(worker_id),
|
||||
url_path_segment_encode(artifact_id),
|
||||
);
|
||||
self.delete_json::<RuntimeHttpUploadedFileDeleteResponse>(&path)
|
||||
.map(|_| ())
|
||||
.map_err(|diagnostic| RuntimeRegistryError::RuntimeOperationFailed {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
code: diagnostic.code,
|
||||
message: diagnostic.message,
|
||||
})
|
||||
}
|
||||
|
||||
fn worker_completions(
|
||||
&self,
|
||||
worker_id: &str,
|
||||
|
||||
@@ -3,8 +3,9 @@ use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
|
||||
use axum::body::Bytes;
|
||||
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
|
||||
use axum::extract::{Extension, Path as AxumPath, Query, Request, State};
|
||||
use axum::extract::{DefaultBodyLimit, Extension, Path as AxumPath, Query, Request, State};
|
||||
use axum::http::header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION, ORIGIN, SET_COOKIE};
|
||||
use axum::http::{HeaderMap, Method, StatusCode, Uri};
|
||||
use axum::middleware::{self, Next};
|
||||
@@ -156,8 +157,9 @@ use worker_runtime::catalog::{
|
||||
};
|
||||
use worker_runtime::config_bundle::ConfigBundle;
|
||||
use worker_runtime::http_server::{
|
||||
RuntimeHttpConfigBundleAvailabilityResponse, RuntimeHttpConfigBundlesResponse,
|
||||
RuntimeHttpSummaryResponse, RuntimeHttpWorkerResponse, RuntimeHttpWorkersResponse,
|
||||
MAX_WORKER_FILE_UPLOAD_BYTES, RuntimeHttpConfigBundleAvailabilityResponse,
|
||||
RuntimeHttpConfigBundlesResponse, RuntimeHttpSummaryResponse, RuntimeHttpWorkerResponse,
|
||||
RuntimeHttpWorkersResponse,
|
||||
};
|
||||
use worker_runtime::identity::{RuntimeWorkerRef, WorkerId};
|
||||
|
||||
@@ -2679,10 +2681,26 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/input",
|
||||
post(send_runtime_worker_input),
|
||||
)
|
||||
.route(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/attachments",
|
||||
post(upload_runtime_worker_file).layer(DefaultBodyLimit::max(MAX_WORKER_FILE_UPLOAD_BYTES)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/attachments/{artifact_id}",
|
||||
delete(delete_runtime_worker_uploaded_file),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/input",
|
||||
post(scoped_send_runtime_worker_input),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachments",
|
||||
post(scoped_upload_runtime_worker_file).layer(DefaultBodyLimit::max(MAX_WORKER_FILE_UPLOAD_BYTES)),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachments/{artifact_id}",
|
||||
delete(scoped_delete_runtime_worker_uploaded_file),
|
||||
)
|
||||
.route(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/completions",
|
||||
post(runtime_worker_completions),
|
||||
@@ -10733,6 +10751,51 @@ async fn scoped_execute_runtime_cleanup(
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct WorkerFileUploadQuery {
|
||||
file_name: String,
|
||||
media_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct WorkerFileUploadResponse {
|
||||
file: protocol::UploadedFileRef,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct WorkerFileDeleteResponse {
|
||||
deleted: bool,
|
||||
}
|
||||
|
||||
async fn scoped_upload_runtime_worker_file(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
|
||||
Query(query): Query<WorkerFileUploadQuery>,
|
||||
body: Bytes,
|
||||
) -> ApiResult<Json<WorkerFileUploadResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
upload_runtime_worker_file(
|
||||
State(api),
|
||||
AxumPath((path.worker.runtime_id, path.worker.worker_id)),
|
||||
Query(query),
|
||||
body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn scoped_delete_runtime_worker_uploaded_file(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath((path, artifact_id)): AxumPath<(ScopedRuntimeWorkerPath, String)>,
|
||||
) -> ApiResult<Json<WorkerFileDeleteResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
delete_runtime_worker_uploaded_file(
|
||||
State(api),
|
||||
AxumPath((path.worker.runtime_id, path.worker.worker_id, artifact_id)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn scoped_send_runtime_worker_input(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
|
||||
@@ -13260,6 +13323,31 @@ async fn send_runtime_worker_input(
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
async fn upload_runtime_worker_file(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
|
||||
Query(query): Query<WorkerFileUploadQuery>,
|
||||
body: Bytes,
|
||||
) -> ApiResult<Json<WorkerFileUploadResponse>> {
|
||||
let worker = resolve_workspace_worker_reference(&api, &runtime_id, &worker_id)?;
|
||||
let file = api
|
||||
.runtime
|
||||
.upload_worker_file(&worker, &query.file_name, &query.media_type, &body)
|
||||
.map_err(|err| err.into_error())?;
|
||||
Ok(Json(WorkerFileUploadResponse { file }))
|
||||
}
|
||||
|
||||
async fn delete_runtime_worker_uploaded_file(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath((runtime_id, worker_id, artifact_id)): AxumPath<(String, String, String)>,
|
||||
) -> ApiResult<Json<WorkerFileDeleteResponse>> {
|
||||
let worker = resolve_workspace_worker_reference(&api, &runtime_id, &worker_id)?;
|
||||
api.runtime
|
||||
.delete_worker_uploaded_file(&worker, &artifact_id)
|
||||
.map_err(|err| err.into_error())?;
|
||||
Ok(Json(WorkerFileDeleteResponse { deleted: true }))
|
||||
}
|
||||
|
||||
async fn runtime_worker_completions(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
|
||||
|
||||
Reference in New Issue
Block a user