Merge branch 'development'
This commit is contained in:
commit
2f8221e894
8 changed files with 187 additions and 65 deletions
|
|
@ -1,19 +1,19 @@
|
|||
<script lang="ts" setup>
|
||||
import type { QTableProps } from 'quasar'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import api from '@/services/HttpService'
|
||||
|
||||
import FileIcon from '@/components/FileIcon.vue'
|
||||
import DialogDelete from '@/components/DialogDelete.vue'
|
||||
import FileFormWrapper from './FileFormWrapper.vue'
|
||||
import FolderFormWrapper from './FolderFormWrapper.vue'
|
||||
|
||||
import { useFileInfoStore } from '@/stores/file-info-data'
|
||||
import useStorage from '@/stores/storage'
|
||||
|
||||
const storageStore = useStorage()
|
||||
const { folder, file, currentInfo } = storeToRefs(storageStore)
|
||||
const { goto, deleteFolder, deleteFile } = storageStore
|
||||
const { goto, deleteFolder, deleteFile, constructUrl } = storageStore
|
||||
|
||||
const { getFormatDate, getSize, getType, getFileInfo } = useFileInfoStore()
|
||||
|
||||
|
|
@ -33,7 +33,14 @@ const props = defineProps<{
|
|||
mode: 'admin' | 'user'
|
||||
}>()
|
||||
|
||||
interface ApiResponse {
|
||||
size: string
|
||||
}
|
||||
|
||||
const deleteState = ref<boolean>(false)
|
||||
const open = ref<boolean>(false)
|
||||
const sizefolder = ref<string>()
|
||||
|
||||
const deletePath = ref<string>('')
|
||||
const deleteTarget = ref<'deleteFolder' | 'deleteFile'>()
|
||||
const deleteMap = { deleteFolder, deleteFile }
|
||||
|
|
@ -50,6 +57,37 @@ function triggerFileDelete(pathname: string) {
|
|||
deleteState.value = !deleteState.value
|
||||
}
|
||||
|
||||
function getSizeFolder(constructFolder: string) {
|
||||
const src = constructFolder.split('/').filter(Boolean)
|
||||
const path = src.join('/') + '/'
|
||||
const res = async () => {
|
||||
const response = await api.get(constructUrl(src, false) + '/size')
|
||||
sizefolder.value = response.data.size
|
||||
}
|
||||
|
||||
if (folder.value[currentInfo.value.path]) {
|
||||
const idx = folder.value[currentInfo.value.path].findIndex(
|
||||
(v) => v.pathname === path,
|
||||
)
|
||||
|
||||
if (idx !== -1) {
|
||||
res()
|
||||
if (folder.value[currentInfo.value.path][idx].folderSize) {
|
||||
if (
|
||||
folder.value[currentInfo.value.path][idx].folderSize !=
|
||||
sizefolder.value
|
||||
) {
|
||||
folder.value[currentInfo.value.path][idx].folderSize =
|
||||
sizefolder.value
|
||||
}
|
||||
} else {
|
||||
folder.value[currentInfo.value.path][idx].folderSize = sizefolder.value
|
||||
}
|
||||
}
|
||||
return getSize(folder.value[currentInfo.value.path][idx].folderSize)
|
||||
}
|
||||
}
|
||||
|
||||
const colFolder = [
|
||||
{
|
||||
name: 'name',
|
||||
|
|
@ -186,18 +224,23 @@ const onRowClick = ((_, row) => {
|
|||
<q-td class="justify-center">
|
||||
<div>
|
||||
<q-icon
|
||||
@click.stop
|
||||
@click.stop="
|
||||
() => {
|
||||
open = !open
|
||||
}
|
||||
"
|
||||
class="q-ma-sm"
|
||||
name="o_info"
|
||||
size="2em"
|
||||
color="primary"
|
||||
/>
|
||||
<q-tooltip
|
||||
:delay="1000"
|
||||
anchor="center left"
|
||||
self="center right"
|
||||
:offset="[5, 1]"
|
||||
>
|
||||
{{ data.row.name }}
|
||||
{{ getSizeFolder(data.row.pathname) }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<div v-if="props.mode === 'admin'">
|
||||
|
|
@ -312,7 +355,6 @@ const onRowClick = ((_, row) => {
|
|||
fileFormComponent?.triggerFileEdit(
|
||||
data.row,
|
||||
data.row.pathname,
|
||||
data.row.fileName,
|
||||
)
|
||||
"
|
||||
id="listViewFileEdit"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type Path = string
|
|||
export interface StorageFolder {
|
||||
pathname: string
|
||||
name: string
|
||||
folderSize?: string
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
|
@ -58,6 +59,7 @@ function constructUrl(path: string | string[], append = true) {
|
|||
return a
|
||||
}
|
||||
}, '')
|
||||
|
||||
return append
|
||||
? url + ['cabinet', '/drawer', '/folder', '/subfolder'][arr.length]
|
||||
: url
|
||||
|
|
@ -137,7 +139,13 @@ const useStorage = defineStore('storageStore', () => {
|
|||
|
||||
if (arr.length >= 4) return // this system does not have more than 4 level
|
||||
|
||||
const res = await api.get<(typeof folder.value)[string]>(constructUrl(arr))
|
||||
const res = await api.post<(typeof folder.value)[string]>(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}storage/list`,
|
||||
{
|
||||
operation: 'folder',
|
||||
path: arr,
|
||||
},
|
||||
)
|
||||
if (res.status === 200 && res.data && Array.isArray(res.data))
|
||||
folder.value[consistantPath(path)] = res.data.sort((a, b) =>
|
||||
a.pathname.localeCompare(b.pathname),
|
||||
|
|
@ -149,8 +157,12 @@ const useStorage = defineStore('storageStore', () => {
|
|||
|
||||
if (arr.length < 3) return // file in this system only lives in level 3 and 4
|
||||
|
||||
const res = await api.get<(typeof file.value)[string]>(
|
||||
constructUrl(arr, false) + '/file',
|
||||
const res = await api.post<(typeof file.value)[string]>(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}storage/list`,
|
||||
{
|
||||
operation: 'file',
|
||||
path: arr,
|
||||
},
|
||||
)
|
||||
if (res.status === 200 && res.data && Array.isArray(res.data))
|
||||
file.value[consistantPath(path)] = res.data.sort((a, b) =>
|
||||
|
|
@ -192,7 +204,7 @@ const useStorage = defineStore('storageStore', () => {
|
|||
socket.on('connect', () => console.info('Socket.io connected.'))
|
||||
socket.on('disconnect', () => console.info('Socket.io disconnected.'))
|
||||
socket.on(
|
||||
'CreateFolder',
|
||||
'FolderCreate',
|
||||
(data: {
|
||||
pathname: string
|
||||
name: string
|
||||
|
|
@ -222,7 +234,7 @@ const useStorage = defineStore('storageStore', () => {
|
|||
// NOTE:
|
||||
// API planned to make new endpoint that can move and rename in one go.
|
||||
// Need to change if api handle move and rename file instead of just edit.
|
||||
socket.on('EditFolder', (data: { from: string; to: string }) => {
|
||||
socket.on('FolderMove', (data: { from: string; to: string }) => {
|
||||
const src = data.from.split('/').filter(Boolean)
|
||||
const dst = data.to.split('/').filter(Boolean)
|
||||
const path = consistantPath(src.slice(0, -1))
|
||||
|
|
@ -259,7 +271,7 @@ const useStorage = defineStore('storageStore', () => {
|
|||
}
|
||||
}
|
||||
})
|
||||
socket.on('DeleteFolder', (data: { pathname: string }) => {
|
||||
socket.on('FolderDelete', (data: { pathname: string }) => {
|
||||
for (let key in folder.value) {
|
||||
if (key.startsWith(data.pathname)) {
|
||||
delete folder.value[key]
|
||||
|
|
@ -315,20 +327,17 @@ const useStorage = defineStore('storageStore', () => {
|
|||
)
|
||||
}
|
||||
})
|
||||
socket.on(
|
||||
'FileUpdateMove',
|
||||
(data: { from: StorageFile; to: StorageFile }) => {
|
||||
const arr = data.from.pathname.split('/').filter(Boolean)
|
||||
const path = consistantPath(arr.slice(0, -1))
|
||||
socket.on('FileMove', (data: { from: StorageFile; to: StorageFile }) => {
|
||||
const arr = data.from.pathname.split('/').filter(Boolean)
|
||||
const path = consistantPath(arr.slice(0, -1))
|
||||
|
||||
if (file.value[path]) {
|
||||
const idx = file.value[path].findIndex(
|
||||
(v) => v.pathname === data.from.pathname,
|
||||
)
|
||||
if (idx !== -1) file.value[path][idx] = data.to
|
||||
}
|
||||
},
|
||||
)
|
||||
if (file.value[path]) {
|
||||
const idx = file.value[path].findIndex(
|
||||
(v) => v.pathname === data.from.pathname,
|
||||
)
|
||||
if (idx !== -1) file.value[path][idx] = data.to
|
||||
}
|
||||
})
|
||||
socket.on('FileUpdate', (data: StorageFile) => {
|
||||
const arr = data.pathname.split('/').filter(Boolean)
|
||||
const path = consistantPath(arr.slice(0, -1))
|
||||
|
|
@ -367,42 +376,67 @@ const useStorage = defineStore('storageStore', () => {
|
|||
msg: `พบชื่อ \"${name}\" ซ้ำในระบบ`,
|
||||
})
|
||||
} else {
|
||||
await api.post(constructUrl(path, true), { name })
|
||||
const arrayPath: string[] = path.split('/').filter(Boolean)
|
||||
await api.post(`${import.meta.env.VITE_API_ENDPOINT}storage/folder`, {
|
||||
path: arrayPath,
|
||||
name: name,
|
||||
})
|
||||
}
|
||||
loader.hide()
|
||||
}
|
||||
async function editFolder(name: string, path: string) {
|
||||
loader.show()
|
||||
await api.put(constructUrl(path, false), { name })
|
||||
const arrayPath: string[] = path.split('/').filter(Boolean)
|
||||
const beforeName = arrayPath.pop()
|
||||
|
||||
await api.put(`${import.meta.env.VITE_API_ENDPOINT}storage/folder`, {
|
||||
from: {
|
||||
name: beforeName,
|
||||
path: arrayPath,
|
||||
},
|
||||
to: {
|
||||
name: name,
|
||||
path: arrayPath,
|
||||
},
|
||||
})
|
||||
loader.hide()
|
||||
}
|
||||
async function deleteFolder(path: string) {
|
||||
loader.show()
|
||||
await api.delete(constructUrl(path, false))
|
||||
await api.delete<(typeof file.value)[string]>(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}storage/folder`,
|
||||
{ data: { path: path.split('/').filter(Boolean) } },
|
||||
)
|
||||
loader.hide()
|
||||
}
|
||||
|
||||
type FileMetadata = {
|
||||
title?: string
|
||||
description?: string
|
||||
keyword?: string[]
|
||||
category?: string[]
|
||||
keyword?: string[]
|
||||
}
|
||||
async function createFile(
|
||||
file: File,
|
||||
data: FileMetadata,
|
||||
path: string = currentInfo.path,
|
||||
) {
|
||||
if (path.split('/').filter(Boolean).length < 3) return // the system only allow file to live in level 3 and 4
|
||||
const arr = path.split('/').filter(Boolean)
|
||||
|
||||
if (arr.length < 3) return // the system only allow file to live in level 3 and 4
|
||||
|
||||
loader.show()
|
||||
const res = await api.post(constructUrl(path, false) + '/file', {
|
||||
file: file.name,
|
||||
...data,
|
||||
})
|
||||
if (res && res.status === 201 && res.data && res.data.upload) {
|
||||
const res = await api.post(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}storage/file`,
|
||||
{
|
||||
path: arr,
|
||||
file: file.name,
|
||||
...data,
|
||||
},
|
||||
)
|
||||
if (res && res.status === 200 && res.data && res.data.uploadUrl) {
|
||||
await axios
|
||||
.put(res.data.upload, file, {
|
||||
.put(res.data.uploadUrl, file, {
|
||||
headers: { 'Content-Type': file.type },
|
||||
onUploadProgress: (e) => console.log(e),
|
||||
})
|
||||
|
|
@ -416,13 +450,29 @@ const useStorage = defineStore('storageStore', () => {
|
|||
if (arr.length < 4) return // the system only allow file to live in level 3 and 4
|
||||
|
||||
loader.show()
|
||||
const res = await api.patch(
|
||||
constructUrl(arr.slice(0, -1), false) + `/file/${arr[arr.length - 1]}`,
|
||||
{ file: file?.name, ...data },
|
||||
|
||||
const srcFile = arr.pop()
|
||||
|
||||
const res = await api.put(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}storage/file`,
|
||||
{
|
||||
...data,
|
||||
from: {
|
||||
file: srcFile,
|
||||
path: arr,
|
||||
},
|
||||
to: file?.name
|
||||
? {
|
||||
file: file.name,
|
||||
path: arr,
|
||||
}
|
||||
: undefined,
|
||||
upload: !!file,
|
||||
},
|
||||
)
|
||||
if (res && res.status === 200 && res.data && res.data.upload) {
|
||||
if (res && res.status === 200 && res.data && res.data.uploadUrl) {
|
||||
await axios
|
||||
.put(res.data.upload, file, {
|
||||
.put(res.data.uploadUrl, file, {
|
||||
headers: { 'Content-Type': file?.type },
|
||||
onUploadProgress: (e) => console.log(e),
|
||||
})
|
||||
|
|
@ -436,9 +486,10 @@ const useStorage = defineStore('storageStore', () => {
|
|||
if (arr.length < 4) return // the system only allow file to live in level 3 and 4
|
||||
|
||||
loader.show()
|
||||
await api.delete(
|
||||
constructUrl(arr.slice(0, -1), false) + `/file/${arr[arr.length - 1]}`,
|
||||
)
|
||||
|
||||
await api.delete(`${import.meta.env.VITE_API_ENDPOINT}storage/file`, {
|
||||
data: { path: arr.slice(0, -1), file: arr[arr.length - 1] },
|
||||
})
|
||||
loader.hide()
|
||||
}
|
||||
|
||||
|
|
@ -461,6 +512,8 @@ const useStorage = defineStore('storageStore', () => {
|
|||
createFile,
|
||||
updateFile,
|
||||
deleteFile,
|
||||
constructUrl,
|
||||
consistantPath,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -49,4 +49,4 @@ server.listen(PORT, "0.0.0.0", () =>
|
|||
console.log(`[APP] Application is running on http://localhost:${PORT}`),
|
||||
);
|
||||
|
||||
rabbitmq.init(amqHandler).catch((e) => console.error(e));
|
||||
// rabbitmq.init(amqHandler).catch((e) => console.error(e));
|
||||
|
|
|
|||
|
|
@ -15,12 +15,18 @@ export class SearchController extends Controller {
|
|||
@Security("bearerAuth")
|
||||
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
|
||||
public async searchFile(@Body() search: Search): Promise<StorageFile[]> {
|
||||
const type = ["match", "match_phrase"] as const;
|
||||
|
||||
const result = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
|
||||
index: DEFAULT_INDEX,
|
||||
query: {
|
||||
bool: {
|
||||
must: search.AND?.map((v) => ({ match: { [v.field]: v.value } })),
|
||||
should: search.OR?.map((v) => ({ match: { [v.field]: v.value } })),
|
||||
must: search.AND?.map((v) => ({
|
||||
[type[search.exact || v.exact ? 1 : 0]]: { [v.field]: v.value },
|
||||
})),
|
||||
should: search.OR?.map((v) => ({
|
||||
[type[search.exact || v.exact ? 1 : 0]]: { [v.field]: v.value },
|
||||
})),
|
||||
},
|
||||
},
|
||||
size: 10000,
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ export class StorageController extends Controller {
|
|||
@Put("file")
|
||||
@Tags("Storage File")
|
||||
@Security("bearerAuth", ["management-role", "admin"])
|
||||
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
|
||||
public async moveFile(
|
||||
@Request() request: { user: { preferred_username: string } },
|
||||
@Body() body: PutFileBody,
|
||||
|
|
@ -494,17 +494,16 @@ export class StorageController extends Controller {
|
|||
});
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์ดังกล่าว");
|
||||
}
|
||||
if (body.to && !(await checkPathExist(DEFAULT_BUCKET, body.to.path.join("/")))) {
|
||||
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย");
|
||||
}
|
||||
if (
|
||||
body.to &&
|
||||
(await checkFileExist(DEFAULT_BUCKET, body.to.path.join("/") + `/${body.to.file}`))
|
||||
) {
|
||||
throw new HttpError(
|
||||
HttpStatusCode.PRECONDITION_FAILED,
|
||||
"พบไฟล์ในต้ำแหน่งปลายทาง ไม่สามารถย้ายได้",
|
||||
);
|
||||
if (body.to && JSON.stringify(body.from) !== JSON.stringify(body.to)) {
|
||||
if (!(await checkPathExist(DEFAULT_BUCKET, body.to.path.join("/")))) {
|
||||
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย");
|
||||
}
|
||||
if (await checkFileExist(DEFAULT_BUCKET, body.to.path.join("/") + `/${body.to.file}`)) {
|
||||
throw new HttpError(
|
||||
HttpStatusCode.PRECONDITION_FAILED,
|
||||
"พบไฟล์ในต้ำแหน่งปลายทาง ไม่สามารถย้ายได้",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!search.hits.hits[0]._source) {
|
||||
// This should not possible.
|
||||
|
|
@ -526,7 +525,7 @@ export class StorageController extends Controller {
|
|||
updatedBy: request.user.preferred_username,
|
||||
};
|
||||
|
||||
if (from && to) {
|
||||
if (from && to && JSON.stringify(from) !== JSON.stringify(to)) {
|
||||
const src = [DEFAULT_BUCKET, ...from.path, ""].join("/") + from.file;
|
||||
const dst = to.path.join("/") + `/${to.file}`;
|
||||
|
||||
|
|
@ -544,11 +543,15 @@ export class StorageController extends Controller {
|
|||
...metadata,
|
||||
path: to.path.join("/") + "/",
|
||||
pathname: dst,
|
||||
fileName: to.file,
|
||||
...dateMeta,
|
||||
},
|
||||
refresh: "wait_for",
|
||||
})
|
||||
.then(async () => await minioClient.removeObject(DEFAULT_INDEX, src))
|
||||
.then(
|
||||
async () =>
|
||||
await minioClient.removeObject(DEFAULT_BUCKET, from.path.join("/") + `/${from.file}`),
|
||||
)
|
||||
.catch((e) => console.error(`ElasticSearch Error: ${e}`));
|
||||
|
||||
io.getInstance()?.emit("FileMove", {
|
||||
|
|
@ -558,6 +561,7 @@ export class StorageController extends Controller {
|
|||
...metadata,
|
||||
path: to.path.join("/") + "/",
|
||||
pathname: dst,
|
||||
fileName: to.file,
|
||||
...dateMeta,
|
||||
},
|
||||
});
|
||||
|
|
@ -566,6 +570,7 @@ export class StorageController extends Controller {
|
|||
const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, dst);
|
||||
return { uploadUrl: presignedUrl };
|
||||
}
|
||||
return this.setStatus(HttpStatusCode.NO_CONTENT);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ export interface Search {
|
|||
AND?: {
|
||||
field: string;
|
||||
value: string;
|
||||
exact?: boolean;
|
||||
}[];
|
||||
OR?: {
|
||||
field: string;
|
||||
value: string;
|
||||
exact?: boolean;
|
||||
}[];
|
||||
exact: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,8 +61,9 @@ const models: TsoaRoute.Models = {
|
|||
"Search": {
|
||||
"dataType": "refObject",
|
||||
"properties": {
|
||||
"AND": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"value":{"dataType":"string","required":true},"field":{"dataType":"string","required":true}}}},
|
||||
"OR": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"value":{"dataType":"string","required":true},"field":{"dataType":"string","required":true}}}},
|
||||
"AND": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"exact":{"dataType":"boolean"},"value":{"dataType":"string","required":true},"field":{"dataType":"string","required":true}}}},
|
||||
"OR": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"exact":{"dataType":"boolean"},"value":{"dataType":"string","required":true},"field":{"dataType":"string","required":true}}}},
|
||||
"exact": {"dataType":"boolean","required":true},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
|
|
@ -891,7 +892,7 @@ export function RegisterRoutes(app: Router) {
|
|||
|
||||
|
||||
const promise = controller.moveFile.apply(controller, validatedArgs as any);
|
||||
promiseHandler(controller, promise, response, 204, next);
|
||||
promiseHandler(controller, promise, response, 200, next);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@
|
|||
"AND": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"exact": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -156,6 +159,9 @@
|
|||
"OR": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"exact": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -170,8 +176,14 @@
|
|||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"exact": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"exact"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
|
@ -2298,7 +2310,7 @@
|
|||
"put": {
|
||||
"operationId": "MoveFile",
|
||||
"responses": {
|
||||
"204": {
|
||||
"200": {
|
||||
"description": "สำเร็จ",
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue