Web Upload & Download allows end users to upload and download files directly through a web browser. External users without an installed agent can also transfer large files, while the web control handles chunked transfers, resume, and progress display.
There are three components.
| Component | Role |
|---|---|
exabyter.js · exabyter.css |
Transfer control embedded in the browser. Handles the file selection UI, chunked transfers, progress, and resume |
upload.jsp / download.jsp |
Server receive/send endpoints. This is where the actual storage path is configured |
WEB-INF/lib/InnorixJAVA.jar |
Transfer library used by the endpoints |
⚠️ Review the security requirements before deployment. The server endpoints accept values sent by the browser as-is. In particular, downloads append
fileNameto the file path, so without path validation, files outside the storage root may be exposed. Read the Required security measures section below first.
ℹ️ Code tabs — Because this configuration consists of a browser script + server endpoint, the examples are provided in two tabs:
JavaScript(client) andJSP(server). The bundle generated by the builder also consists of these two file types.
Getting started#
What the builder generates#
Choose options in the Web Upload & Download tab of the builder and download them to generate a zip with the following structure.
Extract it directly under webapps/ in Tomcat (or another servlet container) and it will run immediately.
exabyter-web-upload/
upload.html <- page built from the options you picked + exabyter.create({...})
upload.jsp <- receiving endpoint (set the storage path here)
exabyter.js
exabyter.css
img/ font/ <- assets referenced by the css
WEB-INF/lib/InnorixJAVA.jar
README.txtIf you choose download mode, the same structure includes download.html · download.jsp.
After deployment, the main setting you need to change is the endpoint's storage path.
upload.jsp—directory(upload storage root),maxPostSize(maximum request body size)download.jsp—filePath(download source root)
Required security measures#
The bundle generated by the builder is a minimal configuration for verifying functionality. Before exposing it to external users, you must apply the following four measures.
| Item | If left unchanged | Required action |
|---|---|---|
| Endpoint authentication | upload.jsp · download.jsp have no authentication, so anyone who knows the URL can call them |
Check the session/token at the beginning of the page and return 401 on failure |
| Download path validation | fileName becomes part of the file path directly, so ../ could read files outside the storage root |
Verify that the canonical path remains inside the storage root. More securely, perform file ID → actual path mapping on the server and do not accept the path itself |
| Upload storage path | The default is a data folder next to the JSP, which may be exposed through the web |
Change it to an absolute path outside the web-accessible area |
| CORS | Reflecting the request Origin directly allows arbitrary sites to call the endpoint |
Restrict it to an allowlist of domains |
The implementation examples below already include authentication checks, path validation, and domain restrictions. Change only the values to match your service.
Implementation#
Upload#
The control is rendered inside a single empty <div>.
<link rel="stylesheet" type="text/css" href="./exabyter.css">
<script src="./exabyter.js"></script>
<div id="fileControl"></div>
<div class="toolbar">
<button onclick="control.openFileDialog()">Attach File</button>
<button onclick="control.directoryDialog()">Attach Folder</button>
<button onclick="control.upload()">Start Upload</button>
</div>// upload.html - same shape as the bootstrap code the builder generates.
var control = exabyter.create({
setElementID: '#fileControl',
setViewType: 'ListView',
transferMode: 'upload',
setUploadURL: './upload.jsp', // receiving endpoint
setTransferStart: { upload: 'manual' }, // 'auto' = start as soon as files are selected
// How files are attached - the builder's Method
enableDropZone: true, // Drag & drop
addFolder: true, // allow folder attachment (Files and folders)
// File policy - the builder's File & storage rules
allowType: ['pdf', 'docx', 'xlsx', 'png'], // omit to allow every type
maxFileCount: 50,
maxFileSize: 1073741824, // 1 GB (bytes)
maxTotalSize: 10737418240, // 10 GB (bytes)
resumeType: 'numbering', // duplicate name: overwrite | numbering | relay (resume)
// Appearance
showTransferWindow: true,
showTransferStatusIcon: true,
enableComplexFolders: false, // hide the folder tree on the left (flat list)
hideClientPath: true,
transferWindowTitle: 'Exabyter',
controlLang: 'ko', // control UI language
});
// Control is ready
control.on('loadComplete', function () {
setStatus('Ready.', 'ready');
});
// All files attached - call upload() here to start automatically
control.on('afterAddFiles', function () {
// control.upload();
});
control.on('uploadStart', function () {
// Upload started
});
control.on('uploadProgress', function () {
// Progress changed
});
control.on('uploadComplete', function () {
setStatus('Upload complete.', 'done');
// Call your service API on completion (the builder's Call API)
fetch('/your/api', { method: 'POST', credentials: 'include' });
});
control.on('uploadError', function () {
setStatus('Upload error - see the console for details.');
});
control.on('uploadCancel', function () {
// Canceled by the user
});<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.innorix.transfer.InnorixUpload" %>
<%
/* ============================================================================
* Exabyter - upload receiving endpoint
* The control slices the file and POSTs each slice; this page writes them into server storage.
* ==========================================================================*/
// --- Authentication (required) ------------------------------------------
// This page has no authentication of its own. Check your session/token first.
// The preflight (OPTIONS) arrives without credentials, so only POST is checked.
String userId = (String) session.getAttribute("userId");
if (request.getMethod().equals("POST") && userId == null) {
response.setStatus(401);
return;
}
// The control sends a CORS preflight (OPTIONS) before POST. Handle uploads on POST only.
if (request.getMethod().equals("POST"))
{
// --- Storage directory --------------------------------------------------
// Default: the "data" folder next to this JSP. Use an absolute path in production.
// directory = "C:/exabyter/data"; // Windows
// directory = "/storage/exabyter"; // Unix
String directory = InnorixUpload.getServletAbsolutePath(request);
directory = directory.substring(0, directory.lastIndexOf("/") + 1) + "data";
int maxPostSize = 2147482624; // max request body size in bytes - match your proxy limits
InnorixUpload uploader = new InnorixUpload(request, response, maxPostSize, directory);
// --- Storage method (the builder's Storage method) ----------------------
// One folder per transfer:
// uploader.setDirectory(directory + "/" + uploader.getParameter("_transferId"));
// One folder per user:
// uploader.setDirectory(directory + "/" + userId);
// One folder per user and per transfer:
// uploader.setDirectory(directory + "/" + userId + "/" + uploader.getParameter("_transferId"));
/*
* _action flags sent by the control:
* speedCheck - transfer speed measurement
* getServerInfo - server info probe
* getFileInfo - file info probe
* attachFile - slice upload in progress
* attachFileCompleted - file upload complete
*/
String _action = uploader.getParameter("_action");
String _orig_filename = uploader.getParameter("_orig_filename");
String _filesize = uploader.getParameter("_filesize");
String _transferId = uploader.getParameter("_transferId");
// Process the incoming slice.
String _run_retval = uploader.run();
uploader = null;
}
// --- CORS headers -----------------------------------------------------------
// List the allowed origins. Reflecting the request Origin lets any site call this endpoint.
java.util.List<String> allowedOrigins = java.util.Arrays.asList(
"https://portal.example.com", // <- replace with your service domain
"http://localhost:8080"); // <- development only, remove in production
String origin = request.getHeader("Origin");
if (origin != null && allowedOrigins.contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
}
response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Authorization,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type, html5");
%>Download#
The key to downloads is passing the list of files to offer to the control.
Items passed to setDownloadList() become the on-screen list, and only the items selected by the user are downloaded.
ℹ️ The JSP below contains the core logic for authentication checks, path validation, and integrity responses. The complete file, including byte-range calculations and streaming, is included in the bundle's
download.jsp(downloaded through Get API Code). This code block alone does not constitute a complete download server.
// download.html
var control = exabyter.create({
setElementID: '#fileControl',
setViewType: 'ListView',
transferMode: 'download',
setTransferStart: { download: 'manual' }, // 'auto' = start as soon as the list is set
downloadDuplicate: 'numbering', // duplicate name: overwrite | numbering | resume
downloadIntegrity: true, // verify file integrity
setVerification: 'enable',
showTransferWindow: true,
showTransferStatusIcon: true,
enableComplexFolders: false,
hideClientPath: true,
transferWindowTitle: 'Exabyter',
controlLang: 'ko',
});
control.on('loadComplete', function () {
setStatus('Ready.', 'ready');
});
control.on('downloadStart', function () { /* transfer started */ });
control.on('downloadProgress', function () { /* progress changed */ });
control.on('downloadComplete', function () {
setStatus('Download complete.', 'done');
});
control.on('downloadError', function () {
setStatus('Download error - see the console for details.');
});
control.on('downloadCancel', function () { /* canceled by the user */ });
// Files to offer - usually mapped straight from your service API response.
control.setDownloadList([
{
downloadURL: './download.jsp?fileName=report_2026Q3.pdf',
displayFileName: 'report_2026Q3.pdf', // name shown to the user
fileSize: 15728640, // bytes
isFolder: false,
},
{
downloadURL: './download.jsp?fileName=training_video.mp4',
displayFileName: 'training_video.mp4',
fileSize: 5368709120,
isFolder: false,
},
]);
// Wire up your buttons
// control.downloadSelectedFiles(); // selected items only
// control.download(); // everything<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.innorix.transfer.InnorixTransfer" %>
<%@ page import="com.innorix.integrity.Integrity" %>
<%@ page import="java.io.*" %>
<%
/* ============================================================================
* Exabyter - download sending endpoint
* The control requests byte ranges in parallel; this page streams the requested range.
* Integrity (MD5) checks and the start/complete signals arrive at the same endpoint.
* ==========================================================================*/
out.clear();
out = pageContext.pushBody();
// --- Authentication (required) ----------------------------------------------
// This page has no authentication of its own. Check your session/token first.
if (session.getAttribute("userId") == null) {
response.setStatus(401);
return;
}
// --- Storage root -----------------------------------------------------------
// Use an absolute path in production.
// filePath = "C:/exabyter/data/"; // Windows (use "/" as the separator)
// filePath = "/storage/exabyter/"; // Unix
String saveDir = request.getSession().getServletContext()
.getRealPath(request.getServletPath()).replace("\\", "/");
String filePath = saveDir.substring(0, saveDir.lastIndexOf("/") + 1) + "data/";
// Parameters the control adds automatically
String szStartOffset = request.getParameter("_StartOffset");
String szEndOffset = request.getParameter("_EndOffset");
String szPrintFileName = request.getParameter("fileName");
String szIntegrity = request.getParameter("_Integrity");
String action = request.getParameter("_Action");
// Start/complete signals - return with no body.
if (action != null && (action.equals("DownloadStart") || action.equals("DownloadComplete"))) {
return;
}
// Decode the file name
String sysFileName = szPrintFileName;
if (szPrintFileName != null) {
sysFileName = new String(szPrintFileName.getBytes("8859_1"), "UTF-8");
}
// --- Path validation (required) ---------------------------------------------
// fileName comes from the browser. Concatenating it directly lets "../" reach files
// outside the storage root, so always check that the canonical path stays inside it.
//
// Safer still: hand out a server-issued ID per file when you build the list
// (e.g. ./download.jsp?fileId=a1b2c3) and map ID -> real path here, never taking a path.
File root = new File(filePath).getCanonicalFile();
File target = new File(root, sysFileName).getCanonicalFile();
if (!target.getPath().startsWith(root.getPath() + File.separator) || !target.isFile()) {
response.setStatus(403);
return;
}
// --- Integrity check: return the MD5 of the requested range -----------------
if (szIntegrity != null && szIntegrity.equalsIgnoreCase("true")) {
long startOffset = szStartOffset != null ? Long.parseLong(szStartOffset) : 0;
long endOffset = szEndOffset != null ? Long.parseLong(szEndOffset) : 0;
Integrity integrity = new Integrity();
String _md5 = "";
try {
_md5 = integrity.getMD5FromFileSlice(target.getPath(), startOffset, endOffset);
} catch (Exception e) { }
response.setStatus(200);
response.getWriter().write(_md5);
response.getWriter().flush();
response.getWriter().close();
return;
}
// --- Stream the file (or range) ---------------------------------------------
// Only the validated target is used (never the raw user-supplied path).
response.setContentType("application/octet-stream");
response.setHeader("Accept-Ranges", "bytes");
// This is the core part. Range math for _StartOffset/_EndOffset and the actual streaming
// are implemented in the bundle's download.jsp - use that file as-is.
%>Server settings you must update#
| Location | Value | Description |
|---|---|---|
upload.jsp |
directory |
Upload storage root. The default is the data folder next to the JSP, so change it to the actual storage path. |
upload.jsp |
maxPostSize |
Maximum request body size (bytes). This must be aligned with limits on the front-end proxy (nginx, etc.). |
upload.jsp |
CORS | The default code reflects the request Origin directly. In production, restrict it to allowed domains. |
download.jsp |
filePath |
Download source root. |
| Common | Authentication · path validation · CORS | Before deployment, confirm again that the four Required security measures above have been applied. |
To separate folders by user/transfer, configure the upload endpoint as follows.
// Per transfer
uploader.setDirectory(directory + "/" + uploader.getParameter("_transferId"));
// Per user (take the user id from the session)
String userId = (String) session.getAttribute("userId");
uploader.setDirectory(directory + "/" + userId);
// Per user and per transfer
uploader.setDirectory(directory + "/" + userId + "/" + uploader.getParameter("_transferId"));Reference#
Builder options ↔ generated code mapping#
Upload tab
| Builder item | Option | Where it is applied |
|---|---|---|
| Method | Drag & drop | enableDropZone: true |
| File select button | enableDropZone: false + attach button |
|
| Connect to existing UI | Connect control.upload() directly to the existing UI without a button |
|
| Start method | Click the upload button | setTransferStart: { upload: 'manual' } |
| Auto-start after selecting files | Call control.upload() in afterAddFiles |
|
| Selection target | Single file | maxFileCount: 1 |
| Files and folders | addFolder: true + attach folder button |
|
| File type | Images / Documents / Videos / Custom | allowType: [...] |
| Max file count | 1 / 10 / 50 / 100 / Custom | maxFileCount |
| Max file size | 100MB / 1GB / 5GB / 10GB / Custom | maxFileSize (bytes) |
| Max total size | 1GB / 5GB / 10GB / 50GB / Custom | maxTotalSize (bytes) |
| Duplicate file name | Overwrite | resumeType: 'overwrite' |
| Save with a new name / Don't overwrite | resumeType: 'numbering' |
|
| Storage method | Fixed folder | Use directory in upload.jsp as-is |
| Per-user / Per-upload / Per-user & per-upload | Branch with uploader.setDirectory(...) in upload.jsp |
|
| Completion message | Enter message | Status message in the uploadComplete handler |
| Call API | URL + POST/GET | fetch(url, { method }) in uploadComplete |
| Developer event hooks | Selected items | control.on('uploadStart' | 'uploadProgress' | 'uploadComplete' | 'uploadError' | 'uploadCancel', …) |
Download tab
| Builder item | Where it is applied |
|---|---|
| Files to offer | control.setDownloadList([{ downloadURL, displayFileName, fileSize, isFolder }]) |
| Start method | setTransferStart: { download: 'manual' | 'auto' } |
| Duplicate name | downloadDuplicate: 'overwrite' | 'numbering' | 'resume' |
| Integrity | downloadIntegrity: true + setVerification: 'enable' |
| Event hooks | control.on('downloadStart' | 'downloadProgress' | 'downloadComplete' | 'downloadError' | 'downloadCancel', …) |
ℹ️ Items such as screen configuration (displayed file-list fields), file naming rules, access permissions, and availability period are determined by your service when building the list. Pass only the final list to the control. Files are transferred one by one exactly as listed.
Common errors#
| Symptom | Cause and solution |
|---|---|
| Control does not appear | The <div> referenced by setElementID does not exist when the script runs, or create() was called before exabyter.js loaded. |
| Upload fails immediately after starting | The setUploadURL path is incorrect or upload.jsp has not been deployed. Check for a 404 in the browser Network tab first. |
| CORS error | The control sends OPTIONS before POST. The Access-Control-Allow-* headers must also be included in the OPTIONS response. |
| 413 on large files | Increase both maxPostSize and the request body size limit on the front-end proxy. |
| Files are saved to the wrong location | The default directory is the data folder next to the JSP. Change it to an absolute path. |
| Korean filenames are corrupted | Check the JSP's pageEncoding="UTF-8" and the container's URI encoding setting (URIEncoding=UTF-8). |
| Extension restriction is not applied | allowType is a lowercase array without dots (['pdf','png']). Because this is client-side validation, validate it again on the server. |
| Download list is empty | Confirm that setDownloadList() is called after loadComplete and that each item has displayFileName. |