Creare un semplice Dropbox con poche righe di codice e IPFS

Come sapete sono un grosso fan dei sistemi decentralizzati, e cosi’ tengo d’occhio tutto quel che esce, cercando di capire come potrei usarlo. In questi giorni sto smanettando con IPFS, e devo dire che alla fine (nonostante un livello di documentazione patetico), una volta capito cosa offre sono riuscito ad farci qualcosa.

Prima di mostrare il codice, vorrei dire una cosa: e’ un esempio. Esempio significa che deve essere un esempio, cioe’ una cosa che semplifica un processo di apprendimento. Se poi volete aggiungerci criptazione, multithread e tutto quanto, siete liberi di farlo. Quindi vi avviso:

L’esempio che faccio sotto pubblica i dati in maniera permanente in tutto il mondo e in chiaro. DUNQUE, non usatelo per fare il mirror di robe importanti, riservate, eccetera.

Prima di tutto, che cosa e’ IPFS. Indifferentemente da tutto il blablabla che gli autori ne fanno (tipo chiamarlo “file system interplanetario”), si tratta di una CDN che lavora usando DHT e Torrent in background. Significa che tutti i nodi possono accedere allo stesso materiale, che rimane in rete per sempre, cioe’ rimane immutabile , sino a quando ci sara’ un solo nodo ad ospitarne una copia. Quindi siete avvisati. Se usate il mio esempio per condividere delle cartelle, quelle rimarranno in rete potenzialmente per sempre, e non c’e’ modo di toglierle.

La prima cosa che dovete fare e’ installare IPNS sul vostro computer. Lo scaricate da qui: https://ipfs.io

Questo vi consentira’ di far partire un nodo della CDN sul vostro computer. Se tutto va bene e la vostra rete domestica non rompe le scatole (tipo UPNP, NAT , firewall e compagnia bella) allora il vostro nodo entra nella CDN pubblica. Se non lo fa, dovrete aprire la porta 4001 usando il vostro router.

Se non sapete farlo, non dovreste leggere questo articolo.

Andiamo avanti, e diamo per scontato che voi abbiate un nodo IPFS funzionante sul vostro computer. Non basta. IPFS ha una feature che era sperimentale ma oggi non e’ piu’ sperimentale, ma la chiamano sperimentale. Poi danno risposte di questo tipo:

Detto questo, occorre far partire ipfs con questo servizio sperimentale-ma-non-piu’-sperimentale avviato. Per farlo dovete avviare il vostro ipfs usando questo parametro:

ipfs daemon --enable-pubsub-experiment

Facendo in questo modo avete abilitato una cosa interessante: tutti quelli che faranno subscribe ad un certo topic leggeranno la stessa cosa. E questo e’ importante, perche’ alla fine dei conti siccome gli hash di IPFS sono come gli hash di Torrent, quando cambierete il contenuto della cartella da condividere dovrete comunicare a tutti gli altri che lo avete fatto, in modo che la aggiornino.

L’esempio che segue e’ di poche decine di righe: vi mostra quello che c’e’ da fare perche’ la magia avvenga, ma non e’ un servizio come dropbox. Significa che dovrete rendere asincrone le funzioni di invio e di ricezione per avere il tutto fatto in automatico. Come ho gia’ scritto, e’ un esempio, quindi deve rimanere semplice per essere capito.

Andiamo al dunque: il programma che vedete non fa altro che prendere una cartella, spararla sulla rete IPFS, cioe’ su una CDN, scrivere su un canale come ripescarla, e poi la cartella viene ripescata e clonata in /data.

E’ ovvio che se ci volete fare un servizio come Dropbox (ma senza server centrale) questo esempio va smanettato: un thread deve fare il refresh del folder e inviarlo ad IPFS se e’ cambiato, l’altro deve archiviare il vecchio folder e scaricare il nuovo, ogni volta che riceve una nuova versione.

Ma questo avrebbe reso l’esempio una cosa lunga e complicata: se siete interessati a fare questa cosa, sapete farlo da soli. Vi voglio solo evitare lo sbatti del “trial and errors”, visto che la documentazione fa cagare lo Zio Jeb che fuma la pipa sulla sedia a dondolo abbracciando una doppietta nel tramonto della Luisiana.

Allora, il codice e’ questo:

package main

import (
	"fmt"
	"os"
	"time"

	shell "github.com/ipfs/go-ipfs-api"
)

var sh *shell.Shell
var _ = time.ANSIC
var topic = "d9f0af67-da62-430f-8960-bd9779323454"
var fold string


