FIle detail page

This commit is contained in:
2025-07-26 14:32:37 +08:00
parent f1867e7916
commit 186e9c00aa
14 changed files with 749 additions and 33 deletions

View File

@@ -1,2 +1,3 @@
/Uploads/ /Uploads/
/wwwroot/dist /Client/node_modules/
/wwwroot/dist

View File

@@ -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

View File

@@ -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'),
} }

View 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;
}

View File

@@ -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>

View File

@@ -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
} }

View 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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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");

View File

@@ -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}";
} }

View 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);
}
}

View 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=="],
}
}

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"highlight.js": "^11.11.1"
}
}

View File

@@ -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();