mirror of
https://github.com/rustfs/console.git
synced 2026-09-01 15:17:45 +08:00
feat: object versions rustfs/rustfs#198
This commit is contained in:
+128
-67
@@ -4,22 +4,27 @@
|
||||
<div class="flex items-center flex-wrap gap-4 ml-auto">
|
||||
<n-button @click="download">
|
||||
<Icon name="ri:download-line" class="mr-2" />
|
||||
<span>{{ t("Download") }}</span>
|
||||
<span>{{ t('Download') }}</span>
|
||||
</n-button>
|
||||
|
||||
<n-button @click="() => (showPreview = true)">
|
||||
<Icon name="ri:eye-line" class="mr-2" />
|
||||
<span>{{ t("Preview") }}</span>
|
||||
<span>{{ t('Preview') }}</span>
|
||||
</n-button>
|
||||
|
||||
<n-button @click="() => (showTagView = true)">
|
||||
<Icon name="ri:price-tag-3-line" class="mr-2" />
|
||||
<span>{{ t("Tags") }}</span>
|
||||
<span>{{ t('Tags') }}</span>
|
||||
</n-button>
|
||||
|
||||
<n-button @click="geVersions">
|
||||
<Icon name="ri:file-history-line" class="mr-2" />
|
||||
<span>{{ t('Version') }}</span>
|
||||
</n-button>
|
||||
|
||||
<n-button v-if="lockStatus" @click="() => (showRetentionView = true)">
|
||||
<Icon name="ri:save-2-line" class="mr-2" />
|
||||
<span>{{ t("Retention") }}</span>
|
||||
<span>{{ t('Retention') }}</span>
|
||||
</n-button>
|
||||
|
||||
<!-- <n-button id="copyTag" ref="copyRef" @click="copySignUrl">
|
||||
@@ -35,13 +40,19 @@
|
||||
<n-descriptions-item :label="t('Object Name')">
|
||||
<span class="select-all">{{ key }}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Object Size')">{{ object?.ContentLength }}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Object Type')">{{ object?.ContentType }}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Object Size')">{{
|
||||
object?.ContentLength
|
||||
}}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Object Type')">{{
|
||||
object?.ContentType
|
||||
}}</n-descriptions-item>
|
||||
<!-- <n-descriptions-item label="存储类型">{{ object?.StorageClass }}</n-descriptions-item> -->
|
||||
<n-descriptions-item label="ETag">
|
||||
<span class="select-all">{{ object?.ETag }}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Last Modified Time')">{{ object?.LastModified }}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Last Modified Time')">{{
|
||||
object?.LastModified
|
||||
}}</n-descriptions-item>
|
||||
|
||||
<!-- 合法保留 -->
|
||||
<n-descriptions-item :label="t('Legal Hold')">
|
||||
@@ -51,16 +62,17 @@
|
||||
v-model:value="legalHold"
|
||||
:loading="legalHoldLoading"
|
||||
:round="false"
|
||||
@update:value="handleChangeLegalStatus">
|
||||
@update:value="handleChangeLegalStatus"
|
||||
>
|
||||
<template #checked>
|
||||
{{ t("Enabled") }}
|
||||
{{ t('Enabled') }}
|
||||
</template>
|
||||
<template #unchecked>
|
||||
{{ t("Disabled") }}
|
||||
{{ t('Disabled') }}
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-tag type="error" size="small" v-else class="mt-2">
|
||||
{{ t("Disabled") }}
|
||||
{{ t('Disabled') }}
|
||||
<template #icon>
|
||||
<Icon name="ri:close-circle-fill" />
|
||||
</template>
|
||||
@@ -70,10 +82,12 @@
|
||||
<!-- 保留 -->
|
||||
<n-descriptions-item :label="t('Retention') + t('Policy')">
|
||||
<n-space>
|
||||
<span>{{ t("Retention Mode") + ": " + (retentionMode ? retentionMode : t("None")) }}</span>
|
||||
<span>{{
|
||||
t('Retention Mode') + ': ' + (retentionMode ? retentionMode : t('None'))
|
||||
}}</span>
|
||||
</n-space>
|
||||
<n-space v-if="retainUntilDate">
|
||||
<span>{{ t("Retention RetainUntilDate") + ": " + retainUntilDate }}</span>
|
||||
<span>{{ t('Retention RetainUntilDate') + ': ' + retainUntilDate }}</span>
|
||||
</n-space>
|
||||
</n-descriptions-item>
|
||||
|
||||
@@ -82,15 +96,28 @@
|
||||
<n-descriptions-item :label="t('Temporary URL')">
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<n-input v-model:value="signedUrl" id="signedUrl" :placeholder="t('Temporary URL')" />
|
||||
<n-button id="signedUrlBtn" data-clipboard-target="#signedUrl">{{ t("Copy") }}</n-button>
|
||||
<n-button id="signedUrlBtn" data-clipboard-target="#signedUrl">{{
|
||||
t('Copy')
|
||||
}}</n-button>
|
||||
</div>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<object-preview-modal v-model:show="showPreview" :bucketName="bucketName" :objectKey="key" />
|
||||
<object-preview-modal
|
||||
v-model:show="showPreview"
|
||||
:bucketName="bucketName"
|
||||
:objectKey="key"
|
||||
:versionId="previewVersionId"
|
||||
/>
|
||||
|
||||
<!-- tagview -->
|
||||
<n-modal v-model:show="showTagView" preset="card" :title="t('Set Tags')" draggable class="max-w-screen-md">
|
||||
<n-modal
|
||||
v-model:show="showTagView"
|
||||
preset="card"
|
||||
:title="t('Set Tags')"
|
||||
draggable
|
||||
class="max-w-screen-md"
|
||||
>
|
||||
<n-card class="max-w-screen-md">
|
||||
<n-space class="my-4">
|
||||
<n-tag
|
||||
@@ -98,7 +125,8 @@
|
||||
v-for="(tag, index) in tags"
|
||||
type="info"
|
||||
closable
|
||||
@close="handledeleteTag(index)">
|
||||
@close="handledeleteTag(index)"
|
||||
>
|
||||
{{ tag.Key }}:{{ tag.Value }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
@@ -107,11 +135,14 @@
|
||||
<n-input v-model:value="tagFormValue.Key" :placeholder="t('Tag Key Placeholder')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Tag Value')" path="Value">
|
||||
<n-input v-model:value="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
|
||||
<n-input
|
||||
v-model:value="tagFormValue.Value"
|
||||
:placeholder="t('Tag Value Placeholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="submitTagForm">{{ t("Add") }}</n-button>
|
||||
<n-button class="mx-4" @click="showTagView = false">{{ t("Cancel") }}</n-button>
|
||||
<n-button type="primary" @click="submitTagForm">{{ t('Add') }}</n-button>
|
||||
<n-button class="mx-4" @click="showTagView = false">{{ t('Cancel') }}</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
@@ -121,33 +152,40 @@
|
||||
v-model:show="showRetentionView"
|
||||
preset="card"
|
||||
:title="t('Retention') + t('Policy')"
|
||||
class="max-w-screen-md">
|
||||
class="max-w-screen-md"
|
||||
>
|
||||
<n-card class="flex-col justify-center items-center">
|
||||
<n-form
|
||||
ref="retentionFormRef"
|
||||
:label-width="200"
|
||||
label-placement="left"
|
||||
label-align="left"
|
||||
class="w-[500px]">
|
||||
class="w-[500px]"
|
||||
>
|
||||
<n-form-item :label="t('Retention Mode')" path="retentionMode" class="flex-auto">
|
||||
<n-radio-group v-model:value="retentionMode">
|
||||
<n-radio value="COMPLIANCE">{{ t("COMPLIANCE") }}</n-radio>
|
||||
<n-radio value="GOVERNANCE">{{ t("GOVERNANCE") }}</n-radio>
|
||||
<n-radio value="COMPLIANCE">{{ t('COMPLIANCE') }}</n-radio>
|
||||
<n-radio value="GOVERNANCE">{{ t('GOVERNANCE') }}</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="t('Retention RetainUntilDate')" path="retainUntilDate" class="flex-auto">
|
||||
<n-form-item
|
||||
:label="t('Retention RetainUntilDate')"
|
||||
path="retainUntilDate"
|
||||
class="flex-auto"
|
||||
>
|
||||
<n-date-picker
|
||||
v-model:formatted-value="retainUntilDate"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
type="datetime"
|
||||
clearable />
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="submitRetention">{{ t("Confirm") }}</n-button>
|
||||
<n-button class="mx-4" @click="resetRetention">{{ t("Reset") }}</n-button>
|
||||
<n-button @click="showRetentionView = false">{{ t("Cancel") }}</n-button>
|
||||
<n-button type="primary" @click="submitRetention">{{ t('Confirm') }}</n-button>
|
||||
<n-button class="mx-4" @click="resetRetention">{{ t('Reset') }}</n-button>
|
||||
<n-button @click="showRetentionView = false">{{ t('Cancel') }}</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
@@ -155,16 +193,24 @@
|
||||
</n-card>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
<VersionsModal
|
||||
:bucketName="bucketName"
|
||||
:objectKey="key"
|
||||
:visible="showVersions"
|
||||
@close="showVersions = false"
|
||||
@preview="handlePreviewVersion"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import dayjs from 'dayjs';
|
||||
import VersionsModal from './versions.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const visibel = ref(false);
|
||||
const bucketName = ref("");
|
||||
const key = ref("");
|
||||
const bucketName = ref('');
|
||||
const key = ref('');
|
||||
|
||||
let objectApi: any;
|
||||
const openDrawer = (bucket: string, objName: string) => {
|
||||
@@ -188,8 +234,8 @@ const showPreview = ref(false);
|
||||
// 标签
|
||||
const showTagView = ref(false);
|
||||
const tagFormValue = ref({
|
||||
Key: "",
|
||||
Value: "",
|
||||
Key: '',
|
||||
Value: '',
|
||||
});
|
||||
interface Tag {
|
||||
Key: string;
|
||||
@@ -220,9 +266,9 @@ const legalHoldLoading = ref(false);
|
||||
// 获取桶的object lock
|
||||
const getObjectLockConfig = async () => {
|
||||
const { getObjectLockConfiguration } = useBucket({});
|
||||
getObjectLockConfiguration(bucketName.value).then((res) => {
|
||||
getObjectLockConfiguration(bucketName.value).then(res => {
|
||||
if (res.ObjectLockConfiguration?.ObjectLockEnabled) {
|
||||
lockStatus.value = res.ObjectLockConfiguration?.ObjectLockEnabled == "Enabled" ? true : false;
|
||||
lockStatus.value = res.ObjectLockConfiguration?.ObjectLockEnabled == 'Enabled' ? true : false;
|
||||
// 如果桶开启了object lock,则获取合法保留状态
|
||||
if (lockStatus.value) {
|
||||
getObjectLegalHoldFn();
|
||||
@@ -241,8 +287,8 @@ const getObjectLegalHoldFn = () => {
|
||||
bucket: bucketName.value,
|
||||
});
|
||||
getObjectLegalHold(key.value)
|
||||
.then((res) => {
|
||||
legalHold.value = res.LegalHold?.Status == "ON" ? true : false;
|
||||
.then(res => {
|
||||
legalHold.value = res.LegalHold?.Status == 'ON' ? true : false;
|
||||
})
|
||||
.finally(() => {
|
||||
legalHoldLoading.value = false;
|
||||
@@ -254,7 +300,7 @@ const handleChangeLegalStatus = () => {
|
||||
bucket: bucketName.value,
|
||||
});
|
||||
putObjectLegalHold(key.value, {
|
||||
Status: legalHold.value ? "ON" : "OFF",
|
||||
Status: legalHold.value ? 'ON' : 'OFF',
|
||||
}).finally(() => {
|
||||
getObjectLegalHoldFn();
|
||||
});
|
||||
@@ -266,7 +312,7 @@ const retentionFormRef = ref();
|
||||
|
||||
const retentionMode = ref<string | null>(null);
|
||||
const retainUntilDate = ref<string | null>(null);
|
||||
console.log("🚀 ~ retainUntilDate:", retainUntilDate.value);
|
||||
console.log('🚀 ~ retainUntilDate:', retainUntilDate.value);
|
||||
|
||||
const retentionLoading = ref(false);
|
||||
const showRetentionView = ref(false);
|
||||
@@ -275,14 +321,14 @@ const getObjectRetentionFn = () => {
|
||||
const { getObjectRetention } = useObject({
|
||||
bucket: bucketName.value,
|
||||
});
|
||||
getObjectRetention(key.value).then((res) => {
|
||||
getObjectRetention(key.value).then(res => {
|
||||
if (res.Retention) {
|
||||
retentionMode.value = res.Retention.Mode || "";
|
||||
retentionMode.value = res.Retention.Mode || '';
|
||||
retainUntilDate.value = res.Retention.RetainUntilDate
|
||||
? dayjs(res.Retention.RetainUntilDate).format("YYYY-MM-DD HH:mm:ss")
|
||||
? dayjs(res.Retention.RetainUntilDate).format('YYYY-MM-DD HH:mm:ss')
|
||||
: null;
|
||||
} else {
|
||||
retentionMode.value = "";
|
||||
retentionMode.value = '';
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -298,7 +344,7 @@ const submitRetention = () => {
|
||||
RetainUntilDate: retainUntilDate.value ? new Date(retainUntilDate.value) : null,
|
||||
})
|
||||
.then(() => {
|
||||
message.success(t("Retention Update Success"));
|
||||
message.success(t('Retention Update Success'));
|
||||
getObjectRetentionFn();
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -318,18 +364,20 @@ const resetRetention = () => {
|
||||
const handledeleteTag = async (index: number) => {
|
||||
const { putObjectTagging } = useObject({ bucket: bucketName.value });
|
||||
dialog.error({
|
||||
title: t("Warning"),
|
||||
content: t("Delete Tag Confirm"),
|
||||
positiveText: t("Confirm"),
|
||||
negativeText: t("Cancel"),
|
||||
title: t('Warning'),
|
||||
content: t('Delete Tag Confirm'),
|
||||
positiveText: t('Confirm'),
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
putObjectTagging(key.value, { TagSet: tags.value.filter((item, keyIndex) => keyIndex !== index) })
|
||||
putObjectTagging(key.value, {
|
||||
TagSet: tags.value.filter((item, keyIndex) => keyIndex !== index),
|
||||
})
|
||||
.then(() => {
|
||||
message.success(t("Tag Update Success"));
|
||||
message.success(t('Tag Update Success'));
|
||||
getTags();
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error(t("Tag Delete Failed", { error: error.message }));
|
||||
.catch(error => {
|
||||
message.error(t('Tag Delete Failed', { error: error.message }));
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -344,19 +392,19 @@ const submitTagForm = async () => {
|
||||
putObjectTagging(key.value, {
|
||||
TagSet: [...tags.value, tag],
|
||||
}).then(() => {
|
||||
message.success(t("Tag Set Success"));
|
||||
message.success(t('Tag Set Success'));
|
||||
getTags();
|
||||
});
|
||||
};
|
||||
|
||||
const object = ref();
|
||||
const loading = ref("pending");
|
||||
const signedUrl = ref("");
|
||||
const loading = ref('pending');
|
||||
const signedUrl = ref('');
|
||||
|
||||
const getObject = () => {
|
||||
objectApi.headObject(key.value).then(async (res: any) => {
|
||||
object.value = res;
|
||||
loading.value = "fulfilled";
|
||||
loading.value = 'fulfilled';
|
||||
signedUrl.value = await objectApi.getSignedUrl(key.value);
|
||||
});
|
||||
};
|
||||
@@ -364,24 +412,23 @@ const getObject = () => {
|
||||
const refreshSignedUrl = async () => {
|
||||
try {
|
||||
signedUrl.value = await objectApi.getSignedUrl(key.value);
|
||||
message.success(t("Get Success"));
|
||||
message.success(t('Get Success'));
|
||||
} catch (error) {
|
||||
message.error(t("Get Failed"));
|
||||
message.error(t('Get Failed'));
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
const dialog = useDialog();
|
||||
|
||||
|
||||
// 复制
|
||||
import ClipboardJS from "clipboard";
|
||||
const clipboard = new ClipboardJS( "#signedUrlBtn");
|
||||
clipboard.on('success', function(e) {
|
||||
import ClipboardJS from 'clipboard';
|
||||
const clipboard = new ClipboardJS('#signedUrlBtn');
|
||||
clipboard.on('success', function (e) {
|
||||
message.success(t('Copy Success'));
|
||||
e.clearSelection();
|
||||
});
|
||||
// 这里成功的时候也响应error,所以这里也加上
|
||||
clipboard.on('error', function(e) {
|
||||
clipboard.on('error', function (e) {
|
||||
message.success(t('Copy Success'));
|
||||
});
|
||||
onUnmounted(() => {
|
||||
@@ -389,10 +436,24 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
const download = async () => {
|
||||
const msg = message.loading(t("Getting URL"));
|
||||
const msg = message.loading(t('Getting URL'));
|
||||
await refreshSignedUrl();
|
||||
const url = await objectApi.getSignedUrl(key.value);
|
||||
msg.destroy();
|
||||
window.open(url, "_blank");
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
// 版本
|
||||
|
||||
const showVersions = ref(false);
|
||||
const previewVersionId = ref<string | null>(null);
|
||||
|
||||
const geVersions = async () => {
|
||||
showVersions.value = true;
|
||||
};
|
||||
|
||||
function handlePreviewVersion(versionId: string) {
|
||||
previewVersionId.value = versionId;
|
||||
showPreview.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -278,6 +278,7 @@ watch(
|
||||
);
|
||||
|
||||
const contents = computed(() => data.value?.contents || []);
|
||||
console.log('🚀 ~ contents:', contents.value);
|
||||
const commonPrefixes = computed(() => data.value?.commonPrefixes || []);
|
||||
const nextToken = computed(() => data.value?.nextContinuationToken || null);
|
||||
|
||||
|
||||
@@ -13,10 +13,19 @@
|
||||
</div>
|
||||
</template>
|
||||
<n-spin v-if="loading" size="large"></n-spin>
|
||||
<div v-else class="min-h-64 max-h-[80vh] overflow-y-auto flex-1 flex flex-col items-center" :class="{ 'justify-center': !canPreview }">
|
||||
<div
|
||||
v-else
|
||||
class="min-h-64 max-h-[80vh] overflow-y-auto flex-1 flex flex-col items-center"
|
||||
:class="{ 'justify-center': !canPreview }"
|
||||
>
|
||||
<template v-if="canPreview">
|
||||
<img v-if="isImage" :src="previewUrl" alt="preview" />
|
||||
<iframe v-else-if="isPdf" :src="previewUrl" class="w-full min-h-[70vh]" frameborder="0"></iframe>
|
||||
<iframe
|
||||
v-else-if="isPdf"
|
||||
:src="previewUrl"
|
||||
class="w-full min-h-[70vh]"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
<pre v-else-if="isText" class="w-full selea">{{ fileContent }}</pre>
|
||||
<video v-else-if="isVideo" controls class="w-full">
|
||||
<source :src="previewUrl" type="video/mp4" />
|
||||
@@ -39,87 +48,144 @@
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl as _getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
bucketName: string
|
||||
objectKey: string
|
||||
}>()
|
||||
show: boolean;
|
||||
bucketName: string;
|
||||
objectKey: string;
|
||||
versionId?: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:show'])
|
||||
const { getSignedUrl, headObject } = useObject({ bucket: props.bucketName })
|
||||
const emit = defineEmits(['update:show']);
|
||||
const { getSignedUrl, headObject } = useObject({ bucket: props.bucketName });
|
||||
|
||||
// 内部状态
|
||||
const previewUrl = ref<string | undefined>(undefined)
|
||||
const contentType = ref<string | undefined>(undefined)
|
||||
const fileContent = ref<string | undefined>(undefined)
|
||||
const loading = ref<boolean>(false)
|
||||
const previewUrl = ref<string | undefined>(undefined);
|
||||
const contentType = ref<string | undefined>(undefined);
|
||||
const fileContent = ref<string | undefined>(undefined);
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const message = useMessage()
|
||||
const message = useMessage();
|
||||
|
||||
// 文件类型配置
|
||||
const fileTypeConfig = {
|
||||
text: {
|
||||
mimes: [
|
||||
'application/json', 'application/xml',
|
||||
'application/javascript', 'application/x-sh',
|
||||
'application/x-csh', 'application/x-python',
|
||||
'application/x-php', 'application/x-perl',
|
||||
'application/x-shellscript', 'application/x-ruby',
|
||||
'application/x-java', 'application/x-cpp',
|
||||
'application/json',
|
||||
'application/xml',
|
||||
'application/javascript',
|
||||
'application/x-sh',
|
||||
'application/x-csh',
|
||||
'application/x-python',
|
||||
'application/x-php',
|
||||
'application/x-perl',
|
||||
'application/x-shellscript',
|
||||
'application/x-ruby',
|
||||
'application/x-java',
|
||||
'application/x-cpp',
|
||||
],
|
||||
extensions: [
|
||||
'.txt', '.json', '.xml', '.js', '.sh', '.csh',
|
||||
'.py', '.php', '.pl', '.sh', '.rb', '.java',
|
||||
'.cpp', '.h', '.hpp', '.c', '.cc', '.hh',
|
||||
'.hxx', '.cxx', '.java', '.kt', '.scala',
|
||||
'.groovy', '.rs', '.go', '.dart', '.swift',
|
||||
'.m', '.mm', '.cs', '.ts', '.jsx', '.tsx',
|
||||
'.html', '.htm', '.css', '.scss', '.sass',
|
||||
'.less', '.styl', '.yaml', '.yml', '.toml',
|
||||
'.ini', '.cfg', '.conf', '.properties', '.md',
|
||||
'.markdown', '.rst', '.r',
|
||||
]
|
||||
'.txt',
|
||||
'.json',
|
||||
'.xml',
|
||||
'.js',
|
||||
'.sh',
|
||||
'.csh',
|
||||
'.py',
|
||||
'.php',
|
||||
'.pl',
|
||||
'.sh',
|
||||
'.rb',
|
||||
'.java',
|
||||
'.cpp',
|
||||
'.h',
|
||||
'.hpp',
|
||||
'.c',
|
||||
'.cc',
|
||||
'.hh',
|
||||
'.hxx',
|
||||
'.cxx',
|
||||
'.java',
|
||||
'.kt',
|
||||
'.scala',
|
||||
'.groovy',
|
||||
'.rs',
|
||||
'.go',
|
||||
'.dart',
|
||||
'.swift',
|
||||
'.m',
|
||||
'.mm',
|
||||
'.cs',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx',
|
||||
'.html',
|
||||
'.htm',
|
||||
'.css',
|
||||
'.scss',
|
||||
'.sass',
|
||||
'.less',
|
||||
'.styl',
|
||||
'.yaml',
|
||||
'.yml',
|
||||
'.toml',
|
||||
'.ini',
|
||||
'.cfg',
|
||||
'.conf',
|
||||
'.properties',
|
||||
'.md',
|
||||
'.markdown',
|
||||
'.rst',
|
||||
'.r',
|
||||
],
|
||||
},
|
||||
image: {
|
||||
extensions: [
|
||||
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg',
|
||||
]
|
||||
extensions: ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'],
|
||||
},
|
||||
audio: {
|
||||
extensions: [
|
||||
'.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.wma',
|
||||
]
|
||||
extensions: ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.wma'],
|
||||
},
|
||||
pdf: {
|
||||
extensions: [
|
||||
'.pdf',
|
||||
]
|
||||
}
|
||||
extensions: ['.pdf'],
|
||||
},
|
||||
};
|
||||
|
||||
// 文件类型判断函数
|
||||
function isFileType(contentType: string | undefined, fileName: string, type: keyof typeof fileTypeConfig): boolean {
|
||||
function isFileType(
|
||||
contentType: string | undefined,
|
||||
fileName: string,
|
||||
type: keyof typeof fileTypeConfig
|
||||
): boolean {
|
||||
if (type === 'text') {
|
||||
return contentType?.startsWith('text/') ||
|
||||
return (
|
||||
contentType?.startsWith('text/') ||
|
||||
fileTypeConfig.text.mimes.some(mime => contentType?.includes(mime)) ||
|
||||
fileTypeConfig.text.extensions.some(ext => fileName.endsWith(ext));
|
||||
fileTypeConfig.text.extensions.some(ext => fileName.endsWith(ext))
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'image') {
|
||||
return contentType?.startsWith('image/') ||
|
||||
fileTypeConfig.image.extensions.some(ext => fileName.endsWith(ext));
|
||||
return (
|
||||
contentType?.startsWith('image/') ||
|
||||
fileTypeConfig.image.extensions.some(ext => fileName.endsWith(ext))
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'audio') {
|
||||
return contentType?.startsWith('audio/') ||
|
||||
fileTypeConfig.audio.extensions.some(ext => fileName.endsWith(ext));
|
||||
return (
|
||||
contentType?.startsWith('audio/') ||
|
||||
fileTypeConfig.audio.extensions.some(ext => fileName.endsWith(ext))
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'pdf') {
|
||||
return contentType === 'application/pdf' ||
|
||||
fileTypeConfig.pdf.extensions.some(ext => fileName.endsWith(ext));
|
||||
return (
|
||||
contentType === 'application/pdf' ||
|
||||
fileTypeConfig.pdf.extensions.some(ext => fileName.endsWith(ext))
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -132,62 +198,87 @@ const isAudio = computed(() => isFileType(contentType.value, props.objectKey, 'a
|
||||
const isPdf = computed(() => isFileType(contentType.value, props.objectKey, 'pdf'));
|
||||
const isText = computed(() => isFileType(contentType.value, props.objectKey, 'text'));
|
||||
|
||||
const canPreview = computed(() => isImage.value || isPdf.value || isText.value || isVideo.value || isAudio.value);
|
||||
const canPreview = computed(
|
||||
() => isImage.value || isPdf.value || isText.value || isVideo.value || isAudio.value
|
||||
);
|
||||
|
||||
// 当 show 为 true 时触发加载逻辑
|
||||
watch(() => props.show, async (newVal) => {
|
||||
if (newVal) {
|
||||
await loadPreview()
|
||||
} else {
|
||||
resetState()
|
||||
watch(
|
||||
() => props.show,
|
||||
async newVal => {
|
||||
if (newVal) {
|
||||
await loadPreview();
|
||||
} else {
|
||||
resetState();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
async function loadPreview() {
|
||||
loading.value = true
|
||||
previewUrl.value = undefined
|
||||
fileContent.value = undefined
|
||||
contentType.value = undefined
|
||||
loading.value = true;
|
||||
previewUrl.value = undefined;
|
||||
fileContent.value = undefined;
|
||||
contentType.value = undefined;
|
||||
|
||||
try {
|
||||
// 获取预签名URL
|
||||
const url = await getSignedUrl(props.objectKey)
|
||||
// 获取预签名URL,支持 versionId
|
||||
let url: string;
|
||||
if (props.versionId) {
|
||||
const $client = useNuxtApp().$s3Client;
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: props.bucketName,
|
||||
Key: props.objectKey,
|
||||
VersionId: props.versionId,
|
||||
});
|
||||
url = await _getSignedUrl($client, command, { expiresIn: 3600 });
|
||||
} else {
|
||||
url = await getSignedUrl(props.objectKey);
|
||||
}
|
||||
previewUrl.value = url;
|
||||
|
||||
previewUrl.value = url
|
||||
|
||||
// 先使用 HEAD 请求获取 Content-Type
|
||||
const response = await headObject(props.objectKey)
|
||||
|
||||
const ctype = response?.ContentType
|
||||
contentType.value = ctype || 'application/octet-stream'
|
||||
// 先使用 HEAD 请求获取 Content-Type,支持 versionId
|
||||
let response: any;
|
||||
if (props.versionId) {
|
||||
const $client = useNuxtApp().$s3Client;
|
||||
response = await $client.send(
|
||||
new HeadObjectCommand({
|
||||
Bucket: props.bucketName,
|
||||
Key: props.objectKey,
|
||||
VersionId: props.versionId,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
response = await headObject(props.objectKey);
|
||||
}
|
||||
const ctype = response?.ContentType;
|
||||
contentType.value = ctype || 'application/octet-stream';
|
||||
|
||||
// 根据 MIME 类型决定是否加载内容
|
||||
if (isText.value) {
|
||||
// 加载文本内容
|
||||
const response = await fetch(url)
|
||||
const text = await response.text()
|
||||
fileContent.value = text
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
fileContent.value = text;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
message.error(t('Preview Failed'))
|
||||
closeModal()
|
||||
console.error(err);
|
||||
message.error(t('Preview Failed'));
|
||||
closeModal();
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
previewUrl.value = undefined
|
||||
fileContent.value = undefined
|
||||
contentType.value = undefined
|
||||
loading.value = false
|
||||
previewUrl.value = undefined;
|
||||
fileContent.value = undefined;
|
||||
contentType.value = undefined;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
emit('update:show', false)
|
||||
emit('update:show', false);
|
||||
}
|
||||
|
||||
const objectKey = computed(() => props.objectKey)
|
||||
const objectKey = computed(() => props.objectKey);
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<n-modal
|
||||
:show="visible"
|
||||
@update:show="emit('close')"
|
||||
preset="card"
|
||||
:title="t('Object Versions')"
|
||||
class="max-w-screen-md"
|
||||
>
|
||||
<n-data-table :columns="columns" :data="versions" :pagination="pagination" />
|
||||
<n-button class="mt-4" @click="$emit('close')">{{ t('Close') }}</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, h } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useObject } from '@/composables/useObject';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl as _getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const props = defineProps<{ bucketName: string; objectKey: string; visible: boolean }>();
|
||||
const emit = defineEmits(['close', 'preview']);
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
|
||||
const versions = ref<any[]>([]);
|
||||
const pagination = ref({ page: 1, pageSize: 10 });
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function shortVersionId(versionId: string): string {
|
||||
if (!versionId) return '';
|
||||
// 以 - . / 分割,取最后一段
|
||||
const parts = versionId.split(/[-./]/);
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('VersionId'),
|
||||
key: 'VersionId',
|
||||
render(row: any) {
|
||||
return shortVersionId(row.VersionId);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('LastModified'),
|
||||
key: 'LastModified',
|
||||
render(row: any) {
|
||||
return row.LastModified ? dayjs(row.LastModified).format('YYYY-MM-DD HH:mm:ss') : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('Size'),
|
||||
key: 'Size',
|
||||
render(row: any) {
|
||||
return typeof row.Size === 'number' ? formatBytes(row.Size) : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('Action'),
|
||||
key: 'action',
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
'n-button',
|
||||
{
|
||||
size: 'small',
|
||||
style: 'margin-right: 8px;',
|
||||
onClick: () => previewVersion(row),
|
||||
},
|
||||
t('Preview')
|
||||
),
|
||||
h(
|
||||
'n-button',
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => downloadVersion(row),
|
||||
},
|
||||
t('Download')
|
||||
),
|
||||
];
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
val => {
|
||||
if (val) fetchVersions();
|
||||
}
|
||||
);
|
||||
|
||||
async function fetchVersions() {
|
||||
const { getObjectVersions } = useObject({ bucket: props.bucketName });
|
||||
try {
|
||||
const { Versions } = await getObjectVersions(props.objectKey);
|
||||
versions.value = Versions || [];
|
||||
} catch (e: any) {
|
||||
message.error(t('Failed to fetch versions'));
|
||||
}
|
||||
}
|
||||
|
||||
async function previewVersion(row: any) {
|
||||
// 触发父组件预览弹窗,传递 versionId
|
||||
emit('preview', row.VersionId);
|
||||
}
|
||||
|
||||
// 新增:自定义带 versionId 的签名 URL 获取方法
|
||||
async function getSignedUrlWithVersion(key: string, versionId: string, expiresIn = 3600) {
|
||||
// 直接用 useObject 里的 bucketName
|
||||
const $client = useNuxtApp().$s3Client;
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: props.bucketName,
|
||||
Key: key,
|
||||
VersionId: versionId,
|
||||
});
|
||||
return await _getSignedUrl($client, command, { expiresIn });
|
||||
}
|
||||
|
||||
async function downloadVersion(row: any) {
|
||||
try {
|
||||
const url = await getSignedUrlWithVersion(props.objectKey, row.VersionId);
|
||||
window.open(url, '_blank');
|
||||
} catch (e: any) {
|
||||
message.error(t('Download failed'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+77
-55
@@ -15,12 +15,12 @@
|
||||
<span>{{ t('Copy URL') }}</span>
|
||||
</n-button>
|
||||
|
||||
<n-button @click="() => showPreview = true">
|
||||
<n-button @click="() => (showPreview = true)">
|
||||
<Icon name="ri:eye-line" class="mr-2" />
|
||||
<span>{{ t('Preview') }}</span>
|
||||
</n-button>
|
||||
|
||||
<n-button @click="() => showTagView = true">
|
||||
<n-button @click="() => (showTagView = true)">
|
||||
<Icon name="ri:price-tag-3-line" class="mr-2" />
|
||||
<span>{{ t('Tags') }}</span>
|
||||
</n-button>
|
||||
@@ -47,21 +47,41 @@
|
||||
<n-spin size="small" />
|
||||
</div>
|
||||
<n-descriptions :column="1">
|
||||
<n-descriptions-item :label="t('Object Name')"><span class="select-all">{{ key }}</span></n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Object Size')">{{ object?.ContentLength }}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Object Name')"
|
||||
><span class="select-all">{{ key }}</span></n-descriptions-item
|
||||
>
|
||||
<n-descriptions-item :label="t('Object Size')">{{
|
||||
object?.ContentLength
|
||||
}}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Object Type')">{{ object?.ContentType }}</n-descriptions-item>
|
||||
<!-- <n-descriptions-item label="存储类型">{{ object?.StorageClass }}</n-descriptions-item> -->
|
||||
<n-descriptions-item label="ETag"><span class="select-all">{{ object?.ETag }}</span></n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Last Modified Time')">{{ object?.LastModified }}</n-descriptions-item>
|
||||
<n-descriptions-item label="ETag"
|
||||
><span class="select-all">{{ object?.ETag }}</span></n-descriptions-item
|
||||
>
|
||||
<n-descriptions-item :label="t('Last Modified Time')">{{
|
||||
object?.LastModified
|
||||
}}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<object-preview-modal v-model:show="showPreview" :bucketName="bucketName" :objectKey="key" />
|
||||
|
||||
<!-- tagview -->
|
||||
<n-modal v-model:show="showTagView" preset="card" :title="t('Set Tags')" draggable class="max-w-screen-md">
|
||||
<n-modal
|
||||
v-model:show="showTagView"
|
||||
preset="card"
|
||||
:title="t('Set Tags')"
|
||||
draggable
|
||||
class="max-w-screen-md"
|
||||
>
|
||||
<n-card class="max-w-screen-md">
|
||||
<n-space class="my-4">
|
||||
<n-tag class="m-2 align-middle" v-for="(tag, index) in tags" type="info" closable @close="handledeleteTag(index)">
|
||||
<n-tag
|
||||
class="m-2 align-middle"
|
||||
v-for="(tag, index) in tags"
|
||||
type="info"
|
||||
closable
|
||||
@close="handledeleteTag(index)"
|
||||
>
|
||||
{{ tag.Key }}:{{ tag.Value }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
@@ -86,87 +106,91 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const { $s3Client } = useNuxtApp();
|
||||
const message = useMessage();
|
||||
const props = defineProps<{ bucket: string; objectKey: string }>()
|
||||
const props = defineProps<{ bucket: string; objectKey: string }>();
|
||||
|
||||
const bucketName = computed(() => props.bucket as string)
|
||||
const { getObjectTagging, putObjectTagging, deleteObjectTagging } = useObject({ bucket: bucketName.value })
|
||||
const bucketName = computed(() => props.bucket as string);
|
||||
const { getObjectTagging, putObjectTagging, deleteObjectTagging } = useObject({
|
||||
bucket: bucketName.value,
|
||||
});
|
||||
|
||||
// 当前路径的前缀, example: '/folder1/folder2/'
|
||||
const key = computed(() => decodeURIComponent(props.objectKey as string))
|
||||
const key = computed(() => decodeURIComponent(props.objectKey as string));
|
||||
|
||||
// 预览内容
|
||||
const showPreview = ref(false)
|
||||
const showPreview = ref(false);
|
||||
// 标签
|
||||
const showTagView = ref(false)
|
||||
const showTagView = ref(false);
|
||||
const tagFormValue = ref({
|
||||
Key: '',
|
||||
Value: ''
|
||||
})
|
||||
Value: '',
|
||||
});
|
||||
interface Tag {
|
||||
Key: string
|
||||
Value: string
|
||||
Key: string;
|
||||
Value: string;
|
||||
}
|
||||
// 获取tags
|
||||
const tags = ref<Tag[]>([])
|
||||
const tags = ref<Tag[]>([]);
|
||||
const getTags = async () => {
|
||||
const resp: any = await getObjectTagging(key.value)
|
||||
tags.value = resp.TagSet || []
|
||||
const resp: any = await getObjectTagging(key.value);
|
||||
tags.value = resp.TagSet || [];
|
||||
};
|
||||
getTags();
|
||||
|
||||
}
|
||||
getTags()
|
||||
|
||||
const dialog = useDialog()
|
||||
const dialog = useDialog();
|
||||
// 删除标签
|
||||
const handledeleteTag = async (index: number) => {
|
||||
dialog.error({
|
||||
title: t("Warning"),
|
||||
content: t("Delete Tag Confirm"),
|
||||
positiveText: t("Confirm"),
|
||||
negativeText: t("Cancel"),
|
||||
title: t('Warning'),
|
||||
content: t('Delete Tag Confirm'),
|
||||
positiveText: t('Confirm'),
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
putObjectTagging(key.value, { TagSet: tags.value.filter((item, keyIndex) => keyIndex !== index) })
|
||||
putObjectTagging(key.value, {
|
||||
TagSet: tags.value.filter((item, keyIndex) => keyIndex !== index),
|
||||
})
|
||||
.then(() => {
|
||||
message.success(t("Tag Update Success"))
|
||||
getTags()
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error(t("Tag Delete Failed", { error: error.message }))
|
||||
message.success(t('Tag Update Success'));
|
||||
getTags();
|
||||
})
|
||||
.catch(error => {
|
||||
message.error(t('Tag Delete Failed', { error: error.message }));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const submitTagForm = async () => {
|
||||
const tag = {
|
||||
Key: tagFormValue.value.Key,
|
||||
Value: tagFormValue.value.Value
|
||||
}
|
||||
Value: tagFormValue.value.Value,
|
||||
};
|
||||
putObjectTagging(key.value, {
|
||||
TagSet: [...tags.value, tag]
|
||||
TagSet: [...tags.value, tag],
|
||||
}).then(() => {
|
||||
message.success(t('Tag Set Success'))
|
||||
getTags()
|
||||
})
|
||||
|
||||
}
|
||||
message.success(t('Tag Set Success'));
|
||||
getTags();
|
||||
});
|
||||
};
|
||||
|
||||
const objectApi = useObject({ bucket: bucketName.value });
|
||||
|
||||
// 在服务端获取数据
|
||||
const { data: object, status, refresh } = useAsyncData(`head-object&${key}`, () => objectApi.headObject(key.value))
|
||||
const {
|
||||
data: object,
|
||||
status,
|
||||
refresh,
|
||||
} = useAsyncData(`head-object&${key}`, () => objectApi.headObject(key.value));
|
||||
|
||||
const download = async () => {
|
||||
const msg = message.loading(t('Getting URL'));
|
||||
const url = await objectApi.getSignedUrl(key.value);
|
||||
msg.destroy();
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const copySignedUrl = async () => {
|
||||
const msg = message.loading(t('Getting URL'));
|
||||
@@ -174,12 +198,10 @@ const copySignedUrl = async () => {
|
||||
await navigator.clipboard.writeText(url);
|
||||
msg.destroy();
|
||||
message.success(t('URL Copied'));
|
||||
}
|
||||
};
|
||||
const deleteObject = async () => {
|
||||
await objectApi.deleteObject(key.value);
|
||||
message.success(t('Delete Success'));
|
||||
router.back();
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
PutObjectRetentionCommand,
|
||||
GetObjectLegalHoldCommand,
|
||||
PutObjectLegalHoldCommand,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl as _getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
ListObjectVersionsCommand,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl as _getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
|
||||
export function useObject({ bucket, region }: { bucket: string; region?: string }) {
|
||||
const $client = useNuxtApp().$s3Client;
|
||||
@@ -54,18 +55,26 @@ export function useObject({ bucket, region }: { bucket: string; region?: string
|
||||
return await $client.send(new DeleteObjectCommand(params));
|
||||
};
|
||||
|
||||
const listObject = async (bucket: string, prefix: string | undefined = undefined, pageSize: number = 25) => {
|
||||
const listObject = async (
|
||||
bucket: string,
|
||||
prefix: string | undefined = undefined,
|
||||
pageSize: number = 25
|
||||
) => {
|
||||
const params = {
|
||||
Bucket: bucket,
|
||||
Prefix: prefix,
|
||||
MaxKeys: pageSize,
|
||||
Delimiter: "/",
|
||||
Delimiter: '/',
|
||||
};
|
||||
|
||||
return await $client.send(new ListObjectsV2Command(params));
|
||||
};
|
||||
|
||||
async function mapAllFiles(bucketName: string, prefix: string, callback: (fileKey: string) => void) {
|
||||
async function mapAllFiles(
|
||||
bucketName: string,
|
||||
prefix: string,
|
||||
callback: (fileKey: string) => void
|
||||
) {
|
||||
let isTruncated = true;
|
||||
let continuationToken: string | undefined = undefined;
|
||||
|
||||
@@ -82,7 +91,7 @@ export function useObject({ bucket, region }: { bucket: string; region?: string
|
||||
|
||||
try {
|
||||
const data = await $client.send(new ListObjectsV2Command(params));
|
||||
data.Contents?.forEach((item) => {
|
||||
data.Contents?.forEach(item => {
|
||||
if (item.Key) {
|
||||
callback(item.Key);
|
||||
}
|
||||
@@ -91,7 +100,7 @@ export function useObject({ bucket, region }: { bucket: string; region?: string
|
||||
isTruncated = data.IsTruncated || false;
|
||||
continuationToken = data.NextContinuationToken;
|
||||
} catch (error) {
|
||||
console.error("Error listing files: ", error);
|
||||
console.error('Error listing files: ', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -138,7 +147,7 @@ export function useObject({ bucket, region }: { bucket: string; region?: string
|
||||
}
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes("Deserialization error")) {
|
||||
if (error.message?.includes('Deserialization error')) {
|
||||
return { Retention: null };
|
||||
}
|
||||
throw error;
|
||||
@@ -173,6 +182,15 @@ export function useObject({ bucket, region }: { bucket: string; region?: string
|
||||
return await $client.send(new PutObjectLegalHoldCommand(params));
|
||||
}
|
||||
|
||||
async function getObjectVersions(key: string) {
|
||||
const params = {
|
||||
Bucket: bucket,
|
||||
Prefix: key,
|
||||
};
|
||||
|
||||
return await $client.send(new ListObjectVersionsCommand(params));
|
||||
}
|
||||
|
||||
return {
|
||||
headObject,
|
||||
putObject,
|
||||
@@ -187,5 +205,6 @@ export function useObject({ bucket, region }: { bucket: string; region?: string
|
||||
putObjectRetention,
|
||||
getObjectLegalHold,
|
||||
putObjectLegalHold,
|
||||
getObjectVersions,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user