Self-hosted service
likun7981/hlink avatar
likun7981/hlink

hlink: batch hard linking for a NAS, with a pruning pass that keeps copies in sync

批量、快速硬链工具(The batch, fast hard link toolkit)

724 stars63 forksTypeScriptMIT

At a glance

What is it?
hlink is a TypeScript toolkit that creates hard links in bulk so that a large media or file collection can live in one place and be referenced from many directories without duplicating the bytes, and its pruning mechanism syncs those links back to the source when a file changes. It installs from npm, ships a Docker image with a web UI on port 9090, and claims twenty thousand files in about a minute.
Who is it for?
hlink is worth adopting if you keep a large collection on a NAS and want it visible from many directories without spending the storage twice, because the hard link approach preserves paths, permissions and tooling while sharing the bytes, and the pruning pass is what stops a renamed or edited source file from leaving broken copies behind.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Hard links let one file appear in many places without copying it

hlink describes itself as a batch, fast hard link toolkit, and the feature list is short enough to read as a specification of what it does and does not attempt.

Duplicate detection is first, and the note next to it matters: it supports detecting duplicates even when file names have changed. That is the difference between a naive tool that finds two files with the same name and one that finds the same content under different names, which is the case that matters in a media collection where a file has been renamed or re-encoded.

Speed is the second claim, stated as 20000 or more files in one minute. The third is multi platform support for Windows, Mac and Linux, which is necessary for a hard link tool because the mechanism is different on each of them.

The fourth is rich configuration, with support for black and white lists and caching among the options. A blacklist and whitelist pair is how you keep certain paths out of the operation or force certain paths in, and the cache is what makes repeated runs over a large tree tolerable.

The fifth is the pruning mechanism, described as letting you sync source files and hard links more conveniently. This is the feature that turns the tool from a one-shot converter into something you can leave in place, and it deserves more attention than the feature list gives it, because a hard link is a snapshot: if the source file is edited in place the link still points at the same inode and the change is visible, but if the source is replaced by a new file the link keeps the old content.

The last two are a web UI for graphical management and a Docker image so you do not have to care about the environment.

The Docker path is the intended way to run it on a NAS

The container is the deployment the project leads with, and the reason is visible in the image itself: the Dockerfile starts from node:lts-alpine, installs bash and su-exec, and installs hlink globally with npm, then copies in an entrypoint script and exposes port 9090.

The su-exec package and the entrypoint script are the interesting parts. A container that writes files into a mounted NAS volume has to write them as the user that owns the files on the NAS, not as root inside the container, and that is what an entrypoint switching to a numeric user id is for. The environment variables in the Dockerfile make the mechanism explicit, with PUID, PGID and UMASK set to defaults of 0, 0 and 022.

The run command passes those in explicitly:

bash
docker run -d --name hlink \
-e PUID=$YOUR_USER_ID \
-e PGID=$YOUR_GROUP_ID \
-e UMASK=$YOUR_UMASK \
-e HLINK_HOME=$YOUR_HLINK_HOME_DIR \
-p 9090:9090 \
-v $YOUR_NAS_VOLUME_PATH:$DOCKER_VOLUME_PATH \
likun7981/hlink:latest

The volume mapping is the whole point. The NAS directory is mounted into the container, and the container hard links files inside it, so the links are created on the NAS filesystem rather than in a container layer that would disappear. The HLINK_HOME variable points at the directory where the tool keeps its own state, including the cache, and the 9090 port mapping exposes the web UI.

A Compose file is given as well, with the same environment variables, the same port mapping and a restart policy of on-failure. The README explains that the placeholders are variables to fill in according to your own setup.

The image is a thin wrapper rather than a custom build. Everything hlink does is done by the npm package, so the container's job is the user mapping and the volume, and the image stays small and simple because of that.

The web UI is its own package and the docs are a separate site

Three of the seven bullets in the feature list are about how you interact with the tool rather than what it does, and the repository confirms they are separate pieces of code.

The web UI is listed as a graphical interface for easier management, and the build scripts show it living in its own package, built by the app build target and developed with a dev target of its own. The README carries a separate screenshot section for the web UI and another for the effect of the tool on files, which suggests the two are worth looking at separately: one shows the management surface, the other shows what happened to the directory.

The documentation is a third thing again. The docs directory is built with VitePress, with a dev script that serves it locally and a build script that produces the static site, and the project homepage is a separate site from the npm package page. The README itself is deliberately short, pointing at that site for a fuller introduction, which is a common arrangement for a project whose feature set needs more explanation than a README can carry.

The Docker image exposes port 9090 for that web UI, so the browser interface and the tool share the container. The entrypoint script is what runs first inside the container, and it is the file that handles the PUID and PGID values before the application starts.

There is also a small project homepage in the repository structure with its own logo asset, and a CHANGELOG.md at the root, which is where to look for what changed in the source tree that has not yet reached the published package.

