Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: downloading blocks of a file by index #271

Merged
merged 10 commits into from
Oct 16, 2023
3 changes: 2 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:

env:
BEE_VERSION: '1.11.1'
FAIROS_IMAGE: 'fairdatasociety/fairos-dfs:v0.9.6'

jobs:
nodejs:
Expand Down Expand Up @@ -38,7 +39,7 @@ jobs:
run: npm install -g @fairdatasociety/fdp-play

- name: Run fdp-play
run: fdp-play start -d --fairos --bee-version $BEE_VERSION
run: fdp-play start -d --fairos --fairos-image $FAIROS_IMAGE --bee-version $BEE_VERSION

## Try getting the node modules from cache, if failed npm ci
- uses: actions/cache@v2
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ await fdp.file.downloadData('my-new-pod', '/myfile.txt', {
console.log(event)
}
})

// or you can download data block-by-block to combine it later
const blockSize = 1000000
const fileMeta = await fdp.file.getMetadata(pod, fullPath)
const result = new Uint8Array(fileMeta.fileSize)
for (let i = 0; i < blocksCount; i++) {
result.set(await fdp.file.downloadDataBlock(fileMeta, i), i * blockSize)
}
```

Deleting a pod
Expand Down
61 changes: 58 additions & 3 deletions src/file/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ import {
uploadBytes,
} from './utils'
import { writeFeedData } from '../feed/api'
import { downloadData, uploadData, uploadDataBlock } from './handler'
import { downloadData, getFileMetadataWithBlocks, uploadData, uploadDataBlock } from './handler'
import { getFileMetadataRawBytes, rawFileMetadataToFileMetadata } from './adapter'
import { DataDownloadOptions, DataUploadOptions, ExternalDataBlock, FileReceiveOptions, FileShareInfo } from './types'
import {
DataDownloadOptions,
DataUploadOptions,
ExternalDataBlock,
FileMetadataWithBlocks,
FileReceiveOptions,
FileShareInfo,
} from './types'
import { addEntryToDirectory, DEFAULT_UPLOAD_OPTIONS, removeEntryFromDirectory } from '../content-items/handler'
import { Reference } from '@ethersphere/bee-js'
import { BeeRequestOptions, Reference } from '@ethersphere/bee-js'
import { getRawMetadata } from '../content-items/utils'
import { assertRawFileMetadata, combine, splitPath } from '../directory/utils'
import { assertEncryptedReference, EncryptedReference } from '../utils/hex'
Expand Down Expand Up @@ -64,6 +71,7 @@ export class File {
): Promise<FileMetadata> {
options = { ...DEFAULT_UPLOAD_OPTIONS, ...options }
assertAccount(this.accountData)
assertPodName(podName)

return uploadData(podName, fullPath, data, this.accountData, options)
}
Expand Down Expand Up @@ -166,4 +174,51 @@ export class File {
index: blockIndex,
}
}

/**
* Downloads file metadata with blocks data
*
* @param podName pod where file is stored
* @param fullPath full path of the file
* @param downloadOptions bee download options
* @param options data download options
*/
async getMetadata(
podName: string,
fullPath: string,
downloadOptions?: BeeRequestOptions,
options?: DataDownloadOptions,
): Promise<FileMetadataWithBlocks> {
assertAccount(this.accountData)
assertPodName(podName)
assertFullPathWithName(fullPath)

return getFileMetadataWithBlocks(
this.accountData.connection.bee,
this.accountData,
podName,
fullPath,
downloadOptions,
options,
)
}

/**
* Downloads data block using file metadata
*
* @param meta file metadata
* @param blockIndex block index
* @param downloadOptions bee download options
*/
async downloadDataBlock(
meta: FileMetadataWithBlocks,
blockIndex: number,
downloadOptions?: BeeRequestOptions,
): Promise<Uint8Array> {
if (blockIndex < 0 || blockIndex >= meta.blocks.length) {
throw new Error('"blockIndex" is out of bounds')
}

return this.accountData.connection.bee.downloadData(meta.blocks[blockIndex].reference, downloadOptions)
}
}
52 changes: 44 additions & 8 deletions src/file/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
DEFAULT_FILE_PERMISSIONS,
downloadBlocksManifest,
externalDataBlocksToBlocks,
extractPathInfo, getDataBlock,
extractPathInfo,
getDataBlock,
getFileMode,
isExternalDataBlocks,
updateDownloadProgress,
Expand All @@ -27,6 +28,7 @@ import {
DataUploadOptions,
DownloadProgressType,
ExternalDataBlock,
FileMetadataWithBlocks,
UploadProgressType,
} from './types'
import { assertPodName, getExtendedPodsListByAccountData, META_VERSION } from '../pod/utils'
Expand Down Expand Up @@ -71,23 +73,24 @@ export async function getFileMetadata(
}

/**
* Downloads file parts and compile them into Data
* Gets file metadata with blocks
*
* @param bee Bee
* @param accountData account data
* @param podName pod name
* @param fullPath full path to the file
* @param downloadOptions download options
* @param dataDownloadOptions data download options
*/
export async function downloadData(
export async function getFileMetadataWithBlocks(
bee: Bee,
accountData: AccountData,
podName: string,
fullPath: string,
downloadOptions?: BeeRequestOptions,
dataDownloadOptions?: DataDownloadOptions,
): Promise<Data> {
): Promise<FileMetadataWithBlocks> {
dataDownloadOptions = dataDownloadOptions ?? {}
const bee = accountData.connection.bee
updateDownloadProgress(dataDownloadOptions, DownloadProgressType.GetPodInfo)
const { podAddress, pod } = await getExtendedPodsListByAccountData(accountData, podName)
updateDownloadProgress(dataDownloadOptions, DownloadProgressType.GetPathInfo)
Expand All @@ -101,15 +104,48 @@ export async function downloadData(
updateDownloadProgress(dataDownloadOptions, DownloadProgressType.DownloadBlocksMeta)
const blocks = await downloadBlocksManifest(bee, fileMetadata.blocksReference, downloadOptions)

return {
...fileMetadata,
...blocks,
}
}

/**
* Downloads file parts and compile them into Data
*
* @param accountData account data
* @param podName pod name
* @param fullPath full path to the file
* @param downloadOptions download options
* @param dataDownloadOptions data download options
*/
export async function downloadData(
accountData: AccountData,
podName: string,
fullPath: string,
downloadOptions?: BeeRequestOptions,
dataDownloadOptions?: DataDownloadOptions,
): Promise<Data> {
dataDownloadOptions = dataDownloadOptions ?? {}
const bee = accountData.connection.bee
const { blocks } = await getFileMetadataWithBlocks(
bee,
accountData,
podName,
fullPath,
downloadOptions,
dataDownloadOptions,
)

let totalLength = 0
for (const block of blocks.blocks) {
for (const block of blocks) {
totalLength += block.size
}

const result = new Uint8Array(totalLength)
let offset = 0
const totalBlocks = blocks.blocks.length
for (const [currentBlockId, block] of blocks.blocks.entries()) {
const totalBlocks = blocks.length
for (const [currentBlockId, block] of blocks.entries()) {
const blockData = {
totalBlocks,
currentBlockId,
Expand Down
7 changes: 6 additions & 1 deletion src/file/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Reference } from '@ethersphere/bee-js'
import { RawFileMetadata } from '../pod/types'
import { FileMetadata, RawFileMetadata } from '../pod/types'

/**
* Download progress info
Expand Down Expand Up @@ -164,6 +164,11 @@ export interface Blocks {
blocks: Block[]
}

/**
* FDP file metadata with blocks data
*/
export type FileMetadataWithBlocks = FileMetadata & Blocks

/**
* FDP file block format
*/
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
DataDownloadOptions,
Block,
ExternalDataBlock,
FileMetadataWithBlocks,
} from './file/types'
export {
calcUploadBlockPercentage,
Expand Down
24 changes: 24 additions & 0 deletions test/integration/node/download-by-index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createFdp, generateRandomHexString, generateUser, makeFileContent } from '../../utils'
import { wrapBytesWithHelpers } from '../../../src/utils/bytes'

jest.setTimeout(400000)
it('Download by index', async () => {
const fdp = createFdp()
const pod = generateRandomHexString()
const { size, blocksCount, blockSize, content, filename, fullPath } = makeFileContent(5000005)
generateUser(fdp)
await fdp.personalStorage.create(pod)
await fdp.file.uploadData(pod, fullPath, content)
const fileMeta = await fdp.file.getMetadata(pod, fullPath)
expect(fileMeta.fileName).toEqual(filename)
expect(fileMeta.blockSize).toEqual(blockSize)
expect(fileMeta.fileSize).toEqual(size)

const result = new Uint8Array(fileMeta.fileSize)
for (let i = 0; i < blocksCount; i++) {
result.set(await fdp.file.downloadDataBlock(fileMeta, i), i * blockSize)
}

const dataBig = wrapBytesWithHelpers(await fdp.file.downloadData(pod, fullPath)).text()
expect(dataBig).toEqual(content)
})
2 changes: 1 addition & 1 deletion test/integration/node/upload-by-index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createFdp, generateRandomHexString, generateUser, makeFileContent } from '../../utils'
import { wrapBytesWithHelpers } from '../../../src/utils/bytes'
import { getDataBlock } from '../../../src/file/utils'
import { getDataBlock } from '../../../src'

jest.setTimeout(400000)
it('Upload by index', async () => {
Expand Down
27 changes: 19 additions & 8 deletions test/utils/fairos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,24 @@ export class FairOSApi {

return config
})
axios.interceptors.response.use(response => {
if (this.withCredentials && response.headers['set-cookie']) {
this.cookies = response.headers['set-cookie']?.[0] as string
}
axios.interceptors.response.use(
response => {
if (this.withCredentials && response.headers['set-cookie']) {
this.cookies = response.headers['set-cookie']?.[0] as string
}

return response
})
return response
},
error => {
if (error.response) {
// Append server response to error message
throw new Error(`AxiosError: ${error.message}. Server response: ${JSON.stringify(error.response?.data)}`)
} else {
// Something happened while setting up the request
throw error
}
},
)
}

private getV2Url(method: string) {
Expand Down Expand Up @@ -73,7 +84,7 @@ export class FairOSApi {
async login(username: string, password: string): Promise<AxiosResponse> {
const url = this.getV2Url('user/login')

return await axios.post(url, {
return axios.post(url, {
userName: username,
password,
})
Expand All @@ -85,7 +96,7 @@ export class FairOSApi {
async register(username: string, password: string, mnemonic: string): Promise<AxiosResponse> {
const url = this.getV2Url('user/signup')

return await axios.post(url, {
return axios.post(url, {
userName: username,
password,
mnemonic,
Expand Down
Loading