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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.

2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
17 changes: 16 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 @@ -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}`)
})
171 changes: 137 additions & 34 deletions app/src/v1/File.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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<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 @@ -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
}
}

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 @@ -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 => `<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 @@ -68,7 +68,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"