Installing from npm gets you the same tool, three ways to run it

The npm installation is the other route, and it is a single global install:

bash
npm i -g hlink

After which the command line is available, and the help output is what you would reach for first:

bash
hlink --help

The repository structure shows that this is one product with three front ends rather than three tools. It is a pnpm monorepo with a packages/ directory, and the root package.json names the members by build script: build:core builds the core package, build:app builds the app package and build:cli builds the command line package. The build script runs them in that order, core first, then app, then cli, which is the dependency order.

That ordering is worth noting because it tells you the layering. The core package holds the hard link logic and the configuration handling, the app package is the web UI, and the cli package is the command line front end. Both the npm install and the Docker image run the same core, so a feature behaves identically whether you drive it from a terminal or from the browser.

The configuration lives in a hlink.config.js file at the repository root, so the settings are JavaScript rather than a data format, which is a natural fit for a Node tool and means the blacklist, whitelist and cache options can be computed rather than only listed.

The development tooling is conventional and modern: vitest for the tests with a watch mode, eslint with prettier applied through lint-staged, husky for the git hooks, and vitepress for the documentation site, with a docs directory and separate dev and build scripts. The release is a TypeScript script with a dry run variant, and the CI publish path is a separate script, with a publish workflow referenced by the badge in the README.

The monorepo grew from a script into three packages

The maintenance picture is the part that deserves a careful look, and it is mixed in a way the version numbers make obvious once you read them.

The package version in the root package.json is 2.0.10, and the three most recent releases are v2.0.10 from 2023-05-30, v2.0.9 from 2023-02-19 and v2.0.8 from 2023-01-10. So the released npm package has been at the same minor version for three years.

The repository itself is not idle. The last push was on 2026-10-15, and the source tree has clearly moved on from the released state. The root package.json version field still says 2.0.10, and the release history has no entry newer than 2023, which means the code in the tree and the code on npm are not the same thing.

That distinction matters more here than it would for a library, because hlink is a tool you run against your data. A web UI change is easy to miss if you install from npm and never rebuild. The repository also carries a CHANGELOG.md, so the change history is there to read.

The licence is MIT, linked from the README and stored in a file named license at the repository root, and the project is published to npm under the name hlink with a download count and a Node compatibility badge in the README. The Docker image is published under likun7981/hlink with a latest tag, and the entrypoint script at the repository root is the file that maps the container to your host user.

There is also a docs/ directory built with VitePress and a project homepage at hlink.likun.me, which is where the more detailed introduction lives.

Editorial conclusion

hlink is worth adopting if you keep a large collection on a NAS and want it visible from many directories without spending the storage twice, because the hard link approach preserves paths, permissions and tooling while sharing the bytes, and the pruning pass is what stops a renamed or edited source file from leaving broken copies behind. It is the wrong tool on a filesystem that does not support hard links, such as most object storage mounts or a FAT-formatted external drive, and it is the wrong tool for a small collection, where the plain copy costs nothing. Before you adopt it, verify three things yourself: that the volume you are pointing it at is a filesystem where hard links work, since the whole approach depends on it, that the ownership variables are set correctly for the NAS, because the container runs as a numeric user id and a mismatch leaves files you cannot delete, and that your duplicate detection settings match how your collection is actually organised, since the tool detects duplicates even when file names have changed and that detection is what decides what gets linked.

Frequently asked questions

What does hlink do?

It is a batch, fast hard link toolkit. It creates hard links in bulk so one file can appear in many directories without duplicating the data, and it detects duplicates even when file names have changed. The project claims twenty thousand or more files can be processed in about a minute.

How do I run hlink in Docker on a NAS?

Map your NAS directory into the container and set the ownership variables. The documented run passes PUID, PGID, UMASK and HLINK_HOME as environment variables, maps port 9090 for the web UI and mounts your NAS volume path, using the likun7981/hlink:latest image. The entrypoint uses su-exec so files are written as your user rather than as root.

How do I install hlink from npm?

Run npm i -g hlink, then call hlink from the command line, with hlink --help showing the available options. The same package is what the Docker image installs globally, and the repository is a pnpm monorepo with separate core, app and cli packages.

What is the pruning feature in hlink for?

It lets you sync source files and their hard links more conveniently. The feature list calls it a trimming mechanism, and it is what keeps linked copies consistent when the source file changes or is replaced, rather than leaving stale copies behind.

What is the latest released version of hlink?

The package version is 2.0.10 and the three most recent releases are v2.0.10 from 2023-05-30, v2.0.9 and v2.0.8 from early 2023. The repository is not archived and the last push was on 2026-10-15, so the source tree is more recent than the published package.

Official sources

  1. License: MIT
  2. likun7981/hlink on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/likun7981-hlink.svg)](https://hysenlabs.com/projects/likun7981-hlink)

Was this useful?