diff --git a/.gitignore b/.gitignore index 0f972d5..854474e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ dist coverage .DS_Store -upload \ No newline at end of file +upload + +app.config.json5 \ No newline at end of file diff --git a/app.config.json5 b/app.config.json5 index f0493d1..059399e 100644 --- a/app.config.json5 +++ b/app.config.json5 @@ -1,8 +1,10 @@ { port: 3005, api: { - host: 'codeflow.xiongxiao.me', // 后台代理 + host: 'localhost:4000', // 后台代理 + testHost: 'localhost:4000', }, + allowedOrigins: ['localhost', 'xiongxiao.me', 'zxj.im'], domain: 'kevisual.xiongxiao.me', resources: 'minio.xiongxiao.me/resources', } diff --git a/package.json b/package.json index 0fa6238..ef4378f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "dev": "nodemon --exec tsx src/index.ts", + "dev": "cross-env NODE_ENV=development nodemon --exec tsx src/index.ts", "build": "rimraf dist && rollup -c", "deploy": "rsync -avz dist/ light:~/apps/var-proxy/backend", "reload": "ssh light pm2 restart proxy" @@ -19,6 +19,7 @@ "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-typescript": "^12.1.0", "@types/node": "^22.7.4", + "cross-env": "^7.0.3", "nodemon": "^3.1.7", "rollup": "^4.24.0", "ts-lib": "^0.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c994d75..6f54ed6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: '@types/node': specifier: ^22.7.4 version: 22.7.4 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 nodemon: specifier: ^3.1.7 version: 3.1.7 @@ -371,6 +374,15 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -506,6 +518,9 @@ packages: is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} @@ -575,6 +590,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -631,6 +650,14 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -749,6 +776,11 @@ packages: webpack-cli: optional: true + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + snapshots: '@abearxiong/use-config@0.0.2': {} @@ -1055,6 +1087,16 @@ snapshots: concat-map@0.0.1: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.3 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + debug@4.3.7(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -1164,6 +1206,8 @@ snapshots: dependencies: '@types/estree': 1.0.6 + isexe@2.0.0: {} + jest-worker@27.5.1: dependencies: '@types/node': 22.7.4 @@ -1226,6 +1270,8 @@ snapshots: normalize-path@3.0.0: {} + path-key@3.1.1: {} + path-parse@1.0.7: {} picocolors@1.1.0: {} @@ -1292,6 +1338,12 @@ snapshots: dependencies: randombytes: 2.1.0 + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + simple-update-notifier@2.0.0: dependencies: semver: 7.6.3 @@ -1411,3 +1463,7 @@ snapshots: - '@swc/core' - esbuild - uglify-js + + which@2.0.2: + dependencies: + isexe: 2.0.0 diff --git a/src/module/get-content-type.ts b/src/module/get-content-type.ts index d592ba3..9289e60 100644 --- a/src/module/get-content-type.ts +++ b/src/module/get-content-type.ts @@ -3,6 +3,7 @@ import path from 'path'; export const getContentType = (filePath: string) => { const extname = path.extname(filePath); const contentType = { + '.txt': 'text/plain', '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', diff --git a/src/module/get-user-app.ts b/src/module/get-user-app.ts index 07d6a19..c650e90 100644 --- a/src/module/get-user-app.ts +++ b/src/module/get-user-app.ts @@ -9,7 +9,7 @@ import { pipeline } from 'stream'; import { promisify } from 'util'; const pipelineAsync = promisify(pipeline); -const { resources } = useConfig<{ resources: string }>(); +const { resources, api } = useConfig<{ resources: string; api: { host: string; testHost: string } }>(); const fileStore = useFileStore('upload'); const demoData = { @@ -36,30 +36,6 @@ const demoData = { ], }, }; -const demoData2 = { - user: 'root', - key: 'codeflow', - appType: 'web-single', // - version: '0.0.1', - domain: null, - type: 'oss', // 是否使用oss - data: { - files: [ - { - name: 'index.html', - path: 'root/codeflow/0.0.1/index.html', - }, - { - name: 'assets/index-14y4J8dP.js', - path: 'root/codeflow/0.0.1/assets/index-14y4J8dP.js', - }, - { - name: 'assets/index-C-libw4a.css', - path: 'root/codeflow/0.0.1/assets/index-C-libw4a.css', - }, - ], - }, -}; type UserAppOptions = { user: string; app: string; @@ -94,16 +70,89 @@ export class UserApp { const value = await redis.hget(key, appFileUrl); return value; } + static async getDomainApp(domain: string) { + const key = 'domain:' + domain; + const value = await redis.get(key); + if (value) { + const [_user, _app] = value.split(':'); + return { + user: _user, + app: _app, + }; + } + + // 获取域名对应的用户和应用 + const isDev = process.env.NODE_ENV === 'development'; + const fetchTestUrl = 'http://' + api.testHost + '/api/router'; + const fetchUrl = 'http://' + api.host + '/api/router'; + const fetchRes = await fetch(isDev ? fetchTestUrl : fetchUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + path: 'app', + key: 'getDomainApp', + data: { + domain, + }, + }), + }).then((res) => res.json()); + if (fetchRes?.code !== 200) { + console.log('fetchRes is error', fetchRes); + return null; + } + const fetchData = fetchRes.data; + const data = { + user: fetchData.user, + app: fetchData.key, + }; + redis.set(key, data.user + ':' + data.app, 'EX', 60 * 60 * 24 * 7); // 24小时 + return data; + } async setCacheData() { const app = this.app; const user = this.user; const key = 'user:app:' + app + ':' + user; - // 如果demoData 不存在则返回 - if (!demoData2) { + const isDev = process.env.NODE_ENV === 'development'; + const fetchTestUrl = 'http://' + api.testHost + '/api/router'; + const fetchUrl = 'http://' + api.host + '/api/router'; + const fetchRes = await fetch(isDev ? fetchTestUrl : fetchUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + path: 'app', + key: 'getApp', + data: { + user, + key: app, + }, + }), + }).then((res) => res.json()); + if (fetchRes?.code !== 200) { + console.log('fetchRes is error', fetchRes); return false; } - const value = await downloadUserAppFiles(user, app, demoData2); - const valueIndexHtml = value.data.files.find((file) => file.name === 'index.html'); + const fetchData = fetchRes.data; + if (!fetchData.type) { + // console.error('fetchData type is error', fetchData); + // return false; + fetchData.type = 'oss'; + } + const value = await downloadUserAppFiles(user, app, fetchData); + if (value.data.files.length === 0) { + console.error('root files length is zero', user, app); + return false; + } + let valueIndexHtml = value.data.files.find((file) => file.name === 'index.html'); + if (!valueIndexHtml) { + valueIndexHtml = value.data.files.find((file) => file.name === 'index.js'); + if (!valueIndexHtml) { + valueIndexHtml = value.data.files[0]; + } + } await redis.set(key, JSON.stringify(value)); await redis.set('user:app:exist:' + app + ':' + user, valueIndexHtml.path, 'EX', 60 * 60 * 24 * 7); // 24小时 const files = value.data.files; @@ -140,9 +189,6 @@ export class UserApp { // 删除所有文件 deleteUserAppFiles(user, app); } - async getData() { - return demoData; - } async close() { // 关闭连接 await redis.quit(); diff --git a/src/module/index.ts b/src/module/index.ts index 8f6c1c6..27f9472 100644 --- a/src/module/index.ts +++ b/src/module/index.ts @@ -7,11 +7,12 @@ import fs from 'fs'; import { useConfig } from '@abearxiong/use-config'; import { redis } from './redis/redis.ts'; import { getContentType } from './get-content-type.ts'; -const { api, domain } = useConfig<{ +const { api, domain, allowedOrigins } = useConfig<{ api: { host: string; }; domain: string; + allowedOrigins: string[]; }>(); const fileStore = useFileStore('upload'); @@ -19,6 +20,18 @@ console.log('filePath', fileStore); const noProxyUrl = ['/', '/favicon.ico']; export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => { const dns = getDNS(req); + // 配置可以跨域 + // 配置可以访问的域名 localhost, xiongxiao.me + const _orings = allowedOrigins || []; + const host = dns.hostName; + if ( + _orings.some((item) => { + return host.includes(item); + }) + ) { + res.setHeader('Access-Control-Allow-Origin', '*'); + } + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); let user, app; let domainApp = false; @@ -33,21 +46,19 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR if (dns.hostName !== domain) { // redis获取域名对应的用户和应用 domainApp = true; - const key = 'domain:' + dns.hostName; - const value = await redis.get(key); - if (!value) { + const data = await UserApp.getDomainApp(dns.hostName); + if (!data) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.write('Invalid domain\n'); return res.end(); } - const [_user, _app] = value.split(':'); - if (!_user || !_app) { + if (!data.user || !data.app) { res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.write('Invalid domain, Config error\n'); + res.write('Invalid domain config\n'); return res.end(); } - user = _user; - app = _app; + user = data.user; + app = data.app; } } const url = req.url; @@ -145,8 +156,10 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR const appFile = await userApp.getFile(appFileUrl); if (!appFile) { const [indexFilePath, etag] = indexFile.split('||'); + const contentType = getContentType(indexFilePath); + const isHTML = contentType.includes('html'); // 不存在的文件,返回indexFile的文件 - res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' }); + res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': isHTML ? 'no-cache' : 'public, max-age=3600' }); const filePath = path.join(fileStore, indexFilePath); const readStream = fs.createReadStream(filePath); readStream.pipe(res); @@ -161,9 +174,10 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR } const filePath = path.join(fileStore, appFilePath); let contentType = getContentType(filePath); + const isHTML = contentType.includes('html'); res.writeHead(200, { 'Content-Type': contentType, - 'Cache-Control': 'public, max-age=3600', // 设置缓存时间为 1 小时 + 'Cache-Control': isHTML ? 'no-cache' : 'public, max-age=3600', // 设置缓存时间为 1 小时 ETag: eTag, }); const readStream = fs.createReadStream(filePath); diff --git a/src/scripts/copy.ts b/src/scripts/copy.ts index 046fa8c..1bb8058 100644 --- a/src/scripts/copy.ts +++ b/src/scripts/copy.ts @@ -31,7 +31,8 @@ const clearData = async () => { }; // clearData(); -clearAllUserApp(); +// clearAllUserApp(); + const expireData = async () => { await redis.set('user:app:exist:' + 'codeflow:root', 'value', 'EX', 2); @@ -39,3 +40,10 @@ const expireData = async () => { }; // expireData(); + +const keysData = async () => { + const keys = await redis.keys('user:app:exist:*'); + console.log('keys', keys); + process.exit(0); +} +keysData(); \ No newline at end of file