✨ File management
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { NIcon, NLayout, NLayoutSider, NMenu, type MenuOption } from 'naive-ui'
|
||||||
import { h, type Component } from 'vue'
|
import { h, type Component } from 'vue'
|
||||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
@@ -34,6 +34,11 @@ const menuOptions: MenuOption[] = [
|
|||||||
key: 'dashboardUsage',
|
key: 'dashboardUsage',
|
||||||
icon: renderIcon(DataUsageRound),
|
icon: renderIcon(DataUsageRound),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Files',
|
||||||
|
key: 'dashboardFiles',
|
||||||
|
icon: renderIcon(AllInboxFilled),
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
function updateMenuSelect(key: string) {
|
function updateMenuSelect(key: string) {
|
||||||
|
@@ -2,7 +2,15 @@
|
|||||||
import LayoutDefault from './layouts/default.vue'
|
import LayoutDefault from './layouts/default.vue'
|
||||||
|
|
||||||
import { RouterView } from 'vue-router'
|
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 { usePreferredDark } from '@vueuse/core'
|
||||||
import { useUserStore } from './stores/user'
|
import { useUserStore } from './stores/user'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
@@ -34,10 +42,14 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme">
|
<n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme">
|
||||||
<n-global-style />
|
<n-global-style />
|
||||||
<n-message-provider placement="bottom">
|
<n-loading-bar-provider>
|
||||||
<layout-default>
|
<n-dialog-provider>
|
||||||
<router-view />
|
<n-message-provider placement="bottom">
|
||||||
</layout-default>
|
<layout-default>
|
||||||
</n-message-provider>
|
<router-view />
|
||||||
|
</layout-default>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-loading-bar-provider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -27,6 +27,12 @@ const router = createRouter({
|
|||||||
component: () => import('../views/dashboard/usage.vue'),
|
component: () => import('../views/dashboard/usage.vue'),
|
||||||
meta: { requiresAuth: true },
|
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()
|
const servicesStore = useServicesStore()
|
||||||
|
|
||||||
// Initialize user state if not already initialized
|
// Initialize user state if not already initialized
|
||||||
if (!userStore.user && localStorage.getItem('authToken')) {
|
if (!userStore.user) {
|
||||||
await userStore.fetchUser()
|
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,13 +47,15 @@ public class FileController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pool = await fs.GetPoolAsync(file.PoolId.Value);
|
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;
|
var dest = pool.StorageConfig;
|
||||||
|
|
||||||
if (!pool.PolicyConfig.AllowAnonymous)
|
if (!pool.PolicyConfig.AllowAnonymous)
|
||||||
if(HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||||
// TODO: Provide ability to add access log
|
return Unauthorized();
|
||||||
|
// TODO: Provide ability to add access log
|
||||||
|
|
||||||
var fileName = string.IsNullOrWhiteSpace(file.StorageId) ? file.Id : file.StorageId;
|
var fileName = string.IsNullOrWhiteSpace(file.StorageId) ? file.Id : file.StorageId;
|
||||||
|
|
||||||
if (!original && file.HasCompression)
|
if (!original && file.HasCompression)
|
||||||
@@ -129,6 +131,34 @@ public class FileController(
|
|||||||
return file;
|
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]
|
[Authorize]
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
public async Task<ActionResult> DeleteFile(string id)
|
public async Task<ActionResult> DeleteFile(string id)
|
||||||
|
@@ -26,7 +26,7 @@ public class FileService(
|
|||||||
{
|
{
|
||||||
private const string CacheKeyPrefix = "file:";
|
private const string CacheKeyPrefix = "file:";
|
||||||
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api for getting file meta with cache,
|
/// The api for getting file meta with cache,
|
||||||
/// the best use case is for accessing the file data.
|
/// the best use case is for accessing the file data.
|
||||||
|
Reference in New Issue
Block a user