✨ Better downloading in drive
This commit is contained in:
		| @@ -7,6 +7,7 @@ | ||||
|       :description="error" | ||||
|       v-else-if="error === '404'" | ||||
|     /> | ||||
|  | ||||
|     <n-card class="max-w-md my-4 mx-8" v-else-if="error === '403'"> | ||||
|       <n-result | ||||
|         status="403" | ||||
| @@ -30,11 +31,16 @@ | ||||
|         </template> | ||||
|       </n-result> | ||||
|     </n-card> | ||||
|  | ||||
|     <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-gi> | ||||
|           <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-thing :title="file.name" :description="formatBytes(file.size)"> | ||||
|                   <template #header-extra> | ||||
| @@ -44,6 +50,26 @@ | ||||
|               </n-list-item> | ||||
|             </n-list> | ||||
|             <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-gi> | ||||
|  | ||||
| @@ -72,6 +98,13 @@ | ||||
|               <span>{{ bundleInfo.passcode ? 'Yes' : 'No' }}</span> | ||||
|             </div> | ||||
|           </n-card> | ||||
|           <n-input | ||||
|             v-model:value="filePass" | ||||
|             type="password" | ||||
|             size="large" | ||||
|             placeholder="File password file decrypt" | ||||
|             class="mt-3" | ||||
|           /> | ||||
|         </n-gi> | ||||
|       </n-grid> | ||||
|     </n-card> | ||||
| @@ -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<string>('') | ||||
| 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) | ||||
| 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 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -86,7 +86,7 @@ | ||||
|             </n-card> | ||||
|           </div> | ||||
|  | ||||
|           <div class="flex flex-col gap-3" v-if="!progress"> | ||||
|           <div class="flex flex-col gap-3"> | ||||
|             <n-input | ||||
|               v-if="fileInfo.is_encrypted" | ||||
|               placeholder="Password" | ||||
| @@ -113,9 +113,14 @@ | ||||
|               </n-popover> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div v-else> | ||||
|             <n-progress processing :percentage="progress" /> | ||||
|           </div> | ||||
|           <n-collapse-transition :show="!!progress"> | ||||
|             <n-progress | ||||
|               :processing="!!progress && progress < 100" | ||||
|               :percentage="progress" | ||||
|               indicator-placement="inside" | ||||
|               class="mt-4" | ||||
|             /> | ||||
|           </n-collapse-transition> | ||||
|         </n-gi> | ||||
|       </n-grid> | ||||
|     </n-card> | ||||
| @@ -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) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user