@@ -9,7 +9,11 @@ import {
99 IPLDNodeData ,
1010 MetadataType ,
1111} from '@autonomys/auto-dag-data'
12- import { FileResponse } from '@autonomys/file-caching'
12+ import {
13+ ByteRange ,
14+ FileCacheOptions ,
15+ FileResponse ,
16+ } from '@autonomys/file-caching'
1317import { z } from 'zod'
1418import { PBNode } from '@ipld/dag-pb'
1519import { HttpError } from '../http/middlewares/error.js'
@@ -29,6 +33,7 @@ import { Readable } from 'stream'
2933import { ReadableStream } from 'stream/web'
3034import { fileCache , nodeCache } from './cache.js'
3135import { dagIndexerRepository } from '../repositories/dag-indexer.js'
36+ import { sliceReadable } from '../utils/readable.js'
3237
3338const fetchNodesSchema = z . object ( {
3439 jsonrpc : z . string ( ) ,
@@ -141,6 +146,123 @@ const fetchObjects = async (objects: ObjectMapping[]) => {
141146 )
142147}
143148
149+ const getNodesForPartialRetrieval = async (
150+ chunks : ExtendedIPLDMetadata [ ] ,
151+ byteRange : ByteRange ,
152+ ) : Promise < {
153+ nodes : string [ ]
154+ firstNodeFileOffset : number
155+ } > => {
156+ let accumulatedLength = 0
157+ const nodeRange : [ number | null , number | null ] = [ null , null ]
158+ let firstNodeFileOffset : number | undefined
159+ let i = 0
160+
161+ logger . debug (
162+ `getNodesForPartialRetrieval called (byteRange=[${ byteRange [ 0 ] } , ${ byteRange [ 1 ] ?? 'EOF' } ])` ,
163+ )
164+
165+ // Searches for the first node that contains the byte range
166+ while ( nodeRange [ 0 ] === null && i < chunks . length ) {
167+ const chunk = chunks [ i ]
168+ const chunkSize = Number ( ( chunk . size ?? 0 ) . valueOf ( ) )
169+ // [accumulatedLength, accumulatedLength + chunkSize) // is the range of the chunk
170+ if (
171+ byteRange [ 0 ] >= accumulatedLength &&
172+ byteRange [ 0 ] < accumulatedLength + chunkSize
173+ ) {
174+ nodeRange [ 0 ] = i
175+ firstNodeFileOffset = accumulatedLength
176+ } else {
177+ accumulatedLength += chunkSize
178+ i ++
179+ }
180+ }
181+
182+ // Searchs for the last node that contains the byte range
183+ // unless the byte range is the last byte of the file
184+ if ( byteRange [ 1 ] ) {
185+ while ( nodeRange [ 1 ] === null && i < chunks . length ) {
186+ const chunk = chunks [ i ]
187+ const chunkSize = Number ( ( chunk . size ?? 0 ) . valueOf ( ) )
188+ if (
189+ byteRange [ 1 ] >= accumulatedLength &&
190+ byteRange [ 1 ] < accumulatedLength + chunkSize
191+ ) {
192+ nodeRange [ 1 ] = i
193+ }
194+ accumulatedLength += chunkSize
195+ i ++
196+ }
197+ }
198+
199+ if ( nodeRange [ 0 ] == null ) {
200+ throw new Error ( 'Byte range not found' )
201+ }
202+
203+ const nodes = chunks
204+ . slice ( nodeRange [ 0 ] , nodeRange [ 1 ] === null ? undefined : nodeRange [ 1 ] + 1 )
205+ . map ( ( e ) => e . cid )
206+
207+ return {
208+ nodes,
209+ firstNodeFileOffset : firstNodeFileOffset ?? 0 ,
210+ }
211+ }
212+
213+ const fetchFileAsStreamWithByteRange = async (
214+ cid : string ,
215+ byteRange : ByteRange ,
216+ ) : Promise < Readable > => {
217+ const chunks = await dsnFetcher . getFileChunks ( cid )
218+ const { nodes, firstNodeFileOffset } = await getNodesForPartialRetrieval (
219+ chunks ,
220+ byteRange ,
221+ )
222+
223+ logger . debug (
224+ `getNodesForPartialRetrieval called (byteRange=[${ byteRange [ 0 ] } , ${ byteRange [ 1 ] ?? 'EOF' } ]) nodes=${ JSON . stringify ( nodes ) } firstNodeFileOffset=${ firstNodeFileOffset } ` ,
225+ )
226+
227+ // We pass all the chunks to the fetchNode function
228+ // So that we can fetch all the nodes within the same piece
229+ // in one go
230+ const siblings = chunks . map ( ( e ) => e . cid )
231+ const stream = new ReadableStream ( {
232+ start : async ( controller ) => {
233+ try {
234+ for ( const chunk of nodes ) {
235+ const node = await dsnFetcher . fetchNode ( chunk , siblings )
236+ const data = safeIPLDDecode ( node )
237+ if ( ! data ) {
238+ throw new HttpError (
239+ 400 ,
240+ 'Bad request: Not a valid auto-dag-data IPLD node' ,
241+ )
242+ }
243+
244+ controller . enqueue ( Buffer . from ( data . data ?? [ ] ) )
245+ }
246+
247+ controller . close ( )
248+ } catch ( error ) {
249+ controller . error ( error )
250+ }
251+ } ,
252+ } )
253+
254+ const metadata = await dsnFetcher . fetchNodeMetadata ( cid )
255+ const fileSize = Number ( metadata . size )
256+ const endIndex = byteRange [ 1 ] ?? fileSize - 1
257+ const length = endIndex - byteRange [ 0 ] + 1
258+
259+ return sliceReadable (
260+ Readable . fromWeb ( stream ) ,
261+ byteRange [ 0 ] - firstNodeFileOffset ,
262+ length ,
263+ )
264+ }
265+
144266/**
145267 * Fetches a file as a stream
146268 *
@@ -150,13 +272,13 @@ const fetchObjects = async (objects: ObjectMapping[]) => {
150272 * @param node - The root node of the file
151273 * @returns A readable stream of the file
152274 */
153- const fetchFileAsStream = async ( cid : string ) : Promise < ReadableStream > => {
275+ const fetchFileAsStream = async ( cid : string ) : Promise < Readable > => {
154276 const chunks = await dsnFetcher . getFileChunks ( cid )
155277
156278 // if a file is a multi-node file, we need to fetch the nodes in the correct order
157279 // bearing in mind there might be multiple levels of links, we need to fetch
158280 // all the links from the root node first and then continue with the next level
159- return new ReadableStream ( {
281+ const stream = new ReadableStream ( {
160282 start : async ( controller ) => {
161283 try {
162284 for ( const chunk of chunks ) {
@@ -181,6 +303,8 @@ const fetchFileAsStream = async (cid: string): Promise<ReadableStream> => {
181303 }
182304 } ,
183305 } )
306+
307+ return Readable . fromWeb ( stream )
184308}
185309
186310const getFileMetadata = (
@@ -202,7 +326,10 @@ const getFileMetadata = (
202326 }
203327}
204328
205- const fetchFile = async ( cid : string ) : Promise < FileResponse > => {
329+ const fetchFile = async (
330+ cid : string ,
331+ options ?: FileCacheOptions ,
332+ ) : Promise < FileResponse > => {
206333 try {
207334 const metadata = await dsnFetcher . fetchNodeMetadata ( cid )
208335 if ( metadata . type !== MetadataType . File ) {
@@ -215,8 +342,12 @@ const fetchFile = async (cid: string): Promise<FileResponse> => {
215342 `Fetching file (cid=${ cid } , size=${ traits . size } , mimeType=${ traits . mimeType } , filename=${ traits . filename } , encoding=${ traits . encoding } )` ,
216343 )
217344
345+ const readable = options ?. byteRange
346+ ? await fetchFileAsStreamWithByteRange ( cid , options . byteRange )
347+ : await fetchFileAsStream ( cid )
348+
218349 return {
219- data : Readable . fromWeb ( await fetchFileAsStream ( cid ) ) ,
350+ data : readable ,
220351 ...traits ,
221352 }
222353 } catch ( error ) {
0 commit comments