I needn't defend the usefulness of screenshotting. Sharing a photo, sharing code, sharing an error message, saving something for later, sending things you found on the internet, etc. For as many things as one could view on a screen, one could screenshot and share.
So, we need two pieces of software:
* the backend on the server
* the front end computer
This guide will be split into two parts, the server part, which is Clojure, and the front end part which will be an implementation in Common Lisp and another in Python. CL because I am a StumpWM user, so it fits into my ecosystem, and Python because it's simple.
So let's get started on the backend. First we need to break this down in the parts:
1. A web sever that handles the requests.
2. A databasing solution that isn't too ugly, for now we will just use on-memory storage, but it would be easy to expand to a real databasing solution.
I'll now be writing code. The source code is all stored on my github at the screenshot-upload repository. Let's start by running up a webserver.
;; src/screenshot-upload/handler.clj
(ns screenshot-upload.handler
(:require [compojure.core :refer :all]
[compojure.route :as route]
[org.httpkit.server :refer [run-server]]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[backend.utils :as utils]))
(defroutes app-routes
(GET "/" [] (utils/list-files))
(POST "/" [:as req] (utils/file-upload req))
(GET "/:key{[a-zA-Z0-9]{4,8}}" [key] (utils/access-file key))
(route/not-found "Not Found"))
That second
GET
route may look a little foreign, but it's just regex to catch all the keys for the files uploaded. [a-zA-Z0-9]
from 4 to 8 characters. Nice feature.(def app
(-> app-routes
(wrap-params)
(wrap-multipart-params)))
(defonce ^:private backend-server (atom nil))
(defn stop-servers [server]
(@server :timeout 5)
(reset! server nil))
(defn -main []
(reset! backend-server (run-server #'app {:port 3000})))
This simple server needs just a few middleware:
wrap-params
, and wrap-multipart-params
. We need wrap-params
for our text parameters; we need to send the type of each file we upload so the server knows what Content-Type
to return in our HTTP headers. wrap-multipart-params
is so we can upload files. Be careful here using
wrap-defaults
, since we want to use this mostly from our backend, it's frustrating to have anti-forgery tokens on, as we primarily want to use this endpoint from inside an application, not on the web, and we don't need the many other options that wrap-defaults
wants us to have.Also, if you want to use HTTPS (which is recommended) many use reverse-proxy
NGINX
servers. This is personally what I do, and in combination with Certbot
this feels to be the simplest way to get up and running with HTTPS.;; src/screenshot-upload/screenshot.clj
(ns backend.utils
(:require [clojure.java.io :as io]
[hiccup.core :refer :all]))
(def files (atom {}))
(defonce characters "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
(defn random-chars
[n]
(->> (repeatedly #(rand-nth characters))
(take n)
(reduce str)))
(defn unique-id
[]
(let [new-id (random-chars (+ 4 (rand-int 4)))]
(if (@files new-id)
(unique-id)
new-id)))
This is our implementation of databasing and id generation. For our use, this is perfect. For production, use a database. MongoDB is an easy replacement here, checkout this post on the Monger (MongoDB client) docs about how to upload files to a MongoDB database from Clojure. Personally, since I was already on a Postgres databasing system, I added a table that saves filename, and filetype, and the files are saved in another directory. This is a pretty bad way, because if I delete an entry on the DB I lose that file, or vice versa.
(defn file-upload
[req]
(let* [id (unique-id)
file (get (req :params) "file")]
(swap! files merge @files {(keyword id)
[(get (req :params) "type")
(:tempfile file)]})
id))
(defn list-files
[]
(html
[:ul
(map (fn [[k v]] [:li (link-to (str "/" (name k))
(name k))]) @files)]))
(defn access-file
[key]
(let [data (@files (keyword key))]
{:status 200
:headers {"Content-Type" (first data)}
:body (second data)}))
Here are the last little pieces.
(file-upload)
is a simple function that takes a request, gets the file out of it. In the :params
map there is a value with key "file" that is actually another map. This map has :tempfile
and that's the actual file. We also grab "type" from the :params
map, as that is our slot to tell the server what to server in the HTTP header.(list-files)
is a function that return HTML containing an unordered list populated with all the files on memory, with links to them. (access-file)
, the last function, is also a simple function that simply returns an HTTP response: status 200, header with Content-Type set to the specified type of each file, and body with the file. That's all there is to this. So let's try it. Clone the repo and run up a server with
lein run
in the base directory. Then we can post something!$ curl -XPOST "localhost:3000/" -F [email protected] -F type=text/plain
=> mhB0d
Navigate to localhost:3000/ and we should see a list containing only that. Click and go! Another thing you could do is upload image files (PNG, JPG, etc) and use
-F type=image
then when you load the link, you will have Content-Type: Image in the HTTP response, and it will load the image. (As opposed to downloading it, which is a drag). Hope you enjoyed another brief runthrough that I had fun building for myself. The next part it just going to be wrapping this little project up with interfaces to be used with Python or Common Lisp.
Until next time. :-)