feat: move S3 file within bucket (#2913)
This commit is contained in:
committed by
GitHub
parent
2870d4882d
commit
cd48fdce2e
@@ -10211,6 +10211,33 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
/w/{workspace}/job_helpers/move_s3_file:
|
||||
get:
|
||||
summary: Move a S3 file from one path to the other within the same bucket
|
||||
operationId: moveS3File
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
- name: src_file_key
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: dest_file_key
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Confirmation
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
/w/{workspace}/job_helpers/multipart_upload_s3_file:
|
||||
post:
|
||||
summary: Upload file to S3 bucket using multipart upload
|
||||
|
||||
@@ -6833,6 +6833,31 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/job_helpers/move_s3_file:
|
||||
get:
|
||||
summary: Move a S3 file from one path to the other within the same bucket
|
||||
operationId: moveS3File
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: src_file_key
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: dest_file_key
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Confirmation
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/job_helpers/multipart_upload_s3_file:
|
||||
post:
|
||||
summary: Upload file to S3 bucket using multipart upload
|
||||
|
||||
@@ -80,6 +80,7 @@ pub fn workspaced_service() -> Router {
|
||||
"/delete_s3_file",
|
||||
delete(delete_s3_file).layer(cors.clone()),
|
||||
)
|
||||
.route("/move_s3_file", get(move_s3_file).layer(cors.clone()))
|
||||
.route(
|
||||
"/multipart_upload_s3_file",
|
||||
post(multipart_upload_s3_file).layer(cors.clone()),
|
||||
@@ -710,6 +711,54 @@ async fn delete_s3_file(
|
||||
return Ok(Json(()));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MoveS3FileQuery {
|
||||
pub src_file_key: String,
|
||||
pub dest_file_key: String,
|
||||
}
|
||||
|
||||
async fn move_s3_file(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<MoveS3FileQuery>,
|
||||
) -> error::JsonResult<()> {
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
|
||||
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
))?;
|
||||
let s3_client = build_s3_client(&s3_resource);
|
||||
|
||||
let s3_bucket = s3_resource.bucket.clone();
|
||||
let source_uri = format!("{}/{}", s3_bucket, query.src_file_key);
|
||||
s3_client
|
||||
.copy_object()
|
||||
.copy_source(&source_uri)
|
||||
.bucket(&s3_bucket)
|
||||
.key(&query.dest_file_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("{:?}", err);
|
||||
error::Error::InternalErr(err.to_string())
|
||||
})?;
|
||||
|
||||
s3_client
|
||||
.delete_object()
|
||||
.bucket(&s3_bucket)
|
||||
.key(&query.src_file_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("{:?}", err);
|
||||
error::Error::InternalErr(err.to_string())
|
||||
})?;
|
||||
return Ok(Json(()));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UploadFileQuery {
|
||||
pub file_key: String,
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
Loader2,
|
||||
Download,
|
||||
Trash,
|
||||
FileUp
|
||||
FileUp,
|
||||
MoveRight
|
||||
} from 'lucide-svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { HelpersService, type UploadFilePart } from '$lib/gen'
|
||||
@@ -24,6 +25,10 @@
|
||||
let deletionModalOpen = false
|
||||
let fileDeletionInProgress = false
|
||||
|
||||
let moveModalOpen = false
|
||||
let moveDestKey: string | undefined = undefined
|
||||
let fileMoveInProgress = false
|
||||
|
||||
let uploadModalOpen = false
|
||||
let fileToUpload: File | undefined = undefined
|
||||
let fileToUploadKey: string | undefined = undefined
|
||||
@@ -89,6 +94,8 @@
|
||||
maxKeys: 1000, // fixed pages of 1000 files for now
|
||||
marker: paginationMarker
|
||||
})
|
||||
allFilesByKey = {}
|
||||
displayedFileKeys = []
|
||||
for (let file_path of availableFiles.windmill_large_files) {
|
||||
let split_path = file_path.s3.split('/')
|
||||
let parent_path: string | undefined = undefined
|
||||
@@ -210,8 +217,10 @@
|
||||
})
|
||||
} finally {
|
||||
fileDeletionInProgress = false
|
||||
deletionModalOpen = false
|
||||
}
|
||||
sendUserToast(`${fileKey} deleted from S3 bucket`)
|
||||
selectedFileKey = { s3: '' }
|
||||
const idx = displayedFileKeys.indexOf(fileKey)
|
||||
if (idx >= 0) {
|
||||
displayedFileKeys.splice(idx, 1)
|
||||
@@ -220,6 +229,27 @@
|
||||
delete allFilesByKey[fileKey]
|
||||
}
|
||||
|
||||
async function moveS3File(srcFileKey: string | undefined, destFileKey: string | undefined) {
|
||||
fileMoveInProgress = true
|
||||
if (srcFileKey === undefined || emptyString(destFileKey)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await HelpersService.moveS3File({
|
||||
workspace: $workspaceStore!,
|
||||
srcFileKey: srcFileKey,
|
||||
destFileKey: destFileKey!
|
||||
})
|
||||
} finally {
|
||||
fileMoveInProgress = false
|
||||
moveModalOpen = false
|
||||
}
|
||||
sendUserToast(`${srcFileKey} moved to ${destFileKey}`)
|
||||
selectedFileKey = { s3: destFileKey! }
|
||||
await loadFiles()
|
||||
await loadFileMetadataPlusPreviewAsync(selectedFileKey.s3)
|
||||
}
|
||||
|
||||
async function uploadFileToS3() {
|
||||
fileUploadErrorMsg = undefined
|
||||
if (fileToUpload === undefined || fileToUploadKey === undefined) {
|
||||
@@ -497,6 +527,17 @@
|
||||
startIcon={{ icon: Download }}
|
||||
iconOnly={true}
|
||||
/>
|
||||
<Button
|
||||
title="Move file"
|
||||
variant="border"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
moveDestKey = fileMetadata?.fileKey ?? ''
|
||||
moveModalOpen = true
|
||||
}}
|
||||
startIcon={{ icon: MoveRight }}
|
||||
iconOnly={true}
|
||||
/>
|
||||
<Button
|
||||
title="Delete file from S3"
|
||||
variant="border"
|
||||
@@ -504,9 +545,7 @@
|
||||
on:click={() => {
|
||||
deletionModalOpen = true
|
||||
}}
|
||||
startIcon={fileDeletionInProgress
|
||||
? { icon: Loader2, classes: 'animate-spin' }
|
||||
: { icon: Trash }}
|
||||
startIcon={{ icon: Trash }}
|
||||
iconOnly={true}
|
||||
/>
|
||||
{/if}
|
||||
@@ -588,7 +627,7 @@
|
||||
startIcon={{ icon: FileUp }}
|
||||
on:click={() => {
|
||||
uploadModalOpen = true
|
||||
}}>Upload New</Button
|
||||
}}>Upload File</Button
|
||||
>
|
||||
<Button
|
||||
disable={selectedFileKey === undefined || emptyString(selectedFileKey.s3)}
|
||||
@@ -608,8 +647,9 @@
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
deleteFileFromS3(fileMetadata?.fileKey)
|
||||
deletionModalOpen = false
|
||||
}}
|
||||
keyListen={false}
|
||||
bind:loading={fileDeletionInProgress}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span
|
||||
@@ -618,6 +658,33 @@
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
open={moveModalOpen}
|
||||
title="Move file to new location"
|
||||
confirmationText="Move"
|
||||
on:canceled={() => {
|
||||
moveModalOpen = false
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
moveS3File(fileMetadata?.fileKey, moveDestKey)
|
||||
}}
|
||||
keyListen={false}
|
||||
bind:loading={fileMoveInProgress}
|
||||
>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="w-24">New key: </span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="folder/nested/file.txt"
|
||||
bind:value={moveDestKey}
|
||||
class="text-2xl"
|
||||
/>
|
||||
</div>
|
||||
<span>Are you sure you want to permanently move {fileMetadata?.fileKey}?</span>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<FileUploadModal
|
||||
open={uploadModalOpen}
|
||||
title="Upload file to S3 bucket"
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
import { fade } from 'svelte/transition'
|
||||
import Button from '../button/Button.svelte'
|
||||
import Badge from '../badge/Badge.svelte'
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
import { AlertTriangle, Loader2 } from 'lucide-svelte'
|
||||
|
||||
export let title: string
|
||||
export let confirmationText: string
|
||||
export let keyListen: boolean = true
|
||||
export let loading: boolean = false
|
||||
|
||||
export let open: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (open) {
|
||||
if (open && keyListen) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
switch (event.key) {
|
||||
@@ -73,11 +75,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
|
||||
<Button on:click={() => dispatch('confirmed')} color="red" size="sm">
|
||||
<span>{confirmationText} <Badge>Enter</Badge></span>
|
||||
<Button disabled={loading} on:click={() => dispatch('confirmed')} color="red" size="sm">
|
||||
{#if loading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{/if}
|
||||
<span
|
||||
>{confirmationText}
|
||||
{#if keyListen}<Badge>Enter</Badge>{/if}</span
|
||||
>
|
||||
</Button>
|
||||
<Button on:click={() => dispatch('canceled')} color="light" size="sm">
|
||||
<span>Cancel <Badge color="dark-gray">Escape</Badge></span>
|
||||
<Button
|
||||
disabled={loading}
|
||||
on:click={() => dispatch('canceled')}
|
||||
color="light"
|
||||
size="sm"
|
||||
>
|
||||
<span
|
||||
>Cancel {#if keyListen}<Badge color="dark-gray">Escape</Badge>{/if}</span
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user