Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ jobs:
context: .
file: Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
build-args: |
PACKAGE_VERSION=${{ env.PACKAGE_VERSION }}
tags: |
ghcr.io/${{ github.repository }}:${{ env.PACKAGE_VERSION }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
FROM node:lts-alpine

# 安装构建 better-sqlite3 所需的依赖
RUN apk add --no-cache python3 make g++

COPY app /notesx/app
COPY db /notesx/db
COPY userfiles /notesx/userfiles
Expand Down
49 changes: 49 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 "清理完成!"

4 changes: 2 additions & 2 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 27 additions & 1 deletion app/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -136,4 +137,29 @@ process.on('SIGTERM', () => {

new Cron(appInstance)

serve(app)
const port = parseInt(process.env.PORT || '3000', 10)
const serverTimeout = parseInt(process.env.SERVER_TIMEOUT || '600000', 10) // Default 10 minutes

// Configure underlying Node server timeout via Hono node server Options
const serverOptions: HonoNodeServerOptions = {
fetch: app.fetch,
port,
serverOptions: {
// keep-alive idle connection timeout, corresponds to Keep-Alive: timeout=... header
keepAliveTimeout: serverTimeout
}
}

const server = serve(serverOptions)

// Manually set timeout options after creating server (compatible with different Node.js versions and type definitions)
// These options are available in Node.js 18+ but type definitions may be incomplete
if ('timeout' in server && typeof (server as any).timeout === 'number') {
(server as any).timeout = serverTimeout
}
if ('requestTimeout' in server && typeof (server as any).requestTimeout === 'number') {
(server as any).requestTimeout = serverTimeout
}
if ('headersTimeout' in server && typeof (server as any).headersTimeout === 'number') {
(server as any).headersTimeout = serverTimeout
}
169 changes: 137 additions & 32 deletions app/src/v1/File.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,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
Expand Down Expand Up @@ -206,7 +241,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)

Expand Down Expand Up @@ -393,37 +454,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<CheckFileResult> {
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
Expand All @@ -440,20 +539,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
}
}

Expand Down
11 changes: 9 additions & 2 deletions app/src/v1/WebNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,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 => `<link rel='stylesheet' href='${cssUrl}'>`).join('\n ')
this.replace(this.placeholders.css, cssLinks)
} else {
// Single CSS file: maintain backward compatibility
this.replace(this.placeholders.css, `<link rel='stylesheet' href='${url}'>`)
}
}

setWidth (width: any) {
Expand Down
4 changes: 3 additions & 1 deletion app/src/v1/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ export async function sha256 (data: string | ArrayBuffer) {
}

export async function sha1 (data: string | Buffer) {
return sha('SHA-1', data)
// Convert Buffer to ArrayBuffer
const arrayBuffer = typeof data === 'string' ? data : new Uint8Array(data).buffer
return sha('SHA-1', arrayBuffer)
}

export async function shortHash (text: string) {
Expand Down
2 changes: 1 addition & 1 deletion app/src/v1/templates/note.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<style>
TEMPLATE_WIDTH
</style>
<link rel='stylesheet' href='TEMPLATE_CSS'>
TEMPLATE_CSS
<link rel='stylesheet' href='TEMPLATE_ASSETS_WEBROOT/global-note-styles.css'>
<script src='TEMPLATE_ASSETS_WEBROOT/app.js'></script>
TEMPLATE_SCRIPTS
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
notesx-server:
image: ghcr.io/note-sx/server:latest
image: ghcr.io/kinboyw/share-note-server:latest
container_name: notesx-server
restart: always
ports:
Expand Down
2 changes: 2 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tools]
node = "v22"