✨ Better downloading in drive
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
:description="error"
|
:description="error"
|
||||||
v-else-if="error === '404'"
|
v-else-if="error === '404'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<n-card class="max-w-md my-4 mx-8" v-else-if="error === '403'">
|
<n-card class="max-w-md my-4 mx-8" v-else-if="error === '403'">
|
||||||
<n-result
|
<n-result
|
||||||
status="403"
|
status="403"
|
||||||
@@ -30,11 +31,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-result>
|
</n-result>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card class="max-w-4xl my-4 mx-8" v-else>
|
<n-card class="max-w-4xl my-4 mx-8" v-else>
|
||||||
<n-grid cols="1 m:2" x-gap="16" y-gap="16" responsive="screen">
|
<n-grid cols="1 m:2" x-gap="16" y-gap="16" responsive="screen">
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<n-card title="Content" size="small">
|
<n-card title="Content" size="small">
|
||||||
<n-list size="small" v-if="bundleInfo.files && bundleInfo.files.length > 0" no-padding>
|
<n-list
|
||||||
|
size="small"
|
||||||
|
v-if="bundleInfo.files && bundleInfo.files.length > 0"
|
||||||
|
style="padding: 0"
|
||||||
|
>
|
||||||
<n-list-item v-for="file in bundleInfo.files" :key="file.id">
|
<n-list-item v-for="file in bundleInfo.files" :key="file.id">
|
||||||
<n-thing :title="file.name" :description="formatBytes(file.size)">
|
<n-thing :title="file.name" :description="formatBytes(file.size)">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
@@ -44,6 +50,26 @@
|
|||||||
</n-list-item>
|
</n-list-item>
|
||||||
</n-list>
|
</n-list>
|
||||||
<n-empty v-else description="No files in this bundle" />
|
<n-empty v-else description="No files in this bundle" />
|
||||||
|
<template #footer>
|
||||||
|
<n-collapse-transition :show="!!downloadProgress">
|
||||||
|
<n-progress
|
||||||
|
type="line"
|
||||||
|
:percentage="downloadProgress"
|
||||||
|
indicator-placement="inside"
|
||||||
|
:status="downloadStatus"
|
||||||
|
processing
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
</n-collapse-transition>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
:disabled="!bundleInfo.files || bundleInfo.files.length === 0 || downloading"
|
||||||
|
@click="downloadAllFiles"
|
||||||
|
>
|
||||||
|
Download All
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
|
|
||||||
@@ -72,6 +98,13 @@
|
|||||||
<span>{{ bundleInfo.passcode ? 'Yes' : 'No' }}</span>
|
<span>{{ bundleInfo.passcode ? 'Yes' : 'No' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
<n-input
|
||||||
|
v-model:value="filePass"
|
||||||
|
type="password"
|
||||||
|
size="large"
|
||||||
|
placeholder="File password file decrypt"
|
||||||
|
class="mt-3"
|
||||||
|
/>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</n-card>
|
</n-card>
|
||||||
@@ -93,12 +126,16 @@ import {
|
|||||||
NEmpty,
|
NEmpty,
|
||||||
NInput,
|
NInput,
|
||||||
NAlert,
|
NAlert,
|
||||||
|
NProgress,
|
||||||
|
NCollapseTransition,
|
||||||
|
useMessage,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { CalendarTodayRound, LockRound } from '@vicons/material'
|
import { CalendarTodayRound, LockRound } from '@vicons/material'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { formatBytes } from './format' // Assuming format.ts is in the same directory
|
import { formatBytes } from './format' // Assuming format.ts is in the same directory
|
||||||
|
import { downloadAndDecryptFile } from './secure'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -108,6 +145,20 @@ const bundleId = route.params.bundleId
|
|||||||
const passcode = ref<string>('')
|
const passcode = ref<string>('')
|
||||||
const passcodeError = ref<string | null>(null)
|
const passcodeError = ref<string | null>(null)
|
||||||
|
|
||||||
|
const filePass = ref<string>('')
|
||||||
|
|
||||||
|
const downloading = ref(false)
|
||||||
|
const downloadProgress = ref<number | undefined>()
|
||||||
|
const downloadStatus = ref<'success' | 'error' | 'info'>('info')
|
||||||
|
|
||||||
|
watch(
|
||||||
|
route,
|
||||||
|
(value) => {
|
||||||
|
if (value.query.passcode) passcode.value = value.query.passcode.toString()
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
const bundleInfo = ref<any>(null)
|
const bundleInfo = ref<any>(null)
|
||||||
async function fetchBundleInfo() {
|
async function fetchBundleInfo() {
|
||||||
try {
|
try {
|
||||||
@@ -139,4 +190,66 @@ onMounted(() => fetchBundleInfo())
|
|||||||
function goToFileDetails(fileId: string) {
|
function goToFileDetails(fileId: string) {
|
||||||
router.push({ path: `/files/${fileId}`, query: { passcode: passcode.value } })
|
router.push({ path: `/files/${fileId}`, query: { passcode: passcode.value } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageDisplay = useMessage()
|
||||||
|
|
||||||
|
async function downloadAllFiles() {
|
||||||
|
if (!bundleInfo.value || !bundleInfo.value.files || bundleInfo.value.files.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
downloading.value = true
|
||||||
|
downloadProgress.value = 0
|
||||||
|
downloadStatus.value = 'info'
|
||||||
|
|
||||||
|
const totalFiles = bundleInfo.value.files.length
|
||||||
|
let completedDownloads = 0
|
||||||
|
|
||||||
|
for (const file of bundleInfo.value.files) {
|
||||||
|
let url = `/api/files/${file.id}`
|
||||||
|
if (passcode.value) {
|
||||||
|
url += `?passcode=${passcode.value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.is_encrypted) {
|
||||||
|
downloadAndDecryptFile(file, filePass.value, file.name, () => {})
|
||||||
|
.catch((err) => {
|
||||||
|
messageDisplay.error('Download failed: ' + err.message, {
|
||||||
|
closable: true,
|
||||||
|
duration: 10000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
completedDownloads++
|
||||||
|
downloadProgress.value = (completedDownloads / totalFiles) * 100
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { credentials: 'include' })
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to download ${file.name}: ${res.statusText}`)
|
||||||
|
}
|
||||||
|
const blob = await res.blob()
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = blobUrl
|
||||||
|
a.download = file.name || 'download' // fallback name
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
|
||||||
|
if (completedDownloads === totalFiles) {
|
||||||
|
downloadStatus.value = 'success'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
messageDisplay.error(`Download failed for ${file.name}: ${err}`)
|
||||||
|
downloadStatus.value = 'error'
|
||||||
|
} finally {
|
||||||
|
completedDownloads++
|
||||||
|
downloadProgress.value = (completedDownloads / totalFiles) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@@ -86,7 +86,7 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3" v-if="!progress">
|
<div class="flex flex-col gap-3">
|
||||||
<n-input
|
<n-input
|
||||||
v-if="fileInfo.is_encrypted"
|
v-if="fileInfo.is_encrypted"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
@@ -113,9 +113,14 @@
|
|||||||
</n-popover>
|
</n-popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<n-collapse-transition :show="!!progress">
|
||||||
<n-progress processing :percentage="progress" />
|
<n-progress
|
||||||
</div>
|
:processing="!!progress && progress < 100"
|
||||||
|
:percentage="progress"
|
||||||
|
indicator-placement="inside"
|
||||||
|
class="mt-4"
|
||||||
|
/>
|
||||||
|
</n-collapse-transition>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</n-card>
|
</n-card>
|
||||||
@@ -205,7 +210,7 @@ const fileSource = computed(() => {
|
|||||||
return url
|
return url
|
||||||
})
|
})
|
||||||
|
|
||||||
function downloadFile() {
|
async function downloadFile() {
|
||||||
if (fileInfo.value.is_encrypted && !filePass.value) {
|
if (fileInfo.value.is_encrypted && !filePass.value) {
|
||||||
messageDisplay.error('Please enter the password to download the file.')
|
messageDisplay.error('Please enter the password to download the file.')
|
||||||
return
|
return
|
||||||
@@ -218,7 +223,40 @@ function downloadFile() {
|
|||||||
progress.value = undefined
|
progress.value = undefined
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
window.open(fileSource.value, '_blank')
|
const res = await fetch(fileSource.value, { credentials: 'include' })
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to download ${fileInfo.value.name}: ${res.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLength = res.headers.get('content-length')
|
||||||
|
if (!contentLength) {
|
||||||
|
throw new Error('Content-Length response header is missing.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = parseInt(contentLength, 10)
|
||||||
|
const reader = res.body!.getReader()
|
||||||
|
const chunks: Uint8Array[] = []
|
||||||
|
let received = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
if (value) {
|
||||||
|
chunks.push(value)
|
||||||
|
received += value.length
|
||||||
|
progress.value = (received / total) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob(chunks)
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = blobUrl
|
||||||
|
a.download = fileInfo.value.name || 'download'
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
Reference in New Issue
Block a user