func main() {

	fmt.Println("Creating new API shell")
	sh = shell.NewShell("127.0.0.1:5001")

	if sh.IsUp() {
		fmt.Println("Shell Created, peer is up: ", sh.IsUp())
	} else {
		fmt.Println("Cannot create Shell, peer is down!")
		os.Exit(1)
	}

	bus, bErr := sh.PubSubSubscribe(topic)
	if bErr != nil {
		fmt.Println("Subscribed to topic failed: ", bErr.Error())
	} else {
		fmt.Println("Successfully subscribed to topic: ", topic)
		os.Exit(2)
	}

	hash, _ := sh.AddDir("./testdir")
	fmt.Println("Directory added at hash :", hash)

	pErr := sh.PubSubPublish(topic, hash)
	if pErr != nil {
		fmt.Println("Cannot write on the topic: ", pErr.Error())
	} else {
		fmt.Println("Success writing on pubsub topic: ", topic)
	}

	m, mErr := bus.Next()
	if mErr != nil {
		fmt.Println("Problems reading from topic: ,", mErr.Error())
	} else {
		fold = fmt.Sprintf("%s", m.Data)
		fmt.Println("Folder address received: ", fold)
	}

	sh.Get(fold, "./data")
	fmt.Println("Directory retrieved")

}

come potete vedere, e’ un programmino semplice semplice coi parametri hardcoded. Dovete solo salvarlo in un file main.go e fare un go get nel folder.

La prima cosa che fara’ e’ di connettersi con il vostro demone IPFS (che supponiamo girare su localhost) , oppure morire nel tentativo. Ecco sotto il momento eroico.

fmt.Println("Creating new API shell")
	sh = shell.NewShell("127.0.0.1:5001")

	if sh.IsUp() {
		fmt.Println("Shell Created, peer is up: ", sh.IsUp())
	} else {
		fmt.Println("Cannot create Shell, peer is down!")
		os.Exit(1)
	}

Fatto questo, dobbiamo fare in modo che il programma si iscriva al canale grazie al quale sara’ avvisato che una nuova versione della directory e’ disponibile. Il seguente blocco lo fa. Ovviamente, se il vostro futuro dropbox avra’ tante macchine, ognuna dovra’ sottoscrivere lo stesso topic. Io l’ho configurato con una stringa casuale molto lunga, che e’ difficile indovinare, ma non e’ sufficiente per una vera sicurezza. Quindi nel vero programma dovrete anche criptare i messaggi in qualche modo. La connessione verso questo “canale” l’ho chiamata “bus”. La ritroveremo.

bus, bErr := sh.PubSubSubscribe(topic)
	if bErr != nil {
		fmt.Println("Subscription to topic failed: ", bErr.Error())
	} else {
		fmt.Println("Successfully subscribed to topic: ", topic)
		os.Exit(2)
	}

Adesso dobbiamo fare la prima parte del porco lavoro: aggiungiamo il folder che vogliamo condividere. Per questo esempio si tratta di una cartella diversa da quella ove riceviamo i files, perche’ e’ un oggetto semplice. Idealmente queste poche righe di codice ciclano di continuo nella stessa cartella.

    hash, _ := sh.AddDir("./testdir")
	fmt.Println("Directory added at hash :", hash)

	pErr := sh.PubSubPublish(topic, hash)
	if pErr != nil {
		fmt.Println("Cannot write on the topic: ", pErr.Error())
	} else {
		fmt.Println("Success writing on pubsub topic: ", topic)
	}

Queste righe non fanno altro che pubblicare la vostra cartella sulla CDN distribuita , (un pochino come quando aggiungete un file su torrent) , e restituisce l’hash. Attenzione, perche’ adesso questa cartella e’ visibile a chiunque abbia un server IPFS che giri sul proprio computer.

Quindi abbiamo detto che abbiamo pubblicato la cartella e abbiamo scritto sul bus il suo hash. Adesso dobbiamo solo leggere l’hash e basandoci su questo dobbiamo scaricare la cartella. Insomma, dobbiamo metterci in ascolto sul canale (bloccante) che chiamiamo “bus” e leggere l’hash. Poi dobbiamo scaricare i contenuti in una data cartella.

m, mErr := bus.Next()
	if mErr != nil {
		fmt.Println("Problems reading from topic: ,", mErr.Error())
	} else {
		fold = fmt.Sprintf("%s", m.Data)
		fmt.Println("Folder address received: ", fold)
	}

	sh.Get(fold, "./data")
	fmt.Println("Directory retrieved")

E puf, ecco fatto. bus.Next() ha atteso il prossimo messaggio e quando ha avuto il nuovo hash ha scaricato daccapo la cartella.

