Skip to content

Commit

Permalink
chore: server download progress + S3 (janhq#1925)
Browse files Browse the repository at this point in the history
* fix: reduce the number of api call

Signed-off-by: James <[email protected]>

* fix: download progress

Signed-off-by: James <[email protected]>

* chore: save blob

* fix: server boot up

* fix: download state not updating

Signed-off-by: James <[email protected]>

* fix: copy assets

* Add Dockerfile CPU for Jan Server and Jan Web

* Add Dockerfile GPU for Jan Server and Jan Web

* feat: S3 adapter

* Update check find count from ./pre-install and correct copy:asserts command

* server add bundleDependencies @janhq/core

* server add bundleDependencies @janhq/core

* fix: update success/failed download state (janhq#1945)

* fix: update success/failed download state

Signed-off-by: James <[email protected]>

* fix: download model progress and state handling for both Desktop and Web

---------

Signed-off-by: James <[email protected]>
Co-authored-by: James <[email protected]>
Co-authored-by: Louis <[email protected]>

* chore: refactor

* fix: load models empty first time open

* Add Docker compose

* fix: assistants onUpdate

---------

Signed-off-by: James <[email protected]>
Co-authored-by: James <[email protected]>
Co-authored-by: Hien To <[email protected]>
Co-authored-by: NamH <[email protected]>
  • Loading branch information
4 people authored Feb 7, 2024
1 parent 1442479 commit 5890ade
Show file tree
Hide file tree
Showing 61 changed files with 956 additions and 517 deletions.
67 changes: 43 additions & 24 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,39 +1,58 @@
FROM node:20-bullseye AS base
FROM node:20-bookworm AS base

# 1. Install dependencies only when needed
FROM base AS deps
FROM base AS builder

# Install g++ 11
RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN yarn install
COPY . ./

# # 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# This will do the trick, use the corresponding env file for each environment.
RUN yarn workspace server install
RUN yarn server:prod
RUN export NITRO_VERSION=$(cat extensions/inference-nitro-extension/bin/version.txt) && \
jq --arg nitroVersion $NITRO_VERSION '(.scripts."downloadnitro:linux" | gsub("\\${NITRO_VERSION}"; $nitroVersion)) | gsub("\r"; "")' extensions/inference-nitro-extension/package.json > /tmp/newcommand.txt && export NEW_COMMAND=$(sed 's/^"//;s/"$//' /tmp/newcommand.txt) && jq --arg newCommand "$NEW_COMMAND" '.scripts."downloadnitro:linux" = $newCommand' extensions/inference-nitro-extension/package.json > /tmp/package.json && mv /tmp/package.json extensions/inference-nitro-extension/package.json
RUN make install-and-build
RUN yarn workspace jan-web install

RUN export NODE_ENV=production && yarn workspace jan-web build

# 3. Production image, copy all the files and run next
# # 2. Rebuild the source code only when needed
FROM base AS runner

# Install g++ 11
RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel && rm -rf /var/lib/apt/lists/*

WORKDIR /app

ENV NODE_ENV=production
# Copy the package.json and yarn.lock of root yarn space to leverage Docker cache
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/node_modules ./node_modules/
COPY --from=builder /app/yarn.lock ./yarn.lock

# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
COPY --from=builder /app/server ./server/
COPY --from=builder /app/docs/openapi ./docs/openapi/

# Copy pre-install dependencies
COPY --from=builder /app/pre-install ./pre-install/

# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache
COPY --from=builder /app/web/out ./web/out/
COPY --from=builder /app/web/.next ./web/.next/
COPY --from=builder /app/web/package.json ./web/package.json
COPY --from=builder /app/web/yarn.lock ./web/yarn.lock
COPY --from=builder /app/models ./models/

# RUN addgroup -g 1001 -S nodejs;
COPY --from=builder /app/server/build ./
RUN npm install -g serve@latest

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder /app/server/node_modules ./node_modules
COPY --from=builder /app/server/package.json ./package.json
EXPOSE 1337 3000 3928

EXPOSE 4000 3928
ENV JAN_API_HOST 0.0.0.0
ENV JAN_API_PORT 1337

ENV PORT 4000
ENV APPDATA /app/data
CMD ["sh", "-c", "cd server && node build/main.js & cd web && npx serve out"]

CMD ["node", "main.js"]
# docker build -t jan .
# docker run -p 1337:1337 -p 3000:3000 -p 3928:3928 jan
65 changes: 65 additions & 0 deletions Dockerfile.gpu
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
FROM nvidia/cuda:12.0.0-devel-ubuntu22.04 AS base

# 1. Install dependencies only when needed
FROM base AS builder

# Install g++ 11
RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel curl gnupg && curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt install nodejs -y && rm -rf /var/lib/apt/lists/*

RUN npm install -g yarn

WORKDIR /app

# Install dependencies based on the preferred package manager
COPY . ./

RUN export NITRO_VERSION=$(cat extensions/inference-nitro-extension/bin/version.txt) && \
jq --arg nitroVersion $NITRO_VERSION '(.scripts."downloadnitro:linux" | gsub("\\${NITRO_VERSION}"; $nitroVersion)) | gsub("\r"; "")' extensions/inference-nitro-extension/package.json > /tmp/newcommand.txt && export NEW_COMMAND=$(sed 's/^"//;s/"$//' /tmp/newcommand.txt) && jq --arg newCommand "$NEW_COMMAND" '.scripts."downloadnitro:linux" = $newCommand' extensions/inference-nitro-extension/package.json > /tmp/package.json && mv /tmp/package.json extensions/inference-nitro-extension/package.json
RUN make install-and-build
RUN yarn workspace jan-web install

RUN export NODE_ENV=production && yarn workspace jan-web build

# # 2. Rebuild the source code only when needed
FROM base AS runner

# Install g++ 11
RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel curl gnupg && curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt-get install nodejs -y && rm -rf /var/lib/apt/lists/*

RUN npm install -g yarn

WORKDIR /app

# Copy the package.json and yarn.lock of root yarn space to leverage Docker cache
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/node_modules ./node_modules/
COPY --from=builder /app/yarn.lock ./yarn.lock

# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
COPY --from=builder /app/server ./server/
COPY --from=builder /app/docs/openapi ./docs/openapi/

# Copy pre-install dependencies
COPY --from=builder /app/pre-install ./pre-install/

# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache
COPY --from=builder /app/web/out ./web/out/
COPY --from=builder /app/web/.next ./web/.next/
COPY --from=builder /app/web/package.json ./web/package.json
COPY --from=builder /app/web/yarn.lock ./web/yarn.lock
COPY --from=builder /app/models ./models/

RUN npm install -g serve@latest

EXPOSE 1337 3000 3928

ENV LD_LIBRARY_PATH=/usr/local/cuda-12.0/targets/x86_64-linux/lib:/usr/local/cuda-12.0/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

ENV JAN_API_HOST 0.0.0.0
ENV JAN_API_PORT 1337

CMD ["sh", "-c", "cd server && node build/main.js & cd web && npx serve out"]

# pre-requisites: nvidia-docker
# docker build -t jan-gpu . -f Dockerfile.gpu
# docker run -p 1337:1337 -p 3000:3000 -p 3928:3928 --gpus all jan-gpu
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ endif

check-file-counts: install-and-build
ifeq ($(OS),Windows_NT)
powershell -Command "if ((Get-ChildItem -Path electron/pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in electron/pre-install does not match the number of subdirectories in extension'; exit 1 } else { Write-Host 'Extension build successful' }"
powershell -Command "if ((Get-ChildItem -Path pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in pre-install does not match the number of subdirectories in extension'; exit 1 } else { Write-Host 'Extension build successful' }"
else
@tgz_count=$$(find electron/pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in electron/pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi
@tgz_count=$$(find pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi
endif

dev: check-file-counts
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,31 @@ make build

This will build the app MacOS m1/m2 for production (with code signing already done) and put the result in `dist` folder.

### Docker mode

- Supported OS: Linux, WSL2 Docker
- Pre-requisites:
- `docker` and `docker compose`, follow instruction [here](https://docs.docker.com/engine/install/ubuntu/)

```bash
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh --dry-run
```

- `nvidia docker`, follow instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) (If you want to run with GPU mode)

- Run Jan in Docker mode

```bash
# CPU mode
docker compose --profile cpu up
# GPU mode
docker compose --profile gpu up
```

This will start the web server and you can access Jan at `http://localhost:3000`.

## Acknowledgements

Jan builds on top of other open-source projects:
Expand Down
3 changes: 2 additions & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"rollup-plugin-typescript2": "^0.36.0",
"ts-jest": "^26.1.1",
"tslib": "^2.6.2",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"rimraf": "^3.0.2"
}
}
1 change: 1 addition & 0 deletions core/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum DownloadRoute {
downloadFile = 'downloadFile',
pauseDownload = 'pauseDownload',
resumeDownload = 'resumeDownload',
getDownloadProgress = 'getDownloadProgress',
}

export enum DownloadEvent {
Expand Down
64 changes: 59 additions & 5 deletions core/src/node/api/routes/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,27 @@ import { HttpServer } from '../HttpServer'
import { createWriteStream } from 'fs'
import { getJanDataFolderPath } from '../../utils'
import { normalizeFilePath } from '../../path'
import { DownloadState } from '../../../types'

export const downloadRouter = async (app: HttpServer) => {
app.get(`/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => {
const modelId = req.params.modelId

console.debug(`Getting download progress for model ${modelId}`)
console.debug(
`All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}`
)

// check if null DownloadManager.instance.downloadProgressMap
if (!DownloadManager.instance.downloadProgressMap[modelId]) {
return res.status(404).send({
message: 'Download progress not found',
})
} else {
return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId])
}
})

app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
const strictSSL = !(req.query.ignoreSSL === 'true')
const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined
Expand All @@ -19,25 +38,55 @@ export const downloadRouter = async (app: HttpServer) => {
})

const localPath = normalizedArgs[1]
const fileName = localPath.split('/').pop() ?? ''
const array = localPath.split('/')
const fileName = array.pop() ?? ''
const modelId = array.pop() ?? ''
console.debug('downloadFile', normalizedArgs, fileName, modelId)

const request = require('request')
const progress = require('request-progress')

const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
progress(rq, {})
.on('progress', function (state: any) {
console.log('download onProgress', state)
const downloadProps: DownloadState = {
...state,
modelId,
fileName,
downloadState: 'downloading',
}
console.debug(`Download ${modelId} onProgress`, downloadProps)
DownloadManager.instance.downloadProgressMap[modelId] = downloadProps
})
.on('error', function (err: Error) {
console.log('download onError', err)
console.debug(`Download ${modelId} onError`, err.message)

const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
if (currentDownloadState) {
DownloadManager.instance.downloadProgressMap[modelId] = {
...currentDownloadState,
downloadState: 'error',
}
}
})
.on('end', function () {
console.log('download onEnd')
console.debug(`Download ${modelId} onEnd`)

const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
if (currentDownloadState) {
if (currentDownloadState.downloadState === 'downloading') {
// if the previous state is downloading, then set the state to end (success)
DownloadManager.instance.downloadProgressMap[modelId] = {
...currentDownloadState,
downloadState: 'end',
}
}
}
})
.pipe(createWriteStream(normalizedArgs[1]))

DownloadManager.instance.setRequest(fileName, rq)
DownloadManager.instance.setRequest(localPath, rq)
res.status(200).send({ message: 'Download started' })
})

app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
Expand All @@ -54,5 +103,10 @@ export const downloadRouter = async (app: HttpServer) => {
const rq = DownloadManager.instance.networkRequests[fileName]
DownloadManager.instance.networkRequests[fileName] = undefined
rq?.abort()
if (rq) {
res.status(200).send({ message: 'Download aborted' })
} else {
res.status(404).send({ message: 'Download not found' })
}
})
}
27 changes: 21 additions & 6 deletions core/src/node/api/routes/fileManager.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import { FileManagerRoute } from '../../../api'
import { HttpServer } from '../../index'
import { join } from 'path'

export const fsRouter = async (app: HttpServer) => {
app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {})
export const fileManagerRouter = async (app: HttpServer) => {
app.post(`/fs/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {
const reflect = require('@alumna/reflect')
const args = JSON.parse(request.body)
return reflect({
src: args[0],
dest: args[1],
recursive: true,
delete: false,
overwrite: true,
errorOnExist: false,
})
})

app.post(`/app/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) => {})
app.post(`/fs/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) =>
global.core.appPath()
)

app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {})
app.post(`/fs/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) =>
join(global.core.appPath(), '../../..')
)

app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {})

app.post(`/app/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
app.post(`/fs/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
}
13 changes: 12 additions & 1 deletion core/src/node/api/routes/fs.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FileSystemRoute } from '../../../api'
import { FileManagerRoute, FileSystemRoute } from '../../../api'
import { join } from 'path'
import { HttpServer } from '../HttpServer'
import { getJanDataFolderPath } from '../../utils'
import { normalizeFilePath } from '../../path'
import { writeFileSync } from 'fs'

export const fsRouter = async (app: HttpServer) => {
const moduleName = 'fs'
Expand All @@ -26,4 +27,14 @@ export const fsRouter = async (app: HttpServer) => {
}
})
})
app.post(`/${FileManagerRoute.writeBlob}`, async (request: any, reply: any) => {
try {
const args = JSON.parse(request.body) as any[]
console.log('writeBlob:', args[0])
const dataBuffer = Buffer.from(args[1], 'base64')
writeFileSync(args[0], dataBuffer)
} catch (err) {
console.error(`writeFile ${request.body} result: ${err}`)
}
})
}
3 changes: 3 additions & 0 deletions core/src/node/api/routes/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { threadRouter } from './thread'
import { fsRouter } from './fs'
import { extensionRouter } from './extension'
import { downloadRouter } from './download'
import { fileManagerRouter } from './fileManager'

export const v1Router = async (app: HttpServer) => {
// MARK: External Routes
Expand All @@ -16,6 +17,8 @@ export const v1Router = async (app: HttpServer) => {
app.register(fsRouter, {
prefix: '/fs',
})
app.register(fileManagerRouter)

app.register(extensionRouter, {
prefix: '/extension',
})
Expand Down
Loading

0 comments on commit 5890ade

Please sign in to comment.