✨ File management
This commit is contained in:
@@ -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) {
|
||||
|
@@ -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-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>
|
||||
|
@@ -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()
|
||||
}
|
||||
|
||||
|
184
DysonNetwork.Drive/Client/src/views/dashboard/files.vue
Normal file
184
DysonNetwork.Drive/Client/src/views/dashboard/files.vue
Normal 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>
|
@@ -47,11 +47,13 @@ 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();
|
||||
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)
|
||||
|
Reference in New Issue
Block a user