support descript.ion file
Massimo Melina committed
Sep 22, 2023 at 01:02 UTC
d6155d604c21b1e4e69279ba1ad4743753b7aadf
6 files changed
+68
-5
README.md
+2
@@ -26,6 +26,7 @@ This is a full rewrite of [the Delphi version](https://github.com/rejetto/hfs2).
26
## Features
27
28
- https
29
+- easy certificate generation
30
- unicode
31
- virtual file system
32
- mobile friendly
@@ -44,6 +45,7 @@ This is a full rewrite of [the Delphi version](https://github.com/rejetto/hfs2).
45
- virtual hosting (plug-in)
46
- anti-brute-force (plug-in)
47
- [reverse-proxy support](https://github.com/rejetto/hfs/wiki/Reverse-proxy)
48
+- comments in file descript.ion
49
50
## Installation
51
frontend/src/BrowseFiles.ts
+1
@@ -214,6 +214,7 @@ const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
214
) : h('a', { href: uri, onClick }, ico, entry.name),
215
),
216
h(CustomCode, { name: 'afterEntryName', props: { entry } }),
217
+ entry.comment && h('div', { className: 'entry-comment' }, entry.comment),
218
h('div', { className: 'entry-panel' },
219
h(EntryDetails, { entry, midnight }),
220
showingButton && h('button', { className: 'file-menu-button', onClick: fileMenu }, hIcon('menu')),
frontend/src/index.scss
+20
@@ -281,6 +281,26 @@ ul.dir {
281
}
282
}
283
}
284
+ .entry-comment {
285
+ display: inline;
286
+ &::before, &::after {
287
+ font-size: 1.5em;
288
+ font-family: serif;
289
+ line-height: 1px;
290
+ position: relative;
291
+ }
292
+ &::before {
293
+ content: open-quote;
294
+ margin-left: 0.5em;
295
+ margin-right: 0.1em;
296
+ top: 0.2em;
297
+ }
298
+ &::after {
299
+ content: "„";
300
+ top: -0.1em;
301
+ margin-left: 0.1em;
302
+ }
303
+ }
304
.entry-panel {
305
float: right;
306
padding-top: 0.3em;
frontend/src/state.ts
+1
@@ -87,6 +87,7 @@ export class DirEntry {
87
public readonly c?: string
88
public readonly p?: string
89
public readonly icon?: string
90
+ public readonly comment?: string
91
// we memoize these value for speed
92
public readonly name: string
93
public readonly uri: string
src/api.file_list.ts
+29
-3
@@ -15,12 +15,17 @@ import {
15
import { ApiError, ApiHandler, SendListReadable } from './apiMiddleware'
16
import { stat } from 'fs/promises'
17
import { mapPlugins } from './plugins'
18
-import { asyncGeneratorToArray, dirTraversal, pattern2filter } from './misc'
18
+import { asyncGeneratorToArray, basename, dirTraversal, parseFile, pattern2filter } from './misc'
19
import _ from 'lodash'
20
import { HTTP_FOOL, HTTP_METHOD_NOT_ALLOWED, HTTP_NOT_FOUND } from './const'
21
import Koa from 'koa'
22
+import { defineConfig } from './config'
23
+import { dirname, join } from 'path'
24
23
-export interface DirEntry { n:string, s?:number, m?:Date, c?:Date, p?: string }
25
+const DESCRIPT_ION = 'descript.ion'
26
+const descriptIon = defineConfig('descript_ion', true)
27
+
28
+export interface DirEntry { n:string, s?:number, m?:Date, c?:Date, p?: string, comment?: string }
29
30
export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c }, ctx) => {
31
const node = await urlToNode(uri || '/', ctx)
@@ -67,11 +72,15 @@ export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c
72
async function* produceEntries() {
73
for await (const sub of walker) {
74
if (ctx.aborted) break
70
- if (!filter(getNodeName(sub)))
75
+ const name = getNodeName(sub)
76
+ if (descriptIon.get() && name === DESCRIPT_ION)
77
+ continue
78
+ if (!filter(name))
79
continue
80
const entry = await nodeToDirEntry(ctx, sub)
81
if (!entry)
82
continue
83
+ entry.comment = await getCommentFor(sub.source)
84
const cbParams = { entry, ctx, listUri: uri, node: sub }
85
try {
86
const res = await Promise.all(onDirEntryHandlers.map(cb => cb(cbParams)))
@@ -134,3 +143,20 @@ export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c
143
}
144
}
145
}
146
+
147
+async function getCommentFor(path?: string) {
148
+ return !path || !descriptIon.get() ? undefined
149
+ : readDescription(dirname(path)).then(x => x.get(basename(path)), () => undefined)
150
+}
151
+
152
+function readDescription(path: string) {
153
+ return parseFile(join(path, DESCRIPT_ION), txt => new Map(txt.split('\n').map(line => {
154
+ const quoted = line[0] === '"' ? 1 : 0
155
+ const i = quoted ? line.indexOf('"', 2) : line.indexOf(' ')
156
+ const fn = line.slice(quoted, i - quoted)
157
+ let comment = line.slice(i + 1)
158
+ if (comment.endsWith('\x04\xc2'))
159
+ comment = comment.slice(0, -2).replaceAll('\\n', '\n')
160
+ return [fn, comment]
161
+ })))
162
+}
\ No newline at end of file
src/util-files.ts
+15
-2
@@ -1,6 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import fs from 'fs/promises'
3
+import fs, { readFile, stat } from 'fs/promises'
4
import { Promisable, try_, tryJson, wait, isWindowsDrive } from './misc'
5
import { promisify } from 'util'
6
import { createWriteStream, mkdirSync, watch } from 'fs'
@@ -166,4 +166,17 @@ export function storeFileAttr(path: string, k: string, v: any) {
166
export async function loadFileAttr(path: string, k: string) {
167
return tryJson(String(await promisify(fsx.get)(path, FILE_ATTR_PREFIX + k)))
168
?? undefined // normalize, as we get null instead of undefined on windows
169
-}
\ No newline at end of file
169
+}
170
+
171
+// read and parse a file, caching unless timestamp has changed
172
+const cache = new Map<string, { ts: Date, parsed: unknown }>()
173
+export async function parseFile<T>(path: string, parse: (raw: string) => T) {
174
+ const { mtime: ts } = await stat(path)
175
+ const cached = cache.get(path)
176
+ if (ts === cached?.ts)
177
+ return cached.parsed as T
178
+ const raw = await readFile(path, 'utf8')
179
+ const parsed = parse(raw)
180
+ cache.set(path, { ts, parsed })
181
+ return parsed
182
+}