INNORIX
전송 빌더전송 파인더개발자리소스고객사
무료 시작하기
INNORIX

LET FILES
MOVE THEMSELVES

INNORIX는 모든 시스템과 환경에서 파일 이동과 자동화를 제공하는 엔터프라이즈 파일 인프라 기업입니다.
5,000개 이상의 기업 및 공공기관에서 사용하고 있습니다.

시작하기

  • 필요한 전송 만들기
  • 필요한 전송 찾기

주요 전송

  • 팀 업무 폴더 동기화
  • 고객에게 대용량 파일 전송
  • 여러 시스템 파일 탐색
  • FTP·SFTP·SCP·rsync 전환
  • 앱에 파일 전송 추가
  • 웹 업로드·다운로드 적용
  • AI·데이터 워크플로 구축
  • 모든 전송 둘러보기→

개발자

  • 개발자 센터
  • 구현 예제
  • API 빠른 시작
  • 개발자 가이드
  • API 레퍼런스
  • GitHub

리소스

  • 리소스 센터
  • 제품 가이드
  • 외부 서비스 연동
  • 배포 및 관리
  • 도움말 센터

고객사

  • 정부
  • 공공부문
  • 제조
  • 엔지니어링
  • 금융
  • 유통
  • IT/통신
  • 미디어
  • 의료
  • 교육

플랜

  • 가격 및 플랜

회사

INNORIX 소개

Exabyter를 찾고 계신가요?

이제 INNORIX Platform에 통합되었습니다

기타 INNORIX 제품

Al.bert — 스마트 교통 AI

글로벌 오피스

  • 미국 뉴욕
  • 대한민국 서울
  • 베트남 호찌민
  • 오피스 위치 보기→

(C)2026 INNORIX. All rights reserved.

  • 보안
  • 상태
  • 이용약관
  • 개인정보 처리방침
  • 쿠키 정책
  1. 개발자
  2. 구현 예제
  3. 웹 업로드 · 다운로드

웹 업로드 · 다운로드

INNORIX Transfer Builder를 사용해 웹 업로드 및 다운로드 파일 전송 흐름을 구성하고 애플리케이션용 설정을 생성합니다.

배포 및 관리
Exabyter
  • 한 곳으로 파일 보내기
  • 여러 곳으로 파일 보내기
  • 여러 곳에서 파일 수집
  • 시스템 간 파일 동기화
  • 웹 업로드 · 다운로드

웹 업로드 · 다운로드는 최종 사용자가 웹 브라우저에서 직접 파일을 올리고 받는 구성입니다. 에이전트를 설치하지 않은 외부 사용자도 대용량 파일을 주고받을 수 있고, 분할 전송·재개·진행률 표시는 웹 컨트롤이 처리합니다.

구성 요소는 세 가지입니다.

구성 역할
exabyter.js · exabyter.css 브라우저에 붙는 전송 컨트롤. 파일 선택 UI, 분할 전송, 진행률, 재개를 담당
upload.jsp / download.jsp 서버 수신·송신 엔드포인트. 실제 저장 경로를 정하는 곳
WEB-INF/lib/InnorixJAVA.jar 엔드포인트가 사용하는 전송 라이브러리

⚠️ 배포 전에 보안 항목을 먼저 확인하세요. 서버 엔드포인트는 브라우저가 보낸 값을 그대로 받습니다. 특히 다운로드는 fileName 을 파일 경로에 붙이는 구조라 경로 검증이 없으면 저장소 밖의 파일이 노출될 수 있습니다. 바로 아래 보안 필수 항목을 먼저 읽으세요.

ℹ️ 코드 탭 안내 — 이 구성은 브라우저 스크립트 + 서버 엔드포인트로 이루어지므로 예제는 JavaScript(클라이언트)와 JSP(서버) 두 탭으로 제공합니다. 빌더가 생성하는 번들도 이 두 가지 파일로 구성됩니다.

시작하기#

빌더가 만들어 주는 것#

빌더에서 웹 업로드 · 다운로드 탭의 옵션을 고르고 내려받으면 다음 구조의 zip 이 생성됩니다. 톰캣(또는 서블릿 컨테이너)의 webapps/ 아래에 그대로 풀면 바로 동작합니다.

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.txt

다운로드 모드를 고르면 같은 구조에 download.html · download.jsp 가 들어갑니다.

배포 후 수정할 곳은 사실상 엔드포인트의 저장 경로 한 군데입니다.

  • upload.jsp — directory(업로드 저장 루트), maxPostSize(요청 본문 최대 크기)
  • download.jsp — filePath(다운로드 원본 루트)

보안 필수 항목#

빌더가 만들어 주는 번들은 동작 확인을 위한 최소 구성입니다. 외부 사용자에게 공개하기 전에 다음 네 가지는 반드시 적용하세요.

항목 그대로 두면 해야 할 일
엔드포인트 인증 upload.jsp · download.jsp 에는 인증이 없어 URL 을 아는 누구나 호출합니다 페이지 맨 앞에서 세션·토큰을 검사하고 실패 시 401 로 종료
다운로드 경로 검증 fileName 이 그대로 파일 경로가 되어 ../ 로 저장 루트 밖 파일을 읽을 수 있습니다 정규화(canonical) 경로가 저장 루트 안인지 확인. 더 안전하게는 파일 ID → 실제 경로 매핑을 서버에서 수행하고 경로 자체를 받지 않기
업로드 저장 경로 기본값이 JSP 옆 data 폴더라 웹으로 노출될 수 있습니다 웹 공개 범위 밖의 절대 경로로 변경
CORS 요청 Origin 을 그대로 반사하면 임의 사이트에서 호출할 수 있습니다 허용 도메인 목록으로 고정

