;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2015 Rémi Birot-Delrue
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see .
(define-module (guix scripts publish-gnunet)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-9)
#:use-module (srfi srfi-11)
#:use-module ((srfi srfi-26) #:select (cut))
#:use-module (srfi srfi-37)
#:use-module ((rnrs bytevectors) #:select (string->utf8
utf8->string))
#:use-module (ice-9 match)
#:use-module (ice-9 ftw)
#:use-module (ice-9 regex)
#:use-module (system foreign)
#:use-module (guix base32)
#:use-module (guix pki)
#:use-module (guix store)
#:use-module (guix ui)
#:use-module ((guix config) #:select (%store-directory))
#:use-module ((gnu gnunet common) #:renamer (symbol-prefix-proc 'gn:))
#:use-module ((gnu gnunet configuration) #:renamer (symbol-prefix-proc 'gn:))
#:use-module ((gnu gnunet scheduler) #:renamer (symbol-prefix-proc 'gn:))
#:use-module ((gnu gnunet container metadata)
#:renamer (symbol-prefix-proc 'gn:))
#:use-module ((gnu gnunet identity) #:renamer (symbol-prefix-proc 'gn:))
#:use-module ((gnu gnunet fs) #:renamer (symbol-prefix-proc 'gn:))
#:use-module ((gnu gnunet fs progress-info)
#:renamer (symbol-prefix-proc 'gn:))
#:use-module (guix scripts publish-utils)
#:export (guix-publish-gnunet))
;; debug variables
(define *simulate?* #t)
(define *index?* #t)
(define *anonymity* 0)
(define (show-help)
(display (_ "Usage: guix publish-gnunet [OPTION]...PACKAGE...
Publish PACKAGE... over GNUnet.\n"))
(display (_ "
-P, --pseudonym=NAME publish the store under the namespace specified by
pseudonym NAME"))
(newline)
(display (_ "
-h, --help display this help and exit"))
(display (_ "
-V, --version display version information and exit"))
(newline)
(show-bug-report-information))
;;+TODO: handle -L (loglevel) and -l (logfile) options
(define %options
(let ((register (lambda (id)
(lambda (opt name arg opts targets)
(values (alist-cons id arg opts) targets)))))
(list (option '(#\h "help") #f #f
(lambda _
(show-help)
(exit 0)))
(option '(#\V "version") #f #f
(lambda _
(show-version-and-exit "guix publish-gnunet")))
(option '(#\c "config") #t #f (register 'config-file))
(option '(#\P "pseudonym") #t #f (register 'pseudonym)))))
(define %default-options '())
;; option for the blocks weâre going to publish
(define %block-options
(gn:make-block-options (gn:time-relative->absolute (gn:time-rel #:days (* 180)))
*anonymity*))
(define %default-config-file "~/.config/gnunet.conf")
;; handles -- connection to a GNUnet service or operation
(define %config #f)
(define %identity #f)
(define %ego #f)
(define %filesharing #f)
;;; utilities to keep track of the publish handles
(define %publish-entries #f)
(define (print-state)
(simple-format (current-error-port) "--- state ---
config \t~a~%
identity \t~a~%
ego \t~a~%
filesharing\t~a~%
entries \t~a~%"
%config %identity %ego %filesharing %publish-entries))
;;;+TODO: replace the `identifier` slot with the use of a hash-table
;;; for %publish-entries.
(define-record-type
(%make-publish-entry handle identifier completed? stopped? error?)
publish-entry?
(handle publish-entry-handle %set-publish-handle!)
(identifier publish-entry-id)
(completed? publish-entry-completed? %set-publish-entry-completeness!)
(stopped? publish-entry-stopped? %set-publish-entry-stopness!)
(error? publish-entry-error? %set-publish-entry-errorness!))
(define (publish-entry-complete! entry)
(when (publish-entry-completed? entry)
(warning (_ "~A: already completed~%") entry))
(%set-publish-entry-completeness! entry #t))
(define (publish-entry-stop! entry)
(when (publish-entry-stopped? entry)
(warning (_ "~A: already stopped~%") entry))
(%set-publish-entry-stopness! entry #t))
(define (publish-entry-error! entry)
(when (publish-entry-error? entry)
(warning (_ "~A: already on error~%") entry))
(%set-publish-entry-errorness! entry #t))
(define* (start-publish filesharing file-info namespace identifier)
"Start the publication of FILE-INFO under NAMESPACE with IDENTIFIER,
return a publish entry."
(simple-format (current-error-port) "start-publish: ~a~%" (gn:file-information-filename file-info))
(%make-publish-entry (gn:start-publish filesharing file-info
#:namespace namespace
#:identifier identifier)
identifier
#f #f #f))
(define* (stop-publish entry)
;; we must advance the entryâs state before calling gn:stop-publish,
;; as otherwise progress-callback would be called with a non-updated
;; entry state.
(simple-format (current-error-port) "stop-publish: ~a~%" entry)
(publish-entry-stop! entry)
(gn:stop-publish (publish-entry-handle entry))
(%set-publish-handle! entry #f))
(define (find-entry id lst)
(find (compose (cut string=? id <>) publish-entry-id) lst))
;; used to make publish-entries identifiers
(define %store-item-regexp
(make-regexp (string-append "^" %store-directory
"/([a-z0-9]+)" ; hash
"-"
"[^/]+" ; program name
"(/.*)?$")))
(define (path->hash path)
"Extract the hash part of the store item PATH."
(match:substring (regexp-exec %store-item-regexp path) 1))
(define (store-item? path)
"Return #t if PATH is of the form:
`%store-directory/-`."
(not (match:substring (regexp-exec %store-item-regexp path) 2)))
;;; utilities to scan a directory and collect each file
;;+FIXME: is the âsymlink targetâ metadata really needed?
(define (file->file-information* path stat)
"Create a file information from a file. If PATH denotes a symlink,
add its target to its metadata (under the #:filename metatype)."
(let* ((meta (gn:make-metadata))
(type (stat:type stat))
(item (case type
((symlink)
(gn:make-metadata-item
;; name of the âextractorâ
"guix publish-gnunet"
;; we use the #:filename (EXTRACTOR_METADATA_FILENAME)
;; metatype because itâs never used in GNUnet
;; (see gnunet/src/fs/fs_dirmetascan.c:374).
#:filename
#:utf8
"text/plain"
(string->utf8 (readlink path))))
((regular) #f)
(else
(leave (_ "~A: invalid file type (~a)~%") path type)))))
(when item (gn:metadata-set! meta item))
(let ((res (gn:file->file-information% %filesharing path %block-options
#:index? #t #:metadata meta)))
;;+FIXME: which exception should be raised?
(when (eq? %null-pointer res)
(throw 'invalid-result "file->file-information*"
"gn:file->file-information%"
(list %filesharing path %block-options #:index #t
#:metadata meta)))
res)))
(define* (directory->file-information* path #:key (add-metadata '()))
"Create a file information from a directory; the content of the
directory isnât scanned.
ADD-METADATA is a list of metadata entries to add to the directoryâs
metadata."
(let ((meta (gn:make-metadata)))
(when (not (null? add-metadata))
(for-each (lambda (item) (gn:metadata-set! meta item)) add-metadata))
(gn:directory->file-information% %filesharing path %block-options
#:metadata meta)))
(define (tree->file-information path tree . meta)
(define (prefix relpath) (string-append path "/" relpath))
(match tree
((file stat) (file->file-information* (prefix file) stat))
((dir stat files ...)
(let ((info (directory->file-information* (prefix dir)
#:add-metadata meta)))
(map (compose (cut gn:file-information-add! info <>)
(cut tree->file-information (prefix dir) <>))
files)
info))
(_ ; shouldnât happen
(leave (_ "failed to access ~A~%") path))))
;;+FIXME: prefix
;;+TODO: optimize?
(define (scan-directory path . metadata)
"Scan the directory PATH, collect each file, and add METADATA to the
root file information."
(apply tree->file-information (dirname path) (file-system-tree path)
metadata))
(define (scan-store-path store path)
"Scan the PATH as a path in STORE and return a file-information."
(let* ((path-info (query-path-info store path))
(narinfo (narinfo-string path path-info (force %private-key)))
(meta-item (gn:make-metadata-item "guix publish-gnunet" #:narinfo
#:utf8 "text/plain"
(string->utf8 narinfo))))
(gn:wrap-file-information (scan-directory path meta-item))))
;;+TODO: handle GNUNET_ARGS
;;+TODO: handle XDG_CONFIG_HOME
;;+TODO: properly handle GNUnet configuration file
;; (add something in Guixâs conf?)
(define (guix-publish-gnunet . args)
(let*-values (((opts paths)
(args-fold* args %options
(lambda (opt name . rest)
(leave (_ "~A: unrecognized option~%") name))
(lambda (arg opts paths)
(values opts (cons arg paths)))
%default-options
(values '() '())))
((pseudo config-file)
(values (assoc-ref opts 'pseudonym)
(or (assoc-ref opts 'config-file)
(begin
(warning (_ "using default config file ~A~%")
%default-config-file)
%default-config-file)))))
(when (not pseudo)
(leave (_ "missing pseudonym option~%")))
(when (null? paths)
(leave (_ "missing store item argument~%")))
(map (lambda (path)
(when (not (access? path R_OK))
(leave (_ "failed to access ~A~%") path)))
paths)
(catch 'file-unavailable
(lambda () (set! %config (gn:load-configuration config-file)))
(lambda (key . args)
(leave (_ "failed to access ~A~%") config-file)))
;;+TODO: add stop-task
(gn:call-with-scheduler
%config
(lambda (_)
(set! %identity
(gn:open-identity-service %config
(identity-callback pseudo paths)))
(gn:add-task! stop-task #:delay (gn:time-rel #:forever))))))
(define (identity-callback pseudo paths)
(lambda (ego name)
"Function called by GNUnetâs identity service. Itâs mapped on each
available ego."
(cond ((not ego) ; last call
(display "identity-callback: last call\n" (current-error-port))
(set! %filesharing
(gn:open-filesharing-service %config "guix publish-gnunet"
progress-callback))
(gn:add-task! (lambda (_) (scan-&-publish paths))))
((and name (string=? pseudo name))
(set! %ego ego)))))
(define (scan-&-publish paths)
"Scan each of the PATHS and start publishing them. Return a list of
publish entries."
(with-error-handling
(with-store store
(set! %publish-entries
(fold (lambda (path entries)
(let ((info (scan-store-path store path))
(hash (path->hash path)))
(cons (start-publish %filesharing info %ego hash)
entries)))
'()
paths)))))
(define (progress-callback info status)
"Called by the filesharing service each time thereâs something to
report about one of our publications."
;;+TODO: shouldnât we stop every publication once all are finished,
;; instead of closing each one separately?
(define (schedule-stop! entry)
(simple-format (current-error-port) " schedule-stop!: ~a~%" entry)
(when (not (publish-entry-stopped? entry))
(display " add-task: STOP-PUBLISH~%" (current-error-port))
(gn:add-task! (lambda (_) (stop-publish entry)))))
(define pinfo-publish-entry
(compose (cut find-entry <> %publish-entries)
path->hash
gn:pinfo-publish-filename))
(simple-format #t "progress-callback: ~a ~a~%"
(gn:pinfo-publish-filename info) status)
(match status
((#:publish #:error)
(let ((entry (pinfo-publish-entry info)))
(simple-format #t (_ "Error publishing: ~a\n")
(gn:pinfo-publish-message info))
(publish-entry-error! entry)
(schedule-stop! entry)))
((#:publish #:completed)
;; only the root directories (e.g. store items) have SKS URIs
(when (gn:pinfo-publish-sks-uri info)
(let ((entry (pinfo-publish-entry info)))
(simple-format #t (_ "~A: published.~%")
(gn:pinfo-publish-filename info))
(publish-entry-complete! entry)
(schedule-stop! entry))))
((#:publish #:stopped)
(simple-format (current-error-port) "progress-cb: ~a~%" (gn:pinfo-publish-filename info))
(when (store-item? (gn:pinfo-publish-filename info))
(simple-format (current-error-port) " store-item!~%")
(when (every publish-entry-stopped? %publish-entries)
(simple-format (current-error-port) " every publish entry stopped:~% ~a~%" %publish-entries)
(gn:schedule-shutdown!)
(display " scheduled shutdown!\n" (current-error-port))
(print-state)
(force-output (current-error-port)))))
;;+TODO: add #:suspend and co
((#:publish (or #:start #:progress #:progress-directory))
*unspecified*)))
#;(define (sum-up)
"Print an overview of the publication."
(let ((failures (filter (compose (cut eq? #:aborted <>)
publish-entry-state)
%publish-entries))
(unknowns (filter (compose not publish-entry-stopped?)
%publish-entries))
(print-entry (compose (cut simple-format #t " ~A~%" <>)
publish-entry-id))
(entries-num (length %publish-entries)))
(when (not (null? failures))
(simple-format #t (_ "~A store item(s) werenât published:~%")
(length failures))
(map print-entry failures))
(when (not (null? unknowns))
(simple-format #t (_ "~A store item(s) have an unknown state:~%")
(length unknowns))
(map print-entry failures))
(simple-format #t (_ "~A/~A store items successfully published.~%")
(- entries-num (length failures)) entries-num)))
;;+FIXME: is running STOP-TASK a second time really needed?
;; GN:STOP-PUBLISH seem synchronous.
(define (stop-task _)
"Stop the various GNUnet services in the right order."
(simple-format (current-error-port) "stop-task: ~a~%" %publish-entries)
(print-state)
(force-output (current-error-port))
(sleep 1)
(cond (%identity
(gn:close-identity-service %identity)
(set! %identity #f))
;; All the publish handles should be stopped before closing the
;; filesharing handle.
(%publish-entries
(map (lambda (entry)
(when (not (publish-entry-stopped? entry))
(simple-format (current-error-port) " stopping ~a:~%" entry)
(force-output)))
%publish-entries)
(display " adding another stop task\n" (current-error-port))
(gn:add-task! stop-task))
(%filesharing
(display " will close filesharing\n" (current-error-port))
(force-output (current-error-port))
;;+TODO: add a hook here?
; (sum-up)
(gn:close-filesharing-service! %filesharing)
(set! %filesharing #f))))