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((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(false) const folder = ref>({}) const file = ref>({}) 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(import.meta.env.VITE_API_HOST) 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, }) } 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 !== path && currentInfo.path.length > path.length && currentInfo.path.startsWith(path) ) { 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