Skip to content
This repository was archived by the owner on Jul 4, 2025. It is now read-only.

Commit af3b3b2

Browse files
committed
update
Signed-off-by: James <namnh0122@gmail.com>
1 parent 0f24986 commit af3b3b2

File tree

9 files changed

+287
-81
lines changed

9 files changed

+287
-81
lines changed

cortex-js/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { FileManagerModule } from './file-manager/file-manager.module';
1616
import { AppLoggerMiddleware } from './infrastructure/middlewares/app.logger.middleware';
1717
import { EventEmitterModule } from '@nestjs/event-emitter';
1818
import { AppController } from './infrastructure/controllers/app.controller';
19+
import { DownloadManagerModule } from './download-manager/download-manager.module';
1920

2021
@Module({
2122
imports: [
@@ -37,6 +38,7 @@ import { AppController } from './infrastructure/controllers/app.controller';
3738
ExtensionModule,
3839
FileManagerModule,
3940
ModelRepositoryModule,
41+
DownloadManagerModule,
4042
],
4143
controllers: [AppController],
4244
providers: [SeedService],

cortex-js/src/command.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { KillCommand } from './infrastructure/commanders/kill.command';
2929
import { PresetCommand } from './infrastructure/commanders/presets.command';
3030
import { EmbeddingCommand } from './infrastructure/commanders/embeddings.command';
3131
import { EventEmitterModule } from '@nestjs/event-emitter';
32+
import { DownloadManagerModule } from './download-manager/download-manager.module';
3233

3334
@Module({
3435
imports: [
@@ -47,6 +48,7 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
4748
AssistantsModule,
4849
MessagesModule,
4950
FileManagerModule,
51+
DownloadManagerModule,
5052
],
5153
providers: [
5254
CortexCommand,

cortex-js/src/domain/models/download.interface.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export class DownloadState {
22
/**
3-
* The id of a particular download. This will be an uuid.
3+
* The id of a particular download. Being used to prevent duplication of downloads.
44
*/
55
id: string;
66

@@ -12,7 +12,7 @@ export class DownloadState {
1212
/**
1313
* The type of download.
1414
*/
15-
type: 'model' | 'miscelanous';
15+
type: DownloadType;
1616

1717
/**
1818
* The status of the download.
@@ -65,3 +65,8 @@ export class DownloadItem {
6565
export interface DownloadStateEvent {
6666
data: DownloadState[];
6767
}
68+
69+
export enum DownloadType {
70+
Model = 'model',
71+
Miscelanous = 'miscelanous',
72+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from '@nestjs/common';
2+
import { DownloadManagerService } from './download-manager.service';
3+
import { HttpModule } from '@nestjs/axios';
4+
5+
@Module({
6+
imports: [HttpModule],
7+
providers: [DownloadManagerService],
8+
exports: [DownloadManagerService],
9+
})
10+
export class DownloadManagerModule {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { DownloadManagerService } from './download-manager.service';
3+
4+
describe('DownloadManagerService', () => {
5+
let service: DownloadManagerService;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
providers: [DownloadManagerService],
10+
}).compile();
11+
12+
service = module.get<DownloadManagerService>(DownloadManagerService);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(service).toBeDefined();
17+
});
18+
});
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import {
2+
DownloadItem,
3+
DownloadState,
4+
DownloadStatus,
5+
DownloadType,
6+
} from '@/domain/models/download.interface';
7+
import { HttpService } from '@nestjs/axios';
8+
import { Injectable } from '@nestjs/common';
9+
import { EventEmitter2 } from '@nestjs/event-emitter';
10+
import { createWriteStream } from 'node:fs';
11+
import { firstValueFrom } from 'rxjs';
12+
13+
@Injectable()
14+
export class DownloadManagerService {
15+
private allDownloadStates: DownloadState[] = [];
16+
17+
constructor(
18+
private readonly httpService: HttpService,
19+
private readonly eventEmitter: EventEmitter2,
20+
) {
21+
// start emitting download state each 500ms
22+
setInterval(() => {
23+
this.eventEmitter.emit('download.event', this.allDownloadStates);
24+
}, 500);
25+
}
26+
27+
async submitDownloadRequest(
28+
downloadId: string,
29+
title: string,
30+
downloadType: DownloadType,
31+
urlToDestination: Record<string, string>,
32+
) {
33+
if (
34+
this.allDownloadStates.find(
35+
(downloadState) => downloadState.id === downloadId,
36+
)
37+
) {
38+
return;
39+
}
40+
41+
const downloadItems: DownloadItem[] = Object.keys(urlToDestination).map(
42+
(url) => {
43+
const destination = urlToDestination[url];
44+
const downloadItem: DownloadItem = {
45+
id: destination,
46+
time: {
47+
elapsed: 0,
48+
remaining: 0,
49+
},
50+
size: {
51+
total: 0,
52+
transferred: 0,
53+
},
54+
status: DownloadStatus.Downloading,
55+
};
56+
57+
return downloadItem;
58+
},
59+
);
60+
61+
const downloadState: DownloadState = {
62+
id: downloadId,
63+
title: title,
64+
type: downloadType,
65+
status: DownloadStatus.Downloading,
66+
children: downloadItems,
67+
};
68+
69+
this.allDownloadStates.push(downloadState);
70+
71+
Object.keys(urlToDestination).forEach((url) => {
72+
const destination = urlToDestination[url];
73+
this.downloadFile(downloadId, url, destination);
74+
});
75+
}
76+
77+
private async downloadFile(
78+
downloadId: string,
79+
url: string,
80+
destination: string,
81+
) {
82+
const response = await firstValueFrom(
83+
this.httpService.get(url, {
84+
responseType: 'stream',
85+
}),
86+
);
87+
// check if response is success
88+
if (!response) {
89+
throw new Error('Failed to download model');
90+
}
91+
92+
const writer = createWriteStream(destination);
93+
const totalBytes = response.headers['content-length'];
94+
95+
// update download state
96+
// TODO: separte this duplicate code to a function
97+
const currentDownloadState = this.allDownloadStates.find(
98+
(downloadState) => downloadState.id === downloadId,
99+
);
100+
if (!currentDownloadState) {
101+
return;
102+
}
103+
const downloadItem = currentDownloadState?.children.find(
104+
(downloadItem) => downloadItem.id === destination,
105+
);
106+
if (downloadItem) {
107+
downloadItem.size.total = totalBytes;
108+
}
109+
110+
let transferredBytes = 0;
111+
112+
writer.on('finish', () => {
113+
const currentDownloadState = this.allDownloadStates.find(
114+
(downloadState) => downloadState.id === downloadId,
115+
);
116+
if (!currentDownloadState) {
117+
return;
118+
}
119+
120+
// update current child status to downloaded, find by destination as id
121+
const downloadItem = currentDownloadState?.children.find(
122+
(downloadItem) => downloadItem.id === destination,
123+
);
124+
if (downloadItem) {
125+
downloadItem.status = DownloadStatus.Downloaded;
126+
}
127+
128+
const allChildrenDownloaded = currentDownloadState?.children.every(
129+
(downloadItem) => downloadItem.status === DownloadStatus.Downloaded,
130+
);
131+
132+
if (allChildrenDownloaded) {
133+
currentDownloadState.status = DownloadStatus.Downloaded;
134+
// TODO: notify if download success so that client can auto refresh
135+
// remove download state if all children is downloaded
136+
this.allDownloadStates = this.allDownloadStates.filter(
137+
(downloadState) => downloadState.id !== downloadId,
138+
);
139+
}
140+
});
141+
142+
writer.on('error', (error) => {
143+
const currentDownloadState = this.allDownloadStates.find(
144+
(downloadState) => downloadState.id === downloadId,
145+
);
146+
if (!currentDownloadState) {
147+
return;
148+
}
149+
150+
const downloadItem = currentDownloadState?.children.find(
151+
(downloadItem) => downloadItem.id === destination,
152+
);
153+
if (downloadItem) {
154+
downloadItem.status = DownloadStatus.Error;
155+
downloadItem.error = error.message;
156+
}
157+
158+
currentDownloadState.status = DownloadStatus.Error;
159+
currentDownloadState.error = error.message;
160+
161+
// TODO: notify if download error
162+
// remove download state if all children is downloaded
163+
this.allDownloadStates = this.allDownloadStates.filter(
164+
(downloadState) => downloadState.id !== downloadId,
165+
);
166+
});
167+
168+
response.data.on('data', (chunk: any) => {
169+
transferredBytes += chunk.length;
170+
171+
const currentDownloadState = this.allDownloadStates.find(
172+
(downloadState) => downloadState.id === downloadId,
173+
);
174+
if (!currentDownloadState) return;
175+
176+
const downloadItem = currentDownloadState?.children.find(
177+
(downloadItem) => downloadItem.id === destination,
178+
);
179+
if (downloadItem) {
180+
downloadItem.size.transferred = transferredBytes;
181+
}
182+
});
183+
184+
response.data.pipe(writer);
185+
}
186+
}

cortex-js/src/infrastructure/repositories/model/model.module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ import { HttpModule } from '@nestjs/axios';
44
import { ModelRepository } from '@/domain/repositories/model.interface';
55
import { ModelRepositoryImpl } from './model.repository';
66
import { FileManagerModule } from '@/file-manager/file-manager.module';
7+
import { DownloadManagerModule } from '@/download-manager/download-manager.module';
78

89
@Module({
9-
imports: [CortexProviderModule, HttpModule, FileManagerModule],
10+
imports: [
11+
CortexProviderModule,
12+
HttpModule,
13+
FileManagerModule,
14+
DownloadManagerModule,
15+
],
1016
providers: [
1117
{
1218
provide: ModelRepository,

cortex-js/src/usecases/models/models.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ExtensionModule } from '@/infrastructure/repositories/extensions/extens
77
import { HttpModule } from '@nestjs/axios';
88
import { FileManagerModule } from '@/file-manager/file-manager.module';
99
import { ModelRepositoryModule } from '@/infrastructure/repositories/model/model.module';
10+
import { DownloadManagerModule } from '@/download-manager/download-manager.module';
1011

1112
@Module({
1213
imports: [
@@ -16,6 +17,7 @@ import { ModelRepositoryModule } from '@/infrastructure/repositories/model/model
1617
HttpModule,
1718
FileManagerModule,
1819
ModelRepositoryModule,
20+
DownloadManagerModule,
1921
],
2022
controllers: [ModelsController],
2123
providers: [ModelsUsecases],

0 commit comments

Comments
 (0)