From d62ebb5d0fa9c548064cfd74fc69e75f26e2e655 Mon Sep 17 00:00:00 2001 From: kinboy Date: Wed, 17 Dec 2025 17:31:14 +0800 Subject: [PATCH 1/4] fix: improve Docker build and HTTP server timeout configuration This commit consolidates fixes for Docker build issues and HTTP server timeout problems when serving large files. Docker build improvements: - Fix GitHub CI Actions platform compatibility issue with node:lts-alpine - Remove --build-from-source option from Dockerfile (not needed) - Add build dependencies (python3, make, g++) for better-sqlite3 compilation - Update CI workflow to only build for linux/amd64 and linux/arm64 platforms HTTP server timeout fixes: - Increase default server timeout to 10 minutes (600000ms) to handle large CSS files - Configure keepAliveTimeout, requestTimeout, and headersTimeout - Fix Buffer to ArrayBuffer conversion in sha1() helper function - Update docker-compose.yml image version These changes resolve timeout issues when serving large CSS files (>8MB) and improve Docker build reliability across different platforms. fix: timeout issues fix: github ci actions node:lts-alpine platform issue fix: remove --build-from-source options --- .github/workflows/ci.yaml | 2 ++ Dockerfile | 3 ++- app/src/index.ts | 17 ++++++++++++++++- app/src/v1/helpers.ts | 4 +++- docker-compose.yml | 2 +- mise.toml | 2 ++ 6 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 mise.toml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b1a001f..b8bad7f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,6 +51,8 @@ jobs: tags: | ghcr.io/${{ github.repository }}:${{ env.PACKAGE_VERSION }} ghcr.io/${{ github.repository }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max - name: Attest build provenance uses: actions/attest-build-provenance@v4 diff --git a/Dockerfile b/Dockerfile index 73304ec..a37ab47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,8 @@ FROM node:lts-alpine # Fonts for server-side SVG-to-PNG rendering of the stats OG image. # Without these, resvg silently drops all text from the rendered card. -RUN apk add --no-cache font-dejavu +# python3/make/g++ are required to compile better-sqlite3 on alpine. +RUN apk add --no-cache font-dejavu python3 make g++ COPY app /notesx/app COPY db /notesx/db diff --git a/app/src/index.ts b/app/src/index.ts index edf7b57..bab78d2 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -1,5 +1,6 @@ import { Hono } from 'hono' import { serve } from '@hono/node-server' +import type { Options as HonoNodeServerOptions } from '@hono/node-server/dist/types' import { serveStatic } from '@hono/node-server/serve-static' import { cors } from 'hono/cors' import { etag } from 'hono/etag' @@ -164,6 +165,20 @@ process.on('SIGTERM', () => { new Cron(appInstance) -serve(app, (info) => { +const port = parseInt(process.env.PORT || '3000', 10) +const serverTimeout = parseInt(process.env.SERVER_TIMEOUT || '600000', 10) // default 10 minutes for large CSS + +// Configure Node HTTP server timeouts via Hono node-server options. +const serverOptions: HonoNodeServerOptions = { + fetch: app.fetch, + port, + serverOptions: { + requestTimeout: serverTimeout, + headersTimeout: serverTimeout, + keepAliveTimeout: serverTimeout + } +} + +const server = serve(serverOptions, (info) => { console.log(`Listening on http://localhost:${info.port}`) }) diff --git a/app/src/v1/helpers.ts b/app/src/v1/helpers.ts index 242ea1d..1f9c86b 100644 --- a/app/src/v1/helpers.ts +++ b/app/src/v1/helpers.ts @@ -68,7 +68,9 @@ export async function sha256 (data: string | ArrayBuffer) { } export async function sha1 (data: string | Buffer) { - return sha('SHA-1', data) + // 将 Buffer 转换为 ArrayBuffer + const arrayBuffer = typeof data === 'string' ? data : new Uint8Array(data).buffer + return sha('SHA-1', arrayBuffer) } export async function shortHash (text: string) { diff --git a/docker-compose.yml b/docker-compose.yml index 1fdc2be..3068b83 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: notesx-server: - image: ghcr.io/note-sx/server:latest + image: ghcr.io/note-sx/server:1.0.0 container_name: notesx-server restart: always ports: diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..df7e1bb --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +node = "v22" From 6b63a9acc5e0e14a48cd62390f619c347dc954d6 Mon Sep 17 00:00:00 2001 From: kinboy Date: Wed, 17 Dec 2025 23:12:26 +0800 Subject: [PATCH 2/4] feat: support multiple CSS file chunks for large CSS files Add support for splitting large CSS files into multiple chunks to avoid timeout issues when serving large CSS files (>8MB). Key changes: - Support CSS file array format: template.css?: Array<{ url: string, hash: string }> - CSS file upload supports multiple chunks with unique filenames (user UID + hash prefix) - checkCss() and checkFiles() return CSS files as arrays instead of single objects - checkFile() supports finding CSS chunks by hash and user UID prefix - WebNote.setCss() supports both array and string formats for backward compatibility - HTML template updated to allow multiple CSS link tags via placeholder Technical implementation: - CSS chunks stored with filenames: {userUID}{hashPrefix8chars} - Multiple CSS files queried by filename prefix matching (LIKE pattern) - Backward compatible: falls back to single CSS file logic if no array provided - CSS file lookup optimized for chunked files using hash + filename prefix Files modified: - app/src/v1/File.ts: CSS handling for upload, check, and note creation - app/src/v1/WebNote.ts: setCss() method to support arrays - app/src/v1/templates/note.html: removed hardcoded link tag, use placeholder - app/src/index.ts: HTTP server timeout configuration - app/src/v1/helpers.ts: Buffer to ArrayBuffer conversion fix --- Makefile | 49 ++++++++++ app/src/v1/File.ts | 171 ++++++++++++++++++++++++++------- app/src/v1/WebNote.ts | 11 ++- app/src/v1/helpers.ts | 2 +- app/src/v1/templates/note.html | 2 +- 5 files changed, 197 insertions(+), 38 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..41a4027 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +.PHONY: build-app docker dev clean help + +# 检测包管理器 (优先使用 pnpm,否则使用 npm) +NPM := $(shell command -v pnpm >/dev/null 2>&1 && echo pnpm || echo npm) + +# 获取版本号 +VERSION := $(shell cd app && $(NPM) pkg get version 2>/dev/null | xargs || echo "1.0.0") +IMAGE_NAME := ghcr.io/kinboyw/share-note-server +DOCKER_IMAGE := $(IMAGE_NAME):$(VERSION) + +help: ## 显示帮助信息 + @echo "可用的命令:" + @echo " make build-app - 构建应用 (编译 TypeScript)" + @echo " make docker - 构建 Docker 镜像 (依赖 build-app)" + @echo " make dev - 启动开发环境 (依赖 docker)" + @echo " make clean - 清理构建产物" + @echo "" + @echo "当前版本: $(VERSION)" + @echo "Docker 镜像: $(DOCKER_IMAGE)" + +build-app: ## 构建应用 + @echo "构建应用 (使用 $(NPM))..." + cd app && $(NPM) run build + @echo "构建完成!" + +docker: build-app ## 构建 Docker 镜像 + @echo "构建 Docker 镜像: $(DOCKER_IMAGE)" + docker build \ + --build-arg PACKAGE_VERSION=$(VERSION) \ + -t $(DOCKER_IMAGE) \ + -t $(IMAGE_NAME):latest \ + . + @echo "Docker 镜像构建完成: $(DOCKER_IMAGE)" + +dev: docker ## 启动开发环境 + @echo "启动开发环境..." + @if docker compose version >/dev/null 2>&1; then \ + docker compose up -d; \ + else \ + docker-compose up -d; \ + fi + @echo "开发环境已启动!" + @echo "服务运行在 http://localhost:3000" + +clean: ## 清理构建产物 + @echo "清理构建产物..." + rm -rf app/dist + @echo "清理完成!" + diff --git a/app/src/v1/File.ts b/app/src/v1/File.ts index 2f629ba..19a8d5e 100644 --- a/app/src/v1/File.ts +++ b/app/src/v1/File.ts @@ -162,13 +162,48 @@ export default class File extends Controller { } } else if (this.extension === 'css') { /* - * CSS is special, as it uses the user's UID for the filename + * CSS files support multiple chunks + * 1. If hash is provided, check if a CSS file with the same hash already exists (avoid duplicate uploads) + * 2. If exists, use that filename + * 3. If not, generate a new filename (user UID + first 8 chars of hash as suffix) */ - this.filename = await this.getCssFilename() - await this.file.load({ - filetype: this.extension, - filename: this.filename - }) + const cssFilename = await this.getCssFilename() + + if (this.hash) { + // Check if a CSS file with the same hash already exists + const existingCss = this.app.db.prepare(` + SELECT filename, hash + FROM files + WHERE filetype = 'css' + AND filename LIKE ? + AND hash = ? + LIMIT 1 + `).get(cssFilename + '%', this.hash) as { filename: string; hash: string } | undefined + + if (existingCss) { + // CSS file with same hash exists, use it + this.filename = existingCss.filename + await this.file.load({ + filetype: this.extension, + filename: this.filename + }) + } else { + // Not found, generate new filename: user UID + first 8 chars of hash as suffix + const hashSuffix = this.hash.substring(0, 8) + this.filename = cssFilename + hashSuffix + await this.file.load({ + filetype: this.extension, + filename: this.filename + }) + } + } else { + // No hash provided, use legacy logic (single CSS file) + this.filename = cssFilename + await this.file.load({ + filetype: this.extension, + filename: this.filename + }) + } } else { /* * Other file-types, not HTML @@ -205,7 +240,33 @@ export default class File extends Controller { const template = this.post.template // Make replacements - note.setCss(this.getDisplayUrl(await this.getCssFilename(), 'css')) + // Handle CSS files: support array format css?: Array<{ url: string, hash: string }> + if (template.css && Array.isArray(template.css) && template.css.length > 0) { + const cssUrls = template.css + .map((cssItem: { url: string; hash: string }) => { + if (!cssItem?.url) return null + + const cssUrl = cssItem.url + if (cssUrl.startsWith('http://') || cssUrl.startsWith('https://')) { + return cssUrl + } else if (cssUrl.startsWith('/')) { + return this.app.baseWebUrl + cssUrl + } else { + return this.app.baseWebUrl + '/' + cssUrl + } + }) + .filter((url: string | null): url is string => url !== null) + + if (cssUrls.length > 0) { + note.setCss(cssUrls) + } else { + // Array is empty or invalid, fallback to legacy single CSS file logic + note.setCss(this.getDisplayUrl(await this.getCssFilename(), 'css')) + } + } else { + // No CSS array provided, use legacy single CSS file logic + note.setCss(this.getDisplayUrl(await this.getCssFilename(), 'css')) + } note.setWidth(template.width) note.enableMathjax(!!template.mathJax) @@ -402,37 +463,75 @@ export default class File extends Controller { return this.cssFilename } + /** + * Check all CSS files for the user + * Returns array format, each element contains url and hash + */ async checkCss () { - const file = await Mapper(this.app.db, 'files') - await file.load({ - filename: await this.getCssFilename(), - filetype: 'css' - }) + // Query all CSS files for this user (matched by filename prefix) + const cssFilename = await this.getCssFilename() + const cssFiles = this.app.db.prepare(` + SELECT filename, hash, filetype + FROM files + WHERE filetype = 'css' + AND filename LIKE ? + ORDER BY filename + `).all(cssFilename + '%') as Array<{ filename: string; hash: string; filetype: string }> + + // Convert to array format, each element contains url and hash + const cssArray = cssFiles.map(cssFile => ({ + url: this.getDisplayUrl(cssFile.filename, cssFile.filetype), + hash: cssFile.hash + })) + return { - success: !!file?.found + success: cssArray.length > 0, + css: cssArray.length > 0 ? cssArray : [] } } /** * Check to see if a file matching this exact contents is already uploaded on the server + * For CSS files, supports checking multiple chunk files by hash */ async checkFile (item?: CheckFileItem): Promise { const params: { [key: string]: string } = { filetype: item?.filetype || this.post.filetype, hash: item?.hash || this.post.hash } + if (params.filetype === 'css') { - // CSS files also need to match on salted UID - params.filename = await this.getCssFilename() - } - const file = await Mapper(this.app.db, 'files') - if (params.filetype && params.hash) { - await file.load(params) - if (file.found) { - const url = this.getDisplayUrl(file.row.filename, file.row.filetype) - return this.returnSuccessUrl(url) + // CSS files: match by hash and user UID (filename prefix) + // Since there may be multiple CSS chunks now, need to find the specific file by hash + const cssFilename = await this.getCssFilename() + if (params.hash) { + // Query if there's a matching hash in the user's CSS files + const cssFile = this.app.db.prepare(` + SELECT filename, hash, filetype + FROM files + WHERE filetype = 'css' + AND filename LIKE ? + AND hash = ? + LIMIT 1 + `).get(cssFilename + '%', params.hash) as { filename: string; hash: string; filetype: string } | undefined + + if (cssFile) { + const url = this.getDisplayUrl(cssFile.filename, cssFile.filetype) + return this.returnSuccessUrl(url) + } + } + } else { + // Non-CSS files: use original logic + const file = await Mapper(this.app.db, 'files') + if (params.filetype && params.hash) { + await file.load(params) + if (file.found) { + const url = this.getDisplayUrl(file.row.filename, file.row.filetype) + return this.returnSuccessUrl(url) + } } } + return { success: false, url: null @@ -449,22 +548,26 @@ export default class File extends Controller { result.push(file) } - // Get the info on the user's CSS (if exists) - const css = await Mapper(this.app.db, 'files') - await css.load({ - filename: await this.getCssFilename(), - filetype: 'css' - }) + // Get the info on the user's CSS files (returns array format) + const cssFilename = await this.getCssFilename() + const cssFiles = this.app.db.prepare(` + SELECT filename, hash, filetype + FROM files + WHERE filetype = 'css' + AND filename LIKE ? + ORDER BY filename + `).all(cssFilename + '%') as Array<{ filename: string; hash: string; filetype: string }> + + // Convert to array format, each element contains url and hash + const cssArray = cssFiles.map(cssFile => ({ + url: this.getDisplayUrl(cssFile.filename, cssFile.filetype), + hash: cssFile.hash + })) return { success: true, files: result, - css: css.notFound - ? null - : { - url: this.getDisplayUrl(await this.getCssFilename(), 'css'), - hash: css.row.hash - } + css: cssArray } } diff --git a/app/src/v1/WebNote.ts b/app/src/v1/WebNote.ts index 3c8bc6e..ac36cb2 100644 --- a/app/src/v1/WebNote.ts +++ b/app/src/v1/WebNote.ts @@ -65,8 +65,15 @@ export default class WebNote { }[tag] || '')) } - setCss (url: string) { - this.replace(this.placeholders.css, url) + setCss (url: string | string[]) { + if (Array.isArray(url)) { + // Multiple CSS files: generate multiple link tags + const cssLinks = url.map(cssUrl => ``).join('\n ') + this.replace(this.placeholders.css, cssLinks) + } else { + // Single CSS file: maintain backward compatibility + this.replace(this.placeholders.css, ``) + } } setWidth (width: any) { diff --git a/app/src/v1/helpers.ts b/app/src/v1/helpers.ts index 1f9c86b..3bf19c4 100644 --- a/app/src/v1/helpers.ts +++ b/app/src/v1/helpers.ts @@ -68,7 +68,7 @@ export async function sha256 (data: string | ArrayBuffer) { } export async function sha1 (data: string | Buffer) { - // 将 Buffer 转换为 ArrayBuffer + // Convert Buffer to ArrayBuffer const arrayBuffer = typeof data === 'string' ? data : new Uint8Array(data).buffer return sha('SHA-1', arrayBuffer) } diff --git a/app/src/v1/templates/note.html b/app/src/v1/templates/note.html index 56d6a09..27d92e6 100644 --- a/app/src/v1/templates/note.html +++ b/app/src/v1/templates/note.html @@ -10,7 +10,7 @@ - + TEMPLATE_CSS TEMPLATE_SCRIPTS From b9acb7960252f6efbbcbe248648a8818cd806ea4 Mon Sep 17 00:00:00 2001 From: kinboy Date: Thu, 18 Dec 2025 09:55:06 +0800 Subject: [PATCH 3/4] fix: docker-compose image update --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3068b83..95e87f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: notesx-server: - image: ghcr.io/note-sx/server:1.0.0 + image: ghcr.io/kinboyw/share-note-server:latest container_name: notesx-server restart: always ports: From 74982f8f591fa85d37084d0877722b41c14166ac Mon Sep 17 00:00:00 2001 From: kinboy Date: Fri, 24 Jul 2026 21:08:16 +0800 Subject: [PATCH 4/4] chore: release v1.5.0 Bump server version for multi-CSS chunk support, larger upload timeouts, and Docker build fixes. --- app/package-lock.json | 4 ++-- app/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 076d345..12420ee 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "notesx-api", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "notesx-api", - "version": "1.4.0", + "version": "1.5.0", "dependencies": { "@hono/node-server": "^2.0.4", "@resvg/resvg-js": "^2.6.2", diff --git a/app/package.json b/app/package.json index 39f1237..0dba19a 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "notesx-api", - "version": "1.4.0", + "version": "1.5.0", "scripts": { "dev": "ts-node src/index.ts", "build": "npx tsc",