Skip to content

Commit

Permalink
Merge pull request #191 from nrkno/fix/http-server-html-list
Browse files Browse the repository at this point in the history
HTTP-server: Add '/list' endpoint
  • Loading branch information
nytamin authored Jul 3, 2024
2 parents fb41089 + 73259d9 commit 09691ec
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 65 deletions.
1 change: 1 addition & 0 deletions apps/http-server/packages/generic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/packageVersion.ts
2 changes: 1 addition & 1 deletion apps/http-server/packages/generic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "yarn rimraf dist && yarn build:main",
"build": "yarn rimraf dist && node scripts/prebuild.js && yarn build:main",
"build:main": "tsc -p tsconfig.json",
"__test": "jest"
},
Expand Down
20 changes: 20 additions & 0 deletions apps/http-server/packages/generic/scripts/prebuild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const fs = require('fs').promises

async function main() {
const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8'))
const libStr = `// ****** This file is generated at build-time by scripts/prebuild.js ******
/**
* The version of the package.json file
*/
export const PACKAGE_JSON_VERSION = '${packageJson.version}'
`

await fs.writeFile('src/packageVersion.ts', libStr, 'utf8')
}

main().catch((e) => {
// eslint-disable-next-line no-console
console.error(e)
// eslint-disable-next-line no-process-exit
process.exit(1)
})
95 changes: 78 additions & 17 deletions apps/http-server/packages/generic/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import cors from '@koa/cors'
import bodyParser from 'koa-bodyparser'

import { HTTPServerConfig, LoggerInstance, stringifyError, first } from '@sofie-package-manager/api'
import { BadResponse, Storage } from './storage/storage'
import { BadResponse, PackageInfo, ResponseMeta, Storage, isBadResponse } from './storage/storage'
import { FileStorage } from './storage/fileStorage'
import { CTX, valueOrFirst } from './lib'
import { parseFormData } from 'pechkin'
// eslint-disable-next-line node/no-unpublished-import
import { PACKAGE_JSON_VERSION } from './packageVersion'

const fsReadFile = promisify(fs.readFile)

Expand All @@ -24,6 +26,8 @@ export class PackageProxyServer {
private storage: Storage
private logger: LoggerInstance

private startupTime = Date.now()

constructor(logger: LoggerInstance, private config: HTTPServerConfig) {
this.logger = logger.category('PackageProxyServer')
this.app.on('error', (err) => {
Expand Down Expand Up @@ -86,13 +90,16 @@ export class PackageProxyServer {
})

this.router.get('/packages', async (ctx) => {
await this.handleStorage(ctx, async () => this.storage.listPackages(ctx))
await this.handleStorage(ctx, async () => this.storage.listPackages())
})
this.router.get('/list', async (ctx) => {
await this.handleStorageHTMLList(ctx, async () => this.storage.listPackages())
})
this.router.get('/package/:path+', async (ctx) => {
await this.handleStorage(ctx, async () => this.storage.getPackage(ctx.params.path, ctx))
await this.handleStorage(ctx, async () => this.storage.getPackage(ctx.params.path))
})
this.router.head('/package/:path+', async (ctx) => {
await this.handleStorage(ctx, async () => this.storage.headPackage(ctx.params.path, ctx))
await this.handleStorage(ctx, async () => this.storage.headPackage(ctx.params.path))
})
this.router.post('/package/:path+', async (ctx) => {
this.logger.debug(`POST ${ctx.request.URL}`)
Expand All @@ -118,22 +125,17 @@ export class PackageProxyServer {
})
this.router.delete('/package/:path+', async (ctx) => {
this.logger.debug(`DELETE ${ctx.request.URL}`)
await this.handleStorage(ctx, async () => this.storage.deletePackage(ctx.params.path, ctx))
await this.handleStorage(ctx, async () => this.storage.deletePackage(ctx.params.path))
})

// Convenient pages:
this.router.get('/', async (ctx) => {
let packageJson = { version: '0.0.0' }
try {
packageJson = JSON.parse(
await fsReadFile('../package.json', {
encoding: 'utf8',
})
)
} catch (err) {
// ignore
ctx.body = {
name: 'Package proxy server',
version: PACKAGE_JSON_VERSION,
uptime: Date.now() - this.startupTime,
info: this.storage.getInfo(),
}
ctx.body = { name: 'Package proxy server', version: packageJson.version, info: this.storage.getInfo() }
})
this.router.get('/uploadForm/:path+', async (ctx) => {
// ctx.response.status = result.code
Expand Down Expand Up @@ -165,12 +167,71 @@ export class PackageProxyServer {
}
})
}
private async handleStorage(ctx: CTX, storageFcn: () => Promise<true | BadResponse>) {
private async handleStorage(ctx: CTX, storageFcn: () => Promise<{ meta: ResponseMeta; body?: any } | BadResponse>) {
try {
const result = await storageFcn()
if (result !== true) {
if (isBadResponse(result)) {
ctx.response.status = result.code
ctx.body = result.reason
} else {
ctx.response.status = result.meta.statusCode
if (result.meta.type !== undefined) ctx.type = result.meta.type
if (result.meta.length !== undefined) ctx.length = result.meta.length
if (result.meta.lastModified !== undefined) ctx.lastModified = result.meta.lastModified

if (result.meta.headers) {
for (const [key, value] of Object.entries<string>(result.meta.headers)) {
ctx.set(key, value)
}
}

if (result.body) ctx.body = result.body
}
} catch (err) {
this.logger.error(`Error in handleStorage: ${stringifyError(err)} `)
ctx.response.status = 500
ctx.body = 'Internal server error'
}
}
private async handleStorageHTMLList(
ctx: CTX,
storageFcn: () => Promise<{ body: { packages: PackageInfo[] } } | BadResponse>
) {
try {
const result = await storageFcn()
if (isBadResponse(result)) {
ctx.response.status = result.code
ctx.body = result.reason
} else {
const packages = result.body.packages

ctx.set('Content-Type', 'text/html')
ctx.body = `<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; }
</style>
</head>
<body>
<h1>Packages</h1>
<table>
${packages
.map(
(pkg) =>
`<tr>
<td><a href="package/${pkg.path}">${pkg.path}</a></td>
<td>${pkg.size}</td>
<td>${pkg.modified}</td>
</tr>`
)
.join('')}
</table>
</body>
</html>`
}
} catch (err) {
this.logger.error(`Error in handleStorage: ${stringifyError(err)} `)
Expand Down
86 changes: 45 additions & 41 deletions apps/http-server/packages/generic/src/storage/fileStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import path from 'path'
import { promisify } from 'util'
import mime from 'mime-types'
import prettyBytes from 'pretty-bytes'
import { asyncPipe, CTX, CTXPost } from '../lib'
import { asyncPipe, CTXPost } from '../lib'
import { HTTPServerConfig, LoggerInstance } from '@sofie-package-manager/api'
import { BadResponse, Storage } from './storage'
import { BadResponse, PackageInfo, ResponseMeta, Storage } from './storage'
import { Readable } from 'stream'

// Note: Explicit types here, due to that for some strange reason, promisify wont pass through the correct typings.
Expand Down Expand Up @@ -39,19 +39,14 @@ export class FileStorage extends Storage {
}

getInfo(): string {
return this._basePath
return `basePath: "${this._basePath}", cleanFileAge: ${this.config.httpServer.cleanFileAge}`
}

async init(): Promise<void> {
await fsMkDir(this._basePath, { recursive: true })
}

async listPackages(ctx: CTX): Promise<true | BadResponse> {
type PackageInfo = {
path: string
size: string
modified: string
}
async listPackages(): Promise<{ meta: ResponseMeta; body: { packages: PackageInfo[] } } | BadResponse> {
const packages: PackageInfo[] = []

const getAllFiles = async (basePath: string, dirPath: string) => {
Expand Down Expand Up @@ -84,9 +79,11 @@ export class FileStorage extends Storage {
return 0
})

ctx.body = { packages: packages }
const meta: ResponseMeta = {
statusCode: 200,
}

return true
return { meta, body: { packages } }
}
private async getFileInfo(paramPath: string): Promise<
| {
Expand Down Expand Up @@ -118,40 +115,40 @@ export class FileStorage extends Storage {
lastModified: stat.mtime,
}
}
async headPackage(paramPath: string, ctx: CTX): Promise<true | BadResponse> {
async headPackage(paramPath: string): Promise<{ meta: ResponseMeta } | BadResponse> {
const fileInfo = await this.getFileInfo(paramPath)

if (!fileInfo.found) {
return { code: 404, reason: 'Package not found' }
}

this.setHeaders(fileInfo, ctx)

ctx.response.status = 204

ctx.body = undefined
const meta: ResponseMeta = {
statusCode: 204,
}
this.updateMetaWithFileInfo(meta, fileInfo)

return true
return { meta }
}
async getPackage(paramPath: string, ctx: CTX): Promise<true | BadResponse> {
async getPackage(paramPath: string): Promise<{ meta: ResponseMeta; body: any } | BadResponse> {
const fileInfo = await this.getFileInfo(paramPath)

if (!fileInfo.found) {
return { code: 404, reason: 'Package not found' }
}

this.setHeaders(fileInfo, ctx)
const meta: ResponseMeta = {
statusCode: 200,
}
this.updateMetaWithFileInfo(meta, fileInfo)

const readStream = fs.createReadStream(fileInfo.fullPath)
ctx.body = readStream

return true
return { meta, body: readStream }
}
async postPackage(
paramPath: string,
ctx: CTXPost,
fileStreamOrText: string | Readable | undefined
): Promise<true | BadResponse> {
): Promise<{ meta: ResponseMeta; body: any } | BadResponse> {
const fullPath = path.join(this._basePath, paramPath)

await fsMkDir(path.dirname(fullPath), { recursive: true })
Expand All @@ -164,25 +161,27 @@ export class FileStorage extends Storage {
plainText = fileStreamOrText
}

const meta: ResponseMeta = {
statusCode: 200,
}

if (plainText) {
// store plain text into file
await fsWriteFile(fullPath, plainText)

ctx.body = { code: 201, message: `${exists ? 'Updated' : 'Inserted'} "${paramPath}"` }
ctx.response.status = 201
return true
meta.statusCode = 201
return { meta, body: { code: 201, message: `${exists ? 'Updated' : 'Inserted'} "${paramPath}"` } }
} else if (fileStreamOrText && typeof fileStreamOrText !== 'string') {
const fileStream = fileStreamOrText
await asyncPipe(fileStream, fs.createWriteStream(fullPath))

ctx.body = { code: 201, message: `${exists ? 'Updated' : 'Inserted'} "${paramPath}"` }
ctx.response.status = 201
return true
meta.statusCode = 201
return { meta, body: { code: 201, message: `${exists ? 'Updated' : 'Inserted'} "${paramPath}"` } }
} else {
return { code: 400, reason: 'No files provided' }
}
}
async deletePackage(paramPath: string, ctx: CTXPost): Promise<true | BadResponse> {
async deletePackage(paramPath: string): Promise<{ meta: ResponseMeta; body: any } | BadResponse> {
const fullPath = path.join(this._basePath, paramPath)

if (!(await this.exists(fullPath))) {
Expand All @@ -191,8 +190,11 @@ export class FileStorage extends Storage {

await fsUnlink(fullPath)

ctx.body = { message: `Deleted "${paramPath}"` }
return true
const meta: ResponseMeta = {
statusCode: 200,
}

return { meta, body: { message: `Deleted "${paramPath}"` } }
}

private async exists(fullPath: string) {
Expand Down Expand Up @@ -280,21 +282,23 @@ export class FileStorage extends Storage {
* @param {CTX} ctx
* @memberof FileStorage
*/
private setHeaders(info: FileInfo, ctx: CTX) {
ctx.type = info.mimeType
ctx.length = info.length
ctx.lastModified = info.lastModified
private updateMetaWithFileInfo(meta: ResponseMeta, info: FileInfo): void {
meta.type = info.mimeType
meta.length = info.length
meta.lastModified = info.lastModified

if (!meta.headers) meta.headers = {}

// Check the config. 0 or -1 means it's disabled:
if (this.config.httpServer.cleanFileAge >= 0) {
ctx.set(
'Expires',
FileStorage.calculateExpiresTimestamp(info.lastModified, this.config.httpServer.cleanFileAge)
meta.headers['Expires'] = FileStorage.calculateExpiresTimestamp(
info.lastModified,
this.config.httpServer.cleanFileAge
)
}
}
/**
* Calculate the expiration timestamp, given a starting Date point and timespan duration
* Calculate the expiration timestamp, given a starting Date point and time-span duration
*
* @private
* @static
Expand Down
Loading

0 comments on commit 09691ec

Please sign in to comment.