Synchronisation de répertoires distants + AjaXplorer

Ces derniers jours, j’ai voulu régler la problématique suivante :
Avoir la possibilité de faire des sauvegardes des documents (Papiers, musique, ebooks etc.) qui sont chez moi, sur mon petit media center. Le plus important étant de faire une sauvegarde distante, j’avais le choix entre faire ces sauvegardes sur le serveur que j’ai chez OVH, et les faire chez un hébergeur qui propose des solutions prêtes à l’emploi (style Dropbox). Comme je n’aime pas du tout le principe des Dropbox-like, et que je préfère faire les choses moi-même (on apprend généralement plus de choses…), j’ai choisi la première solution. En plus de la problématique des sauvegardes, je souhaitais avoir un accès simple aux données que je sauvegarde en ligne, de la même façon que j’ai un accès simple aux données de mon media center (Samba pour les fichiers, Homeplayer pour les medias, via la Freebox). Ayant un accès aux sauvegardes en ligne, je souhaitais également pouvoir ajouter et supprimer des fichiers en ligne, et que ça soit répercuté sur mon media center. Il ne s’agirait donc plus vraiment d’une sauvegarde, mais plutôt d’une synchronisation bidirectionnelle.

La synchronisation

Ceux qui ont de la synchronisation unidirectionnelle à faire connaissent certainement rsync, qui est un très bon outil pour faire des sauvegardes via ssh. Seulement, il n’est pas adapté à la synchronisation bidirectionnelle.
J’ai donc cherché un autre outil, et je l’ai trouvé : unison (je le dis comme s’il s’agissait d’une grande découverte, mais je suppose que les personnes qui ont déjà dû faire de la synchronisation bidirectionnelle connaissent déjà l’outil…).

Unison

Unison fonctionne via un fichier de configuration (un fichier de configuration par dossier à synchroniser… récursivement, évidemment).
Personnellement, j’ai eu besoin d’un fichier de configuration très simple, car je voulais synchroniser l’intégralité des répertoires (pas de fichiers à ignorer à l’intérieur des répertoires). Voici donc à quoi ressemble un fichier de configuration (« photos.prf ») sur mon serveur OVH :

root=ssh://mael@media-center:22//mnt/data/Photos
root=/home/www-data/Photos/

Bien entendu, si on souhaite par la suite utiliser unison en mode non-interactif, il faut effectuer un échange de clés SSH entre les deux serveurs.

On indique donc qu’on veut synchroniser le répertoire « /mnt/data/Photos » du media center avec le répertoire « /home/www-data/Photos » du serveur.
Pour commencer la synchronisation, il suffit alors de lancer la ligne de commande suivante (il faut que le fichier « photos.prf » soit situé dans le répertoire ~/.unison) :

unison photos.prf

Unison va alors comparer les contenus des dossiers – ça peut être assez long la première fois, s’il y a beaucoup de fichiers – , puis demander à l’utilisateur ce qu’il souhaite faire. Personnellement, j’ai systématiquement choisi l’option « g », qui indique à unison de se débrouiller à « merger » les deux répertoires.
Une fois la première synchronisation effectuée, il est possible de lancer unison en mode « silent », pour qu’il synchronise les répertoires d’une manière non interactive. L’option « -silent » sert à ça :

unison -silent photos.prf

Je conseille, si possible, de synchroniser les deux répertoires à la main avant de lancer unison la première fois.

On peut alors imaginer de lancer unison régulièrement grâce à un cron, sachant qu’il n’y a pas à craindre de conflits : si unison se lançait 2 fois en même temps pour la synchronisation des 2 mêmes répertoires, la deuxième synchronisation serait bloquée :

The file /home/user/.unison/lk89675453041cac3ae95768a69fc3f068 on host media-center should be deleted
The file /home/user/.unison/lka0e37cd07ee834a1856ac0d86f24b840 on host serveur should be deleted

Cette solution avec le cron ne me plaît qu’à moitié : je préfère avoir une synchronisation en temps réel : dès qu’un fichier est ajouté sur le media center ou le serveur, la modification est répercutée sur l’autre. Il y a éventuellement la possibilité de lancer des cron très régulièrement, mais c’est sale, et ça doit certainement mobiliser des ressources inutilement.
J’ai donc cherché un moyen de synchroniser en temps réel avec unison.

Synchronisation en temps réel

