Get Linked File By Chunks Handler
This page provides complete examples for implementing the Get Linked File By Chunks backend handler. The getlinkedfilebychunks endpoint streams large files in chunks using HTTP range requests for efficient delivery.
What is Get Linked File By Chunks?
getlinkedfilebychunks is the backend endpoint that:
- Receives a file identifier and range request
- Streams specific byte ranges of large files
- Supports HTTP 206 Partial Content responses
- Enables efficient video/audio streaming and large file downloads
Use this endpoint for files larger than 10MB, video/audio streaming, or when you need resumable downloads.
Complete Get Linked File By Chunks Implementation
Choose your backend language:
- C# / .NET
- PHP
public class LinkedFileRequestDto
{
public string FileId { get; set; }
}
[HttpPost("getlinkedfilebychunks")]
[AllowAnonymous]
public async Task<IActionResult> GetLinkedFileByChunks([FromBody] LinkedFileRequestDto request)
{
string tenantId = GetCurrentTenantId();
if (string.IsNullOrEmpty(request.FileId))
{
return BadRequest(new { error = "File ID is required" });
}
// Parse file identifier
string[] parts = request.FileId.Split(':');
string bucketName = parts[0];
string filePath = parts[1];
var fileStorageService = GetFileStorageService();
string fileName = Path.GetFileName(filePath);
string fileExt = Path.GetExtension(fileName).TrimStart('.');
string mimeType = GetMimeType(fileName);
// Get file metadata to determine size
var metadata = await fileStorageService.GetFileMetadataAsync(
tenantId,
filePath,
bucketName
);
long fileSize = metadata?.ContentLength ?? 0;
if (fileSize <= 0)
{
return NotFound("File size unknown.");
}
// Get file stream
using var fileStream = await fileStorageService.GetFileStreamFromBucket(
tenantId,
filePath,
bucketName
);
// Parse Range header
string rangeHeader = Request.Headers["Range"];
if (!string.IsNullOrEmpty(rangeHeader) && rangeHeader.StartsWith("bytes="))
{
// Parse range (e.g., "bytes=0-1023" or "bytes=1024-")
string[] rangeParts = rangeHeader.Replace("bytes=", "").Split('-');
if (!long.TryParse(rangeParts[0], out long start))
{
return StatusCode(416, "Invalid Range Header");
}
long end = rangeParts.Length > 1 && long.TryParse(rangeParts[1], out long parsedEnd)
? parsedEnd
: fileSize - 1;
// Validate range
if (start >= fileSize || end >= fileSize || start > end)
{
return StatusCode(416, "Requested Range Not Satisfiable");
}
long chunkSize = end - start + 1;
// Set response headers for partial content
Response.StatusCode = 206; // Partial Content
Response.Headers.Add("Accept-Ranges", "bytes");
Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{fileSize}");
Response.ContentType = mimeType;
// Skip to start position
byte[] skipBuffer = new byte[81920]; // 80KB buffer
long bytesToSkip = start;
while (bytesToSkip > 0)
{
int toRead = (int)Math.Min(skipBuffer.Length, bytesToSkip);
int skipped = await fileStream.ReadAsync(skipBuffer, 0, toRead);
if (skipped == 0) break; // End of stream
bytesToSkip -= skipped;
}
// Read and return the requested chunk
byte[] buffer = new byte[81920];
long totalBytesRead = 0;
using var memoryStream = new MemoryStream();
int bytesRead;
while (totalBytesRead < chunkSize &&
(bytesRead = await fileStream.ReadAsync(
buffer,
0,
(int)Math.Min(buffer.Length, chunkSize - totalBytesRead)
)) > 0)
{
await memoryStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
return File(memoryStream.ToArray(), mimeType);
}
return BadRequest("Range header required");
}
private string GetMimeType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".pdf" => "application/pdf",
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".mp4" => "video/mp4",
".webm" => "video/webm",
".mp3" => "audio/mpeg",
".wav" => "audio/wav",
".doc" => "application/msword",
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls" => "application/vnd.ms-excel",
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".zip" => "application/zip",
_ => "application/octet-stream"
};
}
<?php
class FormsPublicController {
public function getLinkedFileByChunks(Request $request) {
$fileId = $request->input('fileId');
if (empty($fileId)) {
return response()->json(['error' => 'File ID is required'], 400);
}
$tenantId = $this->getCurrentTenantId();
$fileStorageService = $this->getFileStorageService();
// Parse file identifier
$parts = explode(':', $fileId, 2);
if (count($parts) !== 2) {
return response()->json(['error' => 'Invalid file ID format'], 400);
}
$bucketName = $parts[0];
$filePath = $parts[1];
$fileName = basename($filePath);
$mimeType = $this->getMimeType($fileName);
// Get file metadata
try {
$metadata = $fileStorageService->getFileMetadata($tenantId, $filePath, $bucketName);
$fileSize = $metadata['ContentLength'] ?? 0;
if ($fileSize <= 0) {
return response()->json(['error' => 'File size unknown'], 404);
}
}
catch (Exception $e) {
Log::error("Failed to get file metadata: {$e->getMessage()}");
return response()->json(['error' => 'File not found'], 404);
}
// Parse Range header
$rangeHeader = $request->header('Range');
if (empty($rangeHeader) || !str_starts_with($rangeHeader, 'bytes=')) {
return response()->json(['error' => 'Range header required'], 400);
}
// Parse range (e.g., "bytes=0-1023")
$range = str_replace('bytes=', '', $rangeHeader);
$rangeParts = explode('-', $range);
$start = intval($rangeParts[0]);
$end = isset($rangeParts[1]) && $rangeParts[1] !== ''
? intval($rangeParts[1])
: $fileSize - 1;
// Validate range
if ($start >= $fileSize || $end >= $fileSize || $start > $end) {
return response('Requested Range Not Satisfiable', 416);
}
$chunkSize = $end - $start + 1;
// Get file stream
$fileStream = $fileStorageService->getFileStream($tenantId, $filePath, $bucketName);
// Skip to start position
if ($start > 0) {
fseek($fileStream, $start);
}
// Set response headers
return response()->stream(
function() use ($fileStream, $chunkSize) {
$bytesRead = 0;
$bufferSize = 81920; // 80KB
while ($bytesRead < $chunkSize && !feof($fileStream)) {
$toRead = min($bufferSize, $chunkSize - $bytesRead);
$data = fread($fileStream, $toRead);
echo $data;
flush();
$bytesRead += strlen($data);
}
fclose($fileStream);
},
206, // Partial Content
[
'Content-Type' => $mimeType,
'Accept-Ranges' => 'bytes',
'Content-Range' => "bytes {$start}-{$end}/{$fileSize}",
'Content-Length' => $chunkSize,
'Cache-Control' => 'public, max-age=3600'
]
);
}
private function getMimeType($fileName) {
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$mimeTypes = [
'pdf' => 'application/pdf',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'mp4' => 'video/mp4',
'webm' => 'video/webm',
'mp3' => 'audio/mpeg',
'wav' => 'audio/wav'
];
return $mimeTypes[$extension] ?? 'application/octet-stream';
}
private function getCurrentTenantId() {
return session('tenant_id') ?? 'default';
}
private function getFileStorageService() {
return app('file.storage');
}
}
?>
Request Structure
POST /your-api-path/getlinkedfilebychunks
Range: bytes=0-1048575
Content-Type: application/json
{
"fileId": "bucket-name:tenant/forms/2026/01/27/video.mp4"
}
Key Fields:
- fileId - The file identifier
- Range header - Specifies the byte range to retrieve (e.g.,
bytes=0-1048575for first 1MB)
Response
HTTP/1.1 206 Partial Content
Content-Type: video/mp4
Accept-Ranges: bytes
Content-Range: bytes 0-1048575/10485760
Content-Length: 1048576
[Binary chunk data]
Response Headers:
- Status: 206 - Partial Content
- Accept-Ranges - Indicates byte range requests are supported
- Content-Range - Shows which bytes are being returned and total file size
- Content-Length - Size of this chunk
HTTP Range Request Examples
First Chunk
Range: bytes=0-1048575
Returns first 1MB (bytes 0-1048575)
Second Chunk
Range: bytes=1048576-2097151
Returns second 1MB (bytes 1048576-2097151)
Last Chunk
Range: bytes=9437184-
Returns from byte 9437184 to end of file
Specific Range
Range: bytes=5242880-6291455
Returns bytes 5242880-6291455 (1MB starting at 5MB offset)
Video Streaming Example
HTML5 Video Player
<video controls width="800">
<source src="/your-api-path/getlinkedfilebychunks?fileId=bucket:path/video.mp4" type="video/mp4">
</video>
The browser automatically sends range requests as the user seeks through the video.
Custom Video Player
class ChunkedVideoPlayer {
private fileId: string;
private chunkSize = 1024 * 1024; // 1MB chunks
async loadChunk(start: number, end: number): Promise<ArrayBuffer> {
const response = await fetch('/your-api-path/getlinkedfilebychunks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Range': `bytes=${start}-${end}`
},
body: JSON.stringify({ fileId: this.fileId })
});
if (response.status !== 206) {
throw new Error('Range request failed');
}
return response.arrayBuffer();
}
}
Large File Download with Progress
async function downloadLargeFile(fileId: string, onProgress: (percent: number) => void) {
// Get file size first
const metaResponse = await fetch('/your-api-path/getlinkedfilemetaevent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileId })
});
const meta = await metaResponse.json();
const fileSize = meta.linkedFileSize;
const chunkSize = 1024 * 1024; // 1MB chunks
const chunks: Blob[] = [];
let downloaded = 0;
for (let start = 0; start < fileSize; start += chunkSize) {
const end = Math.min(start + chunkSize - 1, fileSize - 1);
const response = await fetch('/your-api-path/getlinkedfilebychunks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Range': `bytes=${start}-${end}`
},
body: JSON.stringify({ fileId })
});
const chunk = await response.blob();
chunks.push(chunk);
downloaded += chunk.size;
onProgress((downloaded / fileSize) * 100);
}
// Combine all chunks
const fullBlob = new Blob(chunks, { type: meta.linkedFileMimetype });
return fullBlob;
}
// Usage
downloadLargeFile('bucket:path/large-file.zip', (percent) => {
console.log(`Downloaded: ${percent.toFixed(2)}%`);
});
Performance Optimization
Adjust Chunk Size
Choose chunk size based on use case:
private int GetOptimalChunkSize(string mimeType)
{
return mimeType switch
{
var t when t.StartsWith("video/") => 512 * 1024, // 512KB for video
var t when t.StartsWith("audio/") => 256 * 1024, // 256KB for audio
var t when t.StartsWith("image/") => 128 * 1024, // 128KB for images
_ => 1024 * 1024 // 1MB for other files
};
}
Enable Caching
Cache chunks for frequently accessed files:
Response.Headers.Add("Cache-Control", "public, max-age=3600");
Response.Headers.Add("ETag", ComputeETag(fileId, start, end));
Compression
Enable gzip compression for text-based files:
if (mimeType.StartsWith("text/") || mimeType.Contains("json") || mimeType.Contains("xml"))
{
Response.Headers.Add("Content-Encoding", "gzip");
// Compress chunk before sending
}
Error Handling
public async Task<IActionResult> GetLinkedFileByChunks([FromBody] LinkedFileRequestDto request)
{
try
{
// Validate request
if (string.IsNullOrEmpty(request.FileId))
{
return BadRequest(new { error = "File ID is required" });
}
// Check Range header
string rangeHeader = Request.Headers["Range"];
if (string.IsNullOrEmpty(rangeHeader))
{
return BadRequest(new { error = "Range header is required" });
}
// Check authorization
if (!await CanUserAccessFile(GetCurrentUserId(), request.FileId))
{
return Unauthorized(new { error = "Access denied" });
}
// Get file metadata
var metadata = await GetFileMetadata(request.FileId);
if (metadata == null)
{
return NotFound(new { error = "File not found" });
}
// Process and return chunk...
return File(chunkData, mimeType);
}
catch (ArgumentException ex)
{
return StatusCode(416, new { error = ex.Message });
}
catch (FileNotFoundException)
{
return NotFound(new { error = "File not found" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error serving file chunk");
return StatusCode(500, new { error = "Failed to retrieve file chunk" });
}
}
Next Steps
- Get Linked File Meta Handler - Get file metadata
- Download File Handler - Direct file downloads
- Upload Files Handler - Upload new files