아래 구현 예제에는 인증 검사 · 경로 검증 · 도메인 제한이 이미 들어 있습니다. 값만 서비스에 맞게 바꿔서 쓰세요.

구현#

업로드#

컨트롤은 빈 <div> 하나에 렌더링됩니다.

html
<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");
%>

다운로드#

다운로드는 제공할 파일 목록을 컨트롤에 넘기는 것이 핵심입니다. setDownloadList() 에 넘긴 항목이 화면 목록이 되고, 사용자가 고른 항목만 내려받습니다.

ℹ️ 아래 JSP 는 인증 검사 · 경로 검증 · 무결성 응답까지의 핵심 부분입니다. 바이트 구간 계산과 스트리밍을 포함한 전체 파일은 번들의 download.jsp 에 들어 있습니다 (Get API Code 로 내려받습니다). 이 코드 상자만으로 다운로드 서버가 완성되지는 않습니다.

// 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.
%>

서버에서 꼭 손봐야 할 것#

위치 값 설명
upload.jsp directory 업로드 저장 루트. 기본값은 JSP 옆 data 폴더이므로 반드시 실제 경로로 바꾸세요.
upload.jsp maxPostSize 요청 본문 최대 크기(바이트). 앞단 프록시(nginx 등) 제한과 함께 맞춰야 합니다.
upload.jsp CORS 기본 코드는 요청 Origin 을 그대로 반사합니다. 운영에서는 허용 도메인으로 고정하세요.
download.jsp filePath 다운로드 원본 루트.
공통 인증 · 경로 검증 · CORS 위 보안 필수 항목의 네 가지를 적용했는지 배포 전에 다시 확인하세요.

사용자/전송 단위로 폴더를 나눌 때는 업로드 엔드포인트에서 이렇게 분기합니다.

java
// 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"));

참고#

빌더 옵션 ↔ 생성 코드 매핑#

업로드 탭

빌더 항목 선택지 반영되는 곳
Method Drag & drop enableDropZone: true
File select button enableDropZone: false + 첨부 버튼
Connect to existing UI 버튼 없이 control.upload() 를 기존 UI 에 직접 연결
Start method Click the upload button setTransferStart: { upload: 'manual' }
Auto-start after selecting files afterAddFiles 에서 control.upload() 호출
Selection target Single file maxFileCount: 1
Files and folders addFolder: true + 폴더 첨부 버튼
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 (바이트)
Max total size 1GB / 5GB / 10GB / 50GB / Custom maxTotalSize (바이트)
Duplicate file name Overwrite resumeType: 'overwrite'
Save with a new name / Don't overwrite resumeType: 'numbering'
Storage method Fixed folder upload.jsp 의 directory 그대로
Per-user / Per-upload / Per-user & per-upload upload.jsp 에서 uploader.setDirectory(...) 로 분기
Completion message 문구 입력 uploadComplete 핸들러의 상태 문구
Call API URL + POST/GET uploadComplete 에서 fetch(url, { method })
Developer event hooks 체크한 항목 control.on('uploadStart' | 'uploadProgress' | 'uploadComplete' | 'uploadError' | 'uploadCancel', …)

다운로드 탭

빌더 항목 반영되는 곳
제공할 파일 control.setDownloadList([{ downloadURL, displayFileName, fileSize, isFolder }])
시작 방식 setTransferStart: { download: 'manual' | 'auto' }
중복 이름 downloadDuplicate: 'overwrite' | 'numbering' | 'resume'
무결성 downloadIntegrity: true + setVerification: 'enable'
이벤트 훅 control.on('downloadStart' | 'downloadProgress' | 'downloadComplete' | 'downloadError' | 'downloadCancel', …)

ℹ️ 화면 구성(파일 목록 표시 항목), 파일명 규칙, 접근 권한, 제공 기간 같은 항목은 서비스 쪽에서 목록을 만들 때 결정합니다. 컨트롤에는 최종 목록만 넘기면 됩니다. 파일은 목록에 담긴 그대로 하나씩 전송됩니다.

자주 겪는 오류#

증상 원인과 해결
컨트롤이 안 보임 setElementID 가 가리키는 <div> 가 스크립트 실행 시점에 없거나, exabyter.js 로드 전에 create() 를 호출했습니다.
업로드 시작 직후 실패 setUploadURL 경로가 틀렸거나 upload.jsp 가 배포되지 않았습니다. 브라우저 네트워크 탭에서 404 를 먼저 확인하세요.
CORS 오류 컨트롤은 POST 전에 OPTIONS 를 보냅니다. Access-Control-Allow-* 헤더가 OPTIONS 응답에도 나가야 합니다.
큰 파일에서 413 maxPostSize 와 앞단 프록시의 본문 크기 제한을 함께 올리세요.
저장 경로가 이상한 곳 directory 기본값이 JSP 옆 data 폴더입니다. 절대 경로로 바꾸세요.
한글 파일명 깨짐 JSP 의 pageEncoding="UTF-8" 과 컨테이너의 URI 인코딩 설정(URIEncoding=UTF-8)을 확인하세요.
확장자 제한이 안 걸림 allowType 은 점 없이 소문자 배열입니다(['pdf','png']). 클라이언트 검증이므로 서버에서도 한 번 더 확인하세요.
다운로드 목록이 비어 있음 setDownloadList() 를 loadComplete 이후에 호출했는지, 항목에 displayFileName 이 있는지 확인하세요.
이전시스템 간 파일 동기화

이 페이지에서

  • 시작하기
  • 빌더가 만들어 주는 것
  • 보안 필수 항목
  • 구현
  • 업로드
  • 다운로드
  • 서버에서 꼭 손봐야 할 것
  • 참고
  • 빌더 옵션 ↔ 생성 코드 매핑
  • 자주 겪는 오류