(defpackage :zfs-cleaner.utils (:use :cl) (:export #:regex-match #:include #:exclude #:pick #:snapshot-to-vector #:vector-to-lt #:key-transform #:combine #:derive #:cumsum #:over #:on #:shortcut #:defun-ct)) (in-package :zfs-cleaner.utils) (defmacro shortcut (name function &body bound-args) `(eval-when (:load-toplevel :compile-toplevel :execute) (setf (fdefinition ',name) (,function ,@bound-args)))) (defmacro defun-ct (name (&rest args) &body body) `(eval-when (:load-toplevel :compile-toplevel :execute) (defun ,name ,args ,@body))) (defun-ct regex-match (regex) (lambda (data) (cl-ppcre:scan-to-strings regex data))) (defun-ct include (pred) (lambda (seq) (remove-if-not pred seq))) (defun-ct exclude (pred) (lambda (seq) (remove-if pred seq))) (defun-ct pick (selector) (lambda (seq) (map 'list selector seq))) (defun-ct key-transform (fun key-get key-set) (lambda (it) (let ((key-val (funcall key-get it))) (funcall key-set (funcall fun key-val))))) (defun-ct combine (fun1 fun2) (lambda (item) (list (funcall fun1 item) (funcall fun2 item)))) (defun-ct derive (diff-fun &key (key #'identity)) (lambda (list) (mapcar (lambda (next cur) (cons (funcall diff-fun (funcall key next) (funcall key cur)) next)) (cdr list) list))) (defun-ct cumsum (&key (add-fun #'+) (key #'identity) (combine (lambda (x y) y x)) (zero 0)) (lambda (seq) (nreverse (reduce (lambda (accum next) (let ((key-val (funcall key next)) (old-val (if accum (funcall key (car accum)) zero))) (cons (funcall combine (funcall add-fun old-val key-val) next) accum))) seq :initial-value ())))) (defun-ct over (fun &key (result-type 'list)) (lambda (seq) (map result-type fun seq))) (defun-ct on (fun key) (lambda (it) (funcall fun (funcall key it)))) (defpackage :zfs-cleaner (:use :cl :zfs-cleaner.utils)) (in-package :zfs-cleaner) (defgeneric %get-snapshots (env) (:method ((env (eql :dev))) (alexandria:read-file-into-string #p "/tmp/feeds")) (:method ((env (eql :prod))) (with-output-to-string (s) (uiop:run-program "zfs list -r -t snapshot -H -p tank/feed_archive/feeds" :output s)))) (defun get-snapshots (&optional (env :dev)) (%get-snapshots env)) (shortcut find-date regex-match '(:group (:named-register "year" (:greedy-repetition 4 4 :digit-class)) #\- (:named-register "month" (:greedy-repetition 2 2 :digit-class)) #\- (:named-register "day" (:greedy-repetition 2 2 :digit-class)) #\- (:named-register "hour" (:greedy-repetition 2 2 :digit-class)) #\- (:named-register "minute" (:greedy-repetition 2 2 :digit-class)))) (defun-ct snapshot-to-vector (name) (map 'vector #'parse-integer (nth-value 1 (find-date name)))) (defun-ct vector-to-lt (vec) (apply 'local-time:encode-timestamp 0 0 (coerce (reverse vec) 'list))) (defstruct (zfs-date (:type vector)) year month day hour minute) (defparameter +date-format+ '((:year 4) #\- (:month 2) #\- (:day 2) #\- (:hour 2) #\- (:min 2))) (defun get-snapshots-to-prune (snapshots) (labels ((first-column (it) (elt (fwoar.string-utils:split #\tab it) 0)) (is-saved-snapshot (ts) (or (is-hourly-snapshot ts) (not (is-stale ts)))) (is-hourly-snapshot (it) (= (local-time:timestamp-minute it) 0)) (is-stale (item) (local-time:timestamp< item (local-time:timestamp- (local-time:now) 2 :day)))) (funcall (alexandria:compose (pick #'second) (exclude (alexandria:compose (on #'is-saved-snapshot #'first))) (over (combine (alexandria:compose #'vector-to-lt #'snapshot-to-vector) #'first-column)) (include 'find-date)) (fwoar.string-utils:split #\newline snapshots)))) (defun dev-main () (let ((snapshots (get-snapshots :dev))) (format t "~&~{~a~%~}" (get-snapshots-to-prune snapshots)))) (defun prod-main () (let ((snapshots (get-snapshots :prod))) (format t "~&~{~a~%~}" (get-snapshots-to-prune snapshots))))