added docker deployment and added a preview to the files.
This commit is contained in:
29
.dockerignore
Normal file
29
.dockerignore
Normal 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
1
.gitignore
vendored
@@ -27,4 +27,5 @@ replay_pid*
|
||||
target
|
||||
|
||||
*.env
|
||||
.idea
|
||||
|
||||
|
||||
9
dockerfile
Normal file
9
dockerfile
Normal 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"]
|
||||
@@ -15,6 +15,10 @@ public class Server {
|
||||
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.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 -> {
|
||||
String ua = ctx.header("User-Agent");
|
||||
|
||||
@@ -30,11 +30,6 @@ public class DownloadRoute {
|
||||
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()) {
|
||||
@@ -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
|
||||
private static void recursiveSearch(File dir, String relPath, String searchTerm, List<Map<String, Object>> result) {
|
||||
File[] files = dir.listFiles();
|
||||
@@ -147,4 +148,85 @@ public class DownloadRoute {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
10
src/main/resources/HTML/index.html
vendored
10
src/main/resources/HTML/index.html
vendored
@@ -21,6 +21,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="main-content">
|
||||
<div class="file-header">
|
||||
<div></div>
|
||||
@@ -30,10 +31,17 @@
|
||||
<div>MODIFIED</div>
|
||||
<div>ACTION</div>
|
||||
</div>
|
||||
|
||||
<div id="file-list" class="file-list"></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;">×</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 id="status-text">Ready</div>
|
||||
<div class="help-text">
|
||||
|
||||
123
src/main/resources/HTML/script.js
vendored
123
src/main/resources/HTML/script.js
vendored
@@ -57,6 +57,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
forwardStack = [];
|
||||
selectedIndex = -1;
|
||||
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 {
|
||||
// Download file
|
||||
let cur = getCurrentPath();
|
||||
@@ -64,9 +69,73 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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();
|
||||
statusText.textContent = 'Loading...';
|
||||
let path = getCurrentPath();
|
||||
@@ -93,14 +162,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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';
|
||||
@@ -110,25 +177,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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) {
|
||||
@@ -144,20 +207,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
actionDiv.appendChild(dlBtn);
|
||||
}
|
||||
div.appendChild(actionDiv);
|
||||
|
||||
// Click handlers
|
||||
div.onclick = () => {
|
||||
div.onclick = (e) => {
|
||||
selectItem(index);
|
||||
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);
|
||||
});
|
||||
|
||||
statusText.textContent = `${items.length} item${items.length !== 1 ? 's' : ''}`;
|
||||
|
||||
// Update button states
|
||||
backBtn.disabled = historyIndex <= 0;
|
||||
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>`;
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user