node.default to serve websites
Massimo Melina committed
Dec 24, 2021 at 16:04 UTC
d407f3a64b4562e7d8f674e83681509cf8c77411
11 files changed
+57
-30
README.md
+3
@@ -16,6 +16,7 @@ You won't find all previous features here (yet), but still we got:
16
- search
17
- accounts
18
- resumable downloads
19
+- simple website serving
20
21
# Configuration
22
@@ -42,6 +43,8 @@ Valid keys in a node are:
43
- `perm`: specify who can see this.
44
Use this to limit access to this node.
45
Value is a dictionary, where the key is the username, and the value is `r`.
46
+- `mime`: specify what mime to use for this resource. Use "auto" for automatic detection.
47
+- `default`: to be used with a folder where you want to serve a default html. E.g.: "index.html". Using this will make `mime` default to "auto".
48
49
# Accounts
50
src/apis.ts
+19
-16
@@ -1,6 +1,5 @@
1
import Koa from 'koa'
2
import { vfs, VfsNode, walkNode } from './vfs'
3
-import { Stats } from 'fs'
3
import { stat } from 'fs/promises'
4
import _ from 'lodash'
5
import { getCurrentUser, verifyLogin } from './perm'
@@ -132,24 +131,28 @@ export const frontEndApis: ApiHandlers = {
131
132
async function nodeToDirEntry(node: VfsNode): Promise<DirEntry | null> {
133
try {
135
- return node.source?.includes('//') ? { n:node.name||'' }
136
- : node.source ? statToDirEntry(node.name, await stat(node.source))
137
- : node.name ? { n: node.name + '/' }
138
- : null
134
+ let { name, source, default:def } = node
135
+ if (source?.includes('//'))
136
+ return { n: name || source }
137
+ if (source) {
138
+ if (!name)
139
+ name = basename(source)
140
+ if (def)
141
+ return { n: name }
142
+ const st = await stat(source)
143
+ const folder = st.isDirectory()
144
+ const { ctime, mtime } = st
145
+ return {
146
+ n: name + (folder ? '/' : ''),
147
+ c: ctime,
148
+ m: Math.abs(+mtime-+ctime) < 1000 ? undefined : mtime,
149
+ s: folder ? undefined : st.size,
150
+ }
151
+ }
152
+ return name ? { n: name + '/' } : null
153
}
154
catch (err:any) {
155
console.error(String(err))
156
return null
157
}
158
}
145
-
146
-function statToDirEntry(name: string | undefined, stat:Stats) {
147
- const folder = stat.isDirectory()
148
- const { ctime, mtime } = stat
149
- return {
150
- n: name + (folder ? '/' : ''),
151
- c: ctime,
152
- m: Math.abs(+mtime-+ctime) < 1000 ? undefined : mtime,
153
- s: folder ? undefined : stat.size,
154
- }
155
-}
src/index.ts
+11
-4
@@ -41,14 +41,21 @@ srv.use(async (ctx, next) => {
41
return await next()
42
if (path.startsWith(FRONTEND_URI))
43
return await serveFrontendPrefixed(ctx,next)
44
- const node = await vfs.urlToNode(decodeURI(path), ctx)
44
+ const decoded = decodeURI(path)
45
+ const node = await vfs.urlToNode(decoded, ctx)
46
if (!node)
47
return await next()
48
const { source } = node
48
- if (!source || await isDirectory(source)) { // this folder was requested without the trailing /
49
+ if (!source || await isDirectory(source)) {
50
+ if (!path.endsWith('/')) // this folder was requested without the trailing /
51
+ return ctx.redirect(path + '/')
52
+ if (node.default) {
53
+ const def = await vfs.urlToNode(decoded + node.default, ctx)
54
+ if (def)
55
+ return serveFile(def)(ctx, next)
56
+ }
57
ctx.set({ server:'HFS '+BUILD_TIMESTAMP })
50
- return path.endsWith('/') ? await serveFrontend(ctx, next)
51
- : ctx.redirect(path + '/')
58
+ return await serveFrontend(ctx, next)
59
}
60
if (source)
61
return source.includes('//') ? mount(path,proxy(source,{}))(ctx,next)
src/misc.ts
+5
@@ -10,6 +10,11 @@ export async function isDirectory(path: string) {
10
catch(e) { return false }
11
}
12
13
+export async function isFile(path: string) {
14
+ try { return (await fs.stat(path)).isFile() }
15
+ catch(e) { return false }
16
+}
17
+
18
export function complySlashes(path: string) {
19
return path.replace(/\\/g,'/')
20
}
src/serveFile.ts
+4
-1
@@ -2,7 +2,8 @@ import Koa from 'koa'
2
import { createReadStream } from 'fs'
3
import fs from 'fs/promises'
4
import { METHOD_NOT_ALLOWED, NO_CONTENT } from './const'
5
-import { VfsNode } from './vfs'
5
+import { MIME_AUTO, VfsNode } from './vfs'
6
+import mimetypes from 'mime-types'
7
8
export function serveFile(node: VfsNode) : Koa.Middleware {
9
return async (ctx) => {
@@ -11,6 +12,8 @@ export function serveFile(node: VfsNode) : Koa.Middleware {
12
return
13
const { range } = ctx.request.header
14
ctx.set('Accept-Ranges', 'bytes')
15
+ if (mime === MIME_AUTO)
16
+ mime = mimetypes.lookup(source) || ''
17
if (mime)
18
ctx.type = mime
19
if (ctx.method === 'OPTIONS') {
src/vfs.ts
+4
-1
@@ -25,6 +25,7 @@ export interface VfsNode {
25
hidden?: boolean,
26
rename?: Record<string,string>,
27
perm?: Record<string, SinglePerm>,
28
+ default?: string,
29
mime?: string
30
}
31
@@ -32,6 +33,8 @@ type SinglePerm = 'r' | 'w'
33
34
const EMPTY = { type: VfsNodeType.root }
35
36
+export const MIME_AUTO = 'auto'
37
+
38
export class Vfs {
39
root: VfsNode = EMPTY
40
watcher?: FSWatcher
@@ -98,7 +101,7 @@ export class Vfs {
101
const baseSource = run.source+ '/'
102
const source = baseSource + relativeSource
103
const removed = isMatch(source, [run.remove].flat().map(x => baseSource + x))
101
- return removed || !await fs.stat(source) ? undefined : { source }
104
+ return removed || !await fs.stat(source) ? undefined : { source, mime: run.mime || (run.default && MIME_AUTO) }
105
}
106
return run
107
tests/index.html
deleted
-1
@@ -1 +0,0 @@
1
-<h1>This is a test</h1>
tests/page/index.html
new
+2
@@ -0,0 +1,2 @@
1
+<h1>This is a test</h1>
2
+<img src="gpl.png" alt="gpl logo"/>
tests/test.ts
+3
-2
@@ -10,16 +10,17 @@ const appStarted = new Promise(resolve =>
10
describe('basics', () => {
11
//before(async () => appStarted)
12
it('frontend', req('/', s => s.includes('<body>')))
13
- it('api.list', req('/~/api/file_list', res => inList(res, 'f2/') && inList(res, 'f3/'), {
13
+ it('api.list', req('/~/api/file_list', res => inList(res, 'f2/') && inList(res, 'page'), {
14
data: { path:'/f1/' }
15
}))
16
- it('api.search', req('/~/api/file_list', res => inList(res, 'f2/') && !inList(res, 'f3/'), {
16
+ it('api.search', req('/~/api/file_list', res => inList(res, 'f2/') && !inList(res, 'page'), {
17
data: { path:'f1', search:'2' }
18
}))
19
it('download', req('/f1/f2/alfa.txt', s => s.includes('abcd')))
20
it('partial download', req('/f1/f2/alfa.txt', s => s.includes('a') && !s.includes('d'), {
21
headers: { Range: 'bytes=0-2' }
22
}))
23
+ it('website', req('/f1/page/', s => s.includes('This is a test')))
24
it('missing perm', req('/for-rejetto/', 404))
25
it('proxy', req('/proxy', s => s.includes('github')))
26
})
todo.md
-2
@@ -1,6 +1,4 @@
1
# To do
2
-- vfs: serve an html for a folder?
3
- - "default" property for vfsNode?
2
- log file
3
- throttle speed
4
- frontend: dialogs
vfs.yaml
+6
-3
@@ -4,9 +4,12 @@ children:
4
- name: f2
5
children:
6
- source: tests/alfa.txt
7
- - name: f3
8
- mime: html
9
- source: tests/index.html
7
+ - name: pic
8
+ mime: png
9
+ source: tests/page/gpl.png
10
+ - name: page
11
+ default: index.html
12
+ source: tests/page
13
- name: proxy
14
source: https://raw.githubusercontent.com/nodejs/node/master/README.md
15
- name: for-rejetto