@namphuongtechnologi/azure-blob-transfer
Thư viện upload file dung lượng lớn (Large File Upload) lên Azure Blob Storage dành cho Frontend (framework-agnostic / Vanilla TypeScript). Sử dụng kỹ thuật Block/Chunk upload giúp tối ưu tốc độ, giảm thiểu lỗi mạng, hỗ trợ Retry, Tạm dừng/Tiếp tục (Pause/Resume), và tích hợp hoàn chỉnh vòng đời Upload Session với Backend của bạn.
English Quick Start
Full documentation (in Vietnamese) is below. This section covers the essentials for international users.
A framework-agnostic TypeScript library for uploading large files from the browser to Azure Blob Storage via the block/chunk upload pattern, with a backend session lifecycle wired in.
npm install @namphuongtechnologi/azure-blob-transfer @azure/storage-blob
import { uploadLargeFile, UploadError } from '@namphuongtechnologi/azure-blob-transfer';
const controller = uploadLargeFile({
file, // from <input type="file"> — a Blob (with optional name) works too
chunkSize: 8 * 1024 * 1024, // 8MB per block
concurrency: 4, // parallel stageBlock workers
session: {
// Your backend returns a SAS URL (w+c on one blob) + uploadId
createSession: async (file) => {
const res = await fetch('/api/files/create-upload-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName: file.name, contentType: file.type, fileSize: file.size }),
});
return res.json(); // { uploadUrl, uploadId, blobName, chunkSize? }
},
completeSession: async (uploadId) => {
await fetch('/api/files/complete-upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadId }),
});
},
cancelSession: async (uploadId) => {
await fetch('/api/files/cancel-upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadId }),
});
},
},
// Optional: called when the SAS expires mid-upload — including during
// the final commit — to fetch a fresh SAS URL. Called at most
// `maxSasRefreshes` times (default 3) per upload.
refreshUploadUrl: async (uploadId) => {
const res = await fetch('/api/files/refresh-upload-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadId }),
});
return (await res.json()).data.uploadUrl;
},
onProgress: (p) => console.log(`${p.percent}% — ${p.bytesPerSecond} B/s`),
});
// controller.pause() / controller.resume() / controller.cancel()
try {
const result = await controller.promise; // { totalBytes, totalChunks, durationMs, uploadId, blobName? }
} catch (error) {
if (error instanceof UploadError) {
// error.code: UPLOAD_ABORTED | CHUNK_UPLOAD_FAILED | COMMIT_FAILED | SAS_EXPIRED
// | NETWORK_ERROR | INVALID_CONFIG | SESSION_CREATE_FAILED | SESSION_COMPLETE_FAILED
}
}
Download Large Files (English Quick Start)
import {
downloadLargeFile,
DownloadError,
saveBlobAs,
} from '@namphuongtechnologi/azure-blob-transfer';
const downloadCtrl = downloadLargeFile({
url: 'https://mystorage.blob.core.windows.net/mycontainer/large-video.mp4?sp=r&st=...',
chunkSize: 8 * 1024 * 1024, // 8MB per chunk
concurrency: 4, // 4 parallel range workers
saveAs: true, // Automatically trigger browser file download on completion
onProgress: (p) => {
console.log(
`Progress: ${p.percent}% (${p.downloadedBytes}/${p.totalBytes} bytes, speed: ${p.bytesPerSecond} B/s, ETA: ${p.etaMs}ms)`
);
},
});
// downloadCtrl.pause();
// downloadCtrl.resume();
// downloadCtrl.cancel();
try {
const result = await downloadCtrl.promise;
console.log('Download complete:', result.fileName, result.totalBytes, result.blob);
} catch (error) {
if (error instanceof DownloadError) {
// error.code: DOWNLOAD_ABORTED | CHUNK_DOWNLOAD_FAILED | RANGE_NOT_SUPPORTED | CHECKSUM_MISMATCH | NETWORK_ERROR
}
}
Key behaviors: parallel workers with fail-fast abort of all in-flight requests on error · exponential backoff + jitter retries (transient errors only) · pause/resume without deadlock · timeout + cancel() stay effective for the whole body transfer, not just until headers arrive · automatic single-stream fallback when the server/proxy ignores Range (the already-open response is reused) · If-Match: <ETag> sent on every ranged request so a file modified mid-download fails fast instead of mixing versions · incremental MD5 (per chunk, no extra full-file copy) with automatic verification against a Content-MD5 response header when expectedMd5 is not supplied · always await controller.promise (an unhandled rejection can crash Node).
Note: configuration errors (
INVALID_CONFIG) are thrown synchronously bydownloadLargeFile(...)— wrap the call intry/catch; all runtime errors reject asynchronously viacontroller.promise.
Các tính năng nổi bật (Features)
- Tải lên song song (Parallel chunk upload) — Upload nhiều chunk cùng lúc giúp tận dụng tối đa băng thông mạng.
- Quản lý vòng đời Session (Session lifecycle) — Tự động gọi API Backend của bạn để tạo Session (create), thông báo hoàn thành (complete) hoặc hủy bỏ (cancel).
- Tạm dừng và Tiếp tục (Pause/Resume) — Tạm dừng upload bất kỳ lúc nào; các chunk đang upload dở sẽ hoàn thành rồi dừng lại, sau đó tiếp tục từ đúng vị trí đã dừng. Kết hợp option
resume: true, thư viện còn tự bỏ qua các block đã upload từ phiên trước (sau khi reload trang hoặc mở lại tab) — xem lưu ý cuối mục 4. - Hủy tải lên (Cancel) — Hủy quá trình upload, tự động gửi tín hiệu abort để ngắt các request đang chạy và báo cho Backend dọn dẹp.
- Tự động thử lại (Auto-retry) — Khi một chunk bị lỗi mạng, thư viện tự động thử lại (retry) với thuật toán Exponential Backoff và Jitter, tránh nghẽn server.
- Tự làm mới SAS URL (SAS refresh) — Khi SAS token hết hạn giữa chừng, hook
refreshUploadUrlcho phép lấy SAS mới từ Backend và tiếp tục upload tự động. - Toàn vẹn dữ liệu (Chunk MD5) — Option
computeChecksumtính MD5 từng chunk (mặc định chạy trong Web Worker để không chặn UI thread) và gửi kèmtransactionalContentMD5để Azure xác minh dữ liệu của mỗi block. - Blob metadata & index tags — Gắn
blobMetadatavà/hoặcblobTagslên blob tại thời điểm commit. - Tải file lớn từ URL (Large file download) — Hàm
downloadLargeFiletải song song theo HTTP Range với pause/resume/cancel, tự fallback single-stream khi server không hỗ trợ Range, kèm xác minh MD5 — xem mục 8. - Theo dõi tiến độ (Progress tracking) — Cập nhật tiến độ theo thời gian thực: số byte đã upload, phần trăm, số chunk hoàn thành, kèm tốc độ trung bình (
bytesPerSecond) và thời gian còn lại ước tính (etaMs). - TypeScript Support — Hỗ trợ TypeScript đầy đủ với strict typing.
- Gọn nhẹ (Lightweight) — Ít dependencies, chỉ yêu cầu
@azure/storage-blobnhư một peer dependency.
Khi nào nên dùng thư viện này thay vì Azure SDK thuần?
SDK @azure/storage-blob đã có sẵn BlockBlobClient.uploadData() với parallelism + retry tích hợp. Hãy chọn thư viện này khi bạn cần một trong các điều sau:
- Vòng đời session gắn với Backend: create → upload → complete/cancel tích hợp API của bạn — SDK không có khái niệm này.
- Pause/Resume do UI điều khiển giữa chừng upload thông qua
UploadController. - Tiếp tục upload sau khi reload trang với option
resume: true— bỏ qua các block đã staged ở phiên trước. - Cancel bởi người dùng kèm dọn dẹp phía Backend qua
cancelSession. - Tự làm mới SAS giữa chừng upload với hook
refreshUploadUrl— tính năng SDK không cung cấp.
Ngược lại, nếu chỉ cần upload từ Node/backend mà không có vòng đời session, SDK thuần là đủ.
1. Cài đặt (Installation)
Sử dụng npm hoặc yarn để cài đặt thư viện cùng với thư viện của Azure:
npm install @namphuongtechnologi/azure-blob-transfer @azure/storage-blob
(Nếu bạn dùng yarn: yarn add @namphuongtechnologi/azure-blob-transfer @azure/storage-blob)
Lưu ý về Node.js: Trên browser,
Fileluôn có sẵn. Trên Node, globalFilechỉ ổn định từ Node.js ≥ 20. Từ bản 1.1.0, tham sốfilenhận cảBlob(kèmnametùy chọn) nên trên Node 18 không cần polyfillFile— truyềnBlobtrực tiếp là đủ.
2. Cấu hình Azure Storage CORS (Bắt buộc)
Vì trình duyệt (Frontend) sẽ upload trực tiếp file (các chunks) lên Azure Storage thông qua URL (SAS URL), bạn bắt buộc phải cấu hình CORS (Cross-Origin Resource Sharing) trên Azure Storage Account của mình.
Hướng dẫn cấu hình qua Azure Portal:
- Đăng nhập vào Azure Portal.
- Chọn Storage Account mà bạn sẽ dùng để lưu trữ file.
- Ở menu bên trái, cuộn xuống phần Settings, chọn Resource sharing (CORS).
- Tại tab Blob service, thêm một Rule mới với các thông số sau:
- Allowed origins: Nhập domain web của bạn (VD:
https://your-web-domain.com,http://localhost:3000khi dev). Dùng*để cho phép mọi domain (không khuyến khích trên production). - Allowed methods: Đánh dấu chọn
PUT,OPTIONS,GET,HEAD. - Allowed headers: Nhập
*(Cho phép tất cả các headers). - Exposed headers: Nhập
ETag, x-ms-request-id, x-ms-version, x-ms-blob-content-md5(hoặc đơn giản là*). - Max age: Nhập
3600(1 giờ).
- Allowed origins: Nhập domain web của bạn (VD:
- Nhấn Save để lưu cấu hình.
3. Luồng hoạt động (Upload Flow)
Thư viện hoạt động kết hợp chặt chẽ với Backend của bạn. Thư viện sẽ gọi Backend qua 3 hàm callback mà bạn cung cấp.
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ createSession│ ──► │ Upload Chunks│ ──► │ commitBlockList │ ──► │ completeSession │
│ (your API) │ │ (Azure Blob) │ │ (Azure Blob) │ │ (your API) │
└─────────────┘ └──────────────┘ └─────────────────┘ └──────────────────┘
│ (nếu lỗi mạng / user bấm Cancel)
▼
┌──────────────┐
│cancelSession │
│ (your API) │
└──────────────┘
createSession: Frontend gọi API Backend để Backend tạo SAS URL an toàn (có giới hạn thời gian, quyền ghi) cho file chuẩn bị upload, và trả về cho Frontend. Backend có thể lưu trạng thái upload (Pending) vào database.- Upload Chunks & commitBlockList: Thư viện tự động chia nhỏ file, upload trực tiếp lên Azure Blob song song, và commit sau khi xong. Không qua server Backend của bạn để tiết kiệm băng thông.
completeSession: Sau khi commit thành công trên Azure, gọi Backend để đánh dấu file đã upload hoàn tất (Completed).cancelSession: Nếu có lỗi hoặc user chủ động hủy, gọi Backend để hủy (xóa SAS, xóa bản ghi tạm...).
4. Hướng dẫn sử dụng (Usage Guide)
Bước 1: Khai báo Backend APIs (Ví dụ minh họa)
Backend của bạn cần cung cấp 3 API. (Ví dụ đây là payload mà Frontend sẽ gửi).
- POST
/api/files/create-upload-session(Tạo session)- Headers:
{ 'Content-Type': 'application/json', 'x-api-key': '...' } - Request:
{ fileName, fileSize, contentType } - Response:
{ data: { uploadId, uploadUrl, blobName?, chunkSize? } }hoặc{ uploadId, uploadUrl, blobName?, chunkSize? }(Trong đóuploadUrl/sasUrllà SAS URL do Backend sinh ra,uploadIdlà ID theo dõi trên Backend).
- Headers:
- POST
/api/files/complete-upload(Hoàn thành)- Headers:
{ 'Content-Type': 'application/json', 'x-api-key': '...' } - Request:
{ uploadId, blobName? }
- Headers:
- POST
/api/files/cancel-upload(Hủy bỏ)- Headers:
{ 'Content-Type': 'application/json', 'x-api-key': '...' } - Request:
{ uploadId }
- Headers:
Bước 2: Tích hợp thư viện vào Frontend (Ví dụ với React Hook)
Nếu dự án của bạn sử dụng tính năng upload ở nhiều nơi, bạn nên tạo một Custom Hook (useAzureUpload) để gói gọn toàn bộ logic state và tái sử dụng dễ dàng.
1. Tạo file useAzureUpload.ts:
import { useState, useRef, useCallback } from 'react';
import {
uploadLargeFile,
UploadController,
UploadProgress,
UploadError,
} from '@namphuongtechnologi/azure-blob-transfer';
export function useAzureUpload() {
const [progress, setProgress] = useState<UploadProgress | null>(null);
const [status, setStatus] = useState<
'idle' | 'uploading' | 'paused' | 'completed' | 'error' | 'cancelled'
>('idle');
const [errorMessage, setErrorMessage] = useState<string>('');
const controllerRef = useRef<UploadController | null>(null);
const startUpload = useCallback(async (file: File) => {
try {
setStatus('uploading');
setErrorMessage('');
setProgress(null);
// Khởi tạo upload với các callback API mặc định
const controller = uploadLargeFile({
file,
concurrency: 4,
onProgress: setProgress,
// Nếu API khác, truyền `session: { ... }` vào đây
});
controllerRef.current = controller;
// Chờ hoàn thành
const result = await controller.promise;
setStatus('completed');
return result;
} catch (error) {
if (error instanceof UploadError && error.code === 'UPLOAD_ABORTED') {
setStatus('cancelled');
} else {
setStatus('error');
setErrorMessage(error instanceof Error ? error.message : 'Lỗi không xác định');
}
throw error;
} finally {
controllerRef.current = null;
}
}, []);
const pause = useCallback(() => {
if (controllerRef.current) {
controllerRef.current.pause();
setStatus('paused');
}
}, []);
const resume = useCallback(() => {
if (controllerRef.current) {
controllerRef.current.resume();
setStatus('uploading');
}
}, []);
const cancel = useCallback(() => {
if (controllerRef.current) {
controllerRef.current.cancel();
}
}, []);
return { startUpload, pause, resume, cancel, progress, status, errorMessage };
}
2. Sử dụng Hook trong Component:
Giờ đây Component của bạn sẽ cực kỳ ngắn gọn và sạch sẽ:
import React, { useState } from 'react';
import { useAzureUpload } from './useAzureUpload';
export default function FileUploader() {
const [file, setFile] = useState<File | null>(null);
// Tái sử dụng mọi logic từ Hook
const { startUpload, pause, resume, cancel, progress, status, errorMessage } = useAzureUpload();
const handleUpload = async () => {
if (!file) return;
try {
const result = await startUpload(file);
alert(`Tải lên thành công! (Upload ID: ${result.uploadId})`);
} catch (error) {
console.log('Upload không thành công');
}
};
return (
<div style={{ padding: 20, border: '1px solid #ccc', maxWidth: 500, fontFamily: 'sans-serif' }}>
<h3>Tải file lớn lên Azure</h3>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
disabled={status === 'uploading' || status === 'paused'}
/>
<div style={{ marginTop: 20 }}>
{status === 'idle' && <button onClick={handleUpload}>Bắt đầu Tải lên</button>}
{status === 'uploading' && <button onClick={pause}>Tạm Dừng</button>}
{status === 'paused' && <button onClick={resume}>Tiếp Tục</button>}
{(status === 'uploading' || status === 'paused') && (
<button onClick={cancel} style={{ marginLeft: 10, color: 'red' }}>
Hủy Bỏ
</button>
)}
</div>
{progress && (
<div style={{ marginTop: 20 }}>
<p>
Trạng thái: <strong>{status.toUpperCase()}</strong>
</p>
<p>
Tiến độ: <b>{progress.percent}%</b>
</p>
<div
style={{
width: '100%',
backgroundColor: '#eee',
height: 20,
borderRadius: 4,
overflow: 'hidden',
}}
>
<div
style={{
width: `${progress.percent}%`,
backgroundColor: 'green',
height: '100%',
transition: 'width 0.3s',
}}
/>
</div>
<p style={{ color: '#666', fontSize: 13, marginTop: 8 }}>
Đã tải: {(progress.uploadedBytes / 1024 / 1024).toFixed(2)} MB /{' '}
{(progress.totalBytes / 1024 / 1024).toFixed(2)} MB <br />
Chunks: {progress.completedChunks}/{progress.totalChunks}
</p>
</div>
)}
{status === 'error' && <p style={{ color: 'red', marginTop: 20 }}>Lỗi: {errorMessage}</p>}
{status === 'cancelled' && <p style={{ color: 'orange', marginTop: 20 }}>Đã hủy tải lên.</p>}
</div>
);
}
Pause/Resume hoạt động như thế nào (quan trọng):
Trong cùng một phiên trang:
pause()/resume()giữ trạng thái upload (vị trí chunk, danh sách block, phiên làm việc) trong bộ nhớ của phiên JavaScript hiện tại — hoạt động hoàn hảo khi user tạm dừng rồi tiếp tục mà không reload trang.Sau khi reload trang / đóng tab / app crash: trạng thái trong bộ nhớ đã mất, nhưng bạn có thể tiếp tục upload bằng option
resume: true. Ngay saucreateSession, thư viện gọigetBlockList('uncommitted')trên blob để biết những block nào đã được staged ở lần trước và bỏ qua chúng — chỉ upload các chunk còn thiếu; tiến độ được cộng dồn sẵn từ số byte đã upload. Block ID được sinh cố định theo index chunk (base64 của index đệm 8 chữ số), nên cùng một file với cùngchunkSizesẽ cho cùng bộ block ID ở mọi phiên.Để chế độ này hoạt động, cần thỏa mãn:
- Backend trả lại đúng phiên cũ: app của bạn tự persist
uploadId(VD localStorage/IndexedDB) và gửi kèm khi gọicreateSessionđể Backend trả về cùng session / cùng SAS URL của cùng blob đó. Flow này do app + Backend của bạn quyết định, thư viện không tự persist trạng thái. - SAS URL cần thêm quyền đọc
r(ngoàiw+c) để đọc được block list. chunkSizephải giống nhau giữa các phiên (kể cả giá trị Backend trả về trong kết quảcreateSession) — khácchunkSizenghĩa là khác block ID, mọi chunk sẽ upload lại từ đầu.- Azure chỉ giữ các block chưa commit trong 7 ngày; quá hạn này block bị xóa và upload chạy lại từ đầu.
- Nếu việc hỏi block list thất bại (SAS thiếu quyền đọc, lỗi mạng...), thư viện log warning và upload từ chunk 0 — an toàn, chỉ tốn lại băng thông.
- Backend trả lại đúng phiên cũ: app của bạn tự persist
5. Xử lý lỗi (Error Handling)
Thư viện ném ra lỗi UploadError chứa mã lỗi (code) cụ thể giúp bạn dễ dàng hiển thị thông báo thân thiện cho người dùng.
Lưu ý quan trọng:
controller.promisephải luôn được xử lý (awaittrongtry/catchhoặc gắn.catch()), kể cả sau khi gọicancel()— promise sẽ reject với mãUPLOAD_ABORTED. Promise bị reject mà không ai xử lý sẽ trở thành unhandled rejection (browser chỉ log, Node ≥ 15 có thể crash process).Hủy trong giai đoạn commit/complete: Từ bản 1.1.0,
cancel()bị bỏ qua (kèm cảnh báo console) khi upload đang ở trạng tháicommittinghoặccompleting. Lý do: tại thời điểm đó blob có thể đã tồn tại trên Azure — nếu abort thắng cuộc đua,cancelSessionsẽ xóa nhầm file đã upload hoàn chỉnh; nếu Azure đã nhận commit, blob trở thành "mồ côi" vì backend tưởng phiên đã bị hủy. Hai giai đoạn này rất nhanh (thường < 1 giây), upload sẽ tự hoàn thành vàcontroller.promiseresolve bình thường. Nếu thực sự cần bỏ phiên sau khi đã commit, backend của bạn nên chủ động xóa blob thông qua nghiệp vụ riêng.
import { UploadError } from '@namphuongtechnologi/azure-blob-transfer';
// Bên trong khối catch...
catch (error) {
if (error instanceof UploadError) {
switch (error.code) {
case 'SESSION_CREATE_FAILED':
alert('Lỗi: Không thể kết nối với server để tạo phiên tải lên.');
break;
case 'NETWORK_ERROR':
alert('Lỗi: Sự cố kết nối mạng (offline, DNS, timeout) khi gọi API Backend. Kiểm tra mạng và thử lại.');
break;
case 'UPLOAD_ABORTED':
console.log('Người dùng đã hủy quá trình tải lên.');
break;
case 'CHUNK_UPLOAD_FAILED':
alert(`Lỗi mạng: Không thể tải lên chunk thứ ${error.chunkIndex}. Dù đã thử lại nhiều lần.`);
break;
case 'COMMIT_FAILED':
alert('Lỗi: Đã tải xong nhưng không thể xác nhận (commit) với Azure.');
break;
case 'SAS_EXPIRED':
// Nếu có cấu hình refreshUploadUrl, thư viện đã tự thử lấy SAS mới (tối đa `maxSasRefreshes` lần, mặc định 3) trước khi ném lỗi này.
alert('Lỗi: Phiên làm việc (SAS Token) đã hết hạn. Vui lòng thử lại.');
break;
case 'SESSION_COMPLETE_FAILED':
alert('Lỗi: Tải lên Azure thành công nhưng không thể cập nhật trạng thái với Backend.');
break;
case 'INVALID_CONFIG':
console.error('Lỗi code: Cấu hình thư viện sai.');
break;
default:
alert('Lỗi không xác định.');
}
} else {
console.error('Lỗi:', error);
}
}
5.1. Tự làm mới SAS URL khi hết hạn (SAS Refresh)
Với file rất lớn trên mạng chậm, thời gian upload có thể vượt quá thời hạn của SAS token. Truyền callback refreshUploadUrl — thư viện sẽ tự động gọi nó khi phát hiện SAS hết hạn, tạo client mới với URL mới và tiếp tục upload chunk bị gián đoạn. Số lần refresh tối đa trong một phiên upload là maxSasRefreshes (mặc định 3; với upload kéo dài ~2 giờ mà SAS chỉ sống 15 phút thì cần ~8 lần, hãy tăng option này thay vì phát SAS dài hạn):
const controller = uploadLargeFile({
file,
refreshUploadUrl: async (uploadId) => {
const res = await fetch('/api/files/refresh-upload-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadId }),
});
const { data } = await res.json();
return data.uploadUrl; // SAS URL mới cho CÙNG blob
},
});
Backend cần thêm API POST /api/files/refresh-upload-url — nhận { uploadId }, trả về SAS URL mới (đủ quyền Put Block/Put Block List) cho cùng blob.
Lưu ý:
- Từ bản 1.1.0, refresh SAS cũng áp dụng cho giai đoạn commit: nếu SAS hết hạn trong cửa sổ giữa
stageBlockcuối cùng vàcommitBlockList(rất thực tế với upload kéo dài hàng giờ), thư viện tự refresh và thử lại commit thay vì fail ngay vớiCOMMIT_FAILEDkhi toàn bộ chunk đã upload xong. - Hook
refreshUploadUrlđược tự retry với lỗi transient (network blip, 502...) tối đa 2 lần trước khi coi là thất bại — đồng bộ với cách thư viện xử lý mọi request khác. - Nếu refresh "thành công" mà request kế tiếp vẫn 401/403 (SAS sai quyền ngay từ đầu), thư viện in cảnh báo console cho mỗi lần như vậy để bạn phát hiện cấu hình sai sớm — đừng chỉ dựa vào debug log.
- Khi nhiều worker song song cùng gặp lỗi hết hạn SAS, thư viện chỉ tốn một request refresh: các refresh đồng thời dùng chung một request, và các worker fail trên URL cũ sau khi một worker khác đã refresh xong sẽ tự động dùng URL mới để retry — hoàn toàn không tốn thêm quota.
- Mọi response 401/403 đều được coi là "hết hạn SAS". Nếu SAS sai cấu hình ngay từ đầu (VD sai quyền), thư viện sẽ tiêu tốn đủ quota refresh (
maxSasRefreshes, mặc định 3) trước khi fail vớiSAS_EXPIRED— nếu thấy lỗi này ngay khi upload bắt đầu, hãy kiểm tra cấu hình SAS trước.
6. Gỡ lỗi (Debug Logging)
Thư viện tích hợp sẵn Logger. Để xem các log (chunk nào đang được upload, retry lần mấy, tốc độ...), bạn bật debug lên:
import { logger } from '@namphuongtechnologi/azure-blob-transfer';
// Bật console logs
logger.enable();
// Tắt console logs (Mặc định là tắt)
logger.disable();
Lưu ý: logger là trạng thái global trong process — bật một lần sẽ in log cho mọi upload đang chạy và mọi upload sau đó, không phân biệt phiên. Nếu cần log theo từng upload, quản lý cờ bật/tắt xung quanh vòng đời upload của bạn (option debug theo từng upload được cân nhắc cho v2).
7. Khuyến nghị cấu hình (Recommended Settings)
| Kích thước file | Kích thước Chunk (chunkSize) | Số luồng song song (concurrency) |
|---|---|---|
| Dưới 50MB | 4MB (4 * 1024 * 1024) |
2 |
| 50MB - 200MB | 8MB (8 * 1024 * 1024) |
3 hoặc 4 |
| 200MB - 500MB | 16MB (16 * 1024 * 1024) |
3 hoặc 4 |
| Trên 500MB | 16MB hoặc 32MB | 4 |
Lưu ý: Việc tăng concurrency quá cao (ví dụ: 10) có thể gây nghẽn đường truyền mạng của Client (đặc biệt với mạng yếu), dẫn đến timeout và lỗi upload. Con số lý tưởng cho Web là 3 hoặc 4.
Bộ nhớ: Tại mỗi thời điểm, tối đa concurrency chunk đang nằm trong bộ nhớ, nên mức tiêu thụ đỉnh xấp xỉ concurrency × chunkSize (ví dụ: 16 × 100MB ≈ 1.6GB). Từ bản 1.1.0, thư viện tự cảnh báo (console.warn) khi tích chunkSize × concurrency vượt ~512MB dữ liệu chunk in-flight. Với thiết bị yếu, hãy giữ chunkSize 4–8MB và concurrency 2–4.
Môi trường: Các API mặc định (/api/files/...) dùng URL tương đối + fetch + File, tức chỉ chạy trong trình duyệt. Ở môi trường Node/SSR, hãy tự cung cấp session callbacks (Node 18.13+ có global File, hoặc dùng Blob).
Giới hạn Azure: Mỗi blob được phép chứa tối đa 50.000 block, mỗi block tối đa 100MB. Nếu số chunk vượt 50.000, thư viện tự động tăng chunkSize và log warning. File vượt quá ~4.7TB (50.000 × 100MB) sẽ bị từ chối sớm với lỗi INVALID_CONFIG. File rỗng (0 byte) được chấp nhận: không có chunk nào được upload, thư viện commit block list rỗng và Azure tạo blob 0 byte (lưu ý: API createSession của Backend bạn cũng cần chấp nhận fileSize: 0).
8. Tải file lớn từ URL (Large File Download)
Từ phiên bản mới, thư viện cung cấp thêm hàm downloadLargeFile (và alias downloadLargeFileFromUrl) giúp bạn tải các file dung lượng lớn từ bất kỳ URL nào (Azure Blob SAS URL, CDN, S3, Web Server thông thường, v.v.).
Cách thức hoạt động:
- Kiểm tra hỗ trợ HTTP Range: Gửi request thăm dò (
HEADhoặcGET Range: bytes=0-0) để lấy kích thước file (Content-Length), loại nội dung (Content-Type), tên file từContent-Disposition, và xác định server có hỗ trợ HTTP Range chunking hay không. Lưu ý: HEAD thiếu headerAccept-Ranges: bytesKHÔNG bị coi là "không hỗ trợ Range" (HEAD của chính Azure Blob Storage không gửi header này) — khi đó thư viện thăm dò thêm bằngGET Range: bytes=0-0và chỉ kết luận dựa trên kết quả206/200thực tế. Mọi request thăm dò/tải đều gửi kèmcache: 'no-store'để tránh browser gắn header điều kiện (If-None-Match...) gây 304 và tránh cache hoá các chunk lớn. - Tải song song theo Chunk (Parallel Range Download): Nếu server hỗ trợ Range, file được chia thành các chunk
chunkSize(mặc định 8MB) và tải song song quaconcurrencyworkers (mặc định 4 luồng). - Fallback Streaming: Nếu server không hỗ trợ Range (hoặc bạn bật
forceStream: true), thư viện tự động chuyển sang cơ chế streaming quaReadableStreammột luồng duy nhất mà vẫn cập nhật tiến độ liên tục. - Pause / Resume / Cancel: Điều khiển qua
DownloadControllerdễ dàng như khi upload. - Auto Retry: Tự động thử lại khi mất kết nối hoặc server trả về lỗi transient (408/429/5xx).
- Lưu file về trình duyệt (
saveAs): Hỗ trợ optionsaveAs: true(hoặc truyền tên file) để tự động kích hoạt tải file về máy người dùng trên trình duyệt. Việc kích hoạt lưu là best-effort: trình duyệt không cho biết người dùng đã thực sự lưu file hay chưa — kết quả luôn trả vềblobđầy đủ và trườngresult.saveTriggeredcho biết thao tác kích hoạt có thành công hay không. - Toàn vẹn dữ liệu khi tải song song (
If-Match): Nếu probe trả vềETagmạnh, mỗi request chunk sẽ gửi kèmIf-Match: <ETag>— nếu file bị ghi đè giữa chừng, các chunk request sẽ nhận412và fail ngay với thông báo rõ ràng thay vì âm thầm ghép chunk của 2 phiên bản file. - Tự động xác minh
Content-MD5: Nếu bạn không truyềnexpectedMd5nhưng server trả headerContent-MD5(Azure Blob có sẵn), thư viện tự động so khớp sau khi tải xong.
Giới hạn bộ nhớ (file rất lớn): Toàn bộ file được giữ trong RAM (các chunk +
Blobkết quả — peak ~2 lần kích thước file). MD5 được tính incremental theo từng chunk nên không tốn thêm bản copy thứ ba, nhưng với file nhiều GB trên browser (đặc biệt Safari/iOS) vẫn có thể chạm giới hạn bộ nhớ của tab. Khuyến nghị: với file > 1–2 GB trên browser, cân nhắc chia file phía server hoặc dùng native download; Node.js chịu được file lớn hơn nhiều.
Validation ném lỗi đồng bộ:
downloadLargeFile(...)némINVALID_CONFIGngay khi gọi (trước khi trả controller) nếu cấu hình sai — hãy bọc lời gọi bằngtry/catch, hoặc bắt chúng trêncontroller.promisenếu bạn chắc cấu hình hợp lệ.
Quy tắc cấu hình:
chunkSizephải là số nguyên ≥ 64KB (mặc định 8MB);concurrencylà số nguyên 1–16 (mặc định 4).chunkTimeoutMsmặc định (120s) được tự scale theo chunkSize với ngân sách tối thiểu 64KB/s khi bạn không truyền giá trị tường minh (tránh retry storm cho chunk lớn trên mạng chậm); giá trị bạn truyền luôn được giữ nguyên (kể cảInfinity).
Server/proxy "nuốt" header Range: Nếu request chunk nhận
200 OK(full body) thay vì206 Partial Content, thư viện tự động chuyển sang single-stream và tận dụng luôn response đang mở — không tải lại từ đầu, không retry storm. NếufallbackToStream: false, lỗiRANGE_NOT_SUPPORTEDđược ném ngay (không retry).
import {
downloadLargeFile,
DownloadError,
saveBlobAs,
} from '@namphuongtechnologi/azure-blob-transfer';
// Khởi tạo download
const downloadController = downloadLargeFile({
url: 'https://myaccount.blob.core.windows.net/files/dataset.zip?sas_token...',
chunkSize: 8 * 1024 * 1024, // 8MB mỗi chunk
concurrency: 4, // 4 luồng tải song song
saveAs: true, // Tự động kích hoạt lưu file về máy trên Browser
computeChecksum: true, // Tính toán MD5 hash của file đã tải
onProgress: (p) => {
console.log(
`Tiến độ: ${p.percent}% - ${p.downloadedBytes}/${p.totalBytes} bytes (${p.bytesPerSecond} B/s)`
);
console.log(`Đã hoàn thành ${p.completedChunks}/${p.totalChunks} chunk, còn lại ~${p.etaMs}ms`);
},
});
// Điều khiển luồng tải
// downloadController.pause();
// downloadController.resume();
// downloadController.cancel();
try {
const result = await downloadController.promise;
console.log('Tải file thành công:', {
fileName: result.fileName,
totalBytes: result.totalBytes,
durationMs: result.durationMs,
md5: result.md5,
blob: result.blob,
});
} catch (error) {
if (error instanceof DownloadError) {
console.error('Lỗi khi tải file:', error.code, error.message);
}
}
9. API Reference
UploadOptions (Tham số truyền vào uploadLargeFile)
Lưu ý: Lỗi cấu hình (
INVALID_CONFIG) được ném đồng bộ ngay khi gọiuploadLargeFile(...), trong khi mọi lỗi vận hành khác reject bất đồng bộ quacontroller.promise. Consumer cần xử lý cả hai đường:try/catchquanh lời gọiuploadLargeFile()và.catch()/try-awaittrêncontroller.promise.
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|---|---|---|---|
file |
File | Blob |
bắt buộc | File object lấy từ thẻ <input type="file" /> hoặc Drag&Drop. Nhận cả Blob (kèm name tùy chọn) — hữu ích trên Node 18 nơi File chưa phải global (type UploadableFile). |
apiKey |
string |
tùy chọn | API Key truyền vào header x-api-key cho cả 3 API mặc định (create-upload-session, complete-upload, cancel-upload). |
apiHeaders |
Record<string, string> |
tùy chọn | Các custom headers truyền vào các request API mặc định đến Backend. Lưu ý: Content-Type (không phân biệt hoa thường) sẽ bị bỏ qua kèm warning, vì body của các API mặc định luôn là JSON — ghi đè header này có thể khiến Backend từ chối request. Tương tự, khi đã set apiKey, header x-api-key truyền qua apiHeaders cũng bị bỏ qua kèm warning (option apiKey giữ quyền sở hữu header đó). Giá trị không phải string bị từ chối sớm với INVALID_CONFIG. |
session |
SessionCallbacks |
tùy chọn | Mặc định sẽ gọi các APIs: /api/files/create-upload-session, /api/files/complete-upload, /api/files/cancel-upload. Bạn có thể truyền vào để ghi đè; truyền undefined tường minh để tắt hẳn API mặc định tương ứng (VD: session: { completeSession: undefined } bỏ qua lời gọi complete-upload). Riêng createSession: undefined tường minh sẽ ném INVALID_CONFIG vì phiên không thể tạo nếu thiếu nó. |
apiBaseUrl |
string |
'' |
Tiền tố cho đường dẫn các API Backend mặc định, VD 'https://api.example.com' → https://api.example.com/api/files/create-upload-session. Dấu / cuối tự động bị loại bỏ. Phải là URL tuyệt đối dùng http:/https: — sai định dạng bị từ chối sớm với INVALID_CONFIG (không phải NETWORK_ERROR). Vì header x-api-key và SAS URL được truyền qua lại, luôn dùng HTTPS cho Backend production. Không ảnh hưởng khi bạn tự cung cấp session callbacks. |
chunkSize |
number |
8388608 (8MB) |
Kích thước mỗi chunk (bytes). Backend có thể ghi đè lúc tạo session. |
concurrency |
number |
4 |
Số lượng chunk upload song song. Phải là số nguyên dương; giá trị trên 16 sẽ bị cảnh báo (soft cap khuyến nghị). |
contentType |
string |
file.type hoặc 'application/octet-stream' |
Content-Type của blob — dùng chung cho cả payload createSession gửi Backend lẫn lúc commit lên Azure để hai giá trị luôn khớp. Không truyền thì dùng MIME type của file. |
blobMetadata |
Record<string, string> |
tùy chọn | Metadata gắn lên blob lúc commit (VD: { userId: '123', purpose: 'avatar' }), đọc lại được qua BlobClient.getProperties(). Phải là object chứa toàn giá trị string — sai định dạng bị từ chối sớm với INVALID_CONFIG. |
blobTags |
Record<string, string> |
tùy chọn | Blob index tags gắn lên blob lúc commit (VD: { project: 'alpha' }), dùng cho indexing/lifecycle policy của Azure (yêu cầu account có bật blob index tags). Validate giống blobMetadata. |
computeChecksum |
boolean |
false |
Tính MD5 của từng chunk và gửi kèm header transactionalContentMD5 để Azure xác minh toàn vẹn dữ liệu của mỗi block khi stageBlock. |
useWebWorker |
boolean |
true |
Khi bật computeChecksum: tính MD5 trong Web Worker để không chặn UI thread (chỉ trên browser; môi trường không có Worker sẽ tự chạy trên thread chính). |
resume |
boolean |
false |
Tiếp tục upload từ phiên trước: sau createSession, thư viện hỏi getBlockList('uncommitted') và bỏ qua các block đã staged, tiến độ cộng dồn từ số byte đã có. Yêu cầu: Backend trả lại đúng session/blob cũ (cùng SAS URL), SAS có thêm quyền đọc r, và cùng chunkSize giữa các phiên (chi tiết ở lưu ý cuối mục 4). Nếu hỏi block list thất bại, upload chạy từ chunk 0 kèm warning. |
sessionTimeoutMs |
number |
60000 (60s) |
Timeout (ms) cho các API Backend mặc định (create-upload-session, complete-upload, cancel-upload) để backend bị treo không làm upload treo vô hạn. Truyền Infinity để tắt timeout. |
chunkTimeoutMs |
number |
120000 (120s) |
Timeout (ms) cho mỗi request stageBlock/commitBlockList tới Azure, chống trường hợp kết nối "chết lặng" (mất gói, proxy treo) khiến request treo vô hạn mà retry không bao giờ chạy. Timeout kích hoạt được coi là lỗi transient nên chunk/commit sẽ tự retry với backoff; không bị nhầm thành UPLOAD_ABORTED. Truyền Infinity để tắt timeout. |
retrySessionCreate |
boolean |
false |
Tự retry createSession (tối đa 2 lần). Chỉ bật khi API create của Backend là idempotent. Chỉ retry lỗi transient (408/429/5xx hoặc lỗi mạng); lỗi 4xx deterministic không retry. |
onProgress |
Function |
undefined | Callback nhận object UploadProgress liên tục trong quá trình upload (bao gồm cả số byte đang truyền dở của từng chunk, không chỉ khi chunk hoàn thành): uploadedBytes, totalBytes, percent, completedChunks, totalChunks, kèm bytesPerSecond (tốc độ trung bình từ lúc bắt đầu phase upload, không tính thời gian pause) và etaMs (thời gian còn lại ước tính). percent được giữ tối đa 99% trong khi upload; 100% chỉ phát đúng một lần khi toàn bộ chunk, commit và completeSession đã xong. Nếu callback ném lỗi, upload vẫn tiếp tục (chỉ log warning). |
refreshUploadUrl |
(uploadId: string) => Promise<string> |
tùy chọn | Hook được gọi khi SAS URL hết hạn giữa chừng (mục 5.1) — bao gồm cả giai đoạn commit; trả về SAS URL mới để thư viện tự tiếp tục upload/commit (tối đa maxSasRefreshes lần, mặc định 3; các worker song song chia sẻ một request refresh; hook được tự retry tối đa 2 lần với lỗi transient). URL trả về phải là URL http(s) tuyệt đối — sai định dạng bị từ chối với INVALID_CONFIG. |
maxSasRefreshes |
number |
3 |
Số lần làm mới SAS URL tối đa trong một phiên upload (quota chung cho mọi worker, tính cả giai đoạn commit). Tăng giá trị này khi upload kéo dài nhiều giờ với SAS TTL ngắn. Phải là số nguyên dương — sai bị từ chối với INVALID_CONFIG. |
DownloadOptions (Tham số truyền vào downloadLargeFile)
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|---|---|---|---|
url |
string |
bắt buộc | URL của file cần tải (hỗ trợ HTTP/HTTPS URL, Azure Blob SAS URL, CDN, S3, v.v.). |
fileName |
string |
tùy chọn | Tên file tùy chỉnh. Nếu không truyền, thư viện tự trích xuất từ header Content-Disposition hoặc đường dẫn URL. |
chunkSize |
number |
8388608 (8MB) |
Kích thước mỗi chunk khi tải qua HTTP Range requests (bytes). Phải là số nguyên ≥ 65536 (64KB). |
concurrency |
number |
4 |
Số lượng chunk tải song song. Phải là số nguyên từ 1 đến 16. |
headers |
Record<string, string> |
{} |
Custom headers gửi kèm mỗi request (VD: Authorization, x-ms-*, v.v.). |
headTimeoutMs |
number |
60000 (60s) |
Timeout cho request thăm dò thông tin file (HEAD/probe request). |
chunkTimeoutMs |
number |
120000 (120s)* |
Timeout cho mỗi request tải chunk (bao gồm cả thời gian đọc body). *Nếu không truyền, giá trị mặc định được tự scale theo chunkSize (ngân sách tối thiểu 64KB/s, không thấp hơn 120s). |
saveAs |
boolean | string |
false |
Nếu true hoặc truyền tên file string, tự động kích hoạt tải và lưu file về máy trên trình duyệt sau khi tải xong. |
computeChecksum |
boolean |
false |
Tính toán mã băm MD5 của toàn bộ file sau khi ghép xong. |
expectedMd5 |
string |
tùy chọn | Mã MD5 mong đợi (hex hoặc base64) để kiểm tra tính toàn vẹn dữ liệu. Nếu không khớp sẽ ném lỗi CHECKSUM_MISMATCH. Nếu không truyền, header Content-MD5 của server (nếu có) được tự động dùng để xác minh. |
fallbackToStream |
boolean |
true |
Tự động fallback sang stream tải tuần tự nếu server không hỗ trợ HTTP Range requests. |
forceStream |
boolean |
false |
Ép buộc dùng single-stream download kể cả khi server có hỗ trợ Range requests. |
onProgress |
(progress: DownloadProgress) => void |
tùy chọn | Callback báo cáo tiến độ tải: downloadedBytes, totalBytes, percent, bytesPerSecond, etaMs, completedChunks, totalChunks. |
signal |
AbortSignal |
tùy chọn | AbortSignal bên ngoài để hủy quá trình tải bất cứ lúc nào. |
fetchFn |
typeof fetch |
global fetch |
Custom fetch implementation — hữu ích khi chạy trên Node.js hoặc khi viết test. |
DownloadResult (Kết quả trả về khi tải xong)
| Thuộc tính | Kiểu dữ liệu | Mô tả |
|---|---|---|
blob |
Blob |
File hoàn chỉnh dưới dạng Blob JavaScript. |
totalBytes |
number |
Tổng số byte đã tải về. |
totalChunks |
number |
Tổng số chunk đã tải. |
durationMs |
number |
Tổng thời gian tải file (mili-giây). |
contentType |
string |
MIME Content-Type của file. |
fileName |
string |
Tên file được xác định. |
url |
string |
URL nguồn của file. |
md5 |
string? |
Chuỗi MD5 hex (nếu bật computeChecksum/expectedMd5, hoặc xác minh tự động qua Content-MD5). |
saveTriggered |
boolean? |
Chỉ có mặt khi dùng saveAs: true nếu trình duyệt đã được kích hoạt lưu file thành công (best-effort — không đảm bảo người dùng đã lưu xong); false ở môi trường ngoài browser hoặc khi kích hoạt thất bại. blob luôn khả dụng bất kể giá trị này. |
ChunkRetryOptions / ChunkDownloadRetryOptions (Cấu hình Retry)
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|---|---|---|---|
maxRetries |
number |
3 |
Số lần tối đa thử tải lại mỗi chunk nếu rớt mạng. |
baseDelay |
number |
1000 |
Thời gian chờ (ms) trước khi thử lại lần đầu. Các lần sau tăng theo cấp số nhân. |
maxDelay |
number |
30000 |
Thời gian chờ tối đa (ms) giữa các lần thử lại. |
Các hàm tiện ích khác (Exported Utilities)
Ngoài uploadLargeFile / downloadLargeFile, thư viện export các tiện ích sau cho nhu cầu nâng cao:
| Export | Mô tả |
|---|---|
VERSION |
Phiên bản thư viện (đọc từ package.json lúc build, luôn khớp với bản publish). |
logger |
Bật/tắt debug log toàn cục: logger.enable() / logger.disable() (mặc định tắt — xem mục 6). |
saveBlobAs(blob, fileName) |
Kích hoạt lưu file về máy trên trình duyệt (best-effort, trả về boolean); no-op an toàn ở môi trường ngoài browser. |
extractFileName(url, contentDisposition?) |
Trích tên file từ header Content-Disposition (hỗ trợ RFC 5987 filename*) hoặc đường dẫn URL; fallback 'downloaded_file'. |
computeMd5(data, useWebWorker?) |
Tính MD5 của Blob / ArrayBuffer / Uint8Array, trả về Uint8Array 16 byte; tự dùng Web Worker khi có thể. |
md5Js(data) |
MD5 thuần JavaScript (không Worker) cho môi trường không phải browser. |
terminateMd5Worker() |
Kết thúc Web Worker MD5 dùng chung của thư viện (VD trước khi rời trang). |
generateBlockId(index) / generateBlockIds(n) |
Sinh block ID chuẩn Azure (base64 của index đệm 8 chữ số) — dùng khi bạn tự quản lý block list bên ngoài thư viện. |
retry(fn, options?) |
Retry với exponential backoff + jitter (cùng thuật toán nội bộ), cấu hình qua RetryOptions (maxRetries, baseDelay, maxDelay, factor, jitter, shouldRetry, onRetry, abortSignal). |
10. Checklist bảo mật SAS (Khuyến nghị)
Frontend nhận SAS URL nghĩa là trình duyệt người dùng nói chuyện trực tiếp với Azure — SAS URL do Backend phát ra cần được giới hạn chặt:
- Chỉ cấp quyền
w(write) +c(create) trên đúng 1 blob của phiên upload đó — không cấp quyền xóa/liệt kê container. (Thêm quyềnr(read) nếu bạn dùngresume: true— thư viện cần đọc block list để biết block nào đã staged; xem cuối mục 4.) Với phiên download, chỉ cấp quyềnr(read). - Thời hạn SAS ngắn (đủ cho phiên upload/download + buffer, VD 15–60 phút); nếu file quá lớn khi upload, dùng
refreshUploadUrlđể gia hạn thay vì phát SAS dài hạn. - Backend API (
apiBaseUrl) bắt buộc HTTPS trong production — headerx-api-keyvà SAS URL đều đi qua đây. Từ bản 1.1.0, thư viện tự cảnh báo khiapiBaseUrldùnghttp://trên host không phải localhost (đặc biệt khi có cấu hìnhapiKey). - Cấu hình CORS trên Azure Storage chỉ cho phép đúng origin của frontend bạn (xem mục 2), không dùng
*cho production. - Nếu hạ tầng cho phép, giới hạn thêm theo IP/origin ở tầng proxy hoặc Azure Policy.
- SAS sai quyền ngay từ đầu sẽ tiêu tốn các lần refresh trước khi fail với
SAS_EXPIRED— thấy lỗi này xuất hiện ngay khi upload bắt đầu thì kiểm tra cấu hình SAS trước tiên.
License
MIT Nam Phuong Technology