diff --git a/Dockerfile b/Dockerfile index 95af1b4..77f5aeb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ COPY pnpm-workspace.yaml ./ # 首先复制 package.json, package-lock.json 和 pnpm-lock.yaml 文件 COPY package*.json pnpm-lock.yaml* ./ -COPY tsconfig.json . +COPY tsconfig.base.json . # 利用 Docker 缓存机制,如果依赖没有改变则不会重新执行 pnpm install #100-500 5-40 diff --git a/Dockerfile_BACKUP b/Dockerfile_BACKUP new file mode 100644 index 0000000..4cbe03d --- /dev/null +++ b/Dockerfile_BACKUP @@ -0,0 +1,107 @@ +# 基础镜像 +FROM node:20-alpine as base +# 更改 apk 镜像源为阿里云 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories +# 设置 npm 镜像源 +RUN yarn config set registry https://registry.npmmirror.com + +# 全局安装 pnpm 并设置其镜像源 +RUN yarn global add pnpm && pnpm config set registry https://registry.npmmirror.com + +# 设置工作目录 +WORKDIR /app + +# 复制 pnpm workspace 配置文件 +COPY pnpm-workspace.yaml ./ + +# 首先复制 package.json, package-lock.json 和 pnpm-lock.yaml 文件 +COPY package*.json pnpm-lock.yaml* ./ + +COPY tsconfig.json . +# 利用 Docker 缓存机制,如果依赖没有改变则不会重新执行 pnpm install +#100-500 5-40 + +FROM base As server-build +WORKDIR /app +COPY packages/common /app/packages/common +COPY apps/server /app/apps/server +RUN pnpm install --filter server +RUN pnpm install --filter common +RUN pnpm --filter common generate && pnpm --filter common build:cjs +RUN pnpm --filter server build + +FROM base As server-prod-dep +WORKDIR /app +COPY packages/common /app/packages/common +COPY apps/server /app/apps/server +RUN pnpm install --filter common --prod +RUN pnpm install --filter server --prod + + + +FROM server-prod-dep as server +WORKDIR /app +ENV NODE_ENV production +COPY --from=server-build /app/packages/common/dist ./packages/common/dist +COPY --from=server-build /app/apps/server/dist ./apps/server/dist +COPY apps/server/entrypoint.sh ./apps/server/entrypoint.sh + +RUN chmod +x ./apps/server/entrypoint.sh +RUN apk add --no-cache postgresql-client + + +EXPOSE 3000 + +ENTRYPOINT [ "/app/apps/server/entrypoint.sh" ] + + + +FROM base AS web-build +# 复制其余文件到工作目录 +COPY . . +RUN pnpm install +RUN pnpm --filter web build + +# 第二阶段,使用 nginx 提供服务 +FROM nginx:stable-alpine as web +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories +# 设置工作目录 +WORKDIR /usr/share/nginx/html +# 设置环境变量 +ENV NODE_ENV production +# 将构建的文件从上一阶段复制到当前镜像中 +COPY --from=web-build /app/apps/web/dist . +# 删除默认的nginx配置文件并添加自定义配置 +RUN rm /etc/nginx/conf.d/default.conf +COPY config/nginx/nginx.conf /etc/nginx/conf.d +# 添加 entrypoint 脚本,并确保其可执行 +COPY config/nginx/entrypoint.sh /usr/bin/ +RUN chmod +x /usr/bin/entrypoint.sh +# 安装 envsubst 以支持环境变量替换 +RUN apk add --no-cache gettext +# 暴露 80 端口 +EXPOSE 80 + +CMD ["/usr/bin/entrypoint.sh"] + + +# 使用 Nginx 的 Alpine 版本作为基础镜像 +FROM nginx:stable-alpine as nginx + +# 替换 Alpine 的软件源为阿里云镜像 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories + +# 设置工作目录 +WORKDIR /usr/share/nginx/html + +# 设置环境变量 +ENV NODE_ENV production + +# 安装 envsubst 和 inotify-tools +RUN apk add --no-cache gettext inotify-tools + +# 创建 /data/uploads 目录 +RUN mkdir -p /data/uploads + +# 暴露 80 端口 +EXPOSE 80 \ No newline at end of file diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index a437b08..033337d 100755 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -23,7 +23,7 @@ export class AuthService { private readonly staffService: StaffService, private readonly jwtService: JwtService, private readonly sessionService: SessionService, - ) { } + ) {} async validateFileRequest(params: FileRequest): Promise { try { // 基础参数验证 @@ -31,7 +31,6 @@ export class AuthService { return { isValid: false, error: FileValidationErrorType.INVALID_URI }; } const fileId = extractFileIdFromNginxUrl(params.originalUri); - console.log('auth', params.originalUri, fileId); const resource = await db.resource.findFirst({ where: { fileId } }); // 资源验证 @@ -170,13 +169,13 @@ export class AuthService { showname, department: deptId ? { - connect: { id: deptId }, - } + connect: { id: deptId }, + } : undefined, domain: deptId ? { - connect: { id: deptId }, - } + connect: { id: deptId }, + } : undefined, // domainId: data.deptId, meta: { diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 9de3772..2aaba6e 100644 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -101,7 +101,7 @@ export function getClientIp(req: any): string { return ip || ''; } export async function updatePostState(id: string) { - console.log('updateState'); + // console.log('updateState'); const post = await db.post.findUnique({ where: { id: id, diff --git a/apps/server/src/queue/worker/file.processor.ts b/apps/server/src/queue/worker/file.processor.ts index 0fcc3e3..0414602 100644 --- a/apps/server/src/queue/worker/file.processor.ts +++ b/apps/server/src/queue/worker/file.processor.ts @@ -10,7 +10,6 @@ const pipeline = new ResourceProcessingPipeline() .addProcessor(new VideoProcessor()); export default async function processJob(job: Job) { if (job.name === QueueJobType.FILE_PROCESS) { - // console.log(job); const { resource } = job.data; if (!resource) { throw new Error('No resource provided in job data'); diff --git a/apps/server/src/socket/collaboration/ws-shared-doc.ts b/apps/server/src/socket/collaboration/ws-shared-doc.ts index ae1bd09..cd3721f 100644 --- a/apps/server/src/socket/collaboration/ws-shared-doc.ts +++ b/apps/server/src/socket/collaboration/ws-shared-doc.ts @@ -1,158 +1,185 @@ import { readSyncMessage } from '@nice/common'; -import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates, writeSyncStep1, writeUpdate } from '@nice/common'; +import { + applyAwarenessUpdate, + Awareness, + encodeAwarenessUpdate, + removeAwarenessStates, + writeSyncStep1, + writeUpdate, +} from '@nice/common'; import * as encoding from 'lib0/encoding'; import * as decoding from 'lib0/decoding'; -import * as Y from "yjs" +import * as Y from 'yjs'; import { debounce } from 'lodash'; import { getPersistence, setPersistence } from './persistence'; import { callbackHandler, isCallbackSet } from './callback'; -import { WebSocket } from "ws"; +import { WebSocket } from 'ws'; import { YMessageType } from '@nice/common'; import { WSClient } from '../types'; export const docs = new Map(); -export const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000'); -export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000'); +export const CALLBACK_DEBOUNCE_WAIT = parseInt( + process.env.CALLBACK_DEBOUNCE_WAIT || '2000', +); +export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt( + process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000', +); export const getYDoc = (docname: string, gc = true): WSSharedDoc => { - return docs.get(docname) || createYDoc(docname, gc); + return docs.get(docname) || createYDoc(docname, gc); }; const createYDoc = (docname: string, gc: boolean): WSSharedDoc => { - const doc = new WSSharedDoc(docname, gc); - docs.set(docname, doc); - return doc; + const doc = new WSSharedDoc(docname, gc); + docs.set(docname, doc); + return doc; }; export const send = (doc: WSSharedDoc, conn: WebSocket, m: Uint8Array) => { - if (conn.readyState !== WebSocket.OPEN) { - closeConn(doc, conn); - return; - } - try { - conn.send(m, {}, err => { err != null && closeConn(doc, conn) }); - } catch (e) { - closeConn(doc, conn); - } + if (conn.readyState !== WebSocket.OPEN) { + closeConn(doc, conn); + return; + } + try { + conn.send(m, {}, (err) => { + err != null && closeConn(doc, conn); + }); + } catch (e) { + closeConn(doc, conn); + } }; export const closeConn = (doc: WSSharedDoc, conn: WebSocket) => { - if (doc.conns.has(conn)) { - const controlledIds = doc.conns.get(conn) as Set; - doc.conns.delete(conn); - removeAwarenessStates( - doc.awareness, - Array.from(controlledIds), - null - ); + if (doc.conns.has(conn)) { + const controlledIds = doc.conns.get(conn) as Set; + doc.conns.delete(conn); + removeAwarenessStates(doc.awareness, Array.from(controlledIds), null); - if (doc.conns.size === 0 && getPersistence() !== null) { - getPersistence()?.writeState(doc.name, doc).then(() => { - doc.destroy(); - }); - docs.delete(doc.name); - } + if (doc.conns.size === 0 && getPersistence() !== null) { + getPersistence() + ?.writeState(doc.name, doc) + .then(() => { + doc.destroy(); + }); + docs.delete(doc.name); } - conn.close(); + } + conn.close(); }; -export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8Array) => { - try { - const encoder = encoding.createEncoder(); - const decoder = decoding.createDecoder(message); - const messageType = decoding.readVarUint(decoder); - switch (messageType) { - case YMessageType.Sync: - // console.log(`received sync message ${message.length}`) - encoding.writeVarUint(encoder, YMessageType.Sync); - readSyncMessage(decoder, encoder, doc, conn); - if (encoding.length(encoder) > 1) { - send(doc, conn, encoding.toUint8Array(encoder)); - } - break; - - case YMessageType.Awareness: { - applyAwarenessUpdate( - doc.awareness, - decoding.readVarUint8Array(decoder), - conn - ); - // console.log(`received awareness message from ${conn.origin} total ${doc.awareness.states.size}`) - break; - } - } - } catch (err) { - console.error(err); - doc.emit('error' as any, [err]); - } -}; - -const updateHandler = (update: Uint8Array, _origin: any, doc: WSSharedDoc, _tr: any) => { +export const messageListener = ( + conn: WSClient, + doc: WSSharedDoc, + message: Uint8Array, +) => { + try { const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, YMessageType.Sync); - writeUpdate(encoder, update); - const message = encoding.toUint8Array(encoder); - doc.conns.forEach((_, conn) => send(doc, conn, message)); + const decoder = decoding.createDecoder(message); + const messageType = decoding.readVarUint(decoder); + switch (messageType) { + case YMessageType.Sync: + encoding.writeVarUint(encoder, YMessageType.Sync); + readSyncMessage(decoder, encoder, doc, conn); + if (encoding.length(encoder) > 1) { + send(doc, conn, encoding.toUint8Array(encoder)); + } + break; + + case YMessageType.Awareness: { + applyAwarenessUpdate( + doc.awareness, + decoding.readVarUint8Array(decoder), + conn, + ); + break; + } + } + } catch (err) { + console.error(err); + doc.emit('error' as any, [err]); + } }; -let contentInitializor: (ydoc: Y.Doc) => Promise = (_ydoc) => Promise.resolve(); +const updateHandler = ( + update: Uint8Array, + _origin: any, + doc: WSSharedDoc, + _tr: any, +) => { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, YMessageType.Sync); + writeUpdate(encoder, update); + const message = encoding.toUint8Array(encoder); + doc.conns.forEach((_, conn) => send(doc, conn, message)); +}; + +let contentInitializor: (ydoc: Y.Doc) => Promise = (_ydoc) => + Promise.resolve(); export const setContentInitializor = (f: (ydoc: Y.Doc) => Promise) => { - contentInitializor = f; + contentInitializor = f; }; export class WSSharedDoc extends Y.Doc { - name: string; - conns: Map>; - awareness: Awareness; - whenInitialized: Promise; + name: string; + conns: Map>; + awareness: Awareness; + whenInitialized: Promise; - constructor(name: string, gc: boolean) { - super({ gc }); + constructor(name: string, gc: boolean) { + super({ gc }); - this.name = name; - this.conns = new Map(); - this.awareness = new Awareness(this); - this.awareness.setLocalState(null); + this.name = name; + this.conns = new Map(); + this.awareness = new Awareness(this); + this.awareness.setLocalState(null); - const awarenessUpdateHandler = ({ - added, - updated, - removed - }: { - added: number[], - updated: number[], - removed: number[] - }, conn: WebSocket) => { - const changedClients = added.concat(updated, removed); - if (changedClients.length === 0) return - if (conn !== null) { - const connControlledIDs = this.conns.get(conn) as Set; - if (connControlledIDs !== undefined) { - added.forEach(clientID => { connControlledIDs.add(clientID); }); - removed.forEach(clientID => { connControlledIDs.delete(clientID); }); - } - } - - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, YMessageType.Awareness); - encoding.writeVarUint8Array( - encoder, - encodeAwarenessUpdate(this.awareness, changedClients) - ); - const buff = encoding.toUint8Array(encoder); - - this.conns.forEach((_, c) => { - send(this, c, buff); - }); - }; - - this.awareness.on('update', awarenessUpdateHandler); - this.on('update', updateHandler as any); - - if (isCallbackSet) { - this.on('update', debounce( - callbackHandler as any, - CALLBACK_DEBOUNCE_WAIT, - { maxWait: CALLBACK_DEBOUNCE_MAXWAIT } - ) as any); + const awarenessUpdateHandler = ( + { + added, + updated, + removed, + }: { + added: number[]; + updated: number[]; + removed: number[]; + }, + conn: WebSocket, + ) => { + const changedClients = added.concat(updated, removed); + if (changedClients.length === 0) return; + if (conn !== null) { + const connControlledIDs = this.conns.get(conn) as Set; + if (connControlledIDs !== undefined) { + added.forEach((clientID) => { + connControlledIDs.add(clientID); + }); + removed.forEach((clientID) => { + connControlledIDs.delete(clientID); + }); } + } - this.whenInitialized = contentInitializor(this); + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, YMessageType.Awareness); + encoding.writeVarUint8Array( + encoder, + encodeAwarenessUpdate(this.awareness, changedClients), + ); + const buff = encoding.toUint8Array(encoder); + + this.conns.forEach((_, c) => { + send(this, c, buff); + }); + }; + + this.awareness.on('update', awarenessUpdateHandler); + this.on('update', updateHandler as any); + + if (isCallbackSet) { + this.on( + 'update', + debounce(callbackHandler as any, CALLBACK_DEBOUNCE_WAIT, { + maxWait: CALLBACK_DEBOUNCE_MAXWAIT, + }) as any, + ); } + + this.whenInitialized = contentInitializor(this); + } } diff --git a/apps/server/src/utils/tool.ts b/apps/server/src/utils/tool.ts index 20b6554..c72d511 100755 --- a/apps/server/src/utils/tool.ts +++ b/apps/server/src/utils/tool.ts @@ -1,148 +1,146 @@ -import { createReadStream } from "fs"; -import { createInterface } from "readline"; +import { createReadStream } from 'fs'; +import { createInterface } from 'readline'; -import { db } from '@nice/common'; -import * as tus from "tus-js-client"; +import { db } from '@nice/common'; +import * as tus from 'tus-js-client'; import ExcelJS from 'exceljs'; export function truncateStringByByte(str, maxBytes) { - let byteCount = 0; - let index = 0; - while (index < str.length && byteCount + new TextEncoder().encode(str[index]).length <= maxBytes) { - byteCount += new TextEncoder().encode(str[index]).length; - index++; - } - return str.substring(0, index) + (index < str.length ? "..." : ""); + let byteCount = 0; + let index = 0; + while ( + index < str.length && + byteCount + new TextEncoder().encode(str[index]).length <= maxBytes + ) { + byteCount += new TextEncoder().encode(str[index]).length; + index++; + } + return str.substring(0, index) + (index < str.length ? '...' : ''); } export async function loadPoliciesFromCSV(filePath: string) { - const policies = { - p: [], - g: [] - }; - const stream = createReadStream(filePath); - const rl = createInterface({ - input: stream, - crlfDelay: Infinity - }); + const policies = { + p: [], + g: [], + }; + const stream = createReadStream(filePath); + const rl = createInterface({ + input: stream, + crlfDelay: Infinity, + }); - // Updated regex to handle commas inside parentheses as part of a single field - const regex = /(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g; + // Updated regex to handle commas inside parentheses as part of a single field + const regex = + /(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g; - for await (const line of rl) { - // Ignore empty lines and comments - if (line.trim() && !line.startsWith("#")) { - const parts = []; - let match; - while ((match = regex.exec(line)) !== null) { - // Remove quotes if present and trim whitespace - parts.push(match[0].replace(/^"|"$/g, '').trim()); - } + for await (const line of rl) { + // Ignore empty lines and comments + if (line.trim() && !line.startsWith('#')) { + const parts = []; + let match; + while ((match = regex.exec(line)) !== null) { + // Remove quotes if present and trim whitespace + parts.push(match[0].replace(/^"|"$/g, '').trim()); + } - // Check policy type (p or g) - const ptype = parts[0]; - const rule = parts.slice(1); + // Check policy type (p or g) + const ptype = parts[0]; + const rule = parts.slice(1); - if (ptype === 'p' || ptype === 'g') { - policies[ptype].push(rule); - } else { - console.warn(`Unknown policy type '${ptype}' in policy: ${line}`); - } - } + if (ptype === 'p' || ptype === 'g') { + policies[ptype].push(rule); + } else { + console.warn(`Unknown policy type '${ptype}' in policy: ${line}`); + } } + } - return policies; + return policies; } export function uploadFile(blob: any, fileName: string) { - return new Promise((resolve, reject) => { - const upload = new tus.Upload(blob, { - endpoint: `${process.env.TUS_URL}/files/`, - retryDelays: [0, 1000, 3000, 5000], - metadata: { - filename: fileName, - filetype: - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - }, - onError: (error) => { - console.error("Failed because: " + error); - reject(error); // 错误时,我们要拒绝 promise - }, - onProgress: (bytesUploaded, bytesTotal) => { - const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); - // console.log(bytesUploaded, bytesTotal, `${percentage}%`); - }, - onSuccess: () => { - // console.log('Upload finished:', upload.url); - resolve(upload.url); // 成功后,我们解析 promise,并返回上传的 URL - }, - }); - upload.start(); + return new Promise((resolve, reject) => { + const upload = new tus.Upload(blob, { + endpoint: `${process.env.TUS_URL}/files/`, + retryDelays: [0, 1000, 3000, 5000], + metadata: { + filename: fileName, + filetype: + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + onError: (error) => { + console.error('Failed because: ' + error); + reject(error); // 错误时,我们要拒绝 promise + }, + onProgress: (bytesUploaded, bytesTotal) => { + const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); + }, + onSuccess: () => { + resolve(upload.url); // 成功后,我们解析 promise,并返回上传的 URL + }, }); + upload.start(); + }); } - class TreeNode { - value: string; - children: TreeNode[]; - - constructor(value: string) { - this.value = value; - this.children = []; - } - - addChild(childValue: string): TreeNode { - let newChild = undefined - if (this.children.findIndex(child => child.value === childValue) === -1) { - newChild = new TreeNode(childValue); - this.children.push(newChild) - - } - return this.children.find(child => child.value === childValue) + value: string; + children: TreeNode[]; + constructor(value: string) { + this.value = value; + this.children = []; + } + + addChild(childValue: string): TreeNode { + let newChild = undefined; + if (this.children.findIndex((child) => child.value === childValue) === -1) { + newChild = new TreeNode(childValue); + this.children.push(newChild); } + return this.children.find((child) => child.value === childValue); + } } function buildTree(data: string[][]): TreeNode { - const root = new TreeNode('root'); - try { - for (const path of data) { - let currentNode = root; - for (const value of path) { - currentNode = currentNode.addChild(value); - } - } - return root; + const root = new TreeNode('root'); + try { + for (const path of data) { + let currentNode = root; + for (const value of path) { + currentNode = currentNode.addChild(value); + } } - catch (error) { - console.error(error) - } - - + return root; + } catch (error) { + console.error(error); + } } export function printTree(node: TreeNode, level: number = 0): void { - const indent = ' '.repeat(level); - // console.log(`${indent}${node.value}`); - for (const child of node.children) { - printTree(child, level + 1); - } + const indent = ' '.repeat(level); + for (const child of node.children) { + printTree(child, level + 1); + } } export async function generateTreeFromFile(file: Buffer): Promise { - const workbook = new ExcelJS.Workbook(); - await workbook.xlsx.load(file); - const worksheet = workbook.getWorksheet(1); + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(file); + const worksheet = workbook.getWorksheet(1); - const data: string[][] = []; + const data: string[][] = []; - worksheet.eachRow((row, rowNumber) => { - if (rowNumber > 1) { // Skip header row if any - const rowData: string[] = (row.values as string[]).slice(2).map(cell => (cell || '').toString()); - data.push(rowData.map(value => value.trim())); - } - }); - // Fill forward values - for (let i = 1; i < data.length; i++) { - for (let j = 0; j < data[i].length; j++) { - if (!data[i][j]) data[i][j] = data[i - 1][j]; - } + worksheet.eachRow((row, rowNumber) => { + if (rowNumber > 1) { + // Skip header row if any + const rowData: string[] = (row.values as string[]) + .slice(2) + .map((cell) => (cell || '').toString()); + data.push(rowData.map((value) => value.trim())); } - return buildTree(data); -} \ No newline at end of file + }); + // Fill forward values + for (let i = 1; i < data.length; i++) { + for (let j = 0; j < data[i].length; j++) { + if (!data[i][j]) data[i][j] = data[i - 1][j]; + } + } + return buildTree(data); +} diff --git a/apps/web/entrypoint.sh b/apps/web/entrypoint.sh new file mode 100644 index 0000000..8554e52 --- /dev/null +++ b/apps/web/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# 使用envsubst替换index.html中的环境变量占位符 +envsubst < /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.html.tmp +mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html +# 运行serve来提供静态文件 +exec nginx -g "daemon off;" diff --git a/apps/web/index.html b/apps/web/index.html index 7087b23..816bc23 100755 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -8,6 +8,7 @@