Skip to content

Commit

Permalink
optional file manager
Browse files Browse the repository at this point in the history
  • Loading branch information
MrBrax committed Apr 27, 2022
1 parent 253f8a8 commit 7c2c871
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 12 deletions.
2 changes: 1 addition & 1 deletion client-vue/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "twitchautomator-client",
"version": "0.7.2",
"version": "0.7.3",
"private": true,
"homepage": "https://github.com/MrBrax/TwitchAutomator",
"scripts": {
Expand Down
98 changes: 98 additions & 0 deletions client-vue/src/components/FileManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template>
<table class="table file-manager is-fullwidth is-striped" v-if="!error">
<tr class="file-manager-item" v-for="(item, index) in files">
<td class="file-manager-item-name">{{ item.name }}</td>
<td class="file-manager-item-size">{{ formatBytes(item.size) }}</td>
<td class="file-manager-item-date">{{ item.date }}</td>
<td class="file-manager-item-actions">
<a class="button is-small is-confirm" :href="downloadLink(item)" target="_blank" download><fa icon="download"></fa></a>
<button class="button is-small is-danger" @click="deleteFile(item)"><fa icon="trash"></fa></button>
</td>
</tr>
</table>
<div class="notification is-danger error" v-if="error">
{{ error }}
</div>
</template>

<script lang="ts">
import { useStore } from "@/store";
import { AxiosError } from "axios";
import { defineComponent } from "vue";
// import { library } from "@fortawesome/fontawesome-svg-core";
// import { faSkull, faTrash } from "@fortawesome/free-solid-svg-icons";
// import { useStore } from "@/store";
// import { JobStatus } from "@common/Defs";
// library.add(faSkull, faTrash);
interface ApiFile {
name: string;
size: number;
date: string;
is_dir: boolean;
}
export default defineComponent({
name: "FileManager",
props: {
path: {
type: String,
required: true,
},
web: {
type: String,
required: true,
},
},
setup() {
const store = useStore();
return { store };
},
data(): {
files: ApiFile[];
error: string;
} {
return {
files: [],
error: "",
};
},
created() {
},
mounted() {
this.fetchFileList();
},
methods: {
fetchFileList() {
console.debug("Fetching file list...");
this.$http.get(`/api/v0/files?path=${this.path}`).then((response) => {
this.files = response.data.data.files;
}).catch((error: AxiosError | Error) => {
if ("response" in error && error.response?.data.message) {
// alert(error.response.data.message);
this.error = error.response.data.message;
}
});
},
deleteFile(file: ApiFile) {
this.$http.delete(`/api/v0/files?path=${this.path}&name=${file.name}`).then((response) => {
this.fetchFileList();
});
},
downloadLink(file: ApiFile) {
const base = import.meta.env.BASE_URL || "/";
const url = `${base}${this.web}/${file.name}`;
return url;
},
},
components: {
},
});
</script>

<style lang="scss">
</style>
10 changes: 6 additions & 4 deletions client-vue/src/views/FilesView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
<section class="section">
<div class="section-title"><h1>Clips</h1></div>
<div class="section-content">
WIP
<FileManager path="storage/saved_clips" web="saved_clips" />
</div>
</section>
<section class="section">
<div class="section-title"><h1>VODs</h1></div>
<div class="section-content">
WIP
<FileManager path="storage/saved_vods" web="saved_vods" />
</div>
</section>
</div>
Expand All @@ -18,6 +18,7 @@
<script lang="ts">
import { useStore } from "@/store";
import { defineComponent } from "vue";
import FileManager from "@/components/FileManager.vue";
// import { library } from "@fortawesome/fontawesome-svg-core";
// import { faSkull, faTrash } from "@fortawesome/free-solid-svg-icons";
Expand All @@ -39,6 +40,7 @@ export default defineComponent({
methods: {
},
components: {
},
FileManager
},
});
</script>
</script>
3 changes: 3 additions & 0 deletions client-vue/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export default defineConfig({
port: 8081,
proxy: {
'/api': 'http://localhost:8080',
'/saved_clips': 'http://localhost:8080',
'/saved_vods': 'http://localhost:8080',
'/vods': 'http://localhost:8080',
},
},
})
3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "twitchautomator-server",
"version": "0.0.7",
"version": "0.0.8",
"description": "",
"main": "index.ts",
"scripts": {
Expand Down Expand Up @@ -49,6 +49,7 @@
"express": "^4.17.3",
"minimist": "^1.2.6",
"morgan": "^1.10.0",
"sanitize-filename": "^1.6.3",
"ws": "^8.5.0"
}
}
147 changes: 147 additions & 0 deletions server/src/Controllers/Files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@

import { DataRoot } from "Core/BaseConfig";
import express from "express";
import path from "path";
import fs from "fs";
import sanitize from "sanitize-filename";
import chalk from "chalk";

const allowedDataPaths = [
"storage",
"logs",
];

const validatePath = (nastyPath: string) => {

// sanitize user path
nastyPath = nastyPath.normalize();

// null and other control characters
// eslint-disable-next-line no-control-regex
if (nastyPath.match(/[\x00-\x1f\x80-\x9f]/g)) {
return "Path contains invalid characters";
}

// don't traverse up the tree
if (!nastyPath.startsWith(DataRoot)) {
return "Path is not valid";
}

// only allow paths that are in the allowed paths
if (!allowedDataPaths.some(allowedPath => nastyPath.startsWith(path.join(DataRoot, allowedPath)))) {
return "Access denied";
}

// check if path exists
if (!fs.existsSync(nastyPath)) {
return "Path does not exist";
}

// check if user path is a directory
if (!fs.lstatSync(nastyPath).isDirectory()) {
return "Path is not a directory";
}

return true;

};