Unison propose une option qui permet de faire de la copie en temps réel. Pour cela, il suffit normalement d’ajouter l’option : « -repeat watch » à la commande unison. Mais visiblement, l’option ne fonctionne pas sous la version de Unison utilisée par debian Squeeze : je n’ai jamais réussi à faire fonctionner la synchronisation en temps réel avec la version d’unison présente dans les dépôts.
J’ai donc commencé par créer un script bash utilisant Inotify. Ca fonctionnait plutôt bien, mais j’ai cherché une meilleure solution. Et la seule que j’aie trouvée, c’est de compiler unison depuis le trunk de leur svn.

Il faut d’abord installer les dépendances :

# apt-get install python-pyinotify ocaml subersion emacs23-bin-common

Je ne suis pas certain que « emacs23-bin-common » soit nécessaire : la compilation semblait bien me générer un binaire sans cette dépendance, mais une erreur était tout de même lancée, alors dans le doute…

On peut ensuite récupérer les sources :

# svn checkout https://webdav.seas.upenn.edu/svn/unison/trunk

Puis compiler :

# cd trunk && make NATIVE=true UISTYLE=text

Il faut ensuite copier les deux fichiers src/unison et src/fsmonitor.py dans le PATH

Désormais, la synchronisation en temps réel fonctionne bien. Il suffit donc de modifier la commande pour qu’elle ressemble à ça :

unison photos.prf -repeat watch

Et la synchro en temps réel fonctionne parfaitement. Magique, non ?
On peut également faire un petit script de démarrage :

#!/bin/sh
### BEGIN INIT INFO
# Provides:          unison_sync
# Required-Start:    $local_fs $remote_fs $syslog $network $named
# Required-Stop:     $local_fs $remote_fs $syslog $network $named
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Starts unison sync at boot time
### END INIT INFO

UNISON_CONFIGS="bouquins.prf documents.prf"
PID_DIR="/var/run/unison"
PID_FILE="unison_sync.pid"
LOG_FILE="/var/log/unison_sync.log"

stop() {

        if [ ! -f $PID_DIR/$PID_FILE ]; then
                echo -e "Unison is not running"
                exit 1
        fi
        echo -n "Stopping unison processes... "
        cat $PID_DIR/$PID_FILE | while read PID
        do
                echo -n "Killing $PID... "
                kill $PID
                echo -n "OK. "
        done
        killall fsmonitor.py
        rm $PID_DIR/$PID_FILE
        log "Unison stopped"
}

start() {
        if [ ! -d $PID_DIR ]; then
                mkdir $PID_DIR
        fi
        if [ -f $PID_DIR/$PID_FILE ]; then
                echo -e "Unison is running with pid(s) :\n$(cat $PID_DIR/$PID_FILE)"
                exit 1
        fi
        for conf in $UNISON_CONFIGS
        do
                nohup unison $conf -repeat watch -logfile $LOG_FILE >> $LOG_FILE 2>&1 &
                echo "$! " >> $PID_DIR/$PID_FILE
                log "Started unison for $conf"
        done
}

log() {
        echo "[$(date '+%d-%m-%y %T')] : $1" >> $LOG_FILE
        echo $1
}

case $1 in
        start)
                start
        ;;
        stop)
                stop
        ;;
        *)
                echo "Usage : `basename $0` {start|stop}"
                exit 1
        ;;
esac

Pour debian, à placer dans /etc/init.d
Attention : avec ce mode de fonctionnement, seuls les fichiers ajoutés/modifiés/supprimés après le lancement de la commande sont synchronisés. Il faut donc lancer la commande une fois que les deux dossiers sont déjà synchronisés.

Accès aux fichiers du serveur

On a donc un système qui permet de synchroniser en temps réel 2 répertoires distants. Il faudrait maintenant un moyen simple (et agréable) d’accéder aux fichiers qui sont sur le serveur en ligne. J’ai d’abord pensé à Webdav. C’est simple, et généralement bien intégré au sein d’un explorateur de fichier. Seulement, et j’ignore pourquoi exactement, Webdav s’avère hyper lent au niveau de la navigation et de la copie de fichiers (testé sous Crunchbang Linux, avec thunar). J’ai donc cherché quelque chose de rapide, et aussi de plus « user friendly » (je ne serai à priori pas le seul à accéder aux fichiers). Et j’ai trouvé AjaXplorer. AjaXplorer est une application développée en PHP côté serveur (pas de base de données), et possède une interface entièrement en Ajax côté client.
Voilà à quoi ça ressemble :
.
Dans l’administration de l’application, on peut ajouter des « dépôts », qui vont permettre d’autoriser l’accès à des dossiers situés sur le serveur, entre autres, car on peut également ajouter des dépôts correspondant à un serveur FTP, un partage samba, un partage webdav, et d’autres encore. Les fichiers présents dans les dépôts sont ensuite directement accessible dans le navigateur, et, petit bonus, sont également accessible via webdav, si l’on active l’option. J’ai beaucoup aimé également la possibilité de partager un fichier : on peut créer un lien (virtuel) vers un fichier : AjaXplorer génère un lien, pour lequel on peut indiquer une durée de validité, et qu’on peut protéger par un mot de passe. Il suffit alors de donner ce lien à la personne avec qui ont veut partager le fichier.

