presigned

This commit is contained in:
HugoCasa
2025-04-10 22:37:19 +02:00
parent c8cc52d840
commit 0c5abc1f2f
20 changed files with 306 additions and 178 deletions
+16 -7
View File
@@ -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
+91 -54
View File
@@ -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<String>,
}
#[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<String>,
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<DB>,
Path(w_id): Path<String>,
Json(body): Json<S3TokenRequestBody>,
) -> Result<Json<Vec<String>>> {
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<Json<Vec<S3Object>>> {
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::<i64>().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<ApiAuthed>,
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::<SignedS3TokenClaims>(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<String>,
}
#[cfg(feature = "parquet")]
impl From<LoadImagePreviewQuery> 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<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Query(mut query): Query<FileQueryWithForceViewerAllowedS3Keys>,
Query(mut query): Query<S3ObjectWithForceViewerAllowedS3Keys>,
) -> Result<Response> {
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<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Query(mut query): Query<LoadImagePreviewQuery>,
Query(mut query): Query<S3Object>,
) -> Result<Response> {
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)> {
+3 -1
View File
@@ -111,13 +111,15 @@ pub struct S3AwsOidcResource {
pub audience: Option<String>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presigned: Option<String>,
}
#[cfg(feature = "parquet")]
+1 -1
View File
@@ -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;
+4 -3
View File
@@ -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"
}
+2 -1
View File
@@ -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"
+12 -12
View File
@@ -765,18 +765,6 @@
}}
/>
{/await}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(value)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
{:else}
<FileUpload
{appPath}
@@ -799,6 +787,18 @@
initialValue={value}
/>
{/if}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(value)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
</div>
{:else if inputCat == 'object' || inputCat == 'resource-object' || isListJson}
{#if oneOf && oneOf.length >= 2}
@@ -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}` : '')}
/>
</div>
@@ -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}` : '')}
/>
</div>
@@ -66,18 +66,6 @@
bind:value
/>
{/await}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(value)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
{:else}
<FileUpload
allowMultiple={false}
@@ -95,4 +83,16 @@
defaultValue={value?.s3}
/>
{/if}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(value)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
</div>
@@ -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://')
) {
@@ -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
}
@@ -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}', '<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<typeof partialS3ObjectSchema> {
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
}
@@ -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',
@@ -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)
</script>
{#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'}
<UploadInputEditor bind:componentInput {fileUpload} />
{:else if componentInput?.type === 'uploadS3'}
<div class="w-12/12 pb-2 flex flex-row mb-1 gap-1">
<input
type="text"
placeholder="S3 Folder prefix"
bind:value={s3FolderPrefix}
aria-label="S3 Folder prefix"
<div class="flex flex-col w-full gap-1">
<Toggle
class="flex justify-end"
bind:checked={s3FileUploadRawMode}
size="xs"
options={{ left: 'Raw S3 object input' }}
/>
{#if s3FileUploadRawMode}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
on:focus={(e) => {
dispatch('focus')
}}
code={JSON.stringify(componentInput.value ?? { s3: '' }, null, 2)}
bind:value={componentInput.value}
/>
{/await}
{:else}
<input
type="text"
placeholder="S3 Folder prefix"
bind:value={s3FolderPrefix}
aria-label="S3 Folder prefix"
/>
<UploadInputEditor
bind:componentInput
fileUpload={fileUploadS3}
s3={true}
{workspace}
prefix={s3FolderPrefix}
bind:s3FileUploadRawMode
/>
{/if}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3PickerSelection = undefined
s3FilePicker?.open?.()
}}
startIcon={{ icon: Pipette }}
>
Choose an existing file
</Button>
</div>
<UploadInputEditor
bind:componentInput
fileUpload={fileUploadS3}
s3={true}
{workspace}
prefix={s3FolderPrefix}
/>
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3PickerSelection = undefined
s3FilePicker?.open?.()
}}
startIcon={{ icon: Pipette }}
>
Choose an existing file
</Button>
<S3FilePicker
bind:this={s3FilePicker}
folderOnly={false}
@@ -42,7 +42,8 @@
{#if fieldType === 'number' || fieldType === 'integer'}
<input on:keydown|stopPropagation type="number" bind:value={componentInput.value} />
{:else if fieldType === 'textarea'}
<textarea use:autosize on:keydown|stopPropagation bind:value={componentInput.value}></textarea>
<textarea use:autosize on:keydown|stopPropagation bind:value={componentInput.value}
></textarea>
{:else if fieldType === 'date'}
<input on:keydown|stopPropagation type="date" bind:value={componentInput.value} />
{:else if fieldType === 'time'}
@@ -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<FileUploadData[]> = 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}
@@ -78,7 +78,7 @@ export type UploadInput = {
export type UploadS3Input = {
type: 'uploadS3'
value: string
value: any
}
export type FileUploadData = {
@@ -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'}
@@ -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}
@@ -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}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(!is_static_website ? static_asset_config : undefined)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
{/if}
{:else}
{#key is_static_website}
<FileUpload
@@ -420,6 +406,20 @@
/>
{/key}
{/if}
{#if can_write}
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(!is_static_website ? static_asset_config : undefined)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
{/if}
</div>
</div>
{: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</Button
>
</div>