initial commit , first one was bullshit

This commit is contained in:
2025-08-05 21:38:40 +02:00
parent 13f85e3cdd
commit c6b453ef81
10 changed files with 846 additions and 0 deletions

10
.gitattributes vendored Normal file
View 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
View File

@@ -24,3 +24,7 @@
hs_err_pid*
replay_pid*
target
*.env

62
pom.xml vendored Normal file
View 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>

View 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");
}
});
}
}

View 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());
}
}
}

View 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"));
}
}

View 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
View 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
View 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
View 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;
}
}