A filesystem API for Nim, inspired by Flysystem from the PHP ecosystem.
nimble install flysystem
- Unified API: A consistent interface for working with files across different storage backends (local, cloud, etc.).
- Extensible Drivers: Easily add support for new storage backends by implementing the
Driverinterface. - Rich Metadata: Retrieve detailed file metadata, including size, last modified time, visibility, checksum and MIME type.
- Preventing Directory Traversal: All file operations are securely sandboxed to prevent access outside the designated root directory, mitigating directory traversal vulnerabilities.
- Atomic Writes: Content is written to a temporary file and renamed over the target, so a crash mid-write never corrupts a file.
- Memory-Efficient: Handle large files without loading them entirely into memory via
MemFiles, including a zero-allocationreadLinesiterator. Swap instd/streamswith-d:features.flysystem.useStdStreams. - Async Driver: Opt-in
AsyncLocalDriverimplementing the same methods asynchronously (await d.write(...)) on stdlibasyncfile/asyncdispatchβ no extra dependencies. - POSIX Tooling: Symlinks, advisory file locking,
chmod/chownandstatvfs-based disk-usage helpers. - Security Policies: Wrap any driver with
FilesystemPolicyto enforce read-only mode, size caps, extension/MIME allowlists, hidden-file denial and path limits β attachable per disk. - Multi-Disk Facade: Group several drivers under a
Filesystemand switch between them with one call.
Note
Only the local filesystem driver is available today. The driver interface is ready for custom backends and cloud drivers are on the roadmap.
import flysystem
# the root is resolved at runtime (env var, config key, ...)
let d = newLocalDriver(getCurrentDir() / "uploads")
d.write("hello.txt", "Hello, World!")
echo d.read("hello.txt") # "Hello, World!"
echo d.exists("hello.txt") # trueimport flysystem
let d = newLocalDriver("/srv/storage")
# write / read
d.write("notes.txt", "buy milk")
echo d.read("notes.txt") # "buy milk"
# move & copy
d.move("notes.txt", "archive/notes.txt") # parent dirs are created
d.copy("archive/notes.txt", "notes.bak")
# delete
d.delete("notes.bak")
echo d.exists("notes.bak") # falsed.makeDir("assets/images") # creates parent dirs too
d.copyDir("assets", "backup") # recursive copy
d.moveDir("backup", "old") # recursive move
# by default deleteDir refuses to remove non-empty directories
d.deleteDir("old") # StorageError: Directory not empty
# pass force = true to remove recursively
d.deleteDir("old", force = true)
d.touch("empty.txt") # create an empty file (or bump its mtime)# writes are private (0o600) by default
d.write("secret.txt", "top secret")
# ...or explicitly public (0o644)
d.write("public.txt", "hi", visPublic)
echo d.visibility("secret.txt") # visPrivate
d.setVisibility("secret.txt", visPublic)
# POSIX-only direct permission/owner control
d.chmod("secret.txt", 0o600) # set permission bits
# chown requires an `import std/posix` and appropriate privileges
d.chown("secret.txt", getuid().int, getgid().int) # set uid / gidlet m = d.metadata("notes.txt")
echo m.path # "notes.txt" (root-relative)
echo m.size # 9
echo m.lastModified # Time
echo m.isDir # false
echo d.size("notes.txt") # 9 (int64, via metadata)
echo d.lastModified("notes.txt") # Time (via metadata)
echo d.checksum("notes.txt") # "e194f103..." (md5 hex)
echo d.mimeType("notes.txt") # "text/plain"
# list the contents of a directory (recursively or not)
for item in d.list("assets", recursive = true):
echo item.path# append text
d.append("log.txt", "line1\n")
d.append("log.txt", "line2\n")
# stream the contents of an open file handle
var src = open("localfile.bin", fmRead)
d.writeStream("stored.bin", src)
src.close()
# append a stream to an existing file
src = open("localfile.bin", fmRead)
d.appendStream("log.bin", src)
src.close()
# read a file as a memory-mapped stream (great for large files)
var mf = d.readStream("stored.bin")
echo mf.size
defer: mf.close()
# ...or with std/streams instead of memfiles (compile with
# -d:features.flysystem.useStdStreams, e.g. nimble testStreams):
# var s = d.readStream("stored.bin")
# echo s.readAll()
# s.close() # always required, even for empty files# readLines memory-maps the file and reuses a single buffer, so no
# allocations happen per line. The yielded string is only valid until
# the next iteration β copy it if you need to keep it.
for line in d.readLines("huge.log"):
if "error" in line:
echo lined.createSymlink("latest.txt", "releases/v1.0.txt") # target must stay in the root
echo d.isSymlink("latest.txt") # true
echo d.readLink("latest.txt") # "releases/v1.0.txt"# run a block while holding an exclusive lock on a file (POSIX)
d.withLock("counter.txt"):
# only one writer at a time
let n = parseInt(d.read("counter.txt"))
d.write("counter.txt", $(n + 1))# returns paths relative to the root
echo d.search("*.png") # @["icon.png", "logo.png"]
echo d.search("cache/*.log") # @["cache/run.log"]let du = d.diskUsage() # POSIX-only
echo "free: ", du.free, " bytes of ", du.totalimport std/asyncdispatch
import flysystem/local_async # opt-in, alongside `import flysystem`
let ad = newAsyncLocalDriver("/srv/storage")
proc main() {.async.} =
await ad.write("hello.txt", "hi")
echo await ad.read("hello.txt") # "hi"
echo await ad.size("hello.txt") # 2 (via metadata)
echo await ad.readLines("log.txt") # @["line1", "line2"]
await ad.copyDir("assets", "backup")
waitFor main()File content goes through true non-blocking std/asyncfile; directory,
metadata and POSIX helpers run synchronously inside the async methods.
let fs = newFilesystem()
fs.addDisk("local", newLocalDriver("/tmp/local"))
fs.addDisk("cache", newLocalDriver("/tmp/cache"))
fs.write("hello.txt", "hi") # default disk
echo fs.read("hello.txt") # "hi"
echo fs.exists("hello.txt") # true
fs.delete("hello.txt")
# switch disks on the fly
fs.disk("cache").write("cached.json", "{}")
echo fs.disk("cache").read("cached.json")try:
d.read("missing.txt")
except StorageError as e:
echo e.msg # "Failed to read 'missing.txt': ..."
# every driver is sandboxed: nothing can escape the root
try:
d.write("../evil.txt", "nope")
except StorageError as e:
echo e.msg # "Path traversal detected: ../evil.txt"import flysystem
# a policy layer wraps any driver and rejects violating operations
# with a FilesystemPolicyError (a subtype of StorageError)
let d = newLocalDriver("/srv/uploads")
let rules = PolicyRules(
readOnly: false, # deny all mutating operations
maxFileSize: 10 * 1024 * 1024, # 10 MB per file (0 = unlimited)
maxPathLength: 255, # max path chars (0 = unlimited)
maxDepth: 8, # max path segments (0 = unlimited)
denyHiddenFiles: true, # block ".foo" files and filter listings
allowedExtensions: @["png", "jpg", "txt"],
allowedMimeTypes: @["image/png", "image/jpeg", "text/plain"])
let secure = newFilesystemPolicy(d, rules)
try:
secure.write("evil.exe", "...") # blocked by allowedExtensions
except FilesystemPolicyError as e:
echo e.rule # "allowedExtensions"
echo e.operation # "write"
# attach a policy to a disk on the Filesystem facade
let fs = newFilesystem()
fs.addDisk("uploads", d, rules)
fs.disk("uploads").write("photo.jpg", "...") # ok
try:
fs.disk("uploads").write("payload.bin", "...") # blocked
except FilesystemPolicyError as e:
echo e.rule # "allowedExtensions" (".bin" is not allowlisted)
# bypass the policy when you really need the raw driver
fs.rawDisk("uploads").write("internal.bin", "...")- π Found a bug? Create a new Issue
- π Wanna help? Fork it!
MIT license. Made by Humans from OpenPeeps.
Copyright OpenPeeps & Contributors β All rights reserved.