✨ FIle detail page
This commit is contained in:
3
DysonNetwork.Drive/.gitignore
vendored
3
DysonNetwork.Drive/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/Uploads/
|
/Uploads/
|
||||||
/wwwroot/dist
|
/Client/node_modules/
|
||||||
|
/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
|
||||||
|
@@ -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,182 @@
|
|||||||
<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>
|
<div v-if="fileInfo.is_encrypted">
|
||||||
<div v-else>
|
<n-alert type="info" size="small" title="Encrypted file">
|
||||||
<n-progress :percentage="progress" />
|
The file has been encrypted. Preview not available. Please enter the password to
|
||||||
</div>
|
download it.
|
||||||
|
</n-alert>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<n-image v-if="fileType === 'image'" :src="fileSource" />
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
<n-button @click="downloadFile">Download</n-button>
|
||||||
|
</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,
|
||||||
|
useMessage,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import { DataUsageRound, InfoRound, DetailsRound, FileUploadOutlined } 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 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) {
|
||||||
progress.value = p * 100
|
messageDisplay.error('Please enter the password to download the file.')
|
||||||
}).catch((err) => {
|
return
|
||||||
messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 })
|
}
|
||||||
})
|
if (fileInfo.value.is_encrypted) {
|
||||||
|
downloadAndDecryptFile(fileSource.value, filePass.value, (p: number) => {
|
||||||
|
progress.value = p * 100
|
||||||
|
}).catch((err) => {
|
||||||
|
messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 })
|
||||||
|
})
|
||||||
|
} 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,17 +22,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="mb-3" v-if="modeAdvanced">
|
<div class="mb-3">
|
||||||
<n-input
|
<n-select
|
||||||
v-model:value="filePass"
|
v-model:value="filePool"
|
||||||
placeholder="Enter password to protect the file"
|
:options="pools ?? []"
|
||||||
|
:render-label="renderPoolSelectLabel"
|
||||||
|
:render-tag="renderSingleSelectTag"
|
||||||
|
value-field="id"
|
||||||
|
label-field="name"
|
||||||
|
placeholder="Select a file pool to upload"
|
||||||
clearable
|
clearable
|
||||||
size="large"
|
size="large"
|
||||||
type="password"
|
|
||||||
class="mb-2"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
||||||
|
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>
|
||||||
|
</n-card>
|
||||||
|
</n-collapse-transition>
|
||||||
|
|
||||||
<n-upload
|
<n-upload
|
||||||
multiple
|
multiple
|
||||||
directory-dnd
|
directory-dnd
|
||||||
@@ -45,7 +66,7 @@
|
|||||||
<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 +99,131 @@ import {
|
|||||||
NP,
|
NP,
|
||||||
NInput,
|
NInput,
|
||||||
NSwitch,
|
NSwitch,
|
||||||
|
NSelect,
|
||||||
|
NTag,
|
||||||
|
NCollapseTransition,
|
||||||
|
NFormItem,
|
||||||
type UploadCustomRequestOptions,
|
type UploadCustomRequestOptions,
|
||||||
type UploadSettledFileInfo,
|
type UploadSettledFileInfo,
|
||||||
|
type SelectOption,
|
||||||
|
type SelectRenderTag,
|
||||||
} 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 +234,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 +245,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,7 +276,10 @@ 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
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
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=="],
|
||||||
|
}
|
||||||
|
}
|
5
DysonNetwork.Drive/package.json
Normal file
5
DysonNetwork.Drive/package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"highlight.js": "^11.11.1"
|
||||||
|
}
|
||||||
|
}
|
@@ -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