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