Skip to main content

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
When to Use

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:

Controllers/FormsPublicController.cs
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"
};
}

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-1048575 for 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

HTML - Video Player with Range Support
<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

TypeScript - Load Video Chunk
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

TypeScript - Download with Progress Tracking
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:

C# - Optimize Chunk Size by Type
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:

C# - Enable Chunk Caching
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:

C# - Compress Text Files
if (mimeType.StartsWith("text/") || mimeType.Contains("json") || mimeType.Contains("xml"))
{
Response.Headers.Add("Content-Encoding", "gzip");
// Compress chunk before sending
}

Error Handling

C# - Handle Errors
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