// The Lucia project. // Copyright 2023-2026 DSP, inc. All rights reserved. // Authors: // chiayin.kuo@dsp.im (chiayin), 2023/1/31 // imacat.yang@dsp.im (imacat), 2023/9/23 // cindy.chang@dsp.im (Cindy Chang), 2024/5/30 /** * @module stores/files File management store for listing, uploading, * renaming, downloading, and deleting log/filter/rule files. */ import { defineStore } from "pinia"; import apiClient from "@/api/client.js"; import moment from 'moment'; import apiError from '@/module/apiError.js'; import Swal from 'sweetalert2'; import { uploadFailedFirst, uploadFailedSecond, uploadloader, uploadSuccess, deleteSuccess } from '@/module/alertModal.js'; import { useLoadingStore } from '@/stores/loading'; /** Map of file type to API path segment. */ const FILE_TYPE_PATHS = { 'log': 'logs', 'filter': 'filters', 'log-check': 'log-checks', 'filter-check': 'filter-checks', }; /** * Returns the API base path for a file type and ID. * @param {string} type - The file type. * @param {number} id - The file ID. * @returns {string} The API base path. */ function getFileApiBase(type, id) { return `/api/${FILE_TYPE_PATHS[type]}/${id}`; } /** Pinia store for file CRUD operations and upload workflow. */ export const useFilesStore = defineStore('filesStore', { state: () => ({ allEventFiles: [ { parentLog: '', fileType: '', ownerName: '', } ], switchFilesTagData: { ALL: ['Log', 'Filter', 'Rule', 'Design'], DISCOVER: ['Log', 'Filter', 'Rule'], COMPARE: ['Log','Filter'], DESIGN: ['Log', 'Design'], }, filesTag: 'ALL', httpStatus: 200, uploadId: null, allUploadDetail: null, uploadLogId: null, uploadFileName: null, allDependentsData: null, }), getters: { /** * Get allFiles and switch files tag */ allFiles: state => { let result = state.allEventFiles; const data = state.switchFilesTagData; const filesTag = state.filesTag; result = result.filter(file => data[filesTag].includes(file.fileType)); return result; }, /** * Get upload preview */ uploadDetail: state => { return state.allUploadDetail; }, /** * Get dependents of files data */ dependentsData: state => { return state.allDependentsData; } }, actions: { /** * Fetch All Files api */ async fetchAllFiles() { const api = '/api/files'; let icon = ''; let fileType = ''; let parentLog = ''; try { const response = await apiClient.get(api); this.allEventFiles = response.data; this.allEventFiles.forEach(o => { switch (o.type) { case 'log': icon = 'work_history'; fileType = 'Log'; parentLog = o.name; break; case 'filter': icon = 'tornado'; fileType = 'Filter'; parentLog = o.parent.name; break; case 'log-check': case 'filter-check': icon = 'local_police'; fileType = 'Rule'; parentLog = o.parent.name; break; case 'design': icon = 'shape_line'; break; } o.icon = icon; o.parentLog = parentLog; o.fileType = fileType; o.ownerName = o.owner.name; o.updated_base = o.updated_at; o.accessed_base = o.accessed_at; o.updated_at = moment(o.updated_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm'); o.accessed_at = o.accessed_at ? moment(o.accessed_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm') : null; }); } catch(error) { apiError(error, 'Failed to load the files.'); }; }, /** * Uploads a CSV log file (first stage). * @param {Object} fromData - The form data to send to the backend. */ async upload(fromData) { const api = '/api/logs/csv-uploads'; const config = { data: true, headers: { 'Content-Type': 'multipart/form-data', }, }; uploadloader(); // 進度條 try { const response = await apiClient.post(api, fromData, config); this.uploadId = response.data.id; this.$router.push({name: 'Upload'}); Swal.close(); // 關閉進度條 } catch(error) { if(error.response.status === 422) { // msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv' // type: 'encoding' | 'insufficient_columns' | 'empty' | 'name_suffix' const detail = error.response.data.detail; uploadFailedFirst(detail[0].type, detail[0].msg, detail[0].loc[2]); } else { Swal.close(); // 關閉進度條 apiError(error, 'Failed to upload the files.'); } } }, /** * Fetch upload detail */ async getUploadDetail() { const uploadId = this.uploadId; const api = `/api/logs/csv-uploads/${uploadId}`; try { const response = await apiClient.get(api); this.allUploadDetail = response.data.preview; } catch(error) { apiError(error, 'Failed to get upload detail.'); } }, /** * Adds a log from an uploaded CSV log file (second stage). * @param {Object} data - The request payload for the backend. */ async uploadLog(data) { const uploadId = this.uploadId; const api = `/api/logs/csv-uploads/${uploadId}`; uploadloader(); // 進度條 try { const response = await apiClient.post(api, data); this.uploadLogId = await response.data.id; await Swal.close(); // 關閉進度條 await this.rename(); // 改檔名 await uploadSuccess(); this.$router.push({name: 'Files'}); } catch(error) { if(error.response.status === 422) { const detail = [...error.response.data.detail]; uploadFailedSecond(detail); } else { Swal.close(); // 關閉進度條 apiError(error, 'Failed to upload the log files.'); } } }, /** * Rename a Log * @param {string} type - The file type ('log', 'filter', 'log-check', or 'filter-check'). * @param {number} id - The file ID. * @param {string} name - The file name. */ async rename(type, id, fileName) { // 先判斷有沒有 uploadLogId,有就設定 id 和 type;再判斷檔案型別。 if(this.uploadId && this.uploadFileName) { type = 'log'; id = this.uploadLogId; fileName = this.uploadFileName; } const data = {"name": fileName}; try { await apiClient.put(`${getFileApiBase(type, id)}/name`, data); this.uploadFileName = null; await this.fetchAllFiles(); } catch(error) { apiError(error, 'Failed to rename.'); } }, /** * Get the Dependents of the files * @param {string} type - The file type ('log', 'filter', 'log-check', or 'filter-check'). * @param {number} id - The file ID. */ async getDependents(type, id) { try { const response = await apiClient.get(`${getFileApiBase(type, id)}/dependents`); this.allDependentsData = response.data; } catch(error) { apiError(error, 'Failed to get Dependents of the files.'); } }, /** * Delete file * @param {string} type - The file type ('log', 'filter', 'log-check', or 'filter-check'). * @param {number} id - The file ID. */ async deleteFile(type, id) { if(id === null || id === undefined || isNaN(id)) { console.error('Delete File API Error: invalid id'); return; }; const loading = useLoadingStore(); loading.isLoading = true; try { await apiClient.delete(getFileApiBase(type, id)); await this.fetchAllFiles(); await deleteSuccess(); } catch(error) { apiError(error, 'Failed to delete.'); } finally { loading.isLoading = false; } }, /** * Removes a deletion record permanently. * @param {number} id - The file ID. */ async deletionRecord(id) { let api = ''; const loading = useLoadingStore(); loading.isLoading = true; api = `/api/deletion/${id}`; try { await apiClient.delete(api); } catch(error) { apiError(error, 'Failed to Remove a Deletion Record.') } finally { loading.isLoading = false; } }, /** * Download file as CSV * @param {string} type - The file type ('log', 'filter', 'log-check', or 'filter-check'). * @param {number} id - The file ID. * @param {string} fileName - The file name. */ async downloadFileCSV(type, id, fileName) { if (type !== 'log' && type !== 'filter') return; try { const response = await apiClient.get(`${getFileApiBase(type, id)}/csv`); const csvData = response.data; const blob = new Blob([csvData], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${fileName}.csv`; link.click(); window.URL.revokeObjectURL(url); } catch(error) { apiError(error, 'Failed to delete.'); } }, }, })