File management

This commit is contained in:
2025-07-27 00:29:59 +08:00
parent 02af78ca99
commit 46612b28aa
6 changed files with 250 additions and 13 deletions

View File

@@ -16,7 +16,7 @@
</template>
<script setup lang="ts">
import { DataUsageRound } from '@vicons/material'
import { DataUsageRound, AllInboxFilled } from '@vicons/material'
import { NIcon, NLayout, NLayoutSider, NMenu, type MenuOption } from 'naive-ui'
import { h, type Component } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
@@ -34,6 +34,11 @@ const menuOptions: MenuOption[] = [
key: 'dashboardUsage',
icon: renderIcon(DataUsageRound),
},
{
label: 'Files',
key: 'dashboardFiles',
icon: renderIcon(AllInboxFilled),
}
]
function updateMenuSelect(key: string) {

View File

@@ -2,7 +2,15 @@
import LayoutDefault from './layouts/default.vue'
import { RouterView } from 'vue-router'
import { NGlobalStyle, NConfigProvider, NMessageProvider, lightTheme, darkTheme } from 'naive-ui'
import {
NGlobalStyle,
NConfigProvider,
NMessageProvider,
NDialogProvider,
NLoadingBarProvider,
lightTheme,
darkTheme,
} from 'naive-ui'
import { usePreferredDark } from '@vueuse/core'
import { useUserStore } from './stores/user'
import { onMounted } from 'vue'
@@ -34,10 +42,14 @@ onMounted(() => {
<template>
<n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme">
<n-global-style />
<n-message-provider placement="bottom">
<layout-default>
<router-view />
</layout-default>
</n-message-provider>
<n-loading-bar-provider>
<n-dialog-provider>
<n-message-provider placement="bottom">
<layout-default>
<router-view />
</layout-default>
</n-message-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</n-config-provider>
</template>

View File

@@ -27,6 +27,12 @@ const router = createRouter({
component: () => import('../views/dashboard/usage.vue'),
meta: { requiresAuth: true },
},
{
path: 'files',
name: 'dashboardFiles',
component: () => import('../views/dashboard/files.vue'),
meta: { requiresAuth: true },
}
],
},
{
@@ -42,7 +48,7 @@ router.beforeEach(async (to, from, next) => {
const servicesStore = useServicesStore()
// Initialize user state if not already initialized
if (!userStore.user && localStorage.getItem('authToken')) {
if (!userStore.user) {
await userStore.fetchUser()
}

View File

@@ -0,0 +1,184 @@
<template>
<section class="h-full px-5 py-4">
<n-data-table :columns="tableColumns" :data="files" :pagination="tablePagination" />
</section>
</template>
<script lang="ts" setup>
import {
NDataTable,
NIcon,
NImage,
NButton,
NSpace,
type DataTableColumns,
type PaginationProps,
useDialog,
useMessage,
useLoadingBar,
} from 'naive-ui'
import {
AudioFileRound,
InsertDriveFileRound,
VideoFileRound,
FileDownloadOutlined,
DeleteRound,
} from '@vicons/material'
import { h, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { formatBytes } from '../format'
const router = useRouter()
const files = ref<any[]>([])
const tableColumns: DataTableColumns<any> = [
{
title: 'Preview',
key: 'preview',
render(row: any) {
switch (row.mime_type.split('/')[0]) {
case 'image':
return h(NImage, {
src: '/api/files/' + row.id,
width: 32,
height: 32,
objectFit: 'contain',
style: { aspectRatio: 1 },
})
case 'video':
return h(NIcon, { size: 32 }, { default: () => h(VideoFileRound) })
case 'audio':
return h(NIcon, { size: 32 }, { default: () => h(AudioFileRound) })
default:
return h(NIcon, { size: 32 }, { default: () => h(InsertDriveFileRound) })
}
},
},
{
title: 'Name',
key: 'name',
render(row: any) {
return h(
NButton,
{
text: true,
onClick: () => {
router.push(`/files/${row.id}`)
},
},
{
default: () => row.name,
},
)
},
},
{
title: 'Size',
key: 'size',
render(row: any) {
return formatBytes(row.size)
},
},
{
title: 'Uploaded At',
key: 'created_at',
render(row: any) {
return new Date(row.created_at).toLocaleString()
},
},
{
title: 'Action',
key: 'action',
render(row: any) {
return h(NSpace, {}, [
h(
NButton,
{
quaternary: true,
circle: true,
text: true,
onClick: () => {
window.open(`/api/files/${row.id}`, '_blank')
},
},
{
icon: () => h(NIcon, {}, { default: () => h(FileDownloadOutlined) }),
},
),
h(
NButton,
{
quaternary: true,
circle: true,
text: true,
type: 'error',
onClick: () => {
askDeleteFile(row)
},
},
{
icon: () => h(NIcon, {}, { default: () => h(DeleteRound) }),
},
),
])
},
},
]
const tablePagination = ref<PaginationProps>({
page: 1,
itemCount: 0,
})
async function fetchFiles() {
try {
const response = await fetch('/api/files/me')
if (!response.ok) {
throw new Error('Network response was not ok')
}
const data = await response.json()
files.value = data
tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0')
} catch (error) {
console.error('Failed to fetch files:', error)
}
}
onMounted(() => fetchFiles())
const dialog = useDialog()
const messageDialog = useMessage()
const loadingBar = useLoadingBar()
async function askDeleteFile(file: any) {
dialog.warning({
title: 'Confirm',
content: `Are you sure you want delete ${file.name}?`,
positiveText: 'Sure',
negativeText: 'Not Sure',
draggable: true,
onPositiveClick: () => {
deleteFile(file)
},
})
}
async function deleteFile(file: any) {
try {
loadingBar.start()
const response = await fetch(`/api/files/${file.id}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error('Network response was not ok')
}
tablePagination.value.page = 1
await fetchFiles()
loadingBar.finish()
messageDialog.success('File deleted successfully')
} catch (error) {
loadingBar.error()
messageDialog.error('Failed to delete file: ' + (error as Error).message)
}
}
</script>

View File

@@ -47,12 +47,14 @@ public class FileController(
}
var pool = await fs.GetPoolAsync(file.PoolId.Value);
if (pool is null) return StatusCode(StatusCodes.Status410Gone, "The pool of the file no longer exists or not accessible.");
if (pool is null)
return StatusCode(StatusCodes.Status410Gone, "The pool of the file no longer exists or not accessible.");
var dest = pool.StorageConfig;
if (!pool.PolicyConfig.AllowAnonymous)
if(HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
// TODO: Provide ability to add access log
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
// TODO: Provide ability to add access log
var fileName = string.IsNullOrWhiteSpace(file.StorageId) ? file.Id : file.StorageId;
@@ -129,6 +131,34 @@ public class FileController(
return file;
}
[Authorize]
[HttpGet("me")]
public async Task<ActionResult<List<CloudFile>>> GetMyFiles(
[FromQuery] Guid? pool,
[FromQuery] int offset = 0,
[FromQuery] int take = 20
)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var query = db.Files
.Where(e => e.AccountId == accountId)
.Include(e => e.Pool)
.Skip(offset)
.Take(take);
if (pool.HasValue) query = query.Where(e => e.PoolId == pool);
var files = await query.ToListAsync();
var total = await query.CountAsync();
Response.Headers.Append("X-Total", total.ToString());
return Ok(files);
}
[Authorize]
[HttpDelete("{id}")]
public async Task<ActionResult> DeleteFile(string id)