On peut aussi créer des utilisateurs et des rôles, et leur affecter des droits sur les dépôts.
AjaXplorer permet également d’éditer les fichiers en ligne (fichier textes, documents doc ou odt), de lire les MP3 directement, de naviguer entre les images via un « viewer » et de visualiser les PDF. Comme pour un navigateur de fichiers, on peut forcer l’ouverture d’un type de fichiers par un éditeur particulier.
Une fonctionnalité très intéressante est la possibilité d’ajouter des fichiers directement par un glisser-déposer (drag-and-drop) dans le navigateur. Ca ne semble pas fonctionner sur Iceweasel, sous GNU/Linux : j’ai testé avec différentes versions, pas moyen de le faire fonctionner, alors que ça fonctionne sous chromium, et sous Firefox pour d’autres OS (A noter que j’ai essayé sur d’autres appli, comme wordpress, et que ça ne fonctionne pas non plus, donc le soucis ne vient pas de AjaXplorer).

Evidemment, il ne faut pas s’attendre à naviguer entre les dossiers d’AjaXplorer d’une manière aussi fluide que dans un navigateur de fichiers locaux, mais j’ai quand même trouvé l’utilisation agréable. Il faudrait tester sur des machines moins puissantes comme un netbook, pour voir si le javascript ne ralentit par trop la navigation.

Conclusion

L’association d’un petit outil très puissant (unison), et d’une très bonne application web (ajaXplorer) permet de répondre totalement à l’objectif que je m’étais fixé. Le tout en utilisant uniquement des solutions libres, bien entendu.
Il faut noter que AjaXplorer est entièrement bâtie sur une architecture faite de plugins : d’après ce que j’ai vu, le moindre fonctionnement de l’application fait appel à un plugin, et il est possible de créer des interdépendances entre les plugins. Du coup, le développement d’un plugin pour répondre à un besoin spécifique (j’ai par exemple pensé à un plugin qui permettrait de faire de l’édition/compilation/visualisation de LaTeX) est grandement facilité.
A voir si la solution tiendra dans le temps. Par exemple, si unison plante régulièrement et arrête la synchronisation, ça va vite m’énerver. Mais il n’y a pas de raison, à priori.

Débloquer gnome-keyring avec SLiM

Un pense-bête plus qu’un véritable article.

Quand on utilise SLiM en lieu et place de gdm (ou tout autre gestionnaire de connexion), mais que malgré tout, on continue d’utiliser le network manager de gnome, il peut être assez pénible de devoir débloquer à la main gnome-keyring à chaque ouverture de session.
Il est possible de remédier à cela en utilisant PAM.
Il faut que les paquets gnome-keyring et libpam-gnome-keyring soient installés (et slim aussi, ça va de soi).
Il suffit ensuite de créer le fichier suivant : /etc/pam.d/slim, s’il n’existe pas, et de le remplir de la manière suivante :


#
# The PAM configuration file for the SLiM graphical login manager
#

# Disallows other than root logins when /etc/nologin exists
# (Replaces the `NOLOGINS_FILE' option from login.defs)
auth       requisite  pam_nologin.so

# This module parses environment configuration file(s)
# and also allows you to use an extended config
# file /etc/security/pam_env.conf.
#
# parsing /etc/environment needs "readenv=1"
session       required   pam_env.so readenv=1
# locale variables are also kept into /etc/default/locale in etch
# reading this file *in addition to /etc/environment* does not hurt
session       required   pam_env.so readenv=1 envfile=/etc/default/locale

# Standard Un*x authentication.
@include common-auth

# This allows certain extra groups to be granted to a user
# based on things like time of day, tty, service, and user.
# Please edit /etc/security/group.conf to fit your needs
# (Replaces the `CONSOLE_GROUPS' option in login.defs)
auth       optional   pam_group.so

