Skip to content

Commit 09691ec

Browse files
authored
Merge pull request #191 from nrkno/fix/http-server-html-list
HTTP-server: Add '/list' endpoint
2 parents fb41089 + 73259d9 commit 09691ec

File tree

6 files changed

+182
-65
lines changed

6 files changed

+182
-65
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/packageVersion.ts

apps/http-server/packages/generic/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
77
"scripts": {
8-
"build": "yarn rimraf dist && yarn build:main",
8+
"build": "yarn rimraf dist && node scripts/prebuild.js && yarn build:main",
99
"build:main": "tsc -p tsconfig.json",
1010
"__test": "jest"
1111
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const fs = require('fs').promises
2+
3+
async function main() {
4+
const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8'))
5+
const libStr = `// ****** This file is generated at build-time by scripts/prebuild.js ******
6+
/**
7+
* The version of the package.json file
8+
*/
9+
export const PACKAGE_JSON_VERSION = '${packageJson.version}'
10+
`
11+
12+
await fs.writeFile('src/packageVersion.ts', libStr, 'utf8')
13+
}
14+
15+
main().catch((e) => {
16+
// eslint-disable-next-line no-console
17+
console.error(e)
18+
// eslint-disable-next-line no-process-exit
19+
process.exit(1)
20+
})

apps/http-server/packages/generic/src/server.ts

+78-17
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import cors from '@koa/cors'
88
import bodyParser from 'koa-bodyparser'
99

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

1618
const fsReadFile = promisify(fs.readFile)
1719

@@ -24,6 +26,8 @@ export class PackageProxyServer {
2426
private storage: Storage
2527
private logger: LoggerInstance
2628

29+
private startupTime = Date.now()
30+
2731
constructor(logger: LoggerInstance, private config: HTTPServerConfig) {
2832
this.logger = logger.category('PackageProxyServer')
2933
this.app.on('error', (err) => {
@@ -86,13 +90,16 @@ export class PackageProxyServer {
8690
})
8791

8892
this.router.get('/packages', async (ctx) => {
89-
await this.handleStorage(ctx, async () => this.storage.listPackages(ctx))
93+
await this.handleStorage(ctx, async () => this.storage.listPackages())
94+
})
95+
this.router.get('/list', async (ctx) => {
96+
await this.handleStorageHTMLList(ctx, async () => this.storage.listPackages())
9097
})
9198
this.router.get('/package/:path+', async (ctx) => {
92-
await this.handleStorage(ctx, async () => this.storage.getPackage(ctx.params.path, ctx))
99+
await this.handleStorage(ctx, async () => this.storage.getPackage(ctx.params.path))
93100
})
94101
this.router.head('/package/:path+', async (ctx) => {
95-
await this.handleStorage(ctx, async () => this.storage.headPackage(ctx.params.path, ctx))
102+
await this.handleStorage(ctx, async () => this.storage.headPackage(ctx.params.path))
96103
})
97104
this.router.post('/package/:path+', async (ctx) => {
98105
this.logger.debug(`POST ${ctx.request.URL}`)
@@ -118,22 +125,17 @@ export class PackageProxyServer {
118125
})
119126
this.router.delete('/package/:path+', async (ctx) => {
120127
this.logger.debug(`DELETE ${ctx.request.URL}`)
121-
await this.handleStorage(ctx, async () => this.storage.deletePackage(ctx.params.path, ctx))
128+
await this.handleStorage(ctx, async () => this.storage.deletePackage(ctx.params.path))
122129
})
123130

124131
// Convenient pages:
125132
this.router.get('/', async (ctx) => {
126-
let packageJson = { version: '0.0.0' }
127-
try {
128-
packageJson = JSON.parse(
129-
await fsReadFile('../package.json', {
130-
encoding: 'utf8',
131-
})
132-
)
133-
} catch (err) {
134-
// ignore
133+
ctx.body = {
134+
name: 'Package proxy server',
135+
version: PACKAGE_JSON_VERSION,
136+
uptime: Date.now() - this.startupTime,
137+
info: this.storage.getInfo(),
135138
}
136-
ctx.body = { name: 'Package proxy server', version: packageJson.version, info: this.storage.getInfo() }
137139
})
138140
this.router.get('/uploadForm/:path+', async (ctx) => {
139141
// ctx.response.status = result.code
@@ -165,12 +167,71 @@ export class PackageProxyServer {
165167
}
166168
})
167169
}
168-
private async handleStorage(ctx: CTX, storageFcn: () => Promise<true | BadResponse>) {
170+
private async handleStorage(ctx: CTX, storageFcn: () => Promise<{ meta: ResponseMeta; body?: any } | BadResponse>) {
169171
try {
170172
const result = await storageFcn()
171-
if (result !== true) {
173+
if (isBadResponse(result)) {
172174
ctx.response.status = result.code
173175
ctx.body = result.reason
176+
} else {
177+
ctx.response.status = result.meta.statusCode
178+
if (result.meta.type !== undefined) ctx.type = result.meta.type
179+
if (result.meta.length !== undefined) ctx.length = result.meta.length
180+
if (result.meta.lastModified !== undefined) ctx.lastModified = result.meta.lastModified
181+
182+
if (result.meta.headers) {
183+
for (const [key, value] of Object.entries<string>(result.meta.headers)) {
184+
ctx.set(key, value)
185+
}
186+
}
187+
188+
if (result.body) ctx.body = result.body
189+
}
190+
} catch (err) {
191+
this.logger.error(`Error in handleStorage: ${stringifyError(err)} `)
192+
ctx.response.status = 500
193+
ctx.body = 'Internal server error'
194+
}
195+
}
196+
private async handleStorageHTMLList(
197+
ctx: CTX,
198+
storageFcn: () => Promise<{ body: { packages: PackageInfo[] } } | BadResponse>
199+
) {
200+
try {
201+
const result = await storageFcn()
202+
if (isBadResponse(result)) {
203+
ctx.response.status = result.code
204+
ctx.body = result.reason
205+
} else {
206+
const packages = result.body.packages
207+
208+
ctx.set('Content-Type', 'text/html')
209+
ctx.body = `<!DOCTYPE html>
210+
<html>
211+
<head>
212+
<style>
213+
body { font-family: Arial, sans-serif; }
214+
table { border-collapse: collapse; width: 100%; }
215+
th, td { border: 1px solid #ddd; padding: 8px; }
216+
217+
</style>
218+
</head>
219+
<body>
220+
<h1>Packages</h1>
221+
<table>
222+
${packages
223+
.map(
224+
(pkg) =>
225+
`<tr>
226+
<td><a href="package/${pkg.path}">${pkg.path}</a></td>
227+
<td>${pkg.size}</td>
228+
<td>${pkg.modified}</td>
229+
</tr>`
230+
)
231+
.join('')}
232+
</table>
233+
</body>
234+
</html>`
174235
}
175236
} catch (err) {
176237
this.logger.error(`Error in handleStorage: ${stringifyError(err)} `)

apps/http-server/packages/generic/src/storage/fileStorage.ts

+45-41
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import path from 'path'
33
import { promisify } from 'util'
44
import mime from 'mime-types'
55
import prettyBytes from 'pretty-bytes'
6-
import { asyncPipe, CTX, CTXPost } from '../lib'
6+
import { asyncPipe, CTXPost } from '../lib'
77
import { HTTPServerConfig, LoggerInstance } from '@sofie-package-manager/api'
8-
import { BadResponse, Storage } from './storage'
8+
import { BadResponse, PackageInfo, ResponseMeta, Storage } from './storage'
99
import { Readable } from 'stream'
1010

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

4141
getInfo(): string {
42-
return this._basePath
42+
return `basePath: "${this._basePath}", cleanFileAge: ${this.config.httpServer.cleanFileAge}`
4343
}
4444

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

49-
async listPackages(ctx: CTX): Promise<true | BadResponse> {
50-
type PackageInfo = {
51-
path: string
52-
size: string
53-
modified: string
54-
}
49+
async listPackages(): Promise<{ meta: ResponseMeta; body: { packages: PackageInfo[] } } | BadResponse> {
5550
const packages: PackageInfo[] = []
5651

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

87-
ctx.body = { packages: packages }
82+
const meta: ResponseMeta = {
83+
statusCode: 200,
84+
}
8885

89-
return true
86+
return { meta, body: { packages } }
9087
}
9188
private async getFileInfo(paramPath: string): Promise<
9289
| {
@@ -118,40 +115,40 @@ export class FileStorage extends Storage {
118115
lastModified: stat.mtime,
119116
}
120117
}
121-
async headPackage(paramPath: string, ctx: CTX): Promise<true | BadResponse> {
118+
async headPackage(paramPath: string): Promise<{ meta: ResponseMeta } | BadResponse> {
122119
const fileInfo = await this.getFileInfo(paramPath)
123120

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

128-
this.setHeaders(fileInfo, ctx)
129-
130-
ctx.response.status = 204
131-
132-
ctx.body = undefined
125+
const meta: ResponseMeta = {
126+
statusCode: 204,
127+
}
128+
this.updateMetaWithFileInfo(meta, fileInfo)
133129

134-
return true
130+
return { meta }
135131
}
136-
async getPackage(paramPath: string, ctx: CTX): Promise<true | BadResponse> {
132+
async getPackage(paramPath: string): Promise<{ meta: ResponseMeta; body: any } | BadResponse> {
137133
const fileInfo = await this.getFileInfo(paramPath)
138134

139135
if (!fileInfo.found) {
140136
return { code: 404, reason: 'Package not found' }
141137
}
142-
143-
this.setHeaders(fileInfo, ctx)
138+
const meta: ResponseMeta = {
139+
statusCode: 200,
140+
}
141+
this.updateMetaWithFileInfo(meta, fileInfo)
144142

145143
const readStream = fs.createReadStream(fileInfo.fullPath)
146-
ctx.body = readStream
147144

148-
return true
145+
return { meta, body: readStream }
149146
}
150147
async postPackage(
151148
paramPath: string,
152149
ctx: CTXPost,
153150
fileStreamOrText: string | Readable | undefined
154-
): Promise<true | BadResponse> {
151+
): Promise<{ meta: ResponseMeta; body: any } | BadResponse> {
155152
const fullPath = path.join(this._basePath, paramPath)
156153

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

164+
const meta: ResponseMeta = {
165+
statusCode: 200,
166+
}
167+
167168
if (plainText) {
168169
// store plain text into file
169170
await fsWriteFile(fullPath, plainText)
170171

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

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

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

192191
await fsUnlink(fullPath)
193192

194-
ctx.body = { message: `Deleted "${paramPath}"` }
195-
return true
193+
const meta: ResponseMeta = {
194+
statusCode: 200,
195+
}
196+
197+
return { meta, body: { message: `Deleted "${paramPath}"` } }
196198
}
197199

198200
private async exists(fullPath: string) {
@@ -280,21 +282,23 @@ export class FileStorage extends Storage {
280282
* @param {CTX} ctx
281283
* @memberof FileStorage
282284
*/
283-
private setHeaders(info: FileInfo, ctx: CTX) {
284-
ctx.type = info.mimeType
285-
ctx.length = info.length
286-
ctx.lastModified = info.lastModified
285+
private updateMetaWithFileInfo(meta: ResponseMeta, info: FileInfo): void {
286+
meta.type = info.mimeType
287+
meta.length = info.length
288+
meta.lastModified = info.lastModified
289+
290+
if (!meta.headers) meta.headers = {}
287291

288292
// Check the config. 0 or -1 means it's disabled:
289293
if (this.config.httpServer.cleanFileAge >= 0) {
290-
ctx.set(
291-
'Expires',
292-
FileStorage.calculateExpiresTimestamp(info.lastModified, this.config.httpServer.cleanFileAge)
294+
meta.headers['Expires'] = FileStorage.calculateExpiresTimestamp(
295+
info.lastModified,
296+
this.config.httpServer.cleanFileAge
293297
)
294298
}
295299
}
296300
/**
297-
* Calculate the expiration timestamp, given a starting Date point and timespan duration
301+
* Calculate the expiration timestamp, given a starting Date point and time-span duration
298302
*
299303
* @private
300304
* @static

0 commit comments

Comments
 (0)