added docker deployment and added a preview to the files.

This commit is contained in:
2025-08-06 13:15:35 +02:00
parent c6b453ef81
commit bb812914b8
7 changed files with 249 additions and 19 deletions

29
.dockerignore Normal file
View File

@@ -0,0 +1,29 @@
# ---> Java
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
target

1
.gitignore vendored
View File

@@ -27,4 +27,5 @@ replay_pid*
target target
*.env *.env
.idea

9
dockerfile Normal file
View File

@@ -0,0 +1,9 @@
#fjet dockerfile
FROM eclipse-temurin:17-jdk
WORKDIR /app
COPY pom.xml mvnw* .mvn/ ./
COPY src ./src
RUN ./mvnw clean package -DskipTests
RUN cp target/*.jar fjet.jar
EXPOSE 8080
CMD ["java", "-jar", "fjet.jar"]

View File

@@ -15,6 +15,10 @@ public class Server {
app.get("/api/download", DownloadRoute::handle); // for ?path=... downloads app.get("/api/download", DownloadRoute::handle); // for ?path=... downloads
app.get("/api/download/{file}", DownloadRoute::handle); // legacy/compat app.get("/api/download/{file}", DownloadRoute::handle); // legacy/compat
app.get("/api/list-isos", DownloadRoute::list); app.get("/api/list-isos", DownloadRoute::list);
app.get("/api/view", DownloadRoute::view); // for file preview
// New endpoint for directory version/hash
app.get("/api/dir-version", DownloadRoute::dirVersion);
app.before("/api/*", ctx -> { app.before("/api/*", ctx -> {
String ua = ctx.header("User-Agent"); String ua = ctx.header("User-Agent");

View File

@@ -30,11 +30,6 @@ public class DownloadRoute {
if (resource == null) { if (resource == null) {
ctx.status(404).json(new String[]{"Directory not found"}); ctx.status(404).json(new String[]{"Directory not found"});
return; 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()); File dir = new File(resource.toURI());
if (!dir.exists() || !dir.isDirectory()) { if (!dir.exists() || !dir.isDirectory()) {
@@ -71,6 +66,12 @@ public class DownloadRoute {
} }
} }
// 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);
}
// Helper: recursively search for files/folders containing searchTerm, add to result with relative path // 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) { private static void recursiveSearch(File dir, String relPath, String searchTerm, List<Map<String, Object>> result) {
File[] files = dir.listFiles(); File[] files = dir.listFiles();
@@ -147,4 +148,85 @@ public class DownloadRoute {
ctx.status(500).result("Error: " + e.getMessage()); ctx.status(500).result("Error: " + e.getMessage());
} }
} }
// TODO: Fix the refreshing of the files. it should be done
// but fucking isnt. idk what the fuck is going on
// Serve text/code file content for preview
public static void view(Context ctx) {
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 fullPath = getFullPath(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;
}
// Only allow preview for small files (e.g. <1MB)
if (file.length() > 1024 * 1024) {
ctx.status(413).result("File too large to preview");
return;
}
// Try to detect text
String mime = ctx.header("Accept");
if (mime == null || mime.isEmpty()) mime = "text/plain";
ctx.res().setContentType(mime);
try (InputStream is = new FileInputStream(file)) {
String text = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
ctx.result(text);
}
} catch (Exception e) {
ctx.status(500).result("Error: " + e.getMessage());
}
}
// Returns a hash/version of the directory contents for change detection
public static void dirVersion(io.javalin.http.Context ctx) {
String relPath = ctx.queryParam("path");
if (relPath == null || relPath.isEmpty()) relPath = "/";
try {
var classLoader = Thread.currentThread().getContextClassLoader();
String fullPath = getFullPath(relPath);
var resource = classLoader.getResource(fullPath);
if (resource == null) {
ctx.status(404).result("Directory not found");
return;
}
File dir = new File(resource.toURI());
if (!dir.exists() || !dir.isDirectory()) {
ctx.status(404).result("Not a directory");
return;
}
// Compute a hash based on file names and last modified times
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
md.update(f.getName().getBytes());
md.update(Long.toString(f.lastModified()).getBytes());
}
}
String hash = bytesToHex(md.digest());
ctx.json(java.util.Collections.singletonMap("version", hash));
} catch (Exception e) {
ctx.status(500).result("Error: " + e.getMessage());
}
}
// Helper: convert byte array to hex string (replacement for DatatypeConverter.printHexBinary)
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
} }

View File

@@ -21,6 +21,7 @@
</div> </div>
</div> </div>
<div class="main-content"> <div class="main-content">
<div class="file-header"> <div class="file-header">
<div></div> <div></div>
@@ -30,10 +31,17 @@
<div>MODIFIED</div> <div>MODIFIED</div>
<div>ACTION</div> <div>ACTION</div>
</div> </div>
<div id="file-list" class="file-list"></div> <div id="file-list" class="file-list"></div>
</div> </div>
<!-- File preview modal -->
<div id="preview-modal" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; background:rgba(0,0,0,0.85); z-index:1000; align-items:center; justify-content:center;">
<div style="background:#181818; color:#eee; max-width:80vw; max-height:80vh; min-width:320px; min-height:120px; border-radius:8px; box-shadow:0 2px 16px #000; padding:24px; position:relative; display:flex; flex-direction:column;">
<button id="close-preview" style="position:absolute; top:8px; right:12px; background:#222; color:#fff; border:none; font-size:18px; cursor:pointer;">&times;</button>
<pre id="preview-content" style="overflow:auto; font-family:monospace; font-size:15px; background:none; color:inherit; margin:0; white-space:pre-wrap;"></pre>
</div>
</div>
<div class="status-bar"> <div class="status-bar">
<div id="status-text">Ready</div> <div id="status-text">Ready</div>
<div class="help-text"> <div class="help-text">

View File

@@ -57,6 +57,11 @@ document.addEventListener('DOMContentLoaded', () => {
forwardStack = []; forwardStack = [];
selectedIndex = -1; selectedIndex = -1;
fetchFiles(); fetchFiles();
} else if (isPreviewable(item.name)) {
// Preview file
let cur = getCurrentPath();
let filePath = cur.endsWith('/') ? cur + item.name : cur + '/' + item.name;
showPreview(filePath, item.name);
} else { } else {
// Download file // Download file
let cur = getCurrentPath(); let cur = getCurrentPath();
@@ -64,9 +69,73 @@ document.addEventListener('DOMContentLoaded', () => {
window.location.href = `/api/download?path=${encodeURIComponent(filePath)}`; window.location.href = `/api/download?path=${encodeURIComponent(filePath)}`;
} }
} }
// Returns true if file is previewable (text, md, code, etc)
function isPreviewable(name) {
return /\.(txt|md|js|ts|json|css|html?|java|py|c|cpp|h|xml|sh|bat|ini|conf|log|csv|yml|yaml|go|rs|php|rb|pl|swift|kt|scala|cs|sql)$/i.test(name);
} }
function fetchFiles(searchTerm = '') { // Show preview modal and fetch file content
function showPreview(filePath, fileName) {
const modal = document.getElementById('preview-modal');
const content = document.getElementById('preview-content');
modal.style.display = 'flex';
content.textContent = 'Loading...';
fetch(`/api/view?path=${encodeURIComponent(filePath)}`)
.then(res => {
if (!res.ok) throw new Error('Failed to load file');
return res.text();
})
.then(text => {
content.textContent = text;
})
.catch(err => {
content.textContent = 'Error: ' + err.message;
});
}
// Close preview modal
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('preview-modal');
const closeBtn = document.getElementById('close-preview');
if (closeBtn) {
closeBtn.onclick = () => {
modal.style.display = 'none';
};
}
modal.onclick = (e) => {
if (e.target === modal) modal.style.display = 'none';
};
});
}
// Returns true if file is previewable (text, md, code, etc)
function isPreviewable(name) {
return /\.(txt|md|js|ts|json|css|html?|java|py|c|cpp|h|xml|sh|bat|ini|conf|log|csv|yml|yaml|go|rs|php|rb|pl|swift|kt|scala|cs|sql|java|cpp|c|py|rs|toml)$/i.test(name);
}
// Show preview modal and fetch file content
function showPreview(filePath, fileName) {
const modal = document.getElementById('preview-modal');
const content = document.getElementById('preview-content');
modal.style.display = 'flex';
content.textContent = 'Loading...';
fetch(`/api/view?path=${encodeURIComponent(filePath)}`)
.then(res => {
if (!res.ok) throw new Error('Failed to load file');
return res.text();
})
.then(text => {
content.textContent = text;
})
.catch(err => {
content.textContent = 'Error: ' + err.message;
});
}
let lastDirVersion = null;
let dirVersionPoller = null;
function fetchFiles(searchTerm = '', skipVersionCheck = false) {
setAddressBar(); setAddressBar();
statusText.textContent = 'Loading...'; statusText.textContent = 'Loading...';
let path = getCurrentPath(); let path = getCurrentPath();
@@ -93,14 +162,12 @@ document.addEventListener('DOMContentLoaded', () => {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'file-item'; div.className = 'file-item';
div.dataset.index = index; div.dataset.index = index;
// Icon // Icon
const icon = document.createElement('img'); const icon = document.createElement('img');
icon.className = 'icon'; icon.className = 'icon';
icon.src = item.isDir ? 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIgM0MxLjQ0NzcyIDMgMSAzLjQ0NzcyIDEgNFYxMkMxIDEyLjU1MjMgMS40NDc3MiAxMyAyIDEzSDE0QzE0LjU1MjMgMTMgMTUgMTIuNTUyMyAxNSAxMlY2QzE1IDUuNDQ3NzIgMTQuNTUyMyA1IDE0IDVIOEw2IDNIMloiIGZpbGw9IiNGRkQ3MDAiLz4KPC9zdmc+' : 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgM0MyLjQ0NzcyIDMgMiAzLjQ0NzcyIDIgNFYxMkMyIDEyLjU1MjMgMi40NDc3MiAxMyAzIDEzSDEzQzEzLjU1MjMgMTMgMTQgMTIuNTUyMyAxNCAxMlY0QzE0IDMuNDQ3NzIgMTMuNTUyMyAzIDEzIDNIM1oiIGZpbGw9IiM4ODg4ODgiLz4KPC9zdmc+'; icon.src = item.isDir ? 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIgM0MxLjQ0NzcyIDMgMSAzLjQ0NzcyIDEgNFYxMkMxIDEyLjU1MjMgMS40NDc3MiAxMyAyIDEzSDE0QzE0LjU1MjMgMTMgMTUgMTIuNTUyMyAxNSAxMlY2QzE1IDUuNDQ3NzIgMTQuNTUyMyA1IDE0IDVIOEw2IDNIMloiIGZpbGw9IiNGRkQ3MDAiLz4KPC9zdmc+' : 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgM0MyLjQ0NzcyIDMgMiAzLjQ0NzcyIDIgNFYxMkMyIDEyLjU1MjMgMi40NDc3MiAxMyAzIDEzSDEzQzEzLjU1MjMgMTMgMTQgMTIuNTUyMyAxNCAxMlY0QzE0IDMuNDQ3NzIgMTMuNTUyMyAzIDEzIDNIM1oiIGZpbGw9IiM4ODg4ODgiLz4KPC9zdmc+';
icon.onerror = () => icon.style.display = 'none'; icon.onerror = () => icon.style.display = 'none';
div.appendChild(icon); div.appendChild(icon);
// Name (show relative path if searching) // Name (show relative path if searching)
const nameSpan = document.createElement('span'); const nameSpan = document.createElement('span');
nameSpan.className = 'file-name'; nameSpan.className = 'file-name';
@@ -110,25 +177,21 @@ document.addEventListener('DOMContentLoaded', () => {
nameSpan.textContent = item.name; nameSpan.textContent = item.name;
} }
div.appendChild(nameSpan); div.appendChild(nameSpan);
// Type // Type
const typeSpan = document.createElement('span'); const typeSpan = document.createElement('span');
typeSpan.className = 'file-type'; typeSpan.className = 'file-type';
typeSpan.textContent = item.isDir ? 'Folder' : 'File'; typeSpan.textContent = item.isDir ? 'Folder' : 'File';
div.appendChild(typeSpan); div.appendChild(typeSpan);
// Size // Size
const sizeSpan = document.createElement('span'); const sizeSpan = document.createElement('span');
sizeSpan.className = 'file-size'; sizeSpan.className = 'file-size';
sizeSpan.textContent = item.isDir ? '' : '-'; sizeSpan.textContent = item.isDir ? '' : '-';
div.appendChild(sizeSpan); div.appendChild(sizeSpan);
// Date Modified // Date Modified
const dateSpan = document.createElement('span'); const dateSpan = document.createElement('span');
dateSpan.className = 'file-date'; dateSpan.className = 'file-date';
dateSpan.textContent = ''; dateSpan.textContent = '';
div.appendChild(dateSpan); div.appendChild(dateSpan);
// Action // Action
const actionDiv = document.createElement('div'); const actionDiv = document.createElement('div');
if (!item.isDir) { if (!item.isDir) {
@@ -144,20 +207,21 @@ document.addEventListener('DOMContentLoaded', () => {
actionDiv.appendChild(dlBtn); actionDiv.appendChild(dlBtn);
} }
div.appendChild(actionDiv); div.appendChild(actionDiv);
// Click handlers // Click handlers
div.onclick = () => { div.onclick = (e) => {
selectItem(index); selectItem(index);
if (item.isDir) { if (item.isDir) {
setTimeout(() => openSelectedItem(), 200); openSelectedItem();
} else if (isPreviewable(item.name)) {
// Open preview immediately on single click
let cur = getCurrentPath();
let filePath = cur.endsWith('/') ? cur + item.name : cur + '/' + item.name;
showPreview(filePath, item.name);
} }
}; };
fileList.appendChild(div); fileList.appendChild(div);
}); });
statusText.textContent = `${items.length} item${items.length !== 1 ? 's' : ''}`; statusText.textContent = `${items.length} item${items.length !== 1 ? 's' : ''}`;
// Update button states // Update button states
backBtn.disabled = historyIndex <= 0; backBtn.disabled = historyIndex <= 0;
forwardBtn.disabled = historyIndex >= history.length - 1; forwardBtn.disabled = historyIndex >= history.length - 1;
@@ -167,6 +231,39 @@ document.addEventListener('DOMContentLoaded', () => {
fileList.innerHTML = `<div class="file-item" style="grid-column: 1 / -1; text-align: center; color: #d9534f;">${err.message}</div>`; fileList.innerHTML = `<div class="file-item" style="grid-column: 1 / -1; text-align: center; color: #d9534f;">${err.message}</div>`;
statusText.textContent = 'Error loading files'; statusText.textContent = 'Error loading files';
}); });
// Start polling for directory version if not already started
if (!skipVersionCheck && !dirVersionPoller) {
startDirVersionPolling();
}
}
function startDirVersionPolling() {
if (dirVersionPoller) clearInterval(dirVersionPoller);
let path = getCurrentPath();
function poll() {
fetch(`/api/dir-version?path=${encodeURIComponent(path)}`)
.then(res => res.ok ? res.json() : null)
.then(data => {
if (!data || !data.version) return;
if (lastDirVersion && data.version !== lastDirVersion) {
// Directory changed, refresh file list
fetchFiles('', true); // skipVersionCheck=true to avoid infinite loop
}
lastDirVersion = data.version;
})
.catch(() => {});
}
poll();
dirVersionPoller = setInterval(poll, 3000); // poll every 3 seconds
}
// Stop polling when navigating away
function stopDirVersionPolling() {
if (dirVersionPoller) {
clearInterval(dirVersionPoller);
dirVersionPoller = null;
}
} }
// Navigation event handlers // Navigation event handlers