blob: 01935a68a023a58640a820e31a849d5d962c8b7d (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
(defpackage #:tazblog/store
(:documentation
"This module implements fetching of individual blog entries from DNS.
Yes, you read that correctly.
Each blog post is stored as a set of records in a designated DNS
zone. For the production blog, this zone is `blog.tazj.in.`.
A top-level record at `_posts` contains a list of all published
post IDs.
For each of these post IDs, there is a record at `_meta.$postID`
that contains the title and number of post chunks.
For each post chunk, there is a record at `_$chunkID.$postID` that
contains a base64-encoded post fragment.
This module implements logic for assembling a post out of these
fragments and caching it based on the TTL of its `_meta` record.")
(:use #:cl #:dns #:iterate)
(:import-from #:cl-base64 #:base64-string-to-string))
(in-package :tazblog/store)
;; TODO:
;;
;; - implement DNS caching
(defvar *tazblog-zone* ".blog.tazj.in."
"DNS zone in which blog posts are persisted.")
(deftype entry-id () 'string)
(defun list-entries (&key (offset 0) (count 4) (zone *tazblog-zone*))
"Retrieve COUNT entry IDs from ZONE at OFFSET."
(let ((answers (lookup-txt (concatenate 'string "_posts" zone))))
(map 'vector #'dns-rr-rdata (subseq answers offset (+ offset count)))))
(defun get-entry-meta (entry-id zone)
(let* ((name (concatenate 'string "_meta." entry-id zone))
(answer (lookup-txt name))
(encoded (dns-rr-rdata (alexandria:first-elt answer)))
(meta-json (base64-string-to-string encoded)))
(json:decode-json-from-string meta-json)))
(defun base64-add-padding (string)
"Adds padding to the base64-encoded STRING if required."
(let ((rem (- 4 (mod (length string) 4))))
(if (= 0 rem) string
(format nil "~A~v@{~A~:*~}" string rem "="))))
(defun collect-entry-fragments (entry-id count zone)
(let* ((fragments
(iter (for i from 0 below count)
(for name = (format nil "_~D.~A~A" i entry-id zone))
(collect (alexandria:first-elt (lookup-txt name)))))
(decoded (map 'list (lambda (f)
(base64-string-to-string
(base64-add-padding (dns-rr-rdata f))))
fragments)))
(apply #'concatenate 'string decoded)))
(defstruct entry
(id "" :type string)
(title "" :type string)
(content "" :type string)
(date "" :type string))
(defun get-entry (entry-id &optional (zone *tazblog-zone*))
"Retrieve the entry at ENTRY-ID from ZONE."
(let* ((meta (get-entry-meta entry-id zone))
(count (cdr (assoc :c meta)))
(title (cdr (assoc :t meta)))
(date (cdr (assoc :d meta)))
(content (collect-entry-fragments entry-id count zone)))
(make-entry :id entry-id
:date date
:title title
:content content)))
|