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 +}