Skip to content

Commit 327c068

Browse files
authored
Merge pull request #1064 from autonomys/add-download-file-ep
Add download file endpoint
2 parents 3483e02 + 843cd11 commit 327c068

10 files changed

+246
-2
lines changed

indexers/api/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@nestjs/typeorm": "^10.0.2",
4040
"class-transformer": "^0.5.1",
4141
"class-validator": "^0.14.1",
42+
"mime-types": "^2.1.35",
4243
"passport": "^0.7.0",
4344
"passport-headerapikey": "^1.2.2",
4445
"pg": "^8.13.1",
@@ -53,6 +54,7 @@
5354
"@nestjs/testing": "^10.0.0",
5455
"@types/express": "^5.0.0",
5556
"@types/jest": "^29.5.2",
57+
"@types/mime-types": "^2.1.4",
5658
"@types/node": "^20.3.1",
5759
"@types/supertest": "^6.0.0",
5860
"@typescript-eslint/eslint-plugin": "^8.0.0",

indexers/api/src/app.module.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AccountsController } from './controllers/accounts.controller';
66
import { AppController } from './controllers/app.controller';
77
import { BlocksController } from './controllers/blocks.controller';
88
import { ExtrinsicsController } from './controllers/extrinsics.controller';
9+
import { FilesController } from './controllers/files.controller';
910
import {
1011
Accounts,
1112
ApiDailyUsage,
@@ -14,14 +15,18 @@ import {
1415
ApiKeysMonthlyUsage,
1516
ApiMonthlyUsage,
1617
Blocks,
18+
Chunks,
1719
ConsensusMetadata,
1820
Extrinsics,
21+
FileCids,
22+
Files,
1923
FilesMetadata,
2024
LeaderboardMetadata,
2125
Profile,
2226
StakingMetadata,
2327
} from './entities';
2428
import { ApiUsageService } from './services/api-usage.service';
29+
import { FileRetrieverService } from './services/file-retriever.sevice';
2530

2631
@Module({
2732
imports: [
@@ -42,6 +47,9 @@ import { ApiUsageService } from './services/api-usage.service';
4247
ApiKeysDailyUsage,
4348
ApiKeysMonthlyUsage,
4449
Accounts,
50+
Files,
51+
Chunks,
52+
FileCids,
4553
ConsensusMetadata,
4654
LeaderboardMetadata,
4755
FilesMetadata,
@@ -59,6 +67,9 @@ import { ApiUsageService } from './services/api-usage.service';
5967
ApiKeysDailyUsage,
6068
ApiKeysMonthlyUsage,
6169
Accounts,
70+
Files,
71+
Chunks,
72+
FileCids,
6273
ConsensusMetadata,
6374
LeaderboardMetadata,
6475
FilesMetadata,
@@ -71,7 +82,8 @@ import { ApiUsageService } from './services/api-usage.service';
7182
BlocksController,
7283
ExtrinsicsController,
7384
AccountsController,
85+
FilesController,
7486
],
75-
providers: [ApiKeyStrategy, ApiUsageService],
87+
providers: [ApiKeyStrategy, ApiUsageService, FileRetrieverService],
7688
})
7789
export class AppModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Controller, Get, NotFoundException, Param, Res } from '@nestjs/common';
2+
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
3+
import { InjectRepository } from '@nestjs/typeorm';
4+
import { Response } from 'express';
5+
import { Repository } from 'typeorm';
6+
import { Chunks } from '../entities/files/chunks.entity';
7+
import { Files } from '../entities/files/files.entity';
8+
import { FileRetrieverService } from '../services/file-retriever.sevice';
9+
10+
@ApiTags('Files')
11+
@Controller('files')
12+
export class FilesController {
13+
constructor(
14+
private fileRetrieverService: FileRetrieverService,
15+
@InjectRepository(Files)
16+
private filesRepository: Repository<Files>,
17+
@InjectRepository(Chunks)
18+
private chunksRepository: Repository<Chunks>,
19+
) {}
20+
21+
@Get(':cid')
22+
@ApiOperation({
23+
operationId: 'getFile',
24+
summary: 'Download file by CID',
25+
})
26+
@ApiParam({ name: 'cid', description: 'CID of the file' })
27+
@ApiResponse({
28+
status: 200,
29+
description: 'Returns the file content as a byte stream',
30+
headers: {
31+
'Content-Type': {
32+
description: 'The MIME type of the file',
33+
example: 'application/octet-stream',
34+
},
35+
},
36+
})
37+
async getFile(
38+
@Param('cid') cid: string,
39+
@Res() res: Response,
40+
): Promise<void> {
41+
const file = await this.filesRepository.findOne({
42+
where: {
43+
id: cid,
44+
},
45+
});
46+
if (!file) {
47+
throw new NotFoundException(`File with CID ${cid} not found`);
48+
}
49+
50+
const chunk = await this.chunksRepository.findOne({
51+
where: {
52+
id: cid,
53+
},
54+
});
55+
56+
const contentType = file.contentType() || 'application/octet-stream';
57+
res.setHeader('Content-Type', contentType);
58+
res.setHeader('Content-Length', file.size);
59+
res.setHeader('Content-Disposition', `filename="${file.name}"`);
60+
61+
const isCompressedAndPlainText =
62+
chunk?.upload_options?.encryption?.algorithm === undefined &&
63+
chunk?.upload_options?.compression?.algorithm !== undefined;
64+
if (isCompressedAndPlainText) {
65+
res.setHeader('Content-Encoding', 'deflate');
66+
}
67+
68+
const fileBuffer = await this.fileRetrieverService.getBuffer(cid);
69+
70+
res.send(fileBuffer);
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { Column, Entity } from 'typeorm';
3+
import { BaseEntity } from '../consensus/base.entity';
4+
import { UploadOptions } from './uploadOptions.valueObject';
5+
6+
@Entity('chunks', { schema: 'files' })
7+
export class Chunks extends BaseEntity {
8+
@ApiProperty()
9+
@Column('text')
10+
id: string;
11+
12+
@ApiProperty()
13+
@Column('enum', {
14+
enum: [
15+
'FileChunk',
16+
'File',
17+
'Folder',
18+
'FileInlink',
19+
'FolderInlink',
20+
'Metadata',
21+
'MetadataInlink',
22+
'MetadataChunk',
23+
],
24+
})
25+
type: string;
26+
27+
@ApiProperty()
28+
@Column('numeric')
29+
link_depth: number;
30+
31+
@ApiProperty()
32+
@Column('numeric')
33+
size: number;
34+
35+
@ApiProperty()
36+
@Column('varchar', { length: 255 })
37+
name: string;
38+
39+
@ApiProperty()
40+
@Column('text')
41+
data: string;
42+
43+
@ApiProperty()
44+
@Column('jsonb')
45+
upload_options: UploadOptions;
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { Column, Entity } from 'typeorm';
3+
import { BaseEntity } from '../consensus/base.entity';
4+
5+
@Entity('file_cids', { schema: 'files' })
6+
export class FileCids extends BaseEntity {
7+
@ApiProperty()
8+
@Column('varchar', { length: 161 })
9+
id: string;
10+
11+
@ApiProperty()
12+
@Column('varchar', { length: 161 })
13+
parent_cid: string;
14+
15+
@ApiProperty()
16+
@Column('varchar', { length: 161 })
17+
child_cid: string;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { lookup } from 'mime-types';
3+
import { AfterInsert, AfterLoad, AfterUpdate, Column, Entity } from 'typeorm';
4+
import { BaseEntity } from '../consensus/base.entity';
5+
6+
@Entity('files', { schema: 'files' })
7+
export class Files extends BaseEntity {
8+
@ApiProperty()
9+
@Column('varchar', { length: 80 })
10+
id: string;
11+
12+
@ApiProperty()
13+
@Column('numeric')
14+
size: number;
15+
16+
@ApiProperty()
17+
@Column('varchar', { length: 255, nullable: true })
18+
name: string;
19+
20+
@AfterLoad()
21+
@AfterInsert()
22+
@AfterUpdate()
23+
contentType(): string | undefined {
24+
return lookup(this.name) || undefined;
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export type UploadOptions = {
2+
compression?: CompressionOptions;
3+
encryption?: EncryptionOptions;
4+
};
5+
6+
export type CompressionOptions = {
7+
algorithm: CompressionAlgorithm;
8+
level?: number;
9+
chunkSize?: number;
10+
};
11+
12+
export type EncryptionOptions = {
13+
algorithm: EncryptionAlgorithm;
14+
chunkSize?: number;
15+
};
16+
17+
export enum EncryptionAlgorithm {
18+
AES_256_GCM = 'AES_256_GCM',
19+
}
20+
21+
export enum CompressionAlgorithm {
22+
ZLIB = 'ZLIB',
23+
}

indexers/api/src/entities/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export * from './consensus/transfers.entity';
2626
export * from './leaderboard/metadata.entity';
2727

2828
// Files Entities
29+
export * from './files/chunks.entity';
30+
export * from './files/file-cids.entity';
31+
export * from './files/files.entity';
2932
export * from './files/metadata.entity';
3033

3134
// Staking Entities
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { In, Repository } from 'typeorm';
4+
import { Chunks } from '../entities/files/chunks.entity';
5+
import { FileCids } from '../entities/files/file-cids.entity';
6+
7+
@Injectable()
8+
export class FileRetrieverService {
9+
constructor(
10+
@InjectRepository(FileCids)
11+
private fileCidsRepository: Repository<FileCids>,
12+
@InjectRepository(Chunks)
13+
private chunksRepository: Repository<Chunks>,
14+
) {}
15+
16+
async getBuffer(fileId: string): Promise<Buffer> {
17+
const children = await this.fileCidsRepository.find({
18+
where: {
19+
parent_cid: fileId,
20+
},
21+
});
22+
23+
const chunks = await this.chunksRepository.find({
24+
where: {
25+
id: In(children.map((child) => child.child_cid)),
26+
},
27+
});
28+
29+
return Buffer.concat(
30+
chunks.map((chunk) =>
31+
Buffer.from(
32+
Object.values(JSON.parse(chunk.data) as Record<string, number>),
33+
),
34+
),
35+
);
36+
}
37+
}

indexers/api/yarn.lock

+6-1
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,11 @@
10011001
resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547"
10021002
integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==
10031003

1004+
"@types/mime-types@^2.1.4":
1005+
version "2.1.4"
1006+
resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2"
1007+
integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==
1008+
10041009
"@types/mime@^1":
10051010
version "1.3.5"
10061011
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
@@ -3683,7 +3688,7 @@ [email protected]:
36833688
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
36843689
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
36853690

3686-
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34:
3691+
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34:
36873692
version "2.1.35"
36883693
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
36893694
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==

0 commit comments

Comments
 (0)