diff --git a/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/DownloadController.java b/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/DownloadController.java
index 5bb678e..b33baed 100644
--- a/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/DownloadController.java
+++ b/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/DownloadController.java
@@ -10,7 +10,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
-import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
@@ -28,10 +27,8 @@ public class DownloadController {
@Autowired
private AbstractStorageService storageService;
- @GetMapping("/download/{id}")
- public String page(@PathVariable String id, Model model) {
- fileRepository.findById(id)
- .ifPresent(f -> model.addAttribute("filename", f.getName()));
+ @GetMapping("/download")
+ public String page() {
return "download/page";
}
diff --git a/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/UploadController.java b/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/UploadController.java
index 4760da0..7eafe80 100644
--- a/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/UploadController.java
+++ b/Backend/src/main/java/com/gregor_lohaus/gtransfer/controller/UploadController.java
@@ -45,8 +45,7 @@ public class UploadController {
@RequestParam("hash") String hash,
@RequestParam("name") String name,
@RequestParam(required = false) Integer expiryDays,
- @RequestParam(required = false) Integer downloadLimit,
- Model model) throws IOException {
+ @RequestParam(required = false) Integer downloadLimit) throws IOException {
storageService.put(hash, file.getBytes());
@@ -57,7 +56,6 @@ public class UploadController {
f.setDownloadLimit(limit);
fileRepository.save(f);
- model.addAttribute("id", hash);
return "upload/result :: view";
}
}
diff --git a/Backend/src/main/resources/static/crypto.js b/Backend/src/main/resources/static/crypto.js
index 631b0ae..087c14a 100644
--- a/Backend/src/main/resources/static/crypto.js
+++ b/Backend/src/main/resources/static/crypto.js
@@ -16,13 +16,32 @@ async function encryptFile(arrayBuffer) {
// SHA-256(rawKey) → file identifier sent to server; server never sees the key itself
const rawKey = await crypto.subtle.exportKey('raw', key);
- const hash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', rawKey)))
- .map(b => b.toString(16).padStart(2, '0'))
- .join('');
+ const hash = await hashKey(rawKey);
- // Base64url-encode key for URL fragment
- const base64urlKey = btoa(String.fromCharCode(...new Uint8Array(rawKey)))
- .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
+ const base64urlKey = encodeKey(rawKey);
return { payload, hash, base64urlKey };
}
+
+async function hashKey(rawKey) {
+ return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', rawKey)))
+ .map(b => b.toString(16).padStart(2, '0'))
+ .join('');
+}
+
+async function decryptFile(payload, key) {
+ const bytes = new Uint8Array(payload);
+ const iv = bytes.slice(0, 12);
+ const ciphertext = bytes.slice(12);
+ return crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
+}
+
+function encodeKey(rawKey) {
+ return btoa(String.fromCharCode(...new Uint8Array(rawKey)))
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
+}
+
+function decodeKey(base64url) {
+ const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
+ return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
+}
diff --git a/Backend/src/main/resources/static/download.js b/Backend/src/main/resources/static/download.js
index 759b3c0..e93be04 100644
--- a/Backend/src/main/resources/static/download.js
+++ b/Backend/src/main/resources/static/download.js
@@ -1,87 +1,69 @@
-(async function () {
- const fragment = location.hash.slice(1);
- if (!fragment) {
- showError('No decryption key found in URL.');
- return;
- }
+const fragment = location.hash.slice(1);
+if (!fragment) {
+ showError('No decryption key found in URL.');
+} else try {
+ setStatus('Deriving key\u2026');
- try {
- setStatus('Deriving key\u2026');
+ const rawKeyBytes = decodeKey(fragment);
+ const id = await hashKey(rawKeyBytes);
- // Decode base64url → raw key bytes
- const base64 = fragment.replace(/-/g, '+').replace(/_/g, '/');
- const rawKeyBytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
+ const key = await crypto.subtle.importKey(
+ 'raw', rawKeyBytes, { name: 'AES-GCM' }, false, ['decrypt']
+ );
- // Import key
- const key = await crypto.subtle.importKey(
- 'raw', rawKeyBytes, { name: 'AES-GCM' }, false, ['decrypt']
- );
+ setStatus('Downloading\u2026');
+ const response = await fetch('/download/' + id + '/data');
- // Derive hash → verify it matches the file ID in the URL
- const hash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', rawKeyBytes)))
- .map(b => b.toString(16).padStart(2, '0'))
- .join('');
-
- const pathId = location.pathname.split('/').pop();
- if (hash !== pathId) {
- showError('Invalid link — key does not match file.');
- return;
- }
-
- setStatus('Downloading\u2026');
- const response = await fetch(location.pathname + '/data');
-
- if (response.status === 410) {
- showError('This file has expired or reached its download limit.');
- return;
- }
- if (!response.ok) {
- showError(`Download failed (${response.status}).`);
- return;
- }
-
- // Extract filename from Content-Disposition header
+ if (response.status === 410) {
+ showError('This file has expired or reached its download limit.');
+ } else if (!response.ok) {
+ showError(`Download failed (${response.status}).`);
+ } else {
const disposition = response.headers.get('Content-Disposition') || '';
const filename = disposition.match(/filename="?([^"]+)"?/)?.[1] || 'download';
setStatus('Decrypting\u2026');
- const encrypted = new Uint8Array(await response.arrayBuffer());
- const iv = encrypted.slice(0, 12);
- const ciphertext = encrypted.slice(12);
+ const plaintext = await decryptFile(await response.arrayBuffer(), key);
- const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
-
- // Trigger browser download
const url = URL.createObjectURL(new Blob([plaintext]));
const a = document.createElement('a');
a.href = url;
a.download = filename;
- a.click();
- URL.revokeObjectURL(url);
- showSuccess(filename);
+ htmx.swap(htmx.find('#download-state'), `
+
🔒
+ ${filename}
+ `,
+ { swapStyle: 'innerHTML' });
- } catch (err) {
- showError('Decryption failed: ' + err.message);
+ htmx.on(htmx.find('#download-btn'), 'click', () => {
+ a.click();
+ URL.revokeObjectURL(url);
+ showSuccess(filename);
+ });
}
-})();
+} catch (err) {
+ showError('Decryption failed: ' + err.message);
+}
function setStatus(msg) {
- const el = document.getElementById('download-status');
+ const el = htmx.find('#download-status');
if (el) el.textContent = msg;
}
function showSuccess(filename) {
- document.getElementById('download-state').innerHTML = `
+ htmx.swap(htmx.find('#download-state'), `
✅
${filename}
Your download has started.
- Send a file`;
+ Send a file`,
+ { swapStyle: 'innerHTML' });
}
function showError(msg) {
- document.getElementById('download-state').innerHTML = `
+ htmx.swap(htmx.find('#download-state'), `
⚠
${msg}
- Go home`;
+ Go home`,
+ { swapStyle: 'innerHTML' });
}
diff --git a/Backend/src/main/resources/static/upload.js b/Backend/src/main/resources/static/upload.js
index 8d77746..f70a771 100644
--- a/Backend/src/main/resources/static/upload.js
+++ b/Backend/src/main/resources/static/upload.js
@@ -1,54 +1,50 @@
-const dropZone = document.getElementById('drop-zone');
-const fileInput = document.getElementById('file-input');
+const dropZone = htmx.find('#drop-zone');
+const fileInput = htmx.find('#file-input');
const promptHtml = dropZone.innerHTML;
let selectedFile = null;
-// ── File selection ────────────────────────────────────────────────────────────
-
-dropZone.addEventListener('click', () => {
+htmx.on(dropZone, 'click', () => {
if (selectedFile === null) fileInput.click();
});
-fileInput.addEventListener('change', e => {
+htmx.on(fileInput, 'change', e => {
if (e.target.files[0]) onFileSelected(e.target.files[0]);
});
-dropZone.addEventListener('dragover', e => {
+htmx.on(dropZone, 'dragover', e => {
e.preventDefault();
- dropZone.classList.add('dragover');
+ htmx.addClass(dropZone, 'dragover');
});
-dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
+htmx.on(dropZone, 'dragleave', () => htmx.removeClass(dropZone, 'dragover'));
-dropZone.addEventListener('drop', e => {
+htmx.on(dropZone, 'drop', e => {
e.preventDefault();
- dropZone.classList.remove('dragover');
+ htmx.removeClass(dropZone, 'dragover');
if (e.dataTransfer.files[0] && selectedFile === null) onFileSelected(e.dataTransfer.files[0]);
});
function onFileSelected(file) {
selectedFile = file;
- // Use htmx to fetch the options form — server renders max values from config
htmx.ajax('GET', '/upload/options?name=' + encodeURIComponent(file.name), {
target: '#drop-zone',
swap: 'innerHTML'
});
}
-// ── Upload (called from onclick in server-rendered options form) ──────────────
-
async function startUpload() {
- const expiryDays = document.getElementById('expiry-days')?.value;
- const downloadLimit = document.getElementById('download-limit')?.value;
+ const expiryDays = htmx.find('#expiry-days')?.value;
+ const downloadLimit = htmx.find('#download-limit')?.value;
- dropZone.innerHTML = `
+ htmx.swap(dropZone, `
- Encrypting\u2026
`;
+ Encrypting\u2026
`,
+ { swapStyle: 'innerHTML' });
try {
const { payload, hash, base64urlKey } = await encryptFile(await selectedFile.arrayBuffer());
@@ -65,38 +61,33 @@ async function startUpload() {
const response = await fetch('/upload', { method: 'POST', body: formData });
if (!response.ok) throw new Error(`Server error ${response.status}`);
- // Server returns HTML fragment; prepend origin and append key fragment client-side
- dropZone.innerHTML = await response.text();
+ htmx.swap(dropZone, await response.text(), { swapStyle: 'innerHTML' });
htmx.process(dropZone);
- const shareLink = document.getElementById('share-link');
- shareLink.value = window.location.origin + shareLink.value + '#' + base64urlKey;
+ htmx.find('#share-link').value = window.location.origin + '/download#' + base64urlKey;
} catch (err) {
- dropZone.innerHTML = `
+ htmx.swap(dropZone, `
⚠
${err.message}
- `;
+ `,
+ { swapStyle: 'innerHTML' });
}
}
function setStatus(msg) {
- const el = document.getElementById('upload-status');
+ const el = htmx.find('#upload-status');
if (el) el.textContent = msg;
}
-// ── Reset (called from onclick in server-rendered fragments) ──────────────────
-
function resetUpload() {
selectedFile = null;
fileInput.value = '';
- dropZone.innerHTML = promptHtml;
+ htmx.swap(dropZone, promptHtml, { swapStyle: 'innerHTML' });
}
-// ── Copy link (called from onclick in result fragment) ────────────────────────
-
async function copyLink() {
- await navigator.clipboard.writeText(document.getElementById('share-link').value);
- const btn = document.getElementById('copy-btn');
+ await navigator.clipboard.writeText(htmx.find('#share-link').value);
+ const btn = htmx.find('#copy-btn');
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
}
diff --git a/Backend/src/main/resources/templates/download/page.html b/Backend/src/main/resources/templates/download/page.html
index 2ab594a..b42f137 100644
--- a/Backend/src/main/resources/templates/download/page.html
+++ b/Backend/src/main/resources/templates/download/page.html
@@ -3,10 +3,12 @@
- GTransfer
+ GTransfer
-
+
+
+
@@ -18,8 +20,7 @@
🔒
-
-
Preparing…
+
Preparing…
diff --git a/Backend/src/main/resources/templates/upload/result.html b/Backend/src/main/resources/templates/upload/result.html
index f623ee4..77828b6 100644
--- a/Backend/src/main/resources/templates/upload/result.html
+++ b/Backend/src/main/resources/templates/upload/result.html
@@ -5,8 +5,7 @@
✅
Your file is ready to share
-
+