Skip to content

Commit

Permalink
feat: if-range skip
Browse files Browse the repository at this point in the history
  • Loading branch information
ido-pluto committed Feb 27, 2024
1 parent 63a6e23 commit 2e6fa9c
Show file tree
Hide file tree
Showing 8 changed files with 41 additions and 12 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {downloadFileBrowser} from "ipull/dist/browser.js";

const downloader = await downloadFileBrowser({
url: 'https://example.com/file.large',
acceptRangeAlwaysTrue: true // cors origin request will not return the range header, but we can force it to be true (multi-connection download)
acceptRangeIsKnown: true // cors origin request will not return the range header, but we can force it to be true (multi-connection download)
});

await downloader.download();
Expand Down
2 changes: 1 addition & 1 deletion examples/browser.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

const downloader = await downloadFileBrowser({
url: BIG_IMAGE,
acceptRangeAlwaysTrue: true // cors origin request will not return the range header, but we can force it to be true (multipart download)
acceptRangeIsKnown: true // cors origin request will not return the range header, but we can force it to be true (multipart download)
});

downloader.on("progress", progress => {
Expand Down
8 changes: 4 additions & 4 deletions src/download/download-engine/engine/base-download-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import ProgressStatisticsBuilder, {TransferProgressWithStatus} from "../../trans
import DownloadAlreadyStartedError from "./error/download-already-started-error.js";

export type InputURLOptions = { partsURL: string[] } | { url: string };
export type BaseDownloadEngineOptions<FetchStrategy = "xhr" | "fetch" | "localFile"> = DownloadEngineFileOptions &
export type BaseDownloadEngineOptions<FetchStrategy = "xhr" | "fetch" | "localFile"> =
DownloadEngineFileOptions
& BaseDownloadEngineFetchStreamOptions
&
{
comment?: string;
headers?: Record<string, string>;
acceptRangeIsKnown?: boolean;
fetchStrategy?: FetchStrategy;
defaultFetchDownloadInfo?: BaseDownloadEngineFetchStreamOptions["defaultFetchDownloadInfo"];
};

export interface BaseDownloadEngineEvents extends Omit<DownloadEngineFileEvents, "progress"> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ export type BaseDownloadEngineFetchStreamOptions = {
/**
* If true, parallel download will be enabled even if the server does not return `accept-range` header, this is good when using cross-origin requests

Check warning on line 7 in src/download/download-engine/streams/download-engine-fetch-stream/base-download-engine-fetch-stream.ts

View workflow job for this annotation

GitHub Actions / test

This line has a length of 153. Maximum allowed is 140
*/
acceptRangeAlwaysTrue?: boolean
acceptRangeIsKnown?: boolean
defaultFetchDownloadInfo?: { length: number, acceptRange: boolean }
ignoreIfRangeWithQueryParams?: boolean
};

export default abstract class BaseDownloadEngineFetchStream {
Expand All @@ -32,4 +33,15 @@ export default abstract class BaseDownloadEngineFetchStream {

public close(): void | Promise<void> {
}

protected _appendToURL(url: string) {
const parsed = new URL(url);
if (this.options.ignoreIfRangeWithQueryParams) {
const randomText = Math.random()
.toString(36);
parsed.searchParams.set("_ignore", randomText);
}

return parsed.href;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import BaseDownloadEngineFetchStream from "./base-download-engine-fetch-stream.js";
import InvalidContentLengthError from "./errors/invalid-content-length-error.js";

export default class DownloadEngineFetchStreamFetch extends BaseDownloadEngineFetchStream {
protected async _fetchBytesWithoutRetry(url: string, start: number, end: number, onProgress?: (length: number) => void) {
const response = await fetch(url, {
const response = await fetch(this._appendToURL(url), {
headers: {
accept: "*/*",
...this.options.headers,
range: `bytes=${start}-${end - 1}` // get the range up to end-1. Length 2: 0-1
}
});

const contentLength = parseInt(response.headers.get("content-length")!);
if (contentLength !== end - start) {
throw new InvalidContentLengthError(end - start, contentLength);
}

let receivedLength = 0;
const reader = response.body!.getReader();
const chunks = [];
Expand Down Expand Up @@ -41,7 +47,7 @@ export default class DownloadEngineFetchStreamFetch extends BaseDownloadEngineFe
});

const length = parseInt(response.headers.get("content-length")!);
const acceptRange = this.options.acceptRangeAlwaysTrue || response.headers.get("accept-ranges") === "bytes";
const acceptRange = this.options.acceptRangeIsKnown ?? response.headers.get("accept-ranges") === "bytes";

return {
length,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import BaseDownloadEngineFetchStream from "./base-download-engine-fetch-stream.j
import EmptyResponseError from "./errors/empty-response-error.js";
import StatusCodeError from "./errors/status-code-error.js";
import XhrError from "./errors/xhr-error.js";
import InvalidContentLengthError from "./errors/invalid-content-length-error.js";


export default class DownloadEngineFetchStreamXhr extends BaseDownloadEngineFetchStream {
Expand All @@ -15,12 +16,17 @@ export default class DownloadEngineFetchStreamXhr extends BaseDownloadEngineFetc

const xhr = new XMLHttpRequest();
xhr.responseType = "arraybuffer";
xhr.open("GET", url, true);
xhr.open("GET", this._appendToURL(url), true);
for (const [key, value] of Object.entries(headers)) {
xhr.setRequestHeader(key, value);
}

xhr.onload = function () {
const contentLength = parseInt(xhr.getResponseHeader("content-length")!);
if (contentLength !== end - start) {
throw new InvalidContentLengthError(end - start, contentLength);
}

if (xhr.status >= 200 && xhr.status < 300) {
const arrayBuffer = xhr.response;
if (arrayBuffer) {
Expand Down Expand Up @@ -59,7 +65,7 @@ export default class DownloadEngineFetchStreamXhr extends BaseDownloadEngineFetc
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
const length = xhr.getResponseHeader("Content-Length") || "-";
const acceptRange = this.options.acceptRangeAlwaysTrue || xhr.getResponseHeader("Accept-Ranges") === "bytes";
const acceptRange = this.options.acceptRangeIsKnown ?? xhr.getResponseHeader("Accept-Ranges") === "bytes";
resolve({length: parseInt(length), acceptRange});
} else {
reject(new StatusCodeError(url, xhr.status, xhr.statusText, this.options.headers));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class InvalidContentLengthError extends Error {
constructor(expectedLength: number, gotLength: number) {
super(`Expected ${expectedLength} bytes, but got ${gotLength} bytes. If you on browser try to set "ignoreIfRangeWithQueryParams" to true, this will add a "_ignore" query parameter to the URL to avoid chrome "if-range" header.`);

Check warning on line 3 in src/download/download-engine/streams/download-engine-fetch-stream/errors/invalid-content-length-error.ts

View workflow job for this annotation

GitHub Actions / test

This line has a length of 236. Maximum allowed is 140
}
}
2 changes: 1 addition & 1 deletion test/download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe("File Download", () => {
const MIN_PARALLEL_CONNECTIONS = 4;
const randomNumber = Math.max(MIN_PARALLEL_CONNECTIONS, Math.floor(Math.random() * 30));
const fetchStream = new DownloadEngineFetchStreamFetch({
acceptRangeAlwaysTrue: true
acceptRangeIsKnown: true
});
const writeStream = new DownloadEngineWriteStreamBrowser(() => {
});
Expand Down

0 comments on commit 2e6fa9c

Please sign in to comment.