accounts path in config

Massimo Melina committed Jan 3, 2022 at 17:42 UTC c2ddc5febe974e448c97f4c3231215ed9eef951d
5 files changed +52 -49
README.md
+2
@@ -29,6 +29,8 @@ When not specified, default values will be used.
29 Supported entries are:
30 - `port` where to accept http connections. Default is 80.
31 - `vfs` the files and folders you want to expose. For details see the dedicated following section.
32 +- `log` path of the log file. Default is `access.log`.
33 +- `accounts` path of the accounts file. Default is `accounts.yaml`.
34 - `mime` command what mime-type to be returned with some files.
35 E.g.: `"*.jpg": image/jpeg`
36 You can specify multiple entries, or separate multiple file masks with a p|pe.
config.yaml
-1
@@ -1,7 +1,6 @@
1 port: 80
2 mime:
3 "*.jpg|*.png|*.mp3|*.txt": auto
4 -log: access.log
4 vfs:
5 children:
6 - name: f1
src/misc.ts
+11 -2
@@ -1,6 +1,6 @@
1 import fs from 'fs/promises'
2 import { objSameKeys } from './obj'
3 -import { watch } from 'fs'
3 +import { FSWatcher, watch } from 'fs'
4 import yaml from 'yaml'
5
6 export function enforceFinal(sub:string, s:string) {
@@ -46,17 +46,26 @@ export function wantArray(x:any) {
46 return x == null ? [] : Array.isArray(x) ? x : [x]
47 }
48
49 +// return canceler
50 export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>) {
51 let doing = false
52 + let watcher: FSWatcher
53 const timer = setInterval(()=>{
54 try {
53 - watch(path, load)
55 + watcher = watch(path, load)
56 load().then()
57 clearInterval(timer)
58 }
59 catch(e){
60 }
61 }, 1000)
62 + let running = true
63 + return () => {
64 + if (!running) return
65 + running = false
66 + clearInterval(timer)
67 + watcher?.close()
68 + }
69
70 async function load(){
71 if (doing) return
src/perm.ts
+38 -44
@@ -1,13 +1,12 @@
1 -import { watch } from 'fs'
1 import fs from 'fs/promises'
2 import _ from 'lodash'
3 import yaml from 'yaml'
4 import { hashPassword, verifyPassword } from './crypt'
6 -import { argv } from './const'
7 -import { readFileBusy, setHidden, wantArray } from './misc'
5 +import { setHidden, wantArray, watchLoad } from './misc'
6 import Koa from 'koa'
7 +import { subscribeConfig } from './config'
8
10 -const PATH = argv.accounts || 'accounts.yaml'
9 +let path = ''
10
11 interface Account {
12 user: string, // we'll have user in it, so we don't need to pass it separately
@@ -60,47 +59,42 @@ export async function updateAccount(username: string, changer:Changer) {
59 }
60
61 const saveAccountsAsap = _.debounce(() =>
63 - fs.writeFile(PATH, yaml.stringify({ accounts })).catch(err =>
62 + fs.writeFile(path, yaml.stringify({ accounts })).catch(err =>
63 console.error('Failed at saving accounts file, please ensure it is writable.', String(err))))
64
66 -let doing = false
67 -load().then()
68 -try { watch(PATH, load) } // find a better way to handle missing file
69 -catch(e){}
70 -async function load() {
71 - if (doing) return
72 - doing = true
73 - try {
74 - console.debug('loading', PATH)
75 - let res
76 - try {
77 - res = yaml.parse(await readFileBusy(PATH))
78 - }
79 - catch(e){
80 - console.warn('cannot read', PATH, e)
81 - return
65 +let watcher: undefined | (()=>void)
66 +subscribeConfig({ k:'accounts', defaultValue:'accounts.yaml' }, v => {
67 + watcher?.()
68 + if (!v)
69 + return applyAccounts({})
70 + if (typeof v !== 'string')
71 + return console.error('bad type for accounts')
72 + watcher = watchLoad(path = v, async data => {
73 + const a = data?.accounts
74 + if (!a)
75 + return console.error('accounts file must contain "accounts" key')
76 + await applyAccounts(a)
77 + })
78 +})
79 +
80 +async function applyAccounts(newAccounts:Accounts) {
81 + // we should validate content here
82 + accounts = newAccounts
83 + let changed = false
84 + await Promise.all(_.map(newAccounts, async (rec,k) => {
85 + if (!rec) // an empty object in yaml is stored as null
86 + rec = accounts[k] = { user: '' }
87 + setHidden(rec, { user: k })
88 + rec.belongs = wantArray(rec.belongs).filter(b =>
89 + b in accounts // at this stage the group record may still be null if specified later in the file
90 + || console.error(`user ${k} belongs to non-existing ${b}`) )
91 + if (rec.password) {
92 + rec.hashedPassword = await hashPassword(rec.password)
93 + delete rec.password
94 + changed = true
95 + console.debug('hashing password for', k)
96 }
83 - // we should validate content here
84 - if (!res?.accounts)
85 - return accounts = {}
86 - accounts = res.accounts
87 - let changed = false
88 - await Promise.all(_.map(accounts, async (rec,k) => {
89 - if (!rec) // an empty object in yaml is stored as null
90 - rec = accounts[k] = { user: '' }
91 - setHidden(rec, { user: k })
92 - rec.belongs = wantArray(rec.belongs).filter(b =>
93 - b in accounts // at this stage the group record may still be null if specified later in the file
94 - || console.error(`user ${k} belongs to non-existing ${b}`) )
95 - if (rec.password) {
96 - rec.hashedPassword = await hashPassword(rec.password)
97 - delete rec.password
98 - changed = true
99 - console.debug('hashing password for', k)
100 - }
101 - }))
102 - if (changed)
103 - await saveAccountsAsap()
104 - }
105 - finally { doing = false }
97 + }))
98 + if (changed)
99 + await saveAccountsAsap()
100 }
todo.md
+1 -2
@@ -2,7 +2,6 @@
2 - anti-csrf
3 - upload
4 - search and login dialogs should push to history so that mobile can use back button to close them
5 -- accounts path in config
5 - node.comment
6 - config: max speed (total/per-ip)
7 - config: max connections (total/per-ip)
@@ -10,7 +9,7 @@
9 - user.redirect
10 - config: bans
11 - config: min disk space
13 -- link to parent folder in the list
12 +- link to parent folder in the list (as an option of the frontend?)
13 - archive for search results
14 - archive only selected files
15 - https