diff --git a/content/docs/disk_api.md b/content/docs/disk_api.md index 91c8499..c3e7796 100644 --- a/content/docs/disk_api.md +++ b/content/docs/disk_api.md @@ -89,6 +89,24 @@ const readable = await disk.getStream(key) await pipeline(readable, createWriteStream('./some-file.txt', readable)) ``` +You may stream a subset of the file's bytes by passing a [`range`](#read-options) argument in the `ReadOptions` object +as the second argument. This is useful for serving `Range` HTTP requests, reading media segments, or inspecting file +headers without downloading the entire object. + +```ts +/** + * Stream only bytes 0 through 1023 (inclusive) + */ +const readable = await disk.getStream(key, { + range: { start: 0, end: 1023 }, +}) +``` + +| Param | Type | Description | +| ------- | ------------------------------ | ------------------------------------------ | +| key | `string` | Location of the file | +| options | [`ReadOptions`](#read-options) | An optional object to request a byte range | + ## getBytes The `disk.getBytes` method reads a file's contents as a `Uint8Array` stream. If the file does not exist, the method throws an exception. @@ -105,6 +123,23 @@ const arrayBuffer = await disk.getBytes(key) console.log(new TextDecoder('utf-8').decode(arrayBuffer)) ``` +Like `getStream`, you may read a subset of the file's bytes by passing a [`range`](#read-options) in the `ReadOptions` +object as the second argument. + +```ts +/** + * Read only the first 8 bytes of the file + */ +const arrayBuffer = await disk.getBytes(key, { + range: { start: 0, end: 7 }, +}) +``` + +| Param | Type | Description | +| ------- | ------------------------------ | ------------------------------------------ | +| key | `string` | Location of the file | +| options | [`ReadOptions`](#read-options) | An optional object to request a byte range | + ## delete The `disk.delete` method deletes a file for the given key. The delete operation ignores non-existent files and does not throw an error. @@ -456,6 +491,97 @@ The `getSignedUploadUrl` method accepts the following arguments as the second pa | expiresIn | `string`, `number` | The duration after which the URL will expire. Defaults to `30 mins` | | contentType | `string` | Define the value of the `Content-type` header. During the file upload, you will have to set the `content-type` header to the same value. | +## Read options + +The `disk.getStream` and `disk.getBytes` methods accept an optional `ReadOptions` object as their second parameter. +Currently the only available option is `range`. + +### Range requests +The `ReadOptions` `range` property lets you read a contiguous slice of a file instead of its full contents, which is +helpful for serving HTTP `Range` requests, streaming media segments, or reading a file header. + +```ts +/** + * Read bytes 200 through 299 (inclusive) + */ +await disk.getBytes(key, { + range: { start: 200, end: 299 }, +}) +``` + +Both `start` and `end` are optional, **zero-based**, and **inclusive** byte positions. + +
+ +
+ +start + +
+ +
+ +The first byte to read, counting from `0`. When omitted, the range starts at the beginning of the file (byte `0`). +A range of `{ start: 6 }` reads from byte `6` to the end of the file. + +
+ +
+ +end + +
+ +
+ +The last byte to read (inclusive). When omitted, the range continues to the end of the file. +A range of `{ end: 4 }` reads the first five bytes (`0` through `4`). + +
+ +
+ +:::note + +Only absolute ranges are supported. Suffix ranges (e.g. "the last 500 bytes" via `bytes=-500`) and negative offsets are +**not** supported and are rejected before any I/O. + +::: + +If both `start` and `end` are omitted (or an empty `range: {}` object is passed), the method behaves exactly as it would +without a range and returns the full file contents. + +```ts +// Reads from byte 6 to the end of the file +await disk.getStream(key, { range: { start: 6 } }) + +// Reads the first 5 bytes (0–4 inclusive) +await disk.getStream(key, { range: { end: 4 } }) + +// Reads a single byte +await disk.getStream(key, { range: { start: 0, end: 0 } }) +``` + +#### Range validation + +A range request is validated in two phases, and both throw a unified +[`E_RANGE_UNSATISFIABLE`](./key_concepts.md#unified-exceptions) exception on failure: + +1. **Syntax validation** (before any I/O) — `start` and `end` must be non-negative integers, and `start` may not be greater than `end`. +2. **Bounds validation** — the range must fall within the file's size. Because the underlying storage backends behave inconsistently with out-of-bounds ranges (`fs`, `gcs`, and `s3` may silently clamp or return empty data rather than erroring), FlyDrive performs a preflight size lookup and throws a consistent exception across all drivers when the range exceeds the file size. + +:::note + +To guarantee consistent error behaviour, ranged reads perform an additional metadata lookup before fetching data +(a `stat` for `fs`, a `getMetadata` call for `gcs`, and a `HeadObject` call for `s3`). This incurs one extra round-trip +for the cloud drivers. Non-ranged reads are unaffected. + +::: + +The `range` option is **not** available on the `disk.get` method. Slicing a file at an arbitrary byte boundary can split +a multi-byte UTF-8 character, which would break `get`'s contract of returning a valid UTF-8 string. Use `getBytes` or +`getStream` for partial reads instead. + ## Write options Following is the list of options accepted as a third parameter by the `disk.put`, `disk.putStream`, `disk.copy`, `disk.move`, `disk.copyFromFs`, and `disk.moveFromFs` methods. For example: diff --git a/content/docs/file_api.md b/content/docs/file_api.md index 2035b2f..8b9dce7 100644 --- a/content/docs/file_api.md +++ b/content/docs/file_api.md @@ -41,6 +41,14 @@ await file.getStream() */ await file.getBytes() +/** + * The "getStream" and "getBytes" methods also accept a + * "range" to read a subset of the file's bytes. + * See the Disk API documentation for details. + */ +await file.getStream({ range: { start: 0, end: 1023 } }) +await file.getBytes({ range: { start: 0, end: 1023 } }) + /** * Get file metadata */