feat: move S3 file within bucket (#2913)

This commit is contained in:
Guillaume Bouvignies
2023-12-22 15:02:16 +01:00
committed by GitHub
parent 2870d4882d
commit cd48fdce2e
5 changed files with 195 additions and 12 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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"

View File

@@ -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>