summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPiotr Szarmanski2023-08-03 16:45:31 +0200
committerPiotr Szarmanski2023-08-03 16:45:31 +0200
commitd6d6a174d176d6fc78bbce8a18bddb17ec74ecc1 (patch)
treefcde3736acfa9f98b9cd42665b50ed6a0bb15a42
parent47c9bb7e2518e055717b5bef67afef63780c2984 (diff)
CLI interface.
-rw-r--r--src/backup.lisp77
-rw-r--r--src/cli.lisp144
-rw-r--r--ybackup.asd3
3 files changed, 167 insertions, 57 deletions
diff --git a/src/backup.lisp b/src/backup.lisp
index 8efdaca..d0eee87 100644
--- a/src/backup.lisp
+++ b/src/backup.lisp
@@ -66,9 +66,8 @@
'dir
:permissions (osicat-file-permissions-to-integer (osicat:file-permissions target-file))
:name (filename-last-part target-file)
- :files (let* ((files (uiop:directory*
- (make-pathname :directory (namestring target-file)
- :name :wild :type :wild :version :wild)))
+ :files (let* ((files (append (uiop:directory-files (namestring target-file))
+ (uiop:subdirectories (namestring target-file))))
(table (make-hash-table :size (+ 7 (length files)) :test #'equal)))
(map nil #'(lambda (file)
;; Handle files that aren't in the ecase
@@ -98,7 +97,7 @@ SECRET can be specified in order to use a specific secret for ERIS encryption.
This secret is only necessary when writing. If an incremental backup is done,
the secret used is always the same as in the original repository."
- (declare (type string incremental name)
+ (declare (type (or null string) incremental name)
(type octet-vector secret)
(type (or pathname string) directory))
;; check for existance of directory
@@ -109,23 +108,25 @@ the secret used is always the same as in the original repository."
(*eris-secret* secret))
;; fetch incremental index
- (let ((incremental-index (when incremental (fetch-index incremental))))
- (eris:read-capability-to-urn
- (eris:store-data
- (cpk:with-named-index 'backup-index
- (cpk:encode
- (make-instance 'repository
- :files (package-file directory
- :incremental-record
- (if incremental
- (slot-value incremental-index 'files)
- nil))
- :date (get-universal-time)
- :name name
- :previous-repository
- (eris:read-capability-to-octets (eris:urn-to-read-capability incremental)))))
- *eris-backend*
- :secret *eris-secret*)))))
+ (let ((incremental-index (if incremental (fetch-index incremental) nil)))
+ (eris:read-capability-to-urn
+ (eris:store-data
+ (cpk:with-named-index 'backup-index
+ (cpk:encode
+ (make-instance 'repository
+ :files (package-file directory
+ :incremental-record
+ (if incremental
+ (slot-value incremental-index 'files)
+ nil))
+ :date (get-universal-time)
+ :name name
+ :previous-repository
+ (if incremental
+ (eris:read-capability-to-octets (eris:urn-to-read-capability incremental))
+ nil))))
+ *eris-backend*
+ :secret *eris-secret*)))))
;;; BACKUP READING
@@ -203,8 +204,40 @@ successive directories and the file-name at the end, such as `(\"home\" \"user\"
(defun list-files (urn backend)
(let ((*eris-backend* backend))
- (print-file (slot-value (fetch-index urn) 'files))))
+ (let ((index (fetch-index urn)))
+ (print-file (slot-value index 'files)))))
(defun metadata (urn backend)
(let ((*eris-backend* backend))
(print-repository (fetch-index urn))))
+
+(defun search-predicate (predicate urn backend)
+ "Return a list of files that return non-nil when applied to PREDICATE"
+ (let ((*eris-backend* backend))
+ (let ((index (fetch-index urn)))
+ (fmap-over-dir
+ (lambda (f) (if (funcall predicate f) f nil))
+ (slot-value index 'files)))))
+
+(defun search-regexp (regexp urn backend)
+ "Return a list of files whose names match REGEXP."
+ (let ((scanner (ppcre:create-scanner regexp)))
+ (search-predicate (lambda (file) (ppcre:scan scanner (slot-value file 'name)))
+ urn backend)))
+
+
+(defmacro collect-if (expr collector)
+ (alexandria:with-gensyms (v)
+ `(let ((,v ,expr))
+ (if ,v (,collector ,v)))))
+
+(defun fmap-over-dir (fn dir)
+ "Apply FN to each file under DIR, excluding directories. A list of non-nil
+return values of FN is returned.."
+ (mvlet ((lfiles ldirs
+ (with-collectors (cfiles cdirs)
+ (loop for i being the hash-value of (slot-value dir 'files)
+ do (typecase i
+ (dir (collect-if (fmap-over-dir fn i) cdirs)) ;; makes a list of lists
+ (t (collect-if (funcall fn i) cfiles)))))))
+ (apply #'concatenate 'list lfiles ldirs)))
diff --git a/src/cli.lisp b/src/cli.lisp
index 54c8517..0c2a740 100644
--- a/src/cli.lisp
+++ b/src/cli.lisp
@@ -66,6 +66,23 @@ the ERIS chunks."
:long "backend"
:meta-var "SEXP"
:arg-parser #'read-from-string)
+ (:name :search
+ :description "Searches for the filename in the repository. Prints a URN on successful completion."
+ :long "search"
+ :short #\s
+ :meta-var "FILENAME"
+ :arg-parser #'identity)
+ (:name :extract
+ :description
+ "Extracts a file from the repository. Use with a urn: argument or with --search.
+The file is non-destructively output to the current directory."
+ :long "extract"
+ :short #\e
+ :meta-var "URN")
+ (:name :incremental
+ :description "Set to enable incremental backup. Requires the --repo optio.."
+ :short #\i
+ :long "incremental")
(:name :filter
:description
"A one-argument lambda S-expression that takes a filename as an argument and
@@ -76,56 +93,92 @@ returns nil if the file is to be read or t if it is to be skipped."
(:name :overwrite
:description "Set if the program should overwrite existing files when writing from backup. "
:long "overwrite")
- (:name :incremental
- :description "Set to enable incremental backup. Requires the --repo optio.."
- :short #\i
- :long "incremental")
(:name :repo
- :description "The file that the URN will be written or read from."
+ :description
+ "The file that the URN will be written or read from. If set while using
+--backup, will perform an incremental backup, i.e. non-modified files will not
+be processed (based on mtime)."
:long "repo"
:meta-var "FILE OR URN"
:arg-parser #'identity)
(:name :secret
- :description "The secret used for encryption."
+ :description
+ "The secret used for encryption. Must be either NIL for a null-secret or a
+32-long octet vector sexp. If not used, a random secret will be used. Note that
+it is only useful when creating a backup."
:long "secret"
:short #\s
:meta-var "SECRET"
- :arg-parser #'identity)
+ :arg-parser #'(lambda (arg)
+ (let ((obj (read-from-string arg)))
+ (if (null obj) eris:null-secret
+ (coerce obj 'octet-vector)))))
(:name :metadata
:description "Print repository metadata when reading or listing files."
:long "metadata"
- :short #\m))
+ :short #\m)
+ (:name :debug
+ :description "Enable debugger."
+ :long "debug")
+ (:name :vers
+ :description "Print version."
+ :long "version"
+ :short #\v))
+
+
+;; Repo S-expression format. This gets printed/read to a file.
+(defstruct repo-file
+ urn
+ (secret (crypto:random-data 32)))
(defun file-or-urn-to-urn (file-or-urn)
- (if (string-prefix-p "urn:" file-or-urn)
- file-or-urn
- (with-open-file (file file-or-urn :direction :input :if-does-not-exist :error)
- (read-line file))))
+ "This takes as an argument either an urn: link or a file that contains either a
+\"urn:...\" string or the repo-file structure. Returns one value (urn) or two
+values, urn and the secret (OCTET VECTOR). "
+ (cond
+ ((null file-or-urn) nil)
+ ((string-prefix-p "urn:" file-or-urn) file-or-urn)
+ ((uiop:file-exists-p file-or-urn)
+ (let ((repo-file
+ (with-open-file (file file-or-urn :direction :input :if-does-not-exist :error)
+ (read file))))
+ (if (stringp repo-file) repo-file
+ (values (repo-file-urn repo-file)
+ (coerce (repo-file-secret repo-file) 'octet-vector)))))
+ (t nil)))
+
+(defmacro only-one-of (symbols &body expr)
+ "Make sure that only one of the symbols is bound; otherwise run expr."
+ (alexandria:with-gensyms (v1)
+ `(let ((,v1 0))
+ ,@(loop for i in symbols
+ collecting (list 'when i (list 'setf v1 (list '1+ v1))))
+ (when (> ,v1 1)
+ ,@expr))))
+
(defun main ()
(restart-case (destructuring-bind (&key help backup read list-files
file-backend http-backend backend
- filter overwrite incremental repo
- secret metadata)
+ filter overwrite repo secret metadata
+ search extract debug vers incremental)
(opts:get-opts)
;; some sanity checks
;; exclusive options
- (when (or (and backup read) (and backup list-files) (and read list-files))
- (error "Choose one of read, backup, or list."))
- (when (or (and backend http-backend) (and backend file-backend) (and file-backend http-backend))
+ (only-one-of (backup read list-files search)
+ (error "Choose one of read, backup, list or search."))
+ (only-one-of (backup read list-files extract)
+ (error "Choose one of read, backup, list or extract."))
+
+ (only-one-of (backend http-backend file-backend)
(error "Choose one backend."))
(when help
(opts:describe :prefix #.(format nil "ybackup version ~a" version))
(opts:exit))
- ;; repo argument necessary except for backup
- #|(when (and (not repo) backup)
- (error "Please provide --repo argument."))|#
-
- ;; don't save urns to files named urn:
- #|(when (and backup repo (string-prefix-p "urn:" repo))
- (error "No urns as filenames."))|#
+ (when vers
+ (print version))
(let ((backend
(cond
@@ -133,18 +186,41 @@ returns nil if the file is to be read or t if it is to be skipped."
(http-backend (error "Unimplemented http-backend."))
(backend (eval backend))
(t (error "Choose backend.")))))
- ;; TODO:
- ;; ADD METADATA, SECRET HANDLING (!!!)
(cond
+ ;; list files
(list-files
- (print (list-files (file-or-urn-to-urn repo) backend))
- ())
- (backup (let ((urn (make-backup backup backend :incremental incremental)))
- (if repo (with-open-file (file repo :direction :output :if-does-not-exist :create
- :if-exists :new-version)
- (write-string urn file))
- (princ urn))))
- (read (read-backup (file-or-urn-to-urn repo) backend read :overwrite overwrite)))
+ (pprint (list-files (file-or-urn-to-urn repo) backend))
+ (when metadata (pprint (metadata (file-or-urn-to-urn repo) backend)))
+ (terpri))
+ ;; backup
+ (backup
+ ;; repo being nil is permissible and simply results in
+ ;; i-urn and repo-secret := nil
+ (mvlet* ((incremental-urn repo-secret (file-or-urn-to-urn repo))
+ (secret (cond
+ (secret secret)
+ (repo-secret repo-secret)
+ (t (crypto:random-data 32)))))
+ (let ((urn (make-backup
+ backup
+ backend
+ :incremental (and incremental incremental-urn)
+ :secret secret)))
+ ;; rename here saves the previous file, at least on
+ ;; SBCL
+ (if repo (with-open-file (file repo :direction :output :if-exists :rename)
+ (pprint (make-repo-file :urn urn :secret secret) file))
+ (pprint (make-repo-file :urn urn :secret secret)))
+ (terpri))))
+
+ (read (read-backup (file-or-urn-to-urn repo) backend read :overwrite overwrite))
+ (extract (print "TODO: make this work")
+ (opts:exit))
+ (search (pprint
+ (map 'list #'print-file
+ (search-regexp search (file-or-urn-to-urn repo) backend)))
+ (terpri)))
(opts:exit)))
(exit () (opts:exit))))
+
diff --git a/ybackup.asd b/ybackup.asd
index 529c0a1..b2dd7b2 100644
--- a/ybackup.asd
+++ b/ybackup.asd
@@ -1,7 +1,8 @@
(defsystem "ybackup"
:author "mail@ykonai.net"
:license "LGPLv3 or later"
- :depends-on ("eris" "serapeum" "alexandria" "cl-conspack" "osicat" "unix-opts" "bit-smasher" "local-time")
+ :depends-on ("eris" "serapeum" "alexandria" "cl-conspack" "osicat" "unix-opts" "bit-smasher" "local-time"
+ "ironclad" "cl-ppcre")
:components
((:module "src"
:serial t