Compare commits
14 Commits
a3ce8c8d60
...
f14e2c4781
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f14e2c4781 | ||
|
|
92e02a821c | ||
|
|
d8873e08f2 | ||
|
|
1dd9f4e38f | ||
|
|
383a5d8265 | ||
|
|
93aee21007 | ||
|
|
7a583ec1d6 | ||
|
|
79c438aa3a | ||
|
|
60e18685a3 | ||
|
|
d7ad796bd8 | ||
|
|
7fee0d0a58 | ||
|
|
030b67844f | ||
|
|
b31626dce9 | ||
|
|
4cd88c6505 |
18
.github/workflows/deploy.yml
vendored
18
.github/workflows/deploy.yml
vendored
@ -36,21 +36,3 @@ jobs:
|
||||
file: ./backend/Dockerfile
|
||||
push: true
|
||||
tags: azpect3120/file.gophernest.backend:latest
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@v1.2.1
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
password: ${{ secrets.SERVER_PASSWORD }}
|
||||
port: 22
|
||||
script: |
|
||||
docker-compose -f /home/azpect/Applications/file.gophernest/docker-compose.yml down
|
||||
docker-compose -f /home/azpect/Applications/file.gophernest/docker-compose.yml pull
|
||||
docker-compose -f /home/azpect/Applications/file.gophernest/docker-compose.yml up -d
|
||||
env:
|
||||
FILE_GOPHERNEST_USER: ${{ secrets.FILE_GOPHERNEST_USER }}
|
||||
FILE_GOPHERNEST_PASSWORD: ${{ secrets.FILE_GOPHERNEST_PASSWORD }}
|
||||
FILE_GOPHERNEST_JWT_SECRET: ${{ secrets.FILE_GOPHERNEST_JWT_SECRET }}
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@ -1,12 +1,14 @@
|
||||
# Future Plans
|
||||
|
||||
- [ ] Move files and directories
|
||||
- [ ] Delete files and directories
|
||||
- [x] Delete files and directories
|
||||
- [ ] Undo deleted files (using tmp files maybe, may require a DB)
|
||||
- [ ] Rename files
|
||||
- [ ] Dark/light mode toggle
|
||||
- [ ] Image display (will require some kind of file server)
|
||||
- [ ] Markdown rendering (quite a reach)
|
||||
- [x] Mobile friendly
|
||||
- [ ] Fix selection bug
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@ -1,33 +1,35 @@
|
||||
import express, {Express, Request, Response, Router} from "express";
|
||||
import {Healthcheck} from "./healthcheck";
|
||||
import {printEndpoints, validateHash} from "./utils";
|
||||
import {LogRequestMiddleware} from "./log";
|
||||
import express, { Express, Request, Response, Router } from "express";
|
||||
import { Healthcheck } from "./healthcheck";
|
||||
import { printEndpoints, validateHash } from "./utils";
|
||||
import { LogRequestMiddleware } from "./log";
|
||||
import * as fs from "node:fs";
|
||||
import {entry} from "./entry";
|
||||
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { entry } from "./entry";
|
||||
import cors from "cors";
|
||||
import archiver from "archiver";
|
||||
import {appendDirectoryToArchive, appendFileToArchive} from "./download";
|
||||
import { appendDirectoryToArchive, appendFileToArchive } from "./download";
|
||||
import path from "node:path";
|
||||
import {verifyToken} from "./authenicate";
|
||||
import { verifyToken } from "./authenicate";
|
||||
import jwt from "jsonwebtoken";
|
||||
import {config} from "dotenv";
|
||||
import { config } from "dotenv";
|
||||
import Multer from "multer";
|
||||
import {mkdirSync, readFileSync, rmSync, writeFileSync} from "fs";
|
||||
|
||||
/**
|
||||
* App details
|
||||
*/
|
||||
const PORT = 5000;
|
||||
const APP: Express = express();
|
||||
const ROOT: string = "/home/azpect";
|
||||
// TODO: BACK TO NORMAL PATH
|
||||
const ROOT: string = "/media/vault";
|
||||
// const ROOT: string = "/home/azpect";
|
||||
|
||||
/**
|
||||
* Configure the .env file, this is for testing only, should be ignored in production.
|
||||
*/
|
||||
try {
|
||||
config({path: ".env"});
|
||||
config({ path: ".env" });
|
||||
} catch (error) {
|
||||
console.error(`Could not load the .env file. If this is a production environment, this is normal!`);
|
||||
console.error(`Could not load the .env file. If this is a production environment, this is normal!`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,18 +41,28 @@ const INVALID_EXTS: string[] = ["exe", "dll", "obj", "lib", "bin", "dat", "pdf",
|
||||
* Configure cors, this should work for both production and development.
|
||||
*/
|
||||
const corsOptions: cors.CorsOptions = {
|
||||
origin: ["http://localhost:3100", "https://file.gophernest.net"],
|
||||
methods: ["GET", "POST"]
|
||||
origin: ["http://localhost:3100", "https://file.gophernest.net"],
|
||||
methods: ["GET", "POST"]
|
||||
};
|
||||
APP.use(cors(corsOptions));
|
||||
|
||||
/**
|
||||
* File upload settings
|
||||
*/
|
||||
const upload = Multer({
|
||||
dest: "tmp/",
|
||||
limits: {
|
||||
fileSize: 1024 * 1024 * 100
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Apply middleware, this must be done before the routes are created.
|
||||
*/
|
||||
APP.use(verifyToken);
|
||||
APP.use(LogRequestMiddleware);
|
||||
APP.use(express.json());
|
||||
APP.use(express.urlencoded({extended: true}));
|
||||
APP.use(express.urlencoded({ extended: true }));
|
||||
|
||||
/**
|
||||
* Create routes for modular routing
|
||||
@ -61,140 +73,140 @@ const v1: Router = express.Router();
|
||||
* Return a healthcheck interface loaded with the server's health
|
||||
*/
|
||||
v1.get("/healthcheck", (req: Request, res: Response): void => {
|
||||
let hc: Healthcheck = {
|
||||
health: "Server is in bad health",
|
||||
errors: ["The root directory could not be found."],
|
||||
directory_found: false,
|
||||
};
|
||||
let hc: Healthcheck = {
|
||||
health: "Server is in bad health",
|
||||
errors: ["The root directory could not be found."],
|
||||
directory_found: false,
|
||||
};
|
||||
|
||||
res.status(200).json(hc);
|
||||
res.status(200).json(hc);
|
||||
});
|
||||
|
||||
/**
|
||||
* Make a log in attempt.
|
||||
*/
|
||||
v1.post("/login", (req: Request, res: Response): void => {
|
||||
// Get info from body
|
||||
const {username, password} = req.body;
|
||||
// Get info from body
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Get required info from the environment and validate
|
||||
if (process.env["FILE_GOPHERNEST_USER"] === username && validateHash(password, process.env["FILE_GOPHERNEST_PASSWORD"] as string)) {
|
||||
// Get the secret from the env
|
||||
const jwt_secret: string | undefined = process.env["FILE_GOPHERNEST_JWT_SECRET"];
|
||||
if (!jwt_secret) {
|
||||
res.status(500).json({code: 500, message: "JSON web tokens are not configured."});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the token
|
||||
const token: string = jwt.sign({username}, jwt_secret, {expiresIn: "30d"});
|
||||
res.status(200).json({code: 200, token});
|
||||
} else {
|
||||
res.status(404).json({code: 404, message: "Invalid credentials. Please try again!"});
|
||||
// Get required info from the environment and validate
|
||||
if (process.env["FILE_GOPHERNEST_USER"] === username && validateHash(password, process.env["FILE_GOPHERNEST_PASSWORD"] as string)) {
|
||||
// Get the secret from the env
|
||||
const jwt_secret: string | undefined = process.env["FILE_GOPHERNEST_JWT_SECRET"];
|
||||
if (!jwt_secret) {
|
||||
res.status(500).json({ code: 500, message: "JSON web tokens are not configured." });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the token
|
||||
const token: string = jwt.sign({ username }, jwt_secret, { expiresIn: "30d" });
|
||||
res.status(200).json({ code: 200, token });
|
||||
} else {
|
||||
res.status(404).json({ code: 404, message: "Invalid credentials. Please try again!" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the children of a directory provided in the path query.
|
||||
*/
|
||||
v1.get("/children", (req: Request, res: Response): void => {
|
||||
// Get the path, if it was not provided, use the root
|
||||
const path: string = (req.query.path || ROOT) as string;
|
||||
// Get the path, if it was not provided, use the root
|
||||
const path: string = (req.query.path || ROOT) as string;
|
||||
|
||||
// An array of names which are the children
|
||||
const children_paths: string[] = fs.readdirSync(path);
|
||||
// An array of names which are the children
|
||||
const children_paths: string[] = fs.readdirSync(path);
|
||||
|
||||
// Store a list of the children as entries
|
||||
const children: entry[] = [];
|
||||
// Store a list of the children as entries
|
||||
const children: entry[] = [];
|
||||
|
||||
for (const child of children_paths) {
|
||||
try {
|
||||
const isDir: boolean = fs.statSync(path.concat("/", child)).isDirectory();
|
||||
children.push({
|
||||
name: child,
|
||||
path: path.concat("/", child),
|
||||
directory: isDir
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
for (const child of children_paths) {
|
||||
try {
|
||||
const isDir: boolean = fs.statSync(path.concat("/", child)).isDirectory();
|
||||
children.push({
|
||||
name: child,
|
||||
path: path.concat("/", child),
|
||||
directory: isDir
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
res.status(200).json(children);
|
||||
}
|
||||
res.status(200).json(children);
|
||||
});
|
||||
|
||||
/**
|
||||
* Down a group of files provided in the body.
|
||||
*/
|
||||
v1.post("/download", (req: Request, res: Response): void => {
|
||||
// Get the files from the body
|
||||
const {filePaths} = req.body;
|
||||
// Get the files from the body
|
||||
const { filePaths } = req.body;
|
||||
|
||||
// Validate the path array
|
||||
if (!filePaths || !Array.isArray(filePaths) || filePaths.length === 0) {
|
||||
res.status(400).send({code: 400, error: 'Invalid file paths provided.'});
|
||||
return;
|
||||
// Validate the path array
|
||||
if (!filePaths || !Array.isArray(filePaths) || filePaths.length === 0) {
|
||||
res.status(400).send({ code: 400, error: 'Invalid file paths provided.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const archive: archiver.Archiver = archiver('zip', {
|
||||
zlib: { level: 9 }, // Compression leve
|
||||
});
|
||||
|
||||
// Set the file headers
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=donwloads.zip');
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
|
||||
archive.pipe(res);
|
||||
|
||||
// Add each file to the archive
|
||||
filePaths.forEach((filePath): void => {
|
||||
// This works for files, but for directories, we need to read the files
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
if (!stats.isDirectory()) {
|
||||
appendFileToArchive(filePath, archive);
|
||||
} else {
|
||||
// Call this with the name to include the name of the directory
|
||||
appendDirectoryToArchive(filePath, path.basename(filePath), archive);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error adding file to zip: ${err}`);
|
||||
}
|
||||
});
|
||||
|
||||
const archive: archiver.Archiver = archiver('zip', {
|
||||
zlib: {level: 9}, // Compression leve
|
||||
});
|
||||
// Return errors
|
||||
archive.on('error', (err): void => {
|
||||
res.status(500).send({ error: err.message });
|
||||
});
|
||||
|
||||
// Set the file headers
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=donwloads.zip');
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
|
||||
archive.pipe(res);
|
||||
|
||||
// Add each file to the archive
|
||||
filePaths.forEach((filePath): void => {
|
||||
// This works for files, but for directories, we need to read the files
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
if (!stats.isDirectory()) {
|
||||
appendFileToArchive(filePath, archive);
|
||||
} else {
|
||||
// Call this with the name to include the name of the directory
|
||||
appendDirectoryToArchive(filePath, path.basename(filePath), archive);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error adding file to zip: ${err}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Return errors
|
||||
archive.on('error', (err): void => {
|
||||
res.status(500).send({error: err.message});
|
||||
});
|
||||
|
||||
archive.finalize();
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieve the content of a file provided in the path query.
|
||||
*/
|
||||
v1.get("/content", (req: Request, res: Response): void => {
|
||||
// Get the path, if it was not provided, return no content
|
||||
const tgtPath: string = (req.query.path || "") as string;
|
||||
if (!tgtPath) {
|
||||
res.status(204);
|
||||
return;
|
||||
}
|
||||
// Get the path, if it was not provided, return no content
|
||||
const tgtPath: string = (req.query.path || "") as string;
|
||||
if (!tgtPath) {
|
||||
res.status(204);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the file isn't something we can't edit
|
||||
const ext: string = path.extname(tgtPath).slice(1);
|
||||
if (INVALID_EXTS.includes(ext)) {
|
||||
res.status(400).json({error: `Cannot edit files of type *.${ext}`, code: 400});
|
||||
return;
|
||||
}
|
||||
// Ensure the file isn't something we can't edit
|
||||
const ext: string = path.extname(tgtPath).slice(1);
|
||||
if (INVALID_EXTS.includes(ext)) {
|
||||
res.status(400).json({ error: `Cannot edit files of type *.${ext}`, code: 400 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the file and return it
|
||||
const content = fs.readFileSync(tgtPath, {encoding: "utf-8", flag: "r"})
|
||||
res.status(200).json({content, code: 200});
|
||||
} catch (err) {
|
||||
res.status(500).json({error: `An error occurred on the server. ${err}`, code: 500});
|
||||
}
|
||||
try {
|
||||
// Read the file and return it
|
||||
const content = fs.readFileSync(tgtPath, { encoding: "utf-8", flag: "r" })
|
||||
res.status(200).json({ content, code: 200 });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: `An error occurred on the server. ${err}`, code: 500 });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@ -202,105 +214,188 @@ v1.get("/content", (req: Request, res: Response): void => {
|
||||
* On success, nothing should be sent back, 204.
|
||||
*/
|
||||
v1.post("/update", (req: Request, res: Response): void => {
|
||||
// Get path and content from the request
|
||||
const {path, content} = req.body;
|
||||
// Get path and content from the request
|
||||
const { path, content } = req.body;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(path, content);
|
||||
res.status(200).json({code: 200, message: "Success"});
|
||||
} catch (error) {
|
||||
res.status(500).json({code: 500, error})
|
||||
}
|
||||
try {
|
||||
fs.writeFileSync(path, content);
|
||||
res.status(200).json({ code: 200, message: "Success" });
|
||||
} catch (error) {
|
||||
res.status(500).json({ code: 500, error })
|
||||
}
|
||||
});
|
||||
|
||||
const upload = Multer({
|
||||
dest: "tmp/",
|
||||
limits: {
|
||||
fileSize: 1024 * 1024 * 100
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Custom type for the multer uploads.
|
||||
*/
|
||||
interface UploadedFile {
|
||||
fieldname: string;
|
||||
originalname: string;
|
||||
encoding: string;
|
||||
mimetype: string;
|
||||
destination: string;
|
||||
filename: string;
|
||||
path: string;
|
||||
size: number;
|
||||
fieldname: string;
|
||||
originalname: string;
|
||||
encoding: string;
|
||||
mimetype: string;
|
||||
destination: string;
|
||||
filename: string;
|
||||
path: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
// IMPORTANT! Calling this will expect sudo in places in the FS that require sudo
|
||||
v1.post("/upload", upload.array("files"), (req: Request, res: Response) => {
|
||||
if (!req.files) {
|
||||
res.status(400);
|
||||
if (!req.files) {
|
||||
res.status(400);
|
||||
}
|
||||
|
||||
// Directory to upload the files to
|
||||
const cwd: string[] = JSON.parse(req.body.path);
|
||||
const files = (req as any).files as UploadedFile[];
|
||||
files.forEach((file) => {
|
||||
try {
|
||||
// Get the data that was written to the local tmp path
|
||||
const data: Buffer = readFileSync(file.path);
|
||||
|
||||
// Generate the new path in the FS
|
||||
const newPath: string = path.join("/", ...cwd, file.originalname);
|
||||
|
||||
// Write the new file
|
||||
writeFileSync(newPath, data, { mode: "666" });
|
||||
|
||||
// Delete the tmp file using a relative path
|
||||
rmSync("./" + file.path);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'EACCES') {
|
||||
return res.status(403).json({ code: 403, error: "Permission denied." }); // Specific error
|
||||
} else if (error.code === 'ENOSPC') {
|
||||
return res.status(507).json({ code: 507, error: "Insufficient storage." }); // Specific error
|
||||
} else if (error instanceof TypeError) {
|
||||
return res.status(400).json({ code: 400, error: "Invalid data type." }); // Example of instance check
|
||||
} else {
|
||||
return res.status(500).json({ code: 500, error: "Error processing file." }); // Generic error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Directory to upload the files to
|
||||
const cwd: string[] = JSON.parse(req.body.path);
|
||||
const files = (req as any).files as UploadedFile[];
|
||||
files.forEach((file) => {
|
||||
try {
|
||||
// Get the data that was written to the local tmp path
|
||||
const data: Buffer = readFileSync(file.path);
|
||||
|
||||
// Generate the new path in the FS
|
||||
const newPath: string = path.join("/", ...cwd, file.originalname);
|
||||
|
||||
// Write the new file
|
||||
writeFileSync(newPath, data);
|
||||
|
||||
// Delete the tmp file using a relative path
|
||||
rmSync("./" + file.path);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'EACCES') {
|
||||
return res.status(403).json({code: 403, error: "Permission denied."}); // Specific error
|
||||
} else if (error.code === 'ENOSPC') {
|
||||
return res.status(507).json({code: 507, error: "Insufficient storage."}); // Specific error
|
||||
} else if (error instanceof TypeError) {
|
||||
return res.status(400).json({code: 400, error: "Invalid data type."}); // Example of instance check
|
||||
} else {
|
||||
return res.status(500).json({code: 500, error: "Error processing file."}); // Generic error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
res.status(200).json({code: 200, message: "Success"});
|
||||
res.status(200).json({ code: 200, message: "Success" });
|
||||
});
|
||||
|
||||
v1.post("/create", (req: Request, res: Response): void => {
|
||||
// Generate the path to create
|
||||
const {cwd, name} = req.body;
|
||||
// Generate the path to create
|
||||
const { cwd, name } = req.body;
|
||||
|
||||
try {
|
||||
const newPath: string = path.join("/", ...cwd, name);
|
||||
if (name.endsWith("/")) {
|
||||
mkdirSync(newPath, { recursive: true, mode: "666" })
|
||||
} else {
|
||||
writeFileSync(newPath, "", { mode: "666" })
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === 'EACCES') {
|
||||
res.status(403).json({ code: 403, error: "Permission denied." }); // Specific error
|
||||
return;
|
||||
} else if (error.code === 'ENOSPC') {
|
||||
res.status(507).json({ code: 507, error: "Insufficient storage." }); // Specific error
|
||||
return;
|
||||
} else if (error instanceof TypeError) {
|
||||
res.status(400).json({ code: 400, error: "Invalid data type." }); // Example of instance check
|
||||
return;
|
||||
} else {
|
||||
res.status(500).json({ code: 500, error: "Error processing directory." }); // Generic error
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({ code: 201, message: "Success" });
|
||||
});
|
||||
|
||||
v1.post("/delete", (req: Request, res: Response): void => {
|
||||
// Get the array of paths and the root
|
||||
const { files, root } = req.body;
|
||||
|
||||
// Delete the files
|
||||
// NOTE: These are not moved somewhere, they're just raw deleted
|
||||
for (const file of files) {
|
||||
const absPath = "/" + path.join(...file);
|
||||
rmSync(absPath, { recursive: true });
|
||||
}
|
||||
|
||||
res.status(201).json({ code: 201, message: `Deleted ${files.length} files.` });
|
||||
});
|
||||
|
||||
/**
|
||||
* Files are not deleted from the file system, just moved to a hidden folder where
|
||||
* they can be recovered if needed.
|
||||
*
|
||||
* The hidden folder will be called `.trash` stored in the root of the mount, and
|
||||
* the files entire paths will be created.
|
||||
*
|
||||
* e.g., Deleting /media/vault/main.txt will get moved to
|
||||
* `/media/vault/.trash/<timstamp>/media/vault/main.txt`
|
||||
* assuming /media/vault is the mounted root.
|
||||
* The timestamp will also be appended to allow files with the same path to be deleted.
|
||||
*/
|
||||
v1.post("/remove", (req: Request, res: Response): void => {
|
||||
// Get the array of paths and the root
|
||||
const { files, root } = req.body;
|
||||
|
||||
// Stores the name of the trash directory
|
||||
const trashDir: string = ".trash";
|
||||
const timestamp: string = (new Date()).toISOString();
|
||||
|
||||
console.log(timestamp);
|
||||
|
||||
for (const file of files) {
|
||||
const oldPath = path.join("/", ...file);
|
||||
const newPath = path.join("/", ...root, trashDir, timestamp, ...file);
|
||||
|
||||
console.log(oldPath, " -> ", newPath);
|
||||
try {
|
||||
const newPath: string = path.join("/", ...cwd, name);
|
||||
if (name.endsWith("/")) {
|
||||
mkdirSync(newPath, {mode: "644"})
|
||||
} else {
|
||||
console.log("NOT DIR");
|
||||
writeFileSync(newPath, "", {mode: "644"})
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === 'EACCES') {
|
||||
res.status(403).json({code: 403, error: "Permission denied."}); // Specific error
|
||||
return;
|
||||
} else if (error.code === 'ENOSPC') {
|
||||
res.status(507).json({code: 507, error: "Insufficient storage."}); // Specific error
|
||||
return;
|
||||
} else if (error instanceof TypeError) {
|
||||
res.status(400).json({code: 400, error: "Invalid data type."}); // Example of instance check
|
||||
return;
|
||||
} else {
|
||||
res.status(500).json({code: 500, error: "Error processing directory."}); // Generic error
|
||||
return;
|
||||
}
|
||||
mkdirSync(path.dirname(newPath), { recursive: true, mode: "666" });
|
||||
renameSync(oldPath, newPath)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ code: 500, message: `Failed to delete. ${error}` })
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({ code: 201, message: `Deleted ${files.length} files.` });
|
||||
});
|
||||
|
||||
v1.post("/move", (req: Request, res: Response): void => {
|
||||
// Get the array of paths and the root
|
||||
const { oldPath, newPath } = req.body;
|
||||
|
||||
try {
|
||||
// Get path of new path
|
||||
const newDirectory = path.dirname(newPath);
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!existsSync(newDirectory)) {
|
||||
mkdirSync(newDirectory, { recursive: true });
|
||||
console.log(`Created directory: ${newDirectory}`);
|
||||
}
|
||||
|
||||
res.status(201).json({code: 201, message: "Success"});
|
||||
// Move the file
|
||||
renameSync(oldPath, newPath);
|
||||
console.log(`File moved from ${oldPath} to ${newPath}`);
|
||||
|
||||
res.status(200).json({ code: 200 });
|
||||
} catch (err: any) {
|
||||
console.error("Error moving file:", err);
|
||||
// You might want to send a more specific error message based on 'err.code'
|
||||
if (err.code === 'EXDEV') {
|
||||
// This specific error means moving across different file systems.
|
||||
// Synchronous fallback would be more complex (read/write stream synchronously),
|
||||
// which is why async is preferred here. For synchronous, you'd generally
|
||||
// just fail or implement a blocking copy/delete.
|
||||
res.status(500).json({ code: 500, error: "Error: Cannot move across different file systems synchronously. Try copying and deleting." });
|
||||
} else if (err.code === 'ENOENT') {
|
||||
res.status(404).json({ code: 404, error: "Error: Source file or destination path not found." });
|
||||
} else {
|
||||
res.status(500).json({ code: 500, error: `Failed to move file: ${err.message}` });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@ -312,6 +407,6 @@ APP.use("/v1", v1);
|
||||
* Start the server
|
||||
*/
|
||||
APP.listen(PORT, (): void => {
|
||||
printEndpoints(APP);
|
||||
console.log(`Server listening on :${PORT}`);
|
||||
printEndpoints(APP);
|
||||
console.log(`Server listening on :${PORT}`);
|
||||
});
|
||||
|
||||
@ -17,7 +17,8 @@ services:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
# TODO: This will need to be configured, need to rebuild part of the program for that though
|
||||
- /home/azpect/Documents:/home/azpect/Documents
|
||||
- /media/vault:/media/vault
|
||||
|
||||
environment:
|
||||
FILE_GOPHERNEST_USER: ${FILE_GOPHERNEST_USER}
|
||||
FILE_GOPHERNEST_PASSWORD: ${FILE_GOPHERNEST_PASSWORD}
|
||||
|
||||
@ -6,12 +6,12 @@ import "../index.css"
|
||||
* @constructor
|
||||
*/
|
||||
export default function ChildrenLoading() {
|
||||
return (
|
||||
<div className="size-full flex items-center justify-center">
|
||||
<div
|
||||
className="animate-spin rounded-full border-blue-500 border-3 border-t-transparent size-6 mx-2 ">
|
||||
</div>
|
||||
<p className="text-lg text-black opacity-90">Content loading...</p>
|
||||
</div>
|
||||
);
|
||||
return <>
|
||||
<div className="size-full flex items-center justify-center">
|
||||
<div
|
||||
className="animate-spin rounded-full border-blue-500 border-3 border-t-transparent size-6 mx-2 ">
|
||||
</div>
|
||||
<p className="text-lg text-black opacity-90">Content loading...</p>
|
||||
</div>
|
||||
</>
|
||||
};
|
||||
@ -6,15 +6,15 @@ import "../index.css"
|
||||
* @constructor
|
||||
*/
|
||||
export default function ContentLoading() {
|
||||
return (
|
||||
<div className="h-9/10 w-full flex flex-col items-center justify-center">
|
||||
<div className="flex">
|
||||
<div
|
||||
className="animate-spin rounded-full border-blue-500 border-3 border-t-transparent size-6 mr-2 ">
|
||||
</div>
|
||||
<p className="text-lg text-black opacity-90">Content loading...</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 my-2">For large files, this may take a while.</p>
|
||||
return <>
|
||||
<div className="h-9/10 w-full flex flex-col items-center justify-center">
|
||||
<div className="flex">
|
||||
<div
|
||||
className="animate-spin rounded-full border-blue-500 border-3 border-t-transparent size-6 mr-2 ">
|
||||
</div>
|
||||
);
|
||||
<p className="text-lg text-black opacity-90">Content loading...</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 my-2">For large files, this may take a while.</p>
|
||||
</div>
|
||||
</>
|
||||
};
|
||||
|
||||
@ -5,51 +5,50 @@ import {useState} from "react";
|
||||
* @constructor
|
||||
*/
|
||||
export default function CreateDirectory({close, create}) {
|
||||
const [dirName, setDirName] = useState("");
|
||||
const [dirName, setDirName] = useState("");
|
||||
|
||||
const createDirectory = () => {
|
||||
create(dirName);
|
||||
}
|
||||
const createDirectory = () => create(dirName);
|
||||
const updateDirName = (e) => setDirName(e.target.value);
|
||||
|
||||
const updateDirName = (e) => {
|
||||
setDirName(e.target.value);
|
||||
}
|
||||
const closeWithoutSaving = () => {
|
||||
setDirName("");
|
||||
close();
|
||||
}
|
||||
|
||||
const closeWithoutSaving = () => {
|
||||
setDirName("");
|
||||
close();
|
||||
}
|
||||
return <>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Blured backdrop */}
|
||||
<div className="fixed -inset-10 bg-black opacity-50 blur-lg"></div>
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed -inset-10 bg-black opacity-50 blur-lg"></div>
|
||||
<div className="relative z-50 bg-white p-8 rounded-lg shadow-lg w-2/5 border-1 border-gray-400">
|
||||
<h2 className="text-2xl font-semibold mb-2 text-blue-400">Create a Directory</h2>
|
||||
<p className="text-sm">
|
||||
Create a file or directory in the current directory. To create a directory, include a <span
|
||||
className="font-mono text-black bg-gray-300 p-1 rounded-md">/</span> at the end of the name.
|
||||
Otherwise, the entry created will be a file.
|
||||
</p>
|
||||
<div className="relative z-50 bg-white p-8 rounded-lg shadow-lg w-9/10 lg:w-2/5 border-1 border-gray-400">
|
||||
<h2 className="text-2xl font-semibold mb-2 text-blue-400">
|
||||
Create a Directory
|
||||
</h2>
|
||||
<p className="text-sm">
|
||||
Create a file or directory in the current directory. To create a directory, include a <span
|
||||
className="font-mono text-black bg-gray-300 p-1 rounded-md">/</span> at the end of the name.
|
||||
Otherwise, the entry created will be a file.
|
||||
</p>
|
||||
|
||||
<input className="border-b-2 border-blue-400 w-full p-2 my-4" type="text" name="directoryName"
|
||||
value={dirName}
|
||||
onInput={updateDirName}
|
||||
placeholder="Directory name"/>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={closeWithoutSaving}
|
||||
title="Close without creating"
|
||||
className="bg-red-500 hover:bg-red-600 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 mx-2 rounded hover:cursor-pointer">
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={createDirectory}
|
||||
title="Create directory"
|
||||
className="bg-blue-400 hover:bg-blue-500 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 rounded hover:cursor-pointer">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input className="border-b-2 border-blue-400 w-full p-2 my-4" type="text" name="directoryName"
|
||||
value={dirName}
|
||||
onInput={updateDirName}
|
||||
placeholder="Directory name"/>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={closeWithoutSaving}
|
||||
title="Close without creating"
|
||||
className="bg-red-500 hover:bg-red-600 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 mx-2 rounded hover:cursor-pointer">
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={createDirectory}
|
||||
title="Create directory"
|
||||
className="bg-blue-400 hover:bg-blue-500 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 rounded hover:cursor-pointer">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -1,22 +1,26 @@
|
||||
import "../index.css"
|
||||
import {useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function FileIcon() {
|
||||
return <svg className="text-black h-5 mx-2" stroke="currentColor" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19 9V17.8C19 18.9201 19 19.4802 18.782 19.908C18.5903 20.2843 18.2843 20.5903 17.908 20.782C17.4802 21 16.9201 21 15.8 21H8.2C7.07989 21 6.51984 21 6.09202 20.782C5.71569 20.5903 5.40973 20.2843 5.21799 19.908C5 19.4802 5 18.9201 5 17.8V6.2C5 5.07989 5 4.51984 5.21799 4.09202C5.40973 3.71569 5.71569 3.40973 6.09202 3.21799C6.51984 3 7.0799 3 8.2 3H13M19 9L13 3M19 9H14C13.4477 9 13 8.55228 13 8V3"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
return <>
|
||||
<svg className="text-black h-5 mx-2" stroke="currentColor" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19 9V17.8C19 18.9201 19 19.4802 18.782 19.908C18.5903 20.2843 18.2843 20.5903 17.908 20.782C17.4802 21 16.9201 21 15.8 21H8.2C7.07989 21 6.51984 21 6.09202 20.782C5.71569 20.5903 5.40973 20.2843 5.21799 19.908C5 19.4802 5 18.9201 5 17.8V6.2C5 5.07989 5 4.51984 5.21799 4.09202C5.40973 3.71569 5.71569 3.40973 6.09202 3.21799C6.51984 3 7.0799 3 8.2 3H13M19 9L13 3M19 9H14C13.4477 9 13 8.55228 13 8V3"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</>
|
||||
}
|
||||
|
||||
function DirectoryIcon() {
|
||||
return <svg className="text-black h-5 mx-2" stroke="currentColor" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 8.2C3 7.07989 3 6.51984 3.21799 6.09202C3.40973 5.71569 3.71569 5.40973 4.09202 5.21799C4.51984 5 5.0799 5 6.2 5H9.67452C10.1637 5 10.4083 5 10.6385 5.05526C10.8425 5.10425 11.0376 5.18506 11.2166 5.29472C11.4184 5.4184 11.5914 5.59135 11.9373 5.93726L12.0627 6.06274C12.4086 6.40865 12.5816 6.5816 12.7834 6.70528C12.9624 6.81494 13.1575 6.89575 13.3615 6.94474C13.5917 7 13.8363 7 14.3255 7H17.8C18.9201 7 19.4802 7 19.908 7.21799C20.2843 7.40973 20.5903 7.71569 20.782 8.09202C21 8.51984 21 9.0799 21 10.2V15.8C21 16.9201 21 17.4802 20.782 17.908C20.5903 18.2843 20.2843 18.5903 19.908 18.782C19.4802 19 18.9201 19 17.8 19H6.2C5.07989 19 4.51984 19 4.09202 18.782C3.71569 18.5903 3.40973 18.2843 3.21799 17.908C3 17.4802 3 16.9201 3 15.8V8.2Z"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
return <>
|
||||
<svg className="text-black h-5 mx-2" stroke="currentColor" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 8.2C3 7.07989 3 6.51984 3.21799 6.09202C3.40973 5.71569 3.71569 5.40973 4.09202 5.21799C4.51984 5 5.0799 5 6.2 5H9.67452C10.1637 5 10.4083 5 10.6385 5.05526C10.8425 5.10425 11.0376 5.18506 11.2166 5.29472C11.4184 5.4184 11.5914 5.59135 11.9373 5.93726L12.0627 6.06274C12.4086 6.40865 12.5816 6.5816 12.7834 6.70528C12.9624 6.81494 13.1575 6.89575 13.3615 6.94474C13.5917 7 13.8363 7 14.3255 7H17.8C18.9201 7 19.4802 7 19.908 7.21799C20.2843 7.40973 20.5903 7.71569 20.782 8.09202C21 8.51984 21 9.0799 21 10.2V15.8C21 16.9201 21 17.4802 20.782 17.908C20.5903 18.2843 20.2843 18.5903 19.908 18.782C19.4802 19 18.9201 19 17.8 19H6.2C5.07989 19 4.51984 19 4.09202 18.782C3.71569 18.5903 3.40973 18.2843 3.21799 17.908C3 17.4802 3 16.9201 3 15.8V8.2Z"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,40 +32,47 @@ function DirectoryIcon() {
|
||||
* @returns {{}}
|
||||
* @constructor
|
||||
*/
|
||||
export default function Directory({entry, showHidden, appendPath, toggleSelected, toggleEditing}) {
|
||||
const [selected, setSelected] = useState(false);
|
||||
export default function Directory({ entry, selected, showHidden, appendPath, toggleSelected, toggleEditing }) {
|
||||
const [_selected, setSelected] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (entry.directory) {
|
||||
appendPath(entry.name);
|
||||
} else {
|
||||
toggleEditing(entry.path);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
setSelected(selected);
|
||||
}, [selected]);
|
||||
|
||||
const handleCheck = () => {
|
||||
toggleSelected(entry.name);
|
||||
setSelected(!selected);
|
||||
const handleClick = () => {
|
||||
if (entry.directory) {
|
||||
appendPath(entry.name);
|
||||
} else {
|
||||
toggleEditing(entry.path);
|
||||
}
|
||||
};
|
||||
|
||||
// This is temporary, eventually I will have a real data model that stores
|
||||
// directory vs file status.
|
||||
// const isDirectory = !name.endsWith(".html");
|
||||
if (entry.name.startsWith(".") && !showHidden) {
|
||||
return <></>;
|
||||
}
|
||||
const handleCheck = () => {
|
||||
toggleSelected(entry.name);
|
||||
setSelected(!_selected);
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={`w-full hover:bg-gray-300 flex ${selected ? "bg-blue-300" : "bg-gray-100"}`}>
|
||||
<input className="mx-2 peer checked:bg-blue-300" type="checkbox" checked={selected}
|
||||
onChange={handleCheck}/>
|
||||
<div className="w-full flex bg-gray-100 peer-checked:bg-blue-300 hover:bg-gray-300 peer-hover:bg-gray-300">
|
||||
<button className="flex items-center" onClick={handleClick}>
|
||||
{entry.directory ? <DirectoryIcon/> : <FileIcon/>}
|
||||
<p className="p-2 hover:underline hover:text-blue-400">{entry.name}{entry.directory ? "/" : ""}</p>
|
||||
</button>
|
||||
</div>
|
||||
// This is temporary, eventually I will have a real data model that stores
|
||||
// directory vs file status.
|
||||
// const isDirectory = !name.endsWith(".html");
|
||||
if (entry.name.startsWith(".") && !showHidden) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
</label>
|
||||
);
|
||||
return <>
|
||||
<label className={`w-full hover:bg-gray-300 flex ${_selected ? "bg-blue-300" : "bg-gray-100"}`}>
|
||||
<input
|
||||
className="mx-2 peer checked:bg-blue-300"
|
||||
type="checkbox"
|
||||
checked={_selected}
|
||||
onChange={handleCheck} />
|
||||
<div className="w-full flex bg-gray-100 peer-checked:bg-blue-300 hover:bg-gray-300 peer-hover:bg-gray-300">
|
||||
<button className="flex items-center" onClick={handleClick}>
|
||||
{entry.directory ? <DirectoryIcon /> : <FileIcon />}
|
||||
<p className="p-2 hover:underline hover:text-blue-400">{entry.name}{entry.directory ? "/" : ""}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</label>
|
||||
</>
|
||||
}
|
||||
@ -2,19 +2,25 @@ import Directory from "./Directory.jsx";
|
||||
|
||||
/**
|
||||
* Display the directories in the current path.
|
||||
* @param diretories {string[]} Children of the path.
|
||||
* @param showHidden {boolean} Display hidden items.
|
||||
* @param appendPath {function(string)} Function to add a child to the path.
|
||||
* @param toggleSelected {function(string)} Function to toggle selection status.
|
||||
* @param diretories {string[]} - Children of the path.
|
||||
* @param showHidden {boolean} - Display hidden items.
|
||||
* @param appendPath {function(string)} - Function to add a child to the path.
|
||||
* @param toggleSelected {function(string)} - Function to toggle selection status.
|
||||
* @param selected {string[]} - List of selected names.
|
||||
* @constructor
|
||||
*/
|
||||
export default function DirectoryList({dirs, showHidden, appendPath, toggleSelected, toggleEditing}) {
|
||||
return (
|
||||
<>
|
||||
{dirs.map((dir, idx) => <Directory key={idx} entry={dir} showHidden={showHidden} appendPath={appendPath}
|
||||
toggleSelected={toggleSelected} toggleEditing={toggleEditing}/>)}
|
||||
</>
|
||||
)
|
||||
|
||||
export default function DirectoryList({ dirs, showHidden, appendPath, toggleSelected, toggleEditing, selected }) {
|
||||
|
||||
return <>
|
||||
{dirs.map((dir, idx) =>
|
||||
<Directory
|
||||
key={idx}
|
||||
entry={dir}
|
||||
selected={selected.includes(dir.name)}
|
||||
showHidden={showHidden}
|
||||
appendPath={appendPath}
|
||||
toggleSelected={toggleSelected}
|
||||
toggleEditing={toggleEditing} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@ -6,13 +6,13 @@ import "../index.css"
|
||||
* @constructor
|
||||
*/
|
||||
export default function DownloadLoading() {
|
||||
return (
|
||||
<div className="absolute size-full flex items-center justify-center">
|
||||
<div className="fixed inset-0 bg-black opacity-25 blur-lg"></div>
|
||||
<div
|
||||
className="animate-spin rounded-full border-blue-500 border-3 border-t-transparent size-6 mx-2">
|
||||
</div>
|
||||
<p className="text-lg text-black opacity-90">Preparing files...</p>
|
||||
</div>
|
||||
);
|
||||
return <>
|
||||
<div className="absolute size-full flex items-center justify-center">
|
||||
<div className="fixed inset-0 bg-black opacity-25 blur-lg"></div>
|
||||
<div
|
||||
className="animate-spin rounded-full border-blue-500 border-3 border-t-transparent size-6 mx-2">
|
||||
</div>
|
||||
<p className="text-lg text-black opacity-90">Preparing files...</p>
|
||||
</div>
|
||||
</>
|
||||
};
|
||||
|
||||
@ -1,86 +1,87 @@
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ContentLoading from "./ContentLoading.jsx";
|
||||
|
||||
export default function Editor({content, path, exit, saveExit, loading}) {
|
||||
const [text, setText] = useState("");
|
||||
/**
|
||||
export default function Editor({ content, path, exit, saveExit, loading }) {
|
||||
const [text, setText] = useState("");
|
||||
/**
|
||||
* Store a reference to the text area object
|
||||
* @type {React.RefObject<null>}
|
||||
*/
|
||||
const textareaRef = useRef(null);
|
||||
const textareaRef = useRef(null);
|
||||
|
||||
const updateText = (event) => {
|
||||
setText(event.target.value);
|
||||
};
|
||||
const updateText = (event) => setText(event.target.value);
|
||||
|
||||
useEffect(() => {
|
||||
setText(content);
|
||||
}, [content])
|
||||
useEffect(() => {
|
||||
setText(content);
|
||||
}, [content])
|
||||
|
||||
const handleKeyPress = (event) => {
|
||||
// Override tab changing focus
|
||||
// Uses 2 space indents
|
||||
// TODO: Allow toggle for two and four space indents
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
const handleKeyPress = (event) => {
|
||||
// Override tab changing focus
|
||||
// Uses 2 space indents
|
||||
// TODO: Allow toggle for two and four space indents
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const value = textarea.value;
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const value = textarea.value;
|
||||
|
||||
// Insert tab character
|
||||
// Update textarea value and cursor position
|
||||
textarea.value = value.substring(0, start) + " " + value.substring(end);
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 2; // Move the cursor after the tab
|
||||
// Insert tab character
|
||||
// Update textarea value and cursor position
|
||||
textarea.value = value.substring(0, start) + " " + value.substring(end);
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 2; // Move the cursor after the tab
|
||||
|
||||
// Trigger a change event so React knows the value changed.
|
||||
textarea.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
}
|
||||
};
|
||||
// Trigger a change event so React knows the value changed.
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
/**
|
||||
* Call the parent function with the new content which is
|
||||
* stored in the text state.
|
||||
*/
|
||||
const saveAndExit = () => {
|
||||
saveExit(text);
|
||||
};
|
||||
const saveAndExit = () => saveExit(text);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed inset-0 bg-black opacity-50 blur-lg"></div>
|
||||
<div className="relative z-10 bg-white p-8 rounded-lg shadow-lg w-3/4 h-5/6 border-1 border-gray-400">
|
||||
<h2 className="text-lg font-semibold mb-4 text-blue-400">Editing File: <span
|
||||
className="font-mono text-black bg-gray-300 p-1 rounded-md">{path}</span></h2>
|
||||
{loading && <ContentLoading/>}
|
||||
{loading ||
|
||||
<textarea
|
||||
onKeyDown={handleKeyPress}
|
||||
tabIndex={-1}
|
||||
ref={textareaRef}
|
||||
onInput={updateText}
|
||||
value={text}
|
||||
className="border-1 border-gray-300 rounded-md w-full h-9/10 p-1 resize-none text-sm font-mono">
|
||||
</textarea>
|
||||
}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
title="Exit without saving"
|
||||
onClick={exit}
|
||||
className="bg-red-500 hover:bg-red-600 text-white text-sm font-semibold py-1.5 px-3 rounded hover:cursor-pointer mx-2">
|
||||
Exit
|
||||
</button>
|
||||
<button
|
||||
title="Save changes and exit"
|
||||
onClick={saveAndExit}
|
||||
className="bg-blue-400 hover:bg-blue-500 text-white text-sm font-semibold py-1.5 px-3 rounded hover:cursor-pointer">
|
||||
Save & Exit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
return <>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Blured backdrop */}
|
||||
<div className="fixed inset-0 bg-black opacity-50 blur-lg"></div>
|
||||
|
||||
<div className="w-9/10 lg:w-3/4 h-5/6 relative z-10 bg-white p-4 lg:p-8 rounded-lg shadow-lg border-1 border-gray-400 flex flex-col">
|
||||
<h2 className="text-lg font-semibold mb-4 text-blue-400">
|
||||
Editing File: <span className="font-mono text-black bg-gray-300 p-1 rounded-md">
|
||||
{path}
|
||||
</span>
|
||||
</h2>
|
||||
{loading && <ContentLoading />}
|
||||
{loading ||
|
||||
<textarea
|
||||
onKeyDown={handleKeyPress}
|
||||
tabIndex={-1}
|
||||
ref={textareaRef}
|
||||
onInput={updateText}
|
||||
value={text}
|
||||
className="border-1 border-gray-300 rounded-md w-full flex-grow p-1 resize-none text-sm font-mono">
|
||||
</textarea>
|
||||
}
|
||||
<div className="flex justify-end mt-4">
|
||||
<button
|
||||
title="Exit without saving"
|
||||
onClick={exit}
|
||||
className="bg-red-500 hover:bg-red-600 text-white text-sm font-semibold py-1.5 px-3 rounded hover:cursor-pointer mx-2">
|
||||
Exit
|
||||
</button>
|
||||
<button
|
||||
title="Save changes and exit"
|
||||
onClick={saveAndExit}
|
||||
className="bg-blue-400 hover:bg-blue-500 text-white text-sm font-semibold py-1.5 px-3 rounded hover:cursor-pointer">
|
||||
Save & Exit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -8,21 +8,21 @@ import "../index.css";
|
||||
* @constructor
|
||||
*/
|
||||
export default function Error({error, clear}) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed inset-0 bg-black opacity-50 blur-lg"></div>
|
||||
<div className="relative z-50 bg-white p-8 rounded-lg shadow-lg w-96 border-1 border-gray-400">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-red-600">An Error Occurred!</h2>
|
||||
<p className="mb-4">{error}</p>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={clear}
|
||||
className="bg-red-500 hover:bg-red-600 text-white text-sm font-semibold py-1.5 px-3 rounded hover:cursor-pointer"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
return <>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed inset-0 bg-black opacity-50 blur-lg"></div>
|
||||
<div className="relative z-50 bg-white p-8 rounded-lg shadow-lg w-96 border-1 border-gray-400">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-red-600">An Error Occurred!</h2>
|
||||
<p className="mb-4">{error}</p>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={clear}
|
||||
className="bg-red-500 hover:bg-red-600 text-white text-sm font-semibold py-1.5 px-3 rounded hover:cursor-pointer"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -1,132 +1,134 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import UserInput from "./UserInput.jsx";
|
||||
import PasswordInput from "./PasswordInput.jsx";
|
||||
import RememberMe from "./RememberMe.jsx";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
||||
export default function LoginForm() {
|
||||
/**
|
||||
* URL To the backend web server.
|
||||
* Uses the .env var in local development, but when
|
||||
* pushed to dockerhub, the .env is ignored and the real
|
||||
* backend URL is used.
|
||||
* @type {string}
|
||||
*/
|
||||
const backendUrl = import.meta.env.VITE_BACKEND_URL || "https://backend.gophernest.net";
|
||||
/**
|
||||
* URL To the backend web server.
|
||||
* Uses the .env var in local development, but when
|
||||
* pushed to dockerhub, the .env is ignored and the real
|
||||
* backend URL is used.
|
||||
* @type {string}
|
||||
*/
|
||||
const backendUrl = import.meta.env.VITE_BACKEND_URL || "https://backend.gophernest.net";
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [remember, setRemember] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [remember, setRemember] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
* @type {string}
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
* @type {string}
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
|
||||
/**
|
||||
* Set the email in the form state.
|
||||
* @param newUsername
|
||||
*/
|
||||
const updateUsername = (newUsername) => {
|
||||
setUsername(newUsername);
|
||||
/**
|
||||
* Set the email in the form state.
|
||||
* @param newUsername
|
||||
*/
|
||||
const updateUsername = (newUsername) => {
|
||||
setUsername(newUsername);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the 'remember me' in the form start.
|
||||
* @param newRemember
|
||||
*/
|
||||
const updateRemember = (newRemember) => {
|
||||
setRemember(newRemember);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the password in the form state.
|
||||
* @param newPassword
|
||||
*/
|
||||
const updatePassword = (newPassword) => {
|
||||
setPassword(newPassword);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle the login submission, data is stored in the local storage.
|
||||
* @param event {SubmitEvent}
|
||||
*/
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
const sendAuthReq = async (username, password) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json();
|
||||
console.error(data.message);
|
||||
setError(data.message);
|
||||
return data;
|
||||
}
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the 'remember me' in the form start.
|
||||
* @param newRemember
|
||||
*/
|
||||
const updateRemember = (newRemember) => {
|
||||
setRemember(newRemember);
|
||||
};
|
||||
sendAuthReq(username, password).then((data) => {
|
||||
const { code, token } = data;
|
||||
|
||||
/**
|
||||
* Set the password in the form state.
|
||||
* @param newPassword
|
||||
*/
|
||||
const updatePassword = (newPassword) => {
|
||||
setPassword(newPassword);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle the login submission, data is stored in the local storage.
|
||||
* @param event {SubmitEvent}
|
||||
*/
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
const sendAuthReq = async (username, password) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({username, password}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json();
|
||||
console.error(data.message);
|
||||
setError(data.message);
|
||||
return data;
|
||||
}
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
sendAuthReq(username, password).then((data) => {
|
||||
const {code, token} = data;
|
||||
|
||||
// Should always be 200, but just make sure it is
|
||||
if (code === 200) {
|
||||
// Store JWT in session if the user does not want to be remembered
|
||||
remember ? localStorage.setItem(storage_id, token) : sessionStorage.setItem(storage_id, token);
|
||||
navigate("/login");
|
||||
} else {
|
||||
console.error(data);
|
||||
setError(data.message);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
setError(err.toString());
|
||||
}).finally(() => {
|
||||
// Disable loading bar
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Redirect if the user is logged in
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(storage_id) != null || sessionStorage.getItem(storage_id) != null) {
|
||||
navigate("/dashboard");
|
||||
}
|
||||
// Should always be 200, but just make sure it is
|
||||
if (code === 200) {
|
||||
// Store JWT in session if the user does not want to be remembered
|
||||
remember ? localStorage.setItem(storage_id, token) : sessionStorage.setItem(storage_id, token);
|
||||
navigate("/login");
|
||||
} else {
|
||||
console.error(data);
|
||||
setError(data.message);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
setError(err.toString());
|
||||
}).finally(() => {
|
||||
// Disable loading bar
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`loading: ${loading}`);
|
||||
}, [loading]);
|
||||
// Redirect if the user is logged in
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(storage_id) != null || sessionStorage.getItem(storage_id) != null) {
|
||||
navigate("/dashboard");
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`loading: ${loading}`);
|
||||
}, [loading]);
|
||||
|
||||
|
||||
return <form onSubmit={handleSubmit} className="w-full flex flex-col items-center justify-center">
|
||||
<UserInput onChange={updateUsername}/>
|
||||
<PasswordInput onChange={updatePassword}/>
|
||||
<RememberMe onChange={updateRemember}/>
|
||||
return <>
|
||||
<form onSubmit={handleSubmit} className="w-full flex flex-col items-center justify-center">
|
||||
<UserInput onChange={updateUsername} />
|
||||
<PasswordInput onChange={updatePassword} />
|
||||
<RememberMe onChange={updateRemember} />
|
||||
|
||||
{error && <p className="w-full text-red-500 text-sm my-2">{error}</p>}
|
||||
{error && <p className="w-full text-red-500 text-sm my-2">{error}</p>}
|
||||
|
||||
<button type="submit"
|
||||
disabled={loading}
|
||||
className="mt-8 bg-blue-400 py-2 text-white w-full rounded-sm disabled:bg-gray-400 disabled:cursor-not-allowed">
|
||||
{loading ? "Loading..." : "Login"}
|
||||
</button>
|
||||
<button type="submit"
|
||||
disabled={loading}
|
||||
className="mt-8 bg-blue-400 py-2 text-white w-full rounded-sm disabled:bg-gray-400 disabled:cursor-not-allowed">
|
||||
{loading ? "Loading..." : "Login"}
|
||||
</button>
|
||||
|
||||
<p className="text-gray-400 text-xs text-center mt-6">
|
||||
If you do not have an account, you're in the wrong place!
|
||||
</p>
|
||||
<p className="text-gray-400 text-xs text-center mt-6">
|
||||
If you do not have an account, you're in the wrong place!
|
||||
</p>
|
||||
</form>
|
||||
</>
|
||||
};
|
||||
57
frontend/src/components/MoveDirectory.jsx
Normal file
57
frontend/src/components/MoveDirectory.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Move a file or directory.
|
||||
* @constructor
|
||||
*/
|
||||
export default function MoveDirectory({ close, move, path }) {
|
||||
const [dirName, setDirName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setDirName("/" + path.join("/"));
|
||||
}, [path]);
|
||||
|
||||
const moveDirectory = () => move(dirName);
|
||||
const updateDirName = (e) => setDirName(e.target.value);
|
||||
|
||||
const closeWithoutSaving = () => {
|
||||
setDirName("");
|
||||
close();
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Blured backdrop */}
|
||||
<div className="fixed -inset-10 bg-black opacity-50 blur-lg"></div>
|
||||
|
||||
<div className="relative z-50 bg-white p-8 rounded-lg shadow-lg w-9/10 lg:w-2/5 border-1 border-gray-400">
|
||||
<h2 className="text-2xl font-semibold mb-2 text-blue-400">
|
||||
Move/Rename Directory
|
||||
</h2>
|
||||
<p className="text-sm">
|
||||
Move a file or directory from the current directory to a new location. This can be used to rename files
|
||||
if the base directory remains the same.
|
||||
</p>
|
||||
|
||||
<input className="border-b-2 border-blue-400 w-full p-2 my-4" type="text" name="directoryName"
|
||||
value={dirName}
|
||||
onInput={updateDirName}
|
||||
placeholder="Directory name" />
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={closeWithoutSaving}
|
||||
title="Close without creating"
|
||||
className="bg-red-500 hover:bg-red-600 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 mx-2 rounded hover:cursor-pointer">
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={moveDirectory}
|
||||
title="Move directory"
|
||||
className="bg-blue-400 hover:bg-blue-500 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 rounded hover:cursor-pointer">
|
||||
Move
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import "../index.css"
|
||||
import {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
/**
|
||||
* Main navbar icon
|
||||
@ -9,11 +9,13 @@ import {useNavigate} from "react-router-dom";
|
||||
* @constructor
|
||||
*/
|
||||
function MainIcon() {
|
||||
return <svg className="h-10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 9.5V15.5M12 9.5L10 11.5M12 9.5L14 11.5M8.4 19C5.41766 19 3 16.6044 3 13.6493C3 11.2001 4.8 8.9375 7.5 8.5C8.34694 6.48637 10.3514 5 12.6893 5C15.684 5 18.1317 7.32251 18.3 10.25C19.8893 10.9449 21 12.6503 21 14.4969C21 16.9839 18.9853 19 16.5 19L8.4 19Z"
|
||||
stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
return <>
|
||||
<svg className="h-8 lg:h-10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 9.5V15.5M12 9.5L10 11.5M12 9.5L14 11.5M8.4 19C5.41766 19 3 16.6044 3 13.6493C3 11.2001 4.8 8.9375 7.5 8.5C8.34694 6.48637 10.3514 5 12.6893 5C15.684 5 18.1317 7.32251 18.3 10.25C19.8893 10.9449 21 12.6503 21 14.4969C21 16.9839 18.9853 19 16.5 19L8.4 19Z"
|
||||
stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,19 +24,19 @@ function MainIcon() {
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function DownloadButton({downloadFiles}) {
|
||||
return (
|
||||
<button className="text-black" title="Download files" onClick={downloadFiles}>
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.5535 16.5061C12.4114 16.6615 12.2106 16.75 12 16.75C11.7894 16.75 11.5886 16.6615 11.4465 16.5061L7.44648 12.1311C7.16698 11.8254 7.18822 11.351 7.49392 11.0715C7.79963 10.792 8.27402 10.8132 8.55352 11.1189L11.25 14.0682V3C11.25 2.58579 11.5858 2.25 12 2.25C12.4142 2.25 12.75 2.58579 12.75 3V14.0682L15.4465 11.1189C15.726 10.8132 16.2004 10.792 16.5061 11.0715C16.8118 11.351 16.833 11.8254 16.5535 12.1311L12.5535 16.5061Z"/>
|
||||
<path
|
||||
d="M3.75 15C3.75 14.5858 3.41422 14.25 3 14.25C2.58579 14.25 2.25 14.5858 2.25 15V15.0549C2.24998 16.4225 2.24996 17.5248 2.36652 18.3918C2.48754 19.2919 2.74643 20.0497 3.34835 20.6516C3.95027 21.2536 4.70814 21.5125 5.60825 21.6335C6.47522 21.75 7.57754 21.75 8.94513 21.75H15.0549C16.4225 21.75 17.5248 21.75 18.3918 21.6335C19.2919 21.5125 20.0497 21.2536 20.6517 20.6516C21.2536 20.0497 21.5125 19.2919 21.6335 18.3918C21.75 17.5248 21.75 16.4225 21.75 15.0549V15C21.75 14.5858 21.4142 14.25 21 14.25C20.5858 14.25 20.25 14.5858 20.25 15C20.25 16.4354 20.2484 17.4365 20.1469 18.1919C20.0482 18.9257 19.8678 19.3142 19.591 19.591C19.3142 19.8678 18.9257 20.0482 18.1919 20.1469C17.4365 20.2484 16.4354 20.25 15 20.25H9C7.56459 20.25 6.56347 20.2484 5.80812 20.1469C5.07435 20.0482 4.68577 19.8678 4.40901 19.591C4.13225 19.3142 3.9518 18.9257 3.85315 18.1919C3.75159 17.4365 3.75 16.4354 3.75 15Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
function DownloadButton({ downloadFiles }) {
|
||||
return <>
|
||||
<button className="text-black" title="Download files" onClick={downloadFiles}>
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.5535 16.5061C12.4114 16.6615 12.2106 16.75 12 16.75C11.7894 16.75 11.5886 16.6615 11.4465 16.5061L7.44648 12.1311C7.16698 11.8254 7.18822 11.351 7.49392 11.0715C7.79963 10.792 8.27402 10.8132 8.55352 11.1189L11.25 14.0682V3C11.25 2.58579 11.5858 2.25 12 2.25C12.4142 2.25 12.75 2.58579 12.75 3V14.0682L15.4465 11.1189C15.726 10.8132 16.2004 10.792 16.5061 11.0715C16.8118 11.351 16.833 11.8254 16.5535 12.1311L12.5535 16.5061Z" />
|
||||
<path
|
||||
d="M3.75 15C3.75 14.5858 3.41422 14.25 3 14.25C2.58579 14.25 2.25 14.5858 2.25 15V15.0549C2.24998 16.4225 2.24996 17.5248 2.36652 18.3918C2.48754 19.2919 2.74643 20.0497 3.34835 20.6516C3.95027 21.2536 4.70814 21.5125 5.60825 21.6335C6.47522 21.75 7.57754 21.75 8.94513 21.75H15.0549C16.4225 21.75 17.5248 21.75 18.3918 21.6335C19.2919 21.5125 20.0497 21.2536 20.6517 20.6516C21.2536 20.0497 21.5125 19.2919 21.6335 18.3918C21.75 17.5248 21.75 16.4225 21.75 15.0549V15C21.75 14.5858 21.4142 14.25 21 14.25C20.5858 14.25 20.25 14.5858 20.25 15C20.25 16.4354 20.2484 17.4365 20.1469 18.1919C20.0482 18.9257 19.8678 19.3142 19.591 19.591C19.3142 19.8678 18.9257 20.0482 18.1919 20.1469C17.4365 20.2484 16.4354 20.25 15 20.25H9C7.56459 20.25 6.56347 20.2484 5.80812 20.1469C5.07435 20.0482 4.68577 19.8678 4.40901 19.591C4.13225 19.3142 3.9518 18.9257 3.85315 18.1919C3.75159 17.4365 3.75 16.4354 3.75 15Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,116 +45,117 @@ function DownloadButton({downloadFiles}) {
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function UploadButton({uploadFiles}) {
|
||||
return (
|
||||
<button className="text-black" title="Upload files" onClick={uploadFiles}>
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full font-semibold h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.5535 2.49392C12.4114 2.33852 12.2106 2.25 12 2.25C11.7894 2.25 11.5886 2.33852 11.4465 2.49392L7.44648 6.86892C7.16698 7.17462 7.18822 7.64902 7.49392 7.92852C7.79963 8.20802 8.27402 8.18678 8.55352 7.88108L11.25 4.9318V16C11.25 16.4142 11.5858 16.75 12 16.75C12.4142 16.75 12.75 16.4142 12.75 16V4.9318L15.4465 7.88108C15.726 8.18678 16.2004 8.20802 16.5061 7.92852C16.8118 7.64902 16.833 7.17462 16.5535 6.86892L12.5535 2.49392Z"/>
|
||||
<path
|
||||
d="M3.75 15C3.75 14.5858 3.41422 14.25 3 14.25C2.58579 14.25 2.25 14.5858 2.25 15V15.0549C2.24998 16.4225 2.24996 17.5248 2.36652 18.3918C2.48754 19.2919 2.74643 20.0497 3.34835 20.6516C3.95027 21.2536 4.70814 21.5125 5.60825 21.6335C6.47522 21.75 7.57754 21.75 8.94513 21.75H15.0549C16.4225 21.75 17.5248 21.75 18.3918 21.6335C19.2919 21.5125 20.0497 21.2536 20.6517 20.6516C21.2536 20.0497 21.5125 19.2919 21.6335 18.3918C21.75 17.5248 21.75 16.4225 21.75 15.0549V15C21.75 14.5858 21.4142 14.25 21 14.25C20.5858 14.25 20.25 14.5858 20.25 15C20.25 16.4354 20.2484 17.4365 20.1469 18.1919C20.0482 18.9257 19.8678 19.3142 19.591 19.591C19.3142 19.8678 18.9257 20.0482 18.1919 20.1469C17.4365 20.2484 16.4354 20.25 15 20.25H9C7.56459 20.25 6.56347 20.2484 5.80812 20.1469C5.07435 20.0482 4.68577 19.8678 4.40901 19.591C4.13225 19.3142 3.9518 18.9257 3.85315 18.1919C3.75159 17.4365 3.75 16.4354 3.75 15Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
function UploadButton({ uploadFiles }) {
|
||||
return <>
|
||||
<button className="text-black" title="Upload files" onClick={uploadFiles}>
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full font-semibold h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.5535 2.49392C12.4114 2.33852 12.2106 2.25 12 2.25C11.7894 2.25 11.5886 2.33852 11.4465 2.49392L7.44648 6.86892C7.16698 7.17462 7.18822 7.64902 7.49392 7.92852C7.79963 8.20802 8.27402 8.18678 8.55352 7.88108L11.25 4.9318V16C11.25 16.4142 11.5858 16.75 12 16.75C12.4142 16.75 12.75 16.4142 12.75 16V4.9318L15.4465 7.88108C15.726 8.18678 16.2004 8.20802 16.5061 7.92852C16.8118 7.64902 16.833 7.17462 16.5535 6.86892L12.5535 2.49392Z" />
|
||||
<path
|
||||
d="M3.75 15C3.75 14.5858 3.41422 14.25 3 14.25C2.58579 14.25 2.25 14.5858 2.25 15V15.0549C2.24998 16.4225 2.24996 17.5248 2.36652 18.3918C2.48754 19.2919 2.74643 20.0497 3.34835 20.6516C3.95027 21.2536 4.70814 21.5125 5.60825 21.6335C6.47522 21.75 7.57754 21.75 8.94513 21.75H15.0549C16.4225 21.75 17.5248 21.75 18.3918 21.6335C19.2919 21.5125 20.0497 21.2536 20.6517 20.6516C21.2536 20.0497 21.5125 19.2919 21.6335 18.3918C21.75 17.5248 21.75 16.4225 21.75 15.0549V15C21.75 14.5858 21.4142 14.25 21 14.25C20.5858 14.25 20.25 14.5858 20.25 15C20.25 16.4354 20.2484 17.4365 20.1469 18.1919C20.0482 18.9257 19.8678 19.3142 19.591 19.591C19.3142 19.8678 18.9257 20.0482 18.1919 20.1469C17.4365 20.2484 16.4354 20.25 15 20.25H9C7.56459 20.25 6.56347 20.2484 5.80812 20.1469C5.07435 20.0482 4.68577 19.8678 4.40901 19.591C4.13225 19.3142 3.9518 18.9257 3.85315 18.1919C3.75159 17.4365 3.75 16.4354 3.75 15Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
/**
|
||||
* Information button. Not sure what this is going to do...
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
* @constructor
|
||||
*/
|
||||
function InfoButton() {
|
||||
return (
|
||||
<button className="text-black" title="Filesystem information">
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full font-semibold h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 17.75C12.4142 17.75 12.75 17.4142 12.75 17V11C12.75 10.5858 12.4142 10.25 12 10.25C11.5858 10.25 11.25 10.5858 11.25 11V17C11.25 17.4142 11.5858 17.75 12 17.75Z"/>
|
||||
<path
|
||||
d="M12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd"
|
||||
d="M1.25 12C1.25 6.06294 6.06294 1.25 12 1.25C17.9371 1.25 22.75 6.06294 22.75 12C22.75 17.9371 17.9371 22.75 12 22.75C6.06294 22.75 1.25 17.9371 1.25 12ZM12 2.75C6.89137 2.75 2.75 6.89137 2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C17.1086 21.25 21.25 17.1086 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
return <>
|
||||
<button className="text-black" title="Filesystem information">
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full font-semibold h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 17.75C12.4142 17.75 12.75 17.4142 12.75 17V11C12.75 10.5858 12.4142 10.25 12 10.25C11.5858 10.25 11.25 10.5858 11.25 11V17C11.25 17.4142 11.5858 17.75 12 17.75Z" />
|
||||
<path
|
||||
d="M12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z" />
|
||||
<path fillRule="evenodd" clipRule="evenodd"
|
||||
d="M1.25 12C1.25 6.06294 6.06294 1.25 12 1.25C17.9371 1.25 22.75 6.06294 22.75 12C22.75 17.9371 17.9371 22.75 12 22.75C6.06294 22.75 1.25 17.9371 1.25 12ZM12 2.75C6.89137 2.75 2.75 6.89137 2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C17.1086 21.25 21.25 17.1086 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Logout button, which clears the user from the storage's.
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
* @constructor
|
||||
*/
|
||||
function LogoutButton() {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
* @type {string}
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
|
||||
const handleClick = () => {
|
||||
localStorage.removeItem(storage_id);
|
||||
sessionStorage.removeItem(storage_id);
|
||||
navigate("/login");
|
||||
};
|
||||
const handleClick = () => {
|
||||
localStorage.removeItem(storage_id);
|
||||
sessionStorage.removeItem(storage_id);
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<button className="text-red-500" title="Logout" onClick={handleClick}>
|
||||
<svg
|
||||
className="text-red-500 hover:bg-red-200 mx-1 transition-colors duration-200 p-1.5 rounded-full font-semibold h-8"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15 4H18C19.1046 4 20 4.89543 20 6V18C20 19.1046 19.1046 20 18 20H15M8 8L4 12M4 12L8 16M4 12L16 12"
|
||||
strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
return <>
|
||||
<button className="text-red-500" title="Logout" onClick={handleClick}>
|
||||
<svg
|
||||
className="text-red-500 hover:bg-red-200 mx-1 transition-colors duration-200 p-1.5 rounded-full font-semibold h-8"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15 4H18C19.1046 4 20 4.89543 20 6V18C20 19.1046 19.1046 20 18 20H15M8 8L4 12M4 12L8 16M4 12L16 12"
|
||||
strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
/**
|
||||
* Search bar input, with controlled state.
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
* @constructor
|
||||
*/
|
||||
function SearchBar() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
/**
|
||||
/**
|
||||
* Update the controlled state.
|
||||
* @param event {InputEvent}
|
||||
*/
|
||||
const updateSearch = (event) => {
|
||||
setSearch(event.target.value);
|
||||
};
|
||||
*/
|
||||
const updateSearch = (event) => setSearch(event.target.value);
|
||||
|
||||
return (
|
||||
<div className="mx-4 w-1/4">
|
||||
<input
|
||||
className="px-2 py-1 w-full text-sm focus:outline-none focus:shadow-sm shadow-blue-300 transition-shadow duration-200 rounded-sm border-1 border-gray-400"
|
||||
type="search"
|
||||
value={search}
|
||||
onInput={updateSearch}
|
||||
placeholder="Search filesystem"/>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="mx-4 w-1/2 lg:w-1/4">
|
||||
<input
|
||||
className="px-2 py-1 w-full text-sm focus:outline-none focus:shadow-sm shadow-blue-300 transition-shadow duration-200 rounded-sm border-1 border-gray-400"
|
||||
type="search"
|
||||
value={search}
|
||||
onInput={updateSearch}
|
||||
placeholder="Search filesystem" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Navbar({downloadFiles, uploadFiles}) {
|
||||
return <nav className="absolute w-full p-2 flex items-center border-b-1 border-gray-400 bg-gray-100">
|
||||
<MainIcon/>
|
||||
export default function Navbar({ downloadFiles, uploadFiles }) {
|
||||
return <>
|
||||
<nav className="absolute w-full p-2 flex items-center border-b-1 border-gray-400 bg-gray-100">
|
||||
<MainIcon />
|
||||
|
||||
<h3 className="text-xl font-mono px-3">file.gophernest.net</h3>
|
||||
<SearchBar/>
|
||||
<h3 className="hidden lg:block text-xl font-mono px-3">file.gophernest.net</h3>
|
||||
|
||||
<div className="min-h-fit ml-auto flex">
|
||||
<DownloadButton downloadFiles={downloadFiles}/>
|
||||
<UploadButton uploadFiles={uploadFiles}/>
|
||||
<InfoButton/>
|
||||
<LogoutButton/>
|
||||
</div>
|
||||
<SearchBar />
|
||||
|
||||
<div className="min-h-fit ml-auto flex">
|
||||
<DownloadButton downloadFiles={downloadFiles} />
|
||||
<UploadButton uploadFiles={uploadFiles} />
|
||||
<InfoButton />
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</nav>
|
||||
</>;
|
||||
}
|
||||
@ -8,63 +8,61 @@ import "../index.css"
|
||||
* @constructor
|
||||
*/
|
||||
export default function PasswordInput({onChange}) {
|
||||
// Example of the controlled input state pattern
|
||||
const [password, setPassword] = useState("");
|
||||
const [hidden, setHidden] = useState(true);
|
||||
// Example of the controlled input state pattern
|
||||
const [password, setPassword] = useState("");
|
||||
const [hidden, setHidden] = useState(true);
|
||||
|
||||
// Generate ID for the input element
|
||||
const id = useId();
|
||||
// Generate ID for the input element
|
||||
const id = useId();
|
||||
|
||||
/**
|
||||
/**
|
||||
* Toggle the hidden state which will determine the type of the input.
|
||||
*/
|
||||
const toggleHidden = () => {
|
||||
setHidden(!hidden);
|
||||
}
|
||||
const toggleHidden = () => setHidden(!hidden);
|
||||
|
||||
/**
|
||||
/**
|
||||
* Update the password
|
||||
* @param event {InputEvent}
|
||||
*/
|
||||
const updatePassword = (event) => {
|
||||
const newPassword = event.target.value;
|
||||
setPassword(newPassword);
|
||||
const updatePassword = (event) => {
|
||||
const newPassword = event.target.value;
|
||||
setPassword(newPassword);
|
||||
|
||||
// Do not rely on other state, otherwise we get one render behind
|
||||
onChange(newPassword);
|
||||
}
|
||||
// Do not rely on other state, otherwise we get one render behind
|
||||
onChange(newPassword);
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="relative w-full my-2">
|
||||
<input
|
||||
type={hidden ? "password" : "text"}
|
||||
value={password}
|
||||
onInput={updatePassword}
|
||||
name={id}
|
||||
required={true}
|
||||
placeholder="Password"
|
||||
className="border border-gray-300 rounded-sm py-2 px-4 placeholder:italic w-full"/>
|
||||
<button
|
||||
onClick={toggleHidden}
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 px-3 h-full my-auto">
|
||||
{hidden ?
|
||||
<svg className="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.68936 6.70456C2.52619 6.32384 2.08528 6.14747 1.70456 6.31064C1.32384 6.47381 1.14747 6.91472 1.31064 7.29544L2.68936 6.70456ZM15.5872 13.3287L15.3125 12.6308L15.5872 13.3287ZM9.04145 13.7377C9.26736 13.3906 9.16904 12.926 8.82185 12.7001C8.47466 12.4742 8.01008 12.5725 7.78417 12.9197L9.04145 13.7377ZM6.37136 15.091C6.14545 15.4381 6.24377 15.9027 6.59096 16.1286C6.93815 16.3545 7.40273 16.2562 7.62864 15.909L6.37136 15.091ZM22.6894 7.29544C22.8525 6.91472 22.6762 6.47381 22.2954 6.31064C21.9147 6.14747 21.4738 6.32384 21.3106 6.70456L22.6894 7.29544ZM19 11.1288L18.4867 10.582V10.582L19 11.1288ZM19.9697 13.1592C20.2626 13.4521 20.7374 13.4521 21.0303 13.1592C21.3232 12.8663 21.3232 12.3914 21.0303 12.0985L19.9697 13.1592ZM11.25 16.5C11.25 16.9142 11.5858 17.25 12 17.25C12.4142 17.25 12.75 16.9142 12.75 16.5H11.25ZM16.3714 15.909C16.5973 16.2562 17.0619 16.3545 17.409 16.1286C17.7562 15.9027 17.8545 15.4381 17.6286 15.091L16.3714 15.909ZM5.53033 11.6592C5.82322 11.3663 5.82322 10.8914 5.53033 10.5985C5.23744 10.3056 4.76256 10.3056 4.46967 10.5985L5.53033 11.6592ZM2.96967 12.0985C2.67678 12.3914 2.67678 12.8663 2.96967 13.1592C3.26256 13.4521 3.73744 13.4521 4.03033 13.1592L2.96967 12.0985ZM12 13.25C8.77611 13.25 6.46133 11.6446 4.9246 9.98966C4.15645 9.16243 3.59325 8.33284 3.22259 7.71014C3.03769 7.3995 2.90187 7.14232 2.8134 6.96537C2.76919 6.87696 2.73689 6.80875 2.71627 6.76411C2.70597 6.7418 2.69859 6.7254 2.69411 6.71533C2.69187 6.7103 2.69036 6.70684 2.68957 6.70503C2.68917 6.70413 2.68896 6.70363 2.68892 6.70355C2.68891 6.70351 2.68893 6.70357 2.68901 6.70374C2.68904 6.70382 2.68913 6.70403 2.68915 6.70407C2.68925 6.7043 2.68936 6.70456 2 7C1.31064 7.29544 1.31077 7.29575 1.31092 7.29609C1.31098 7.29624 1.31114 7.2966 1.31127 7.2969C1.31152 7.29749 1.31183 7.2982 1.31218 7.299C1.31287 7.30062 1.31376 7.30266 1.31483 7.30512C1.31698 7.31003 1.31988 7.31662 1.32353 7.32483C1.33083 7.34125 1.34115 7.36415 1.35453 7.39311C1.38127 7.45102 1.42026 7.5332 1.47176 7.63619C1.57469 7.84206 1.72794 8.13175 1.93366 8.47736C2.34425 9.16716 2.96855 10.0876 3.8254 11.0103C5.53867 12.8554 8.22389 14.75 12 14.75V13.25ZM15.3125 12.6308C14.3421 13.0128 13.2417 13.25 12 13.25V14.75C13.4382 14.75 14.7246 14.4742 15.8619 14.0266L15.3125 12.6308ZM7.78417 12.9197L6.37136 15.091L7.62864 15.909L9.04145 13.7377L7.78417 12.9197ZM22 7C21.3106 6.70456 21.3107 6.70441 21.3108 6.70427C21.3108 6.70423 21.3108 6.7041 21.3109 6.70402C21.3109 6.70388 21.311 6.70376 21.311 6.70368C21.3111 6.70352 21.3111 6.70349 21.3111 6.7036C21.311 6.7038 21.3107 6.70452 21.3101 6.70576C21.309 6.70823 21.307 6.71275 21.3041 6.71924C21.2983 6.73223 21.2889 6.75309 21.2758 6.78125C21.2495 6.83757 21.2086 6.92295 21.1526 7.03267C21.0406 7.25227 20.869 7.56831 20.6354 7.9432C20.1669 8.69516 19.4563 9.67197 18.4867 10.582L19.5133 11.6757C20.6023 10.6535 21.3917 9.56587 21.9085 8.73646C22.1676 8.32068 22.36 7.9668 22.4889 7.71415C22.5533 7.58775 22.602 7.48643 22.6353 7.41507C22.6519 7.37939 22.6647 7.35118 22.6737 7.33104C22.6782 7.32097 22.6818 7.31292 22.6844 7.30696C22.6857 7.30398 22.6867 7.30153 22.6876 7.2996C22.688 7.29864 22.6883 7.29781 22.6886 7.29712C22.6888 7.29677 22.6889 7.29646 22.689 7.29618C22.6891 7.29604 22.6892 7.29585 22.6892 7.29578C22.6893 7.29561 22.6894 7.29544 22 7ZM18.4867 10.582C17.6277 11.3882 16.5739 12.1343 15.3125 12.6308L15.8619 14.0266C17.3355 13.4466 18.5466 12.583 19.5133 11.6757L18.4867 10.582ZM18.4697 11.6592L19.9697 13.1592L21.0303 12.0985L19.5303 10.5985L18.4697 11.6592ZM11.25 14V16.5H12.75V14H11.25ZM14.9586 13.7377L16.3714 15.909L17.6286 15.091L16.2158 12.9197L14.9586 13.7377ZM4.46967 10.5985L2.96967 12.0985L4.03033 13.1592L5.53033 11.6592L4.46967 10.5985Z"
|
||||
fill="#1C274C"/>
|
||||
</svg>
|
||||
:
|
||||
<svg className="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9 4.45962C9.91153 4.16968 10.9104 4 12 4C16.1819 4 19.028 6.49956 20.7251 8.70433C21.575 9.80853 22 10.3606 22 12C22 13.6394 21.575 14.1915 20.7251 15.2957C19.028 17.5004 16.1819 20 12 20C7.81811 20 4.97196 17.5004 3.27489 15.2957C2.42496 14.1915 2 13.6394 2 12C2 10.3606 2.42496 9.80853 3.27489 8.70433C3.75612 8.07914 4.32973 7.43025 5 6.82137"
|
||||
stroke="#1C274C" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
<path
|
||||
d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z"
|
||||
stroke="#1C274C" strokeWidth="1.5"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
return <>
|
||||
<div className="relative w-full my-2">
|
||||
<input
|
||||
type={hidden ? "password" : "text"}
|
||||
value={password}
|
||||
onInput={updatePassword}
|
||||
name={id}
|
||||
required={true}
|
||||
placeholder="Password"
|
||||
className="border border-gray-300 rounded-sm py-2 px-4 placeholder:italic w-full"/>
|
||||
<button
|
||||
onClick={toggleHidden}
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 px-3 h-full my-auto">
|
||||
{hidden ?
|
||||
<svg className="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.68936 6.70456C2.52619 6.32384 2.08528 6.14747 1.70456 6.31064C1.32384 6.47381 1.14747 6.91472 1.31064 7.29544L2.68936 6.70456ZM15.5872 13.3287L15.3125 12.6308L15.5872 13.3287ZM9.04145 13.7377C9.26736 13.3906 9.16904 12.926 8.82185 12.7001C8.47466 12.4742 8.01008 12.5725 7.78417 12.9197L9.04145 13.7377ZM6.37136 15.091C6.14545 15.4381 6.24377 15.9027 6.59096 16.1286C6.93815 16.3545 7.40273 16.2562 7.62864 15.909L6.37136 15.091ZM22.6894 7.29544C22.8525 6.91472 22.6762 6.47381 22.2954 6.31064C21.9147 6.14747 21.4738 6.32384 21.3106 6.70456L22.6894 7.29544ZM19 11.1288L18.4867 10.582V10.582L19 11.1288ZM19.9697 13.1592C20.2626 13.4521 20.7374 13.4521 21.0303 13.1592C21.3232 12.8663 21.3232 12.3914 21.0303 12.0985L19.9697 13.1592ZM11.25 16.5C11.25 16.9142 11.5858 17.25 12 17.25C12.4142 17.25 12.75 16.9142 12.75 16.5H11.25ZM16.3714 15.909C16.5973 16.2562 17.0619 16.3545 17.409 16.1286C17.7562 15.9027 17.8545 15.4381 17.6286 15.091L16.3714 15.909ZM5.53033 11.6592C5.82322 11.3663 5.82322 10.8914 5.53033 10.5985C5.23744 10.3056 4.76256 10.3056 4.46967 10.5985L5.53033 11.6592ZM2.96967 12.0985C2.67678 12.3914 2.67678 12.8663 2.96967 13.1592C3.26256 13.4521 3.73744 13.4521 4.03033 13.1592L2.96967 12.0985ZM12 13.25C8.77611 13.25 6.46133 11.6446 4.9246 9.98966C4.15645 9.16243 3.59325 8.33284 3.22259 7.71014C3.03769 7.3995 2.90187 7.14232 2.8134 6.96537C2.76919 6.87696 2.73689 6.80875 2.71627 6.76411C2.70597 6.7418 2.69859 6.7254 2.69411 6.71533C2.69187 6.7103 2.69036 6.70684 2.68957 6.70503C2.68917 6.70413 2.68896 6.70363 2.68892 6.70355C2.68891 6.70351 2.68893 6.70357 2.68901 6.70374C2.68904 6.70382 2.68913 6.70403 2.68915 6.70407C2.68925 6.7043 2.68936 6.70456 2 7C1.31064 7.29544 1.31077 7.29575 1.31092 7.29609C1.31098 7.29624 1.31114 7.2966 1.31127 7.2969C1.31152 7.29749 1.31183 7.2982 1.31218 7.299C1.31287 7.30062 1.31376 7.30266 1.31483 7.30512C1.31698 7.31003 1.31988 7.31662 1.32353 7.32483C1.33083 7.34125 1.34115 7.36415 1.35453 7.39311C1.38127 7.45102 1.42026 7.5332 1.47176 7.63619C1.57469 7.84206 1.72794 8.13175 1.93366 8.47736C2.34425 9.16716 2.96855 10.0876 3.8254 11.0103C5.53867 12.8554 8.22389 14.75 12 14.75V13.25ZM15.3125 12.6308C14.3421 13.0128 13.2417 13.25 12 13.25V14.75C13.4382 14.75 14.7246 14.4742 15.8619 14.0266L15.3125 12.6308ZM7.78417 12.9197L6.37136 15.091L7.62864 15.909L9.04145 13.7377L7.78417 12.9197ZM22 7C21.3106 6.70456 21.3107 6.70441 21.3108 6.70427C21.3108 6.70423 21.3108 6.7041 21.3109 6.70402C21.3109 6.70388 21.311 6.70376 21.311 6.70368C21.3111 6.70352 21.3111 6.70349 21.3111 6.7036C21.311 6.7038 21.3107 6.70452 21.3101 6.70576C21.309 6.70823 21.307 6.71275 21.3041 6.71924C21.2983 6.73223 21.2889 6.75309 21.2758 6.78125C21.2495 6.83757 21.2086 6.92295 21.1526 7.03267C21.0406 7.25227 20.869 7.56831 20.6354 7.9432C20.1669 8.69516 19.4563 9.67197 18.4867 10.582L19.5133 11.6757C20.6023 10.6535 21.3917 9.56587 21.9085 8.73646C22.1676 8.32068 22.36 7.9668 22.4889 7.71415C22.5533 7.58775 22.602 7.48643 22.6353 7.41507C22.6519 7.37939 22.6647 7.35118 22.6737 7.33104C22.6782 7.32097 22.6818 7.31292 22.6844 7.30696C22.6857 7.30398 22.6867 7.30153 22.6876 7.2996C22.688 7.29864 22.6883 7.29781 22.6886 7.29712C22.6888 7.29677 22.6889 7.29646 22.689 7.29618C22.6891 7.29604 22.6892 7.29585 22.6892 7.29578C22.6893 7.29561 22.6894 7.29544 22 7ZM18.4867 10.582C17.6277 11.3882 16.5739 12.1343 15.3125 12.6308L15.8619 14.0266C17.3355 13.4466 18.5466 12.583 19.5133 11.6757L18.4867 10.582ZM18.4697 11.6592L19.9697 13.1592L21.0303 12.0985L19.5303 10.5985L18.4697 11.6592ZM11.25 14V16.5H12.75V14H11.25ZM14.9586 13.7377L16.3714 15.909L17.6286 15.091L16.2158 12.9197L14.9586 13.7377ZM4.46967 10.5985L2.96967 12.0985L4.03033 13.1592L5.53033 11.6592L4.46967 10.5985Z"
|
||||
fill="#1C274C"/>
|
||||
</svg>
|
||||
:
|
||||
<svg className="h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9 4.45962C9.91153 4.16968 10.9104 4 12 4C16.1819 4 19.028 6.49956 20.7251 8.70433C21.575 9.80853 22 10.3606 22 12C22 13.6394 21.575 14.1915 20.7251 15.2957C19.028 17.5004 16.1819 20 12 20C7.81811 20 4.97196 17.5004 3.27489 15.2957C2.42496 14.1915 2 13.6394 2 12C2 10.3606 2.42496 9.80853 3.27489 8.70433C3.75612 8.07914 4.32973 7.43025 5 6.82137"
|
||||
stroke="#1C274C" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
<path
|
||||
d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z"
|
||||
stroke="#1C274C" strokeWidth="1.5"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -2,52 +2,84 @@
|
||||
* Takes the user back to the home directory. The onClick prop
|
||||
* is called when the button is clicked.
|
||||
* @param onClick {function}
|
||||
* @param enabled
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function HomeButton({onClick, enabled}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={!enabled}
|
||||
className="hover:bg-gray-200 p-1.5 rounded-full transition-colors duration-150 disabled:text-gray-500 disabled:hover:bg-red-300 disabled:cursor-not-allowed text-black">
|
||||
<svg className="h-4"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path d="M1 6V15H6V11C6 9.89543 6.89543 9 8 9C9.10457 9 10 9.89543 10 11V15H15V6L8 0L1 6Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
function HomeButton({ onClick, enabled }) {
|
||||
return <>
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={!enabled}
|
||||
className="hover:bg-gray-200 p-1.5 rounded-full transition-colors duration-150 disabled:text-gray-500 disabled:hover:bg-red-300 disabled:cursor-not-allowed text-black">
|
||||
<svg className="h-4"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path d="M1 6V15H6V11C6 9.89543 6.89543 9 8 9C9.10457 9 10 9.89543 10 11V15H15V6L8 0L1 6Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
function BackButton({onClick, enabled}) {
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
disabled={!enabled}
|
||||
className="hover:bg-gray-200 p-1.5 mr-1 rounded-full transition-colors duration-150 disabled:text-gray-500 disabled:hover:bg-red-300 disabled:cursor-not-allowed text-black">
|
||||
<svg className="h-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4 10L3.29289 10.7071L2.58579 10L3.29289 9.29289L4 10ZM21 18C21 18.5523 20.5523 19 20 19C19.4477 19 19 18.5523 19 18L21 18ZM8.29289 15.7071L3.29289 10.7071L4.70711 9.29289L9.70711 14.2929L8.29289 15.7071ZM3.29289 9.29289L8.29289 4.29289L9.70711 5.70711L4.70711 10.7071L3.29289 9.29289ZM4 9L14 9L14 11L4 11L4 9ZM21 16L21 18L19 18L19 16L21 16ZM14 9C17.866 9 21 12.134 21 16L19 16C19 13.2386 16.7614 11 14 11L14 9Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
function BackButton({ onClick, enabled }) {
|
||||
return <>
|
||||
<button onClick={onClick}
|
||||
disabled={!enabled}
|
||||
className="hover:bg-gray-200 p-1.5 mr-1 rounded-full transition-colors duration-150 disabled:text-gray-500 disabled:hover:bg-red-300 disabled:cursor-not-allowed text-black">
|
||||
<svg className="h-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4 10L3.29289 10.7071L2.58579 10L3.29289 9.29289L4 10ZM21 18C21 18.5523 20.5523 19 20 19C19.4477 19 19 18.5523 19 18L21 18ZM8.29289 15.7071L3.29289 10.7071L4.70711 9.29289L9.70711 14.2929L8.29289 15.7071ZM3.29289 9.29289L8.29289 4.29289L9.70711 5.70711L4.70711 10.7071L3.29289 9.29289ZM4 9L14 9L14 11L4 11L4 9ZM21 16L21 18L19 18L19 16L21 16ZM14 9C17.866 9 21 12.134 21 16L19 16C19 13.2386 16.7614 11 14 11L14 9Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
function CreateButton({onClick}) {
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
title="Create a file/directory"
|
||||
className="hover:bg-gray-200 p-1.5 mr-1 rounded-full transition-colors duration-150 disabled:text-gray-500 disabled:hover:bg-red-300 disabled:cursor-not-allowed text-black">
|
||||
<svg className="h-5" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M13 3H8.2C7.0799 3 6.51984 3 6.09202 3.21799C5.71569 3.40973 5.40973 3.71569 5.21799 4.09202C5 4.51984 5 5.0799 5 6.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.0799 21 8.2 21H10M13 3L19 9M13 3V7.4C13 7.96005 13 8.24008 13.109 8.45399C13.2049 8.64215 13.3578 8.79513 13.546 8.89101C13.7599 9 14.0399 9 14.6 9H19M19 9V10M14 21L16.025 20.595C16.2015 20.5597 16.2898 20.542 16.3721 20.5097C16.4452 20.4811 16.5147 20.4439 16.579 20.399C16.6516 20.3484 16.7152 20.2848 16.8426 20.1574L21 16C21.5523 15.4477 21.5523 14.5523 21 14C20.4477 13.4477 19.5523 13.4477 19 14L14.8426 18.1574C14.7152 18.2848 14.6516 18.3484 14.601 18.421C14.5561 18.4853 14.5189 18.5548 14.4903 18.6279C14.458 18.7102 14.4403 18.7985 14.405 18.975L14 21Z"
|
||||
stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
function CreateButton({ onClick }) {
|
||||
return <>
|
||||
<button onClick={onClick}
|
||||
title="Create a file/directory"
|
||||
className="hover:bg-gray-200 p-1.5 mr-1 rounded-full transition-colors duration-150 disabled:text-gray-500 disabled:hover:bg-red-300 disabled:cursor-not-allowed text-black">
|
||||
<svg className="h-5" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M13 3H8.2C7.0799 3 6.51984 3 6.09202 3.21799C5.71569 3.40973 5.40973 3.71569 5.21799 4.09202C5 4.51984 5 5.0799 5 6.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.0799 21 8.2 21H10M13 3L19 9M13 3V7.4C13 7.96005 13 8.24008 13.109 8.45399C13.2049 8.64215 13.3578 8.79513 13.546 8.89101C13.7599 9 14.0399 9 14.6 9H19M19 9V10M14 21L16.025 20.595C16.2015 20.5597 16.2898 20.542 16.3721 20.5097C16.4452 20.4811 16.5147 20.4439 16.579 20.399C16.6516 20.3484 16.7152 20.2848 16.8426 20.1574L21 16C21.5523 15.4477 21.5523 14.5523 21 14C20.4477 13.4477 19.5523 13.4477 19 14L14.8426 18.1574C14.7152 18.2848 14.6516 18.3484 14.601 18.421C14.5561 18.4853 14.5189 18.5548 14.4903 18.6279C14.458 18.7102 14.4403 18.7985 14.405 18.975L14 21Z"
|
||||
stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
function DeleteButton({ onClick, enabled }) {
|
||||
return <>
|
||||
<button onClick={onClick}
|
||||
disabled={!enabled}
|
||||
title="Delete selected files/directories"
|
||||
className="hover:bg-gray-200 p-1.5 mr-1 rounded-full transition-colors duration-150 disabled:text-gray-500 disabled:hover:bg-red-300 disabled:cursor-not-allowed text-black">
|
||||
<svg className="h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M18 6L17.1991 18.0129C17.129 19.065 17.0939 19.5911 16.8667 19.99C16.6666 20.3412 16.3648 20.6235 16.0011 20.7998C15.588 21 15.0607 21 14.0062 21H9.99377C8.93927 21 8.41202 21 7.99889 20.7998C7.63517 20.6235 7.33339 20.3412 7.13332 19.99C6.90607 19.5911 6.871 19.065 6.80086 18.0129L6 6M4 6H20M16 6L15.7294 5.18807C15.4671 4.40125 15.3359 4.00784 15.0927 3.71698C14.8779 3.46013 14.6021 3.26132 14.2905 3.13878C13.9376 3 13.523 3 12.6936 3H11.3064C10.477 3 10.0624 3 9.70951 3.13878C9.39792 3.26132 9.12208 3.46013 8.90729 3.71698C8.66405 4.00784 8.53292 4.40125 8.27064 5.18807L8 6"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
function MoveButton({ onClick, enabled }) {
|
||||
return <>
|
||||
<button onClick={onClick}
|
||||
disabled={!enabled}
|
||||
title="Moved a selected file/directory"
|
||||
className="hover:bg-gray-200 p-1.5 mr-1 rounded-full transition-colors duration-150 disabled:text-gray-500 disabled:hover:bg-red-300 disabled:cursor-not-allowed text-black">
|
||||
<svg className="h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 21V3M12 21L14 19M12 21L10 19M12 3L14 5M12 3L10 5M21 12H3M21 12L19 10M21 12L19 14M3 12L5 10M3 12L5 14"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,11 +90,15 @@ function CreateButton({onClick}) {
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function PathElement({name, index, onClick}) {
|
||||
const handleClick = () => {
|
||||
onClick(index);
|
||||
};
|
||||
return <button onClick={handleClick}>/<span className="hover:underline cursor-pointer">{name}</span></button>
|
||||
function PathElement({ name, index, onClick }) {
|
||||
const handleClick = () => onClick(index);
|
||||
return <>
|
||||
<button onClick={handleClick}>
|
||||
/<span className="hover:underline cursor-pointer">
|
||||
{name}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,19 +109,26 @@ function PathElement({name, index, onClick}) {
|
||||
* @param backArrow {function}
|
||||
* @param enabled {boolean}
|
||||
* @param create {function}
|
||||
* @param remove {function}
|
||||
* @param removeEnable {boolean}
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function PathDisplay({path, updatePath, backHome, backArrow, enabled, create}) {
|
||||
return (
|
||||
<div
|
||||
className="w-2/3 mt-8 border-b-1 border-gray-400 bg-white flex items-center truncate">
|
||||
<HomeButton onClick={backHome} enabled={enabled}/>
|
||||
<BackButton onClick={backArrow} enabled={enabled}/>
|
||||
{path.map((seg, idx) => <PathElement name={seg} key={idx} index={idx} onClick={updatePath}/>)}
|
||||
<div className="ml-auto h-full flex items-center">
|
||||
<CreateButton onClick={create}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
export default function PathDisplay({ path, updatePath, backHome, backArrow, enabled, create, remove, removeEnable, move, moveEnabled }) {
|
||||
return <>
|
||||
<div className="w-9/10 lg:w-2/3 mt-8 border-b-1 border-gray-400 bg-white flex items-center truncate">
|
||||
<HomeButton onClick={backHome} enabled={enabled} />
|
||||
<BackButton onClick={backArrow} enabled={enabled} />
|
||||
{path.map((seg, idx) => <PathElement name={seg} key={idx} index={idx} onClick={updatePath} />)}
|
||||
<div className="ml-auto h-full flex items-center">
|
||||
<DeleteButton onClick={remove} enabled={removeEnable} />
|
||||
</div>
|
||||
<div className="h-full flex items-center">
|
||||
<MoveButton onClick={move} enabled={moveEnabled} />
|
||||
</div>
|
||||
<div className="h-full flex items-center">
|
||||
<CreateButton onClick={create} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -7,21 +7,23 @@ import {useId, useState} from "react";
|
||||
* @constructor
|
||||
*/
|
||||
export default function RememberMe({onChange}) {
|
||||
const [remember, setRemember] = useState(false);
|
||||
const rememberMeId = useId();
|
||||
const [remember, setRemember] = useState(false);
|
||||
const rememberMeId = useId();
|
||||
|
||||
/**
|
||||
/**
|
||||
* Toggle the value of the 'remember me' toggle.
|
||||
* It also calls the 'onChange' function which should
|
||||
* be used to store the state in the parent component.
|
||||
*/
|
||||
const toggleRemember = () => {
|
||||
onChange(!remember);
|
||||
setRemember(!remember);
|
||||
};
|
||||
const toggleRemember = () => {
|
||||
onChange(!remember);
|
||||
setRemember(!remember);
|
||||
};
|
||||
|
||||
return <div className="w-full flex items-center my-2" onClick={toggleRemember}>
|
||||
<input name={rememberMeId} checked={remember} type="checkbox" className="mx-1"/>
|
||||
<label htmlFor={rememberMeId} className="text-sm select-none cursor-pointer">Remember me for 30 days</label>
|
||||
return <>
|
||||
<div className="w-full flex items-center my-2" onClick={toggleRemember}>
|
||||
<input name={rememberMeId} checked={remember} type="checkbox" className="mx-1"/>
|
||||
<label htmlFor={rememberMeId} className="text-sm select-none cursor-pointer">Remember me for 30 days</label>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -8,111 +8,109 @@ import {useRef, useState} from "react";
|
||||
* @constructor
|
||||
*/
|
||||
function FileList({files}) {
|
||||
return (
|
||||
<ul className="text-xs overflow-auto max-h-[200px] italic">
|
||||
{files.map((file) => <li className="py-0.5">{file.name}</li>)}
|
||||
</ul>
|
||||
);
|
||||
return (
|
||||
<ul className="text-xs overflow-auto max-h-[200px] italic">
|
||||
{files.map((file) => <li className="py-0.5">{file.name}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Uploader({close, upload}) {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
/**
|
||||
/**
|
||||
* References to the elements.
|
||||
* @type {React.RefObject<null>}
|
||||
*/
|
||||
const inputElement = useRef(null);
|
||||
const inputElement = useRef(null);
|
||||
|
||||
/**
|
||||
/**
|
||||
* Prevent the default drag behavior and apply visual classes.
|
||||
* @param e {object} Event object from the div wrapper.
|
||||
*/
|
||||
const dragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
e.target.classList.add('border-blue-500', 'bg-blue-100');
|
||||
};
|
||||
const dragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
e.target.classList.add('border-blue-500', 'bg-blue-100');
|
||||
};
|
||||
|
||||
/**
|
||||
/**
|
||||
* Prevent the default drag behavior and apply visual classes.
|
||||
* @param e {object} Event object from the div wrapper.
|
||||
*/
|
||||
const dragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
e.target.classList.remove('border-blue-500', 'bg-blue-100');
|
||||
};
|
||||
const dragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
e.target.classList.remove('border-blue-500', 'bg-blue-100');
|
||||
};
|
||||
|
||||
/**
|
||||
/**
|
||||
* Prevent the default drag behavior.
|
||||
* @param e {object} Event object from the div wrapper.
|
||||
*/
|
||||
const dragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
const dragOver = (e) => e.preventDefault();
|
||||
|
||||
/**
|
||||
/**
|
||||
* When files are dropped into the div, this will be called, to append the files.
|
||||
* @param e {object} Event object from the div wrapper.
|
||||
*/
|
||||
const drop = (e) => {
|
||||
e.preventDefault();
|
||||
e.target.classList.remove('border-blue-500', 'bg-blue-100');
|
||||
setFiles([...files, ...e.dataTransfer.files]);
|
||||
};
|
||||
const drop = (e) => {
|
||||
e.preventDefault();
|
||||
e.target.classList.remove('border-blue-500', 'bg-blue-100');
|
||||
setFiles([...files, ...e.dataTransfer.files]);
|
||||
};
|
||||
|
||||
/**
|
||||
/**
|
||||
* Click event for the input element.
|
||||
* Uses the ref hook to access the element.
|
||||
*/
|
||||
const click = () => {
|
||||
inputElement.current.click();
|
||||
};
|
||||
const click = () => inputElement.current.click();
|
||||
|
||||
/**
|
||||
/**
|
||||
* When the input changes, append the new files.
|
||||
* @param e {object} Event object from the input.
|
||||
*/
|
||||
const inputChange = (e) => {
|
||||
setFiles([...files, ...e.target.files]);
|
||||
};
|
||||
const inputChange = (e) => setFiles([...files, ...e.target.files]);
|
||||
|
||||
const uploadFiles = () => {
|
||||
upload(files);
|
||||
};
|
||||
const uploadFiles = () => upload(files);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed -inset-10 bg-black opacity-50 blur-lg"></div>
|
||||
<div className="relative z-50 bg-white p-8 rounded-lg shadow-lg w-2/5 border-1 border-gray-400">
|
||||
<h2 className="text-2xl font-semibold mb-2 text-blue-400">Upload Files</h2>
|
||||
<p className="text-sm">
|
||||
Files uploaded will be added to the current directory. Currently, directory
|
||||
uploads are not supported. If you want to upload a directory you can create a new one and upload the
|
||||
files into it.
|
||||
</p>
|
||||
<div
|
||||
className="my-5 border-2 border-dashed p-8 rounded-md text-center cursor-pointer border-gray-400 hover:bg-blue-100 hover:border-blue-500 transition-all duration-100"
|
||||
onDragEnter={dragEnter} onDragLeave={dragLeave} onDragOver={dragOver} onDrop={drop} onClick={click}
|
||||
>
|
||||
<input multiple type="file" className="hidden" ref={inputElement} onChange={inputChange}/>
|
||||
<p className="italic">Drag and drop files here or click to select</p>
|
||||
</div>
|
||||
<FileList files={files}/>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={close}
|
||||
title="Close without uploading"
|
||||
className="bg-red-500 hover:bg-red-600 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 mx-2 rounded hover:cursor-pointer">
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={uploadFiles}
|
||||
title="Upload files"
|
||||
className="bg-blue-400 hover:bg-blue-500 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 rounded hover:cursor-pointer">
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
return <>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed -inset-10 bg-black opacity-50 blur-lg"></div>
|
||||
<div className="relative z-50 bg-white p-8 rounded-lg shadow-lg w-9/10 lg:w-2/5 border-1 border-gray-400">
|
||||
<h2 className="text-2xl font-semibold mb-2 text-blue-400">Upload Files</h2>
|
||||
<p className="text-sm">
|
||||
Files uploaded will be added to the current directory. Currently, directory
|
||||
uploads are not supported. If you want to upload a directory you can create a new one and upload the
|
||||
files into it.
|
||||
</p>
|
||||
<div
|
||||
className="my-5 border-2 border-dashed p-8 rounded-md text-center cursor-pointer border-gray-400 hover:bg-blue-100 hover:border-blue-500 transition-all duration-100"
|
||||
onDragEnter={dragEnter} onDragLeave={dragLeave} onDragOver={dragOver} onDrop={drop} onClick={click}
|
||||
>
|
||||
<input
|
||||
multiple
|
||||
type="file"
|
||||
className="hidden"
|
||||
ref={inputElement}
|
||||
onChange={inputChange}
|
||||
/>
|
||||
<p className="italic">Drag and drop files here or click to select</p>
|
||||
</div>
|
||||
);
|
||||
<FileList files={files}/>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={close}
|
||||
title="Close without uploading"
|
||||
className="bg-red-500 hover:bg-red-600 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 mx-2 rounded hover:cursor-pointer">
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={uploadFiles}
|
||||
title="Upload files"
|
||||
className="bg-blue-400 hover:bg-blue-500 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 rounded hover:cursor-pointer">
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -9,29 +9,29 @@ import "../index.css"
|
||||
* @constructor
|
||||
*/
|
||||
export default function UserInput({onChange}) {
|
||||
// Example of the controlled input state pattern
|
||||
const [email, setEmail] = useState("");
|
||||
// Example of the controlled input state pattern
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
// Generate ID for the input element
|
||||
const id = useId();
|
||||
// Generate ID for the input element
|
||||
const id = useId();
|
||||
|
||||
/**
|
||||
/**
|
||||
* Update the password controlled state
|
||||
* @param event {InputEvent}
|
||||
*/
|
||||
const updateEmail = (event) => {
|
||||
const email = event.target.value;
|
||||
setEmail(email);
|
||||
onChange(email);
|
||||
}
|
||||
const updateEmail = (event) => {
|
||||
const email = event.target.value;
|
||||
setEmail(email);
|
||||
onChange(email);
|
||||
}
|
||||
|
||||
return <input
|
||||
type="text"
|
||||
name={id}
|
||||
value={email}
|
||||
onInput={updateEmail}
|
||||
placeholder="Username"
|
||||
required={true}
|
||||
className="border border-gray-300 rounded-sm py-2 px-4 placeholder:italic w-full my-2"
|
||||
/>
|
||||
return <input
|
||||
type="text"
|
||||
name={id}
|
||||
value={email}
|
||||
onInput={updateEmail}
|
||||
placeholder="Username"
|
||||
required={true}
|
||||
className="border border-gray-300 rounded-sm py-2 px-4 placeholder:italic w-full my-2"
|
||||
/>
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import DirectoryList from "../components/DirectoryList.jsx";
|
||||
import PathDisplay from "../components/PathDisplay.jsx";
|
||||
import Navbar from "../components/Navbar.jsx";
|
||||
@ -9,414 +9,588 @@ import ChildrenLoading from "../components/ChildrenLoading.jsx";
|
||||
import DownloadLoading from "../components/DownloadLoading.jsx";
|
||||
import Uploader from "../components/Uploader.jsx";
|
||||
import CreateDirectory from "../components/CreateDirectory.jsx";
|
||||
import MoveDirectory from "../components/MoveDirectory.jsx";
|
||||
|
||||
export default function Dashboard() {
|
||||
// Store the default path
|
||||
const defaultPath = ["media", "vault"];
|
||||
// ---- CONSTANTS ---- //
|
||||
/**
|
||||
* Default path
|
||||
* Uses the .env var in local development, but when
|
||||
* pushed to dockerhub, the .env is ignored and the
|
||||
* default path is used.
|
||||
*/
|
||||
const defaultPath = (import.meta.env.VITE_DEFAULT_PATH || "media,vault").split(',');
|
||||
|
||||
/**
|
||||
* URL To the backend web server.
|
||||
* Uses the .env var in local development, but when
|
||||
* pushed to dockerhub, the .env is ignored and the real
|
||||
* backend URL is used.
|
||||
* @type {string}
|
||||
*/
|
||||
const backendUrl = import.meta.env.VITE_BACKEND_URL || "https://backend.gophernest.net";
|
||||
/**
|
||||
* URL To the backend web server.
|
||||
* Uses the .env var in local development, but when
|
||||
* pushed to dockerhub, the .env is ignored and the
|
||||
* real backend URL is used.
|
||||
*/
|
||||
const backendUrl = import.meta.env.VITE_BACKEND_URL || "https://backend.gophernest.net";
|
||||
|
||||
const [token, setToken] = useState(null);
|
||||
const [path, setPath] = useState([...defaultPath]);
|
||||
const [showHidden, setShowHidden] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
const [editing, setEditing] = useState("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [childrenLoading, setChildrenLoading] = useState(false);
|
||||
const [downloadLoading, setDownloadLoading] = useState(false);
|
||||
const [contentLoading, setContentLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
|
||||
|
||||
/**
|
||||
* The name of the value stored in local storage.
|
||||
* @type {string}
|
||||
*/
|
||||
const storage_id = "gophernest_credentials";
|
||||
// ---- STATE ---- //
|
||||
|
||||
/**
|
||||
* Update the token from the local or session storage.
|
||||
* This function assumes one of them exists.
|
||||
* If it does not, the token will be null.
|
||||
* This function will return the token as well, to allow for other usecases.
|
||||
*/
|
||||
const updateToken = () => {
|
||||
const t = localStorage.getItem(storage_id) ? localStorage.getItem(storage_id) : sessionStorage.getItem(storage_id);
|
||||
setToken(t);
|
||||
return t;
|
||||
};
|
||||
// General state
|
||||
const [token, setToken] = useState(null);
|
||||
const [path, setPath] = useState([...defaultPath]);
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async (token) => {
|
||||
const response = await fetch(`${backendUrl}/v1/children?path=/${path.join("/")}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error("Something went wrong");
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
// User settings
|
||||
const [showHidden, setShowHidden] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
|
||||
setChildrenLoading(true);
|
||||
// Modals
|
||||
const [error, setError] = useState(null);
|
||||
const [editing, setEditing] = useState("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [moving, setMoving] = useState(false);
|
||||
|
||||
// If the token doesnt exit, update it and use the return value.
|
||||
// This is a silly work around to prevent the first render from not working
|
||||
// TODO: Fix this shit.
|
||||
let tkn = token ? token : updateToken();
|
||||
|
||||
getData(tkn).then((data) => {
|
||||
setFiles(data);
|
||||
}).finally(() => {
|
||||
setChildrenLoading(false);
|
||||
}).catch((err) => {
|
||||
setError("Failed to fetch data from server.");
|
||||
console.error(err);
|
||||
});
|
||||
// Loading spinners
|
||||
const [childrenLoading, setChildrenLoading] = useState(false);
|
||||
const [downloadLoading, setDownloadLoading] = useState(false);
|
||||
const [contentLoading, setContentLoading] = useState(false);
|
||||
|
||||
|
||||
// Handle the state for the content being modified in the text editor.
|
||||
const [editingFileContent, setEditingFileContent] = useState("");
|
||||
|
||||
// Other
|
||||
const navigate = useNavigate();
|
||||
|
||||
// ---- FUNCTIONS ---- //
|
||||
|
||||
/**
|
||||
* Update the token from the local or session storage.
|
||||
* This function assumes one of them exists.
|
||||
* If it does not, the token will be null.
|
||||
* This function will return the token as well, to allow for other usecases.
|
||||
* @returns {string | null} Token from browser storage
|
||||
*/
|
||||
const updateToken = () => {
|
||||
const t = localStorage.getItem(storage_id) ? localStorage.getItem(storage_id) : sessionStorage.getItem(storage_id);
|
||||
setToken(t);
|
||||
return t;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the error in the error state.
|
||||
*/
|
||||
const clearError = () => setError(null);
|
||||
|
||||
/**
|
||||
* Toggle editing of a file.
|
||||
* @param path {string}
|
||||
*/
|
||||
const toggleEditing = (path) => setEditing(path);
|
||||
|
||||
/**
|
||||
* Close the file that is being edited.
|
||||
*/
|
||||
const exitFile = () => setEditing("");
|
||||
|
||||
/**
|
||||
* Show the hidden files
|
||||
*/
|
||||
const toggleHidden = (e) => setShowHidden(e.target.checked);
|
||||
|
||||
/**
|
||||
* Toggle the upload modal.
|
||||
* This can be used in the navbar and the close button!
|
||||
*/
|
||||
const toggleUploading = () => setUploading(!uploading);
|
||||
|
||||
/**
|
||||
* Show the creating modal.
|
||||
*/
|
||||
const createDir = () => {
|
||||
setSelected([]);
|
||||
setCreating(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the creating modal.
|
||||
*/
|
||||
const closeCreate = () => setCreating(false);
|
||||
|
||||
/**
|
||||
* Hide the moving modal.
|
||||
*/
|
||||
const closeMoving = () => setMoving(false);
|
||||
|
||||
/**
|
||||
* Show the moving modal.
|
||||
*/
|
||||
const showMoving = () => setMoving(true);
|
||||
|
||||
// ---- HANDLERS --- //
|
||||
|
||||
/**
|
||||
* Updates the path by slicing [0:index]
|
||||
* @param index {number} - Index to slice to.
|
||||
*/
|
||||
const updatePath = (index) => {
|
||||
let newPath = path.slice(0, index + 1);
|
||||
if (newPath.length < defaultPath.length) {
|
||||
newPath = [...defaultPath];
|
||||
}
|
||||
setPath(newPath);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the path back to the default.
|
||||
* TODO: Fix this in production? Not sure why
|
||||
*/
|
||||
const backHome = () => setPath([...defaultPath]);
|
||||
|
||||
/**
|
||||
* Add name to the path.
|
||||
* @param name {string} - Target child
|
||||
*/
|
||||
const appendPath = (name) => setPath([...path, name])
|
||||
|
||||
/**
|
||||
* Back arrow, goes back one directory (cd ..)
|
||||
*/
|
||||
const backArrow = () => {
|
||||
if (path.length > defaultPath.length) {
|
||||
setPath(path.slice(0, path.length - 1));
|
||||
}
|
||||
}
|
||||
|
||||
const exitAndSaveFile = (newContent) => {
|
||||
// Send request to server to update the file. This will return nothing
|
||||
// so no need for any promise handling.
|
||||
updateContent(editing, newContent).then((data) => {
|
||||
if (data.code === 200) {
|
||||
setEditing("");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This isn't fast, but hopefully the use case will be small batches.
|
||||
* @param file {string} - The file to toggle
|
||||
*/
|
||||
const toggleSelected = (file) => {
|
||||
if (!selected.includes(file)) {
|
||||
setSelected([...selected, file]);
|
||||
} else {
|
||||
const idx = selected.indexOf(file);
|
||||
setSelected([...selected.slice(0, idx), ...selected.slice(idx + 1, selected.length)])
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function for when the download button is clicked.
|
||||
*/
|
||||
const downloadFiles = () => {
|
||||
// Do not allow empty downloads
|
||||
if (selected.length === 0) {
|
||||
setError("Please select files/directories to download.");
|
||||
return
|
||||
}
|
||||
|
||||
setDownloadLoading(true);
|
||||
|
||||
const targets = selected.map((file) => `/${path.join("/")}/${file}`);
|
||||
|
||||
download(targets).catch((err) => {
|
||||
setError(`Download error: ${err}.`)
|
||||
}).finally(() => {
|
||||
setDownloadLoading(false)
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a directory or file in the backend
|
||||
* @param name {string} Name of new file or directory
|
||||
*/
|
||||
const createDirectory = (name) => {
|
||||
create(name, path).then((data) => {
|
||||
if (data.code === 201) {
|
||||
setCreating(false);
|
||||
}
|
||||
}).catch((error) => {
|
||||
setError(error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the selected files.
|
||||
*/
|
||||
const removeSelected = () => {
|
||||
if (selected.length === 0) {
|
||||
setError("Please select files or directories to delete");
|
||||
return;
|
||||
}
|
||||
|
||||
// Files are stored as arrays of paths
|
||||
const files = selected.map((file) => [...path, file]);
|
||||
|
||||
remove(files).then((data) => {
|
||||
if (data.code === 201) {
|
||||
setSelected([]);
|
||||
|
||||
}, [path, uploading, creating]);
|
||||
// Fetch the new files & deselect everything
|
||||
fetchFiles();
|
||||
}
|
||||
}).catch((error) => {
|
||||
setError(error);
|
||||
});
|
||||
};
|
||||
|
||||
const moveSelected = (newPath) => {
|
||||
const oldPath = "/" + [...path, selected].join("/");
|
||||
|
||||
// Redirect if the user isn't logged in, otherwise update the state.
|
||||
// Store the token in the storage, it should be attached to every request.
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem(storage_id) == null && sessionStorage.getItem(storage_id) == null) {
|
||||
navigate("/login");
|
||||
} else {
|
||||
updateToken();
|
||||
}
|
||||
}, [navigate]);
|
||||
console.log("@oldPath", oldPath);
|
||||
console.log("@newPath", newPath);
|
||||
|
||||
/**
|
||||
* Updates the path by slicing [0:index]
|
||||
* @param index {number} Index to slice to.
|
||||
*/
|
||||
const updatePath = (index) => {
|
||||
let newPath = path.slice(0, index + 1);
|
||||
if (newPath.length < defaultPath.length) {
|
||||
newPath = [...defaultPath];
|
||||
}
|
||||
setPath(newPath);
|
||||
};
|
||||
move(oldPath, newPath).then((data) => {
|
||||
if (data.code === 200) {
|
||||
setMoving(false);
|
||||
setSelected([]);
|
||||
fetchFiles();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the path back to the default.
|
||||
*/
|
||||
const backHome = () => {
|
||||
// TODO: Fix this in production
|
||||
setPath([...defaultPath]);
|
||||
};
|
||||
// ---- SERVER FUNCTIONS ---- //
|
||||
|
||||
/**
|
||||
* Add name to the path.
|
||||
* @param name Target child
|
||||
*/
|
||||
const appendPath = (name) => {
|
||||
setPath([...path, name])
|
||||
};
|
||||
|
||||
/**
|
||||
* Back arrow, goes back one directory (cd ..)
|
||||
*/
|
||||
const backArrow = () => {
|
||||
if (path.length > defaultPath.length) {
|
||||
setPath(path.slice(0, path.length - 1));
|
||||
}
|
||||
/**
|
||||
* Fetch a list of files in the current directory.
|
||||
*/
|
||||
const fetchFiles = () => {
|
||||
const getData = async (token) => {
|
||||
const response = await fetch(`${backendUrl}/v1/children?path=/${path.join("/")}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error("Something went wrong");
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* This isn't fast, but hopefully the use case will be small batches.
|
||||
* @param file {string} The file to toggle
|
||||
*/
|
||||
const toggleSelected = (file) => {
|
||||
if (!selected.includes(file)) {
|
||||
setSelected([...selected, file]);
|
||||
} else {
|
||||
const idx = selected.indexOf(file);
|
||||
setSelected([...selected.slice(0, idx), ...selected.slice(idx + 1, selected.length)])
|
||||
}
|
||||
};
|
||||
setChildrenLoading(true);
|
||||
|
||||
/**
|
||||
* Callback function for when the download button is clicked.
|
||||
*/
|
||||
const downloadFiles = () => {
|
||||
// Do not allow empty downloads
|
||||
if (selected.length === 0) {
|
||||
setError("Please select files/directories to download.");
|
||||
return
|
||||
}
|
||||
// If the token doesnt exit, update it and use the return value.
|
||||
// This is a silly work around to prevent the first render from not working
|
||||
// TODO: Fix this shit.
|
||||
let tkn = token ? token : updateToken();
|
||||
|
||||
const download = async (paths) => {
|
||||
try {
|
||||
const resp = await fetch(`${backendUrl}/v1/download`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({filePaths: paths}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json();
|
||||
setError(`Error ${data.code}: ${data.error}`);
|
||||
} else {
|
||||
// TODO: Figure out how tf this works.
|
||||
const blob = await resp.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = "downloads.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Download error: ${err}`);
|
||||
}
|
||||
};
|
||||
getData(tkn).then((data) => {
|
||||
setFiles(data);
|
||||
}).finally(() => {
|
||||
setChildrenLoading(false);
|
||||
}).catch((err) => {
|
||||
setError("Failed to fetch data from server.");
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
setDownloadLoading(true);
|
||||
setSelected([]);
|
||||
};
|
||||
|
||||
const targets = [];
|
||||
selected.forEach((file) => {
|
||||
targets.push(`/${path.join("/")}/${file}`);
|
||||
});
|
||||
/**
|
||||
* Update the contents of a file.
|
||||
* @param path {string} - Path to the file.
|
||||
* @param content {string} - New content to add to the file.
|
||||
* @return {any} - Server response, nothing.
|
||||
*/
|
||||
const updateContent = async (path, content) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/update`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ path, content }),
|
||||
})
|
||||
|
||||
// TODO: Implement UI for errors
|
||||
download(targets).catch((err) => {
|
||||
setError(`Download error: ${err}.`)
|
||||
}).finally(() => {
|
||||
setDownloadLoading(false)
|
||||
});
|
||||
};
|
||||
if (!resp.ok) setError("An error occurred when saving the file. Please try again.");
|
||||
|
||||
/**
|
||||
* Clear the error in the error state.
|
||||
*/
|
||||
const clearError = () => {
|
||||
setError(null);
|
||||
const json = resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Download a list of files.
|
||||
* @param paths {string[]} - List of paths to download.
|
||||
* @returns void
|
||||
*/
|
||||
const download = async (paths) => {
|
||||
try {
|
||||
const resp = await fetch(`${backendUrl}/v1/download`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ filePaths: paths }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json();
|
||||
setError(`Error ${data.code}: ${data.error}`);
|
||||
} else {
|
||||
// TODO: Figure out how tf this works.
|
||||
const blob = await resp.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = "downloads.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Download error: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the content of a file.
|
||||
* @param path {string} - File content.
|
||||
* @returns {any} - Server response with content.
|
||||
*/
|
||||
const fetchContent = async (path) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/content?path=${path}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!resp.ok) setError("Something went wrong! Failed to get file content.")
|
||||
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a file or directory.
|
||||
* @param name {string} - Name of the file or directory.
|
||||
* @param cwd {string[]} - Current directory as a list of paths.
|
||||
* @return {any} Server response.
|
||||
*/
|
||||
const create = async (name, cwd) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ name, cwd })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle editing of a file.
|
||||
* @param path {string}
|
||||
*/
|
||||
const toggleEditing = (path) => {
|
||||
setEditing(path);
|
||||
const json = resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a list of files.
|
||||
* @param files {string[][]} - List of file paths as list of sub-paths.
|
||||
* @returns {any} Server response.
|
||||
*/
|
||||
const remove = async (files) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ files, root: defaultPath })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
const move = async (oldPath, newPath) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/move`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ oldPath, newPath })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
|
||||
}
|
||||
// ---- EFFECTS ---- //
|
||||
|
||||
// Update the file list from the server each time an action requires
|
||||
// an update.
|
||||
useEffect(() => {
|
||||
fetchFiles();
|
||||
}, [path, uploading, creating]);
|
||||
|
||||
|
||||
// Redirect if the user isn't logged in, otherwise update the state.
|
||||
// Store the token in the storage, it should be attached to every request.
|
||||
useEffect(() => {
|
||||
(localStorage.getItem(storage_id) == null && sessionStorage.getItem(storage_id) == null)
|
||||
? navigate("/login")
|
||||
: updateToken();
|
||||
}, [navigate]);
|
||||
|
||||
// Load the content from the file into the editor and allow the user to begin
|
||||
// editing the file.
|
||||
useEffect(() => {
|
||||
setContentLoading(true);
|
||||
|
||||
// Prevent running when nothing is being edited. Also prevents a call on mount.
|
||||
if (editing) {
|
||||
// Fetch the data and handle errors accordingly
|
||||
fetchContent(editing).then((data) => {
|
||||
if (data.code === 200) {
|
||||
setEditingFileContent(data.content);
|
||||
} else {
|
||||
// An error occurred, do not open the editor
|
||||
setEditing("");
|
||||
setError(data.error);
|
||||
}
|
||||
}).finally(() => {
|
||||
setContentLoading(false)
|
||||
});
|
||||
}
|
||||
|
||||
}, [editing]);
|
||||
|
||||
|
||||
/**
|
||||
* This will be where the magic happens, where the files are upload
|
||||
* @param files {object[]} - List of files to upload.
|
||||
*/
|
||||
const upload = (files) => {
|
||||
const uploadFiles = async (_files) => {
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files', _files[i]); // 'files' is the field name
|
||||
}
|
||||
|
||||
// Add the current path to the form data.
|
||||
// formData.append('path', "/" + path.join("/"));
|
||||
formData.append('path', JSON.stringify(path));
|
||||
|
||||
const resp = await fetch(`${backendUrl}/v1/upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
const exitFile = () => {
|
||||
setEditing("");
|
||||
};
|
||||
if (files.length === 0) {
|
||||
setError("Cannot upload nothing. Please select files to upload.");
|
||||
return;
|
||||
}
|
||||
|
||||
const exitAndSaveFile = (newContent) => {
|
||||
const updateContent = async (path, content) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/update`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({path, content}),
|
||||
})
|
||||
if (!resp.ok) {
|
||||
setError("An error occurred when saving the file. Please try again.");
|
||||
}
|
||||
return await resp.json();
|
||||
};
|
||||
uploadFiles(files).then((data) => {
|
||||
if (data.code === 200) {
|
||||
setUploading(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Send request to server to update the file. This will return nothing
|
||||
// so no need for any promise handling.
|
||||
updateContent(editing, newContent).then((data) => {
|
||||
if (data.code === 200) {
|
||||
setEditing("");
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="w-full min-h-screen h-screen pb-8">
|
||||
<Navbar downloadFiles={downloadFiles} uploadFiles={toggleUploading} />
|
||||
<div className="h-full w-full flex flex-col items-center justify-center pb-8">
|
||||
|
||||
/**
|
||||
* Handle the state for the content being modified in the text editor.
|
||||
*/
|
||||
const [editingFileContent, setEditingFileContent] = useState("");
|
||||
useEffect(() => {
|
||||
const fetchContent = async (path) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/content?path=${path}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!resp.ok) {
|
||||
// TODO: Add this back in, its broken right now.
|
||||
setError("Something went wrong! Failed to get file content.")
|
||||
}
|
||||
return await resp.json();
|
||||
};
|
||||
{downloadLoading && <DownloadLoading />}
|
||||
{creating && <CreateDirectory close={closeCreate} create={createDirectory} />}
|
||||
{uploading && <Uploader close={toggleUploading} upload={upload} />}
|
||||
{moving && <MoveDirectory close={closeMoving} move={moveSelected} path={[...path, selected[0]]} />}
|
||||
|
||||
setContentLoading(true);
|
||||
// Prevent running when nothing is being edited. Also prevents a call on mount.
|
||||
if (editing) {
|
||||
// Fetch the data and handle errors accordingly
|
||||
fetchContent(editing).then((data) => {
|
||||
if (data.code === 200) {
|
||||
setEditingFileContent(data.content);
|
||||
} else {
|
||||
// An error occurred, do not open the editor
|
||||
setEditing("");
|
||||
setError(data.error);
|
||||
}
|
||||
}).finally(() => {
|
||||
setContentLoading(false)
|
||||
});
|
||||
{error && <Error error={error} clear={clearError} />}
|
||||
{
|
||||
(editing !== "" && !error) &&
|
||||
<Editor
|
||||
content={editingFileContent}
|
||||
path={editing}
|
||||
exit={exitFile}
|
||||
saveExit={exitAndSaveFile}
|
||||
loading={contentLoading}
|
||||
/>
|
||||
}
|
||||
|
||||
}, [editing]);
|
||||
<PathDisplay
|
||||
path={path}
|
||||
updatePath={updatePath}
|
||||
backHome={backHome}
|
||||
backArrow={backArrow}
|
||||
enabled={path.length > defaultPath.length}
|
||||
create={createDir}
|
||||
remove={removeSelected}
|
||||
removeEnable={selected.length > 0}
|
||||
move={showMoving}
|
||||
moveEnabled={selected.length === 1}
|
||||
/>
|
||||
|
||||
const toggleHidden = (e) => {
|
||||
setShowHidden(e.target.checked);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the upload modal.
|
||||
* This can be used in the navbar and the close button!
|
||||
*/
|
||||
const toggleUploading = () => {
|
||||
setUploading(!uploading);
|
||||
};
|
||||
|
||||
/**
|
||||
* This will be where the magic happens, where the files are upload
|
||||
* @param files {object[]}
|
||||
* TODO: Actually do something here...
|
||||
*/
|
||||
const upload = (files) => {
|
||||
const uploadFiles = async (_files) => {
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files', _files[i]); // 'files' is the field name
|
||||
}
|
||||
|
||||
// Add the current path to the form data.
|
||||
// formData.append('path', "/" + path.join("/"));
|
||||
formData.append('path', JSON.stringify(path));
|
||||
|
||||
const resp = await fetch(`${backendUrl}/v1/upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
if (files.length === 0) {
|
||||
setError("Cannot upload nothing. Please select files to upload.");
|
||||
return;
|
||||
}
|
||||
|
||||
uploadFiles(files).then((data) => {
|
||||
if (data.code === 200) {
|
||||
setUploading(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createDir = () => {
|
||||
setCreating(true);
|
||||
};
|
||||
|
||||
const closeCreate = () => {
|
||||
setCreating(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a directory or file in the backend
|
||||
* @param name {string} Name of new directory/file
|
||||
*/
|
||||
const createDirectory = (name) => {
|
||||
const create = async (name, cwd) => {
|
||||
const resp = await fetch(`${backendUrl}/v1/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({name, cwd})
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json()
|
||||
setError(data.error);
|
||||
return data;
|
||||
}
|
||||
|
||||
return await resp.json();
|
||||
};
|
||||
|
||||
create(name, path).then((data) => {
|
||||
if (data.code === 201) {
|
||||
setCreating(false);
|
||||
}
|
||||
}).catch((error) => {
|
||||
setError(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen h-screen pb-8">
|
||||
<Navbar downloadFiles={downloadFiles} uploadFiles={toggleUploading}/>
|
||||
<div className="h-full w-full flex flex-col items-center justify-center pb-8">
|
||||
{downloadLoading && <DownloadLoading/>}
|
||||
{creating && <CreateDirectory close={closeCreate} create={createDirectory}/>}
|
||||
{uploading && <Uploader close={toggleUploading} upload={upload}/>}
|
||||
|
||||
{error && <Error error={error} clear={clearError}/>}
|
||||
{(editing !== "" && !error) &&
|
||||
<Editor content={editingFileContent} path={editing} exit={exitFile} saveExit={exitAndSaveFile}
|
||||
loading={contentLoading}/>}
|
||||
|
||||
<PathDisplay path={path} updatePath={updatePath} backHome={backHome} backArrow={backArrow}
|
||||
enabled={path.length > defaultPath.length} create={createDir}/>
|
||||
<div className="w-2/3 h-5/6 overflow-y-auto border-1 border-gray-300">
|
||||
{childrenLoading && <ChildrenLoading/>}
|
||||
<DirectoryList dirs={files} showHidden={showHidden} appendPath={appendPath}
|
||||
toggleSelected={toggleSelected} toggleEditing={toggleEditing}/>
|
||||
</div>
|
||||
<div className="w-2/3 flex justify-end items-center">
|
||||
<label className="text-sm mx-2" htmlFor="showHiddenItems">Show Hidden Items</label>
|
||||
<input
|
||||
className="p-2"
|
||||
name="showHiddenItems"
|
||||
type="checkbox"
|
||||
checked={showHidden}
|
||||
onClick={toggleHidden}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-9/10 lg:w-2/3 h-5/6 overflow-y-auto border-1 border-gray-300">
|
||||
{childrenLoading && <ChildrenLoading />}
|
||||
<DirectoryList
|
||||
dirs={files}
|
||||
showHidden={showHidden}
|
||||
appendPath={appendPath}
|
||||
toggleSelected={toggleSelected} // TODO: Rework the toggleSelected functionality
|
||||
toggleEditing={toggleEditing}
|
||||
selected={selected}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
<div className="w-9/10 lg:w-2/3 flex justify-end items-center">
|
||||
<label className="text-sm mx-2" htmlFor="showHiddenItems">Show Hidden Items</label>
|
||||
<input
|
||||
className="p-2"
|
||||
name="showHiddenItems"
|
||||
type="checkbox"
|
||||
checked={showHidden}
|
||||
onClick={toggleHidden} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -2,22 +2,23 @@ import "../index.css";
|
||||
import LoginForm from "../components/LoginForm.jsx";
|
||||
|
||||
function Login() {
|
||||
/**
|
||||
* This is just for allowing easy changes to the domain, if a change occurs.
|
||||
* @type {string}
|
||||
*/
|
||||
const domain = "files.gophernest.net"
|
||||
/**
|
||||
* This is just for allowing easy changes to the domain, if a change occurs.
|
||||
* @type {string}
|
||||
*/
|
||||
const domain = "files.gophernest.net"
|
||||
|
||||
return <>
|
||||
<div className="min-h-screen h-screen w-full bg-gray-200 flex items-center justify-center">
|
||||
<h2 className="absolute top-0 left-0 font-[550] font-mono italic text-xl m-6">{domain}</h2>
|
||||
<div className="w-2/7 h-fit bg-white border-1 rounded-sm border-gray-300 p-12">
|
||||
<p className="text-gray-400">Please enter your details</p>
|
||||
<h1 className="text-3xl font-semibold mt-2 mb-12"> Welcome Back </h1>
|
||||
<LoginForm/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
return <>
|
||||
<div className="min-h-screen h-screen w-full bg-gray-200 flex items-center justify-center">
|
||||
<h2 className="absolute top-0 left-0 font-[550] font-mono italic text-xl m-6">{domain}</h2>
|
||||
|
||||
<div className="w-9/10 lg:w-2/7 h-fit bg-white border rounded-sm border-gray-300 p-12">
|
||||
<p className="text-gray-400">Please enter your details</p>
|
||||
<h1 className="text-3xl font-semibold mt-2 mb-12"> Welcome Back </h1>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default Login;
|
||||
@ -1,12 +1,12 @@
|
||||
import "../index.css"
|
||||
|
||||
export default function NotFound() {
|
||||
return <>
|
||||
<div className="min-h-screen h-screen w-full bg-gray-200 flex items-center justify-center">
|
||||
<div className="w-2/7 h-fit bg-white border-1 rounded-sm border-gray-300 p-12">
|
||||
<h1 className="text-6xl font-semibold mt-2 mb-12 opacity-50 text-gray-400"> 404 </h1>
|
||||
<p className="text-sm text-black"> This page could not be found or does not exist.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
return <>
|
||||
<div className="min-h-screen h-screen w-full bg-gray-200 flex items-center justify-center">
|
||||
<div className="w-2/7 h-fit bg-white border-1 rounded-sm border-gray-300 p-12">
|
||||
<h1 className="text-6xl font-semibold mt-2 mb-12 opacity-50 text-gray-400"> 404 </h1>
|
||||
<p className="text-sm text-black"> This page could not be found or does not exist.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
Reference in New Issue
Block a user