# Uncomment and edit /etc/security/time.conf if you need to set
# time restrainst on logins.
# (Replaces the `PORTTIME_CHECKS_ENAB' option from login.defs
# as well as /etc/porttime)
# account    requisite  pam_time.so

# Uncomment and edit /etc/security/access.conf if you need to
# set access limits.
# (Replaces /etc/login.access file)
# account  required       pam_access.so

# Sets up user limits according to /etc/security/limits.conf
# (Replaces the use of /etc/limits in old login)
session    required   pam_limits.so

# SELinux needs to intervene at login time to ensure that the process
# starts in the proper default security context.
# Uncomment the following line to enable SELinux
# session required pam_selinux.so multiple

# Standard Un*x account and session
@include common-account
@include common-session
@include common-password

auth	optional	pam_gnome_keyring.so
session	optional	pam_gnome_keyring.so  auto_start

Les deux dernières lignes débloquent gnome-keyring.
Plus qu’à se déconnecter et se reconnecter, et ça devrait fonctionner.

Télécommande universelle avec arduino. 2 – Une appli android

Introduction

Suite au précédent billet, où j’expliquais comment j’avais mis en place un petit serveur web sur un arduino, j’écris ce billet dans lequel je vais montrer une application android que j’ai faite pour contrôler le serveur web.

Une image de télécommande

Je voulais que l’interface de l’application ressemble à une télécommande, parce que c’est plus sympa. J’ai donc trouvé une image de télécommande que j’ai un peu modifiée :
image telecommande
(oui, c’est moche)
Il faut ensuite faire en sorte que les différents boutons de l’image soient cliquables dans l’application android. Pour cela, j’ai d’abord créé une classe « Bouton » :

public class Bouton {
	private Float xMin;
	private Float xMax;
	private Float yMin;
	private Float yMax;
	private int idCommande;

	public Bouton(int idCommande, Float xMin, Float xMax, Float yMin, Float yMax) {
		this.xMin = xMin;
		this.xMax = xMax;
		this.yMin = yMin;
		this.yMax = yMax;
		this.idCommande = idCommande;
	}

	public int getIdCommande() {
		return idCommande;
	}

	public void setIdCommande(int idCommande) {
		this.idCommande = idCommande;
	}
	public Float getxMin() {
		return xMin;
	}
	public void setxMin(Float xMin) {
		this.xMin = xMin;
	}
	public Float getxMax() {
		return xMax;
	}
	public void setxMax(Float xMax) {
		this.xMax = xMax;
	}
	public Float getyMin() {
		return yMin;
	}
	public void setyMin(Float yMin) {
		this.yMin = yMin;
	}
	public Float getyMax() {
		return yMax;
	}
	public void setyMax(Float yMax) {
		this.yMax = yMax;
	}

}

