Redesign file browser, file upload (server)
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express'
|
||||
import { deleteAllTables, deleteExerciseProgressTables, prepopulateDatabase, prepopulateExerciseDatabase } from '../scripts/databaseHelper'
|
||||
import fs from "fs"
|
||||
|
||||
export const api = Router()
|
||||
|
||||
/**
|
||||
* Status check endpoint
|
||||
*/
|
||||
api.get("/", (req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(200).send()
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset the whole database to factory state
|
||||
* Doesn't effect ExerciseTable and ExerciseGroupTable
|
||||
*/
|
||||
api.get("/resetdatabase", async (req: Request, res: Response, next: NextFunction) => {
|
||||
// Step 1: Delete all data tables
|
||||
deleteAllTables()
|
||||
@@ -19,35 +25,13 @@ api.get("/resetdatabase", async (req: Request, res: Response, next: NextFunction
|
||||
res.status(200).send()
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset ExerciseTable and ExerciseGroupTable to factory state
|
||||
*/
|
||||
api.get("/resetExerciseProgress", async (req: Request, res: Response, next: NextFunction) => {
|
||||
deleteExerciseProgressTables()
|
||||
|
||||
await prepopulateExerciseDatabase()
|
||||
|
||||
res.status(200).send()
|
||||
})
|
||||
|
||||
/**
|
||||
* Get all uploaded file names
|
||||
*/
|
||||
api.get("/files", async (req: Request, res: Response) => {
|
||||
let dirNames = fs.readdirSync("./backend/images")
|
||||
let result = []
|
||||
|
||||
dirNames.forEach(dir => {
|
||||
let fileNames = fs.readdirSync("./backend/images/" + dir)
|
||||
|
||||
result.push({
|
||||
folder: dir,
|
||||
files: fileNames.map(file => {
|
||||
return {
|
||||
name: file,
|
||||
size: fs.statSync("./backend/images/" + dir + "/" + file).size,
|
||||
url: "http://localhost:3000/static/" + dir + "/" + file
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
res.status(200).json(result)
|
||||
})
|
||||
@@ -11,7 +11,9 @@ import { calcOverallRating, calcRatingValues } from "../scripts/calcScripts";
|
||||
|
||||
export const band = Router()
|
||||
|
||||
// Get all bands
|
||||
/**
|
||||
* Get all bands
|
||||
*/
|
||||
band.get("/", (req: Request, res: Response) => {
|
||||
let sort = req.query.sort
|
||||
let count = req.query.count
|
||||
@@ -64,7 +66,9 @@ band.get("/", (req: Request, res: Response) => {
|
||||
})
|
||||
})
|
||||
|
||||
// Get all information about one band
|
||||
/**
|
||||
* Get all information about one band
|
||||
*/
|
||||
band.get("/band/:name", (req: Request, res: Response) => {
|
||||
Band.findOne({
|
||||
where: {
|
||||
@@ -123,7 +127,9 @@ band.get("/band/:name", (req: Request, res: Response) => {
|
||||
})
|
||||
|
||||
|
||||
// Band search
|
||||
/**
|
||||
* Band search
|
||||
*/
|
||||
band.get("/search", (req: Request, res: Response) => {
|
||||
Band.findAll({
|
||||
where: {
|
||||
@@ -139,7 +145,9 @@ band.get("/search", (req: Request, res: Response) => {
|
||||
})
|
||||
|
||||
|
||||
// Edit band
|
||||
/**
|
||||
* Edit band
|
||||
*/
|
||||
band.patch("/", (req: Request, res: Response) => {
|
||||
Band.update(req.body, {
|
||||
where: {
|
||||
@@ -152,7 +160,9 @@ band.patch("/", (req: Request, res: Response) => {
|
||||
})
|
||||
|
||||
|
||||
// New band
|
||||
/**
|
||||
* New band
|
||||
*/
|
||||
band.post("/", (req: Request, res: Response) => {
|
||||
Band.create(req.body)
|
||||
.then(result => {
|
||||
@@ -160,6 +170,9 @@ band.post("/", (req: Request, res: Response) => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Delete a band
|
||||
*/
|
||||
band.delete("/", (req: Request, res: Response) => {
|
||||
Band.destroy({
|
||||
where: {
|
||||
|
||||
52
software/backend/routes/files.routes.ts
Normal file
52
software/backend/routes/files.routes.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express'
|
||||
import fs from "fs"
|
||||
import multer from "multer"
|
||||
const upload = multer({ dest: './backend/images/' })
|
||||
|
||||
export const files = Router()
|
||||
|
||||
/**
|
||||
* Get all folders
|
||||
*/
|
||||
files.get("/folders", async (req: Request, res: Response) => {
|
||||
let dirNames = fs.readdirSync("./backend/images")
|
||||
let result = []
|
||||
|
||||
dirNames.forEach(dir => {
|
||||
result.push({
|
||||
name: dir,
|
||||
nrOfItems: fs.readdirSync("./backend/images/" + dir).length
|
||||
})
|
||||
})
|
||||
|
||||
res.status(200).json(result)
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Get all uploaded file names by file name
|
||||
*/
|
||||
files.get("/:folder", async (req: Request, res: Response) => {
|
||||
let result = []
|
||||
let fileNames = fs.readdirSync("./backend/images/" + req.params.folder + "/")
|
||||
|
||||
fileNames.forEach(file => {
|
||||
result.push({
|
||||
name: file,
|
||||
size: fs.statSync("./backend/images/" + req.params.folder + "/" + file).size,
|
||||
url: "http://localhost:3000/static/" + req.params.folder + "/" + file
|
||||
})
|
||||
})
|
||||
|
||||
res.status(200).json(result)
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Upload a file
|
||||
*/
|
||||
files.post("/", upload.single("file"), function (req: Request, res: Response, next: NextFunction) {
|
||||
console.log(req.file)
|
||||
|
||||
res.status(200).send()
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import { genre } from './routes/genre.routes'
|
||||
import { location } from './routes/location.routes'
|
||||
import { city } from './routes/city.routes'
|
||||
import { exercises } from './routes/exercise.routes'
|
||||
import { files } from './routes/files.routes'
|
||||
|
||||
const app = express()
|
||||
const port = 3000
|
||||
@@ -43,7 +44,7 @@ app.use("/accounts", account)
|
||||
app.use("/cities", city)
|
||||
app.use("/concerts", concert)
|
||||
app.use("/exercises", exercises)
|
||||
|
||||
app.use("/files", files)
|
||||
|
||||
// Start server
|
||||
const server = app.listen(port, () => {
|
||||
|
||||
3864
software/package-lock.json
generated
3864
software/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"axios": "^1.7.7",
|
||||
"body-parser": "^1.20.2",
|
||||
@@ -41,6 +42,7 @@
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"express": "^4.21.1",
|
||||
"moment": "^2.30.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pinia": "^2.2.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sequelize": "^6.37.4",
|
||||
|
||||
45
software/src/data/api/files.api.ts
Normal file
45
software/src/data/api/files.api.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import axios from "axios"
|
||||
|
||||
const BASE_URL = "http://localhost:3000/files"
|
||||
|
||||
/**
|
||||
* Fetch all public folders on server
|
||||
*
|
||||
* @returns Response from server
|
||||
*/
|
||||
export function fetchFolderNames() {
|
||||
return axios.get(BASE_URL + "/folders")
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all static file names
|
||||
*
|
||||
* @param dirName Name of folder where to scan files
|
||||
*
|
||||
* @returns Response from server
|
||||
*/
|
||||
export function fetchFileNames(dirName: string) {
|
||||
return axios.get(BASE_URL + "/" + dirName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to the server
|
||||
*
|
||||
* @param file File to store on server
|
||||
*
|
||||
* @returns Response from server
|
||||
*/
|
||||
export function postFile(file, folder: string) {
|
||||
let formData = new FormData()
|
||||
|
||||
formData.append("file", file)
|
||||
formData.append("folder", folder)
|
||||
|
||||
console.log(formData)
|
||||
|
||||
return axios.post(BASE_URL, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -27,13 +27,4 @@ export function resetDatabase() {
|
||||
*/
|
||||
export function resetExerciseProgress() {
|
||||
return axios.get(BASE_URL + "/resetExerciseProgress")
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all static file names
|
||||
*
|
||||
* @returns Response from server
|
||||
*/
|
||||
export function fetchFileNames() {
|
||||
return axios.get(BASE_URL + "/files")
|
||||
}
|
||||
@@ -238,7 +238,10 @@
|
||||
},
|
||||
"user": "Angaben zur Person",
|
||||
"registrationNumber": "Matrikelnummer",
|
||||
"yourFullName": "Vollständiger Name"
|
||||
"yourFullName": "Vollständiger Name",
|
||||
"chooseFile": "Datei auswählen",
|
||||
"chooseDestinationFolder": "Zielordner auswählen",
|
||||
"upload": "Hochladen"
|
||||
},
|
||||
"genre": {
|
||||
"withoutBand": "ohne Band"
|
||||
|
||||
@@ -238,7 +238,10 @@
|
||||
},
|
||||
"user": "About person",
|
||||
"registrationNumber": "Matrikel number",
|
||||
"yourFullName": "Full name"
|
||||
"yourFullName": "Full name",
|
||||
"chooseFile": "Choose file",
|
||||
"chooseDestinationFolder": "Choose destination folder",
|
||||
"upload": "Upload"
|
||||
},
|
||||
"genre": {
|
||||
"withoutBand": "without Band"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useGenreStore } from '@/stores/genre.store';
|
||||
import { usePreferencesStore } from '@/stores/preferences.store';
|
||||
import dashboardCard from './dashboardCard.vue';
|
||||
import { useOrderStore } from '@/stores/order.store';
|
||||
import { useFilesStore } from '@/stores/files.store';
|
||||
|
||||
const concertStore = useConcertStore()
|
||||
const bandStore = useBandStore()
|
||||
@@ -17,10 +18,11 @@ const locationStore = useLocationStore()
|
||||
const exerciseStore = useExerciseStore()
|
||||
const preferencesStore = usePreferencesStore()
|
||||
const orderStore = useOrderStore()
|
||||
const filesStore = useFilesStore()
|
||||
|
||||
exerciseStore.solveExercise(2, 1)
|
||||
|
||||
preferencesStore.getStaticFiles()
|
||||
filesStore.getStaticFolders()
|
||||
bandStore.getBands()
|
||||
locationStore.getLocations()
|
||||
genreStore.getGenres()
|
||||
@@ -98,10 +100,10 @@ orderStore.getAllOrders()
|
||||
<dashboard-card
|
||||
:title="$t('misc.file', 2)"
|
||||
icon="mdi-file"
|
||||
:first-line="preferencesStore.staticFiles.reduce((counter, obj) => {
|
||||
return counter += obj.files.length
|
||||
}, 0) + ' ' + $t('misc.file', 2)"
|
||||
:second-line="preferencesStore.staticFiles.length + ' ' + $t('misc.folder', 2)"
|
||||
:first-line="filesStore.staticFolders.reduce((counter, obj) => {
|
||||
return counter + obj.nrOfItems
|
||||
}, 0) + ' ' + $t('misc.file', 2)"
|
||||
:second-line="filesStore.staticFolders.length + ' ' + $t('misc.folder', 2)"
|
||||
button-route="/admin/files"
|
||||
:loading="preferencesStore.fetchInProgress"
|
||||
/>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import actionDialog from '@/components/basics/actionDialog.vue';
|
||||
|
||||
const showDialog = defineModel("showDialog")
|
||||
|
||||
defineProps({
|
||||
url: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<action-dialog v-model="showDialog" max-width="500">
|
||||
<v-img :src="url" max-height="400" />
|
||||
</action-dialog>
|
||||
</template>
|
||||
64
software/src/pages/admin/filesAdminPage/fileUploadDialog.vue
Normal file
64
software/src/pages/admin/filesAdminPage/fileUploadDialog.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import actionDialog from '@/components/basics/actionDialog.vue';
|
||||
import outlinedButton from '@/components/basics/outlinedButton.vue';
|
||||
import { useFilesStore } from '@/stores/files.store';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const filesStore = useFilesStore()
|
||||
|
||||
const test = ref()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<action-dialog
|
||||
v-model="filesStore.showFileUploadDialog"
|
||||
:title="$t('misc.uploadFile')"
|
||||
icon="mdi-file"
|
||||
max-width="800"
|
||||
>
|
||||
<v-form :model-value="test">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-file-input
|
||||
v-model="filesStore.fileUpload"
|
||||
clearable
|
||||
:label="$t('misc.chooseFile')"
|
||||
:disabled="filesStore.fetchInProgress"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-radio-group
|
||||
v-model="filesStore.fileUploadDir"
|
||||
:label="$t('misc.chooseDestinationFolder')"
|
||||
:disabled="filesStore.fetchInProgress"
|
||||
>
|
||||
<v-radio
|
||||
v-for="folder of filesStore.staticFolders"
|
||||
:label="folder.name + '/'"
|
||||
:value="folder.name"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-btn type="submit">Submit</v-btn>
|
||||
</v-form>
|
||||
|
||||
<template #actions>
|
||||
<outlined-button
|
||||
@click="filesStore.uploadFile"
|
||||
prepend-icon="mdi-file-upload"
|
||||
color="green"
|
||||
:disabled="filesStore.fileUploadDir.length == 0 || filesStore.fileUpload == undefined"
|
||||
:loading="filesStore.fetchInProgress"
|
||||
>
|
||||
{{ $t('misc.upload') }}
|
||||
</outlined-button>
|
||||
</template>
|
||||
</action-dialog>
|
||||
</template>
|
||||
@@ -1,44 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import adminDataLayout from '@/layouts/adminDataLayout.vue';
|
||||
import { usePreferencesStore } from '@/stores/preferences.store';
|
||||
import filePreviewDialog from './filePreviewDialog.vue';
|
||||
import { ref } from 'vue';
|
||||
import FileUploadDialog from './fileUploadDialog.vue';
|
||||
import { useFilesStore } from '@/stores/files.store';
|
||||
|
||||
const preferencesStore = usePreferencesStore()
|
||||
const showDialog = ref(false)
|
||||
const filesStore = useFilesStore()
|
||||
const showPreviewDialog = ref(false)
|
||||
const previewFile = ref("")
|
||||
|
||||
preferencesStore.getStaticFiles()
|
||||
filesStore.getStaticFolders()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<admin-data-layout
|
||||
:add-button-string="$t('misc.uploadFile')"
|
||||
:fetch-in-progress="preferencesStore.fetchInProgress"
|
||||
:on-add-click="() => { /** todo */ }"
|
||||
:fetch-in-progress="filesStore.fetchInProgress"
|
||||
:on-add-click="() => { filesStore.showFileUploadDialog = true }"
|
||||
>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="folder of preferencesStore.staticFiles"
|
||||
cols="12"
|
||||
md="3"
|
||||
sm="6"
|
||||
>
|
||||
<v-row >
|
||||
<v-col cols="2" class="border">
|
||||
<v-list>
|
||||
<v-list-subheader>{{ folder.folder }}/</v-list-subheader>
|
||||
<v-list-item
|
||||
v-for="file of folder.files"
|
||||
:title="file.name"
|
||||
:subtitle="Math.round(file.size / 1024) + ' KB'"
|
||||
@click="() => { previewFile = file.url; showDialog = true }"
|
||||
<v-list-item
|
||||
v-for="folder of filesStore.staticFolders"
|
||||
:key="folder.name"
|
||||
:value="folder"
|
||||
:title="folder.name + '/'"
|
||||
@click="filesStore.selectedFolder = folder; filesStore.getStaticFiles()"
|
||||
/>
|
||||
</v-list>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="4" class="border">
|
||||
<v-skeleton-loader
|
||||
:loading="filesStore.fetchInProgress"
|
||||
type="list-item-two-line"
|
||||
>
|
||||
<v-list max-height="800" class="w-100">
|
||||
<v-list-item
|
||||
v-for="file of filesStore.staticFiles"
|
||||
:title="file.name"
|
||||
:value="file.name"
|
||||
:subtitle="Math.round(file.size / 1024) + ' KB'"
|
||||
@click="() => { filesStore.selectedFile = file }"
|
||||
/>
|
||||
</v-list>
|
||||
</v-skeleton-loader>
|
||||
</v-col>
|
||||
|
||||
<v-col class="border">
|
||||
<v-img
|
||||
v-if="filesStore.selectedFile != undefined"
|
||||
:src="filesStore.selectedFile.url" max-height="400" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</admin-data-layout>
|
||||
|
||||
<file-preview-dialog
|
||||
v-model:show-dialog="showDialog"
|
||||
v-model:show-dialog="showPreviewDialog"
|
||||
:url="previewFile"
|
||||
/>
|
||||
|
||||
<file-upload-dialog />
|
||||
</template>
|
||||
61
software/src/stores/files.store.ts
Normal file
61
software/src/stores/files.store.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { fetchFileNames, fetchFolderNames, postFile } from "@/data/api/files.api";
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
export const useFilesStore = defineStore('filesStore', {
|
||||
state: () => ({
|
||||
/** Request to server sent, waiting for data response */
|
||||
fetchInProgress: ref(false),
|
||||
|
||||
staticFolders: ref<Array<{name: string, nrOfItems: number}>>([]),
|
||||
|
||||
selectedFolder: ref<{name: string, nrOfItems: number}>(),
|
||||
|
||||
/** List of files on the server */
|
||||
staticFiles: ref<Array<{name: string, size: number, url: string}>>([]),
|
||||
|
||||
selectedFile: ref<{name: string, size: number, url: string}>(),
|
||||
|
||||
showFileUploadDialog: ref(false),
|
||||
|
||||
fileUpload: ref(),
|
||||
|
||||
fileUploadDir: ref(""),
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async getStaticFolders() {
|
||||
this.fetchInProgress = true
|
||||
|
||||
fetchFolderNames()
|
||||
.then(res => {
|
||||
this.staticFolders = res.data
|
||||
this.fetchInProgress = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Request all available static files on server
|
||||
*/
|
||||
async getStaticFiles() {
|
||||
this.fetchInProgress = true
|
||||
|
||||
fetchFileNames(this.selectedFolder.name)
|
||||
.then(res => {
|
||||
this.staticFiles = res.data
|
||||
this.fetchInProgress = false
|
||||
})
|
||||
},
|
||||
|
||||
async uploadFile() {
|
||||
this.fetchInProgress = true
|
||||
|
||||
postFile(this.uploadFile, this.fileUploadDir)
|
||||
.then(response => {
|
||||
console.log(response)
|
||||
this.showFileUploadDialog = false
|
||||
this.fetchInProgress = false
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import { useLocalStorage } from "@vueuse/core";
|
||||
import { ThemeEnum } from "../data/enums/themeEnums";
|
||||
import { LanguageEnum } from "../data/enums/languageEnum";
|
||||
import { ref } from "vue";
|
||||
import { fetchFileNames, fetchServerState, resetDatabase, resetExerciseProgress } from "@/data/api/mainApi";
|
||||
import { fetchServerState,resetDatabase, resetExerciseProgress } from "@/data/api/mainApi";
|
||||
import { ServerStateEnum } from "@/data/enums/serverStateEnum";
|
||||
import { BannerStateEnum } from "@/data/enums/bannerStateEnum";
|
||||
import { useFeedbackStore } from "./feedback.store";
|
||||
@@ -32,9 +32,6 @@ export const usePreferencesStore = defineStore('preferencesStore', {
|
||||
/** Show the "Factory reset" confirm dialog */
|
||||
showFactoryResetDialog: ref(false),
|
||||
|
||||
/** List of files on the server */
|
||||
staticFiles: ref([]),
|
||||
|
||||
/** Marks the first run of the app */
|
||||
firstStartup: useLocalStorage<Boolean>("hackmycart/preferencesStore/firstStartup", true),
|
||||
|
||||
@@ -108,19 +105,6 @@ export const usePreferencesStore = defineStore('preferencesStore', {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Request all available static files on server
|
||||
*/
|
||||
async getStaticFiles() {
|
||||
this.fetchInProgress = true
|
||||
|
||||
fetchFileNames()
|
||||
.then(res => {
|
||||
this.staticFiles = res.data
|
||||
this.fetchInProgress = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset all store values to factory state
|
||||
*/
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"backend/**/*.ts",
|
||||
"backend/**/*.json",
|
||||
"backend/images/**/**/*"
|
||||
]
|
||||
, "backend/server.js" ]
|
||||
}
|
||||
Reference in New Issue
Block a user