├── .github └── workflows │ └── tests.yml ├── CHANGELOG.md ├── README.md ├── changes ├── README.md └── config.toml ├── embedfs.nimble ├── src └── embedfs.nim └── tests ├── .gitignore ├── config.nims ├── data ├── bar.txt ├── foo.txt ├── image.png └── subdir │ ├── apple.txt │ └── banana.txt └── test1.nim /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | 5 | jobs: 6 | tests: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | nimversion: 11 | - binary:stable 12 | - nightly:https://github.com/nim-lang/nightlies/releases/tag/2023-03-31-version-2-0-2e4ba4ad93c6d9021b6de975cf7ac78e67acba26 13 | os: 14 | - ubuntu-latest 15 | - macOS-latest 16 | - windows-latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - uses: iffy/install-nim@v4 20 | with: 21 | version: ${{ matrix.nimversion }} 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | - name: Test 25 | run: | 26 | nimble install -y 27 | nimble test -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.2.0 - 2024-02-19 2 | 3 | - **NEW:** Support Nim v2 4 | 5 | # v0.1.2 - 2024-02-19 6 | 7 | - **FIX:** Absolute paths work better now 8 | 9 | # v0.1.1 - 2023-03-24 10 | 11 | - **FIX:** Prevent escaping to parent directory when in non-embed mode 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # embedfs 2 | 3 | Embed directories of files in your executable. It's like `staticRead`/`slurp` for whole directories. 4 | 5 | [![.github/workflows/tests.yml](https://github.com/iffy/nim-embedfs/actions/workflows/tests.yml/badge.svg)](https://github.com/iffy/nim-embedfs/actions/workflows/tests.yml) 6 | 7 | ## Installation 8 | 9 | ``` 10 | nimble install embedfs 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```nim 16 | import embedfs 17 | const data = embedDir("data") 18 | echo data.get("somefile.txt").get() 19 | doAssert data.get("nonexisting.txt").isNone 20 | 21 | for filename in data.listDir(): 22 | echo filename 23 | 24 | for filename in data.walk(): 25 | echo filename 26 | ``` 27 | 28 | You can also change from embedding at compile-time (the default) to reading files from disk at runtime for testing purposes with `embed = false`, because sometimes it's nice to not have to recompile the whole program just to get new assets: 29 | 30 | ```nim 31 | import embedfs 32 | const data = embedDir("data", embed = false) 33 | writeFile("data"/"foo.txt", "foo") 34 | doAssert data.get("foo.txt") == some("foo") 35 | writeFile("data"/"foo.txt", "new value") 36 | doAssert data.get("foo.txt") == some("new value") 37 | ``` -------------------------------------------------------------------------------- /changes/README.md: -------------------------------------------------------------------------------- 1 | `changer` makes it easy to manage a `CHANGELOG.md` file. It works in Nim projects and other languages, too. 2 | 3 | # Installation 4 | 5 | ``` 6 | nimble install changer 7 | ``` 8 | 9 | # Configuration 10 | 11 | You can configure how `changer` behaves by editing the `changes/config.toml` file. 12 | 13 | # Usage 14 | 15 | Start a changelog in a project by running: 16 | 17 | changer init 18 | 19 | Every time you want to add something to the changelog, make a new Markdown file in `./changes/` named like this: 20 | 21 | - `fix-NAME.md` 22 | - `new-NAME.md` 23 | - `break-NAME.md` 24 | - `other-NAME.md` 25 | 26 | Use the tool to add a changelog entry: 27 | 28 | changer add 29 | 30 | When you're ready to release a new version, preview the new changelog with: 31 | 32 | changer bump -n 33 | 34 | Then make the new changelog (and update the version of any `.nimble` file): 35 | 36 | changer bump 37 | -------------------------------------------------------------------------------- /changes/config.toml: -------------------------------------------------------------------------------- 1 | update_nimble = true 2 | update_package_json = true 3 | 4 | # Replacements are string substitutions using 5 | # https://nitely.github.io/nim-regex/regex.html#replace%2Cstring%2CRegex%2Cstring%2Cint 6 | # They run on each snippet in the order defined, before the snippet is 7 | # added to the CHANGELOG.md file. 8 | # For example, uncomment the following lines to replace issue numbers with 9 | # links to your GitHub project. 10 | 11 | # [[replacement]] 12 | # pattern = '#(\d+)' 13 | # replace = "[#$1](https://github.com/YOURNAME/YOURPROJECT/issues/$1)" 14 | -------------------------------------------------------------------------------- /embedfs.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2.0" 4 | author = "Matt Haggard" 5 | description = "Embed static files easily" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.6.10" 13 | -------------------------------------------------------------------------------- /src/embedfs.nim: -------------------------------------------------------------------------------- 1 | ## If you want to refactor this implementation to make it 2 | ## better (more efficient/faster/compression), just make sure the tests pass. 3 | import std/os 4 | import std/strutils 5 | import std/tables 6 | import std/options; export options 7 | 8 | type 9 | EmbeddedTable* = Table[string, string] 10 | EmbeddedFS* = distinct EmbeddedTable 11 | ## This distinct type is to prevent users from depending 12 | ## on the implementation being a Table 13 | 14 | RuntimeEmbeddedFS* = distinct string 15 | 16 | template VLOG(msg: string) = 17 | when defined(embedfsVerbose): 18 | echo "[embedfs] ", msg 19 | else: 20 | discard 21 | 22 | func looksAbsolute*(path: string): bool = 23 | when doslikeFileSystem: 24 | path.len >= 3 and path[1..2] == ":\\" 25 | else: 26 | path.startsWith("/") 27 | 28 | template embedDir*(dirname: string, embed:static[bool] = true): untyped = 29 | ## Embed a directory of files into this program. 30 | ## 31 | ## `dirname` = directory to embed 32 | ## 33 | ## `embed` = if true (default), embed files into the program. 34 | ## If `false`, files are read from disk at runtime. This is useful when 35 | ## testing (so you don't have to recompile the program to test changes 36 | ## in embedded assets). 37 | when embed: 38 | const tmp = static: 39 | var files = initTable[string, string](0) 40 | let fulldir = if dirname.looksAbsolute: 41 | dirname 42 | else: 43 | instantiationInfo(-1, true).filename.parentDir / dirname 44 | VLOG "embedding dir " & fulldir 45 | for relpath in walkDirRec(fulldir, relative = true): 46 | files[relpath] = staticRead(fulldir / relpath) 47 | VLOG " + " & relpath & " size=" & $files[relpath].len 48 | files 49 | tmp.EmbeddedFS 50 | else: 51 | if dirname.looksAbsolute: 52 | dirname.RuntimeEmbeddedFS 53 | else: 54 | (instantiationInfo(-1, true).filename.parentDir / dirname).RuntimeEmbeddedFS 55 | 56 | iterator listDir*(ed: EmbeddedFS|RuntimeEmbeddedFS, subdir = ""): string = 57 | ## List all embedded file names within the given directory 58 | when ed is EmbeddedFS: 59 | for key in ed.EmbeddedTable.keys: 60 | if subdir == "": 61 | if DirSep notin key: 62 | yield key 63 | else: 64 | if key.startsWith(subdir & DirSep): 65 | yield key.substr(len(subdir)+1) 66 | else: 67 | # runtime "embed" 68 | let root = ed.string.absolutePath 69 | let fulldir = root / subdir 70 | if fulldir.isRelativeTo(root): 71 | for item in walkDir(fulldir): 72 | if item.kind == pcFile: 73 | yield item.path.relativePath(fulldir) 74 | 75 | iterator walk*(ed: EmbeddedFS|RuntimeEmbeddedFS): string = 76 | ## List all embedded file names 77 | when ed is EmbeddedFS: 78 | for key in ed.EmbeddedTable.keys: 79 | yield key 80 | else: 81 | # runtime "embed" 82 | for path in walkDirRec(ed.string, relative=true): 83 | yield path 84 | 85 | proc get*(ed: EmbeddedFS|RuntimeEmbeddedFS, filename: string): Option[string] = 86 | ## Get a previously-embedded file's contents 87 | when ed is EmbeddedFS: 88 | if ed.EmbeddedTable.hasKey(filename): 89 | return some(ed.EmbeddedTable[filename]) 90 | else: 91 | # runtime "embed" 92 | try: 93 | let root = ed.string.absolutePath 94 | let path = root / filename 95 | if path.isRelativeTo(root): 96 | return some(readFile(path)) 97 | except CatchableError: 98 | discard 99 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore extensionless files 2 | * 3 | !/**/ 4 | !*.* 5 | _dynamic 6 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/data/bar.txt: -------------------------------------------------------------------------------- 1 | bar -------------------------------------------------------------------------------- /tests/data/foo.txt: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /tests/data/image.png: -------------------------------------------------------------------------------- 1 | fakeimage 2 | -------------------------------------------------------------------------------- /tests/data/subdir/apple.txt: -------------------------------------------------------------------------------- 1 | apple 2 | -------------------------------------------------------------------------------- /tests/data/subdir/banana.txt: -------------------------------------------------------------------------------- 1 | banana 2 | -------------------------------------------------------------------------------- /tests/test1.nim: -------------------------------------------------------------------------------- 1 | import std/algorithm 2 | import std/os 3 | import std/sequtils 4 | import std/unittest 5 | import embedfs 6 | 7 | let TESTDIR = currentSourcePath.parentDir() 8 | 9 | test "listDir": 10 | const fs = embedDir("data") 11 | let list = fs.listDir().toSeq().sorted() 12 | check list == @["bar.txt", "foo.txt", "image.png"] 13 | 14 | let list2 = fs.listDir("subdir").toSeq().sorted() 15 | check list2 == @["apple.txt", "banana.txt"] 16 | 17 | test "walk": 18 | const fs = embedDir("data") 19 | let list = fs.walk().toSeq().sorted() 20 | check list == @["bar.txt", "foo.txt", "image.png", 21 | "subdir"/"apple.txt", "subdir"/"banana.txt"] 22 | 23 | test "get": 24 | const fs = embedDir("data") 25 | check fs.get("bar.txt") == some("bar") 26 | 27 | test "dynamic": 28 | const fs = embedDir("_dynamic", embed = false) 29 | let dyndir = TESTDIR/"_dynamic" 30 | removeDir(dyndir) 31 | createDir(dyndir) 32 | check fs.listDir().toSeq().len == 0 33 | check fs.walk().toSeq().len == 0 34 | check fs.get("foo.txt").isNone 35 | 36 | writeFile(dyndir/"foo.txt", "foo") 37 | writeFile(dyndir/"bar.txt", "bar") 38 | createDir(dyndir/"sub") 39 | writeFile(dyndir/"sub"/"hey.txt", "hey") 40 | 41 | check fs.listDir().toSeq().sorted() == @["bar.txt", "foo.txt"] 42 | check fs.listDir("sub").toSeq().sorted() == @["hey.txt"] 43 | check fs.walk().toSeq().sorted() == @["bar.txt", "foo.txt", "sub"/"hey.txt"] 44 | check fs.get("foo.txt") == some("foo") 45 | 46 | writeFile(dyndir/"foo.txt", "foo2") 47 | check fs.get("foo.txt") == some("foo2") 48 | 49 | test "dynamic path": 50 | const fs = embedDir("data", embed = false) 51 | writeFile(TESTDIR/"data"/"dyn.txt", "dyn") 52 | defer: removeFile(TESTDIR/"data"/"dyn.txt") 53 | check fs.get("dyn.txt") == some("dyn") 54 | 55 | test "no parent dir": 56 | const fs = embedDir("data", embed = false) 57 | check fs.get(".."/"config.nims").isNone 58 | check fs.listDir("..").toSeq().len == 0 59 | 60 | test "no absolute dir": 61 | const fs = embedDir("data", embed = false) 62 | check fs.get(currentSourcePath.absolutePath).isNone 63 | check fs.listDir(currentSourcePath.absolutePath.parentDir).toSeq().len == 0 64 | --------------------------------------------------------------------------------