Cette classe contient donc les coordonnées d’un bouton, et la commande que ce bouton envoie.
Il faut ensuite faire une fonction qui construit une liste de boutons :

	/**
	 * Creation de la liste des boutons en fonction des coordonnées
	 */
	private void createButtonsList() {

		Bouton bouton0 = new Bouton(0, 219f, 263f, 99f, 137f);
		boutons.add(bouton0);

		Bouton bouton1 = new Bouton(1, 126f, 183f, 157f, 189f);
		boutons.add(bouton1);
		Bouton bouton2 = new Bouton(2, 215f, 273f, 157f, 189f);
		boutons.add(bouton2);
		Bouton bouton3 = new Bouton(3, 300f, 359f, 157f, 189f);
		boutons.add(bouton3);
		Bouton bouton4 = new Bouton(4, 126f, 183f, 210f, 241f);
		boutons.add(bouton4);
		Bouton bouton5 = new Bouton(5, 215f, 273f, 210f, 241f);
		boutons.add(bouton5);
		Bouton bouton6 = new Bouton(6, 300f, 359f, 210f, 241f);
		boutons.add(bouton6);

		Bouton bouton7 = new Bouton(7, 115f, 155f, 265f, 296f);
		boutons.add(bouton7);
		Bouton bouton8 = new Bouton(8, 329f, 370f, 265f, 296f);
		boutons.add(bouton8);

		Bouton bouton9 = new Bouton(9, 191f, 294f, 294f, 334f);
		boutons.add(bouton9);
		Bouton bouton10 = new Bouton(10, 133f, 186f, 316f, 391f);
		boutons.add(bouton10);
		Bouton bouton11 = new Bouton(11, 299f, 350f, 316f, 391f);
		boutons.add(bouton11);
		Bouton bouton12 = new Bouton(12, 191f, 294f, 374f, 413f);
		boutons.add(bouton12);

		Bouton bouton13 = new Bouton(13, 134f, 180f, 421f, 456f);
		boutons.add(bouton13);
		Bouton bouton14 = new Bouton(14, 302f, 347f, 421f, 456f);
		boutons.add(bouton14);

		Bouton bouton15 = new Bouton(15, 151f, 189f, 481f, 530f);
		boutons.add(bouton15);
		Bouton bouton16 = new Bouton(16, 221f, 259f, 481f, 530f);
		boutons.add(bouton16);
		Bouton bouton17 = new Bouton(17, 293f, 331f, 481f, 530f);
		boutons.add(bouton17);

		Bouton bouton18 = new Bouton(18, 151f, 189f, 552f, 601f);
		boutons.add(bouton18);
		Bouton bouton19 = new Bouton(19, 221f, 259f, 552f, 601f);
		boutons.add(bouton19);
		Bouton bouton20 = new Bouton(20, 293f, 331f, 552f, 601f);
		boutons.add(bouton20);

		Bouton bouton21 = new Bouton(21, 151f, 189f, 622f, 670f);
		boutons.add(bouton21);
		Bouton bouton22 = new Bouton(22, 221f, 259f, 622f, 670f);
		boutons.add(bouton22);
		Bouton bouton23 = new Bouton(23, 293f, 331f, 622f, 670f);
		boutons.add(bouton23);

		Bouton bouton24 = new Bouton(24, 151f, 189f, 694f, 743f);
		boutons.add(bouton24);
		Bouton bouton25 = new Bouton(25, 221f, 259f, 694f, 743f);
		boutons.add(bouton25);
		Bouton bouton26 = new Bouton(26, 293f, 331f, 694f, 743f);
		boutons.add(bouton26);

	}

J’ai récupéré les coordonnées en « cliquant » sur l’image et en affichant les coordonnées du point cliqué.
Il faut ensuite récupérer le bouton cliqué, quand un utilisateur appuie sur l’écran. Voici la fonction utilisée pour cela :

	private Bouton findBouton(Float x, Float y) {
		for (Bouton btn : boutons) {
			if (x > btn.getxMin() && x < btn.getxMax() && y > btn.getyMin() && y < btn.getyMax()) {
				return btn;
			}
		}

		return null;
	}

Où les paramètres x et y sont les coordonnées du « clic » de l’utilisateur. Quand on a récupéré le bon bouton, on peut alors envoyer une requête post vers le serveur :

	private void post(int command) {
		private HttpClient httpclient = new DefaultHttpClient();
		private HttpPost httppost = new HttpPost("http://192.168.1.20/ws");;
		try {
			List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
			nameValuePairs.add(new BasicNameValuePair("device", "1")));
			nameValuePairs.add(new BasicNameValuePair("command", String.valueOf(command)));
			httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
			HttpResponse response = httpclient.execute(httppost);
			HttpEntity entity = response.getEntity();
			InputStream is = entity.getContent();
			is.close();
		} catch (IOException e) {
			e.printStackTrace();
			Toast.makeText(getApplicationContext(), "Erreur lors de la connexion au serveur. Vérifier l'url", Toast.LENGTH_LONG).show();
		} catch (Exception e) {
			e.printStackTrace();
			Toast.makeText(getApplicationContext(), "Erreur inattendue", Toast.LENGTH_LONG).show();
		}

	}

Et voilà, on a tout ce qu’il faut pour commander le serveur depuis un client android.
J’ai fait une application un peu plus complète qui permet :

  • De régler l’url à utiliser
  • D’ajouter plusieurs télécommandes
  • De sélectionner une télécommande
  • D’envoyer des commandes au serveur

Voici le projet : lien, sous la forme d’un projet eclipse (c’est ma première application android, donc elle n’est pas forcément codée de façon optimale).
Après quelques tests, il s’avère que l’application est bien réactive, que la réception et le traitement d’une commande par le serveur se font très rapidement. Même avec des appuis répétés sur la télécommande, on ne ressent pas de lag. Reste à voir ce que ça donnera avec l’émetteur infrarouge (pas pour tout de suite, je déménage bientôt, et je n’aurai peut-être pas le temps de continuer le projet pour le moment).

Conclusion

La deuxième étape du projet de télécommande universelle est maintenant achevée. Il me reste :

  • A faire un circuit capable de décoder des signaux infrarouges
  • A faire un circuit capable de reproduire les signaux infrarouges décodés