diff --git a/ipfs-gno.asd b/cl-ipfs-api2.asd similarity index 71% rename from ipfs-gno.asd rename to cl-ipfs-api2.asd index b00de27..09d36a3 100755 --- a/ipfs-gno.asd +++ b/cl-ipfs-api2.asd @@ -1,7 +1,7 @@ -(defsystem "ipfs-gno" +(defsystem "cl-ipfs-api²" :version "0.1" :author "Jaidyn Ann " - :license "Cooperative Software License" + :license "AGPLv3" :depends-on ("drakma" "yason" "arnesi") :components ((:file "package") (:file "main"))) diff --git a/main.lisp b/main.lisp index 231f91c..5414c21 100644 --- a/main.lisp +++ b/main.lisp @@ -1,59 +1,240 @@ -(in-package :ipfs-gno) +(in-package :cl-ipfs-api2) (defparameter *api-host* "http://127.0.0.1:5001") +(defparameter *api-root* "/api/v0/") -;; STRING LIST … → (STRING/VARYING RETURN-CODE HEADER-LIST …) -(defun api-call (call arguments &key (method :get) (parameters nil) - (want-stream nil)) - (let ((call-url (string+ *api-host* "/api/v0/" call)) - (first-arg T)) + +;; STRING LIST [:LIST :BOOLEAN :SYMBOL] → STRING | HASHTABLE | (NIL STRING) +(defun ipfs-call (call arguments &key (parameters nil) (want-stream nil) + (method :GET)) + "Make an IPFS HTTP API call. Quite commonly used. + Some calls return strings/raw data, and others return JSON. + When strings/arbitrary data are recieved, they're returned verbatim. + But, when JSON is returned, it is parsed into a hashtable. + If the JSON is 'error JSON', I.E., it signals that an error has been + recieved, two values are returned: NIL and the string-error-message." + (let ((result + (drakma:http-request + (make-call-url *api-host* *api-root* call arguments) + :method method + :parameters parameters + :want-stream want-stream))) + + (cond (want-stream result) + ((stringp result) result) + ((vectorp result) + (let ((json (yason:parse (flexi-streams:octets-to-string result)))) + (cond ((equal "error" (gethash "Type" json)) + (values nil (gethash "Message" json))) + ('T json))))))) + + +;; STRING STRING LIST → STRING +(defun make-call-url (host root call arguments) + "Create the URL of an API call, as per the given arguments. + Symbols are assumed to be something like 'T (so boolean), nil likewise. + Arguments should look like this: + (('recursive' nil)('name' 'xabbu'))" + (let ((call-url (string+ host root call)) + (first-arg 'T)) (mapcar (lambda (arg-pair) - (format t "FIRST: ~A~%" first-arg) - (if arg-pair - (progn (setq call-url - (string+ call-url (if first-arg "?" "&") - (car arg-pair) "=" (cadr arg-pair))) - (setq first-arg nil)))) + (when arg-pair + (setq call-url + (string+ + call-url + (if first-arg "?" "&") + (first arg-pair) "=" + (cond ((not (second arg-pair)) + "false") + ((symbolp (second arg-pair)) + "true") + ('T (second arg-pair)))))) + (setq first-arg nil)) arguments) - - (drakma:http-request call-url :method method :parameters parameters - :want-stream want-stream))) + call-url)) -;; STRING :NUMBER :NUMBER → STRING +;; FORM FORM → FORM +(defmacro bind-api-result (call form) + "Wrap around an #'ipfs-call form; if #'call returns an error, then + return NIL and the error message-- (NIL STRING)-- otherwise, execute + #'form. + Binds the result of the API call to… you guessed it, the variable 'result'. + The error message is assigned to 'message', if such a thing exists." + `(multiple-value-bind (result message) + ,call + (if message + (values nil message) + ,form))) + + + +;; ------------------------------------- +;; / CALLS + +;; PATHNAME → (HASH-STRING SIZE-NUMBER) || (NIL STRING) +(defun add (pathname &key (pin 't) (only-hash nil)) + "Add a file to IPFS, return it's hash. + http://127.0.0.1:8080/ipns/docs.ipfs.io/reference/api/http/#api-v0-add" + (bind-api-result + (ipfs-call "add" `(("pin" ,pin) ("only-hash" ,only-hash)) + :method :post :parameters `(("file" . ,pathname))) + + (values (gethash "Hash" result) + (parse-integer (gethash "Size" result)) + (gethash "Name" result)))) + +;; STRING :NUMBER :NUMBER → STRING || (NIL STRING) (defun cat (ipfs-path &key (offset nil) (length nil)) - "Return a string of the data at the given IPFS path." - (api-call "cat" - `(("arg" ,ipfs-path) - ,(if offset `("offset" ,offset)) - ,(if length `("length" ,length))))) + "Return a string of the data at the given IPFS path. + /ipns/docs.ipfs.io/reference/api/http/#api-v0-cat" + (bind-api-result + (ipfs-call "cat" + `(("arg" ,ipfs-path) + ,(if offset `("offset" ,offset)) + ,(if length `("length" ,length)))) + result)) +;; STRING [:BOOLEAN :BOOLEAN] → (LIST LIST LIST LIST) || (NIL STRING) +(defun ls (ipfs-path &key (resolve-type 't) (size 't)) + "Returns all sub-objects (IPFS hashes) under a given IPFS/IPNS directory + path. Returns four lists; hashes, sizes, names, types. They are related + positionally-- aka, item #2 in the hash-list, will have a size of #2 in the + size-list, etc. + /ipns/docs.ipfs.io/reference/api/http/#api-v0-ls" + (bind-api-result + (ipfs-call "ls" `(("arg" ,ipfs-path) + ("resolve-type" ,resolve-type) ("size" ,size))) -;; STRING PATHNAME --> NIL + (let ((links (gethash "Links" + (car (gethash "Objects" result))))) + (values (mapcar (lambda (link) (gethash "Hash" link)) links) + (mapcar (lambda (link) (gethash "Size" link)) links) + (mapcar (lambda (link) (gethash "Name" link)) links) + (mapcar (lambda (link) (gethash "Type" link)) links))))) + +;; STRING PATHNAME → NIL (defun dl (ipfs-path out-file) "Write an IPFS file directly to a file on the local file-system. - Non-recursive, in the case of directories. - (Thanks to this thread ♥) https://stackoverflow.com/a/12607423" + Non-recursive, in the case of directories… for now. + (Thanks to this thread ♥: https://stackoverflow.com/a/12607423) + Is a general replacement for the 'get' API call, but actually just uses + the 'cat' call, due to some issues with using 'get'. + Will not actually return NIL when an error is reached (like other functions) + with an error-message, it'lll just write the error JSON to the file. + Whoops." (with-open-file (out-stream out-file :direction :output :element-type '(unsigned-byte 8) :if-exists :overwrite :if-does-not-exist :create) (let ((in-stream - (api-call "cat" `(("arg" ,ipfs-path)) :want-stream 'T))) + (ipfs-call "cat" `(("arg" ,ipfs-path)) :want-stream 'T))) (awhile (read-byte in-stream nil nil) (write-byte it out-stream)) (close in-stream)))) +;; ----------------- -;; PATHNAME → STRING -(defun add (file-path) - "Add a file to IPFS, return it's hash. Does not work recursively." - (gethash "Hash" - (yason:parse - (flexi-streams:octets-to-string - (api-call "add" '() :method :post - :parameters `(("file" . ,file-path))))))) +;; [STRING] → (STRING LIST STRING STRING STRING) +(defun id (&key (peer-id nil)) + "Return info on a node by ID. Has multiple return-values, as follows: + public-key, address-list, agent-version, protocol-version, and peer ID. + The last might be redundant if it was specified as an argument; otherwise, + it tells you your own node's ID. + /ipns/docs.ipfs.io/reference/api/http/#api-v0-id" + (bind-api-result + (ipfs-call "id" `(,(if peer-id (list "arg" peer-id)))) + (values (gethash "PublicKey" result) + (gethash "Addresses" result) + (gethash "AgentVersion" result) + (gethash "ProtocolVersion" result) + (gethash "ID" result)))) + +;; ----------------- + +;; STRING → (STRING || (NIL STRING) +(defun dns (domain &key (recursive 't)) + "Resolve a domain into a path (usually /ipfs/). + http://127.0.0.1:8080/ipns/docs.ipfs.io/reference/api/http/#api-v0-dns" + (bind-api-result + (ipfs-call "dns" `(("arg" ,domain) ("recursive" ,recursive))) + (gethash "Path" result))) + +;; STRING [:BOOLEAN :NUMBER :NUMBER] → STRING || (NIL STRING) +(defun resolve (ipfs-path &key (recursive 't) (dht-record-count nil) + (dht-timeout 30)) + "Resolve a given name to an IPFS path." + (bind-api-result + (ipfs-call "resolve" `(("arg" ,ipfs-path) ("recursive" ,recursive) + ,(if dht-record-count + (list "dht-record-count" dht-record-count)) + ("dht-timeout" ,(string+ dht-timeout "s")))) + (gethash "Path" result))) + +;; ----------------- + +;; NIL → NIL +(defun shutdown () + "Shut down the connected IPFS node. + /ipns/docs.ipfs.io/reference/api/http/#api-v0-shutdown" + (ipfs-call "shutdown" '())) + + + +;; ------------------------------------- +;; / CONFIG CALLS + +;; STRING [:STRING :BOOLEAN :BOOLEAN] → STRING || (NIL STRING) +(defun config (key &key (value nil) (bool nil) (json nil)) + "Get/set a config key's value. + http://127.0.0.1:8080/ipns/docs.ipfs.io/reference/api/http/#api-v0-config" + (bind-api-result + (ipfs-call "config" `(("arg" ,key) ,(if value (list "value" value)) + ("bool" ,bool) ("json" ,json))) + `(,(gethash "Key" result) ,(gethash "Value" result)))) + +;; NIL → HASH-TABLE +(defun config/show () + "Return the config file's contents, in a Yason, JSON-parsed hashtable. + Doesn't quite line up with #api-v0-config-show" + (bind-api-result + (ipfs-call "config/show" '()) + result)) + + + +;; ------------------------------------- +;; / VERSION CALLS + +;; NIL → (STRING STRING STRING STRING STRING) +(defun version () + "Return versioning information on this IPFS node + /ipns/docs.ipfs.io/reference/api/http/#api-v0-version" + (bind-api-result + (ipfs-call "version" '()) + (values (gethash "Version" result) + (gethash "Commit" result) + (gethash "Repo" result) + (gethash "System" result) + (gethash "Golang" result)))) + +;; NIL → (STRING STRING STRING STRING) +(defun version/deps () + "Return info about dependencies used for build. + /ipns/docs.ipfs.io/reference/api/http/#api-v0-version" + (bind-api-result + (ipfs-call "version/deps" '()) + (values (gethash "Version" result) + (gethash "Path" result) + (gethash "ReplacedBy" result) + (gethash "Sum" result)))) + + + + +;; ------------------------------------- +;; UTIL ;; STRING-A STRING-B … STRING-N → STRING (defun string+ (&rest strings) diff --git a/package.lisp b/package.lisp index 0f1bde3..c57e65f 100644 --- a/package.lisp +++ b/package.lisp @@ -1,9 +1,26 @@ -(defpackage :ipfs-gno +(defpackage :cl-ipfs-api² (:use :cl :arnesi) + (:nicknames :cl-ipfs :ipfs :cl-ipfs-api2) (:export *api-host* + *api-root* + + ;; / calls :dl :cat - :add)) + :add + :dns + :id + :ls + :resolve + :shutdown -(in-package :ipfs-gno) + ;; / config calls + :config + config/show + + ;; /version calls + :version + :version/deps)) + +(in-package :cl-ipfs-api²)