// console.debug("C:\\", validatePath("C:\\"));
// console.debug("C:\\storage", validatePath("C:\\storage"));
// console.debug("/", validatePath("/"));
// console.debug("/storage", validatePath("/storage"));
// console.debug(path.join(DataRoot, ".."), validatePath(path.join(DataRoot, "..")));
// console.debug(path.join(DataRoot, "\u0000"), validatePath(path.join(DataRoot, "\u0000")));
// console.debug(path.join(DataRoot, "CON1"), validatePath(path.join(DataRoot, "CON1")));
// console.debug(path.join(DataRoot, "storage", "saved_vods"), validatePath(path.join(DataRoot, "storage", "saved_vods")));
// console.debug(path.join(DataRoot, "cache"), validatePath(path.join(DataRoot, "cache")));
// console.debug(path.join(DataRoot, "logs"), validatePath(path.join(DataRoot, "logs")));

export function ListFiles(req: express.Request, res: express.Response): void {

const user_path = req.query.path as string;

if (user_path == undefined) {
res.status(400).send({
status: "ERROR",
message: "Path is not defined"
});
return;
}

const full_path = path.join(DataRoot, user_path);

const validation = validatePath(full_path);
if (validation !== true) {
res.status(400).send({
status: "ERROR",
message: validation,
});
return;
}

const raw_files = fs.readdirSync(full_path);

const files = raw_files.map((file) => {
return {
name: file,
size: fs.statSync(path.join(full_path, file)).size,
date: fs.statSync(path.join(full_path, file)).mtime,
is_dir: fs.lstatSync(path.join(full_path, file)).isDirectory(),
};
});

res.send({
status: "OK",
data: {
files: files,
},
});

}

export function DeleteFile(req: express.Request, res: express.Response): void {

const user_path = req.query.path as string;
const file_name = req.query.name as string;

if (user_path == undefined) {
res.status(400).send({
status: "ERROR",
message: "Path is not defined",
});
return;
}

const full_path = path.join(DataRoot, user_path);

const validation = validatePath(full_path);
if (validation !== true) {
res.status(400).send({
status: "ERROR",
message: validation,
});
return;
}

const sanitized_file_name = sanitize(file_name);

const full_file_path = path.join(full_path, sanitized_file_name);

if (!fs.existsSync(full_file_path)) {
res.status(400).send({
status: "ERROR",
message: "File does not exist",
});
return;
}

fs.unlinkSync(full_file_path);
console.log(chalk.bgRedBright.whiteBright(`Deleting file: ${full_file_path}`));

res.send({
status: "OK",
message: "File deleted",
});

}
10 changes: 5 additions & 5 deletions server/src/Controllers/Log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ import { BaseConfigDataFolder } from "../Core/BaseConfig";
import { Log } from "../Core/Log";

export function GetLog(req: express.Request, res: express.Response) {

const filename = req.params.filename;

const start_from = req.params.start_from ? parseInt(req.params.start_from) : 0;

if (!filename) {
res.status(400).send("Missing filename");
return;
}

let log_lines: ApiLogLine[] = [];

try {
log_lines = Log.fetchLog(filename, start_from) as ApiLogLine[];
} catch (error) {
Expand All @@ -34,7 +34,7 @@ export function GetLog(req: express.Request, res: express.Response) {
}

const line_num = log_lines.length;

const logfiles = fs.readdirSync(BaseConfigDataFolder.logs).filter(f => f.endsWith(".jsonline")).map(f => f.replace(".log.jsonline", ""));

res.send({
Expand Down
17 changes: 16 additions & 1 deletion server/src/Routes/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import * as Debug from "../Controllers/Debug";
import * as Favourites from "../Controllers/Favourites";
import * as Notifications from "../Controllers/Notifications";
import * as Tools from "../Controllers/Tools";
import * as Files from "../Controllers/Files";
import { TwitchVOD } from "../Core/TwitchVOD";
import chalk from "chalk";

const router = express.Router();

Expand Down Expand Up @@ -89,8 +91,21 @@ router.post("/tools/reset_channels", Tools.ResetChannels);
router.post("/tools/vod_download", Tools.DownloadVod);
router.post("/tools/chat_download", Tools.DownloadChat);

if (process.env.TCD_ENABLE_FILES_API) {
router.get("/files", Files.ListFiles);
router.delete("/files", Files.DeleteFile);
console.log(chalk.bgRedBright.whiteBright("Files API enabled"));
} else {
router.get("/files", (req, res) => {
res.status(404).send({ status: "ERROR", message: "Files API is disabled on this server. Enable with the TCD_ENABLE_FILES_API environment variable." });
});
router.delete("/files", (req, res) => {
res.status(404).send({ status: "ERROR", message: "Files API is disabled on this server. Enable with the TCD_ENABLE_FILES_API environment variable." });
});
}

router.get("/test_video_download", (req, res) => {
if (!req.query.video_id){
if (!req.query.video_id) {
res.send("Missing video_id");
return;
}
Expand Down
Loading

0 comments on commit 7c2c871

Please sign in to comment.