HFS is a file server offering a virtual file system (vfs). You can easily share a single file instead of the whole folder, or you can rename it, but without touching the real file, just virtually.
Listing files, searching files, zipping folders, it's all very fast, streamed while data is produced, so you don't have to wait.
This project is in an early stage and distribution will be made easier.
This is a full rewrite of the Delphi version. You won't find all previous features here (yet), but still we got:
- https
- unicode
- virtual file system
- mobile friendly front-end
- search
- accounts
- resumable downloads
- download folders as zip archive
- simple website serving
- plug-ins
- log file
- speed throttler
- admin web interface
As you can see from the list above, we already have some goods that you can't find in HFS 2
- https
- fully supports unicode
- more robust
- plugins system
- ZIP format for archives instead of TAR
- more flexible permissions
- smaller
- more tested
- easier to configure (not sure about this anymore)
- go to https://github.com/rejetto/hfs/releases
- click on
Assets - download the right version for your computer
- run the file
If your system is not covered, you can try this alternative version:
- install node.js version 16+ from https://nodejs.org/
- download and unzip
hfs-node.zip - chmod +x run
- launch
./run
To install a plugin you just copy its folder inside plugins folder.
Delete it to uninstall.
HFS will ignore all folders with -disabled at the end of the name.
- Install Node.js 16+
- Install Typescript: launch
npm -g i typescript - Launch
npm run build-allin the root
You'll see some warnings about vulnerabilities. Fear not, for those are in the dev tools we are using.
If you want to be assured, run npm audit --production that will exclude dev stuff, and you should see something
more reassuring, like "found 0 vulnerabilities", hopefully.
One way of working on sources here is to npm run watch-server.
This will give you auto-restarting of the server on back-end changes.
Set an env DEV=1 to let the code know we are in a dev environment.
If you want to work on the frontend and admin too, you should first
- set an env
FRONTEND_PROXY=3005 npm run start-frontend- set an env
ADMIN_PROXY=3006 npm run start-admin
Having this env-s will make the server get all related stuff from the other dev servers.
Otherwise, you should be sure that frontend and admin have been built, and its files are ready to be used in dist folder.
In this latter case, the DEV=1 you set before will make the server get the files from inside the dist folder.
A plug-in is a folder with a plugin.js file in it.
Plug-ins can be hot-swapped, and at some extent can be edited without restarting the server.
Each plug-in has access to the same set of features. Normally you'll have a plug-in that's a theme, and another that's a firewall, but nothing is preventing a single plug-in from doing both tasks.
plugin.js is a javascript module that exports an init function like this:
exports.init = api => ({
frontend_css: 'mystyle.css'
})The init function is called when the module is loaded and should return an object with things to customize.
In the example above we are asking a css file to be loaded in the frontend.
The parameter api object contains some useful things we'll see later.
You can decide to return things in the init function, or directly in the exports.
If you need to access the api you must use init, otherwise you can go directly with exports.
Let's first look at the things you can return:
-
description: stringtry to explain what this plugin is for. This must go inexportsand use "double quotes". -
version: numberuse progressive numbers to distinguish each release. This must go inexports. -
frontend_css: string | string[]path to one or more css files that you want the frontend to load. These are to be placed in thepublicfolder (refer below). -
frontend_js: string | string[]path to one or more js files that you want the frontend to load. These are to be placed in thepublicfolder (refer below). -
middleware: (Context) => void | true | functiona function that will be used as a middleware: it can interfere with http activity.To know what the Context object contains please refer to Koa's documentation. You don't get the
nextparameter as in standard Koa's middlewares because this is different, but we are now explaining how to achieve the same results. To interrupt other middlewares on this http request, returntrue. If you want to execute something in the "upstream" of middlewares, return a function. -
unload: functioncalled when unloading a plugin. This is a good place for example to clearInterval(). -
onDirEntry: ({ entry: DirEntry, listPath: string }) => void | falseby providing this callback you can manipulate the record that is sent to the frontend (entry), or you can return false to exclude this entry from the results. -
config: { [key]: FieldDescriptor }declare a set of admin-configurable values owned by the plugin that will be displayed inside Admin panel for change. Each property is identified by its key, and the descriptor is another object with options about the field. A simple empty object{}is a text field.Eg: you want a
messagetext. You add this to yourplugin.js:exports.config = { message: {} }
Once the admin has chosen a value for it, the value will be saved in the main config file, under the
plugins_configproperty.plugins_config: name_of_the_plugin: message: Hi there!
When necessary your plugin will read its value using
api.getConfig('message').
Currently, these properties are supported:
type: 'string' | 'number' | 'boolean' | 'select' | 'multiselect'. Default isstring.label: stringwhat name to display next to the field. Default is based onkey.helperText: stringextra text printed next to the field.
Based on type, other properties are supported:
stringmultiline: boolean. Default isfalse.
numbermin: numbermax: number
selectoptions: { [label]: AnyJsonValue }
multiselectit's likeselectbut its result is an array of values.
The api object you get as parameter of the init contains the following:
-
require: functionuse this instead of standardrequirefunction to access modules already loaded by HFS. -
getConfig(key: string): anyget config's value set up by usingexports.config. -
const: objectall constants of theconst.tsfile are exposed here. E.g. BUILD_TIMESTAMP, API_VERSION, etc. -
getConnections: Connections[]retrieve current list of active connections. -
events: EventEmitterthis is the main events emitter used by HFS. -
srcDir: stringthis can be useful if you need to import some extra function not available inapi.exports.init = api => { const { watchLoad } = api.require(api.srcDir + '/watchLoad') }
You should try to keep this kind of behavior at its minimum, as name of sources and of elements in them are subject to change. If you need something for your plugin that's not covered by
api, you can test it with this method, but you should then discuss it on the forum because an addition toapiis your best option for making a future-proof plugin.
Each plug-in can have a public folder, and its files will be accessible at /~/plugins/PLUGIN_NAME/FILENAME.
The following information applies to the default front-end, and may not apply to a custom one.
Once your script is loaded into the frontend (via frontend_js), you will have access to the HFS object in the global scope.
There you'll find HFS.onEvent function that is the base of communication.
onEvent(eventName:string, callback: (object) => any) your callback will be called on the specified event.
Depending on the event you'll have an object with parameters in it, and may return some output. Refer to the specific event for further information.
This is a list of available frontend events, with respective parameters and output.
additionalEntryProps-
you receive each entry of the list, and optionally produce HTML code that will be added in the
entry-propscontainer. -
parameters
{ entry: Entry }The
Entrytype is an object with the following properties:n: stringname of the entry, including relative path in some cases.s?: numbersize of the entry, in bytes. It may be missing, for example for folders.t?: Dategeneric timestamp, combination of creation-time and modified-time.c?: Datecreation-time.m?: Datemodified-time.
-
output
string | void
-
General configuration is read by default from file config.yaml.
When not specified, default values will be used.
Supported entries are:
portwhere to accept http connections. Default is 80.vfsthe files and folders you want to expose. For details see the dedicated following section.logpath of the log file. Default isaccess.log.log_rotationfrequency of log rotation. Accepted values aredaily,weekly,monthly, or empty string to disable. Default isweekly.error_logpath of the log file for errors. Default iserror.log.errors_in_main_logif you want to use a single file for both kind of entries. Default is false.accountspath of the accounts file. Default isaccounts.yaml.mimecommand what mime-type to be returned with some files. E.g.:"*.jpg": image/jpegYou can specify multiple entries, or separate multiple file masks with a p|pe. You can use the special valueautoto attempt automatic detection.max_kbpsthrottle output speed. Default is Infinity.max_kbps_per_ipthrottle output speed on a per-ip basis. Default is Infinity.zip_calculate_size_for_secondshow long should we wait before the zip archive starts streaming, trying to understand its finale size. Default is 1.open_browser_at_startshould HFS open browser on localhost on start? Default is true.https_portlisten on a specific port. Default is 443.certuse this file for https certificate. Minimum to start https is to give a cert and a private_key. Default is none.private_keyuse this file for https private key. Default is none.allowed_refereryou can decide what domains can link to your files. Wildcards supported. Default is any.blocka list of rules that will block connections. E.g.:Syntax supports, other than simple address,block: - ip: 192.168.0.90*as wildcard and CIDR format.plugins_configthis is a generic place where you can find/put configuration for each plugin, at least those that need configuration.enable_pluginsif a plugin is not present here, it won't run. Defaults is[ antibrute ].custom_headerprovide HTML code to be put at the top of your Frontend. Default is none.localhost_adminshould Admin be accessed without credentials when on localhost. Default is true.proxiesnumber of proxies between server and clients to be trusted about providing clients' IP addresses. Default is 0.
The virtual file system is a tree of files and folders, collectively called nodes. By default, a node is a folder, unless you provide for it a source that's a file. Valid keys in a node are:
name: this is the name we'll use to display this file/folder. If not provided, HFS will infer it from the source. At leastnameorsourcemust be provided.source: absolute or relative path of where to get the contentchildren: just for folders, specify its virtual children. Value is a list and its entries are nodes.rename: similar to name, but it's from the parent node point. Use this to change the name of entries that are read from the source, not listed in the VFS. Value is a dictionary, where the key is the original name.mime: specify what mime to use for this resource. Use "auto" for automatic detection.default: to be used with a folder where you want to serve a default html. E.g.: "index.html". Using this will makemimedefault to "auto".can_read: specify who can download this entry. Value is aWhoCandescriptor, which is one of these valuestrue: anyone can, even people who didn't log in. This is normally the default value.false: no one can."*": any account can, i.e. anyone who logged in.[ frank, peter ]: the list of accounts who can.
can_see: specify who can see this entry. Even if a user can download you can still make the file not appear in the list. Remember that to see in the list you must also be able to download, or else you won't see it anyway. Value is aWhoCandescriptor, refer above.masks: maps a file mask to a set of properties as the one documented in this section. E.g.masks: "**/*.mp3": can_read: false "*.jpg|*.png": mime: auto
Accounts are kept in accounts.yaml if any, or you can decide another file by passing parameter --accounts.
Inside the file, all accounts should go under accounts: key, as a dictionary where the key is the username.
E.g.
accounts:
admin:
password: hello123
belongs: group1
guest:
password: guest
group1:
As soon as the file is read HFS will encrypt passwords in a non-reversible way. It means that password property is replaced with an encrypted property: srp.
For this reason HFS needs that your accounts file is writable.
As you can see in the example, group1 has no password. This implies that you cannot log in as group1, but still group1 exists and its purpose is to
gather multiple accounts and refer to them collectively as group1, so you can quickly share powers among several accounts.
Other options you can define as properties of an account:
ignore_limitsto ignore speed limits. Default isfalse.redirectprovide a URL if you want the user to be redirected upon login. Default is none.adminsettrueif you want to let this account log in to the Admin interface.belongsan array of usernames of other accounts from which to inherit their permissions.