Skip to content

Commit c522c84

Browse files
authored
Merge pull request #428 from autonomys/update-chunked-downloads
feat: support conditional fetching from cache or dsn
2 parents 418880e + 47f88c5 commit c522c84

File tree

3 files changed

+100
-12
lines changed

3 files changed

+100
-12
lines changed

packages/auto-files/src/api.ts

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { httpBodyToStream } from '@autonomys/asynchronous'
12
import { CompressionOptions, EncryptionOptions } from '@autonomys/auto-drive'
23
import { Readable } from 'stream'
34
import { withRetries } from './utils'
4-
55
interface FetchedFile {
66
length: bigint
77
compression: CompressionOptions | undefined
@@ -40,15 +40,14 @@ export const createAutoFilesApi = (baseUrl: string, apiSecret: string) => {
4040
const getChunk = async (cid: string, chunk: number): Promise<ArrayBuffer | null> => {
4141
const response = await authFetch(`${baseUrl}/files/${cid}/partial?chunk=${chunk}`)
4242
if (!response.ok) {
43-
throw new Error('Error fetching chunk')
43+
throw new Error(`Error fetching chunk: ${response.status} ${response.statusText}`)
4444
}
4545

4646
if (response.status === 204) {
4747
return null
4848
}
4949

5050
const buffer = await response.arrayBuffer()
51-
console.log('Chunk download finished:', buffer.byteLength)
5251
return buffer
5352
}
5453

@@ -61,19 +60,21 @@ export const createAutoFilesApi = (baseUrl: string, apiSecret: string) => {
6160
* @returns A Promise that resolves to a FetchedFile object containing the file data and metadata
6261
* @throws Error if the file metadata fetch fails
6362
*/
64-
const getFile = async (
63+
const getChunkedFile = async (
6564
cid: string,
6665
{
6766
retriesPerFetch = 3,
6867
onProgress,
6968
}: { retriesPerFetch?: number; onProgress?: (progress: number) => void } = {},
7069
): Promise<FetchedFile> => {
71-
const response = await withRetries(
72-
() => authFetch(`${baseUrl}/files/${cid}/metadata`),
73-
retriesPerFetch,
74-
)
70+
const response = await withRetries(() => authFetch(`${baseUrl}/files/${cid}/metadata`), {
71+
retries: retriesPerFetch,
72+
onRetry: (error, pendingRetries) => {
73+
console.error(`Error fetching file header, pending retries: ${pendingRetries}`, error)
74+
},
75+
})
7576
if (!response.ok) {
76-
throw new Error('Error fetching file header')
77+
throw new Error(`Error fetching file header: ${response.status} ${response.statusText}`)
7778
}
7879

7980
const metadata = await response.json()
@@ -90,7 +91,9 @@ export const createAutoFilesApi = (baseUrl: string, apiSecret: string) => {
9091
return {
9192
data: new Readable({
9293
async read() {
93-
const chunk = await withRetries(() => getChunk(cid, i++), retriesPerFetch)
94+
const chunk = await withRetries(() => getChunk(cid, i++), {
95+
retries: retriesPerFetch,
96+
})
9497
this.push(chunk ? Buffer.from(chunk) : null)
9598
totalDownloaded += BigInt(chunk?.byteLength ?? 0)
9699
onProgress?.(Number((BigInt(precision) * totalDownloaded) / length) / precision)
@@ -102,5 +105,64 @@ export const createAutoFilesApi = (baseUrl: string, apiSecret: string) => {
102105
}
103106
}
104107

105-
return { getFile }
108+
/**
109+
* Checks if a file is cached on the gateway
110+
* @param cid - The content identifier of the file
111+
* @returns A Promise that resolves to true if the file is cached, false otherwise
112+
* @throws Error if the file status check fails
113+
*/
114+
const isFileCached = async (cid: string) => {
115+
const response = await authFetch(`${baseUrl}/files/${cid}/status`)
116+
if (!response.ok) {
117+
throw new Error(`Error checking file status: ${response.status} ${response.statusText}`)
118+
}
119+
120+
const status = await response.json()
121+
122+
return status.isCached
123+
}
124+
125+
const getFileFromCache = async (cid: string): Promise<Readable> => {
126+
const response = await authFetch(`${baseUrl}/files/${cid}`)
127+
if (!response.ok) {
128+
throw new Error(`Error fetching file from cache: ${response.status} ${response.statusText}`)
129+
}
130+
131+
if (!response.body) {
132+
throw new Error('No body in response')
133+
}
134+
135+
return httpBodyToStream(response.body)
136+
}
137+
138+
/**
139+
* Fetches a complete file from the API with support for progress tracking and retries
140+
* @param cid - The content identifier of the file to fetch
141+
* @returns A Promise that resolves to a FetchedFile object containing the file data and metadata
142+
* @throws Error if the file metadata fetch fails
143+
*/
144+
const getFile = async (
145+
cid: string,
146+
{
147+
retriesPerFetch = 3,
148+
onProgress,
149+
ignoreCache = false,
150+
}: {
151+
retriesPerFetch?: number
152+
onProgress?: (progress: number) => void
153+
ignoreCache?: boolean
154+
} = {},
155+
) => {
156+
if (!ignoreCache && (await isFileCached(cid))) {
157+
return getFileFromCache(cid)
158+
}
159+
160+
const file = await getChunkedFile(cid, {
161+
retriesPerFetch,
162+
onProgress,
163+
})
164+
return file.data
165+
}
166+
167+
return { getFile, isFileCached, getChunkedFile }
106168
}

packages/auto-files/src/utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
export const withRetries = <T>(fn: () => Promise<T>, retries: number, delay: number = 1000) => {
1+
export const withRetries = <T>(
2+
fn: () => Promise<T>,
3+
{
4+
retries = 3,
5+
delay = 1000,
6+
onRetry,
7+
}: {
8+
retries?: number
9+
delay?: number
10+
onRetry?: (error: Error, pendingRetries: number) => void
11+
} = {},
12+
) => {
213
return new Promise<T>((resolve, reject) => {
314
const attempt = async () => {
415
try {
516
const result = await fn()
617
resolve(result)
718
} catch (error) {
819
if (retries > 0) {
20+
onRetry?.(error as Error, retries)
921
await new Promise((resolve) => setTimeout(resolve, delay))
1022
attempt()
1123
} else {

packages/utility/asynchronous/src/streams/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,17 @@ export const streamToBuffer = async (stream: Readable): Promise<Buffer> => {
1919
stream.on('error', (error) => reject(error))
2020
})
2121
}
22+
23+
export const httpBodyToStream = (body: ReadableStream): Readable => {
24+
const reader = body.getReader()
25+
return new Readable({
26+
async read() {
27+
const { done, value } = await reader.read()
28+
if (done) {
29+
this.push(null)
30+
} else {
31+
this.push(value)
32+
}
33+
},
34+
})
35+
}

0 commit comments

Comments
 (0)