diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 6a4c0d2c41..4e3ece03b3 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -6323,12 +6323,7 @@ paths: s3_objects: type: array items: - type: object - properties: - s3: - type: string - storage: - type: string + $ref: "#/components/schemas/S3Object" responses: "200": description: signed s3 tokens @@ -6337,7 +6332,7 @@ paths: schema: type: array items: - type: string + $ref: "#/components/schemas/S3Object" /w/{workspace}/apps_u/execute_component/{path}: post: @@ -16053,3 +16048,17 @@ components: required: - account_id - installation_id + + S3Object: + type: object + properties: + s3: + type: string + filename: + type: string + storage: + type: string + presigned: + type: string + required: + - s3 \ No newline at end of file diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 3074befc19..7bc321a9f8 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -70,10 +70,14 @@ use windmill_common::{ use windmill_git_sync::{handle_deployment_metadata, DeployedObject}; use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel}; +#[cfg(feature = "parquet")] +use hmac::Mac; #[cfg(feature = "parquet")] use windmill_common::{ jwt, + oauth2::HmacSha256, s3_helpers::{build_object_store_client, S3Object}, + variables::get_workspace_key, }; pub fn workspaced_service() -> Router { @@ -236,7 +240,8 @@ pub struct S3Input { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct S3Key { s3_path: String, - resource: String, + #[serde(skip_serializing_if = "Option::is_none")] + storage: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -1568,18 +1573,6 @@ struct S3DeleteTokenClaims { pub exp: usize, } -#[cfg(feature = "parquet")] -#[derive(Serialize, Deserialize)] -struct SignedS3TokenClaims { - file_key: String, - storage: Option, - workspace: String, - pub exp: usize, -} - -#[cfg(feature = "parquet")] -const S3_TOKEN_PREFIX: &str = "__wm_s3_token_"; - #[cfg(feature = "parquet")] #[derive(Deserialize)] struct S3TokenRequestBody { @@ -1587,23 +1580,74 @@ struct S3TokenRequestBody { } #[cfg(feature = "parquet")] async fn sign_s3_objects( + Extension(db): Extension, Path(w_id): Path, Json(body): Json, -) -> Result>> { - let futures = body.s3_objects.iter().map(|s3_object| async { - jwt::encode_with_internal_secret(SignedS3TokenClaims { - file_key: s3_object.s3.clone(), - storage: s3_object.storage.clone(), - workspace: w_id.clone(), - exp: (chrono::Utc::now() + chrono::Duration::hours(12)).timestamp() as usize, - }) - .await - .map(|token| format!("{}{}", S3_TOKEN_PREFIX, token)) +) -> Result>> { + let workspace_key = get_workspace_key(&w_id, &db).await?; + + let futures = body.s3_objects.into_iter().map(|s3_object| async { + let exp = (chrono::Utc::now() + chrono::Duration::hours(12)).timestamp(); + let mut message = format!("file_key={}&exp={}", s3_object.s3.clone(), exp); + if let Some(ref storage) = s3_object.storage { + message = format!("{}&storage={}", message, storage); + } + + let mut max = HmacSha256::new_from_slice(workspace_key.as_bytes()) + .map_err(|err| Error::internal_err(format!("Failed to create hmac: {}", err)))?; + max.update(message.as_bytes()); + let result = max.finalize(); + let signature = hex::encode(result.into_bytes()); + + let presigned = format!("exp={}&sig={}", exp, signature); + + Ok::<_, Error>(S3Object { presigned: Some(presigned), ..s3_object }) }); - let tokens = futures::future::try_join_all(futures).await?; + let signed_s3_objects = futures::future::try_join_all(futures).await?; - Ok(Json(tokens)) + Ok(Json(signed_s3_objects)) +} + +#[cfg(feature = "parquet")] +async fn validate_s3_signature(s3_object: &S3Object, w_id: &str, db: &DB) -> Result<()> { + use url::form_urlencoded; + + let workspace_key = get_workspace_key(w_id, &db).await?; + + let params: HashMap<_, _> = + form_urlencoded::parse(s3_object.presigned.as_ref().unwrap().as_bytes()) + .into_owned() + .collect(); + + let exp = params + .get("exp") + .map(|s| s.parse::().unwrap_or_default()) + .ok_or_else(|| Error::NotAuthorized("Missing exp".to_string()))?; + + let signature = params + .get("sig") + .ok_or_else(|| Error::NotAuthorized("Missing signature".to_string()))?; + + let mut message = format!("file_key={}&exp={}", s3_object.s3.clone(), exp); + if let Some(ref storage) = s3_object.storage { + message = format!("{}&storage={}", message, storage); + } + + let mut mac = HmacSha256::new_from_slice(workspace_key.as_bytes()) + .map_err(|err| Error::internal_err(format!("Failed to create hmac: {}", err)))?; + + mac.update(message.as_bytes()); + + let signature_bytes = hex::decode(signature)?; + mac.verify_slice(&signature_bytes) + .map_err(|err| Error::NotAuthorized(format!("Invalid signature: {}", err)))?; + + if exp < chrono::Utc::now().timestamp() { + return Err(Error::NotAuthorized("Signature expired".to_string())); + } + + Ok(()) } #[cfg(not(feature = "parquet"))] @@ -2014,7 +2058,7 @@ async fn get_on_behalf_authed_from_app( async fn check_if_allowed_to_access_s3_file_from_app( db: &DB, opt_authed: &Option, - file_query: &mut LoadImagePreviewQuery, + file_query: &mut S3Object, w_id: &str, path: &str, policy: &Policy, @@ -2022,15 +2066,8 @@ async fn check_if_allowed_to_access_s3_file_from_app( // if anonymous, check that the file was the result of an app script ran by an anonymous user in the last 3 hours // otherwise, if logged in, allow any file (TODO: change that when we implement better s3 policy) - if file_query.file_key.starts_with(S3_TOKEN_PREFIX) { - let token = file_query.file_key.strip_prefix(S3_TOKEN_PREFIX).unwrap(); - let claims = jwt::decode_with_internal_secret::(token).await?; - if claims.workspace != w_id { - return Err(Error::BadRequest("Invalid workspace in token".to_string())); - } - file_query.file_key = claims.file_key; - file_query.storage = claims.storage; - return Ok(()); + if file_query.presigned.is_some() { + validate_s3_signature(file_query, w_id, &db).await?; } let allowed = opt_authed.is_some() @@ -2039,7 +2076,7 @@ async fn check_if_allowed_to_access_s3_file_from_app( .as_ref() .unwrap() .iter() - .any(|key| key.s3_path == file_query.file_key) + .any(|key| key.s3_path == file_query.s3 && key.storage == file_query.storage) || { sqlx::query_scalar!( r#"SELECT EXISTS ( @@ -2051,7 +2088,7 @@ async fn check_if_allowed_to_access_s3_file_from_app( AND script_path LIKE $3 || '/%' AND result @> ('{"s3":"' || $1 || '"}')::jsonb )"#, - file_query.file_key, + file_query.s3, w_id, path, ) @@ -2069,29 +2106,18 @@ async fn check_if_allowed_to_access_s3_file_from_app( #[cfg(feature = "parquet")] #[derive(Deserialize)] -pub struct FileQueryWithForceViewerAllowedS3Keys { +pub struct S3ObjectWithForceViewerAllowedS3Keys { #[serde(flatten)] - pub file_query: LoadImagePreviewQuery, + pub file_query: S3Object, pub force_viewer_allowed_s3_keys: Option, } -#[cfg(feature = "parquet")] -impl From for DownloadFileQuery { - fn from(query: LoadImagePreviewQuery) -> Self { - DownloadFileQuery { - file_key: query.file_key, - storage: query.storage, - s3_resource_path: None, - } - } -} - #[cfg(feature = "parquet")] async fn download_s3_file_from_app( OptAuthed(opt_authed): OptAuthed, Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, - Query(mut query): Query, + Query(mut query): Query, ) -> Result { let path = path.to_path(); @@ -2123,7 +2149,11 @@ async fn download_s3_file_from_app( None, "", &w_id, - query.file_query.into(), + DownloadFileQuery { + file_key: query.file_query.s3, + s3_resource_path: None, + storage: query.file_query.storage, + }, ) .await } @@ -2140,7 +2170,7 @@ async fn load_s3_file_image_preview_from_app( OptAuthed(opt_authed): OptAuthed, Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, - Query(mut query): Query, + Query(mut query): Query, ) -> Result { let path = path.to_path(); @@ -2157,7 +2187,14 @@ async fn load_s3_file_image_preview_from_app( ) .await?; - load_image_preview_internal(on_behalf_authed, &db, "", &w_id, query).await + load_image_preview_internal( + on_behalf_authed, + &db, + "", + &w_id, + LoadImagePreviewQuery { file_key: query.s3, storage: query.storage }, + ) + .await } fn get_on_behalf_of(policy: &Policy) -> Result<(String, String)> { diff --git a/backend/windmill-common/src/s3_helpers.rs b/backend/windmill-common/src/s3_helpers.rs index 2c5a76a2e1..e3dff5350c 100644 --- a/backend/windmill-common/src/s3_helpers.rs +++ b/backend/windmill-common/src/s3_helpers.rs @@ -111,13 +111,15 @@ pub struct S3AwsOidcResource { pub audience: Option, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct S3Object { pub s3: String, #[serde(skip_serializing_if = "Option::is_none")] pub storage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub filename: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub presigned: Option, } #[cfg(feature = "parquet")] diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 582b9c3abe..3167a7faea 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -902,7 +902,7 @@ pub async fn get_cached_resource_value_if_valid( S3Object { s3: s3_file_key.clone(), storage: resource.storage.clone(), - filename: None, + ..Default::default() }, ) .await; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 19a7ba80c2..c7127b4173 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -79,7 +79,8 @@ "y-monaco": "^0.1.4", "y-websocket": "^1.5.4", "yaml": "^2.3.4", - "yjs": "^13.6.7" + "yjs": "^13.6.7", + "zod": "^3.24.2" }, "devDependencies": { "@floating-ui/core": "^1.3.1", @@ -11085,9 +11086,9 @@ }, "node_modules/zod": { "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "license": "MIT", - "optional": true, - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index f5e7c1cbd0..d2cb878ce5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -152,7 +152,8 @@ "y-monaco": "^0.1.4", "y-websocket": "^1.5.4", "yaml": "^2.3.4", - "yjs": "^13.6.7" + "yjs": "^13.6.7", + "zod": "^3.24.2" }, "peerDependencies": { "svelte": "^4.0.0" diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index 554de1ae1f..5dffbf553f 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -765,18 +765,6 @@ }} /> {/await} - {:else} {/if} + {:else if inputCat == 'object' || inputCat == 'resource-object' || isListJson} {#if oneOf && oneOf.length >= 2} diff --git a/frontend/src/lib/components/DisplayResult.svelte b/frontend/src/lib/components/DisplayResult.svelte index 09ae37c888..d9eecafc4d 100644 --- a/frontend/src/lib/components/DisplayResult.svelte +++ b/frontend/src/lib/components/DisplayResult.svelte @@ -776,7 +776,7 @@ appPath ? 'apps_u/load_image_preview/' + appPath : 'job_helpers/load_image_preview' - }?file_key=${encodeURIComponent(result.s3)}` + + }?${appPath ? 's3' : 'file_key'}=${encodeURIComponent(result.s3)}` + (result.storage ? `&storage=${result.storage}` : '')} /> @@ -788,7 +788,7 @@ appPath ? 'apps_u/load_image_preview/' + appPath : 'job_helpers/load_image_preview' - }?file_key=${encodeURIComponent(result.s3)}` + + }?${appPath ? 's3' : 'file_key'}=${encodeURIComponent(result.s3)}` + (result.storage ? `&storage=${result.storage}` : '')} /> diff --git a/frontend/src/lib/components/S3ObjectPicker.svelte b/frontend/src/lib/components/S3ObjectPicker.svelte index e19af40079..f877681615 100644 --- a/frontend/src/lib/components/S3ObjectPicker.svelte +++ b/frontend/src/lib/components/S3ObjectPicker.svelte @@ -66,18 +66,6 @@ bind:value /> {/await} - {:else} {/if} + diff --git a/frontend/src/lib/components/apps/components/display/AppImage.svelte b/frontend/src/lib/components/apps/components/display/AppImage.svelte index a7be34ca3a..c38c6fb1d2 100644 --- a/frontend/src/lib/components/apps/components/display/AppImage.svelte +++ b/frontend/src/lib/components/apps/components/display/AppImage.svelte @@ -11,7 +11,7 @@ import ResolveStyle from '../helpers/ResolveStyle.svelte' import { defaultIfEmptyString } from '$lib/utils' import { userStore } from '$lib/stores' - import { computeS3ImageViewerPolicy } from '../../editor/appUtilsS3' + import { computeS3ImageViewerPolicy, isPartialS3Object } from '../../editor/appUtilsS3' export let id: string export let configuration: RichConfigurations @@ -22,7 +22,7 @@ if (!isEditor) { return undefined } - const policy = computeS3ImageViewerPolicy(configuration, $app) + const policy = computeS3ImageViewerPolicy(configuration) return policy } @@ -46,14 +46,17 @@ let imageUrl: string | undefined = undefined - async function getS3Image(source: string | undefined) { + async function getS3Image(source: string | undefined, storage?: string) { if (!source) return '' const appPathOrUser = defaultIfEmptyString( $appPath, `u/${$userStore?.username ?? 'unknown'}/newapp` ) const params = new URLSearchParams() - params.append('file_key', source) + params.append('s3', source) + if (storage) { + params.append('storage', storage) + } const forceViewerPolicies = computeForceViewerPolicies() if (forceViewerPolicies) { @@ -64,7 +67,11 @@ } async function loadImage() { - if ( + if (isPartialS3Object(resolvedConfig.source)) { + imageUrl = await getS3Image(resolvedConfig.source.s3, resolvedConfig.source.storage) + } else if (resolvedConfig.source && typeof resolvedConfig.source !== 'string') { + throw new Error('Invalid s3 object' + typeof resolvedConfig.source) + } else if ( resolvedConfig.sourceKind === 's3 (workspace storage)' || resolvedConfig.source?.startsWith('s3://') ) { diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index ae669ba17e..46fde6f024 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -352,10 +352,10 @@ .filter((x) => (x.data as AppComponent).type === 'imagecomponent') .map((x) => { const c = x.data as AppComponent - const config = c.configuration as any - return computeS3ImageViewerPolicy(config, $app) + const config = c.configuration + return computeS3ImageViewerPolicy(config) }) - .filter(Boolean) as { s3_path?: string | undefined; resource?: string | undefined }[] + .filter(Boolean) as { s3_path: string; storage?: string | undefined }[] policy.allowed_s3_keys = s3FileKeys } diff --git a/frontend/src/lib/components/apps/editor/appUtilsS3.ts b/frontend/src/lib/components/apps/editor/appUtilsS3.ts index 5938db0435..60ca4f1e6b 100644 --- a/frontend/src/lib/components/apps/editor/appUtilsS3.ts +++ b/frontend/src/lib/components/apps/editor/appUtilsS3.ts @@ -1,7 +1,7 @@ import type { AppInput, EvalInputV2 } from '../inputType' -import type { App } from '../types' +import type { App, RichConfigurations } from '../types' import { collectOneOfFields } from './appUtils' - +import { z } from 'zod' function filenameExprToRegex(template: string) { const filenameEscaped = template.replaceAll('${file.name}', '') // replace filename with placeholder const escapedTemplate = filenameEscaped @@ -51,12 +51,12 @@ export function computeS3FileInputPolicy(s3Config: any, app: App) { ? resourceInput.value ? [removeResourcePrefix(resourceInput.value)] : [] - : collectOneOfFields( + : (collectOneOfFields( { s3_resource: resourceInput }, app - ).s3_resource?.map((s) => removeResourcePrefix(s)) ?? [] + ).s3_resource?.map((s) => removeResourcePrefix(s)) ?? []) : [] const allow_user_resources = @@ -81,12 +81,35 @@ export function computeS3FileInputPolicy(s3Config: any, app: App) { } } -export function computeS3ImageViewerPolicy(config: any, app: App) { - if ( - config.sourceKind.value === 's3 (workspace storage)' || - config.source.value.startsWith('s3://') - ) { - return { s3_path: config.source.value.replace('s3://', ''), resource: 'default' } +const partialS3ObjectSchema = z.object({ + s3: z.string(), + storage: z.string().optional() +}) + +export function isPartialS3Object(input: unknown): input is z.infer { + return partialS3ObjectSchema.safeParse(input).success +} + +export function computeS3ImageViewerPolicy(config: RichConfigurations) { + if (config.source.type === 'static') { + if (isPartialS3Object(config.source.value)) { + return { + s3_path: config.source.value.s3, + storage: config.source.value.storage + } + } else if ( + typeof config.source.value === 'string' && + ((config.sourceKind.type === 'static' && + config.sourceKind.value === 's3 (workspace storage)') || + config.source.value.startsWith('s3://')) + ) { + return { + s3_path: config.source.value.replace('s3://', ''), + storage: undefined + } + } else { + return undefined + } } else { return undefined } diff --git a/frontend/src/lib/components/apps/editor/component/components.ts b/frontend/src/lib/components/apps/editor/component/components.ts index 2344b57947..52cb6b9ad5 100644 --- a/frontend/src/lib/components/apps/editor/component/components.ts +++ b/frontend/src/lib/components/apps/editor/component/components.ts @@ -484,6 +484,12 @@ export const selectOptions = { animationTimingFunctionOptions: ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'], prose: ['sm', 'Default', 'lg', 'xl', '2xl'], imageSourceKind: [ + 'url', + 'png encoded as base64', + 'jpeg encoded as base64', + 'svg encoded as base64' + ], + imageSourceKindWithS3: [ 'url', 's3 (workspace storage)', 'png encoded as base64', @@ -2903,7 +2909,11 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm' type: 'static', fieldType: 'array', subFieldType: 'object', - value: [{ header: 'First', foo: 1 }, { header: 'Second', foo: 2 }, { header: 'Third', foo: 3 }] as object[] + value: [ + { header: 'First', foo: 1 }, + { header: 'Second', foo: 2 }, + { header: 'Third', foo: 3 } + ] as object[] }, numberOfSubgrids: 1 } @@ -3089,8 +3099,8 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm' sourceKind: { fieldType: 'select', type: 'static', - selectOptions: selectOptions.imageSourceKind, - value: 'url' as (typeof selectOptions.imageSourceKind)[number] + selectOptions: selectOptions.imageSourceKindWithS3, + value: 'url' as (typeof selectOptions.imageSourceKindWithS3)[number] }, imageFit: { fieldType: 'select', diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte index bbc342e5e2..6c5994002d 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte @@ -64,15 +64,18 @@ let s3FilePicker: S3FilePicker | undefined let s3PickerSelection: { s3: string; storage?: string } | undefined = undefined let s3FolderPrefix: string = '' + let s3FileUploadRawMode = componentInput?.type == 'uploadS3' && !!componentInput.value?.s3 + + $: console.log('rawmode', s3FileUploadRawMode) function updateSelectedS3File() { if (s3PickerSelection) { - // @ts-ignore - componentInput = { - ...componentInput, - type: 'static', - value: `s3://${s3PickerSelection.s3}` + if (componentInput.type === 'uploadS3') { + componentInput.value = { + ...s3PickerSelection + } } + s3FileUploadRawMode = true } } @@ -118,6 +121,8 @@ onConnect: applyConnection } } + + $: console.log('input', componentInput) {#if !(resourceOnly && (fieldType !== 'object' || !format?.startsWith('resource-')))} @@ -162,6 +167,11 @@ (componentInput['expr'] == '' || componentInput['expr'] == undefined) ) { componentInput['expr'] = JSON.stringify(componentInput['value']) + } else if (fileUploadS3 && fieldType === 'text' && e.detail != 'uploadS3') { + componentInput['value'] = '' + } else if (e.detail == 'uploadS3') { + s3FileUploadRawMode = false + componentInput['value'] = { s3: '' } } if (shouldFormatExpression) { @@ -238,34 +248,55 @@ {:else if componentInput?.type === 'upload'} {:else if componentInput?.type === 'uploadS3'} -
- + + {#if s3FileUploadRawMode} + {#await import('$lib/components/JsonEditor.svelte')} + + {:then Module} + { + dispatch('focus') + }} + code={JSON.stringify(componentInput.value ?? { s3: '' }, null, 2)} + bind:value={componentInput.value} + /> + {/await} + {:else} + + + {/if} +
- - {:else if fieldType === 'textarea'} - + {:else if fieldType === 'date'} {:else if fieldType === 'time'} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/UploadInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/UploadInputEditor.svelte index 4758d1f357..85d975278b 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/UploadInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/UploadInputEditor.svelte @@ -15,6 +15,7 @@ export let s3: boolean | undefined = false export let prefix: string | undefined = undefined export let workspace: string | undefined = undefined + export let s3FileUploadRawMode: boolean = false let fileUploads: Writable = writable([]) @@ -38,11 +39,13 @@ return `${cleanPrefix}${file.name}` }} on:addition={({ detail }) => { + // @ts-ignore componentInput = { ...componentInput, - type: 'static', - value: `s3://${detail.path}` + type: 'uploadS3', + value: { s3: detail.path } } + s3FileUploadRawMode = true }} /> {:else} diff --git a/frontend/src/lib/components/apps/inputType.ts b/frontend/src/lib/components/apps/inputType.ts index 4bc2fc8532..e348300628 100644 --- a/frontend/src/lib/components/apps/inputType.ts +++ b/frontend/src/lib/components/apps/inputType.ts @@ -78,7 +78,7 @@ export type UploadInput = { export type UploadS3Input = { type: 'uploadS3' - value: string + value: any } export type FileUploadData = { diff --git a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte index 519acbb1d9..afaaf9befd 100644 --- a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte +++ b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte @@ -15,7 +15,7 @@ focus-within:border-blue-500 hover:bg-blue-50 dark:hover:bg-frost-900 focus-with duration-200 rounded-lg p-1 gap-2" href={`${base}/api/w/${workspaceId ?? $workspaceStore}${ appPath ? `/apps_u/download_s3_file/${appPath}` : '/job_helpers/download_s3_file' - }?file_key=${encodeURIComponent(s3object?.s3 ?? '')}${ + }?${appPath ? 's3' : 'file_key'}=${encodeURIComponent(s3object?.s3 ?? '')}${ s3object?.storage ? `&storage=${s3object.storage}` : '' }`} download={s3object?.s3.split('/').pop() ?? 'unnamed_download.file'} diff --git a/frontend/src/lib/components/common/fileUpload/FileUpload.svelte b/frontend/src/lib/components/common/fileUpload/FileUpload.svelte index 1d2555f577..9e5180b76c 100644 --- a/frontend/src/lib/components/common/fileUpload/FileUpload.svelte +++ b/frontend/src/lib/components/common/fileUpload/FileUpload.svelte @@ -19,8 +19,8 @@ export let containerText: string = folderOnly ? 'Drag and drop a folder here or click to browse' : allowMultiple - ? 'Drag and drop files here or click to browse' - : 'Drag and drop a file here or click to browse' + ? 'Drag and drop files here or click to browse' + : 'Drag and drop a file here or click to browse' export let customResourcePath: string | undefined = undefined export let customResourceType: 's3' | 'azure_blob' | undefined = undefined // when customResourcePath is provided, this should be provided as well. Will default to S3 if not export let customClass: string = '' @@ -70,7 +70,10 @@ | undefined) | undefined = undefined - const dispatch = createEventDispatcher() + const dispatch = createEventDispatcher<{ + addition: { path?: string; filename?: string } + deletion: { path: string } + }>() type FileUploadData = { name: string @@ -146,9 +149,9 @@ } else { path = typeof pathTransformer == 'function' - ? (await pathTransformer?.({ + ? ((await pathTransformer?.({ file: fileToUpload - })) ?? fileToUploadKey + })) ?? fileToUploadKey) : fileToUploadKey } const uploadData: FileUploadData = { @@ -269,10 +272,10 @@ appPath ? `/api/w/${ workspace ?? $workspaceStore - }/apps_u/upload_s3_file/${appPath}?${params.toString()}` + }/apps_u/upload_s3_file/${appPath}?${params.toString()}` : `/api/w/${ workspace ?? $workspaceStore - }/job_helpers/upload_s3_file?${params.toString()}`, + }/job_helpers/upload_s3_file?${params.toString()}`, true ) xhr?.setRequestHeader('Content-Type', 'application/octet-stream') @@ -469,10 +472,10 @@ color={fileUpload.errorMessage ? '#ef4444' : fileUpload.cancelled - ? '#eab308' - : fileUpload.progress === 100 - ? '#22c55e' - : '#3b82f6'} + ? '#eab308' + : fileUpload.progress === 100 + ? '#22c55e' + : '#3b82f6'} ended={fileUpload.cancelled || fileUpload.errorMessage !== undefined} > {#if fileUpload.errorMessage} diff --git a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte index d1743d1ec7..cebf1ca38e 100644 --- a/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte +++ b/frontend/src/lib/components/triggers/http/RouteEditorInner.svelte @@ -362,7 +362,7 @@ class="flex justify-end" bind:checked={s3FileUploadRawMode} size="xs" - options={{ left: 'Existing file' }} + options={{ left: 'Raw S3 object input' }} disabled={!can_write} /> {/if} @@ -385,20 +385,6 @@ code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)} /> {/if} - {#if can_write} - - {/if} {:else} {#key is_static_website} {/key} {/if} + {#if can_write} + + {/if} {:else} @@ -612,10 +612,10 @@ href={itemKind === 'flow' ? `/flows/add?${SECRET_KEY_PATH}=${encodeURIComponent(variable_path)}&hub=${ HubFlow.SIGNATURE_TEMPLATE - }` + }` : `/scripts/add?${SECRET_KEY_PATH}=${encodeURIComponent( variable_path - )}&hub=hub%2F${HUB_SCRIPT_ID}`} + )}&hub=hub%2F${HUB_SCRIPT_ID}`} target="_blank">Create from template