initial commit , first one was bullshit
This commit is contained in:
10
.gitattributes
vendored
Normal file
10
.gitattributes
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
*.css linguist-vendored
|
||||||
|
*.js linguist-vendored
|
||||||
|
*.json linguist-vendored
|
||||||
|
*.html linguist-vendored
|
||||||
|
*.xml linguist-vendored
|
||||||
|
*.md linguist-documentation
|
||||||
|
*.txt linguist-documentation
|
||||||
|
*.properties linguist-documentation
|
||||||
|
*.yml linguist-documentation
|
||||||
|
*.yaml linguist-documentation
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,3 +24,7 @@
|
|||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
replay_pid*
|
replay_pid*
|
||||||
|
|
||||||
|
target
|
||||||
|
|
||||||
|
*.env
|
||||||
|
|
||||||
|
|||||||
62
pom.xml
vendored
Normal file
62
pom.xml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.filejet</groupId>
|
||||||
|
<artifactId>filejet</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.cdimascio</groupId>
|
||||||
|
<artifactId>dotenv-java</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.javalin</groupId>
|
||||||
|
<artifactId>javalin</artifactId>
|
||||||
|
<version>6.7.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.17.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<version>2.0.16</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>com.filejet.API.Server</mainClass>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
29
src/main/java/com/filejet/API/Server.java
Normal file
29
src/main/java/com/filejet/API/Server.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.filejet.API;
|
||||||
|
// server logic
|
||||||
|
|
||||||
|
import com.filejet.API.routes.DownloadRoute;
|
||||||
|
import com.filejet.API.util.EnvLoader;
|
||||||
|
import io.javalin.Javalin;
|
||||||
|
|
||||||
|
|
||||||
|
public class Server {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Javalin app = Javalin.create(config -> {
|
||||||
|
config.staticFiles.add("/HTML/");
|
||||||
|
}).start(EnvLoader.getPort());
|
||||||
|
|
||||||
|
app.get("/api/download", DownloadRoute::handle); // for ?path=... downloads
|
||||||
|
app.get("/api/download/{file}", DownloadRoute::handle); // legacy/compat
|
||||||
|
app.get("/api/list-isos", DownloadRoute::list);
|
||||||
|
|
||||||
|
app.before("/api/*", ctx -> {
|
||||||
|
String ua = ctx.header("User-Agent");
|
||||||
|
if (ua == null || ua.toLowerCase().contains("bot")
|
||||||
|
|| ua.toLowerCase().contains("curl")
|
||||||
|
) {
|
||||||
|
ctx.status(403).result("Forbidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
150
src/main/java/com/filejet/API/routes/DownloadRoute.java
Normal file
150
src/main/java/com/filejet/API/routes/DownloadRoute.java
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package com.filejet.API.routes;
|
||||||
|
|
||||||
|
import io.javalin.http.Context;
|
||||||
|
import com.filejet.API.util.EnvLoader;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
|
||||||
|
public class DownloadRoute {
|
||||||
|
// Use resource path for ISO_DIR
|
||||||
|
private static final String isoDir = EnvLoader.getISODir();
|
||||||
|
private static final AtomicInteger activeDownloaders = new AtomicInteger(0);
|
||||||
|
|
||||||
|
// Caffeine cache: key = path + '|' + searchTerm, value = List<Map<String, Object>>
|
||||||
|
private static final Cache<String, List<Map<String, Object>>> searchCache =
|
||||||
|
Caffeine.newBuilder().maximumSize(100).expireAfterWrite(java.time.Duration.ofMinutes(10)).build();
|
||||||
|
|
||||||
|
public static void list(Context ctx) {
|
||||||
|
// List files and folders in the given path (relative to ISO_DIR), or search recursively if 'search' param is present
|
||||||
|
String relPath = ctx.queryParam("path");
|
||||||
|
if (relPath == null || relPath.isEmpty()) relPath = "/";
|
||||||
|
String searchTerm = ctx.queryParam("search");
|
||||||
|
try {
|
||||||
|
var classLoader = Thread.currentThread().getContextClassLoader();
|
||||||
|
String fullPath = getFullPath(relPath);
|
||||||
|
var resource = classLoader.getResource(fullPath);
|
||||||
|
if (resource == null) {
|
||||||
|
ctx.status(404).json(new String[]{"Directory not found"});
|
||||||
|
return;
|
||||||
|
// Helper: construct the full resource path from isoDir and relPath
|
||||||
|
private static String getFullPath(String relPath) {
|
||||||
|
String basePath = isoDir.endsWith("/") ? isoDir : isoDir + "/";
|
||||||
|
return basePath + (relPath.startsWith("/") ? relPath.substring(1) : relPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File dir = new File(resource.toURI());
|
||||||
|
if (!dir.exists() || !dir.isDirectory()) {
|
||||||
|
ctx.status(404).json(new String[]{"Not a directory"});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> result;
|
||||||
|
if (searchTerm != null && !searchTerm.isEmpty()) {
|
||||||
|
String cacheKey = relPath + "|" + searchTerm.toLowerCase();
|
||||||
|
result = searchCache.getIfPresent(cacheKey);
|
||||||
|
if (result == null) {
|
||||||
|
result = new ArrayList<>();
|
||||||
|
recursiveSearch(dir, relPath, searchTerm.toLowerCase(), result);
|
||||||
|
searchCache.put(cacheKey, result);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
File[] files = dir.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
ctx.json(new Object[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result = new ArrayList<>();
|
||||||
|
for (File f : files) {
|
||||||
|
var entry = new HashMap<String, Object>();
|
||||||
|
entry.put("name", f.getName());
|
||||||
|
entry.put("isDir", f.isDirectory());
|
||||||
|
result.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.json(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
ctx.status(500).json(new String[]{"Error: " + e.getMessage()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: recursively search for files/folders containing searchTerm, add to result with relative path
|
||||||
|
private static void recursiveSearch(File dir, String relPath, String searchTerm, List<Map<String, Object>> result) {
|
||||||
|
File[] files = dir.listFiles();
|
||||||
|
if (files == null) return;
|
||||||
|
for (File f : files) {
|
||||||
|
String rel = relPath.endsWith("/") ? relPath + f.getName() : relPath + "/" + f.getName();
|
||||||
|
if (f.getName().toLowerCase().contains(searchTerm)) {
|
||||||
|
var entry = new HashMap<String, Object>();
|
||||||
|
entry.put("name", f.getName());
|
||||||
|
entry.put("isDir", f.isDirectory());
|
||||||
|
entry.put("path", rel);
|
||||||
|
result.add(entry);
|
||||||
|
}
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
recursiveSearch(f, rel, searchTerm, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handle(Context ctx) throws IOException {
|
||||||
|
// Download a file from a given path (relative to ISO_DIR)
|
||||||
|
String relPath = ctx.queryParam("path");
|
||||||
|
if (relPath == null || relPath.isEmpty()) {
|
||||||
|
ctx.status(400).result("Missing file path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var classLoader = Thread.currentThread().getContextClassLoader();
|
||||||
|
String basePath = isoDir.endsWith("/") ? isoDir : isoDir + "/";
|
||||||
|
String fullPath = basePath + (relPath.startsWith("/") ? relPath.substring(1) : relPath);
|
||||||
|
var resource = classLoader.getResource(fullPath);
|
||||||
|
if (resource == null) {
|
||||||
|
ctx.status(404).result("Not Found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File file = new File(resource.toURI());
|
||||||
|
if (!file.exists() || !file.isFile()) {
|
||||||
|
ctx.status(404).result("Not Found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int totalMB = EnvLoader.getMaxBandwitdh();
|
||||||
|
int active = activeDownloaders.incrementAndGet();
|
||||||
|
int perClickKB = (totalMB * 1024) / active;
|
||||||
|
try (InputStream fis = new FileInputStream(file);
|
||||||
|
OutputStream os = ctx.res().getOutputStream()) {
|
||||||
|
ctx.res().setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
|
||||||
|
ctx.res().setContentType("application/octet-stream");
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
int read;
|
||||||
|
long lastSent = System.nanoTime();
|
||||||
|
long byteSent = 0;
|
||||||
|
int throttleRate = perClickKB * 1024;
|
||||||
|
while ((read = fis.read(buf)) != -1) {
|
||||||
|
os.write(buf, 0, read);
|
||||||
|
byteSent += read;
|
||||||
|
long elapsedNs = System.nanoTime() - lastSent;
|
||||||
|
double elapsedSec = elapsedNs / 1_000_000_000.0;
|
||||||
|
double currentRate = byteSent / elapsedSec;
|
||||||
|
if (currentRate > throttleRate) {
|
||||||
|
long sleepTime = (long) ((byteSent / (double) throttleRate - elapsedSec) * 1000);
|
||||||
|
if (sleepTime > 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(sleepTime);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
// do nothing :3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
activeDownloaders.decrementAndGet();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
ctx.status(500).result("Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/main/java/com/filejet/API/util/EnvLoader.java
Normal file
29
src/main/java/com/filejet/API/util/EnvLoader.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.filejet.API.util;
|
||||||
|
|
||||||
|
// we load the env file from resources here
|
||||||
|
|
||||||
|
import io.github.cdimascio.dotenv.Dotenv;
|
||||||
|
|
||||||
|
public class EnvLoader {
|
||||||
|
//set dotenv up to use
|
||||||
|
private static final Dotenv dotenv = Dotenv.configure()
|
||||||
|
.directory("src/main/resources")
|
||||||
|
.load();
|
||||||
|
|
||||||
|
public static String get(String key) {
|
||||||
|
return dotenv.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getPort() {
|
||||||
|
// return a plain int (ports aren't float)
|
||||||
|
return Integer.parseInt(get("PORT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getISODir() {
|
||||||
|
return get("ISO_DIR");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getMaxBandwitdh() {
|
||||||
|
return Integer.parseInt(get("MAX_TOTAL_BANDWIDTH_MB"));
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/resources/Content/info.md
Normal file
9
src/main/resources/Content/info.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
store your files here for the webserver to host:
|
||||||
|
|
||||||
|
.env should be in resources and look like this:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
PORT=8080
|
||||||
|
ISO_DIR=Content/
|
||||||
|
MAX_TOTAL_BANDWIDTH_MB=10
|
||||||
|
```
|
||||||
45
src/main/resources/HTML/index.html
vendored
Normal file
45
src/main/resources/HTML/index.html
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<script src="script.js" defer></script>
|
||||||
|
<title>FileJet</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="terminal">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title-bar">-- FileJet --</div>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<button id="back-btn" class="btn">◀ Back</button>
|
||||||
|
<button id="forward-btn" class="btn">Forward ▶</button>
|
||||||
|
<button id="up-btn" class="btn">↑ Up</button>
|
||||||
|
<input type="text" id="address-input" class="address-bar" placeholder="Address..." readonly>
|
||||||
|
<input type="text" id="search-input" class="search-bar" placeholder="Search...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="file-header">
|
||||||
|
<div></div>
|
||||||
|
<div>NAME</div>
|
||||||
|
<div>TYPE</div>
|
||||||
|
<div>SIZE</div>
|
||||||
|
<div>MODIFIED</div>
|
||||||
|
<div>ACTION</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="file-list" class="file-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-bar">
|
||||||
|
<div id="status-text">Ready</div>
|
||||||
|
<div class="help-text">
|
||||||
|
↑↓: Navigate | ENTER: Open | ESC: Deselect | TAB: Focus controls
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
267
src/main/resources/HTML/script.js
vendored
Normal file
267
src/main/resources/HTML/script.js
vendored
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const fileList = document.getElementById('file-list');
|
||||||
|
const backBtn = document.getElementById('back-btn');
|
||||||
|
const forwardBtn = document.getElementById('forward-btn');
|
||||||
|
const upBtn = document.getElementById('up-btn');
|
||||||
|
const addressInput = document.getElementById('address-input');
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
const statusText = document.getElementById('status-text');
|
||||||
|
|
||||||
|
let history = ['/'];
|
||||||
|
let historyIndex = 0;
|
||||||
|
let forwardStack = [];
|
||||||
|
let selectedIndex = -1;
|
||||||
|
let currentItems = [];
|
||||||
|
|
||||||
|
function getCurrentPath() {
|
||||||
|
return history[historyIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAddressBar() {
|
||||||
|
let p = getCurrentPath();
|
||||||
|
addressInput.value = p === '/' ? 'Computer' : p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelection() {
|
||||||
|
const items = fileList.querySelectorAll('.file-item');
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
item.classList.toggle('focused', index === selectedIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectItem(index) {
|
||||||
|
const items = fileList.querySelectorAll('.file-item');
|
||||||
|
if (index >= 0 && index < items.length) {
|
||||||
|
selectedIndex = index;
|
||||||
|
updateSelection();
|
||||||
|
// Scroll into view
|
||||||
|
items[selectedIndex].scrollIntoView({ block: 'nearest' });
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
if (currentItems[selectedIndex]) {
|
||||||
|
const item = currentItems[selectedIndex];
|
||||||
|
statusText.textContent = `Selected: ${item.name} (${item.isDir ? 'Folder' : 'File'})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSelectedItem() {
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < currentItems.length) {
|
||||||
|
const item = currentItems[selectedIndex];
|
||||||
|
if (item.isDir) {
|
||||||
|
let cur = getCurrentPath();
|
||||||
|
let next = cur.endsWith('/') ? cur + item.name : cur + '/' + item.name;
|
||||||
|
history = history.slice(0, historyIndex + 1);
|
||||||
|
history.push(next);
|
||||||
|
historyIndex++;
|
||||||
|
forwardStack = [];
|
||||||
|
selectedIndex = -1;
|
||||||
|
fetchFiles();
|
||||||
|
} else {
|
||||||
|
// Download file
|
||||||
|
let cur = getCurrentPath();
|
||||||
|
let filePath = cur.endsWith('/') ? cur + item.name : cur + '/' + item.name;
|
||||||
|
window.location.href = `/api/download?path=${encodeURIComponent(filePath)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchFiles(searchTerm = '') {
|
||||||
|
setAddressBar();
|
||||||
|
statusText.textContent = 'Loading...';
|
||||||
|
let path = getCurrentPath();
|
||||||
|
let url = `/api/list-isos?path=${encodeURIComponent(path)}`;
|
||||||
|
if (searchTerm) {
|
||||||
|
url += `&search=${encodeURIComponent(searchTerm)}`;
|
||||||
|
}
|
||||||
|
fetch(url)
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch file list');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(items => {
|
||||||
|
fileList.innerHTML = '';
|
||||||
|
currentItems = [];
|
||||||
|
selectedIndex = -1;
|
||||||
|
if (!items.length) {
|
||||||
|
fileList.innerHTML = '<div class="file-item" style="grid-column: 1 / -1; text-align: center; color: #888;">No files or folders found.</div>';
|
||||||
|
statusText.textContent = 'No items found';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentItems = items;
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'file-item';
|
||||||
|
div.dataset.index = index;
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
const icon = document.createElement('img');
|
||||||
|
icon.className = 'icon';
|
||||||
|
icon.src = item.isDir ? 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIgM0MxLjQ0NzcyIDMgMSAzLjQ0NzcyIDEgNFYxMkMxIDEyLjU1MjMgMS40NDc3MiAxMyAyIDEzSDE0QzE0LjU1MjMgMTMgMTUgMTIuNTUyMyAxNSAxMlY2QzE1IDUuNDQ3NzIgMTQuNTUyMyA1IDE0IDVIOEw2IDNIMloiIGZpbGw9IiNGRkQ3MDAiLz4KPC9zdmc+' : 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgM0MyLjQ0NzcyIDMgMiAzLjQ0NzcyIDIgNFYxMkMyIDEyLjU1MjMgMi40NDc3MiAxMyAzIDEzSDEzQzEzLjU1MjMgMTMgMTQgMTIuNTUyMyAxNCAxMlY0QzE0IDMuNDQ3NzIgMTMuNTUyMyAzIDEzIDNIM1oiIGZpbGw9IiM4ODg4ODgiLz4KPC9zdmc+';
|
||||||
|
icon.onerror = () => icon.style.display = 'none';
|
||||||
|
div.appendChild(icon);
|
||||||
|
|
||||||
|
// Name (show relative path if searching)
|
||||||
|
const nameSpan = document.createElement('span');
|
||||||
|
nameSpan.className = 'file-name';
|
||||||
|
if (searchTerm && item.path) {
|
||||||
|
nameSpan.textContent = item.path;
|
||||||
|
} else {
|
||||||
|
nameSpan.textContent = item.name;
|
||||||
|
}
|
||||||
|
div.appendChild(nameSpan);
|
||||||
|
|
||||||
|
// Type
|
||||||
|
const typeSpan = document.createElement('span');
|
||||||
|
typeSpan.className = 'file-type';
|
||||||
|
typeSpan.textContent = item.isDir ? 'Folder' : 'File';
|
||||||
|
div.appendChild(typeSpan);
|
||||||
|
|
||||||
|
// Size
|
||||||
|
const sizeSpan = document.createElement('span');
|
||||||
|
sizeSpan.className = 'file-size';
|
||||||
|
sizeSpan.textContent = item.isDir ? '' : '-';
|
||||||
|
div.appendChild(sizeSpan);
|
||||||
|
|
||||||
|
// Date Modified
|
||||||
|
const dateSpan = document.createElement('span');
|
||||||
|
dateSpan.className = 'file-date';
|
||||||
|
dateSpan.textContent = '';
|
||||||
|
div.appendChild(dateSpan);
|
||||||
|
|
||||||
|
// Action
|
||||||
|
const actionDiv = document.createElement('div');
|
||||||
|
if (!item.isDir) {
|
||||||
|
const dlBtn = document.createElement('button');
|
||||||
|
dlBtn.className = 'download-btn';
|
||||||
|
dlBtn.textContent = 'Download';
|
||||||
|
dlBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
let cur = getCurrentPath();
|
||||||
|
let filePath = cur.endsWith('/') ? cur + item.name : cur + '/' + item.name;
|
||||||
|
window.location.href = `/api/download?path=${encodeURIComponent(filePath)}`;
|
||||||
|
};
|
||||||
|
actionDiv.appendChild(dlBtn);
|
||||||
|
}
|
||||||
|
div.appendChild(actionDiv);
|
||||||
|
|
||||||
|
// Click handlers
|
||||||
|
div.onclick = () => {
|
||||||
|
selectItem(index);
|
||||||
|
if (item.isDir) {
|
||||||
|
setTimeout(() => openSelectedItem(), 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fileList.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
statusText.textContent = `${items.length} item${items.length !== 1 ? 's' : ''}`;
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
backBtn.disabled = historyIndex <= 0;
|
||||||
|
forwardBtn.disabled = historyIndex >= history.length - 1;
|
||||||
|
upBtn.disabled = getCurrentPath() === '/';
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
fileList.innerHTML = `<div class="file-item" style="grid-column: 1 / -1; text-align: center; color: #d9534f;">${err.message}</div>`;
|
||||||
|
statusText.textContent = 'Error loading files';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation event handlers
|
||||||
|
backBtn.addEventListener('click', () => {
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
selectedIndex = -1;
|
||||||
|
fetchFiles();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
forwardBtn.addEventListener('click', () => {
|
||||||
|
if (historyIndex < history.length - 1) {
|
||||||
|
historyIndex++;
|
||||||
|
selectedIndex = -1;
|
||||||
|
fetchFiles();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
upBtn.addEventListener('click', () => {
|
||||||
|
let cur = getCurrentPath();
|
||||||
|
if (cur === '/' || cur === '') return;
|
||||||
|
let parts = cur.split('/').filter(Boolean);
|
||||||
|
parts.pop();
|
||||||
|
let upPath = '/' + parts.join('/');
|
||||||
|
if (upPath === '') upPath = '/';
|
||||||
|
history = history.slice(0, historyIndex + 1);
|
||||||
|
history.push(upPath);
|
||||||
|
historyIndex++;
|
||||||
|
selectedIndex = -1;
|
||||||
|
fetchFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
addressInput.addEventListener('click', () => {
|
||||||
|
addressInput.select();
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
selectedIndex = -1;
|
||||||
|
fetchFiles(e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.target.tagName === 'INPUT') return;
|
||||||
|
|
||||||
|
switch(e.key) {
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
if (selectedIndex > 0) {
|
||||||
|
selectItem(selectedIndex - 1);
|
||||||
|
} else if (currentItems.length > 0) {
|
||||||
|
selectItem(currentItems.length - 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
if (selectedIndex < currentItems.length - 1) {
|
||||||
|
selectItem(selectedIndex + 1);
|
||||||
|
} else if (currentItems.length > 0) {
|
||||||
|
selectItem(0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Enter':
|
||||||
|
e.preventDefault();
|
||||||
|
openSelectedItem();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Escape':
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = -1;
|
||||||
|
updateSelection();
|
||||||
|
statusText.textContent = `${currentItems.length} item${currentItems.length !== 1 ? 's' : ''}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Backspace':
|
||||||
|
e.preventDefault();
|
||||||
|
upBtn.click();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'F5':
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = -1;
|
||||||
|
fetchFiles(searchInput.value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '/':
|
||||||
|
e.preventDefault();
|
||||||
|
searchInput.focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
fetchFiles();
|
||||||
|
});
|
||||||
241
src/main/resources/HTML/styles.css
vendored
Normal file
241
src/main/resources/HTML/styles.css
vendored
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #000;
|
||||||
|
color: #eee;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background: #000;
|
||||||
|
border: 2px solid #333;
|
||||||
|
padding: 20px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #444;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover:not(:disabled) {
|
||||||
|
background: #444;
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus {
|
||||||
|
outline: 1px solid #fff;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-bar,
|
||||||
|
.search-bar {
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-bar {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-bar:focus,
|
||||||
|
.search-bar:focus {
|
||||||
|
outline: 1px solid #fff;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px 1fr 80px 100px 120px 100px;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #111;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #000;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px 1fr 80px 100px 120px 100px;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
background: #181818;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.selected {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.focused {
|
||||||
|
background: #333;
|
||||||
|
outline: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-type,
|
||||||
|
.file-size,
|
||||||
|
.file-date {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.file-list::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list::-webkit-scrollbar-track {
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list::-webkit-scrollbar-thumb {
|
||||||
|
background: #333;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal cursor effect */
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 50% { opacity: 1; }
|
||||||
|
51%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor::after {
|
||||||
|
content: '█';
|
||||||
|
color: #fff;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.file-header,
|
||||||
|
.file-item {
|
||||||
|
grid-template-columns: 24px 1fr 60px 80px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-header :nth-child(5),
|
||||||
|
.file-header :nth-child(6),
|
||||||
|
.file-item :nth-child(5),
|
||||||
|
.file-item :nth-child(6) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user