;;; zip/unzip (in-package :qml) #+ios (defun cl-user::read-sequence* (buffer stream &rest arguments) ;; hack for iOS, where 'read-sequence' doesn't update 'file-position'; ;; every occurrence of 'read-sequence' in library :zip must be replaced ;; with this function; ;; we can't replace it globally, because some code (like Quicklisp) ;; would stop working (let ((p1 (file-position stream)) (p2 (apply 'cl:read-sequence buffer stream arguments))) (when p1 (file-position stream (+ p1 p2))) ; update manually p2)) ;;; web server handler (in-package :s-http-server) (defvar *web-server* nil) (defvar *empty-line* #.(map 'vector 'char-code (list #\Return #\Linefeed #\Return #\Linefeed))) (defconstant +buffer-length+ 8192) (defun form-data-filename (data start end) "Searches for 'filename=' in current form data field header." (let ((p1 (search #.(x:string-to-bytes "filename=\"") data :start2 start :end2 end))) (when p1 (incf p1 #.(length "filename=\"")) (x:bytes-to-string (subseq data p1 (position #.(char-code #\") data :start p1)))))) (defun save-file (stream content-length boundary) "Saves uploaded file(s) under 'data/'. Requires 'multipart/form-data'." ;; read all data into a buffer first (let* ((content (make-array 0 :element-type '(unsigned-byte 8) :adjustable t)) (boundary-length (length boundary)) (start (+ boundary-length 2))) (loop :with buffer = (make-array +buffer-length+ :element-type '(unsigned-byte 8)) :for index = 0 :then (+ index pos) ;; don't read past end, would block http connection :for pos = (read-sequence buffer stream :end (min +buffer-length+ (- content-length index))) :do (adjust-array content (+ index pos)) (replace content buffer :start1 index :end2 pos) :while (< index content-length)) ;; loop through all form-data and save file(s) (x:while-it (search boundary content :start2 start) (let ((filename (form-data-filename content (+ start 2) (- x:it 2)))) (unless (x:empty-string filename) (let ((pathname (merge-pathnames (x:cc (if (string= "bin" (pathname-type filename)) "" "data/") filename)))) (ensure-directories-exist pathname) (with-open-file (out pathname :direction :output :if-exists :supersede :element-type '(unsigned-byte 8)) (write-sequence (subseq content (+ 4 (search *empty-line* content :start2 start)) (- x:it 4)) out))))) (setf start (+ x:it boundary-length))))) (defun ensure-multipart/form-data (headers) "Searches headers for 'multipart/form-data' and returns its boundary string." (let ((content-type (cdr (assoc :content-type headers)))) (when (search "multipart/form-data" content-type) (x:string-to-bytes (subseq content-type (+ (search "boundary=" content-type) #.(length "boundary="))))))) (defun static-resource/upload-handler (s-http-server handler http-request stream) "Hosts static resources from a document root. If the http request is POST, it saves uploaded files on the server." ;; slightely extended version of 's-http-server:static-resource-handler' ;; (see 'upload' below) (destructuring-bind (context document-root &rest options) handler (let* ((path (get-path http-request)) (resource-pathname (compute-real-resource-pathname document-root path context (or (getf options :pathname-builder) #'make-real-resource-pathname))) (expires-max-age (or (getf options :expires-max-age) (* 60 60)))) (if (probe-file resource-pathname) (progn ;; upload (when (eql :post (get-method http-request)) (let* ((headers (get-headers http-request)) (boundary (ensure-multipart/form-data headers))) (when boundary (save-file (get-stream (get-http-connection http-request)) (parse-integer (cdr (assoc :content-length headers))) boundary) (loc:extract-map-bin t) ; evtl. tiles to extract ;; if uploaded file is app:*backup-data-file*, unzip data, close app ;; (restart needed) (let ((data.zip (x:cc "data/" app:*backup-data-file*))) (x:when-it (probe-file data.zip) (qml:qlog "data file found, unzipping...") (app:unzip x:it (app:in-data-path)) (delete-file x:it) (app:toast (qml:tr "Data uploaded, closing app...")) (qml:qlog "closing app...") (qml:qsingle-shot 10000 'qml:qquit)))))) (logm s-http-server :debug "Serving ~s" resource-pathname) (host-static-resource http-request stream resource-pathname :expires-max-age expires-max-age)) (progn (logm s-http-server :error "Failed to find ~s" resource-pathname) (values t 404 (standard-http-html-error-response http-request stream 404 "Resource Not Found" path))))))) (defun start () (let ((ini (null *web-server*))) (when ini (setf *web-server* (make-s-http-server))) (start-server *web-server*) (when ini (app:make-backup) (qml:qlog "data zipped, ready for download") (register-context-handler *web-server* "/" 'static-resource/upload-handler :arguments (list *default-pathname-defaults*)))) (x:when-it (app:my-ip) (app:message-dialog (format nil "http://~A:1701" x:it)))) (defun stop () (stop-server *web-server*) (setf *web-server* nil) (app:toast (qml:tr "web-server stopped"))) (export (list 'start 'stop))