Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extended dir-list information #241

Merged
merged 10 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ Options: `html`, `json`

Directory list can be also in `html` format; in that case, `list.render` function is required.

You can override the option with URL parameter `format`. Options are `html` and `json`.

```bash
GET .../public/assets?format=json
```

will return the response as json independent of `list.format`.

**Example:**

```js
Expand Down Expand Up @@ -297,6 +305,79 @@ GET .../public/index
GET .../public/index.json
```

#### `list.extendedFolderInfo`

Default: `undefined`

If `true` some extended information for folders will be accessible in `list.render` and in the json response.

```js
render(dirs, files) {
const dir = dirs[0];
dir.fileCount // number of files in this folder
dir.totalFileCount // number of files in this folder (recursive)
dir.folderCount // number of folders in this folder
dir.totalFolderCount // number of folders in this folder (recursive)
dir.totalSize // size of all files in this folder (recursive)
dir.lastModified // most recent last modified timestamp of all files in this folder (recursive)
}
```

Warning: This will slightly decrease the performance, especially for deeply nested file structures.

#### `list.jsonFormat`

Default: `names`

Options: `names`, `extended`

This option determines the output format when `json` is selected.

`names`:
```json
{
"dirs": [
"dir1",
"dir2"
],
"files": [
"file1.txt",
"file2.txt"
]
}
```

`extended`:
```json
{
"dirs": [
{
"name": "dir1",
"stats": {
"dev": 2100,
"size": 4096,
...
},
"extendedInfo": {
"fileCount": 4,
"totalSize": 51233,
...
}
}
],
"files": [
{
"name": "file1.txt",
"stats": {
"dev": 2200,
"size": 554,
...
}
}
]
}
```

#### `preCompressed`

Default: `false`
Expand Down
15 changes: 15 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/// <reference types="node" />

import { FastifyPluginCallback, FastifyReply } from 'fastify';
import { Stats } from 'fs';

declare module "fastify" {
interface FastifyReply {
Expand All @@ -13,14 +14,26 @@ declare module "fastify" {
}
}

interface ExtendedInformation {
fileCount: number;
totalFileCount: number;
folderCount: number;
totalFolderCount: number;
totalSize: number;
lastModified: number;
}

interface ListDir {
href: string;
name: string;
stats: Stats;
extendedInfo?: ExtendedInformation;
}

interface ListFile {
href: string;
name: string;
stats: Stats;
}

interface ListRender {
Expand All @@ -31,6 +44,8 @@ interface ListOptions {
format: 'json' | 'html';
names: string[];
render: ListRender;
extendedFolderInfo?: boolean;
jsonFormat?: 'names' | 'extended';
}

// Passed on to `send`
Expand Down
137 changes: 98 additions & 39 deletions lib/dirList.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict'

const path = require('path')
const fs = require('fs')
const fs = require('fs').promises
const pLimit = require('p-limit')

const dirList = {
/**
Expand All @@ -10,36 +11,84 @@ const dirList = {
* @param {function(error, entries)} callback
* note: can't use glob because don't get error on non existing dir
*/
list: function (dir, callback) {
list: async function (dir, options) {
const entries = { dirs: [], files: [] }
fs.readdir(dir, (err, files) => {
if (err) {
return callback(err)
}
if (files.length < 1) {
callback(null, entries)
const files = await fs.readdir(dir)
if (files.length < 1) {
return entries
}

const limit = pLimit(4)
await Promise.all(files.map(filename => limit(async () => {
let stats
try {
stats = await fs.stat(path.join(dir, filename))
} catch (error) {
return
}
let j = 0
for (let i = 0; i < files.length; i++) {
const filename = files[i]
fs.stat(path.join(dir, filename), (err, file) => {
if (!err) {
if (file.isDirectory()) {
entries.dirs.push(filename)
} else {
entries.files.push(filename)
}
const entry = { name: filename, stats }
if (stats.isDirectory()) {
if (options.extendedFolderInfo) {
entry.extendedInfo = await getExtendedInfo(path.join(dir, filename))
}
entries.dirs.push(entry)
} else {
entries.files.push(entry)
}
})))

async function getExtendedInfo (folderPath) {
const depth = folderPath.split(path.sep).length
let totalSize = 0
let fileCount = 0
let totalFileCount = 0
let folderCount = 0
let totalFolderCount = 0
let lastModified = 0

async function walk (dir) {
const files = await fs.readdir(dir)
const limit = pLimit(4)
await Promise.all(files.map(filename => limit(async () => {
const filePath = path.join(dir, filename)
let stats
try {
stats = await fs.stat(filePath)
} catch (error) {
return
}

if (j++ >= files.length - 1) {
entries.dirs.sort()
entries.files.sort()
callback(null, entries)
if (stats.isDirectory()) {
totalFolderCount++
if (filePath.split(path.sep).length === depth + 1) {
folderCount++
}
await walk(filePath)
} else {
totalSize += stats.size
totalFileCount++
if (filePath.split(path.sep).length === depth + 1) {
fileCount++
}
lastModified = Math.max(lastModified, stats.mtimeMs)
}
})
})))
}
})

await walk(folderPath)
return {
totalSize,
fileCount,
totalFileCount,
folderCount,
totalFolderCount,
lastModified
}
}

entries.dirs.sort((a, b) => a.name.localeCompare(b.name))
entries.files.sort((a, b) => a.name.localeCompare(b.name))
return entries
},

/**
Expand All @@ -49,33 +98,43 @@ const dirList = {
* @param {ListOptions} options
* @param {string} route request route
*/
send: function ({ reply, dir, options, route }) {
dirList.list(dir, (err, entries) => {
if (err) {
reply.callNotFound()
return
}
send: async function ({ reply, dir, options, route }) {
let entries
try {
console.time('dsf')
entries = await dirList.list(dir, options)
console.timeEnd('dsf')
} catch (error) {
return reply.callNotFound()
}
const format = reply.request.query.format || options.format
if (format !== 'html') {
if (options.jsonFormat !== 'extended') {
const nameEntries = { dirs: [], files: [] }
entries.dirs.forEach(entry => nameEntries.dirs.push(entry.name))
entries.files.forEach(entry => nameEntries.files.push(entry.name))

if (options.format !== 'html') {
reply.send(nameEntries)
} else {
reply.send(entries)
return
}
return
}

const html = options.render(
entries.dirs.map(entry => dirList.htmlInfo(entry, route)),
entries.files.map(entry => dirList.htmlInfo(entry, route)))
reply.type('text/html').send(html)
})
const html = options.render(
entries.dirs.map(entry => dirList.htmlInfo(entry, route)),
entries.files.map(entry => dirList.htmlInfo(entry, route)))
reply.type('text/html').send(html)
},

/**
* provide the html information about entry and route, to get name and full route
* @param {string} entry file or dir name
* @param entry file or dir name and stats
* @param {string} route request route
* @return {ListFile}
*/
htmlInfo: function (entry, route) {
return { href: path.join(path.dirname(route), entry).replace(/\\/g, '/'), name: entry }
return { href: path.join(path.dirname(route), entry.name).replace(/\\/g, '/'), name: entry.name, stats: entry.stats, extendedInfo: entry.extendedInfo }
},

/**
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"encoding-negotiator": "^2.0.1",
"fastify-plugin": "^3.0.0",
"glob": "^7.1.4",
"p-limit": "^3.1.0",
"readable-stream": "^3.4.0",
"send": "^0.17.1"
},
Expand Down
Loading