https://github.com/atlas-engineer/nfiles
User configuration and data file management
https://github.com/atlas-engineer/nfiles
Last synced: about 1 month ago
JSON representation
User configuration and data file management
- Host: GitHub
- URL: https://github.com/atlas-engineer/nfiles
- Owner: atlas-engineer
- License: bsd-3-clause
- Created: 2022-01-28T08:10:43.000Z (about 4 years ago)
- Default Branch: master
- Last Pushed: 2025-01-03T23:33:30.000Z (over 1 year ago)
- Last Synced: 2026-03-02T22:57:17.484Z (about 2 months ago)
- Language: Common Lisp
- Size: 203 KB
- Stars: 22
- Watchers: 4
- Forks: 5
- Open Issues: 6
-
Metadata Files:
- Readme: readme.org
- License: LICENSE
Awesome Lists containing this project
- awesome-cl - nfiles - File persistence, watching, data synchronization, (per user profile) path resolution, and structured data retrieval. Has pre-defined classes for configuration files, remote fetched files, data files, Lisp-readable files and many others. [BSD][15]. (Online editors ## / Third-party APIs)
README
#+TITLE: NFiles
NFiles is a Common Lisp library that deals with customizable path resolution,
file persistence and loading.
Its main use case is help manage user-centric files like configuration files.
In some aspects, it can be seen as "Common Lisp 'logical pathnames' over CLOS".
** Goals
- Performance ::
- Data is not persisted to disk if it does not need to.
- Files are read only once (unless modified externally).
- Extensibility :: Persist any data structure the way you want it.
- Reliability :: no corruption and no data loss should occur.
** Features
- Dynamic and customizable path expansion.
- Extensible serialization and deserialization.
- Cached reads and writes ::
When a =file= object expands to the same path as another one, a =read= or
=write= on it won't do anything in case there was no change since last write.
- (*Experimental!*) On-the-fly PGP encryption.
- Profile support.
- On read errors, existing files are backed up.
- On write errors, no file is written to disk, the existing file is preserved.
- A =remote-file= can point to a URL, which is automatically downloaded if the
local file is not found.
** Motivation
This package was developed after dealing with a problem when delivering Common
Lisp images: when an image is generated, path expansion may already be resolved
and thus hard-coded within the image, which makes it unfit for delivery.
Consider this:
#+begin_src lisp
> (defvar *foo-path* (uiop:xdg-config-home))
*FOO-PATH*
> *foo-path*
#P"/home/johndoe/.config/"
#+end_src
Now if I ship this image to my friend Kaboom, =*foo-path*= will expand to
#+begin_src lisp
#P"/home/johndoe/.config/"
#+end_src
on their machine instead of the expected
#+begin_src lisp
#P"/home/kaboom/.config/"
#+end_src
** Examples
A basic session:
#+begin_src lisp
(defvar *config-file* (make-instance 'nfiles:config-file :base-path #p"my-app/init.lisp"))
(nfiles:expand *config-file*)
; => #P"/home/johndoe/.config/my-app/init.lisp"
(setf (nfiles:content *config-file*) "Hello file!") ; The file is written to disk.
(nfiles:content *config-file*)
; => "Hello file!"
#+end_src
The following convenience macro ensures the file is updated when done with the
body:
#+begin_src lisp
(nfiles:with-file-content (content *config-file*)
(format t "Length: ~a~%" (length content))
(setf content (serapeum:string-replace "file" content "config")))
#+end_src
The =with-paths= helper allows for let-style bindings of the expanded paths:
#+begin_src lisp
(let ((file1 (make-instance 'nfiles:file))
(file2 (make-instance 'nfiles:file :base-path #p"alt")))
(nfiles:with-paths ((path1 file1)
(path2 file2))
(list path1 path2)))
#+end_src
A =remote-file= works the same but needs some specialization:
#+begin_src lisp
(defmethod nfiles:fetch ((profile nfiles:profile) (file remote-counter-file) &key)
(dex:get (nfiles:url file)))
;; Optional:
(defmethod nfiles:check ((profile nfiles:profile) (file remote-counter-file) content &key)
(let ((path (nfiles:expand file)))
(ironclad:byte-array-to-hex-string
(ironclad:digest-file :sha3 path))))
(let ((file (make-instance 'nfiles:remote-file
;; The URL to download from if the file is not found on disk.
:url (quri:uri "https://example.org")
;; Without base-path, the file won't be saved to disk.
:base-path #p"/tmp/index.html"
:checksum "794df316afac91572b899b52b54f53f04ef71f275a01c44b776013573445868c95317fc4a173a973e90addec7792ff8b637bdd80b1a6c60b03814a6544652a90")))
;; On access, file is automatically downloaded if needed and the checksum is verified:
(nfiles:content file)
;; ...
)
#+end_src
See the [[file:package.lisp][package]] documentation for a usage guide and more examples.
** Configuration
NFiles was designed with configurability in mind. All configuration happens through
subclassing of the =file= and =profile= classes together with method
specialization.
All configuration methods are specialized against =profile= and =file= so that
the user can easily *compose* the behaviour:
- Profile-customization impacts all files using that profile;
- File-customization impacts the files of that specific type (or subtype)
regardless of their profile.
Of course you can specialize against both!
The specialization methods are divided into the following:
- =resolve= :: This is where path resolution is done. On call site, prefer the
=expand= convenience wrapper.
- =deserialize= and =serialize= :: This is how the content is transformed
to the file on disk. These functions are meant to be called by the
=read-file= and =write-file= methods.
- =read-file= and =write-file= :: This is how the file is read and written to
disk. These functions are responsible for calling the =deserialize= and
=serialize= methods.
- =fetch= :: This generic function is only called for =remote-file= objects. You
_must_ define its methods. It does not have any method by default so as to
not burden NFiles with undesirable dependencies.
- =check :=: Like =fetch=, this generic function is only called for =remote-file=
objects to test the integrity of the downloaded file. You _must_ define its
methods. It does not have any method by default so as to not burden NFiles
with undesirable dependencies.
** Conditions and restarts
Some NFiles-specific conditions are raised in case of exceptional situations to
provide for interactive and customizable behaviour:
- =external-modification= :: The file was modified externally. See the
=on-external-modification= slot to automate what to do in this case.
- Read error restarts can also customized, see the =on-read-error= slot to
automate what to do in this case.
- =process-error= :: This may be raised for instance when =gpg= fails to encrypt.
The =use-recipient= restart is provided to retry with the given recipient.
** Shadowing
NFiles 1 shadows =cl:delete=, thus you should not =:use= the package (as with
any other library anyways).
** Platform support
It's pure Common Lisp and all compilers plus all operating systems should be
supported.
Some notes:
- All compilers but SBCL depend on [[https://github.com/sionescu/iolib][IOlib]] to preserve file attributes.
- Android devices also depend on [[https://github.com/sionescu/iolib][IOlib]] to preserve file attributes,
regardless of the compiler.
- File attributes might not be preserved on Windows.
** Roadmap
- Improve PGP support.
- Support OS-level locks (à-la Emacs / LibreOffice).
- Improve portability.
- In particular, preservation of file attributes may not work on
Windows.
- Compressing =write-file= and =read-file= (for instance with zstd / lz).
- But is it such a good idea? Users should prefer compression at
the level of the file system.
** Change log
*** 1.1.4
- Remove =NASDF= as a dependency.
*** 1.1.3
- Ensure a =cl:pathname= is returned from =resolve=.
- Complete some missing documentation.
*** 1.1.2
- Add restarter functions for =ask=, =reload=, etc.
Mind that =cl:delete= is now shadowed (this was necessary to preserve backward
compatibility).
Do not =:use= the package!
- Switch from =hu.dwim.defclass-star= to [[https://github.com/atlas-engineer/nclasses/][nclasses]].
*** 1.1.1
- Allow path expansion for =virtual-file= (as in 1.0.0).
This restores the usefulness of virtual-files, namely to handle the
path-expansion business while deferring the read/write business to a
third-party.
=virtual-profile= still nullifies path expansions (as in 1.1.0).
*** 1.1.0
- Add support for Android.
- Nullify path expansion for =virtual-file= and =virtual-profile=.
- Ensure that the =deserialize= method of =virtual-file= and =virtual-profile=
return nil.
- Fix =basename= corner case.
- Add report messages to all restarts.
** History
NFiles was originally developed for user file management in [[https://nyxt.atlas.engineer][Nyxt]], so the "N"
may stand for it, or "New", or whatever poetic meaning you may find behind it!