Initial release.
.gitignore unknown status 1
| 1 | + | data |
main.scm unknown status 1
| 1 | + | (use-modules (pastebin httpserver) | |
| 2 | + | (web server)) | |
| 3 | + | ||
| 4 | + | (define (run-pastebin args) | |
| 5 | + | (let ((data-dir (cadr args))) | |
| 6 | + | (if (not (file-exists? data-dir)) | |
| 7 | + | (mkdir data-dir)) | |
| 8 | + | (run-server (make-pastebin-handler data-dir) 'http '(#:addr 0)))) |
pastebin/data.scm unknown status 1
| 1 | + | (define-module (pastebin data)) | |
| 2 | + | ||
| 3 | + | (use-modules (srfi srfi-9) | |
| 4 | + | (srfi srfi-1) | |
| 5 | + | (ice-9 ftw) | |
| 6 | + | (ice-9 textual-ports)) | |
| 7 | + | ||
| 8 | + | (export <pb-data> | |
| 9 | + | <pb-entry> | |
| 10 | + | pb-entry-id | |
| 11 | + | pb-entry-text | |
| 12 | + | pb-data-open | |
| 13 | + | pb-data-close | |
| 14 | + | call-with-dir-as-pb-data | |
| 15 | + | pb-data-get-top | |
| 16 | + | pb-data-new-entry) | |
| 17 | + | ||
| 18 | + | (define-record-type <pb-data> | |
| 19 | + | (make-pb-data dir) | |
| 20 | + | pb-data? | |
| 21 | + | (dir pb-data-dir set-pb-data-dir!)) | |
| 22 | + | ||
| 23 | + | (define-record-type <pb-entry> | |
| 24 | + | (make-pb-entry id text) | |
| 25 | + | pb-entry? | |
| 26 | + | (id pb-entry-id set-pb-entry-id!) | |
| 27 | + | (text pb-entry-text set-pb-entry-text!)) | |
| 28 | + | ||
| 29 | + | ;; input: dir: string | |
| 30 | + | ;; output: <pb-data> | |
| 31 | + | (define (pb-data-open dir) | |
| 32 | + | (make-pb-data dir)) | |
| 33 | + | ||
| 34 | + | (define (pb-data-close pb-data) #t) | |
| 35 | + | ||
| 36 | + | (define (call-with-dir-as-pb-data dir p) | |
| 37 | + | (let ((pb-data (pb-data-open dir))) | |
| 38 | + | (let ((R (p pb-data))) | |
| 39 | + | (pb-data-close pb-data) | |
| 40 | + | R))) | |
| 41 | + | ||
| 42 | + | (define (get-file-path pb-data filename) | |
| 43 | + | (string-append (pb-data-dir pb-data) "/" filename)) | |
| 44 | + | ||
| 45 | + | ;; input: <pb-data>, integer | |
| 46 | + | ;; output: list of <pb-entry> | |
| 47 | + | (define (pb-data-get-top pb-data n) | |
| 48 | + | (map | |
| 49 | + | (lambda (filename) | |
| 50 | + | (make-pb-entry | |
| 51 | + | filename | |
| 52 | + | (call-with-input-file (get-file-path pb-data filename) | |
| 53 | + | (lambda (port) | |
| 54 | + | (get-string-all port))))) | |
| 55 | + | (let ((file-ls (list-files pb-data))) | |
| 56 | + | (reverse (take-right file-ls (min n (length file-ls))))))) | |
| 57 | + | ||
| 58 | + | ;; input: <pb-data>, text | |
| 59 | + | ;; output: <pb-entry> | |
| 60 | + | (define (pb-data-new-entry pb-data text) | |
| 61 | + | (let ((next-filename (get-next-filename pb-data))) | |
| 62 | + | (call-with-output-file (get-file-path pb-data next-filename) | |
| 63 | + | (lambda (port) | |
| 64 | + | (put-string port text))) | |
| 65 | + | (make-pb-entry next-filename text))) | |
| 66 | + | ||
| 67 | + | (define (list-files pb-data) | |
| 68 | + | (scandir (pb-data-dir pb-data) (lambda (filename) (= (string-length filename) 5)))) | |
| 69 | + | ||
| 70 | + | (define (get-next-filename pb-data) | |
| 71 | + | (let ((entries (list-files pb-data))) | |
| 72 | + | (if (null? entries) | |
| 73 | + | "00000" | |
| 74 | + | (get-next-5digit (last entries))))) | |
| 75 | + | ||
| 76 | + | (define (get-next-5digit str) | |
| 77 | + | (let A ((add #t) | |
| 78 | + | (char-int-ls (reverse (map char->integer (string->list str)))) | |
| 79 | + | (result '())) | |
| 80 | + | (if (null? char-int-ls) | |
| 81 | + | (list->string (map integer->char result)) | |
| 82 | + | (if add | |
| 83 | + | (let* ((hd (car char-int-ls)) (tl (cdr char-int-ls))) | |
| 84 | + | (if (= hd #x7A) | |
| 85 | + | (A #t tl (cons #x30 result)) | |
| 86 | + | (A #f tl (cons (case hd | |
| 87 | + | ((#x39) #x41) | |
| 88 | + | ((#x5A) #x61) | |
| 89 | + | (else (1+ hd))) result)))) | |
| 90 | + | (A #f '() (append (reverse char-int-ls) result)))))) |
pastebin/httpserver.scm unknown status 1
| 1 | + | (define-module (pastebin httpserver)) | |
| 2 | + | ||
| 3 | + | (use-modules (web request) | |
| 4 | + | (web response) | |
| 5 | + | (sxml simple) | |
| 6 | + | (pastebin data) | |
| 7 | + | (rnrs bytevectors) | |
| 8 | + | (ice-9 textual-ports) | |
| 9 | + | (ice-9 regex) | |
| 10 | + | (srfi srfi-1)) | |
| 11 | + | ||
| 12 | + | (export make-pastebin-handler) | |
| 13 | + | ||
| 14 | + | (define (read-parts reqbody boundary) | |
| 15 | + | (define b2 (string-append "(\r\n)?--" boundary)) | |
| 16 | + | (let A ((start 0) (parts '())) | |
| 17 | + | (let ((sm (string-match b2 reqbody start))) | |
| 18 | + | (if sm | |
| 19 | + | ;; +2 => CRLF | |
| 20 | + | (A (+ 2 (match:end sm 0)) | |
| 21 | + | (cons (substring reqbody start (match:start sm 0)) parts)) | |
| 22 | + | (cdr (reverse parts)))))) | |
| 23 | + | ||
| 24 | + | (define (get-new-pin str pin) | |
| 25 | + | (let ((crlfi (string-contains str "\r\n" pin))) | |
| 26 | + | (if crlfi crlfi (string-length str)))) | |
| 27 | + | ||
| 28 | + | (define (parse-part partstr) | |
| 29 | + | (let A ((headers '()) (pin 0)) | |
| 30 | + | (let* ((newpin (get-new-pin partstr pin)) | |
| 31 | + | (line (substring partstr pin newpin))) | |
| 32 | + | (if (string-null? line) | |
| 33 | + | (cons (reverse headers) (substring partstr (+ 2 newpin))) | |
| 34 | + | (A (cons line headers) (+ 2 newpin)))))) | |
| 35 | + | ||
| 36 | + | (define (get-content-dispo-name-from-headers headers) | |
| 37 | + | (let ((fl (find (lambda (line) (string-prefix-ci? "content-disposition: " line)) headers))) | |
| 38 | + | (if fl | |
| 39 | + | (let ((sm (string-match "name=(.*)" fl))) | |
| 40 | + | (if sm (string-trim-both (match:substring sm 1) #\") "")) | |
| 41 | + | ""))) | |
| 42 | + | ||
| 43 | + | (define (read-multipart-form-data reqbody boundary) | |
| 44 | + | (define parts (read-parts reqbody boundary)) | |
| 45 | + | (map | |
| 46 | + | (lambda (part) | |
| 47 | + | (let ((pp (parse-part part))) | |
| 48 | + | (cons (get-content-dispo-name-from-headers (car pp)) (cdr pp)))) | |
| 49 | + | parts)) | |
| 50 | + | ||
| 51 | + | (define (templatize title body) | |
| 52 | + | `(html (head | |
| 53 | + | (title ,title) | |
| 54 | + | (meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))) | |
| 55 | + | (body ,@body))) | |
| 56 | + | ||
| 57 | + | (define (make-pastebin-handler data-path) | |
| 58 | + | (lambda (request request-body) | |
| 59 | + | (if (eq? (request-method request) 'POST) | |
| 60 | + | (let* ((headers (request-headers request)) | |
| 61 | + | (content-type-all (assq-ref headers 'content-type)) | |
| 62 | + | (content-type (if content-type-all (car content-type-all) #f)) | |
| 63 | + | (boundary (if (eq? content-type 'multipart/form-data) | |
| 64 | + | (assq-ref (cdr content-type-all) 'boundary) #f)) | |
| 65 | + | (reqbody-string (utf8->string request-body))) | |
| 66 | + | (if boundary | |
| 67 | + | (let ((form-data (read-multipart-form-data reqbody-string boundary))) | |
| 68 | + | (call-with-dir-as-pb-data | |
| 69 | + | data-path | |
| 70 | + | (lambda (pb-data) | |
| 71 | + | (pb-data-new-entry pb-data | |
| 72 | + | (assoc-ref form-data "text")))))))) | |
| 73 | + | ||
| 74 | + | (values (build-response | |
| 75 | + | #:code 200 | |
| 76 | + | #:headers `((content-type . (text/html)))) | |
| 77 | + | ||
| 78 | + | (lambda (port) | |
| 79 | + | (let* ((top5 (call-with-dir-as-pb-data | |
| 80 | + | data-path | |
| 81 | + | (lambda (pb-data) (pb-data-get-top pb-data 5)))) | |
| 82 | + | (sxml (templatize | |
| 83 | + | "pastebin" | |
| 84 | + | `((form (@ (method "post") (enctype "multipart/form-data")) | |
| 85 | + | (textarea (@ (name "text")) "") (input (@ (type "submit")))) | |
| 86 | + | (table (@ (border 1)) (tr (th "id") (th "text")) | |
| 87 | + | ,(map (lambda (entry) `(tr (td ,(pb-entry-id entry)) | |
| 88 | + | (td ,(pb-entry-text entry)))) | |
| 89 | + | top5)))))) | |
| 90 | + | (display "<!DOCTYPE html>\n" port) | |
| 91 | + | (sxml->xml sxml port)))))) |