hrms-edm/Services/client/src/stores/storage.ts
2023-12-12 16:01:46 +07:00

457 lines
13 KiB
TypeScript

import { computed, reactive, ref } from 'vue'
import { defineStore } from 'pinia'
import { io } from 'socket.io-client'
import axios from 'axios'
import { useErrorStore } from '@/stores/error'
import api from '@/services/HttpService'
import { useLoader } from './loader'
type Path = string
export interface StorageFolder {
pathname: string
name: string
createdAt: string
createdBy: string
}
export interface StorageFile {
pathname: string
path: string
fileName: string
fileSize: string
fileType: string
title: string
description: string
category: string[]
keyword: string[]
updatedAt: string
updatedBy: string
createdAt: string
createdBy: string
}
export interface Structure extends StorageFolder {
folder: Structure[]
file: StorageFile[]
}
type Tree = Structure[]
function constructUrl(path: string | string[], append = true) {
const arr = Array.isArray(path) ? path : path.split('/').filter(Boolean)
const url =
import.meta.env.VITE_API_ENDPOINT +
arr.reduce<string>((a, v, i) => {
switch (String(i)) {
case '0':
return `cabinet/${v}`
case '1':
return `${a}/drawer/${v}`
case '2':
return `${a}/folder/${v}`
case '3':
return `${a}/subfolder/${v}`
default:
return a
}
}, '')
return append
? url + ['cabinet', '/drawer', '/folder', '/subfolder'][arr.length]
: url
}
function consistantPath(path: string | string[]) {
return Array.isArray(path)
? path.join('/') + '/'
: path.split('/').filter(Boolean).join('/') + '/'
}
const useStorage = defineStore('storageStore', () => {
const error = useErrorStore()
const loader = useLoader()
const init = ref<boolean>(false)
const folder = ref<Record<Path, StorageFolder[]>>({})
const file = ref<Record<Path, StorageFile[]>>({})
const tree = computed(() => {
let structure: Tree = []
// parse list of folder and list of file into tree
Object.entries(folder.value).forEach(([key, value]) => {
const arr = key.split('/').filter(Boolean)
// Once run then it is init
if (!init.value) init.value = true
if (arr.length === 0) {
structure = value.map((v) => ({
pathname: v.pathname,
name: v.name,
createdAt: v.createdAt,
createdBy: v.createdBy,
folder: [],
file: [],
}))
} else {
let current: Structure | undefined
// traverse into tree
arr.forEach((v, i) => {
current =
i === 0
? structure.find((x) => x.name === v)
: current?.folder.find((x) => x.name === v)
})
// set data in tree (object is references to the same object)
if (current) {
current.folder = value.map((v) => ({
pathname: v.pathname,
name: v.name,
createdAt: v.createdAt,
createdBy: v.createdBy,
folder: [],
file: [],
}))
current.file = file.value[key] ?? []
}
}
})
return structure
})
const currentInfo = reactive<{
path: string
dept: number
}>({
path: '',
dept: 0,
})
if (!init.value) goto(sessionStorage.getItem('path') || '')
async function getStorage(path: string = '') {
const arr = path.split('/').filter(Boolean)
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))
if (res.status === 200 && res.data && Array.isArray(res.data))
folder.value[consistantPath(path)] = res.data.sort((a, b) =>
a.pathname.localeCompare(b.pathname),
)
}
async function getStorageFile(path: string = '') {
const arr = path.split('/').filter(Boolean)
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',
)
if (res.status === 200 && res.data && Array.isArray(res.data))
file.value[consistantPath(path)] = res.data.sort((a, b) =>
a.pathname.localeCompare(b.pathname),
)
}
async function goto(path: string = '', force = false) {
loader.show()
const arr = path.split('/').filter(Boolean)
// get all parent to the root structure
// this will also triggher init structure as it get root structure
for (let i = 0; i < arr.length; i++) {
const current = consistantPath(arr.slice(0, i - arr.length))
if (!folder.value[current] || force) await getStorage(current)
}
path = consistantPath(path)
// only get this path once, after that will get from socket.io-client instead
if (!folder.value[path] || force) await getStorage(path)
if (!file.value[path] || force) await getStorageFile(path)
currentInfo.path = path
currentInfo.dept = path.split('/').filter(Boolean).length
sessionStorage.setItem('path', path)
loader.hide()
}
async function gotoParent() {
const arr = currentInfo.path.split('/').filter(Boolean)
await goto(consistantPath(arr.slice(0, -1)))
}
// socket.io zone
const socket = io('http://localhost:25565')
socket.on('connect', () => console.info('Socket.io connected.'))
socket.on('disconnect', () => console.info('Socket.io disconnected.'))
socket.on(
'CreateFolder',
(data: {
pathname: string
name: string
createdAt: string
createdBy: string
}) => {
const arr = data.pathname.split('/').filter(Boolean)
const path = consistantPath(arr.slice(0, -1))
if (folder.value[path]) {
const idx = folder.value[path].findIndex(
(v) => v.pathname === data.pathname,
)
if (idx === -1) {
folder.value[path].push({
pathname: data.pathname,
name: data.name,
createdAt: data.createdAt,
createdBy: data.createdBy,
})
} else {
error.title = 'ตั้งชื่อ Folder ซ้ำ'
error.msg = ''
error.show()
}
folder.value[path].sort((a, b) => a.pathname.localeCompare(b.pathname))
}
},
)
// 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 }) => {
const src = data.from.split('/').filter(Boolean)
const dst = data.to.split('/').filter(Boolean)
const path = consistantPath(src.slice(0, -1))
if (folder.value[path]) {
const val = folder.value[path].find((v) => v.pathname === data.from)
if (val) {
val.pathname = data.to
val.name = dst[dst.length - 1]
}
}
const regex = new RegExp(`^${data.from}`)
for (let key in folder.value) {
if (key.startsWith(data.from)) {
folder.value[key.replace(regex, data.to)] = folder.value[key].map(
(v) => {
v.pathname = v.pathname.replace(regex, data.to)
return v
},
)
delete folder.value[key]
}
}
for (let key in file.value) {
if (key.startsWith(data.from)) {
file.value[key.replace(regex, data.to)] = file.value[key].map((v) => {
v.pathname = v.pathname.replace(regex, data.to)
return v
})
delete file.value[key]
}
}
})
socket.on('DeleteFolder', (data: { pathname: string }) => {
for (let key in folder.value) {
if (key.startsWith(data.pathname)) {
delete folder.value[key]
}
}
for (let key in file.value) {
if (key.startsWith(data.pathname)) {
delete file.value[key]
}
}
const arr = data.pathname.split('/').filter(Boolean)
const path = consistantPath(arr.slice(0, -1))
if (folder.value[path]) {
folder.value[path] = folder.value[path].filter(
(v) => v.pathname !== data.pathname,
)
}
if (currentInfo.path.includes(arr[arr.length - 1])) {
error.title = 'ตำเเหน่งที่คุณอยู่ถูกลบ'
error.msg = ''
error.show()
}
})
socket.on('FileUpload', (data: StorageFile) => {
const arr = data.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.pathname,
)
if (idx !== -1) file.value[path][idx] = data
else file.value[path].push(data)
file.value[path].sort((a, b) => a.pathname.localeCompare(b.pathname))
}
})
socket.on('FileDelete', (data: { pathname: string }) => {
const arr = data.pathname.split('/').filter(Boolean)
const path = consistantPath(arr.slice(0, -1))
if (file.value[path]) {
file.value[path] = file.value[path].filter(
(v) => v.pathname !== data.pathname,
)
}
})
socket.on(
'FileUpdateMove',
(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
}
},
)
socket.on('FileUpdate', (data: StorageFile) => {
const arr = data.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.pathname,
)
if (idx !== -1) file.value[path][idx] = data
}
})
socket.on('FileUploadRequest', (data: StorageFile) => {
const arr = data.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.pathname,
)
if (idx !== -1) file.value[path][idx] = data
else file.value[path].push(data)
file.value[path].sort((a, b) => a.pathname.localeCompare(b.pathname))
}
})
async function createFolder(name: string, path: string = currentInfo.path) {
loader.show()
await api.post(constructUrl(path, true), { name })
loader.hide()
}
async function editFolder(name: string, path: string) {
loader.show()
await api.put(constructUrl(path, false), { name })
loader.hide()
}
async function deleteFolder(path: string) {
loader.show()
await api.delete(constructUrl(path, false))
loader.hide()
}
type FileMetadata = {
title?: string
description?: string
keyword?: string[]
category?: 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
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) {
await axios
.put(res.data.upload, file, {
headers: { 'Content-Type': file.type },
onUploadProgress: (e) => console.log(e),
})
.catch((e) => console.error(e))
}
loader.hide()
}
async function updateFile(pathname: string, data: FileMetadata, file?: File) {
const arr = pathname.split('/')
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 },
)
if (res && res.status === 201 && res.data && res.data.upload) {
await axios
.put(res.data.upload, file, {
headers: { 'Content-Type': file?.type },
onUploadProgress: (e) => console.log(e),
})
.catch((e) => console.error(e))
}
loader.hide()
}
async function deleteFile(pathname: string) {
const arr = pathname.split('/')
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]}`,
)
loader.hide()
}
return {
// information
currentInfo,
folder,
file,
tree,
// fetch
getStorage,
getStorageFile,
// traverse
goto,
gotoParent,
// operation
createFolder,
editFolder,
deleteFolder,
createFile,
updateFile,
deleteFile,
}
})
export default useStorage