diff --git a/DysonNetwork.Drive/Client/src/views/bundles.vue b/DysonNetwork.Drive/Client/src/views/bundles.vue
index 05c564c..39ff2f9 100644
--- a/DysonNetwork.Drive/Client/src/views/bundles.vue
+++ b/DysonNetwork.Drive/Client/src/views/bundles.vue
@@ -7,6 +7,7 @@
:description="error"
v-else-if="error === '404'"
/>
+
+
-
+
@@ -44,6 +50,26 @@
+
+
+
+
+
+ Download All
+
+
@@ -72,6 +98,13 @@
{{ bundleInfo.passcode ? 'Yes' : 'No' }}
+
@@ -93,12 +126,16 @@ import {
NEmpty,
NInput,
NAlert,
+ NProgress,
+ NCollapseTransition,
+ useMessage,
} from 'naive-ui'
import { CalendarTodayRound, LockRound } from '@vicons/material'
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 { downloadAndDecryptFile } from './secure'
const route = useRoute()
const router = useRouter()
@@ -108,6 +145,20 @@ const bundleId = route.params.bundleId
const passcode = ref('')
const passcodeError = ref(null)
+const filePass = ref('')
+
+const downloading = ref(false)
+const downloadProgress = ref()
+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(null)
async function fetchBundleInfo() {
try {
@@ -139,4 +190,66 @@ onMounted(() => fetchBundleInfo())
function goToFileDetails(fileId: string) {
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
+ }
+ }
+ }
+}
diff --git a/DysonNetwork.Drive/Client/src/views/files.vue b/DysonNetwork.Drive/Client/src/views/files.vue
index debe2d6..2e7ec75 100644
--- a/DysonNetwork.Drive/Client/src/views/files.vue
+++ b/DysonNetwork.Drive/Client/src/views/files.vue
@@ -86,7 +86,7 @@
-
-
-
-
+
+
+
@@ -205,7 +210,7 @@ const fileSource = computed(() => {
return url
})
-function downloadFile() {
+async function downloadFile() {
if (fileInfo.value.is_encrypted && !filePass.value) {
messageDisplay.error('Please enter the password to download the file.')
return
@@ -218,7 +223,40 @@ function downloadFile() {
progress.value = undefined
})
} 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)
}
}