Compare commits
	
		
			2 Commits
		
	
	
		
			f1867e7916
			...
			cf9903e500
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| cf9903e500 | |||
| 186e9c00aa | 
							
								
								
									
										1
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | |||||||
| /Uploads/ | /Uploads/ | ||||||
|  | /Client/node_modules/ | ||||||
| /wwwroot/dist | /wwwroot/dist | ||||||
							
								
								
									
										1
									
								
								DysonNetwork.Drive/Client/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Drive/Client/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -8,6 +8,7 @@ pnpm-debug.log* | |||||||
| lerna-debug.log* | lerna-debug.log* | ||||||
|  |  | ||||||
| node_modules | node_modules | ||||||
|  | **/node_modules/highlight.js/ | ||||||
| .DS_Store | .DS_Store | ||||||
| dist | dist | ||||||
| dist-ssr | dist-ssr | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| <html lang=""> | <html lang=""> | ||||||
|   <head> |   <head> | ||||||
|     <meta charset="UTF-8" /> |     <meta charset="UTF-8" /> | ||||||
|     <link rel="icon" href="/favicon.ico" /> |     <link rel="icon" href="/favicon.png" /> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|     <title>Solar Network Drive</title> |     <title>Solar Network Drive</title> | ||||||
|     <app-data /> |     <app-data /> | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								DysonNetwork.Drive/Client/public/favicon.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								DysonNetwork.Drive/Client/public/favicon.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 70 KiB | 
| @@ -10,7 +10,7 @@ const router = createRouter({ | |||||||
|       component: () => import('../views/index.vue') |       component: () => import('../views/index.vue') | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       path: '/files', |       path: '/files/:fileId', | ||||||
|       name: 'files', |       name: 'files', | ||||||
|       component: () => import('../views/files.vue'), |       component: () => import('../views/files.vue'), | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								DysonNetwork.Drive/Client/src/types/pool.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								DysonNetwork.Drive/Client/src/types/pool.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | export interface SnFilePool { | ||||||
|  |     id:                  string; | ||||||
|  |     name:                string; | ||||||
|  |     storage_config:      StorageConfig; | ||||||
|  |     billing_config:      BillingConfig; | ||||||
|  |     public_indexable:    boolean; | ||||||
|  |     public_usable:       boolean; | ||||||
|  |     no_optimization:     boolean; | ||||||
|  |     no_metadata:         boolean; | ||||||
|  |     allow_encryption:    boolean; | ||||||
|  |     allow_anonymous:     boolean; | ||||||
|  |     require_privilege:   number; | ||||||
|  |     account_id:          null; | ||||||
|  |     resource_identifier: string; | ||||||
|  |     created_at:          Date; | ||||||
|  |     updated_at:          Date; | ||||||
|  |     deleted_at:          null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface BillingConfig { | ||||||
|  |     cost_multiplier: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface StorageConfig { | ||||||
|  |     region:        string; | ||||||
|  |     bucket:        string; | ||||||
|  |     endpoint:      string; | ||||||
|  |     secret_id:     string; | ||||||
|  |     secret_key:    string; | ||||||
|  |     enable_signed: boolean; | ||||||
|  |     enable_ssl:    boolean; | ||||||
|  |     image_proxy:   null; | ||||||
|  |     access_proxy:  null; | ||||||
|  |     expiration:    null; | ||||||
|  | } | ||||||
| @@ -1,36 +1,212 @@ | |||||||
| <template> | <template> | ||||||
|   <section class="h-full relative flex items-center justify-center"> |   <section class="min-h-full relative flex items-center justify-center"> | ||||||
|     <n-card class="max-w-lg" title="Download file"> |     <n-spin v-if="!fileInfo && !error" /> | ||||||
|       <div class="flex flex-col gap-3" v-if="!progress"> |     <n-result status="404" title="No file was found" :description="error" v-else-if="error" /> | ||||||
|         <n-input placeholder="File ID" v-model:value="fileId" /> |     <n-card class="max-w-4xl my-4" v-else> | ||||||
|         <n-input placeholder="Password" v-model:value="filePass" type="password" /> |       <n-grid :cols="2" x-gap="16"> | ||||||
|         <n-button @click="downloadFile">Download</n-button> |         <n-gi> | ||||||
|  |           <div v-if="fileInfo.is_encrypted"> | ||||||
|  |             <n-alert type="info" size="small" title="Encrypted file"> | ||||||
|  |               The file has been encrypted. Preview not available. Please enter the password to | ||||||
|  |               download it. | ||||||
|  |             </n-alert> | ||||||
|           </div> |           </div> | ||||||
|           <div v-else> |           <div v-else> | ||||||
|         <n-progress :percentage="progress" /> |             <n-image v-if="fileType === 'image'" :src="fileSource" class="w-full" /> | ||||||
|  |             <video v-else-if="fileType === 'video'" :src="fileSource" controls class="w-full" /> | ||||||
|           </div> |           </div> | ||||||
|  |         </n-gi> | ||||||
|  |  | ||||||
|  |         <n-gi> | ||||||
|  |           <div class="mb-3"> | ||||||
|  |             <n-card title="File Infomation" size="small"> | ||||||
|  |               <div class="flex gap-2"> | ||||||
|  |                 <span class="flex-grow-1 flex items-center gap-2"> | ||||||
|  |                   <n-icon> | ||||||
|  |                     <info-round /> | ||||||
|  |                   </n-icon> | ||||||
|  |                   File Type | ||||||
|  |                 </span> | ||||||
|  |                 <span>{{ fileInfo.mime_type }} ({{ fileType }})</span> | ||||||
|  |               </div> | ||||||
|  |               <div class="flex gap-2"> | ||||||
|  |                 <span class="flex-grow-1 flex items-center gap-2"> | ||||||
|  |                   <n-icon> | ||||||
|  |                     <data-usage-round /> | ||||||
|  |                   </n-icon> | ||||||
|  |                   File Size | ||||||
|  |                 </span> | ||||||
|  |                 <span>{{ formatBytes(fileInfo.size) }}</span> | ||||||
|  |               </div> | ||||||
|  |               <div class="flex gap-2"> | ||||||
|  |                 <span class="flex-grow-1 flex items-center gap-2"> | ||||||
|  |                   <n-icon> | ||||||
|  |                     <file-upload-outlined /> | ||||||
|  |                   </n-icon> | ||||||
|  |                   Uploaded At | ||||||
|  |                 </span> | ||||||
|  |                 <span>{{ new Date(fileInfo.created_at).toLocaleString() }}</span> | ||||||
|  |               </div> | ||||||
|  |               <div class="flex gap-2"> | ||||||
|  |                 <span class="flex-grow-1 flex items-center gap-2"> | ||||||
|  |                   <n-icon> | ||||||
|  |                     <details-round /> | ||||||
|  |                   </n-icon> | ||||||
|  |                   Techical Info | ||||||
|  |                 </span> | ||||||
|  |                 <n-button text size="small" @click="showTechDetails = !showTechDetails"> | ||||||
|  |                   {{ showTechDetails ? 'Hide' : 'Show' }} | ||||||
|  |                 </n-button> | ||||||
|  |               </div> | ||||||
|  |  | ||||||
|  |               <n-collapse-transition :show="showTechDetails"> | ||||||
|  |                 <div v-if="showTechDetails" class="mt-2 flex flex-col gap-1"> | ||||||
|  |                   <p class="text-xs opacity-75">#{{ fileInfo.id }}</p> | ||||||
|  |  | ||||||
|  |                   <n-card size="small" content-style="padding: 0" embedded> | ||||||
|  |                     <div class="overflow-x-auto px-4 py-2"> | ||||||
|  |                       <n-code | ||||||
|  |                         :code="JSON.stringify(fileInfo.file_meta, null, 4)" | ||||||
|  |                         language="json" | ||||||
|  |                         :hljs="hljs" | ||||||
|  |                       /> | ||||||
|  |                     </div> | ||||||
|  |                   </n-card> | ||||||
|  |                 </div> | ||||||
|  |               </n-collapse-transition> | ||||||
|  |             </n-card> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <div class="flex flex-col gap-3" v-if="!progress"> | ||||||
|  |             <n-input | ||||||
|  |               v-if="fileInfo.is_encrypted" | ||||||
|  |               placeholder="Password" | ||||||
|  |               v-model:value="filePass" | ||||||
|  |               type="password" | ||||||
|  |             /> | ||||||
|  |             <div class="flex gap-2"> | ||||||
|  |               <n-button class="flex-grow-1" @click="downloadFile">Download</n-button> | ||||||
|  |               <n-popover placement="bottom" trigger="hover"> | ||||||
|  |                 <template #trigger> | ||||||
|  |                   <n-button> | ||||||
|  |                     <n-icon> | ||||||
|  |                       <qr-code-round /> | ||||||
|  |                     </n-icon> | ||||||
|  |                   </n-button> | ||||||
|  |                 </template> | ||||||
|  |                 <n-qr-code | ||||||
|  |                   type="svg" | ||||||
|  |                   :value="currentUrl" | ||||||
|  |                   :size="160" | ||||||
|  |                   icon-src="/favicon.png" | ||||||
|  |                   error-correction-level="H" | ||||||
|  |                 /> | ||||||
|  |               </n-popover> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |           <div v-else> | ||||||
|  |             <n-progress processing :percentage="progress" /> | ||||||
|  |           </div> | ||||||
|  |         </n-gi> | ||||||
|  |       </n-grid> | ||||||
|     </n-card> |     </n-card> | ||||||
|   </section> |   </section> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { NCard, NInput, NButton, NProgress, useMessage } from 'naive-ui' | import { | ||||||
| import { ref } from 'vue' |   NCard, | ||||||
|  |   NInput, | ||||||
|  |   NButton, | ||||||
|  |   NProgress, | ||||||
|  |   NResult, | ||||||
|  |   NSpin, | ||||||
|  |   NImage, | ||||||
|  |   NAlert, | ||||||
|  |   NIcon, | ||||||
|  |   NCollapseTransition, | ||||||
|  |   NCode, | ||||||
|  |   NGrid, | ||||||
|  |   NGi, | ||||||
|  |   NPopover, | ||||||
|  |   NQrCode, | ||||||
|  |   useMessage, | ||||||
|  | } from 'naive-ui' | ||||||
|  | import { | ||||||
|  |   DataUsageRound, | ||||||
|  |   InfoRound, | ||||||
|  |   DetailsRound, | ||||||
|  |   FileUploadOutlined, | ||||||
|  |   QrCodeRound, | ||||||
|  | } from '@vicons/material' | ||||||
|  | import { useRoute } from 'vue-router' | ||||||
|  | import { computed, onMounted, ref } from 'vue' | ||||||
|  |  | ||||||
| import { downloadAndDecryptFile } from './secure' | import { downloadAndDecryptFile } from './secure' | ||||||
|  |  | ||||||
|  | import hljs from 'highlight.js/lib/core' | ||||||
|  | import json from 'highlight.js/lib/languages/json' | ||||||
|  |  | ||||||
|  | hljs.registerLanguage('json', json) | ||||||
|  |  | ||||||
|  | const route = useRoute() | ||||||
|  |  | ||||||
|  | const error = ref<string | null>(null) | ||||||
|  |  | ||||||
| const filePass = ref<string>('') | const filePass = ref<string>('') | ||||||
| const fileId = ref<string>('') | const fileId = route.params.fileId | ||||||
|  |  | ||||||
| const progress = ref<number | undefined>(0) | const progress = ref<number | undefined>(0) | ||||||
|  |  | ||||||
|  | const showTechDetails = ref<boolean>(false) | ||||||
|  |  | ||||||
| const messageDisplay = useMessage() | const messageDisplay = useMessage() | ||||||
|  |  | ||||||
|  | const currentUrl = window.location.href | ||||||
|  |  | ||||||
|  | const fileInfo = ref<any>(null) | ||||||
|  | async function fetchFileInfo() { | ||||||
|  |   try { | ||||||
|  |     const resp = await fetch('/api/files/' + fileId + '/info') | ||||||
|  |     if (!resp.ok) { | ||||||
|  |       throw new Error('Failed to fetch file info: ' + resp.statusText) | ||||||
|  |     } | ||||||
|  |     fileInfo.value = await resp.json() | ||||||
|  |   } catch (err) { | ||||||
|  |     error.value = (err as Error).message | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | onMounted(() => fetchFileInfo()) | ||||||
|  |  | ||||||
|  | const fileType = computed(() => { | ||||||
|  |   if (!fileInfo.value) return 'unknown' | ||||||
|  |   return fileInfo.value.mime_type?.split('/')[0] || 'unknown' | ||||||
|  | }) | ||||||
|  | const fileSource = computed(() => `/api/files/${fileId}`) | ||||||
|  |  | ||||||
| function downloadFile() { | function downloadFile() { | ||||||
|   downloadAndDecryptFile('/api/files/' + fileId.value, filePass.value, (p: number) => { |   if (fileInfo.value.is_encrypted && !filePass.value) { | ||||||
|  |     messageDisplay.error('Please enter the password to download the file.') | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   if (fileInfo.value.is_encrypted) { | ||||||
|  |     downloadAndDecryptFile(fileSource.value, filePass.value, fileInfo.value.name, (p: number) => { | ||||||
|       progress.value = p * 100 |       progress.value = p * 100 | ||||||
|     }).catch((err) => { |     }).catch((err) => { | ||||||
|       messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 }) |       messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 }) | ||||||
|  |       progress.value = undefined | ||||||
|     }) |     }) | ||||||
|  |   } else { | ||||||
|  |     window.open(fileSource.value, '_blank') | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function formatBytes(bytes: number, decimals = 2): string { | ||||||
|  |   if (bytes === 0) return '0 Bytes' | ||||||
|  |   const k = 1024 | ||||||
|  |   const dm = decimals < 0 ? 0 : decimals | ||||||
|  |   const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] | ||||||
|  |   const i = Math.floor(Math.log(bytes) / Math.log(k)) | ||||||
|  |   return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -22,16 +22,37 @@ | |||||||
|         </div> |         </div> | ||||||
|       </template> |       </template> | ||||||
|  |  | ||||||
|       <div class="mb-3" v-if="modeAdvanced"> |       <div class="mb-3"> | ||||||
|  |         <n-select | ||||||
|  |           v-model:value="filePool" | ||||||
|  |           :options="pools ?? []" | ||||||
|  |           :render-label="renderPoolSelectLabel" | ||||||
|  |           :render-tag="renderSingleSelectTag" | ||||||
|  |           value-field="id" | ||||||
|  |           label-field="name" | ||||||
|  |           placeholder="Select a file pool to upload" | ||||||
|  |           clearable | ||||||
|  |           size="large" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <n-collapse-transition :show="modeAdvanced"> | ||||||
|  |         <n-card title="Advance Options" size="small" class="mb-3"> | ||||||
|  |           <div> | ||||||
|  |             <p class="pl-1 mb-0.5">File Password</p> | ||||||
|             <n-input |             <n-input | ||||||
|               v-model:value="filePass" |               v-model:value="filePass" | ||||||
|  |               :disabled="!currentFilePool?.allow_encryption" | ||||||
|               placeholder="Enter password to protect the file" |               placeholder="Enter password to protect the file" | ||||||
|           clearable |               show-password-toggle | ||||||
|               size="large" |               size="large" | ||||||
|               type="password" |               type="password" | ||||||
|               class="mb-2" |               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> | ||||||
|  |         </n-card> | ||||||
|  |       </n-collapse-transition> | ||||||
|  |  | ||||||
|       <n-upload |       <n-upload | ||||||
|         multiple |         multiple | ||||||
| @@ -39,13 +60,16 @@ | |||||||
|         with-credentials |         with-credentials | ||||||
|         show-preview-button |         show-preview-button | ||||||
|         list-type="image" |         list-type="image" | ||||||
|  |         show-download-button | ||||||
|         :custom-request="customRequest" |         :custom-request="customRequest" | ||||||
|  |         :custom-download="customDownload" | ||||||
|         :create-thumbnail-url="createThumbnailUrl" |         :create-thumbnail-url="createThumbnailUrl" | ||||||
|  |         @preview="customPreview" | ||||||
|       > |       > | ||||||
|         <n-upload-dragger> |         <n-upload-dragger> | ||||||
|           <div style="margin-bottom: 12px"> |           <div style="margin-bottom: 12px"> | ||||||
|             <n-icon size="48" :depth="3"> |             <n-icon size="48" :depth="3"> | ||||||
|               <upload-outlined /> |               <cloud-upload-round /> | ||||||
|             </n-icon> |             </n-icon> | ||||||
|           </div> |           </div> | ||||||
|           <n-text style="font-size: 16px"> Click or drag a file to this area to upload </n-text> |           <n-text style="font-size: 16px"> Click or drag a file to this area to upload </n-text> | ||||||
| @@ -78,30 +102,131 @@ import { | |||||||
|   NP, |   NP, | ||||||
|   NInput, |   NInput, | ||||||
|   NSwitch, |   NSwitch, | ||||||
|  |   NSelect, | ||||||
|  |   NTag, | ||||||
|  |   NCollapseTransition, | ||||||
|   type UploadCustomRequestOptions, |   type UploadCustomRequestOptions, | ||||||
|   type UploadSettledFileInfo, |   type UploadSettledFileInfo, | ||||||
|  |   type SelectOption, | ||||||
|  |   type SelectRenderTag, | ||||||
|  |   type UploadFileInfo, | ||||||
| } from 'naive-ui' | } from 'naive-ui' | ||||||
| import { onMounted, ref } from 'vue' | import { computed, h, onMounted, ref } from 'vue' | ||||||
| import { UploadOutlined } from '@vicons/material' | import { CloudUploadRound } from '@vicons/material' | ||||||
| import { useUserStore } from '@/stores/user' | import { useUserStore } from '@/stores/user' | ||||||
|  | import type { SnFilePool } from '@/types/pool' | ||||||
|  |  | ||||||
| import * as tus from 'tus-js-client' | import * as tus from 'tus-js-client' | ||||||
|  |  | ||||||
| const userStore = useUserStore() | const userStore = useUserStore() | ||||||
|  |  | ||||||
| const version = ref<any>(null) | const version = ref<any>(null) | ||||||
|  |  | ||||||
| async function fetchVersion() { | async function fetchVersion() { | ||||||
|   const resp = await fetch('/api/version') |   const resp = await fetch('/api/version') | ||||||
|   version.value = await resp.json() |   version.value = await resp.json() | ||||||
| } | } | ||||||
|  |  | ||||||
| onMounted(() => fetchVersion()) | onMounted(() => fetchVersion()) | ||||||
|  |  | ||||||
|  | type SnFilePoolOption = SnFilePool & any | ||||||
|  |  | ||||||
|  | const pools = ref<SnFilePoolOption[] | undefined>() | ||||||
|  | async function fetchPools() { | ||||||
|  |   const resp = await fetch('/api/pools') | ||||||
|  |   pools.value = await resp.json() | ||||||
|  | } | ||||||
|  | onMounted(() => fetchPools()) | ||||||
|  |  | ||||||
|  | const renderSingleSelectTag: SelectRenderTag = ({ option }) => { | ||||||
|  |   return h( | ||||||
|  |     'div', | ||||||
|  |     { | ||||||
|  |       style: { | ||||||
|  |         display: 'flex', | ||||||
|  |         alignItems: 'center', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     [option.name as string], | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function renderPoolSelectLabel(option: SelectOption & SnFilePool, selected: boolean) { | ||||||
|  |   return h( | ||||||
|  |     'div', | ||||||
|  |     { | ||||||
|  |       style: { | ||||||
|  |         padding: '8px 2px', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     [ | ||||||
|  |       h('div', null, [option.name as string]), | ||||||
|  |       h( | ||||||
|  |         'div', | ||||||
|  |         { | ||||||
|  |           style: { | ||||||
|  |             display: 'flex', | ||||||
|  |             gap: '0.25rem', | ||||||
|  |             marginTop: '2px', | ||||||
|  |             marginLeft: '-2px', | ||||||
|  |             marginRight: '-2px', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         [ | ||||||
|  |           option.public_usable && | ||||||
|  |             h( | ||||||
|  |               NTag, | ||||||
|  |               { | ||||||
|  |                 type: 'info', | ||||||
|  |                 size: 'small', | ||||||
|  |                 round: true, | ||||||
|  |               }, | ||||||
|  |               { default: () => 'Public Shared' }, | ||||||
|  |             ), | ||||||
|  |           option.public_indexable && | ||||||
|  |             h( | ||||||
|  |               NTag, | ||||||
|  |               { | ||||||
|  |                 type: 'success', | ||||||
|  |                 size: 'small', | ||||||
|  |                 round: true, | ||||||
|  |               }, | ||||||
|  |               { default: () => 'Public Indexable' }, | ||||||
|  |             ), | ||||||
|  |           option.allow_encryption && | ||||||
|  |             h( | ||||||
|  |               NTag, | ||||||
|  |               { | ||||||
|  |                 type: 'warning', | ||||||
|  |                 size: 'small', | ||||||
|  |                 round: true, | ||||||
|  |               }, | ||||||
|  |               { default: () => 'Allow Encryption' }, | ||||||
|  |             ), | ||||||
|  |           option.allow_anonymous && | ||||||
|  |             h( | ||||||
|  |               NTag, | ||||||
|  |               { | ||||||
|  |                 type: 'info', | ||||||
|  |                 size: 'small', | ||||||
|  |                 round: true, | ||||||
|  |               }, | ||||||
|  |               { default: () => 'Allow Anonymous' }, | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ], | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
| const modeAdvanced = ref(false) | const modeAdvanced = ref(false) | ||||||
|  |  | ||||||
|  | const filePool = ref<string | null>(null) | ||||||
| const filePass = ref<string>('') | const filePass = ref<string>('') | ||||||
|  |  | ||||||
|  | const currentFilePool = computed(() => { | ||||||
|  |   if (!filePool.value) return null | ||||||
|  |   return pools.value?.find((pool) => pool.id === filePool.value) ?? null | ||||||
|  | }) | ||||||
|  |  | ||||||
| function customRequest({ | function customRequest({ | ||||||
|   file, |   file, | ||||||
|   data, |   data, | ||||||
| @@ -112,6 +237,9 @@ function customRequest({ | |||||||
|   onError, |   onError, | ||||||
|   onProgress, |   onProgress, | ||||||
| }: UploadCustomRequestOptions) { | }: UploadCustomRequestOptions) { | ||||||
|  |   const requestHeaders: Record<string, string> = {} | ||||||
|  |   if (filePool.value) requestHeaders['X-FilePool'] = filePool.value | ||||||
|  |   if (filePass.value) requestHeaders['X-FilePass'] = filePass.value | ||||||
|   const upload = new tus.Upload(file.file, { |   const upload = new tus.Upload(file.file, { | ||||||
|     endpoint: '/api/tus', |     endpoint: '/api/tus', | ||||||
|     retryDelays: [0, 3000, 5000, 10000, 20000], |     retryDelays: [0, 3000, 5000, 10000, 20000], | ||||||
| @@ -120,7 +248,7 @@ function customRequest({ | |||||||
|       filetype: file.type ?? 'application/octet-stream', |       filetype: file.type ?? 'application/octet-stream', | ||||||
|     }, |     }, | ||||||
|     headers: { |     headers: { | ||||||
|       'X-FilePass': filePass.value, |       ...requestHeaders, | ||||||
|       ...headers, |       ...headers, | ||||||
|     }, |     }, | ||||||
|     onError: function (error) { |     onError: function (error) { | ||||||
| @@ -151,8 +279,25 @@ function customRequest({ | |||||||
|   }) |   }) | ||||||
| } | } | ||||||
|  |  | ||||||
| function createThumbnailUrl(_file: File | null, fileInfo: UploadSettledFileInfo): string | undefined { | function createThumbnailUrl( | ||||||
|  |   _file: File | null, | ||||||
|  |   fileInfo: UploadSettledFileInfo, | ||||||
|  | ): string | undefined { | ||||||
|   if (!fileInfo) return undefined |   if (!fileInfo) return undefined | ||||||
|   return fileInfo.url ?? undefined |   return fileInfo.url ?? undefined | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function customDownload(file: UploadFileInfo) { | ||||||
|  |   const { url, name } = file | ||||||
|  |   if (!url) | ||||||
|  |     return | ||||||
|  |   window.open(url.replace('/api', ''), '_blank') | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function customPreview(file: UploadFileInfo, detail: { event: MouseEvent }) { | ||||||
|  |   detail.event.preventDefault() | ||||||
|  |   const { url, type } = file | ||||||
|  |   if (!url) return | ||||||
|  |   window.open(url.replace('/api', ''), '_blank') | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -1,92 +1,94 @@ | |||||||
| export async function downloadAndDecryptFile( | export async function downloadAndDecryptFile( | ||||||
|   url: string, |   url: string, | ||||||
|   password: string, |   password: string, | ||||||
|   onProgress?: (progress: number) => void |   fileName: string, | ||||||
|  |   onProgress?: (progress: number) => void, | ||||||
| ): Promise<void> { | ): Promise<void> { | ||||||
|   const response = await fetch(url); |   const response = await fetch(url) | ||||||
|   if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`); |   if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`) | ||||||
|  |  | ||||||
|   const contentLength = +(response.headers.get('Content-Length') || 0); |   const contentLength = +(response.headers.get('Content-Length') || 0) | ||||||
|   const reader = response.body!.getReader(); |   const reader = response.body!.getReader() | ||||||
|   const chunks: Uint8Array[] = []; |   const chunks: Uint8Array[] = [] | ||||||
|   let received = 0; |   let received = 0 | ||||||
|  |  | ||||||
|   while (true) { |   while (true) { | ||||||
|     const { done, value } = await reader.read(); |     const { done, value } = await reader.read() | ||||||
|     if (done) break; |     if (done) break | ||||||
|     if (value) { |     if (value) { | ||||||
|       chunks.push(value); |       chunks.push(value) | ||||||
|       received += value.length; |       received += value.length | ||||||
|       if (contentLength && onProgress) { |       if (contentLength && onProgress) { | ||||||
|         onProgress(received / contentLength); |         onProgress(received / contentLength) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const fullBuffer = new Uint8Array(received); |   const fullBuffer = new Uint8Array(received) | ||||||
|   let offset = 0; |   let offset = 0 | ||||||
|   for (const chunk of chunks) { |   for (const chunk of chunks) { | ||||||
|     fullBuffer.set(chunk, offset); |     fullBuffer.set(chunk, offset) | ||||||
|     offset += chunk.length; |     offset += chunk.length | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const decryptedBytes = await decryptFile(fullBuffer, password); |   const decryptedBytes = await decryptFile(fullBuffer, password) | ||||||
|  |  | ||||||
|   // Create a blob and trigger a download |   // Create a blob and trigger a download | ||||||
|   const blob = new Blob([decryptedBytes]); |   const blob = new Blob([decryptedBytes]) | ||||||
|   const downloadUrl = URL.createObjectURL(blob); |   const downloadUrl = URL.createObjectURL(blob) | ||||||
|   const a = document.createElement('a'); |   const a = document.createElement('a') | ||||||
|   a.href = downloadUrl; |   a.href = downloadUrl | ||||||
|   a.download = 'decrypted_file'; // You may allow customization |   a.download = fileName | ||||||
|   document.body.appendChild(a); |   document.body.appendChild(a) | ||||||
|   a.click(); |   a.click() | ||||||
|   a.remove(); |   a.remove() | ||||||
|   URL.revokeObjectURL(downloadUrl); |   URL.revokeObjectURL(downloadUrl) | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function decryptFile( | export async function decryptFile(fileBuffer: Uint8Array, password: string): Promise<Uint8Array> { | ||||||
|   fileBuffer: Uint8Array, |   const salt = fileBuffer.slice(0, 16) | ||||||
|   password: string |   const nonce = fileBuffer.slice(16, 28) | ||||||
| ): Promise<Uint8Array> { |   const tag = fileBuffer.slice(28, 44) | ||||||
|   const salt = fileBuffer.slice(0, 16); |   const ciphertext = fileBuffer.slice(44) | ||||||
|   const nonce = fileBuffer.slice(16, 28); |  | ||||||
|   const tag = fileBuffer.slice(28, 44); |  | ||||||
|   const ciphertext = fileBuffer.slice(44); |  | ||||||
|  |  | ||||||
|   const enc = new TextEncoder(); |   const enc = new TextEncoder() | ||||||
|   const keyMaterial = await crypto.subtle.importKey( |   const keyMaterial = await crypto.subtle.importKey( | ||||||
|     'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey'] |     'raw', | ||||||
|   ); |     enc.encode(password), | ||||||
|  |     { name: 'PBKDF2' }, | ||||||
|  |     false, | ||||||
|  |     ['deriveKey'], | ||||||
|  |   ) | ||||||
|   const key = await crypto.subtle.deriveKey( |   const key = await crypto.subtle.deriveKey( | ||||||
|     { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, |     { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, | ||||||
|     keyMaterial, |     keyMaterial, | ||||||
|     { name: 'AES-GCM', length: 256 }, |     { name: 'AES-GCM', length: 256 }, | ||||||
|     false, |     false, | ||||||
|     ['decrypt'] |     ['decrypt'], | ||||||
|   ); |   ) | ||||||
|  |  | ||||||
|   const fullCiphertext = new Uint8Array(ciphertext.length + tag.length); |   const fullCiphertext = new Uint8Array(ciphertext.length + tag.length) | ||||||
|   fullCiphertext.set(ciphertext); |   fullCiphertext.set(ciphertext) | ||||||
|   fullCiphertext.set(tag, ciphertext.length); |   fullCiphertext.set(tag, ciphertext.length) | ||||||
|  |  | ||||||
|   let decrypted: ArrayBuffer; |   let decrypted: ArrayBuffer | ||||||
|   try { |   try { | ||||||
|     decrypted = await crypto.subtle.decrypt( |     decrypted = await crypto.subtle.decrypt( | ||||||
|       { name: 'AES-GCM', iv: nonce, tagLength: 128 }, |       { name: 'AES-GCM', iv: nonce, tagLength: 128 }, | ||||||
|       key, |       key, | ||||||
|       fullCiphertext |       fullCiphertext, | ||||||
|     ); |     ) | ||||||
|   } catch { |   } catch { | ||||||
|     throw new Error("Incorrect password or corrupted file."); |     throw new Error('Incorrect password or corrupted file.') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const magic = new TextEncoder().encode("DYSON1"); |   const magic = new TextEncoder().encode('DYSON1') | ||||||
|   const decryptedBytes = new Uint8Array(decrypted); |   const decryptedBytes = new Uint8Array(decrypted) | ||||||
|   for (let i = 0; i < magic.length; i++) { |   for (let i = 0; i < magic.length; i++) { | ||||||
|     if (decryptedBytes[i] !== magic[i]) { |     if (decryptedBytes[i] !== magic[i]) { | ||||||
|       throw new Error("Incorrect password or corrupted file."); |       throw new Error('Incorrect password or corrupted file.') | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return decryptedBytes.slice(magic.length); |   return decryptedBytes.slice(magic.length) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										288
									
								
								DysonNetwork.Drive/Migrations/20250726034305_FilePoolAuthorize.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								DysonNetwork.Drive/Migrations/20250726034305_FilePoolAuthorize.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,288 @@ | |||||||
|  | // <auto-generated /> | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using DysonNetwork.Drive; | ||||||
|  | using DysonNetwork.Drive.Storage; | ||||||
|  | using DysonNetwork.Shared.Data; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
|  | using NodaTime; | ||||||
|  | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Migrations | ||||||
|  | { | ||||||
|  |     [DbContext(typeof(AppDatabase))] | ||||||
|  |     [Migration("20250726034305_FilePoolAuthorize")] | ||||||
|  |     partial class FilePoolAuthorize | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||||
|  |         { | ||||||
|  | #pragma warning disable 612, 618 | ||||||
|  |             modelBuilder | ||||||
|  |                 .HasAnnotation("ProductVersion", "9.0.7") | ||||||
|  |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|  |             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||||
|  |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<string>("Id") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Description") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("description"); | ||||||
|  |  | ||||||
|  |                     b.Property<Dictionary<string, object>>("FileMeta") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("file_meta"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("HasCompression") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("has_compression"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("HasThumbnail") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("has_thumbnail"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Hash") | ||||||
|  |                         .HasMaxLength(256) | ||||||
|  |                         .HasColumnType("character varying(256)") | ||||||
|  |                         .HasColumnName("hash"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsEncrypted") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_encrypted"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsMarkedRecycle") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_marked_recycle"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("MimeType") | ||||||
|  |                         .HasMaxLength(256) | ||||||
|  |                         .HasColumnType("character varying(256)") | ||||||
|  |                         .HasColumnName("mime_type"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid?>("PoolId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("pool_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("sensitive_marks"); | ||||||
|  |  | ||||||
|  |                     b.Property<long>("Size") | ||||||
|  |                         .HasColumnType("bigint") | ||||||
|  |                         .HasColumnName("size"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("StorageId") | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("storage_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("StorageUrl") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("storage_url"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("UploadedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("uploaded_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("UploadedTo") | ||||||
|  |                         .HasMaxLength(128) | ||||||
|  |                         .HasColumnType("character varying(128)") | ||||||
|  |                         .HasColumnName("uploaded_to"); | ||||||
|  |  | ||||||
|  |                     b.Property<Dictionary<string, object>>("UserMeta") | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("user_meta"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_files"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("PoolId") | ||||||
|  |                         .HasDatabaseName("ix_files_pool_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("files", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("ExpiredAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("expired_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("FileId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(32) | ||||||
|  |                         .HasColumnType("character varying(32)") | ||||||
|  |                         .HasColumnName("file_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("ResourceId") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("resource_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Usage") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("usage"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_file_references"); | ||||||
|  |  | ||||||
|  |                     b.HasIndex("FileId") | ||||||
|  |                         .HasDatabaseName("ix_file_references_file_id"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("file_references", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => | ||||||
|  |                 { | ||||||
|  |                     b.Property<Guid>("Id") | ||||||
|  |                         .ValueGeneratedOnAdd() | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid?>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("AllowAnonymous") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("allow_anonymous"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("AllowEncryption") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("allow_encryption"); | ||||||
|  |  | ||||||
|  |                     b.Property<BillingConfig>("BillingConfig") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("billing_config"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("CreatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("created_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant?>("DeletedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("Name") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasMaxLength(1024) | ||||||
|  |                         .HasColumnType("character varying(1024)") | ||||||
|  |                         .HasColumnName("name"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("NoMetadata") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("no_metadata"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("NoOptimization") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("no_optimization"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("PublicIndexable") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("public_indexable"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("PublicUsable") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("public_usable"); | ||||||
|  |  | ||||||
|  |                     b.Property<int>("RequirePrivilege") | ||||||
|  |                         .HasColumnType("integer") | ||||||
|  |                         .HasColumnName("require_privilege"); | ||||||
|  |  | ||||||
|  |                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasColumnType("jsonb") | ||||||
|  |                         .HasColumnName("storage_config"); | ||||||
|  |  | ||||||
|  |                     b.Property<Instant>("UpdatedAt") | ||||||
|  |                         .HasColumnType("timestamp with time zone") | ||||||
|  |                         .HasColumnName("updated_at"); | ||||||
|  |  | ||||||
|  |                     b.HasKey("Id") | ||||||
|  |                         .HasName("pk_pools"); | ||||||
|  |  | ||||||
|  |                     b.ToTable("pools", (string)null); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("PoolId") | ||||||
|  |                         .HasConstraintName("fk_files_pools_pool_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("Pool"); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||||
|  |                 { | ||||||
|  |                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") | ||||||
|  |                         .WithMany() | ||||||
|  |                         .HasForeignKey("FileId") | ||||||
|  |                         .OnDelete(DeleteBehavior.Cascade) | ||||||
|  |                         .IsRequired() | ||||||
|  |                         .HasConstraintName("fk_file_references_files_file_id"); | ||||||
|  |  | ||||||
|  |                     b.Navigation("File"); | ||||||
|  |                 }); | ||||||
|  | #pragma warning restore 612, 618 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,51 @@ | |||||||
|  | using System; | ||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class FilePoolAuthorize : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<Guid>( | ||||||
|  |                 name: "account_id", | ||||||
|  |                 table: "pools", | ||||||
|  |                 type: "uuid", | ||||||
|  |                 nullable: true); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<bool>( | ||||||
|  |                 name: "public_indexable", | ||||||
|  |                 table: "pools", | ||||||
|  |                 type: "boolean", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: false); | ||||||
|  |  | ||||||
|  |             migrationBuilder.AddColumn<bool>( | ||||||
|  |                 name: "public_usable", | ||||||
|  |                 table: "pools", | ||||||
|  |                 type: "boolean", | ||||||
|  |                 nullable: false, | ||||||
|  |                 defaultValue: false); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "account_id", | ||||||
|  |                 table: "pools"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "public_indexable", | ||||||
|  |                 table: "pools"); | ||||||
|  |  | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "public_usable", | ||||||
|  |                 table: "pools"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -192,6 +192,10 @@ namespace DysonNetwork.Drive.Migrations | |||||||
|                         .HasColumnType("uuid") |                         .HasColumnType("uuid") | ||||||
|                         .HasColumnName("id"); |                         .HasColumnName("id"); | ||||||
|  |  | ||||||
|  |                     b.Property<Guid?>("AccountId") | ||||||
|  |                         .HasColumnType("uuid") | ||||||
|  |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|                     b.Property<bool>("AllowAnonymous") |                     b.Property<bool>("AllowAnonymous") | ||||||
|                         .HasColumnType("boolean") |                         .HasColumnType("boolean") | ||||||
|                         .HasColumnName("allow_anonymous"); |                         .HasColumnName("allow_anonymous"); | ||||||
| @@ -227,6 +231,14 @@ namespace DysonNetwork.Drive.Migrations | |||||||
|                         .HasColumnType("boolean") |                         .HasColumnType("boolean") | ||||||
|                         .HasColumnName("no_optimization"); |                         .HasColumnName("no_optimization"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("PublicIndexable") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("public_indexable"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("PublicUsable") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("public_usable"); | ||||||
|  |  | ||||||
|                     b.Property<int>("RequirePrivilege") |                     b.Property<int>("RequirePrivilege") | ||||||
|                         .HasColumnType("integer") |                         .HasColumnType("integer") | ||||||
|                         .HasColumnName("require_privilege"); |                         .HasColumnName("require_privilege"); | ||||||
|   | |||||||
| @@ -30,11 +30,15 @@ public class FilePool : ModelBase, IIdentifiedResource | |||||||
|     [MaxLength(1024)] public string Name { get; set; } = string.Empty; |     [MaxLength(1024)] public string Name { get; set; } = string.Empty; | ||||||
|     [Column(TypeName = "jsonb")] public RemoteStorageConfig StorageConfig { get; set; } = new(); |     [Column(TypeName = "jsonb")] public RemoteStorageConfig StorageConfig { get; set; } = new(); | ||||||
|     [Column(TypeName = "jsonb")] public BillingConfig BillingConfig { get; set; } = new(); |     [Column(TypeName = "jsonb")] public BillingConfig BillingConfig { get; set; } = new(); | ||||||
|  |     public bool PublicIndexable { get; set; } = false; | ||||||
|  |     public bool PublicUsable { get; set; } = false; | ||||||
|     public bool NoOptimization { get; set; } = false; |     public bool NoOptimization { get; set; } = false; | ||||||
|     public bool NoMetadata { get; set; } = false; |     public bool NoMetadata { get; set; } = false; | ||||||
|     public bool AllowEncryption { get; set; } = true; |     public bool AllowEncryption { get; set; } = true; | ||||||
|     public bool AllowAnonymous { get; set; } = true; |     public bool AllowAnonymous { get; set; } = true; | ||||||
|     public int RequirePrivilege { get; set; } = 0; |     public int RequirePrivilege { get; set; } = 0; | ||||||
|      |      | ||||||
|  |     public Guid? AccountId { get; set; } | ||||||
|  |  | ||||||
|     public string ResourceIdentifier => $"file-pool/{Id}"; |     public string ResourceIdentifier => $"file-pool/{Id}"; | ||||||
| } | } | ||||||
							
								
								
									
										25
									
								
								DysonNetwork.Drive/Storage/FilePoolController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								DysonNetwork.Drive/Storage/FilePoolController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | using DysonNetwork.Shared.Proto; | ||||||
|  | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Mvc; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Drive.Storage; | ||||||
|  |  | ||||||
|  | [ApiController] | ||||||
|  | [Route("/api/pools")] | ||||||
|  | public class FilePoolController(AppDatabase db) : ControllerBase | ||||||
|  | { | ||||||
|  |     [HttpGet] | ||||||
|  |     [Authorize] | ||||||
|  |     public async Task<ActionResult<List<FilePool>>> ListUsablePools() | ||||||
|  |     { | ||||||
|  |         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||||
|  |  | ||||||
|  |         var accountId = Guid.Parse(currentUser.Id); | ||||||
|  |         var pools = await db.Pools | ||||||
|  |             .Where(p => p.PublicUsable || p.AccountId == accountId) | ||||||
|  |             .ToListAsync(); | ||||||
|  |  | ||||||
|  |         return Ok(pools); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -126,7 +126,7 @@ public class FileService( | |||||||
|             contentType = "application/octet-stream"; |             contentType = "application/octet-stream"; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         var hash = await HashFileAsync(stream, fileSize: fileSize); |         var hash = await HashFileAsync(ogFilePath); | ||||||
|  |  | ||||||
|         var file = new CloudFile |         var file = new CloudFile | ||||||
|         { |         { | ||||||
| @@ -136,7 +136,7 @@ public class FileService( | |||||||
|             Size = fileSize, |             Size = fileSize, | ||||||
|             Hash = hash, |             Hash = hash, | ||||||
|             AccountId = Guid.Parse(account.Id), |             AccountId = Guid.Parse(account.Id), | ||||||
|             IsEncrypted = !string.IsNullOrWhiteSpace(encryptPassword) |             IsEncrypted = !string.IsNullOrWhiteSpace(encryptPassword) && pool.AllowEncryption | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         var existingFile = await db.Files.AsNoTracking().FirstOrDefaultAsync(f => f.Hash == hash); |         var existingFile = await db.Files.AsNoTracking().FirstOrDefaultAsync(f => f.Hash == hash); | ||||||
| @@ -274,7 +274,7 @@ public class FileService( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Handles file optimization (image compression, video thumbnailing) and uploads to remote storage in the background. |     /// Handles file optimization (image compression, video thumbnail) and uploads to remote storage in the background. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     private async Task ProcessAndUploadInBackgroundAsync( |     private async Task ProcessAndUploadInBackgroundAsync( | ||||||
|         string fileId, |         string fileId, | ||||||
| @@ -350,15 +350,23 @@ public class FileService( | |||||||
|                             var snapshotTime = mediaInfo.Duration > TimeSpan.FromSeconds(5) |                             var snapshotTime = mediaInfo.Duration > TimeSpan.FromSeconds(5) | ||||||
|                                 ? TimeSpan.FromSeconds(5) |                                 ? TimeSpan.FromSeconds(5) | ||||||
|                                 : TimeSpan.FromSeconds(1); |                                 : TimeSpan.FromSeconds(1); | ||||||
|  |  | ||||||
|                             await FFMpeg.SnapshotAsync(originalFilePath, thumbnailPath, captureTime: snapshotTime); |                             await FFMpeg.SnapshotAsync(originalFilePath, thumbnailPath, captureTime: snapshotTime); | ||||||
|  |  | ||||||
|  |                             if (File.Exists(thumbnailPath)) | ||||||
|  |                             { | ||||||
|                                 uploads.Add((thumbnailPath, ".thumbnail.webp", "image/webp", true)); |                                 uploads.Add((thumbnailPath, ".thumbnail.webp", "image/webp", true)); | ||||||
|                                 hasThumbnail = true; |                                 hasThumbnail = true; | ||||||
|                             } |                             } | ||||||
|  |                             else | ||||||
|  |                             { | ||||||
|  |                                 logger.LogWarning("FFMpeg did not produce thumbnail for video {FileId}", fileId); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|                         catch (Exception ex) |                         catch (Exception ex) | ||||||
|                         { |                         { | ||||||
|                             logger.LogError(ex, "Failed to generate thumbnail for video {FileId}", fileId); |                             logger.LogError(ex, "Failed to generate thumbnail for video {FileId}", fileId); | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         break; |                         break; | ||||||
|  |  | ||||||
|                     default: |                     default: | ||||||
| @@ -405,11 +413,12 @@ public class FileService( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static async Task<string> HashFileAsync(Stream stream, int chunkSize = 1024 * 1024, long? fileSize = null) |     private static async Task<string> HashFileAsync(string filePath, int chunkSize = 1024 * 1024) | ||||||
|     { |     { | ||||||
|         fileSize ??= stream.Length; |         using var stream = File.OpenRead(filePath); | ||||||
|  |         var fileSize = stream.Length; | ||||||
|         if (fileSize > chunkSize * 1024 * 5) |         if (fileSize > chunkSize * 1024 * 5) | ||||||
|             return await HashFastApproximateAsync(stream, chunkSize); |             return await HashFastApproximateAsync(filePath, chunkSize); | ||||||
|  |  | ||||||
|         using var md5 = MD5.Create(); |         using var md5 = MD5.Create(); | ||||||
|         var hashBytes = await md5.ComputeHashAsync(stream); |         var hashBytes = await md5.ComputeHashAsync(stream); | ||||||
| @@ -417,8 +426,10 @@ public class FileService( | |||||||
|         return Convert.ToHexString(hashBytes).ToLowerInvariant(); |         return Convert.ToHexString(hashBytes).ToLowerInvariant(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static async Task<string> HashFastApproximateAsync(Stream stream, int chunkSize = 1024 * 1024) |     private static async Task<string> HashFastApproximateAsync(string filePath, int chunkSize = 1024 * 1024) | ||||||
|     { |     { | ||||||
|  |         await using var stream = File.OpenRead(filePath); | ||||||
|  |          | ||||||
|         // Scale the chunk size to kB level |         // Scale the chunk size to kB level | ||||||
|         chunkSize *= 1024; |         chunkSize *= 1024; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								DysonNetwork.Drive/bun.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								DysonNetwork.Drive/bun.lock
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | { | ||||||
|  |   "lockfileVersion": 1, | ||||||
|  |   "workspaces": { | ||||||
|  |     "": { | ||||||
|  |       "dependencies": { | ||||||
|  |         "highlight.js": "^11.11.1", | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   "packages": { | ||||||
|  |     "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -13,6 +13,13 @@ public static class PageStartup | |||||||
| #pragma warning disable ASP0016 | #pragma warning disable ASP0016 | ||||||
|         app.MapFallback(async context => |         app.MapFallback(async context => | ||||||
|         { |         { | ||||||
|  |             if (context.Request.Path.StartsWithSegments("/api") || context.Request.Path.StartsWithSegments("/cgi")) | ||||||
|  |             { | ||||||
|  |                 context.Response.StatusCode = StatusCodes.Status404NotFound; | ||||||
|  |                 await context.Response.WriteAsync("Not found"); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |              | ||||||
|             var html = await File.ReadAllTextAsync(defaultFile); |             var html = await File.ReadAllTextAsync(defaultFile); | ||||||
|  |  | ||||||
|             using var scope = app.Services.CreateScope(); |             using var scope = app.Services.CreateScope(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user