Skip to content

Commit 1ae73d0

Browse files
authored
Support byte range downloads (#436)
* chore: update dependencies - Update uuid to ^11.1.0 and add @types/uuid - Update @autonomys/file-caching to ^1.5.1 - Update related dependencies across packages * feat: add HTTP range request utility functions - Add parseByteRange function for parsing Range headers - Add createPartialStream function for creating range-aware streams - Add utility functions for handling HTTP 206 partial content responses - Support for byte-range requests in readable streams * feat: implement range request support in download services - Add ByteRange interface for range request parameters - Implement partial download support in memory cache - Add range-aware download methods in download service - Support for caching partial content with byte ranges - Add sync download functionality with range support * feat: add HTTP range request support to download controllers - Update download controller to handle Range headers - Implement HTTP 206 Partial Content responses - Add proper Content-Range and Accept-Ranges headers - Support partial file downloads in object controllers - Update use cases to handle byte range parameters - Add range request support to object download endpoints * test: add comprehensive tests for partial retrieval functionality - Add tests for byte range parsing and validation - Test partial content download scenarios - Verify HTTP 206 response handling - Add test cases for range request edge cases - Test memory cache integration with range requests * refactor: move to separate files related logic * refactor: enhance download response handling with byte range support - Introduced isExpectedDocument function to streamline document expectation checks - Refactored handleDownloadResponseHeaders to utilize setFileResponseHeaders and setFolderResponseHeaders for improved clarity - Added byte range handling in setFileResponseHeaders for partial content responses - Updated content disposition and content type settings based on encryption and document expectations * refactor: reorganize file use cases * fix(testing): cache populated in async setting * feat: update auto files gateway version * update: bump auto-sdk version * refactor: update FilesUseCases tests and imports - Refactored tests for FilesUseCases to use getObjectInformation instead of getMetadata. - Updated mock implementations to reflect changes in the download blocking logic. - Cleaned up imports in files.spec.ts and index.ts for better organization and clarity. * refactor: clean up imports in files.spec.ts - Removed unused dbMigration import to streamline the test file and improve clarity. * fix: update byte range header handling in getByteRange function - Changed the header used to retrieve the byte range from 'content-range' to 'range' for improved accuracy in handling byte range requests. * fix: enhance byte range validation in getByteRange function - Added validation checks to ensure start and end byte values are non-negative and that the start value does not exceed the end value, improving the robustness of byte range handling. * chore: update package dependencies to version 1.5.11 * chore: update yarn.lock to reflect package versions 1.5.11 for auto-dag-data and file-caching
1 parent e3e34f0 commit 1ae73d0

File tree

25 files changed

+1710
-1153
lines changed

25 files changed

+1710
-1153
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[submodule "files-gateway"]
22
path = submodules/files-gateway
33
url = https://github.com/autonomys/auto-files-gateway.git
4-
branch = v1.0.3
4+
branch = v1.0.4

auth/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"serverless-http": "^3.2.0",
3333
"siwe": "^3.0.0",
3434
"typescript": "^5.7.3",
35-
"uuid": "^11.0.5"
35+
"uuid": "^11.1.0"
3636
},
3737
"devDependencies": {
3838
"@testcontainers/postgresql": "^10.14.0",
@@ -43,7 +43,7 @@
4343
"@types/express": "^5.0.0",
4444
"@types/jest": "^29.5.14",
4545
"@types/multer": "^1.4.12",
46-
"@types/uuid": "^9.0.0",
46+
"@types/uuid": "^9.0.1",
4747
"db-migrate-plugin-typescript": "^2.0.0",
4848
"eslint": "^8.57.1",
4949
"eslint-config-prettier": "^9.1.0",

backend/__tests__/e2e/uploads/files.spec.ts

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import { ObjectUseCases } from '../../../src/useCases/objects/object.js'
2929
import { uploadsRepository } from '../../../src/repositories/uploads/uploads.js'
3030
import { asyncIterableToPromiseOfArray } from '@autonomys/asynchronous'
3131
import {
32-
FilesUseCases,
3332
NodesUseCases,
3433
SubscriptionsUseCases,
3534
} from '../../../src/useCases/index.js'
@@ -38,19 +37,19 @@ import {
3837
metadataRepository,
3938
nodesRepository,
4039
} from '../../../src/repositories/index.js'
41-
import { memoryDownloadCache } from '../../../src/services/download/memoryDownloadCache/index.js'
4240
import { jest } from '@jest/globals'
43-
import { downloadService } from '../../../src/services/download/index.js'
4441
import { BlockstoreUseCases } from '../../../src/useCases/uploads/blockstore.js'
4542
import { Rabbit } from '../../../src/drivers/rabbit.js'
4643
import { EventRouter } from '../../../src/services/eventRouter/index.js'
4744
import { MAX_RETRIES } from '../../../src/services/eventRouter/tasks.js'
45+
import { DownloadUseCase } from '../../../src/useCases/objects/downloads.js'
46+
import { downloadService } from '../../../src/services/download/index.js'
4847

4948
const files = [
5049
{
5150
filename: 'test.pdf',
5251
mimeType: 'application/pdf',
53-
rndBuffer: Buffer.alloc(1024 ** 2).fill(0),
52+
rndBuffer: Buffer.alloc(64000).fill(0),
5453
},
5554
{
5655
filename: 'test.txt',
@@ -246,15 +245,28 @@ files.map((file, index) => {
246245
})
247246

248247
describe('Downloading the file', () => {
248+
let handleCacheMock: jest.SpiedFunction<
249+
typeof downloadService.handleCache
250+
>
251+
249252
it('should be able to retrieve the file', async () => {
250-
const { startDownload } = await FilesUseCases.downloadObjectByUser(
253+
handleCacheMock = jest.spyOn(downloadService, 'handleCache')
254+
255+
const { startDownload } = await DownloadUseCase.downloadObjectByUser(
251256
user,
252257
cid,
253258
)
254259
const file = await startDownload()
255260
const fileArray = await asyncIterableToPromiseOfArray(file)
256261
const fileBuffer = Buffer.concat(fileArray)
257262
expect(fileBuffer).toEqual(rndBuffer)
263+
264+
expect(handleCacheMock).toHaveBeenCalledWith(
265+
cid,
266+
expect.any(Object),
267+
expect.any(Object),
268+
expect.any(BigInt),
269+
)
258270
})
259271

260272
it('should have been added an interaction', async () => {
@@ -269,26 +281,6 @@ files.map((file, index) => {
269281

270282
expect(interactions).toHaveLength(1)
271283
})
272-
273-
it('download cache should be updated', async () => {
274-
// Wait for the async context to finish
275-
await new Promise((resolve) => setTimeout(resolve, 300))
276-
const asyncFromDatabase = await downloadService.fsCache.get(cid)
277-
expect(asyncFromDatabase).not.toBeNull()
278-
const fileArrayFromDatabase = await asyncIterableToPromiseOfArray(
279-
asyncFromDatabase!.data,
280-
)
281-
const fileBufferFromDatabase = Buffer.concat(fileArrayFromDatabase)
282-
expect(fileBufferFromDatabase).toEqual(rndBuffer)
283-
284-
const asyncFromMemory = memoryDownloadCache.get(cid)
285-
expect(asyncFromMemory).not.toBeNull()
286-
const fileArrayFromMemory = await asyncIterableToPromiseOfArray(
287-
asyncFromMemory!,
288-
)
289-
const fileBufferFromMemory = Buffer.concat(fileArrayFromMemory)
290-
expect(fileBufferFromMemory).toEqual(rndBuffer)
291-
})
292284
})
293285

294286
describe('Object Information', () => {
@@ -358,7 +350,7 @@ files.map((file, index) => {
358350
)
359351
})
360352

361-
it('metadata should be updated as archived', async () => {
353+
it('metadata should be updated as archived and cache should be updated', async () => {
362354
const processArchivalSpy = jest
363355
.spyOn(EventRouter, 'publish')
364356
.mockReturnValue()
@@ -368,9 +360,6 @@ files.map((file, index) => {
368360
const metadata = await metadataRepository.getMetadata(cid)
369361
expect(metadata).not.toBeNull()
370362

371-
expect(memoryDownloadCache.has(cid)).toBe(true)
372-
expect(downloadService.fsCache.get(cid)).not.toBeNull()
373-
374363
expect(processArchivalSpy).toHaveBeenCalledWith({
375364
id: 'object-archived',
376365
params: {

backend/__tests__/e2e/uploads/folder.spec.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ import {
1414
UploadType,
1515
UserWithOrganization,
1616
} from '@auto-drive/models'
17-
import {
18-
FilesUseCases,
19-
NodesUseCases,
20-
ObjectUseCases,
21-
} from '../../../src/useCases/index.js'
17+
import { NodesUseCases, ObjectUseCases } from '../../../src/useCases/index.js'
2218
import { UploadsUseCases } from '../../../src/useCases/uploads/uploads.js'
2319
import { dbMigration } from '../../utils/dbMigrate.js'
2420
import { PreconditionError } from '../../utils/error.js'
@@ -37,6 +33,7 @@ import { asyncIterableToPromiseOfArray } from '@autonomys/asynchronous'
3733
import PizZip from 'pizzip'
3834
import { BlockstoreUseCases } from '../../../src/useCases/uploads/blockstore.js'
3935
import { Rabbit } from '../../../src/drivers/rabbit.js'
36+
import { DownloadUseCase } from '../../../src/useCases/objects/downloads.js'
4037

4138
describe('Folder Upload', () => {
4239
let user: UserWithOrganization
@@ -365,7 +362,7 @@ describe('Folder Upload', () => {
365362
})
366363

367364
it('should be able to download folder as zip', async () => {
368-
const zip = await FilesUseCases.downloadObjectByUser(user, folderCID)
365+
const zip = await DownloadUseCase.downloadObjectByUser(user, folderCID)
369366
const dataStream = await zip.startDownload()
370367
const zipArray = await asyncIterableToPromiseOfArray(dataStream)
371368
const zipBuffer = Buffer.concat(zipArray)

backend/__tests__/unit/useCases/files.spec.ts

Lines changed: 121 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { jest } from '@jest/globals'
22
import { ObjectUseCases } from '../../../src/useCases/objects/object.js'
3-
import { FilesUseCases } from '../../../src/useCases/objects/files.js'
3+
import { ObjectStatus } from '@auto-drive/models'
4+
import { ByteRange } from '@autonomys/file-caching'
5+
import { DownloadUseCase } from '../../../src/useCases/objects/downloads.js'
46
import { OffchainMetadata } from '@autonomys/auto-dag-data'
7+
import { FilesUseCases } from '../../../src/useCases/index.js'
58

69
jest.unstable_mockModule('../../../src/useCases/objects/object.js', () => ({
710
ObjectUseCases: {
@@ -27,10 +30,25 @@ describe('FilesUseCases', () => {
2730
chunks: [],
2831
}
2932

30-
jest.spyOn(ObjectUseCases, 'getMetadata').mockResolvedValue(metadata)
33+
jest.spyOn(ObjectUseCases, 'getObjectInformation').mockResolvedValue({
34+
metadata,
35+
tags: [],
36+
cid: '<cid>',
37+
createdAt: '',
38+
status: ObjectStatus.Processing,
39+
uploadState: {
40+
uploadedNodes: 0,
41+
totalNodes: 0,
42+
archivedNodes: 0,
43+
minimumBlockDepth: 0,
44+
maximumBlockDepth: 0,
45+
},
46+
owners: [],
47+
publishedObjectId: null,
48+
})
3149
jest.spyOn(ObjectUseCases, 'shouldBlockDownload').mockResolvedValue(false)
3250

33-
const result = await FilesUseCases.downloadObjectByAnonymous(
51+
const result = await DownloadUseCase.downloadObjectByAnonymous(
3452
metadata.dataCid,
3553
)
3654

@@ -44,21 +62,108 @@ describe('FilesUseCases', () => {
4462
type: 'file',
4563
}
4664

47-
const metadata: OffchainMetadata = {
48-
totalSize: 100n,
49-
type: 'file',
50-
dataCid: 'test-cid',
51-
totalChunks: 1,
52-
chunks: [],
53-
}
54-
55-
jest.spyOn(ObjectUseCases, 'getMetadata').mockResolvedValue(metadata)
65+
jest.spyOn(ObjectUseCases, 'getObjectInformation').mockResolvedValue({
66+
metadata: {
67+
totalSize: 100n,
68+
type: 'file',
69+
dataCid: 'test-cid',
70+
totalChunks: 1,
71+
chunks: [],
72+
},
73+
tags: ['insecure'],
74+
cid: '',
75+
createdAt: '',
76+
status: ObjectStatus.Processing,
77+
uploadState: {
78+
uploadedNodes: 0,
79+
totalNodes: 0,
80+
archivedNodes: 0,
81+
minimumBlockDepth: 0,
82+
maximumBlockDepth: 0,
83+
},
84+
owners: [],
85+
publishedObjectId: null,
86+
})
5687
jest.spyOn(ObjectUseCases, 'shouldBlockDownload').mockResolvedValue(true)
5788

5889
await expect(
59-
FilesUseCases.downloadObjectByAnonymous(mockFile.cid, ['insecure']),
60-
).rejects.toThrow(
61-
new Error('File download is blocked by blocking tags or is banned'),
62-
)
90+
DownloadUseCase.downloadObjectByAnonymous(mockFile.cid, {
91+
blockingTags: ['insecure'],
92+
}),
93+
).rejects.toThrow(new Error('File is blocked'))
94+
})
95+
96+
describe('getNodesForPartialRetrieval', () => {
97+
const tests = [
98+
{
99+
name: 'should return the nodes for a partial retrieval',
100+
nodes: [
101+
{ cid: 'test-cid', size: 100n },
102+
{ cid: 'test-cid-2', size: 100n },
103+
{ cid: 'test-cid-3', size: 50n },
104+
],
105+
byteRange: [0, 99] as ByteRange,
106+
expectedNodes: ['test-cid'],
107+
expectedFirstNodeFileOffset: 0,
108+
},
109+
{
110+
name: 'should return the nodes for a partial retrieval',
111+
nodes: [
112+
{ cid: 'test-cid', size: 100n },
113+
{ cid: 'test-cid-2', size: 100n },
114+
{ cid: 'test-cid-3', size: 50n },
115+
],
116+
byteRange: [0, 100] as ByteRange,
117+
expectedNodes: ['test-cid', 'test-cid-2'],
118+
expectedFirstNodeFileOffset: 0,
119+
},
120+
{
121+
name: 'should return the nodes for a partial retrieval',
122+
nodes: [
123+
{ cid: 'test-cid', size: 100n },
124+
{ cid: 'test-cid-2', size: 100n },
125+
{ cid: 'test-cid-3', size: 50n },
126+
],
127+
byteRange: [1, 100] as ByteRange,
128+
expectedNodes: ['test-cid', 'test-cid-2'],
129+
expectedFirstNodeFileOffset: 0,
130+
},
131+
{
132+
name: 'should return the nodes for a partial retrieval',
133+
nodes: [
134+
{ cid: 'test-cid', size: 100n },
135+
{ cid: 'test-cid-2', size: 100n },
136+
{ cid: 'test-cid-3', size: 50n },
137+
],
138+
byteRange: [0, undefined] as ByteRange,
139+
expectedNodes: ['test-cid', 'test-cid-2', 'test-cid-3'],
140+
expectedFirstNodeFileOffset: 0,
141+
},
142+
{
143+
name: 'should return the nodes for a partial retrieval',
144+
nodes: [
145+
{ cid: 'test-cid', size: 100n },
146+
{ cid: 'test-cid-2', size: 100n },
147+
{ cid: 'test-cid-3', size: 50n },
148+
],
149+
byteRange: [100, undefined] as ByteRange,
150+
expectedNodes: ['test-cid-2', 'test-cid-3'],
151+
expectedFirstNodeFileOffset: 100,
152+
},
153+
]
154+
155+
for (const test of tests) {
156+
it(`${test.name} (byteRange=[${test.byteRange[0]},${test.byteRange[1]}])`, async () => {
157+
const result = await FilesUseCases.getNodesForPartialRetrieval(
158+
test.nodes,
159+
test.byteRange,
160+
)
161+
162+
expect(result.nodes).toEqual(test.expectedNodes)
163+
expect(result.firstNodeFileOffset).toEqual(
164+
test.expectedFirstNodeFileOffset,
165+
)
166+
})
167+
}
63168
})
64169
})

backend/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
"dependencies": {
2222
"@auto-drive/models": "workspace:*",
2323
"@auto-files/rpc-apis": "workspace:*",
24-
"@autonomys/asynchronous": "^1.5.1",
25-
"@autonomys/auto-dag-data": "^1.5.1",
26-
"@autonomys/auto-drive": "^1.5.1",
27-
"@autonomys/auto-files": "^1.5.2",
28-
"@autonomys/file-caching": "^1.5.1",
29-
"@autonomys/rpc": "^1.5.1",
24+
"@autonomys/asynchronous": "^1.5.11",
25+
"@autonomys/auto-dag-data": "^1.5.11",
26+
"@autonomys/auto-drive": "^1.5.11",
27+
"@autonomys/auto-files": "^1.5.11",
28+
"@autonomys/file-caching": "^1.5.11",
29+
"@autonomys/rpc": "^1.5.11",
3030
"@keyvhq/sqlite": "^2.1.6",
3131
"@polkadot/api": "^15.8.1",
3232
"@polkadot/types": "^15.8.1",
@@ -52,7 +52,7 @@
5252
"pg-format": "^1.0.4",
5353
"pizzip": "^3.1.7",
5454
"tar": "^7.4.3",
55-
"uuid": "^10.0.0",
55+
"uuid": "^11.1.0",
5656
"websocket": "^1.0.35",
5757
"zod": "^3.23.8"
5858
},

0 commit comments

Comments
 (0)