;;; 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-11)
#:use-module ((srfi srfi-26) #:select (cut))
#:use-module (srfi srfi-37)
#:use-module ((rnrs bytevectors) #:select (string->utf8))
#:use-module (ice-9 match)
#:use-module (ice-9 ftw)
#:use-module (system foreign)
#:use-module (guix base32)
#:use-module (guix pki)
#:use-module (guix store)
#:use-module (guix ui)
#: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)
(define %publish #f)
(define %publish-completed? #f)
(define %publish-error? #f)
(define %publish-stopped? #f)
;;; utilities to scan a directory and collect each file
(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 ((res (gn:file->file-information% %filesharing path
%block-options #:index? #t)))
;;+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)))
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))))
(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)
(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 args
(leave (_ "failed to access ~A~%") config-file)))
(gn:call-with-scheduler
%config
(lambda (_)
(set! %identity
(gn:open-identity-service %config
(identity-callback pseudo paths)))
(gn:add-task! (get-stop-task (car paths))
#:delay (gn:time-rel #:seconds 30))))))
(define (identity-callback pseudo paths)
(lambda (ego name)
"Function called by GNUnetâs identity service. Itâs mapped on each
available ego."
(cond ((not name)
(set! %filesharing
(gn:open-filesharing-service %config "guix publish-gnunet"
(get-progress-callback
(car paths))))
(when (not (null? (cdr paths)))
(warning (_ "Additional store paths will be ignored.~%")))
(scan-&-publish (car paths)))
((string= pseudo name)
(set! %ego ego)))))
(define (scan-&-publish path)
"Scan each of the PATHS and start publishing them."
(define (start-publish-path store path)
(let ((filename (basename path))
(id (basename path)))
(set! %publish
(gn:start-publish %filesharing (scan-store-path store path)
#:namespace %ego #:identifier id))))
(with-error-handling
(with-store store
(start-publish-path store path))))
(define (get-progress-callback path)
(lambda (info status)
"Called by the filesharing service each time thereâs something to
report about one of our publications."
(define parent? (string=? path (gn:pinfo-publish-filename info)))
(match status
((#:publish #:start)
(when parent?
(simple-format #t (_ "Publishing ~A...~%") path)))
((#:publish #:completed)
;; only the root directories (e.g. store items) have SKS URIs
(when (gn:pinfo-publish-sks-uri info)
(set! %publish-completed? #:t)
(simple-format #t (_ "~A: published.~%") (gn:pinfo-publish-filename info))
(gn:add-task! (lambda (_)
(when %publish
(gn:stop-publish %publish)
(set! %publish #f))
#t))))
((#:publish #:stopped)
(when parent?
(set! %publish-stopped? #t)
(gn:schedule-shutdown!)))
((#:publish #:error)
(set! %publish-error? #t)
(simple-format #t (_ "Error publishing ~a:~%\t~a~%")
(gn:pinfo-publish-filename info)
(gn:pinfo-publish-message info))
(gn:schedule-shutdown!))
((#:publish (or #:progress #:progress-directory))
*unspecified*))))
(define (sum-up path)
(simple-format #t (if %publish-error?
(_ "~A: has not been published.~%")
(_ "~A: successfully published.~%")) path))
;;+FIXME: is running STOP-TASK a second time really needed?
;; GN:STOP-PUBLISH seem synchronous.
(define (get-stop-task path)
(lambda (_)
"Stop the various GNUnet services in the right order."
(force-output)
(usleep 200)
(when %identity
(gn:close-identity-service %identity))
;; All the publish handles should be stopped before closing the
;; filesharing handle.
(cond (%publish
(gn:stop-publish %publish)
(set! %publish #f)
(gn:add-task! stop-task))
(%filesharing ; last call to stop-task
;;+TODO: add a hook here?
(sum-up path)
(gn:close-filesharing-service! %filesharing)
(set! %filesharing #f)))))