PicList/src/main/server/index.ts
2024-04-15 23:12:02 +08:00

226 lines
6.8 KiB
TypeScript

import http from 'http'
import routers from './routerManager'
import {
handleResponse,
ensureHTTPLink
} from './utils'
import picgo from '@core/picgo'
import logger from '@core/picgo/logger'
import axios from 'axios'
import multer from 'multer'
import { app } from 'electron'
import path from 'path'
import fs from 'fs-extra'
import { configPaths } from '~/universal/utils/configPaths'
const DEFAULT_PORT = 36677
const DEFAULT_HOST = '0.0.0.0'
const appPath = app.getPath('userData')
const serverTempDir = path.join(appPath, 'serverTemp')
fs.ensureDirSync(serverTempDir)
const multerStorage = multer.diskStorage({
destination: function (_req: any, _file: any, cb: (arg0: null, arg1: any) => void) {
fs.ensureDirSync(serverTempDir)
cb(null, serverTempDir)
},
filename: function (_req: any, file: { originalname: any }, cb: (arg0: null, arg1: any) => void) {
// eslint-disable-next-line no-control-regex
if (!/[^\u0000-\u00ff]/.test(file.originalname)) {
file.originalname = Buffer.from(file.originalname, 'latin1').toString(
'utf8'
)
}
cb(null, file.originalname)
}
})
const uploadMulter = multer({
storage: multerStorage
})
class Server {
#httpServer: http.Server
#config: IServerConfig
constructor () {
this.#config = this.getConfigWithDefaults()
this.#httpServer = http.createServer(this.#handleRequest)
}
getConfigWithDefaults () {
let config = picgo.getConfig<IServerConfig>(configPaths.settings.server)
if (!this.#isValidConfig(config)) {
config = { port: DEFAULT_PORT, host: DEFAULT_HOST, enable: true }
picgo.saveConfig({ [configPaths.settings.server]: config })
}
config.host = config.host === '127.0.0.1' ? '0.0.0.0' : config.host
return config
}
#isValidConfig (config: IObj | undefined) {
return config && config.port && config.host && (config.enable !== undefined)
}
#handleRequest = (request: http.IncomingMessage, response: http.ServerResponse) => {
switch (request.method) {
case 'OPTIONS':
handleResponse({ response })
break
case 'POST':
this.#handlePostRequest(request, response)
break
case 'GET':
this.#handleGetRequest(request, response)
break
default:
logger.warn(`[PicList Server] don't support [${request.method}] method`)
response.statusCode = 405
response.end()
}
}
#handlePostRequest = (request: http.IncomingMessage, response: http.ServerResponse) => {
const [url, query] = (request.url || '').split('?')
if (!routers.getHandler(url, 'POST')) {
logger.warn(`[PicList Server] don't support [${url}] endpoint`)
handleResponse({
response,
statusCode: 404,
body: {
success: false
}
})
} else {
const remoteAddress = request.socket.remoteAddress || 'unknown'
logger.info('[PicList Server] get a POST request from IP:', remoteAddress)
let urlSP = query ? new URLSearchParams(query) : undefined
if (remoteAddress === '::1' || remoteAddress === '127.0.0.1') {
const serverKey = picgo.getConfig<string>(configPaths.settings.serverKey) || ''
if (urlSP) {
urlSP.set('key', serverKey)
} else {
urlSP = new URLSearchParams('key=' + serverKey)
}
}
if (request.headers['content-type'] && request.headers['content-type'].startsWith('multipart/form-data')) {
// @ts-ignore
uploadMulter.any()(request, response, (err: any) => {
if (err) {
logger.info('[PicList Server]', err)
return handleResponse({
response,
body: {
success: false,
message: 'Error processing formData'
}
})
}
// @ts-ignore
const list = request.files.map(file => file.path)
logger.info('[PicList Server] get a formData request')
const handler = routers.getHandler(url!, 'POST')?.handler
if (handler) {
handler({
list,
response,
urlparams: urlSP
})
}
})
} else {
let body: string = ''
let postObj: IObj
request.on('data', chunk => {
body += chunk
})
request.on('end', () => {
try {
postObj = (body === '') ? {} : JSON.parse(body)
} catch (err: any) {
logger.error('[PicList Server]', err)
return handleResponse({
response,
body: {
success: false,
message: 'Not sending data in JSON format'
}
})
}
logger.info('[PicList Server] get the request', body)
const handler = routers.getHandler(url!, 'POST')?.handler
handler!({
...postObj,
response,
urlparams: urlSP
})
})
}
}
}
#handleGetRequest = (_request: http.IncomingMessage, response: http.ServerResponse) => {
const [url, query] = (_request.url || '').split('?')
if (!routers.getHandler(url, 'GET')) {
logger.info(`[PicList Server] don't support [${url}] endpoint`)
response.statusCode = 404
response.end()
} else {
const handler = routers.getHandler(url, 'GET')?.handler
if (handler) {
handler({
response,
urlparams: query ? new URLSearchParams(query) : undefined
})
}
}
}
// port as string is a bug
#listen = (port: number | string) => {
logger.info(`[PicList Server] is listening at ${port} of ${this.#config.host}`)
if (typeof port === 'string') {
port = parseInt(port, 10)
}
this.#httpServer.listen(port, this.#config.host).on('error', async (err: ErrnoException) => {
if (err.code === 'EADDRINUSE') {
try {
// make sure the system has a PicGo Server instance
await axios.post(ensureHTTPLink(`${this.#config.host}:${port}/heartbeat`))
logger.info(`[PicList Server] server is already running at ${port}`)
this.shutdown(true)
} catch (e) {
logger.warn(`[PicList Server] ${port} is busy, trying with port ${(port as number) + 1}`)
// fix a bug: not write an increase number to config file
// to solve the auto number problem
this.#listen((port as number) + 1)
}
} else {
logger.error('[PicList Server]', err)
}
})
}
startup () {
if (this.#config.enable) {
this.#listen(this.#config.port)
}
}
shutdown (hasStarted?: boolean) {
this.#httpServer.close()
if (!hasStarted) {
logger.info('[PicList Server] shutdown')
}
}
restart () {
this.shutdown()
this.#config = this.getConfigWithDefaults()
this.startup()
}
}
export default new Server()