Skip to content

fix(ios): Support Range requests for reading local files #692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 72 additions & 26 deletions src/ios/IONAssetHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,51 +17,97 @@ - (instancetype)initWithBasePath:(NSString *)basePath andScheme:(NSString *)sche
return self;
}

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
{
NSString * startPath = @"";
NSURL * url = urlSchemeTask.request.URL;
NSString * stringToLoad = url.path;
NSString * scheme = url.scheme;

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask {
NSString * startPath = @""; // Initialize startPath
NSURL * url = urlSchemeTask.request.URL; // Extract URL from the request
NSString * stringToLoad = url.path; // Get the path component of the URL
NSString * scheme = url.scheme; // Get the scheme of the URL
// Check if the URL's scheme matches the custom scheme
if ([scheme isEqualToString:self.scheme]) {
// If the path starts with "/_app_file_", strip this prefix
if ([stringToLoad hasPrefix:@"/_app_file_"]) {
startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""];
} else {
// Otherwise, build the path relative to self.basePath
startPath = self.basePath ? self.basePath : @"";
if ([stringToLoad isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) {
startPath = [startPath stringByAppendingString:@"/index.html"];
} else {
startPath = [startPath stringByAppendingString:stringToLoad];
}
}
}
}
NSError * fileError = nil;
NSData * data = nil;
if ([self isMediaExtension:url.pathExtension]) {
data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError];
}
if (!data || fileError) {
data = [[NSData alloc] initWithContentsOfFile:startPath];
}
NSInteger statusCode = 200;
if (!data) {
NSString * mimeType = [self getMimeType:url.pathExtension]; // Get MIME type based on file extension
NSInteger statusCode = 200; // Default to 200 OK status
NSURL * localUrl = [NSURL URLWithString:url.absoluteString]; // Create a URL object
id response = nil;

NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:startPath]) {
// File not found, return 404
statusCode = 404;
response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:@{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache" }];
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didFinish];
return;
}
NSURL * localUrl = [NSURL URLWithString:url.absoluteString];
NSString * mimeType = [self getMimeType:url.pathExtension];
id response = nil;
if (data && [self isMediaExtension:url.pathExtension]) {
response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
} else {
NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"};
response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers];

// Check for Range header
NSString *rangeHeader = [urlSchemeTask.request valueForHTTPHeaderField:@"Range"];
NSUInteger fileLength = (NSUInteger)[[fileManager attributesOfItemAtPath:startPath error:nil] fileSize];

if (rangeHeader) {
NSRange range = NSMakeRange(NSNotFound, 0);
if ([rangeHeader hasPrefix:@"bytes="]) {
NSString *byteRange = [rangeHeader substringFromIndex:6];
NSArray<NSString *> *rangeParts = [byteRange componentsSeparatedByString:@"-"];
NSUInteger start = (NSUInteger)[rangeParts[0] integerValue];
NSUInteger end = rangeParts.count > 1 && ![rangeParts[1] isEqualToString:@""] ? (NSUInteger)[rangeParts[1] integerValue] : fileLength - 1;
range = NSMakeRange(start, end - start + 1);
}

if (range.location != NSNotFound) {
// Ensure range is valid
if (range.location >= fileLength) {
statusCode = 416; // Requested Range Not Satisfiable
response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:@{ @"Content-Type" : mimeType, @"Content-Range": [NSString stringWithFormat:@"bytes */%lu", (unsigned long)fileLength], @"Cache-Control": @"no-cache" }];
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didFinish];
return;
}

NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:startPath];
[fileHandle seekToFileOffset:range.location];
data = [fileHandle readDataOfLength:range.length];
[fileHandle closeFile];

statusCode = 206; // Partial Content
NSString *contentRange = [NSString stringWithFormat:@"bytes %lu-%lu/%lu", (unsigned long)range.location, (unsigned long)(range.location + range.length - 1), (unsigned long)fileLength];
response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:@{ @"Content-Type" : mimeType, @"Content-Range": contentRange, @"Content-Length": [NSString stringWithFormat:@"%lu", (unsigned long)range.length], @"Cache-Control": @"no-cache" }];
}
}

if (!response) {
// Load entire file if no range is requested
data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError];
if (!data || fileError) {
data = [[NSData alloc] initWithContentsOfFile:startPath];
}

if (!data) {
statusCode = 404;
}

NSDictionary *headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache" };
response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers];
} // Send response and data to the WKWebView
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
if (data) {
[urlSchemeTask didReceiveData:data];
}
[urlSchemeTask didFinish];

}

- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask
Expand Down