L’esempio e’ davvero banale, ma vi mostra tutti gli ingredienti principali per scrivere un demone che , usando IPFS, sia in grado di replicare una cartella ovunque nel mondo o quasi. Ovviamente, mancano diverse cose cruciali:

  • Il protocollo di pubsub, detto “floodsub”, puo’ replicare i messaggi, e lo fara’ quasi sicuramente. Questo significa che dovreste implementare voi una qualche sicurezza sulla versione che avete gia’. Se ogni demone ricarica la cartella ogni minuto, deve pubblicare una nuova versione solo se e’ diversa dalla precedente. Nel ciclo loop che scriverete, quindi dovrete inserire questa sicurezza.
  • La API “sh.Get” non capisce la rimozione. Significa che di per se’, se un peer ha rimosso un file e nella nuova versione esso manca, non verra’ rimosso dalla cartella di destinazione. Questo significa che tocca a voi ruotare le cartelle per scaricare tutto daccapo, o implementare qualche tipo di diff. Potete anche usare messaggi piu’ complessi del semplice hash, spiegando ai vostri peer cosa sia cambiato.
  • Chiaramente, chiunque indovini il nome del topic potrebbe conoscere tutto il gioco. Vi consiglio quindi di mitigare il problema usando dei nomi molto lunghi e difficili da indovinare.
  • Offuscare il topic non basta: sarebbe consigliato che voi criptiate il messaggio che state trasferendo , in modo che l’hash sia a sua volta illeggibile.
  • Ma anche offuscare l’hash non basta, visto che tutto il contenuto e’ comunque online per sempre. E’ difficilissimo trovarlo, certo, ma e’ online. Quindi dovreste criptare la cartella PRIMA di inviarla.

Come vedete , il codice e’ molto primitivo, ma la parte di trasporto che vi consente di sparare una cartella “nella CDN” e poi scaricarla e’ molto semplice da scrivere. La parte complicata di IPFS e’ che il suo sistema di DNS (IPNS) fa cagare al punto di essere inutilizzabile, sia perche’ e’ molto lento nel propagare le entries (rispetto a pubsub) , sia perche’ e’ tremendamente inefficiente nel TTL: il vostro alias non e’ decisamente durevole, a meno che non vi curiate manualmente (o con cron) di rinnovarlo.

Al contrario, il problema del contenuto mutevole e’ semplice da risolvere se usiamo un canale di tipo PubSub per spedire il messaggio a zonzo, con dentro il nostro nuovo hash.

Personalmente intendo usarlo per una app che usi IPFS per replicare dropbox, ma senza server centrali. Si tratta di scrivere tutta la parte mancante che ho menzionato sopra. Ma non e’ complicatissimo, e’ solo una questione di trovare il tempo. Ma come potete vedere, ci si possono fare le cose piu’ disparate. O meglio , si potrebbe, se la documentazione non richiedesse cosi’ tanta pazienza per essere decodificata.

Quindi pubblico questo messaggio sperando di risparmiarvi delle bestemmie.

Perche’ usare questo e non, che so io, syncthing ? La risposta e’ nel versioning: siccome tutto quello che scrivete e’ immutabile, per avere a disposizione tutte le versioni precedenti non dovete salvarne una copia da qualche parte, o salvare i diff. Dovete solo tenere un log di ogni hash, insieme alla data. E avrete la copia della cartella da scaricare nel caso in cui vogliate tornare indietro, se necessario manualmente, usando il comando che leggete qui

ipfs get <hash>

Quindi in pratica il vostro versioning non consiste nel caricare sul computer destinazione una copia delle vecchie cartelle, ma nel salvare un tracciato degli hash passati.

In definitiva, poi, se non avete configurato le macchine IPFS come gateway (o fate un “pin” del folder usando sh.Pin() ), i demoni ipfs non rimuoveranno il contenuto, continuando a fare il seed come fa torrent. Di conseguenza, tutto viene salvato, ma a voi non serve averne una copia: e’ il vantaggio di usare una CDN immutabile come infrastruttura. Fa tutto la cdn.

Per fare il pin del folder dovrete aggiungere poche righe al codice, poco dopo aver aggiunto il folder:

    hash, _ := sh.AddDir("./testdir")
	fmt.Println("Directory added at hash :", hash)
	
	pinErr :=  sh.Pin(hash)
	if pinErr != nil {
		fmt.Println("Pinning failed: ", pinErr.Error() )
	}else{
		fmt.Println("Folder Pinned" )
	}

Di per se’, usando IPFS e golang, scrivere un servizio come dropbox non e’ complicatissimo.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *