✨ File bundle
This commit is contained in:
@@ -1,133 +1,88 @@
|
||||
<template>
|
||||
<section class="h-full relative flex items-center justify-center">
|
||||
<section class="h-full relative flex flex-col items-center justify-center">
|
||||
<n-card class="max-w-lg my-4 mx-8" title="About" v-if="!userStore.user">
|
||||
<p>Welcome to the <b>Solar Drive</b></p>
|
||||
<p>We help you upload, collect, and share files with ease in mind.</p>
|
||||
<p>To continue, login first.</p>
|
||||
|
||||
<p class="mt-4 opacity-75 text-xs">
|
||||
<span v-if="version == null">Loading...</span>
|
||||
<span v-else>
|
||||
v{{ version.version }} @
|
||||
{{ version.commit.substring(0, 6) }}
|
||||
{{ version.updatedAt }}
|
||||
</span>
|
||||
</p>
|
||||
</n-card>
|
||||
<n-card class="max-w-2xl" title="Upload to Solar Network" v-else>
|
||||
<template #header-extra>
|
||||
<div class="flex gap-2 items-center">
|
||||
<p>Advance Mode</p>
|
||||
<n-switch v-model:value="modeAdvanced" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-collapse-transition :show="showRecycleHint">
|
||||
<n-alert size="small" type="warning" title="Recycle Enabled" class="mb-3">
|
||||
You're uploading to a pool which enabled recycle. If the file you uploaded didn't
|
||||
referenced from the Solar Network. It will be marked and will be deleted some while later.
|
||||
</n-alert>
|
||||
</n-collapse-transition>
|
||||
|
||||
<div class="mb-3">
|
||||
<file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" />
|
||||
</div>
|
||||
|
||||
<n-collapse-transition :show="modeAdvanced">
|
||||
<n-card title="Advance Options" size="small" class="mb-3">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div>
|
||||
<p class="pl-1 mb-0.5">File Password</p>
|
||||
<n-input
|
||||
v-model:value="filePass"
|
||||
:disabled="!currentFilePool?.allow_encryption"
|
||||
placeholder="Enter password to protect the file"
|
||||
show-password-toggle
|
||||
size="large"
|
||||
type="password"
|
||||
class="mb-2"
|
||||
/>
|
||||
<p class="pl-1 text-xs opacity-75 mt-[-4px]">
|
||||
Only available for Stellar Program and certian file pool.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="pl-1 mb-0.5">File Expiration Date</p>
|
||||
<n-date-picker
|
||||
v-model:value="fileExpire"
|
||||
type="datetime"
|
||||
clearable
|
||||
:is-date-disabled="disablePreviousDate"
|
||||
/>
|
||||
</div>
|
||||
<n-card class="max-w-2xl" v-else content-style="padding: 0;">
|
||||
<n-tabs type="line" animated :tabs-padding="20" pane-style="padding: 20px">
|
||||
<template #suffix>
|
||||
<div class="flex gap-2 items-center me-4">
|
||||
<p>Advance Mode</p>
|
||||
<n-switch v-model:value="modeAdvanced" size="small" />
|
||||
</div>
|
||||
</n-card>
|
||||
</n-collapse-transition>
|
||||
</template>
|
||||
|
||||
<n-upload
|
||||
multiple
|
||||
directory-dnd
|
||||
with-credentials
|
||||
show-preview-button
|
||||
list-type="image"
|
||||
show-download-button
|
||||
:custom-request="customRequest"
|
||||
:custom-download="customDownload"
|
||||
:create-thumbnail-url="createThumbnailUrl"
|
||||
@preview="customPreview"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<div style="margin-bottom: 12px">
|
||||
<n-icon size="48" :depth="3">
|
||||
<cloud-upload-round />
|
||||
</n-icon>
|
||||
<n-tab-pane name="direct" tab="Direct Upload" :disabled="isBundleMode">
|
||||
<div class="mb-3">
|
||||
<file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" />
|
||||
</div>
|
||||
<upload-area
|
||||
:filePool="filePool"
|
||||
:pools="pools as SnFilePool[]"
|
||||
:modeAdvanced="modeAdvanced"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="bundle" tab="Bundle Upload">
|
||||
<div class="mb-3">
|
||||
<bundle-select v-model:bundle="selectedBundleId" :disabled="isBundleMode" />
|
||||
</div>
|
||||
<n-text style="font-size: 16px"> Click or drag a file to this area to upload </n-text>
|
||||
<n-p depth="3" style="margin: 8px 0 0 0">
|
||||
Strictly prohibit from uploading sensitive information. For example, your bank card PIN
|
||||
or your credit card expiry date.
|
||||
</n-p>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
|
||||
<p class="mt-4 opacity-75 text-xs">
|
||||
<span v-if="version == null">Loading...</span>
|
||||
<span v-else>
|
||||
v{{ version.version }} @
|
||||
{{ version.commit.substring(0, 6) }}
|
||||
{{ version.updatedAt }}
|
||||
</span>
|
||||
</p>
|
||||
<n-modal v-model:show="showCreateBundleModal" preset="dialog" title="Create New Bundle">
|
||||
<bundle-form ref="bundleFormRef" :value="newBundle" />
|
||||
<template #action>
|
||||
<n-button @click="showCreateBundleModal = false">Cancel</n-button>
|
||||
<n-button type="primary" @click="createBundle">Create</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<n-button @click="showCreateBundleModal = true" class="mb-3" :disabled="isBundleMode">
|
||||
Create New Bundle
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!selectedBundleId && !newBundleId && !isBundleMode"
|
||||
@click="isBundleMode ? cancelBundleUpload() : proceedToBundleUpload()"
|
||||
>
|
||||
{{ isBundleMode ? 'Cancel' : 'Proceed to Upload' }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div v-if="bundleUploadMode" class="mt-3">
|
||||
<upload-area
|
||||
:filePool="filePool"
|
||||
:pools="pools as SnFilePool[]"
|
||||
:modeAdvanced="modeAdvanced"
|
||||
:bundleId="currentBundleId!"
|
||||
/>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
|
||||
<p class="mt-4 opacity-75 text-xs">
|
||||
<span v-if="version == null">Loading...</span>
|
||||
<span v-else>
|
||||
v{{ version.version }} @
|
||||
{{ version.commit.substring(0, 6) }}
|
||||
{{ version.updatedAt }}
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NCard,
|
||||
NUpload,
|
||||
NUploadDragger,
|
||||
NIcon,
|
||||
NText,
|
||||
NP,
|
||||
NInput,
|
||||
NSwitch,
|
||||
NCollapseTransition,
|
||||
NDatePicker,
|
||||
NAlert,
|
||||
type UploadCustomRequestOptions,
|
||||
type UploadSettledFileInfo,
|
||||
type UploadFileInfo,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { NCard, NSwitch, NTabs, NTabPane, NButton, NModal } from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { CloudUploadRound } from '@vicons/material'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import type { SnFilePool } from '@/types/pool'
|
||||
|
||||
import FilePoolSelect from '@/components/FilePoolSelect.vue'
|
||||
|
||||
import * as tus from 'tus-js-client'
|
||||
import UploadArea from '@/components/UploadArea.vue'
|
||||
import BundleSelect from '@/components/BundleSelect.vue'
|
||||
import BundleForm from '@/components/form/BundleForm.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -150,105 +105,57 @@ onMounted(() => fetchPools())
|
||||
const modeAdvanced = ref(false)
|
||||
|
||||
const filePool = ref<string | null>(null)
|
||||
const filePass = ref<string>('')
|
||||
const fileExpire = ref<number | null>(null)
|
||||
|
||||
const currentFilePool = computed(() => {
|
||||
if (!filePool.value) return null
|
||||
return pools.value?.find((pool) => pool.id === filePool.value) ?? null
|
||||
})
|
||||
const showRecycleHint = computed(() => {
|
||||
if (!filePool.value) return true
|
||||
return currentFilePool.value.policy_config?.enable_recycle || false
|
||||
})
|
||||
|
||||
const messageDisplay = useMessage()
|
||||
const bundles = ref<any>([])
|
||||
const selectedBundleId = ref<string | null>(null)
|
||||
const showCreateBundleModal = ref(false)
|
||||
const newBundle = ref<any>({})
|
||||
const bundleFormRef = ref<any>(null)
|
||||
const bundleUploadMode = ref(false)
|
||||
const currentBundleId = ref<string | null>(null)
|
||||
const newBundleId = ref<string | null>(null)
|
||||
const isBundleMode = ref(false)
|
||||
|
||||
function customRequest({
|
||||
file,
|
||||
headers,
|
||||
withCredentials,
|
||||
onFinish,
|
||||
onError,
|
||||
onProgress,
|
||||
}: UploadCustomRequestOptions) {
|
||||
const requestHeaders: Record<string, string> = {}
|
||||
if (filePool.value) requestHeaders['X-FilePool'] = filePool.value
|
||||
if (filePass.value) requestHeaders['X-FilePass'] = filePass.value
|
||||
if (fileExpire.value) requestHeaders['X-FileExpire'] = fileExpire.value.toString()
|
||||
const upload = new tus.Upload(file.file, {
|
||||
endpoint: '/api/tus',
|
||||
retryDelays: [0, 3000, 5000, 10000, 20000],
|
||||
removeFingerprintOnSuccess: false,
|
||||
uploadDataDuringCreation: false,
|
||||
metadata: {
|
||||
filename: file.name,
|
||||
'content-type': file.type ?? 'application/octet-stream',
|
||||
},
|
||||
headers: {
|
||||
'X-DirectUpload': 'true',
|
||||
...requestHeaders,
|
||||
...headers,
|
||||
},
|
||||
onShouldRetry: () => false,
|
||||
onError: function (error) {
|
||||
if (error instanceof tus.DetailedError) {
|
||||
const failedBody = error.originalResponse?.getBody()
|
||||
if (failedBody != null)
|
||||
messageDisplay.error(`Upload failed: ${failedBody}`, {
|
||||
duration: 10000,
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
console.error('[DRIVE] Upload failed:', error)
|
||||
onError()
|
||||
},
|
||||
onProgress: function (bytesUploaded, bytesTotal) {
|
||||
onProgress({ percent: (bytesUploaded / bytesTotal) * 100 })
|
||||
},
|
||||
onSuccess: function (payload) {
|
||||
const rawInfo = payload.lastResponse.getHeader('x-fileinfo')
|
||||
const jsonInfo = JSON.parse(rawInfo as string)
|
||||
console.log('[DRIVE] Upload successful: ', jsonInfo)
|
||||
file.url = `/api/files/${jsonInfo.id}`
|
||||
file.type = jsonInfo.mime_type
|
||||
onFinish()
|
||||
},
|
||||
onBeforeRequest: function (req) {
|
||||
const xhr = req.getUnderlyingObject()
|
||||
xhr.withCredentials = withCredentials
|
||||
},
|
||||
})
|
||||
upload.findPreviousUploads().then(function (previousUploads) {
|
||||
if (previousUploads.length) {
|
||||
upload.resumeFromPreviousUpload(previousUploads[0])
|
||||
async function createBundle() {
|
||||
try {
|
||||
await bundleFormRef.value?.formRef?.validate()
|
||||
const resp = await fetch('/api/bundles', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newBundle.value),
|
||||
})
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to create bundle')
|
||||
}
|
||||
upload.start()
|
||||
})
|
||||
const createdBundle = await resp.json()
|
||||
bundles.value.push(createdBundle)
|
||||
selectedBundleId.value = createdBundle.id
|
||||
newBundleId.value = createdBundle.id
|
||||
showCreateBundleModal.value = false
|
||||
newBundle.value = {}
|
||||
} catch (error) {
|
||||
console.error('Failed to create bundle:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function createThumbnailUrl(
|
||||
_file: File | null,
|
||||
fileInfo: UploadSettledFileInfo,
|
||||
): string | undefined {
|
||||
if (!fileInfo) return undefined
|
||||
return fileInfo.url ?? undefined
|
||||
function proceedToBundleUpload() {
|
||||
currentBundleId.value = selectedBundleId.value || newBundleId.value
|
||||
bundleUploadMode.value = true
|
||||
isBundleMode.value = true
|
||||
}
|
||||
|
||||
function customDownload(file: UploadFileInfo) {
|
||||
const { url } = file
|
||||
if (!url) return
|
||||
window.open(url.replace('/api', ''), '_blank')
|
||||
}
|
||||
|
||||
function customPreview(file: UploadFileInfo, detail: { event: MouseEvent }) {
|
||||
detail.event.preventDefault()
|
||||
const { url } = file
|
||||
if (!url) return
|
||||
window.open(url.replace('/api', ''), '_blank')
|
||||
}
|
||||
|
||||
function disablePreviousDate(ts: number) {
|
||||
return ts <= Date.now()
|
||||
function cancelBundleUpload() {
|
||||
bundleUploadMode.value = false
|
||||
isBundleMode.value = false
|
||||
currentBundleId.value = null
|
||||
selectedBundleId.value = null
|
||||
newBundleId.value = null
|
||||
}
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user