partial download

Massimo Melina committed Dec 16, 2021 at 19:23 UTC 9d4ab8ce8b358e7149edc07849e7ca102143b9a8
7 files changed +51 -16
dev-notes.md
+1
@@ -8,6 +8,7 @@
8 - https://www.tutorialspoint.com/koajs/koajs_quick_guide.htm
9
10 # To do
11 +- more tests
12 - permissions
13 - search
14 - vfs: serve an html for a folder?
src/apis.ts
+2 -2
@@ -13,8 +13,8 @@ export function apiMw(apis: ApiHandlers) : Koa.Middleware {
13 return async (ctx, next) => {
14 const params = ctx.request.body
15 console.debug('API', ctx.method, ctx.path, params)
16 - // @ts-ignore
17 - ctx.assert(ctx.path in apis, 404, 'invalid api')
16 + if (!(ctx.path in apis))
17 + return ctx.throw(404, 'invalid api')
18 const cb = (apis as any)[ctx.path]
19 const res = await cb(params, ctx)
20 if (res)
src/index.ts
+3 -3
@@ -4,7 +4,7 @@ import bodyParser from 'koa-bodyparser'
4 import { apiMw, frontEndApis } from './apis'
5 import { serveFrontend } from './frontend'
6 import { API_URI, argv, DEV, FRONTEND_URI } from './const'
7 -import { createReadStream } from 'fs'
7 +import { serveFile } from './serveFile'
8 import { vfs } from './vfs'
9 import { isDirectory } from './misc'
10 import proxy from 'koa-better-http-proxy'
@@ -37,8 +37,8 @@ srv.use(async (ctx, next) => {
37 return path.endsWith('/') ? await serveFrontend(ctx,next)
38 : ctx.redirect(ctx.path+'/')
39 if (source)
40 - return ctx.body = source.includes('//') ? mount(path,proxy(source,{}))(ctx,next)
41 - : createReadStream(source)
40 + return source.includes('//') ? mount(path,proxy(source,{}))(ctx,next)
41 + : serveFile(source)(ctx,next)
42 await next()
43 })
44
src/pbkdf2.ts
+5 -6
@@ -21,7 +21,7 @@ async function pbkdf2(password, iterations=1e6) {
21 const pwUtf8 = new TextEncoder().encode(password); // encode pw as UTF-8
22 const pwKey = await crypto.subtle.importKey('raw', pwUtf8, 'PBKDF2', false, ['deriveBits']); // create pw key
23
24 - const saltUint8 = crypto.getRandomValues(new Uint8Array(16)); // get random salt
24 + const saltUint8 = crypto.getRandomValues(new Uint8Array(16)); // get random salt
25
26 const params = { name: 'PBKDF2', hash: 'SHA-256', salt: saltUint8, iterations: iterations }; // pbkdf2 params
27 const keyBuffer = await crypto.subtle.deriveBits(params, pwKey, 256); // derive key
@@ -30,14 +30,13 @@ async function pbkdf2(password, iterations=1e6) {
30
31 const saltArray = Array.from(new Uint8Array(saltUint8)); // salt as byte array
32
33 - const iterHex = ('000000'+iterations.toString(16)).slice(-6); // iter’n count as hex
34 - const iterArray = iterHex.match(/.{2}/g).map(byte => parseInt(byte, 16)); // iter’ns as byte array
33 + const iterHex = ('000000'+iterations.toString(16)).slice(-6); // iter’n count as hex
34 + const iterArray = iterHex.match(/.{2}/g).map(byte => parseInt(byte, 16)); // iter’ns as byte array
35
36 const compositeArray = [].concat(saltArray, iterArray, keyArray); // combined array
37 const compositeStr = compositeArray.map(byte => String.fromCharCode(byte)).join(''); // combined as string
38 - const compositeBase64 = btoa('v01'+compositeStr); // encode as base64
39 -
40 - return compositeBase64; // return composite key
38 + // encode as base64
39 + return btoa('v01' + compositeStr); // return composite key
40 }
41
42
src/perm.ts
+3 -2
@@ -3,8 +3,9 @@ import fs from 'fs/promises'
3 import _ from 'lodash'
4 import yaml from 'yaml'
5 import { hashPassword } from './crypt'
6 +import { argv } from './const'
7
7 -const PATH = 'accounts.yaml'
8 +const PATH = argv.accounts || 'accounts.yaml'
9
10 interface UserDetails {
11 user: string, // we'll have user in it, so we don't need to pass it separately
@@ -33,7 +34,7 @@ async function load() {
34 file = await fs.readFile(PATH, 'utf8')
35 }
36 catch(e){
36 - console.warn('cannot read', PATH)
37 + console.warn('cannot read', PATH, e)
38 return
39 }
40 const res = yaml.parse(file)
src/serveFile.ts new
+31
@@ -0,0 +1,31 @@
1 +import Koa from 'koa'
2 +import { createReadStream } from 'fs'
3 +import fs from 'fs/promises'
4 +
5 +export function serveFile(source: string) : Koa.Middleware {
6 + return async (ctx) => {
7 + const { range } = ctx.request.header
8 + ctx.set('Accept-Ranges', 'bytes')
9 + if (!range)
10 + return ctx.body = createReadStream(source)
11 + const ranges = range.split('=')[1]
12 + if (ranges.includes(','))
13 + return ctx.throw(400, 'multi-range not supported')
14 + let bytes = ranges?.split('-')
15 + if (!bytes?.length)
16 + return ctx.throw(400, 'bad range')
17 + const stat = await fs.stat(source)
18 + const max = stat.size - 1
19 + let start = Number(bytes[0])
20 + let end = Number(bytes[1]) || max
21 + if (end > max || start > max) {
22 + ctx.status = 416
23 + ctx.set('Content-Range', `bytes ${stat.size}`)
24 + ctx.body = 'Requested Range Not Satisfiable'
25 + return
26 + }
27 + ctx.status = 206
28 + ctx.set('Content-Range', `bytes ${start}-${end}/${stat.size}`)
29 + ctx.body = createReadStream(source, { start, end })
30 + }
31 +}
\ No newline at end of file
src/vfs.ts
+6 -3
@@ -2,7 +2,7 @@ import { argv } from './const'
2 import yaml from 'yaml'
3 import fs from 'fs/promises'
4 import { FSWatcher, watch } from 'fs'
5 -import { basename } from 'path'
5 +import { dirname, basename } from 'path'
6 import { isMatch } from 'micromatch'
7 import { complySlashes, prefix } from './misc'
8 import { getCurrentUser } from './perm'
@@ -30,10 +30,13 @@ const EMPTY = { type: VfsNodeType.root }
30 export class Vfs {
31 root: VfsNode = EMPTY
32 watcher?: FSWatcher
33 + basePath: string
34
35 constructor(path?:string) {
36 + this.basePath = ''
37 if (path)
36 - this.load(path).then()
38 + this.load(path).then(()=>
39 + this.basePath = dirname(path))
40 else
41 this.reset()
42 }
@@ -51,7 +54,7 @@ export class Vfs {
54 console.debug('loaded')
55 }
56 catch(e) {
54 - console.error(`Load failed for ${path}`)
57 + console.error(`Load failed for ${path}`,e)
58 }
59 this.watcher?.close()
60 this.watcher = undefined