initial commit

This commit is contained in:
Jakobus Schürz 2019-03-05 13:20:31 +01:00
commit d73ec91cf2
99 changed files with 8117 additions and 0 deletions

0
.builddeb Normal file
View file

0
.publish-git Normal file
View file

0
.update Normal file
View file

29
LICENSE Normal file
View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2017, Jakobus Schürz
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

59
Makefile Normal file
View file

@ -0,0 +1,59 @@
DIR=$(shell basename $(CURDIR))
VERSION=`gawk '$$1 == "Version:"{print $$2}' $(DIR)/DEBIAN/control`
ARCH=`gawk '$$1 == "Architecture:"{print $$2}' $(DIR)/DEBIAN/control`
COMMIT = $(shell date "+xe%Y%m%d_%H%M%S")
SUBDIRS := $(shell find $(DIR) -type d -print)
FILTER := $(abspath .git% %.deb .publish-git .builddeb %.swp Makefile)
FILTERORIG := $(abspath .git% %.deb .publish-git .builddeb %.swp Makefile) $(shell test -e noupdate.files && cat noupdate.files) /DEBIAN%
FILES := $(filter-out $(FILTER), $(abspath $(shell find . -mindepth 1 -type f -print) ))
ORIGS := $(filter-out $(FILTERORIG), $(realpath $(subst ./$(DIR),,$(shell sudo find . -mindepth 2 -type f -print))))
#FILES := $(filter-out $(FILTER), $(abspath $(shell find . -mindepth 1 -type f -exec echo {} \;) ))
#ORIGS := $(filter-out $(FILTERORIG), $(realpath $(subst ./$(DIR),,$(shell sudo find . -mindepth 2 -type f -exec echo {] \;))))
FILESGIT := $(filter-out $(abspath .git%), $(abspath $(shell find . -mindepth 1 -type f -print)))
#all: $(DIR)/DEBIAN/control
#$(DIR)/DEBIAN/control: $(FILES)
-include Makefile.repo
all: .builddeb
@#echo FILE $(FILESGIT)
.builddeb: $(FILES)
@#echo FILT $(FILTER)
@#echo FILE $(FILES)
@echo `gawk -f ../increment.awk $(DIR)/DEBIAN/control`
sed -e "s/^Version:.*/`gawk -f ../increment.awk $(DIR)/DEBIAN/control`/" $(DIR)/DEBIAN/control > $(DIR)/DEBIAN/control.tmp
mv $(DIR)/DEBIAN/control.tmp $(DIR)/DEBIAN/control
fakeroot dpkg-deb --build $(DIR) "$(DIR)_$(VERSION)_$(ARCH).deb"
ln -sf "$(DIR)_$(VERSION)_$(ARCH).deb" "$(DIR)_current_$(ARCH).deb"
aptly repo add xundeenergie "$(DIR)_$(VERSION)_$(ARCH).deb"
touch .builddeb
buildonlydeb: $(FILES)
@#echo FILT $(FILTER)
@#echo FILE $(FILES)
@echo `gawk -f ../increment.awk $(DIR)/DEBIAN/control`
sed -e "s/^Version:.*/`gawk -f ../increment.awk $(DIR)/DEBIAN/control`/" $(DIR)/DEBIAN/control > $(DIR)/DEBIAN/control.tmp
mv $(DIR)/DEBIAN/control.tmp $(DIR)/DEBIAN/control
fakeroot dpkg-deb --build $(DIR) "$(DIR)_$(VERSION)_$(ARCH).deb"
ln -sf "$(DIR)_$(VERSION)_$(ARCH).deb" "$(DIR)_current_$(ARCH).deb"
.update: $(ORIGS)
@#for i in $(ORIGS); do $$i;done
@echo "$(ORIGS)"
@echo "Copy originals to $(DIR)"
@for i in $(ORIGS); do sudo cp -uv $$i $(DIR)$$i;done
touch .update
.publish-git: $(FILESGIT)
fakeroot git add .
fakeroot git commit -m $(COMMIT) && git push origin master || exit 0
touch .publish-git
pull-git:
git pull origin || exit 0

30
Makefile.old Normal file
View file

@ -0,0 +1,30 @@
DIR=$(shell basename $(CURDIR))
VERSION=`gawk '$$1 == "Version:"{print $$2}' $(DIR)/DEBIAN/control`
ARCH=`gawk '$$1 == "Architecture:"{print $$2}' $(DIR)/DEBIAN/control`
COMMIT = $(shell date "+xe%Y%m%d_%H%M%S")
SUBDIRS := $(shell find $(DIR) -type d -print)
FILTER := $(abspath .git% %.deb .%)
FILTERORIG := $(abspath .git% %.deb) /DEBIAN%
FILES := $(filter-out $(FILTER), $(abspath $(shell find . -mindepth 1 -type f -print)))
ORIGS := $(filter-out $(FILTERORIG), $(realpath $(subst ./$(DIR),,$(shell find . -mindepth 2 -type f -print))))
FILESGIT := $(filter-out $(abspath .git%), $(abspath $(shell find . -mindepth 1 -type f -print)))
all: $(DIR)/DEBIAN/control
$(DIR)/DEBIAN/control: $(FILES)
echo DIR $(DIR)
sed -e "s/^Version:.*/`gawk -f ../increment.awk $(DIR)/DEBIAN/control`/" $(DIR)/DEBIAN/control > $(DIR)/DEBIAN/control.tmp
mv $(DIR)/DEBIAN/control.tmp $(DIR)/DEBIAN/control
sudo dpkg-deb --build $(DIR) "$(DIR)_$(VERSION)_$(ARCH).deb"
aptly repo add xundeenergie "$(DIR)_$(VERSION)_$(ARCH).deb"
update:
for i in $(ORIGS); do sudo cp -u $$i $(DIR)$$i;done
publish-git: $(FILESGIT)
sudo git add .
git commit -m $(COMMIT)
git push origin master

161
README.md Executable file
View file

@ -0,0 +1,161 @@
# mkbackup-btrfs
Make snapshots recursively from btrfs-subvolumes
This scripts are written in python3 and replace https://github.com/xundeenergie/mkbtrbackup
## Automatic installation with script
Start a debian-live session. Download a netboot.iso or any other stretch-live.iso
Take care, that you've installed btrfs-progs and kernel in the highest possible version.
Download and install the package mkbackup-btrfs.deb (always a symlink to the latest package) from https://github.com/xundeenergie/mkbackup-btrfs
Or just download and make it executeable
wget https://raw.githubusercontent.com/xundeenergie/mkbackup-btrfs/master/mkbackup-btrfs/usr/local/bin/create-btrfs-subs.sh
chmod a+x create-btrfs-subs-sh
Then create a new as big as possible partition and format it with btrfs.
If you want to use UEFI, your partition-table must be GPT. For UEFI you need an extra partition for ESP (Efi System Partition). Just lool on other places how to format a Disk for the usage with (U)EFI.
If you've created a big btrfs-partition, mount it in your live-system:
mount /dev/sdXY /mnt -osubvolume=/,compress
X is your drive, Y is the number oft the btrfs-partition on drive X.
then change to /mnt
cd /mnt
and run the script
sudo /path/to/download/create-btrfs-subs.sh
/path/to/download is where you've downloaded the script before.
You will end up in a chroot of your new installed ground-system. It is not bootable now.
Install a kernel, tzdata, initramfs, grub or refind, install other packages you need (firmware, network-manager, desktops...)
## Manually creation of the subvolume-structure, no installation!
You have to prepare your installation with some subvolumes.
First create a directory
mkdir -p /var/cache/btrfs_pool_SYSTEM
and
mkdir -p /var/cache/backup
The first is for the local HD, to mount the whole btrfs-partition, the btrfs-pool.
The second one is for the external HD to save the backup.
This two directories are hardcoded for the default-configuration in the python-skript.
Mount your btrfs-partition to /var/cache/btrfs_pool_SYSTEM
mount -t btrfs -ocompress=lzo /dev/sdXY /var/cache/btrfs_pool_SYSTEM
You neet two major subvolumes.
One for your system, which is snapshotted on every upgrade/update, on successfull boots and so on.
The other one for data you will need accurat even if you boot from an older snapshot (recover your system, you need /home, /var/spool accurat - it's user-data!).
The first is called for example: "@debian"
The second one is hardcoded with "`__ALWAYSCURRENT__`"
```
btrfs subvol create /var/cache/btrfs_pool_SYSTEM/@debian
btrfs subvol create /var/cache/btrfs_pool_SYSTEM/__ALWAYSCURRENT__
```
The system mounts the default-subvolume on bootup. So be sure, that @debian is your default-subvolume.
prepare your /etc/fstab to mount the always current subvolumes from `__ALWAYSCURRENT__`
Create the following subvolumes there:
home
opt
srv
usr-local
var-cache
var-lib-mpd
var-lib-named
var-log
var-opt
var-spool
var-spool-dovecot
var-tmp
var-www
Look at the fstab-example for mounting all this subvolumes.
Copy your data to this subvolumes
cp --reflink=always -ar source destination
Reboot an check if all this subvolumes are mounted correctly. You can clean the original directories in @debian while they are overmounted with the new ones, if you go to /var/cache/btrfs_pool_SYSTEM/@debian/sub/vol/ume and delete it there.
Be carefull. If you copy the systemd-units to your system, the timers and units are enabled!! Disable all the snapshotting with:
systemctl stop mkbackup.target
and disable it
systemctl disable mkbackup.target
Enable and start it if all the data and subvolume-structure is correct and working.
You can make your first snapshot with:
systemctl start mkbackup@manually.service
##mlocate:
To avoid, that mlocate searches the backups, edit its configuration
/etc/updatedb.conf
PRUNE_BIND_MOUNTS="yes"
# PRUNENAMES=".git .bzr .hg .svn"
PRUNEPATHS="/tmp /var/spool /media /backup /backup-local /var/cache/backup /var/cache/btrfs_pool_SYSTEM"
PRUNEFS="NFS nfs nfs4 rpc_pipefs afs binfmt_misc proc smbfs autofs iso9660 ncpfs coda devpts ftpfs devfs mfs shfs sysfs cifs lustre tmpfs usbfs udf fuse.glusterfs fuse.sshfs curlftpfs fuse.MksnapshotFS.py"
==Ignore Subvolumes on creating a System-Snapshot
If you want to ignore a subvolume from making a backup-Snapshot, you can handle this on several ways.
The easiest way is to drop a Drop-In-File for example like this for a guest-session-home:
editor /etc/mkbackup-btrfs.conf.d/guestsession.conf
[DEFAULT]
ignore = +/home/gast
The filename doesn't matter. But it must end in ".conf"
This can be done by placing such a Drop-In with a debian-package, or manually.
You can choose, if it should be ignored generally or only on specific interval-snapshots.
The example above appends /home/gast to an existing list of ignored snapshots.
[DEFAULT]
ignore = /home/gast
this replaces every ignore-list with only this subvolume "/home/gast"
the "+" before the subvolume means, that the subvolume(s) given are being appended to a existing list. Without a "+", the list is replaced by the given.
If you want to ignore a specific subvolume additionally on a specific interval (f.e. /var/www should not be backed up on hourly snapshots), place this in a file:
[hourly]
ignore = +/var/www
You can set the ignore-Option in every section also in /etc/mkbackup-btrfs.conf
It works the same way. DEFAULT is valid to every interval, and default-Values get overwritten or extended (missing or given "+") by the interval-sections
To ignore several subvolumes when using mkbackup-btrfs from commandline, just use the -i option (more than once)
mkbackup -v -t manually -i /home/guest -i /var/www create SNP @debian
This overrides settings from config-files.
TODO:
- Regular-Expressions for ignoring subvolumes. Test and describe it in Todo
=Changelog
18.9.2016:
-added experimental new code btrfssubvols.py - not working yet!!
-new library in /usr/lib/python3/dist-packages for config-parsing
-new Fuse-Filesystem for usermounting the backups and snapshots in $HOME/backup

5
files/DEBIAN/conffiles Normal file
View file

@ -0,0 +1,5 @@
/etc/apt/apt.conf.d/00mksnapshot_btrfs
lib/systemd/system/timer-aptupgrade.target
lib/systemd/system/timer-aptupgrade.timer
lib/systemd/system/timer-plugin.target
lib/systemd/system/timer-plugin.timer

14
files/DEBIAN/control Normal file
View file

@ -0,0 +1,14 @@
Package: mkbackup-btrfs
Version: 0.8.48
Section: backup
Priority: extra
Architecture: all
Maintainer: Jakobus Schürz <xundeenergie@gmail.com>
Homepage: http://github.com/xundeenergie/mksnapshot
Provides: mkbackup-btrfs
Depends: python3, python3-progressbar, python3-psutil, btrfs-progs, systemd-alt-cron, python3-paramiko, python-paramiko, python-fuse, deb-systemd-helper-new, system-notification
Recommends: python3-pip, xe-base-config
Suggests: install-on-btrfs
Description: Backup-Suite for btrfs written in python3
This backup-suite handles all around making backups on btrfs-filesystems, including a gnome-shell-extension
This version includes also handling for remote (via ssh) locations for source and/or backup.

93
files/DEBIAN/postinst Executable file
View file

@ -0,0 +1,93 @@
#!/bin/sh
# postinst script for webpy-example
#
# see: dh_installdeb(1)
#_DEB_SYSTEMD_HELPER_DEBUG=1
set -e
# summary of how this script can be called:
# * <postinst> `configure' <most-recently-configured-version>
# * <old-postinst> `abort-upgrade' <new version>
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
# <new-version>
# * <postinst> `abort-remove'
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
# <failed-install-package> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
# source debconf library
#. /usr/share/debconf/confmodule
# For user-services only, for earch target an extra section! for system-services user deb-systemd*
# put in full unit-name. for example "mkbackup@hourly.service"
case "$1" in
configure)
# normal systemd-units
#SERVICES="mkbackup.target backup.automount var-cache-backup.automount btrfs-scrub@var-cache-btrfs_pool_SYSTEM.service"
SERVICES="mkbackup.target backup.automount var-cache-backup.automount"
for s in $SERVICES; do
deb-systemd-helper-new unmask $s >/dev/null || true
if deb-systemd-helper-new --quiet was-enabled $s; then
# Enables the unit on first installation, creates new
# symlinks on upgrades if the unit file has changed.
deb-systemd-helper-new enable $s >/dev/null || true
else
# Update the statefile to add new symlinks (if any), which need to be
# cleaned up on purge. Also remove old symlinks.
deb-systemd-helper-new update-state $s >/dev/null || true
fi
done
# instantiated systemd-units
DEVICE=$(awk '$9 == "btrfs" && $5 == "/" {gsub("/dev/","",$10);print $10}' /proc/self/mountinfo)
ROT=$(cat /sys/block/$(printf '%s' "$DEVICE" | sed 's/[0-9]//g')/queue/rotational)
if test $ROT -eq 0; then
INTERVALS="hourly daily weekly aptupgrade plugin afterboot manually"
else
INTERVALS="aptupgrade plugin manually"
fi
CINT=$(for i in $INTERVALS;do echo "mkbackup@${i}.service" ; done)
SERVICES="btrfs-scrub@var-cache-btrfs_pool_SYSTEM.service mkbackup-conf@mkbackup\x2dbtrfs.path mkbackup-conf@mkbackup\x2dbtrfs.service $CINT"
for s in $SERVICES; do
#deb-systemd-helper-new-xe unmask $s >/dev/null || true
deb-systemd-helper-new unmask $s || true
# was-enabled defaults to true, so new installations run enable.
echo "enable $s"
if deb-systemd-helper-new --quiet was-enabled $s; then
# Enables the unit on first installation, creates new
# symlinks on upgrades if the unit file has changed.
deb-systemd-helper-new enable $s >/dev/null || true
else
# Update the statefile to add new symlinks (if any), which need to be
# cleaned up on purge. Also remove old symlinks.
deb-systemd-helper-new update-state $s >/dev/null || true
fi
done
;;
abort-upgrade|abort-remove|abort-deconfigure)
exit 0
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

53
files/DEBIAN/postrm Executable file
View file

@ -0,0 +1,53 @@
#! /bin/sh
set -e
# In case this system is running systemd, we make systemd reload the unit files
# to pick up changes.
if [ -d /run/systemd/system ] ; then
systemctl --system daemon-reload >/dev/null || true
fi
# System-Services:
# put in full unit-name. for example "mkbackup@hourly.service"
#SERVICES="mkbackup.target backup.automount var-cache-backup.automount btrfs-scrub@var-cache-btrfs_pool_SYSTEM.service mkbackup@manually.service mkbackup@aptupgrade.service mkbackup@daily.service mkbackup@weekly.service mkbackup@monthly.service mkbackup@plugin.service mkbackup@manually.service"
SERVICES="mkbackup.target backup.automount var-cache-btrfs_pool_SYSTEM.automount"
INTERVALS="hourly daily weekly aptupgrade plugin afterboot manually"
CINT="$(for i in $INTERVALS;do echo "mkbackup@${i}.service" ; done)"
INSTSERVICES="btrfs-scrub@var-cache-btrfs_pool_SYSTEM.service mkbackup-conf@mkbackup\x2dbtrfs.path mkbackup-conf@mkbackup\x2dbtrfs.service"
case "$1" in
purge)
# systemctl disable $SERVICES $INSTSERVICES
# for s in $INSTSERVICES;do
# rm -rf /lib/systemd/system/${s}
# done
if [ -x "/usr/bin/deb-systemd-helper-new" ]; then
deb-systemd-helper-new purge $SERVICES $INSTSERVICES $CINT >/dev/null
deb-systemd-helper-new unmask $SERVICES $INSTSERVICES $CINT >/dev/null
fi
;;
abort-upgrade)
;;
remove)
# systemctl mask $SERVICES
# for s in $INSTSERVICES;do
# ln -s /dev/null /lib/systemd/system/${s}
# done
if [ -x "/usr/bin/deb-systemd-helper-new" ]; then
deb-systemd-helper-new mask $SERVICES $INSTSERVICES $CINT >/dev/null
fi
;;
upgrade|failed-upgrade|abort-install|disappear)
;;
*)
echo "postrm called with unknown argument \`$1'" >&2
exit 0
;;
esac

View file

@ -0,0 +1,2 @@
//Take a snapshot befor upgrading the system
DPkg::Pre-Invoke {"/bin/systemctl start timer-aptupgrade.timer";};

View file

@ -0,0 +1,2 @@
[DEFAULT]
ignore=+var-lib-docker

View file

@ -0,0 +1,34 @@
[DEFAULT]
Description = Erstellt ein Backup
[dmin]
Description = + alle 10 Minuten
[hourly]
Description = + jede Stunde
[daily]
Description = + jeden Tag
[weekly]
Description = + jede Woche
[monthly]
Description = + jedes Monat
[yearly]
Description = + einmal jedes Jahr
[afterboot]
Description = + nach jedem erfolgreichen Boot
[plugin]
Description = + nach jedem Einstecken des externen Backup-Mediums
[manually]
Description = + bei manuellem Aufruf
[aptupgrade]
Description = + vor jedem Systemupgrade oder jeder Paketinstallation

View file

@ -0,0 +1,11 @@
[daily]
ignore=+/home/jakob/src/live-images
[weekly]
ignore=+/home/jakob/src/live-images
[monthly]
ignore=+/home/jakob/src/live-images
[yearly]
ignore=+/home/jakob/src/live-images

View file

@ -0,0 +1,2 @@
[DEFAULT]
ignore=+\.icedove|thunderbird/.*/ImapMail

View file

@ -0,0 +1,8 @@
#!/bin/sh
DEVICE="$1"; shift
ACTION="$1"; shift
MAINPID="$1"
[ x"$MAINPID" = "x" ] && exit 0
/bin/ps h -o command -p "$MAINPID" && /sbin/btrfs $ACTION cancel "$DEVICE" || exit 0

View file

@ -0,0 +1,135 @@
#!/bin/bash
# Create udev-rule and mount-entry for new backup-volume
ACTION=$1
case $2 in
-u)
UUID=$3
DEV=$(readlink -f /dev/disk/by-uuid/$UUID)
PARTUUID="$(blkid /dev/disk/by-uuid/$UUID -o value -s PARTUUID)"
;;
u-*)
UUID=${2#u-}
DEV=$(readlink -f /dev/disk/by-uuid/$UUID)
PARTUUID="$(blkid /dev/disk/by-uuid/$UUID -o value -s PARTUUID)"
;;
-p)
PARTUUID=$3
DEV=$(readlink -f /dev/disk/by-partuuid/$PARTUUID)
UUID="$(blkid /dev/disk/by-partuuid/$PARTUUID -o value -s UUID)"
;;
p-*)
PARTUUID=${2#p-}
DEV=$(readlink -f /dev/disk/by-partuuid/$PARTUUID)
UUID="$(blkid /dev/disk/by-partuuid/$PARTUUID -o value -s UUID)"
;;
d-*)
DEV=${2#d-}
UUID="$(blkid $DEV -o value -s UUID)"
PARTUUID="$(blkid $DEV -o value -s PARTUUID)"
;;
*)
DEV="$(/bin/systemd-escape -p -u $2)"
UUID="$(blkid $DEV -o value -s UUID)"
#PARTUUID="$(blkid $DEV -o value -s PARTUUID)"
PRE="d-"
;;
esac
#DESTUDEV="/tmp/"
#DESTSYSTEMD="/tmp/"
DESTUDEV="/etc/udev/rules.d/"
DESTSYSTEMD="/etc/systemd/system/"
SYSTEMCTL="/bin/systemctl"
echo "$ACTION ${DEV} | ${UUID} | ${PARTUUID}"
sleep 1
if [ "$DEV"x = "x" ]
then
TYPE="btrfs"
else
TYPE="$(blkid $DEV -o value -s TYPE)"
echo "T $TYPE | $DEV"
fi
if [ "$PARTUUID"x = "x" ]; then
if [ "$UUID"x = "x" ]; then
echo "$PARTUUID | $UUID | $DEV is no valid device"
exit 3
else
DUUID="$UUID" #DUUID is uuid which is taken to use
SUUID="ID_FS_UUID" #SUUID is the string for the udev-rule it's UUID or PARTUUID
ID="uuid" #ID is also for the udev-rule. To look in /dev/disk/by-uuid or /dev/disk/by-partuuid
PRE="u-"
fi
else
DUUID="$PARTUUID"
SUUID="ID_PART_ENTRY_UUID"
ID="partuuid"
PRE="p-"
fi
#echo "$DUUID | $SUUID | $ID | $PRE"
# Start by udev
start () {
mkdir -p "${DESTSYSTEMD}var-cache-backup.mount.d/"
echo "[Mount]
What=/dev/disk/by-${ID}/${DUUID}
" > "${DESTSYSTEMD}var-cache-backup.mount.d/source.conf"
$SYSTEMCTL daemon-reload
}
# Create udev-Rule for new external drive
register () {
#echo "$UUID"
#echo "AAA $(/bin/systemd-escape /dev/disk/by-${ID}/${DUUID}|sed -e 's@\\@\\\\@g')"
#echo "ACTION==\"add\", KERNEL==\"sd*\", SUBSYSTEMS==\"usb\", ENV{${SUUID}}==\"$DUUID\", SYMLINK+=\"disk/mars\", TAG+=\"systemd\", ENV{SYSTEMD_WANTS}+=\"mkbackup-external@${PRE}${DUUID}.service\", ENV{SYSTEMD_WANTS}+=\"mkbackup@BKP.target\", ENV{SYSTEMD_WANTS}+=\"smartctl-fast@$(/bin/systemd-escape /dev/disk/by-${ID}/${DUUID}|sed -e 's@\\@\\\\@g').service\"
echo "ACTION==\"add\", KERNEL==\"sd*\", SUBSYSTEMS==\"usb\", ENV{${SUUID}}==\"$DUUID\", SYMLINK+=\"disk/mars\", TAG+=\"systemd\", ENV{SYSTEMD_WANTS}+=\"mkbackup-external@${PRE}${DUUID}.service\", ENV{SYSTEMD_WANTS}+=\"mkbackup@BKP.target\", ENV{SYSTEMD_WANTS}+=\"smartctl-fast@$(/bin/systemd-escape /dev/disk/by-${ID}/${DUUID}).service\"
ACTION==\"remove\", KERNEL==\"sd*\", SUBSYSTEMS==\"usb\", ENV{${SUUID}}=\"$DUUID\", \
RUN+=\"${SYSTEMCTL} --no-block stop mkbackup@BKP.target\"" > "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules"
}
# delete udev-rule, if external drive is not longer in use for backups.
unregister () {
[ -e "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules" ] && rm "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules"
}
case $TYPE in
btrfs)
;;
*)
echo "$DEV isn't a btrfs-filesystem. Exiting"; exit 1;;
esac
case $ACTION in
register)
#setup udev-rule for device
register
;;
unregister)
#delete udev-rule for device
unregister ;;
start)
#activate device
start;;
stop)
#deactivate device
stop ;;
*)
echo "$ACTION not recognized";
exit 2;;
esac
$SYSTEMCTL daemon-reload
#/bin/systemctl
exit 0

View file

@ -0,0 +1,9 @@
[Unit]
Description=Set Environment syssubvol for systemd
[Service]
Type=simple
ExecStart=/bin/sh -c '/bin/systemctl set-environment SYSSUBVOL=`/usr/bin/syssubvol`'
[Install]
WantedBy=basic.target

View file

@ -0,0 +1,2 @@
[Unit]
RequiresMountsFor=/var/log

View file

@ -0,0 +1,13 @@
[Unit]
Description=Automounting of /backup - activated by backup.path
#Before=local-fs.target remote-fs.target
#DefaultDependencies = yes
After=backup.path
[Automount]
TimeoutIdleSec=15s
Where=/backup
#[Install]
#WantedBy=mkbackup.target

View file

@ -0,0 +1,11 @@
[Unit]
Description=Mounts Backup-Snapshots from System to /backup
After=local-fs.target
#BindsTo=backup.automount
[Mount]
What=MksnapshotFS.py
Where=/backup
Type=fuse
TimeoutSec=10s
Options=noauto,ro,nofail

View file

@ -0,0 +1,12 @@
[Unit]
Description=Start automounter for /backup, if this directory exists
PartOf=mkbackup.target
#Before=local-fs.target
#DefaultDependencies = no
[Path]
PathExists=/backup
Unit=backup.automount
[Install]
WantedBy=mkbackup.target

View file

@ -0,0 +1,41 @@
[Unit]
Description=Balance btrfs-filesystem %f
#ConditionPathExists=!/run/btrfs-actions
#ConditionDirectoryNotEmpty=!/run/btrfs-actions
#ConditionPathExists=!/run/mkbackup
#ConditionDirectoryNotEmpty=!/run/mkbackup
ConditionACPower=true
Conflicts=sleep.target suspend.target shutdown.target
Wants=btrfs-scrub@%i.service
OnFailure=status-email-root@%n.service
Wants=status-email-root@%n.service
Before=status-email-root@%n.service
RefuseManualStart=true
[Service]
RuntimeDirectory=btrfs-actions
Type=oneshot
#Restart=always
Nice=12
IOSchedulingClass=3
#IOSchedulingPriority=
CPUSchedulingPolicy=idle
ExecStartPre=/bin/touch /run/btrfs-actions/balance-systemd.lock
#ExecStart=/bin/btrfs balance start -musage=0 -dusage=0 -v %f
ExecStart=/bin/btrfs balance start -musage=5 -dusage=5 -v %f
#ExecStart=/bin/btrfs balance start -musage=10 -v %f
#ExecStart=/bin/btrfs balance start -musage=40 -v %f
#ExecStart=/bin/btrfs balance start -musage=60 -v %f
#ExecStart=/bin/btrfs balance start -dusage=0 -v %f
#ExecStart=/bin/btrfs balance start -dusage=5 -v %f
#ExecStart=/bin/btrfs balance start -dusage=10 -v %f
#ExecStart=/bin/btrfs balance start -dusage=40 -v %f
#ExecStart=/bin/btrfs balance start -dusage=65 -v %f
ExecStop=/usr/lib/systemd/scripts/btrfs-action.sh %f balance $MAINPID
ExecStopPost=/bin/rm /run/btrfs-actions/balance-systemd.lock

View file

@ -0,0 +1,35 @@
[Unit]
#Description=Scrub btrfs-filesystem %f %i
Description=Scrubs filesystem on %f weekly
Wants=btrfs-balance@%i.service
After=btrfs-balance@%i.service
#ConditionPathExists=!/run/btrfs-actions
#ConditionDirectoryNotEmpty=!/run/btrfs-actions
#ConditionPathExists=!/run/mkbackup
#ConditionDirectoryNotEmpty=!/run/mkbackup
#ConditionACPower=true
#Wants=btrfs-status-email-root@%i.service
#Before=btrfs-status-email-root@%i.service
Conflicts=sleep.target suspend.target shutdown.target
OnFailure=status-email-root@%n.service
[Service]
RuntimeDirectory=btrfs-actions
#Type=oneshot
Nice=11
IOSchedulingClass=3
#IOSchedulingPriority=
CPUSchedulingPolicy=idle
ExecStartPre=/bin/touch /run/btrfs-actions/scrub-systemd-%i.lock
ExecStart=/bin/btrfs scrub start -Bd %f
ExecStop=/usr/lib/systemd/scripts/btrfs-action.sh %f scrub $MAINPID
ExecStopPost=/bin/rm /run/btrfs-actions/scrub-systemd-%i.lock
[Install]
WantedBy=timer-weekly.target

View file

@ -0,0 +1,8 @@
[Unit]
Description=btrfs-status email for %f to root
[Service]
Type=oneshot
ExecStart=/usr/local/bin/systemd-btrfs-email root@localhost %i %f
#User=nobody
Group=systemd-journal

View file

@ -0,0 +1,11 @@
[Unit]
Description=Upate /tmp/%I.conf.tmp if /etc/%I* changes
[Path]
PathModified=/etc/%I.conf
PathModified=/etc/%I.conf.d/
PathChanged=/tmp/%I.conf.tmp
Unit=mkbackup-conf@%i.service
[Install]
WantedBy=paths.target

View file

@ -0,0 +1,9 @@
[Unit]
Description=Create or update temporary conf-file /tmp/%I.conf.tmp for shell-extension
[Service]
#ExecStart=/bin/sh -c "(/usr/local/bin/mkbackup list -i --print-config > /tmp/%I.conf.tmp)"
ExecStart=/usr/local/bin/mkbackup list -i --print-config -o /tmp/%I.conf.tmp
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,8 @@
[Unit]
Description=Activate external Drive/Partition for mkbackup for device %i
Wants=mkbackup@BKP.target
Before=mkbackup@BKP.target
[Service]
Type=oneshot
ExecStart=/usr/lib/systemd/scripts/mksnapshot-create-volume.sh start %i

View file

@ -0,0 +1,6 @@
[Unit]
Description=Activate external Drive/Partition for mkbackup/mksnapshot for device %I.
[Service]
Type=oneshot
ExecStart=/usr/lib/systemd/scripts/mksnapshot-create-volume.sh register %i

View file

@ -0,0 +1,6 @@
[Unit]
Description=Deactivate external Drive/Partition for mkbackup/mksnapshot for device %I.
[Service]
Type=oneshot
ExecStart=/usr/lib/systemd/scripts/mksnapshot-create-volume.sh unregister %i

View file

@ -0,0 +1,6 @@
[Unit]
Description=Enables all backup.services
[Install]
WantedBy=basic.target

View file

@ -0,0 +1,13 @@
[Unit]
Description=Activate backup on Device %i
[Path]
PathExists=/dev/disk/by-partuuid/%i
#PathExists=/dev/disk/by-uuid/%i
#Unit=systemd-cryptsetup@mars.service
#Unit=status-email-jakob@%n.service
Unit=var-cache-backup@%i.mount
#Unit=dev-disk-by\x2dpartuuid-%i.mount
[Install]
WantedBy=paths.target

View file

@ -0,0 +1,44 @@
[Unit]
Description=Make %i-backup from system-subvolume
#ConditionPathExists=!/run/btrfs-actions
#ConditionDirectoryNotEmpty=!/run/btrfs-actions
#ConditionPathExists=!/run/mkbackup
ConditionPathExistsGlob=!/run/mkbackup/*
#ConditionPathExists=!/run/btrfs-actions
#ConditionDirectoryNotEmpty=!/run/mkbackup
Requires=set-environ.service
Wants=dpkg-get-selection.service
#Wants=mkbackup-transfer.service
After=set-environ.service dpkg-get-selection.service mkbackup.target
#After=set-environ.service dpkg-get-selection.service
#Before=mkbackup-transfer.service
Conflicts=suspend.target shutdown.target sleep.target
OnFailure=status-email-root@%n.service
[Service]
#Type=oneshot
#Type=simple
Type=idle
#EnvironmentFile=/etc/mkbtrbackup.conf.d/%i.conf
#BusName=at.xundeenergie.mkbackup
RuntimeDirectory=mkbackup
Nice=10
IOSchedulingClass=3
#IOSchedulingPriority=
CPUSchedulingPolicy=idle
ExecStartPre=/bin/sh -c "(/bin/systemctl is-active -q mkbackup.target)"
#ExecStartPre=/bin/touch /run/mkbackup-%i/systemd.lock
ExecStart=/usr/local/bin/mkbackup -V -v -t %i create SNP
#ExecStopPost=-/bin/rm /run/mkbackup-%i/systemd.lock
KillMode=mixed
[Install]
WantedBy=timer-%i.target
DefaultInstance=mkbackup@manually.service

View file

@ -0,0 +1,5 @@
[Unit]
Description=Target for services for %i in mkbackup
Documentation=man:systemd.special(7)
StopWhenUnneeded=no

View file

@ -0,0 +1,8 @@
[Unit]
Description=Target for services for %i in mkbackup
Documentation=man:systemd.special(7)
StopWhenUnneeded=no
[Install]
WantedBy=local-fs.target

View file

@ -0,0 +1,5 @@
[Unit]
Description=Smartctl Health-Test for %f
[Service]
ExecStart=/usr/sbin/smartctl -H %f

View file

@ -0,0 +1,3 @@
[Unit]
Description=Triggered by apt
StopWhenUnneeded=yes

View file

@ -0,0 +1,15 @@
[Unit]
Description=Runs backup %I before system-update/upgrade with apt
#BindsTo=mkbackup@BKP.target
PartOf=mkbackup@SNP.target
OnFailure=status-email-root@%n.service
BindsTo=timer-xe.target
[Timer]
OnActiveSec=1s
AccuracySec=2s
Unit=timer-aptupgrade.target
RemainAfterElapse=false
[Install]
WantedBy=mkbackup@SNP.target

View file

@ -0,0 +1,3 @@
[Unit]
Description=Target after successfully plugging in external backup-drive
StopWhenUnneeded=yes

View file

@ -0,0 +1,16 @@
[Unit]
Description=Runs backup %I after pluggin in external HD
BindsTo=mkbackup@BKP.target
#PartOf=mkbackup@BKP.target
OnFailure=status-email-root@%n.service
RefuseManualStart=yes
RefuseManualStop=yes
[Timer]
OnActiveSec=30s
AccuracySec=10min
Unit=timer-plugin.target
RemainAfterElapse=false
[Install]
WantedBy=mkbackup@BKP.target

View file

@ -0,0 +1,5 @@
[Unit]
Description=dbus-notification after umount from %I
[Service]
ExecStart=/usr/bin/dbus-send --system /at/xundeenergie/notifications at.xundeenergie.notifications.Notification string:"External Disk" string:"%I unmounted" string:"Platte kann ausgesteckt werden"

View file

@ -0,0 +1,17 @@
# Automatically generated by systemd-fstab-generator
[Unit]
Documentation=man:fstab(5) man:systemd-fstab-generator(8)
DefaultDependencies=no
Conflicts=umount.target
Before=umount.target
#BindsTo=mkbackup@BKP.target
PartOf=mkbackup@BKP.target
[Automount]
Where=/var/cache/backup
TimeoutIdleSec=10s
[Install]
WantedBy=mkbackup@BKP.target

View file

@ -0,0 +1,18 @@
[Unit]
SourcePath=/etc/fstab
Documentation=man:fstab(5) man:systemd-fstab-generator(8)
#Requires=var-cache-backup.automount
#After=var-cache-backup.automount
PartOf=var-cache-backup.automount
#Wants=backup.mount backup.automount
#Before=backup.mount
#Before=umount-notification@%p.service
#Wants=umount-notification@%P.service
[Mount]
#What - drop-in in var-cache-backup.mount.d by mkbackup-start@...service which is started by udev-rule
#What=/dev/disk/by-uuid/e75ad421-9cda-4031-a61c-4e99bc882e1c
Where=/var/cache/backup
Type=btrfs
TimeoutSec=10s
Options=noauto,user,noatime,nofail,compress=lzo,space_cache,noinode_cache,relatime,subvol=/

459
files/usr/bin/MksnapshotFS.py Executable file
View file

@ -0,0 +1,459 @@
#!/usr/bin/python -u
#!/usr/bin/env python
# Copyright (C) 2001 Jeff Epler <jepler@unpythonic.dhs.org>
# Copyright (C) 2006 Csaba Henk <csaba.henk@creo.hu>
#
# This program can be distributed under the terms of the GNU LGPL.
# See the file COPYING.
#
import os, sys
from errno import *
from stat import *
import fcntl
# pull in some spaghetti to make this stuff work without fuse-py being installed
try:
import _find_fuse_parts
except ImportError:
pass
import fuse
from fuse import Fuse
# JS - Snapshotnamehandling, get the current user and so on
import getpass
import socket
from time import strptime, strftime
from datetime import datetime,date,time,timedelta
#from mksnapshotconfig import *
from mkbackup.mkbackup_btrfs_config import *
DEBUG = False
if not hasattr(fuse, '__version__'):
raise RuntimeError, \
"your fuse-py doesn't know of fuse.__version__, probably it's too old."
fuse.fuse_python_api = (0, 2)
fuse.feature_assert('stateful_files', 'has_init')
def flag2mode(flags):
md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
if flags | os.O_APPEND:
m = m.replace('w', 'a', 1)
return m
class Xmp(Fuse):
def __init__(self, *args, **kw):
Fuse.__init__(self, *args, **kw)
if DEBUG:
for i in args: print "ARG: "+i
for i in kw: print "KW: "+i
#get hostname
# die auskommentierte if-Funktion liefert hostname.localdomain.
# if socket.gethostname().find('.')>=0:
# hostname=socket.gethostname()
# else:
# hostname=socket.gethostbyaddr(socket.gethostname())[0]
#socket.gethostname() liefert nur "hostname" ohne .localdomain
hostname=socket.gethostname()
# do stuff to set up your filesystem here, if you want
#import thread
#thread.start_new_thread(self.mythread, ())
CONFIG=Config()
self.root=CONFIG.getStorePath('SNP').encode()
self.local=CONFIG.getStorePath('SNP').encode()
self.extern=CONFIG.getStorePath('BKP').encode()
self.snapshots={}
self.direntries=[]
Xmp.realpath=""
self.USER=getpass.getuser()
if os.getuid() == 0:
self.uroot = True
else:
self.uroot = False
try:
self.HOME=os.environ['HOME']
except:
self.HOME = None
self.uroot = True
# JS - some helpers
def translate(self,subvolname,location='loc'):
n = len(subvolname.split('.'))
ac = ""
sn = subvolname.split('.')[0]+'.'
if n == 1:
#return subvolname+'--'+location
return 'CURRENT--'+location
elif n == 2:
return subvolname+'--'+location
elif n == 3:
try:
sndt = datetime.strptime(subvolname.split('.')[1],"%Y-%m-%d_%H:%M:%S") #snapshot-timestamp
except:
return subvolname+'--'+location
action = subvolname.split('.')[2]
if self.uroot:
ac='-'+action
else:
sn = ''
if action == 'manually':
ac="-"+action
elif action == 'aptupgrade':
ac="-"+action
else:
ac=""
snd = datetime.date(sndt) #snapshot-date
if snd == datetime.date(datetime.now()):
#today
dts = sn+'heute_%H-%M-%S'
elif snd == datetime.date(datetime.now()-timedelta(days=1)):
#yesterday
dts = sn+'gestern_%H-%M-%S'
# elif snd == datetime.date(datetime.now()-timedelta(days=2)):
# #day before yesterday
# dts = 'vorgestern %H-%M-%S'
else:
#any day else
dts = sn+'%Y.%m.%d_%H-%M-%S'
if self.uroot:
return datetime.strftime(sndt,dts)+ac+'--'+location
else:
return datetime.strftime(sndt,dts)+'--'+location+ac
else:
return subvolname+'--'+location
def __lsnapshots(self):
if DEBUG: print "ls snapshots"
self.snapshots.clear()
for location in self.BDIRS.iterkeys():
if DEBUG == True: print 'Scan location '+location+': '+self.BDIRS[location]
try:
dirents = os.listdir(self.BDIRS[location])
except:
continue
if self.uroot:
for entry in dirents:
if os.path.exists(self.BDIRS[location]+'/'+entry) \
and not os.path.islink(self.BDIRS[location]+'/'+entry):
self.snapshots[self.translate(entry,location=location)] \
= [self.BDIRS[location], '/'+entry+'/', location]
else:
for entry in dirents:
if os.path.exists(self.BDIRS[location]+'/'+entry+self.HOME) \
and not os.path.islink(self.BDIRS[location]+'/'+entry):
self.snapshots[self.translate(entry,location=location)] \
= [self.BDIRS[location], '/'+entry+self.HOME+'/', location]
def __realpath(self,path):
ss = path.split('/')[1]
#sv = './'+'/'.join(path.split('/')[2:])
sv = '/'.join(path.split('/')[2:])
if path == "/" and (self.root in self.BDIRS.values()):
return self.root
elif ss in self.snapshots:
self.root = self.snapshots[ss][0]
Xmp.realpath = self.snapshots[ss][0]+self.snapshots[ss][1]
return self.snapshots[ss][0]+self.snapshots[ss][1]+sv
else:
self.root = self.BDIRS['loc']
Xmp.realpath = self.root + path
return Xmp.realpath
def __lsdir(self,path):#
if DEBUG: print "ls dir"
ss = path.split('/')[1]
subdir = '/'+'/'.join(path.split('/')[2:])
dirents = ['.', '..']
#if path == "/" and (self.root.strip('/') == self.BDIRS['loc'].strip('/') or self.root == self.BDIRS['ext'].strip('/')):
if path == "/" and (self.root in self.BDIRS.values()):
self.root = self.BDIRS['loc']
dirents.extend(self.snapshots.keys())
elif ss in self.snapshots or path == "./":
dirents.extend(os.listdir(self.__realpath(path)))
else:
self.root = self.BDIRS['loc']
dirents.extend(os.listdir(path))
return dirents
# def mythread(self):
#
# """
# The beauty of the FUSE python implementation is that with the python interp
# running in foreground, you can have threads
# """
# print "mythread: started"
# while 1:
# time.sleep(120)
# print "mythread: ticking"
def getattr(self, path):
return os.lstat(self.__realpath(path))
if path == "/" and (self.root in self.BDIRS.values()):
return os.lstat(self.root)
else:
return os.lstat(self.__realpath(path))
def readlink(self, path):
return os.readlink(self.__realpath(path))
def readdir(self, path, offset):
if path == "/" and (self.root in self.BDIRS.values()):
if DEBUG: print path
pass
if path == "/":
self.__lsnapshots()
for e in self.__lsdir(path):
yield fuse.Direntry(e)
def unlink(self, path):
return -EROFS
#os.unlink("." + path)
def rmdir(self, path):
return -EROFS
#os.rmdir("." + path)
def symlink(self, path, path1):
return -EROFS
#os.symlink(path, "." + path1)
def rename(self, path, path1):
return -EROFS
#os.rename("." + path, "." + path1)
def link(self, path, path1):
return -EROFS
#os.link("." + path, "." + path1)
def chmod(self, path, mode):
return -EROFS
#os.chmod("." + path, mode)
def chown(self, path, user, group):
return -EROFS
#os.chown("." + path, user, group)
def truncate(self, path, len):
return -EROFS
#f = open("." + path, "a")
#f.truncate(len)
#f.close()
def mknod(self, path, mode, dev):
return -EROFS
#os.mknod("." + path, mode, dev)
def mkdir(self, path, mode):
return -EROFS
#os.mkdir("." + path, mode)
def utime(self, path, times):
os.utime(self.__realpath(path), times)
# The following utimens method would do the same as the above utime method.
# We can't make it better though as the Python stdlib doesn't know of
# subsecond preciseness in acces/modify times.
#
# def utimens(self, path, ts_acc, ts_mod):
# os.utime("." + path, (ts_acc.tv_sec, ts_mod.tv_sec))
def access(self, path, mode):
if DEBUG: print "access " + path + '||'+str(mode)
if not os.access(self.__realpath(path), mode):
return -EACCES
# This is how we could add stub extended attribute handlers...
# (We can't have ones which aptly delegate requests to the underlying fs
# because Python lacks a standard xattr interface.)
#
# def getxattr(self, path, name, size):
# val = name.swapcase() + '@' + path
# if size == 0:
# # We are asked for size of the value.
# return len(val)
# return val
#
# def listxattr(self, path, size):
# # We use the "user" namespace to please XFS utils
# aa = ["user." + a for a in ("foo", "bar")]
# if size == 0:
# # We are asked for size of the attr list, ie. joint size of attrs
# # plus null separators.
# return len("".join(aa)) + len(aa)
# return aa
def statfs(self):
"""
Should return an object with statvfs attributes (f_bsize, f_frsize...).
Eg., the return value of os.statvfs() is such a thing (since py 2.2).
If you are not reusing an existing statvfs object, start with
fuse.StatVFS(), and define the attributes.
To provide usable information (ie., you want sensible df(1)
output, you are suggested to specify the following attributes:
- f_bsize - preferred size of file blocks, in bytes
- f_frsize - fundamental size of file blcoks, in bytes
[if you have no idea, use the same as blocksize]
- f_blocks - total number of blocks in the filesystem
- f_bfree - number of free blocks
- f_files - total number of file inodes
- f_ffree - nunber of free file inodes
"""
return os.statvfs(".")
def fsinit(self):
if DEBUG: print "fsinit"
self.BDIRS={'loc':'/'+self.local.strip('/'),'ext':'/'+self.extern.strip('/')}
os.chdir('/'+self.local.strip('/'))
self.__lsnapshots()
class XmpFile(object):
def __init__(self, path, flags, *mode):
path = Xmp.realpath+'/'.join(path.split('/')[2:])
self.fd = os.open(path, flags, *mode)
self.file = os.fdopen(self.fd, flag2mode(flags))
def read(self, length, offset):
self.file.seek(offset)
return self.file.read(length)
def write(self, buf, offset):
return -EROFS
self.file.seek(offset)
self.file.write(buf)
return len(buf)
def release(self, flags):
self.file.close()
def _fflush(self):
if 'w' in self.file.mode or 'a' in self.file.mode:
self.file.flush()
def fsync(self, isfsyncfile):
self._fflush()
if isfsyncfile and hasattr(os, 'fdatasync'):
os.fdatasync(self.fd)
else:
os.fsync(self.fd)
def flush(self):
self._fflush()
# cf. xmp_flush() in fusexmp_fh.c
os.close(os.dup(self.fd))
def fgetattr(self):
return os.fstat(self.fd)
def ftruncate(self, len):
return -EROFS
#self.file.truncate(len)
def lock(self, cmd, owner, **kw):
#return -EROFS
# The code here is much rather just a demonstration of the locking
# API than something which actually was seen to be useful.
# Advisory file locking is pretty messy in Unix, and the Python
# interface to this doesn't make it better.
# We can't do fcntl(2)/F_GETLK from Python in a platfrom independent
# way. The following implementation *might* work under Linux.
#
# if cmd == fcntl.F_GETLK:
# import struct
#
# lockdata = struct.pack('hhQQi', kw['l_type'], os.SEEK_SET,
# kw['l_start'], kw['l_len'], kw['l_pid'])
# ld2 = fcntl.fcntl(self.fd, fcntl.F_GETLK, lockdata)
# flockfields = ('l_type', 'l_whence', 'l_start', 'l_len', 'l_pid')
# uld2 = struct.unpack('hhQQi', ld2)
# res = {}
# for i in xrange(len(uld2)):
# res[flockfields[i]] = uld2[i]
#
# return fuse.Flock(**res)
# Convert fcntl-ish lock parameters to Python's weird
# lockf(3)/flock(2) medley locking API...
op = { fcntl.F_UNLCK : fcntl.LOCK_UN,
fcntl.F_RDLCK : fcntl.LOCK_SH,
fcntl.F_WRLCK : fcntl.LOCK_EX }[kw['l_type']]
if cmd == fcntl.F_GETLK:
return -EOPNOTSUPP
elif cmd == fcntl.F_SETLK:
if op != fcntl.LOCK_UN:
op |= fcntl.LOCK_NB
elif cmd == fcntl.F_SETLKW:
pass
else:
return -EINVAL
fcntl.lockf(self.fd, op, kw['l_start'], kw['l_len'])
def main(self, *a, **kw):
self.file_class = self.XmpFile
return Fuse.main(self, *a, **kw)
def main():
usage = """
Userspace nullfs-alike: mirror the filesystem tree from some point on.
""" + Fuse.fusage
server = Xmp(version="%prog " + fuse.__version__,
usage=usage,
dash_s_do='setsingle')
server.parser.add_option(mountopt="root", metavar="PATH", default=server.local,
help="mirror filesystem from under PATH [default: %default]")
server.parser.add_option(mountopt="local", metavar="PATH", default=server.local,
help="set path to filesystem from internal HDD/SSD under PATH [default: %default]")
server.parser.add_option(mountopt="extern", metavar="PATH", default=server.extern,
help="set path to filesystem from external HDD/SSD under PATH [default: %default]")
server.parser.add_option(mountopt="uroot", metavar="BOOL", default=server.uroot,
help="""use ist for mounting on /backup for root -
whole snapshots! BOOL [default: %default]""")
server.parse(values=server, errex=1)
try:
if server.fuse_args.mount_expected():
os.chdir(server.local)
except OSError:
print >> sys.stderr, "can't enter root of underlying filesystem"
sys.exit(1)
server.main()
if __name__ == '__main__':
main()

59
files/usr/bin/mkbackup-gui Executable file
View file

@ -0,0 +1,59 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Copyright (C) 2013 Thomas Bechtold <thomasbechtold@jpberlin.de>
# This file is part of mkbackup-gui.
# This program 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 2
# of the License, or (at your option) any later version.
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#import os
#gi_typelib_path = ["/usr/lib/x86_64-linux-gnu/mkbackup-gui/girepository-1.0",]
#if 'GI_TYPELIB_PATH' in os.environ:
# gi_typelib_path.append(os.environ['GI_TYPELIB_PATH'])
#os.environ['GI_TYPELIB_PATH'] = ":".join(gi_typelib_path)
#ld_library_path = ["/usr/lib/x86_64-linux-gnu/mkbackup-gui",]
#if 'LD_LIBRARY_PATH' in os.environ:
# ld_library_path.append(os.environ['LD_LIBRARY_PATH'])
#os.environ['LD_LIBRARY_PATH'] = ":".join(ld_library_path)
import os
import sys
sys.path.insert(1, '/usr/lib/python3.6/site-packages')
import gettext, locale
from gettext import gettext as _
gettext.textdomain("mkbackup-gui")
locale.textdomain("mkbackup-gui")
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
from dfeet.application import DFeetApp
if __name__ == "__main__":
data_dir = "/usr/share/mkbackup-gui"
#use local paths when debugging
if os.getenv("DFEET_DEBUG") is not None:
data_dir = os.path.join(os.path.dirname(__file__), "..", "data")
Gtk.IconTheme.get_default().prepend_search_path(
os.path.join(os.path.dirname(__file__), "..", "data", "icons"))
#start the application
print(data_dir)
app = MkBackup(package="mkbackup-gui", version="0.1.0", data_dir=data_dir)
sys.exit(app.run(sys.argv))

11
files/usr/bin/syssubvol Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/python3
import sys
from mkbackup.mkbackup_btrfs_config import MountInfo
def relpath(mp):
print(MountInfo().relpath(mp))
if __name__ == "__main__":
for i in sys.argv[1:]:
relpath(i)

View file

@ -0,0 +1 @@
../../../python3/dist-packages/mkbackup/mkbackup_btrfs_config.py

View file

@ -0,0 +1 @@
../../../python3/dist-packages/mkbackup/mkbackup_emitter.py

View file

@ -0,0 +1 @@
../../../python3/dist-packages/mkbackup/system_notification_emitter.py

View file

@ -0,0 +1,38 @@
"""Emmitter functionality."""
import dbus
import dbus.service
import dbus.glib
class Emitter(dbus.service.Object):
"""Emitter DBUS service object."""
def __init__(self, conn=None, object_path=None, bus_name=None):
"""Initialize the emitter DBUS service object."""
dbus.service.Object.__init__(self, conn=conn, object_path=object_path)
@dbus.service.signal(dbus_interface='at.xundeenergie.Notification')
def low(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a low test signal')
@dbus.service.signal(dbus_interface='at.xundeenergie.Notification')
def normal(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a normal test signal')
@dbus.service.signal(dbus_interface='at.xundeenergie.Notification')
def critical(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a critical test signal')
""" Example to use
Simple_Notification = Emitter(dbus.SystemBus(),
'/at/xundeenergie/notifications/simple/Notification')
Advanced_Notification = Emitter(dbus.SystemBus(),
'/at/xundeenergie/notifications/advanced/Notification')
#Simple_Notification.low('M')
Advanced_Notification.normal(
{'sender': 'emitter1.py', 'header': 'Testmessage', 'body': 'Test Body'})
"""

View file

@ -0,0 +1,35 @@
#!/usr/bin/python3 -d
#!/usr/bin/env python3
import dbus, dbus.service, dbus.exceptions
import sys
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
# Initialize a main loop
DBusGMainLoop(set_as_default=True)
loop = GLib.MainLoop()
# Declare a name where our service can be reached
try:
sysbus_name = dbus.service.BusName("at.xundeenergie",
bus=dbus.SystemBus(),
do_not_queue=True)
except dbus.exceptions.NameExistsException:
print("service is already running")
sys.exit(1)
# Run the loop
try:
# Create our initial objects
from services.mkbackup import MkBackupDBus
MkBackupDBus(sysbus_name, "/at/xundeenergie/mkbackup/Intervals")
loop.run()
except KeyboardInterrupt:
print("keyboard interrupt received")
except Exception as e:
print("Unexpected exception occurred: '{}'".format(str(e)))
finally:
loop.quit()

View file

@ -0,0 +1,117 @@
import dbus
import dbus.service
import random
import time
import os
from mkbackup_btrfs_config import Config, MountInfo, connect, Myos, __version__
config = Config()
class MkBackup:
def __init__(self, bus_name, base_path):
# self._bus = dbus.SystemBus()
# self.notification = Notification()
Intervals(bus_name, base_path)
for intv in config.ListIntervals():
Properties(bus_name, os.path.join(base_path, intv), intv)
class Intervals(dbus.service.Object):
def __init__(self, bus_name, bus_path):
super().__init__(bus_name, bus_path)
@dbus.service.method(dbus_interface='at.xundeenergie.mkbackup.Intervals',
in_signature='', out_signature='v')
def Names(self):
return config.ListIntervals()
class Properties(dbus.service.Object):
def __init__(self, bus_name, bus_path, interval):
super().__init__(bus_name, bus_path)
self.interface = "at.xundeenergie.mkbackup.Status"
self.interval = interval
self.STATI = ['reset', 'stop', 'running', 'finished']
self.properties = dict()
self.properties[self.interface] = dict()
self.properties[self.interface]['progress'] = 0 # 0-100
self.properties[self.interface]['status'] = 'stop' # stop, running, finished, reset
self.properties[self.interface]['transfer'] = config.getTransfer(interval)
self.properties[self.interface]['lastrun'] = 0 # datetime
self.properties[self.interface]['finished'] = True # Boolean
self.properties[self.interface]['name'] = interval # Boolean
self.properties['function'] = dict()
self.properties['function']['progress'] = self.update_progress
self.properties['function']['status'] = self.update_status
# self.properties['function']['lastrun'] = self.update_lastrun
from dbus import Interface
def update_progress(self, interface, incr):
print("Update progress: %i / %i, %s" % (float(incr),
self.properties[interface]['progress'],self.properties[interface]['status']))
if self.properties[interface]['status'] == 'running':
if 0 < self.properties[interface]['progress'] + float(incr) < 100:
#self.properties[interface]['progress'] = self.properties[interface]['progress'] + float(incr)
self.properties[interface]['progress'] += float(incr)
elif self.properties[interface]['progress'] + float(incr) >= 100:
self.properties[interface]['progress'] = 99
else:
print('B', incr, type(incr))
return self.properties[interface]['progress']
def update_status(self, interface, status):
if status in self.STATI:
print("Update status: %s" % status)
print("Status: ", self.properties[interface]['status'])
if status == 'finished':
self.properties[interface]['status'] = status
self.properties[interface]['progress'] = 100
elif status == 'stop':
self.properties[interface]['status'] = status
self.properties[interface]['progres'] = 0
elif status == 'reset':
self.properties[interface]['status'] = 'running'
self.properties[interface]['progress'] = 0
print("status: ", self.properties[interface]['status'])
return self.properties[interface]['status']
@dbus.service.method(dbus.PROPERTIES_IFACE,
in_signature='ss', out_signature='v')
def Get(self, interface_name, property_name):
return self.GetAll(interface_name)[property_name]
@dbus.service.method(dbus.PROPERTIES_IFACE,
in_signature='s', out_signature='a{sv}')
def GetAll(self, interface_name):
if interface_name == self.interface:
return self.properties[interface_name]
else:
raise dbus.exceptions.DBusException(
'at.xundeenergie.mkbackup.UnknownInterface',
'The Foo object does not implement the %s interface'
% interface_name)
@dbus.service.method(dbus.PROPERTIES_IFACE,
in_signature='ssv')
def Set(self, interface_name, property_name, new_value):
# validate the property name and value, update internal state…
"""https://recalll.co/ask/v/topic/D-Bus-D-Feet-Send-Dictionary-of-String%2CVariants-in-Python-Syntax/5565e1372bd273d7108b7b82
__import__('gi.repository.GLib', globals(), locals(), ['Variant']).Variant("s", "value")"""
if interface_name in self.properties:
if property_name in self.properties[interface_name]:
func = self.properties['function'].get(property_name)
new_value = func(interface_name, new_value)
#self.properties[str(interface_name)][str(property_name)] = new_value
self.PropertiesChanged(interface_name,
{ property_name: new_value, 'interval': self.interval}, [])
else:
raise dbus.exceptions.DBusException(
'at.xundeenergie.mkbackup.UnknownInterface',
'The Foo object does not implement the %s interface'
% interface_name)
@dbus.service.signal(dbus.PROPERTIES_IFACE,
signature='sa{sv}as')
def PropertiesChanged(self, interface_name, changed_properties,
invalidated_properties):
pass

View file

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
from gi.repository import GObject, Gtk, Gio
from mkbackup.uiloader import UILoader
import os
from dfeet.wnck_utils import IconTable
class IntervalBox(Gtk.Box):
"""class to represent a snapshot-interval"""
def __init__(self, interval):
super(IntervalBox, self).__init__(spacing=5, expand=True)
self.__interval_name = interval
self.__enabled = False
self.__icon_table = IconTable.get_instance()
self.__icon_image = Gtk.Image.new_from_pixbuf(self.__icon_table.default_icon)
self.__hbox = Gtk.HBox(spacing=5, halign=Gtk.Align.START)
self.pack_start(self.__hbox, True, True, 0)
# icon
self.__hbox.pack_start(self.__icon_image, True, True, 0)
# other information
self.__vbox_right = Gtk.VBox(spacing=5, expand=True)
self.__hbox.pack_start(self.__vbox_right, True, True, 0)
# first element
self.__label_interval_name = Gtk.Label()
self.__label_interval_name.set_halign(Gtk.Align.START)
self.__vbox_right.pack_start(self.__label_interval_name, True, True, 0)
# second element
self.__label_info = Gtk.Label()
self.__label_info.set_halign(Gtk.Align.START)
self.__vbox_right.pack_start(self.__label_info, True, True, 0)
# switch to enable/disable it
self.__switch_enabled = Gtk.Switch()
self.__switch_enabled.set_halign(Gtk.Align.START)
self.__switch_enabled.connect('notify::active', self.on_switch_activated)
self.__vbox_right.pack_start(self.__switch_enabled, True, True, 0)
# transfer snapshot to backup
self.__check_transfer = GtkCheckButton()
self.__check_transfer.set_active(props['transfer'])
self.__vbox_right.pack_start(self.__check_transfer, True, True, 0)
# progressbar
self.__progress = Gtk.ProgressBar()
self.__progress.set_fraction(props['progress']/100)
self.__vbox_right.pack_start(self.__progress, True, True, 0)
# separator for the boxes
self.pack_end(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0)
# update widget information
self.__update_widget()
self.show_all()
class IntervalWatch(object):
"""watch a given snapshot-interval"""
def __init__(self, interval):
self.__interval_name = interval
# Setup ui
ui = Gtk.Builder()
ui.add_from_file("test.glade")

View file

@ -0,0 +1,56 @@
#!/usr/bin/env python3
# Take in a single optional integral argument
import sys
import argparse
import os
arg_parser = argparse.ArgumentParser(description='Get random numbers')
arg_parser.add_argument('bits', nargs='?', default=16)
arg_parser.add_argument('-s', '--slow', action='store_true',
default=False, required=False,
help='Use the slow method')
args = arg_parser.parse_args()
# Encapsulate calling the Status object on the session bus with a main loop
import dbus, dbus.exceptions, dbus.mainloop.glib
import dbus.service
import threading
from gi.repository import GLib
from time import sleep
from mkbackup_emitter import Emitter as EM
class EmDBUS(EM):
""" Example to use
progress = Emitter(dbus.SystemBus(),
'/at/xundeenergie/mkbackup/Status')
progress.start(
{'intv': 'hourly'})
progress.update(
{'intv': 'hourly', 'progr': 5})
progress.finished(
{'intv': 'hourly'})
progress.reset(
{'intv': 'hourly'})
"""
def __init__(self, interval):
super().__init__(conn=dbus.SystemBus(), bus_name='at.xundeenergie', object_path=os.path.join('/at/xundeenergie/mkbackup/Intervals', interval))
parser = argparse.ArgumentParser()
args = parser.parse_args()
args.action='daily'
args.mdb = EmDBUS(args.action)
args.mdb.Reset()
args.mdb.Start()
print("RUN PROGRAMM")
steps = 20
for i in range(0,steps):
sleep(0.5)
args.mdb.Update(100/steps)
args.mdb.Finished()

View file

@ -0,0 +1,751 @@
from __future__ import (absolute_import, division, print_function,
unicode_literals)
#from builtins import *
import sys
import subprocess
import socket
import os
import errno
import re
import paramiko
__author__ = "Jakobus Schuerz <jakobus.schuerz@gmail.com>"
__version__ = "0.04.0"
# Useful for very coarse version differentiation.
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
PY34 = sys.version_info[0:2] >= (3, 4)
if PY3:
from configparser import ConfigParser
from configparser import RawConfigParser, NoOptionError, NoSectionError
else:
from ConfigParser import ConfigParser
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError
class Error(Exception):
pass
class NoSubvolumeError(Error):
def __init__(self):
print("ERROR - Snapshot not found" )
pass
class SSHConnectionError(Error):
def __init__(self):
print("ERROR - ssh-connection not available" )
pass
def s2bool(s):
return s.lower() in ['true','yes','y','1'] if s else False
# quote awk-argument in ssh-command
def quote_argument(argument):
return '"%s"' % (
argument
.replace('\\', '\\\\')
.replace('"', '\\"')
.replace('$', '\\$')
.replace('`', '\\`')
)
def connect(conn=None):
if conn == None:
pass
else:
if not conn['active']:
try:
#if conn['conn'].is_active(): print("Session alive")
#conn['conn'].close()
conn['conn'].connect(conn['host'],conn['port'],conn['user'],auth_timeout=10)
conn['active'] = True
#print("open connection for %s@%s" % (conn['user'], conn['host']))
# except (paramiko.BadHostKeyException,
# paramiko.AuthenticationException, paramiko.SSHException,
# socket.gaierror, socket.error) as e:
# #print("C",e)
# #raise e
# print("No connection to host %s" % (conn['host']))
# return(False)
# except (paramiko.BadHostKeyException, paramiko.AuthenticationException, paramiko.SSHException, socket.error) as e:
# raise e
except:
return(False)
return(True)
raise SSHConnectionError
else:
return(True)
# try:
# #if conn['conn'].is_active(): print("Session alive")
# conn['conn'].close()
# conn['conn'].connect(conn['host'],conn['port'],conn['user'])
# print("open connection for %s@%s" % (conn['user'], conn['host']))
# except (paramiko.BadHostKeyException, paramiko.AuthenticationException, paramiko.SSHException, socket.error) as e:
# print("C",e)
# raise e
class MountInfo():
def __init__(self,mountinfo='/proc/self/mountinfo',conn=None):
self.mi = dict()
if conn == None:
mif = open(mountinfo)
else:
if connect(conn):
#conn['conn'].connect(conn['host'],conn['port'],conn['user'])
sftp_client = conn['conn'].open_sftp()
mif = sftp_client.open(mountinfo)
else:
print("Host not reachable (MountInfo): %s" % (conn['host']))
mif = open(mountinfo)
try:
for line in mif:
if len(line.split()) == 10:
a,b,c,relpath,mntp,d,typ,fstype,dev,opts = line.split()
else:
a,b,c,relpath,mntp,d,e,typ,fstype,dev,opts = line.split()
mntp = mntp.replace('\\040',' ')
self.mi[mntp] = dict()
self.mi[mntp]['relpath'] = relpath
self.mi[mntp]['typ'] = typ
self.mi[mntp]['fstype'] = fstype
self.mi[mntp]['dev'] = dev
self.mi[mntp]['opts'] = opts
finally:
mif.close()
def __check(self,mountpoint,attribute):
if not mountpoint[0] == '/':
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), mountpoint)
mp = mountpoint.rstrip('/')if len(mountpoint) > 1 else mountpoint
rec = False
rep = ''
if os.path.exists(mp):
try:
rp = self.mi[mp][attribute]
rep = mp
except:
rec = True
a,rep,rp = self.__check(os.path.dirname(mp),attribute)
return [rec,rep,rp]
else:
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), mp)
def relpath(self,mountpoint):
rec,rep,mp = self.__check(mountpoint,'relpath')
#print(mountpoint,rec,rep,mp)
if rec: return mp.replace(rep,'') + mountpoint
return mp
def fstype(self,mountpoint):
return self.__check(mountpoint,'fstype')[2]
def typ(self,mountpoint):
return self.__check(mountpoint,'typ')[2]
def device(self,mountpoint):
return self.__check(mountpoint,'dev')[2]
class MyConfigParser(ConfigParser):
comment = """replace get in Configparser to give the default-option, if a
section doesn't exist, and option exists in default"""
def get(self, section, option, **kw):
try:
return ConfigParser.get(self, section, option, raw=True)
except:
return ConfigParser.get(self, 'DEFAULT', option, raw=True)
class Myos():
def __init__(self,dry=False):
self.dry = dry
pass
def __run__(self,command,conn=None):
if not conn == None:
if connect(conn):
out=''
stdin, stdout, stderr = conn['conn'].exec_command(command)
for line in stdout:
out += line
return(out)
else:
print("Host not reachable (Myos): %s" % (conn['host']))
def stat(self,path,conn=None):
if not conn == None:
command='/usr/bin/stat'
return(self.__run__(command,conn))
else:
return os.stat(path)
def path_isdir(self,path,conn=None):
#print("myos.path",os.path.exists(path))
if not conn == None:
command='/bin/test -d %s' % (path)
return(self.__run__(command,conn))
else:
# print("is local dir %s" % (path))
return os.path.isdir(path)
def path_isfile(self,path,conn=None):
if not conn == None:
command='/bin/test -f %s' % (path)
return(self.__run__(command,conn))
else:
# print("is local file %s" % (path))
return os.path.isfile(path)
def path_realpath(self,path,conn=None):
#print("myos.path",os.path.exists(path))
if not conn == None:
command='/usr/bin/realpath %s' % (path)
return(self.__run__(command,conn))
else:
# print("local realpath for %s" % (path))
return os.path.realpath(path)
def path_exists(self,path,conn=None):
#print("myos.path",os.path.exists(path))
if not conn == None:
command='/bin/test -e %s' % (path)
return(self.__run__(command,conn))
else:
# print("exists-local %s" % (path))
return os.path.exists(path)
def remove(self,path,conn=None):
if self.dry == True:
print('Remove %s (dry run)' % (path))
return
else:
if not conn == None:
command='/bin/rm %s' % (path)
return(self.__run__(command,conn))
else:
# print("remove-local %s" % (path))
return os.remove(path)
def rename(self,From,To,conn=None):
if self.dry == True:
print('Rename %s to %s (dry run)' % (From,To))
return
else:
if not conn == None:
# print("RENAME",From,To,conn['host'])
command='/bin/mv %s %s' % (From,To)
return(self.__run__(command,conn))
else:
# print("rename-local %s %s" % (From,To))
return os.rename(From,To)
def path_islink(self,path,conn=None):
if not conn == None:
command='/bin/test -h %s' % (path)
return(self.__run__(command,conn))
else:
return os.path.islink(path)
def listdir(self,path,conn=None):
if not conn == None:
command='/bin/ls %s' % (path)
return(self.__run__(command,conn))
else:
return os.listdir(path)
class Config():
def __init__(self,cfile='/etc/mkbackup-btrfs.conf'):
self.cfile = cfile
#self.config = ConfigParser()
self.config = MyConfigParser()
#self.hostname = subprocess.check_output("/bin/hostname",shell=True).decode('utf8').split('\n')[0]
self.hostname=socket.gethostname()
self.mountinfo = MountInfo()
self.syssubvol = self.mountinfo.relpath('/')[1:]
#self.syssubvol=subprocess.check_output(['/usr/bin/grub-mkrelpath','/'], shell=False).decode('utf8').split("\n")[0].strip("/")
self.ssh = dict()
self.ssh_cons = dict()
#if os.path.exists(self.cfile):
if Myos().path_exists(self.cfile):
pass #print('OK')
else:
print('Default-Config created at %s' % (self.cfile))
self.CreateConfig()
self._read()
for i in self.ListIntervals() +['DEFAULT']:
self.ssh[i] = dict()
for s in ['SRC', 'SNP', 'BKP']:
#print('X',self.getSSHLogin(s,i))
self.ssh[i][s] = dict()
if self.getSSHLogin(s,i) != None:
c,x,p,uh = self.getSSHLogin(s,i).strip().split(' ')
u,h = uh.split('@')
if not uh in self.ssh_cons:
self.ssh_cons[uh] = paramiko.SSHClient()
self.ssh_cons[uh].set_missing_host_key_policy(paramiko.AutoAddPolicy())
#self.ssh_cons[uh].connect(h, int(p), u)
#self.ssh[i][s]['conn'] = uh
self.ssh[i][s]['conn'] = self.ssh_cons[uh]
self.ssh[i][s]['host'] = h
self.ssh[i][s]['port'] = int(p)
self.ssh[i][s]['user'] = u
self.ssh[i][s]['creds'] = (h, int(p), u)
self.ssh[i][s]['active'] = False
else:
self.ssh[i][s] = None
def _read(self):
csup = dict() #for each dropin-file a csup = config-superseed-dict-entry
#self.config = ConfigParser()
self.config.read(self.cfile)
self.csupdir = self.cfile+'.d'
if os.path.exists(str(self.csupdir)) and os.path.isdir(str(self.csupdir)):
for csuplst in os.listdir(self.csupdir):
if csuplst.endswith('.conf'):
csup[csuplst] = ConfigParser()
csup[csuplst].read(self.csupdir+'/'+csuplst)
# first superseed defaults
for i in sorted(csup.keys()):
# Set Options
for j in ['DEFAULT']:
for k in csup[i].defaults() if j == 'DEFAULT' else csup[i].options(j):
if self.config.has_section(j) or j == 'DEFAULT':
if k == 'ignore':
# only attend pattern on option 'ignore'
orig = self.config.get(j,k) + ',' if self.config.has_option(j,k) else ''
elif k == 'description':
# only attend pattern on option 'description'
#orig = self.config.get(j,k) if self.config.has_option(j,k) else ''
orig = ''
else:
# if option is not ignore, do the same as without
# +, but remove + as first character
orig = ''
self.config.set(j,k,orig + re.sub('^\+','',csup[i].get(j,k)))
#self.config.set(j,k,re.sub('^\+*','',csup[i].get(j,k)))
else:
# add section
self.config.add_section(j)
for k in csup[i].options(j):
# add option to new section
self.config.set(j,k,re.sub('^\+','',csup[i].get(j,k)))
# second superseed normal options
for i in sorted(csup.keys()):
for j in csup[i].sections():
for k in csup[i].defaults() if j == 'DEFAULT' else csup[i].options(j):
if self.config.has_section(j) or j == 'DEFAULT':
if k == 'ignore':
# only attend pattern on option 'ignore'
orig = self.config.get(j,k) + ',' if self.config.has_option(j,k) else ''
elif k == 'description':
# only attend pattern on option 'description'
#print(self.config.get(j,k))
orig = self.config.get(j,k) if self.config.has_option(j,k) else ''
else:
# if option is not ignore, do the same as without
# +, but remove + as first character
orig = ''
self.config.set(j,k,orig + re.sub('^\+','',csup[i].get(j,k)))
else:
# add section
self.config.add_section(j)
for k in csup[i].options(j):
# add option to new section
self.config.set(j,k,re.sub('^\+','',csup[i].get(j,k)))
# If directory, where mkbackup-btrfs is started from, is one of SNP or
# BKP, set SRC to the configured path and store
psrc = os.getcwd()
if '/'+psrc.strip('/') == self.getMountPath('SNP')[1]:
#print("A",self.getMountPath('SNP'))
self.config.set('DEFAULT','SRC', ' '.join(self.getMountPath('SNP')))
self.config.set('DEFAULT','srcstore', self.getStoreName('SNP'))
elif '/'+psrc.strip('/') == self.getMountPath('SNP')[1]+'/'+self.getStoreName('SNP'):
#print("B")
self.config.set('DEFAULT','SRC', ' '.join(self.getMountPath('SNP')))
self.config.set('DEFAULT','srcstore', self.getStoreName('SNP'))
elif '/'+psrc.strip('/') == self.getMountPath('BKP')[1]+'/'+self.getStoreName('BKP'):
#print("C")
self.config.set('DEFAULT','SRC', ' '.join(self.getMountPath('BKP')))
self.config.set('DEFAULT','srcstore', self.getStoreName('BKP'))
elif '/'+psrc.strip('/') == self.getMountPath('BKP')[1]:
#print("D")
self.config.set('DEFAULT','SRC', ' '.join(self.getMountPath('BKP')))
self.config.set('DEFAULT','srcstore', self.getStoreName('BKP'))
else:
#print("E")
self.config.set('DEFAULT','SRC', psrc)
self.config.set('DEFAULT','srcstore', '')
# for i in self.config.sections():
# print('XX[%s]' %(i))
# for j in self.config.options(i):
# print("XX"+j+' = ',self.__trnName(self.config.get(i,j)))
# print('')
def getssh(self,tag,store):
tg = tag if tag in self.ssh else 'DEFAULT'
return(self.ssh[tg][store])
def getSsh(self,tag):
tg = tag if tag in self.ssh else 'DEFAULT'
return(self.ssh[tg])
def CreateConfig(self):
self.config['DEFAULT'] = {
'Description': "Erstellt ein Backup",
'SNPMNT': '/var/cache/btrfs_pool_SYSTEM',
'BKPMNT': '/var/cache/backup',
'snpstore': '',
'bkpstore': '$h',
'volumes': '$S,__ALWAYSCURRENT__',
'interval': 5,
'symlink': 'LAST',
'transfer': False,
'notification': None,
'notification_urgency': None}
self.config['hourly'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '24','transfer': True}
self.config['daily'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '7','transfer': True}
self.config['weekly'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '5','transfer': True}
self.config['monthly'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '12','transfer': True}
self.config['yearly'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '7','transfer': True}
self.config['afterboot'] = {'volumes': '$S','interval': '4','symlink': 'LASTBOOT'}
self.config['aptupgrade'] = {'volumes': '$S','interval': '6','symlink': 'BEFOREUPDATE'}
self.config['dmin'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '6'}
self.config['plugin'] = {'volumes': '$S,__ALWAYSCURRENT__','interval':
'5','transfer': True, 'notification': 'desktop', 'notification_urgency': 1}
self.config['manually'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '5','symlink': 'MANUALLY',
'transfer': True, 'notification': 'desktop', 'notification_urgency': 2}
with open(self.cfile, 'w') as configfile:
try:
self.config.write(configfile)
except:
exit("Failure during creation of config-file")
return(self.config)
def PrintConfig(self,tag=None,of=None):
if tag == None:
seclist = self.config.sections()
else:
seclist = [tag]
out = list()
if tag == None:
out.append('[DEFAULT]')
for j in self.config.defaults():
out.append("%s = %s" % (j,self.__trnName(self.config.get('DEFAULT',j))))
out.append('')
#for i in self.config.sections() if tag == None else [tag]:
for i in seclist:
out.append('[%s]' %(i))
for j in self.config.options(i):
out.append("%s = %s" % (j,self.__trnName(self.config.get(i,j))))
out.append('')
if of != None:
with open(of, 'w') as f:
try:
f.write('\n'.join(out))
except:
raise
exit("Failure during creation of tmp-config-file")
else:
print('\n'.join(out))
def ListIntervals(self):
LST = []
for i in self.config.sections():
LST.append(i)
LST.append('misc')
return(LST)
def ListIntervalsFull(self):
#self._read()
LST = []
for i in self.config.sections():
LST.append(i+': '+str(self.config.get(i,'interval')))
LST.append('misc: '+str(self.config.get(i,'interval')))
return(LST)
def ListSymlinkNames(self):
#self._read()
LST = []
for i in self.config.sections():
LST.append(self.config.get(i,'symlink'))
return(list(set(LST)))
def getMountPath(self, store='SRC', tag='DEFAULT', shlogin=False, original=True):
if store == 'SRC':
path = self.config.get(tag,'SRC')
elif store == 'SNP':
path = self.config.get(tag,'SNPMNT')
elif store == 'BKP':
path = self.config.get(tag,'BKPMNT')
else:
print("EE - getMountPath: store %s is not allowed (%s) set path to SRC" % (store,tag))
path = self.config.get('DEFAULT','SRC')
#print('PATH',tag,path)
_ssh = path.split(':')
if len(_ssh) == 1:
path = _ssh[0]
SSH = None
elif len(_ssh) == 2:
userhost,path = _ssh
port = '22'
SSH = [userhost,port]
elif len(_ssh) == 3:
userhost,port,path = _ssh
SSH = [userhost,port]
if shlogin:
return(SSH)
else:
if original:
sshout = ''
if SSH != None: sshout=':'.join(SSH)+':'
return(sshout+'/'+path.strip('/'))
else:
return('/'+path.strip('/'))
# avoid deleting of / - but it's buggy, so return above is inserted
if '/'+path.strip('/') != "/":
return('/'+path.strip('/'))
else:
return(None)
def getSSHLogin(self,store='SRC',tag='DEFAULT'):
if self.getMountPath(store=store,tag=tag,shlogin=True) is None:
return(None)
else:
uh,p = self.getMountPath(store=store,tag=tag,shlogin=True)
return('ssh -p %s %s ' % (p,uh))
def getStoreName(self,store='SRC',tag='DEFAULT'):
if store == 'SRC':
try:
path = self.config.get(tag,'srcstore').strip('/')
except:
path = self.config.get('DEFAULT','srcstore').strip('/')
elif store == 'SNP':
try:
path = self.__trnName(self.config.get(tag,'snpstore').strip('/'))
except:
path = self.__trnName(self.config.get('DEFAULT','snpstore').strip('/'))
elif store == 'BKP':
try:
path = self.__trnName(self.config.get(tag,'bkpstore').strip('/'))
except:
path = self.__trnName(self.config.get('DEFAULT','bkpstore').strip('/'))
if '/'+path.strip('/') != "/":
return(path.strip('/'))
else:
return('')
def getStorePath(self,store='SRC',tag='DEFAULT',original=False):
sn = '/'+self.getStoreName(store=store,tag=tag) if len(self.getStoreName(store=store,tag=tag)) > 0 else ''
return(self.getMountPath(store=store,tag=tag,original=original) + sn)
def cmdsh(self,tag='DEFAULT',store='SRC',cmd=''):
if self.getssh(tag,store) == None:
return('',subprocess.check_output(cmd, shell=True).decode(),'')
else:
out = ''
conn = self.getssh(tag,store)
connect(conn)
return conn['conn'].exec_command(cmd)
def remotecommand(self,tag='DEFAULT',store='SRC',cmd='',stderr=None):
if self.getssh(tag,store) == None:
#print("noconn")
try:
ret = subprocess.run(cmd,stderr=stderr,stdout=subprocess.PIPE)
if ret.returncode > 0:
pass
except subprocess.CalledProcessError as e:
raise
return ret.stdout.decode("utf-8").rstrip('/n')
else:
#print("conn",self.ssh[tag][store]['host'])
out = ''
conn = self.getssh(tag,store)
if connect(conn):
stdin, stdout, stderr = conn['conn'].exec_command(' '.join(cmd))
if not stdout:
#print("Xr")
out = stdout.readlines()
err = stderr.readlines()
return(''.join(out) if len(err) == 0 else False)
else:
#print("Yr",stdout.read().decode("utf-8"))
return(stdout.read().decode("utf-8"))
else:
print("Host not reachable (remcomd): %s" % (conn['host']))
return('')
def getDevice(self,store='SRC',tag='DEFAULT'):
mp = self.getMountPath(store=store,tag=tag,original=False)
conn = self.getssh(tag,store)
connect(conn)
mi = MountInfo(conn=conn)
amount=mi.fstype(mp)
if mi.fstype(mp) == 'autofs':
try:
Myos().stat(self.getStorePath(store=store,tag=tag),conn=conn)
except:
Myos().stat(os.path.dirname(self.getStorePath(store=store,tag=tag)),conn=conn)
mi = MountInfo(conn=self.getssh(tag,store))
return(mi.device(mp) if mi.fstype(mp) != 'autofs' else None)
def getUUID(self,store='SRC',tag='DEFAULT'):
try:
device = self.getDevice(store=store,tag=tag)
except FileNotFoundError:
#print("UID_NOENT")
#raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), store + " " + tag)
raise
except:
return None
#print("DEVICE",device,store,tag)
if device == None: return None
cmd = ['/sbin/blkid', device.rstrip("\n"), '-o', 'value', '-s', 'UUID']
uuid = self.remotecommand(tag,store,cmd)
#print("UUID",uuid,store,tag,' '.join(cmd))
return uuid.rstrip('\n') if uuid.rstrip('\n') != '' else None
def setBKPPath(self,mount):
#self._read()
self.config['DEFAULT']['BKPMNT'] = mount
def setBKPStore(self,store):
#self._read()
self.config['DEFAULT']['bkpstore'] = store
def setSNPPath(self,mount):
#self._read()
self.config['DEFAULT']['SNPMNT'] = mount
def setSNPStore(self,store):
#self._read()
self.config['DEFAULT']['snpstore'] = store
def getInterval(self,intv='misc'):
#self._read()
try:
return(self.config.get(intv,'interval'))
except:
return(self.config.get('DEFAULT','interval'))
def getTransfer(self,intv='misc'):
#self._read()
try:
return(s2bool(self.config.get(intv,'transfer')))
except:
return(s2bool(self.config.get('DEFAULT','transfer')))
def getSymLink(self,intv='misc'):
#self._read()
try:
return(self.config.get(intv,'symlink'))
except:
return(self.config.get('DEFAULT','symlink'))
def getIsDefault(self,intv='misc'):
#self._read()
try:
self.config.get(intv,'interval')
return(intv)
except:
return('default')
def getVolumes(self,tag='default'):
#self._read()
VOLSTRANS = []
try:
VOLS = self.config.get(tag,'volumes')
except:
VOLS = self.config.get('DEFAULT','volumes')
for vol in VOLS.split(','):
VOLSTRANS.append(self.__trnName(vol))
return(VOLSTRANS)
def ListIntVolumes(self):
#self._read()
VOLSTRANS = []
for intv in self.ListIntervals():
try:
VOLS = self.config.get(intv,'volumes')
except:
VOLS = self.config.get('DEFAULT','volumes')
VOLS = VOLS.split(',')
for i, item in enumerate(VOLS):
VOLS[i] = self.__trnName(item)
VOLSTRANS.append('\t'+intv+': '+' '.join(VOLS))
return(VOLSTRANS)
def getIgnores(self,intv='misc'):
try:
r = self.config.get(intv,'ignore')
except:
try:
r = self.config.get('DEFAULT','ignore')
except:
r=None
return(None if r == '' or r == None else r)
def getNotification(self,intv='misc'):
#print('GN',intv)
try:
r = self.config.get(intv,'notification')
except:
try:
r = self.config.get('DEFAULT','notification')
except:
r=None
return(None if r == '' or r == None else r)
def getUrgency(self,intv='misc'):
#print('GU',intv)
try:
r = self.config.get(intv,'notification_urgency')
except:
try:
r = self.config.get('DEFAULT','notification_urgency')
except:
r=None
return(None if r == '' or r == None else r)
def __trnName(self,short):
ret = list()
for sh in short.split(','):
if sh == "$S": ret.append(self.syssubvol)
elif sh == "$h": ret.append(self.hostname)
else: ret.append(sh)
return(','.join(ret))

View file

@ -0,0 +1,49 @@
"""Emmitter functionality."""
import dbus
import dbus.service
import dbus.glib
class Emitter(dbus.service.Object):
"""Emitter DBUS service object."""
def __init__(self, conn=None, object_path=None, bus_name=None):
"""Initialize the emitter DBUS service object."""
dbus.service.Object.__init__(self, conn=conn, object_path=object_path)
@dbus.service.signal(dbus_interface='at.xundeenergie.mkbackup.Status')
def update(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a update signal')
@dbus.service.signal(dbus_interface='at.xundeenergie.mkbackup.Status')
def start(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a start signal')
@dbus.service.signal(dbus_interface='at.xundeenergie.mkbackup.Status')
def finished(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a finished signal')
@dbus.service.signal(dbus_interface='at.xundeenergie.mkbackup.Status')
def reset(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a reset signal')
""" Example to use
progress = Emitter(dbus.SystemBus(),
'/at/xundeenergie/mkbackup/Status')
progress.start(
{'intv': 'hourly'})
progress.update(
{'intv': 'hourly', 'progr': 5})
progress.finished(
{'intv': 'hourly'})
progress.reset(
{'intv': 'hourly'})
"""

View file

@ -0,0 +1,41 @@
#!/usr/bin/python3 -d
#!/usr/bin/env python3
import dbus, dbus.service, dbus.exceptions
import sys
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
# Initialize a main loop
DBusGMainLoop(set_as_default=True)
loop = GLib.MainLoop()
# Declare a name where our service can be reached
try:
sysbus_name = dbus.service.BusName("at.xundeenergie",
bus=dbus.SystemBus(),
do_not_queue=True)
except dbus.exceptions.NameExistsException:
print("service is already running")
sys.exit(1)
# Run the loop
try:
# Create our initial objects
from services.mkbackup import MkBackup
#from services.notifications import AllStati
#from services.notifications import Properties
#from services.mkbackup import Properties
MkBackup(sysbus_name, "/at/xundeenergie/mkbackup/Intervals")
#AllStati(sysbus_name, "/at/xundeenergie/mkbackup")
#Properties(sysbus_name, "/at/xundeenergie/mkbackup")
#Properties(sysbus_name, dbus_interface="/at/xundeenergie/mkbackup")
loop.run()
except KeyboardInterrupt:
print("keyboard interrupt received")
except Exception as e:
print("Unexpected exception occurred: '{}'".format(str(e)))
finally:
loop.quit()

View file

@ -0,0 +1,117 @@
import dbus
import dbus.service
import random
import time
import os
from mkbackup_btrfs_config import Config, MountInfo, connect, Myos, __version__
config = Config()
class MkBackup:
def __init__(self, bus_name, base_path):
# self._bus = dbus.SystemBus()
# self.notification = Notification()
Intervals(bus_name, base_path)
for intv in config.ListIntervals():
Properties(bus_name, os.path.join(base_path, intv), intv)
class Intervals(dbus.service.Object):
def __init__(self, bus_name, bus_path):
super().__init__(bus_name, bus_path)
@dbus.service.method(dbus_interface='at.xundeenergie.mkbackup.Intervals',
in_signature='', out_signature='v')
def Names(self):
return config.ListIntervals()
class Properties(dbus.service.Object):
def __init__(self, bus_name, bus_path, interval):
super().__init__(bus_name, bus_path)
self.interface = "at.xundeenergie.mkbackup.Status"
self.interval = interval
self.STATI = ['reset', 'stop', 'running', 'finished']
self.properties = dict()
self.properties[self.interface] = dict()
self.properties[self.interface]['progress'] = 0 # 0-100
self.properties[self.interface]['status'] = 'stop' # stop, running, finished, reset
self.properties[self.interface]['transfer'] = config.getTransfer(interval)
self.properties[self.interface]['lastrun'] = 0 # datetime
self.properties[self.interface]['finished'] = True # Boolean
self.properties[self.interface]['name'] = interval # Boolean
self.properties['function'] = dict()
self.properties['function']['progress'] = self.update_progress
self.properties['function']['status'] = self.update_status
# self.properties['function']['lastrun'] = self.update_lastrun
from dbus import Interface
def update_progress(self, interface, incr):
print("Update progress: %i / %i, %s" % (float(incr),
self.properties[interface]['progress'],self.properties[interface]['status']))
if self.properties[interface]['status'] == 'running':
if 0 < self.properties[interface]['progress'] + float(incr) < 100:
#self.properties[interface]['progress'] = self.properties[interface]['progress'] + float(incr)
self.properties[interface]['progress'] += float(incr)
elif self.properties[interface]['progress'] + float(incr) >= 100:
self.properties[interface]['progress'] = 99
else:
print('B', incr, type(incr))
return self.properties[interface]['progress']
def update_status(self, interface, status):
if status in self.STATI:
print("Update status: %s" % status)
print("Status: ", self.properties[interface]['status'])
if status == 'finished':
self.properties[interface]['status'] = status
self.properties[interface]['progress'] = 100
elif status == 'stop':
self.properties[interface]['status'] = status
self.properties[interface]['progres'] = 0
elif status == 'reset':
self.properties[interface]['status'] = 'running'
self.properties[interface]['progress'] = 0
print("status: ", self.properties[interface]['status'])
return self.properties[interface]['status']
@dbus.service.method(dbus.PROPERTIES_IFACE,
in_signature='ss', out_signature='v')
def Get(self, interface_name, property_name):
return self.GetAll(interface_name)[property_name]
@dbus.service.method(dbus.PROPERTIES_IFACE,
in_signature='s', out_signature='a{sv}')
def GetAll(self, interface_name):
if interface_name == self.interface:
return self.properties[interface_name]
else:
raise dbus.exceptions.DBusException(
'at.xundeenergie.mkbackup.UnknownInterface',
'The Foo object does not implement the %s interface'
% interface_name)
@dbus.service.method(dbus.PROPERTIES_IFACE,
in_signature='ssv')
def Set(self, interface_name, property_name, new_value):
# validate the property name and value, update internal state…
"""https://recalll.co/ask/v/topic/D-Bus-D-Feet-Send-Dictionary-of-String%2CVariants-in-Python-Syntax/5565e1372bd273d7108b7b82
__import__('gi.repository.GLib', globals(), locals(), ['Variant']).Variant("s", "value")"""
if interface_name in self.properties:
if property_name in self.properties[interface_name]:
func = self.properties['function'].get(property_name)
new_value = func(interface_name, new_value)
#self.properties[str(interface_name)][str(property_name)] = new_value
self.PropertiesChanged(interface_name,
{ property_name: new_value, 'interval': self.interval}, [])
else:
raise dbus.exceptions.DBusException(
'at.xundeenergie.mkbackup.UnknownInterface',
'The Foo object does not implement the %s interface'
% interface_name)
@dbus.service.signal(dbus.PROPERTIES_IFACE,
signature='sa{sv}as')
def PropertiesChanged(self, interface_name, changed_properties,
invalidated_properties):
pass

View file

@ -0,0 +1,242 @@
import dbus
import dbus.service
import random
import time
import os
from gi.repository import GLib
from mkbackup_btrfs_config import Config, MountInfo, connect, Myos, __version__
config = Config()
from system_notification_emitter import Emitter
class Notification(Emitter):
def __init__(self):
super().__init__(conn=dbus.SystemBus(), object_path='/at/xundeenergie/notifications/advanced/Notification')
class Intervals:
def __init__(self, bus_name, base_path="/at/xundeenergie/mkbackup"):
#super().__init__(bus_name, "/at/xundeenergie/mkbackup")
self.base_path = base_path
self._bus = dbus.SystemBus()
# self.stati = dict()
# self.stati[None] = None
self.notification = Notification()
for intv in config.ListIntervals():
# self.stati[intv] = dict()
# self.stati[intv]['progress'] = 0
# self.stati[intv]['trans'] = config.getTransfer(intv)
# self.stati[intv]['lastrun'] = 0
# self.stati[intv]['finished'] = True
#self._set_listeners(os.path.join(base_path, intv))
# self.stati[intv]['methods'] = Properties(bus_name,
# os.path.join(base_path, intv))
Properties(bus_name, os.path.join(base_path, intv), intv)
def _set_listeners(self,dbus_path):
"""
dbus-send --system "/at/xundeenergie/mkbackup"
--dest="at.xundeenergie.mkbackup"
"at.xundeenergie.mkbackup.Status.progress" string:hourly "int16:10"
"""
update = self._bus.add_signal_receiver(
path=dbus_path,
handler_function=self._progress,
dbus_interface="at.xundeenergie.mkbackup.Status",
signal_name='update')
finished = self._bus.add_signal_receiver(
path=dbus_path,
handler_function=self._finished,
dbus_interface="at.xundeenergie.mkbackup.Status",
signal_name='finished')
start = self._bus.add_signal_receiver(
path=dbus_path,
handler_function=self._start,
dbus_interface="at.xundeenergie.mkbackup.Status",
signal_name='start')
reset = self._bus.add_signal_receiver(
path=dbus_path,
handler_function=self._reset,
dbus_interface="at.xundeenergie.mkbackup.Status",
signal_name='reset')
def _remove_listeners(self,intv):
update = self._bus.remove_signal_receiver(
path=dbus_path,
dbus_interface="at.xundeenergie.mkbackup.Status",
signal_name='update')
finished = self._bus.remove_signal_receiver(
path=dbus_path,
dbus_interface="at.xundeenergie.mkbackup.Status",
signal_name='finished')
start = self._bus.remove_signal_receiver(
path=dbus_path,
dbus_interface="at.xundeenergie.mkbackup.Status",
signal_name='start')
reset = self._bus.remove_signal_receiver(
path=dbus_path,
dbus_interface="at.xundeenergie.mkbackup.Status",
signal_name='reset')
try:
del self.state[intv]
except KeyError as ex:
print("No such key: '%s'" % ex.message)
def _start(self, intv):
self.stati[intv]['progress'] = 0
if self.stati[intv]['finished']:
print("Reset %s first" % (intv))
return
print("%s start" % (intv))
def _progress(self, intv, progr):
if progr >= 0 and not self.stati[intv]['finished']:
if 0 < self.stati[intv]['progress'] + progr <= 99:
self.stati[intv]['progress'] += progr
print("%s run progress: %i/100%%" % (str(intv),
int(self.stati[intv]['progress'])))
else:
self.stati[intv]['progress'] = 99
self.sig_update(intv, self.stati[intv]['progress'])
print("Progress: %s" % (self.stati[intv]['progress']))
def _finished(self, intv):
self.stati[intv]['finished'] = True
self.stati[intv]['progress'] = 100
self._send_notification(body="%s finished" % (intv))
self.sig_update(intv, self.stati[intv]['progress'])
print("%s finished" % (intv))
def _reset(self, intv):
self.stati[intv]['finished'] = False
self.stati[intv]['progress'] = 0
print("%s reset" % (intv))
def _send_notification(self, header="backup", body="Unconfigured Message"):
msg = dict()
msg['sender'] = "mkbackup"
msg['header'] = header
msg['body'] = body
self.notification.normal(msg)
class AllStati(dbus.service.Object):
def __init__(self, bus_name, base_path="/at/xundeenergie/mkbackup"):
super().__init__(bus_name, base_path)
@dbus.service.method("at.xundeenergie.mkbackup",
in_signature='', out_signature='v')
def Intervals(self):
return config.ListIntervals()
MY_INTERFACE = 'at.xundeenergie.mkbackup.Properties'
class Status(dbus.service.Object):
def __init__(self, bus_name, base_path, intv, stati):
super().__init__(bus_name, base_path+'/'+intv)
self.interface_name = base_path+'/'+intv
self.property_name = intv
self.status = stati
@dbus.service.method("at.xundeenergie.mkbackup.Status",
in_signature='', out_signature='v')
def Progress(self):
return self.status['progress']
@dbus.service.method("at.xundeenergie.mkbackup.Status",
in_signature='', out_signature='a{sv}')
def Props(self):
return {
'progress': self.status['progress'],
'transfer': self.status['trans'],
'lastrun' : self.status['lastrun'],
'finished': self.status['finished']
}
@dbus.service.signal("at.xundeenergie.mkbackup.Status", signature='si')
def sig_update(self, intv, progr):
pass
@dbus.service.signal("at.xundeenergie.mkbackup.Status", signature='si')
def sig_finished(self, intv, progr):
pass
class Properties(dbus.service.Object):
def __init__(self, bus_name, bus_path, interval):
super().__init__(bus_name, bus_path)
self.interface_name = dbus.PROPERTIES_IFACE
self.interface = "at.xundeenergie.mkbackup.Status"
self.interval = interval
self.properties = dict()
self.properties[self.interface] = dict()
self.properties[self.interface]['progress'] = 0
self.properties[self.interface]['transfer'] = config.getTransfer(interval)
self.properties[self.interface]['lastrun'] = 0
self.properties[self.interface]['finished'] = True
from dbus import Interface
@dbus.service.method(dbus.PROPERTIES_IFACE,
in_signature='ss', out_signature='v')
def Get(self, interface_name, property_name):
return self.GetAll(interface_name)[property_name]
@dbus.service.method(dbus.PROPERTIES_IFACE,
in_signature='s', out_signature='a{sv}')
def GetAll(self, interface_name):
print('I', interface_name, 'sI', self.interface, 'P', self.properties,
'PP',self.properties[interface_name])
if interface_name == self.interface:
return self.properties[interface_name]
else:
raise dbus.exceptions.DBusException(
'com.example.UnknownInterface',
'The Foo object does not implement the %s interface'
% interface_name)
@dbus.service.method(dbus.PROPERTIES_IFACE,
in_signature='ssv')
def Set(self, interface_name, property_name, new_value):
# validate the property name and value, update internal state…
"""https://recalll.co/ask/v/topic/D-Bus-D-Feet-Send-Dictionary-of-String%2CVariants-in-Python-Syntax/5565e1372bd273d7108b7b82
__import__('gi.repository.GLib', globals(), locals(), ['Variant']).Variant("s", "value")"""
if interface_name == self.interface:
self.properties[str(interface_name)][str(property_name)] = new_value
self.PropertiesChanged(str(interface_name),
{ str(property_name): new_value }, [])
@dbus.service.signal(dbus.PROPERTIES_IFACE,
signature='sa{sv}as')
def PropertiesChanged(self, interface_name, changed_properties,
invalidated_properties):
pass
#class Notifications(dbus.service.Object):
# def __init__(self, bus_name):
# super().__init__(bus_name, "/at/xundeenergie/mkbackup/Status")
#
# random.seed()
#
# @dbus.service.method("at.xundeenergie.mkbackup.Status",
# in_signature='i', out_signature='s')
# def quick(self, bits=8):
# return str(random.getrandbits(bits))
#
# @dbus.service.method("at.xundeenergie.mkbackup.Status",
# in_signature='i', out_signature='s')
# def slow(self, bits=8):
# thread = SlowThread(bits, self.slow_result)
# return thread.thread_id
#
# @dbus.service.signal("at.xundeenergie.mkbackup.Status", signature='ss')
# def slow_result(self, thread_id, result):
# pass
#
#

View file

@ -0,0 +1,47 @@
import dbus.service
import random
import time
import threading
class SlowThread(object):
def __init__(self, bits, callback):
self._callback = callback
self.result = ''
self.thread = threading.Thread(target=self.work, args=(bits,))
self.thread.start()
self.thread_id = str(self.thread.ident)
def work(self, bits):
num = ''
while True:
num += str(random.randint(0, 1))
bits -= 1
time.sleep(1)
if bits <= 0:
break
self._callback(self.thread_id, str(int(num, 2)))
class RandomData(dbus.service.Object):
def __init__(self, bus_name):
super().__init__(bus_name, "/com/larry_price/test/RandomData")
random.seed()
@dbus.service.method("com.larry_price.test.RandomData",
in_signature='i', out_signature='s')
def quick(self, bits=8):
return str(random.getrandbits(bits))
@dbus.service.method("com.larry_price.test.RandomData",
in_signature='i', out_signature='s')
def slow(self, bits=8):
thread = SlowThread(bits, self.slow_result)
return thread.thread_id
@dbus.service.signal("com.larry_price.test.RandomData", signature='ss')
def slow_result(self, thread_id, result):
pass

View file

@ -0,0 +1,38 @@
"""Emmitter functionality."""
import dbus
import dbus.service
import dbus.glib
class Emitter(dbus.service.Object):
"""Emitter DBUS service object."""
def __init__(self, conn=None, object_path=None, bus_name=None):
"""Initialize the emitter DBUS service object."""
dbus.service.Object.__init__(self, conn=conn, object_path=object_path)
@dbus.service.signal(dbus_interface='at.xundeenergie.Notification')
def low(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a low test signal')
@dbus.service.signal(dbus_interface='at.xundeenergie.Notification')
def normal(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a normal test signal')
@dbus.service.signal(dbus_interface='at.xundeenergie.Notification')
def critical(self,*args,**kwargs):
"""Emmit a test signal."""
print('Emitted a critical test signal')
""" Example to use
Simple_Notification = Emitter(dbus.SystemBus(),
'/at/xundeenergie/notifications/simple/Notification')
Advanced_Notification = Emitter(dbus.SystemBus(),
'/at/xundeenergie/notifications/advanced/Notification')
#Simple_Notification.low('M')
Advanced_Notification.normal(
{'sender': 'emitter1.py', 'header': 'Testmessage', 'body': 'Test Body'})
"""

View file

@ -0,0 +1,9 @@
#!/bin/sh
DEVICE="$1"; shift
ACTION="$1"; shift
MAINPID="$1"
BTRFS=/bin/btrfs
[ x"$MAINPID" = "x" ] && exit 0
/bin/ps h -o command -p "$MAINPID" && $BTRFS $ACTION cancel "$DEVICE" || exit 0

View file

@ -0,0 +1,137 @@
#!/bin/bash
# Create udev-rule and mount-entry for new backup-volume
ACTION=$1
case $2 in
-u)
UUID=$3
DEV=$(readlink -f /dev/disk/by-uuid/$UUID)
PARTUUID="$(blkid /dev/disk/by-uuid/$UUID -o value -s PARTUUID)"
;;
u-*)
UUID=${2#u-}
DEV=$(readlink -f /dev/disk/by-uuid/$UUID)
PARTUUID="$(blkid /dev/disk/by-uuid/$UUID -o value -s PARTUUID)"
;;
-p)
PARTUUID=$3
DEV=$(readlink -f /dev/disk/by-partuuid/$PARTUUID)
UUID="$(blkid /dev/disk/by-partuuid/$PARTUUID -o value -s UUID)"
;;
p-*)
PARTUUID=${2#p-}
DEV=$(readlink -f /dev/disk/by-partuuid/$PARTUUID)
UUID="$(blkid /dev/disk/by-partuuid/$PARTUUID -o value -s UUID)"
;;
d-*)
DEV=${2#d-}
UUID="$(blkid $DEV -o value -s UUID)"
PARTUUID="$(blkid $DEV -o value -s PARTUUID)"
;;
*)
DEV="$(/bin/systemd-escape -p -u $2)"
UUID="$(blkid $DEV -o value -s UUID)"
#PARTUUID="$(blkid $DEV -o value -s PARTUUID)"
PRE="d-"
;;
esac
#DESTUDEV="/tmp/"
#DESTSYSTEMD="/tmp/"
DESTUDEV="/etc/udev/rules.d/"
DESTSYSTEMD="/etc/systemd/system/"
SYSTEMCTL="/bin/systemctl"
echo "$ACTION ${DEV} | ${UUID} | ${PARTUUID}"
sleep 1
if [ "$DEV"x = "x" ]
then
TYPE="btrfs"
else
TYPE="$(blkid $DEV -o value -s TYPE)"
echo "T $TYPE | $DEV"
fi
if [ "$PARTUUID"x = "x" ]; then
if [ "$UUID"x = "x" ]; then
echo "$PARTUUID | $UUID | $DEV is no valid device"
exit 3
else
DUUID="$UUID" #DUUID is uuid which is taken to use
SUUID="ID_FS_UUID" #SUUID is the string for the udev-rule it's UUID or PARTUUID
ID="uuid" #ID is also for the udev-rule. To look in /dev/disk/by-uuid or /dev/disk/by-partuuid
PRE="u-"
fi
else
DUUID="$PARTUUID"
SUUID="ID_PART_ENTRY_UUID"
ID="partuuid"
PRE="p-"
fi
#echo "$DUUID | $SUUID | $ID | $PRE"
# Start by udev
start () {
mkdir -p "${DESTSYSTEMD}var-cache-backup.mount.d/"
cat <<EOF > "${DESTSYSTEMD}var-cache-backup.mount.d/source.conf"
[Mount]
What=/dev/disk/by-${ID}/${DUUID}
EOF
$SYSTEMCTL daemon-reload
}
# Create udev-Rule for new external drive
register () {
cat <<EOF > "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules"
ACTION=="add", KERNEL=="sd*", SUBSYSTEMS=="usb", ENV{${SUUID}}=="$DUUID", SYMLINK+="disk/mars", TAG+="systemd", ENV{SYSTEMD_WANTS}+="mkbackup-external@${PRE}${DUUID}.service", ENV{SYSTEMD_WANTS}+="mkbackup@BKP.target", ENV{SYSTEMD_WANTS}+="smartctl-fast@$(/bin/systemd-escape -p /dev/disk/by-${ID}/${DUUID}).service"
ACTION=="remove", KERNEL=="sd*", SUBSYSTEMS=="usb", ENV{${SUUID}}="$DUUID", \
RUN+="${SYSTEMCTL} --no-block stop mkbackup@BKP.target"
EOF
}
# delete udev-rule, if external drive is not longer in use for backups.
unregister () {
[ -e "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules" ] && rm "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules"
}
case $TYPE in
btrfs)
;;
*)
echo "$DEV isn't a btrfs-filesystem. Exiting"; exit 1;;
esac
case $ACTION in
register)
#setup udev-rule for device
register
;;
unregister)
#delete udev-rule for device
unregister ;;
start)
#activate device
start;;
stop)
#deactivate device
stop ;;
*)
echo "$ACTION not recognized";
exit 2;;
esac
$SYSTEMCTL daemon-reload
#/bin/systemctl
exit 0

View file

@ -0,0 +1,12 @@
#!/bin/bash
/usr/sbin/sendmail -t <<ERRMAIL
To: $1
From: systemd <root@$HOSTNAME>
Subject: $2
Content-Transfer-Encoding: 8bit
Content-Type: text/plain; charset=UTF-8
$(btrfs scrub status "$3")
ERRMAIL

View file

@ -0,0 +1,11 @@
[Unit]
#Description=Starts mounting backups to %h/backup, if this directory exists
#BindsTo=mkbackup-userdir.path
Conflicts=shutdown.target sleep.target suspend.target
[Path]
PathExists=%h/backup
Unit=mkbackup-userdir.service
[Install]
WantedBy=paths.target

View file

@ -0,0 +1,10 @@
[Unit]
Description=Show all userspezific backups in %h/backup
BindsTo=mkbackup-userdir.path
Conflicts=shutdown.target reboot.target umount.target
Before=shutdown.target reboot.target sleep.target umount.target
[Service]
ExecStart=/usr/bin/MksnapshotFS.py -f %h/backup -o ro,allow_root
ExecStop=-/bin/fusermount -u %h/backup
Restart=on-success

View file

@ -0,0 +1 @@
../mkbackup-userdir.path

View file

@ -0,0 +1 @@
mkbackup-btrfs

2156
files/usr/local/bin/mkbackup-btrfs Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
#!/bin/bash
#mawk '$5 ~ "^/$" {gsub ("/","",$4);print $4}' /proc/self/mountinfo
#SV1=$(basename "$(grub-mkrelpath /)")
SV1="XXX"
[ -x /bin/cat ] || exit 1
SV2=$(grep -sq "=subvol=" /proc/cmdline && /bin/cat /proc/cmdline |sed 's/^.*=subvol=\([^ ]*\) .*$/\1/')
#echo "SV1: $SV1 SV2: $SV2"
if [ "$SV1" == "$SV2" ];then
echo "$SV1"
else
echo "$SV2"
fi

View file

@ -0,0 +1,27 @@
#####################################
## System - Lokal
#####################################
# Hier kein subvol angeben. Ist in grub.cfg "rootflags=subvol=@debian"
LABEL=debian / btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime 0 0
#LABEL=debian /boot/grub/x86_64-efi btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/boot-grub-x86_64-efi 0 0
LABEL=debian /home btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/home 0 0
LABEL=debian /usr/local btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/usr-local 0 0
LABEL=debian /opt btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/opt 0 0
LABEL=debian /var/opt btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-opt 0 0
LABEL=debian /var/log btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-log 0 0
LABEL=debian /var/lib/mpd btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-lib-mpd 0 0
#LABEL=debian /var/lib/named btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-lib-named 0 0
LABEL=debian /var/spool btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-spool 0 0
LABEL=debian /var/spool/dovecot btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-spool-dovecot 0 0
LABEL=debian /var/tmp btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-tmp 0 0
#LABEL=debian /var/www btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-www 0 0
LABEL=debian /var/cache btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-cache 0 0
#LABEL=debian /srv btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/srv 0 0
tmpfs /tmp tmpfs nosuid,size=25% 0 0
UUID=2AE9-56D2 /boot/efi vfat umask=0077 0 0
####################################
## BACKUP
####################################
## Ist über unit-Files in /etc/systemd/system geregelt

View file

@ -0,0 +1,92 @@
/*
Copyright (c) 2011-2012, Giovanni Campagna <scampa.giovanni@gmail.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the GNOME nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
var Gettext = imports.gettext;
var Gio = imports.gi.Gio;
var Config = imports.misc.config;
var ExtensionUtils = imports.misc.extensionUtils;
/**
* initTranslations:
* @domain: (optional): the gettext domain to use
*
* Initialize Gettext to load translations from extensionsdir/locale.
* If @domain is not provided, it will be taken from metadata['gettext-domain']
*/
function initTranslations(domain) {
var extension = ExtensionUtils.getCurrentExtension();
domain = domain || extension.metadata['gettext-domain'];
// check if this extension was built with "make zip-file", and thus
// has the locale files in a subfolder
// otherwise assume that extension has been installed in the
// same prefix as gnome-shell
var localeDir = extension.dir.get_child('locale');
if (localeDir.query_exists(null))
Gettext.bindtextdomain(domain, localeDir.get_path());
else
Gettext.bindtextdomain(domain, Config.LOCALEDIR);
}
/**
* getSettings:
* @schema: (optional): the GSettings schema id
*
* Builds and return a GSettings schema for @schema, using schema files
* in extensionsdir/schemas. If @schema is not provided, it is taken from
* metadata['settings-schema'].
*/
function getSettings(schema) {
var extension = ExtensionUtils.getCurrentExtension();
schema = schema || extension.metadata['settings-schema'];
var GioSSS = Gio.SettingsSchemaSource;
// check if this extension was built with "make zip-file", and thus
// has the schema files in a subfolder
// otherwise assume that extension has been installed in the
// same prefix as gnome-shell (and therefore schemas are available
// in the standard folders)
var schemaDir = extension.dir.get_child('schemas');
var schemaSource;
if (schemaDir.query_exists(null))
schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
GioSSS.get_default(),
false);
else
schemaSource = GioSSS.get_default();
var schemaObj = schemaSource.lookup(schema, true);
if (!schemaObj)
throw new Error('Schema ' + schema + ' could not be found for extension '
+ extension.metadata.uuid + '. Please check your installation.');
return new Gio.Settings({ settings_schema: schemaObj });
}

View file

@ -0,0 +1,638 @@
var GLib = imports.gi.GLib;
var Lang = imports.lang;
var Main = imports.ui.main;
var PanelMenu = imports.ui.panelMenu;
var PopupMenu = imports.ui.popupMenu;
var St = imports.gi.St;
var Shell = imports.gi.Shell;
var Gettext = imports.gettext.domain('gnome-shell-extensions');
var _ = Gettext.gettext;
var ExtensionUtils = imports.misc.extensionUtils;
var Me = ExtensionUtils.getCurrentExtension();
var Convenience = Me.imports.convenience;
var Util = imports.misc.util;
var PopupServiceItem = Me.imports.popupServiceItem.PopupServiceItem;
var PopupTargetItem = Me.imports.popupTargetItem.PopupTargetItem;
var PopupMenuItem = Me.imports.popupManuallyItem.PopupServiceItem;
var MountMenuItem = Me.imports.popupMountItem.MountMenuItem;
var DriveMenuItem = Me.imports.popupDriveItem.DriveMenuItem;
var VolMenuItem = Me.imports.popupBkpVolumItem.PopupBKPItem;
var Gio = imports.gi.Gio;
var Mainloop = imports.mainloop;
var refreshTime = 3.0;
var MainLabel;
var icon;
var MainIcon;
var ExtIcon;
var extMediaName = 'external backup-drive';
var Drives = new Object();
//var DisabledIcon = 'my-caffeine-off-symbolic';
//var EnabledIcon = 'my-caffeine-on-symbolic';
var DisabledIcon = 'system-run-symbolic';
var EnabledIcon = 'system-run-symbolic';
//var ConfFile = '/etc/mkbackup-btrfs.conf';
var ConfFile = '/tmp/mkbackup-btrfs.conf.tmp';
var BackupManager = new Lang.Class({
Name: 'BackupManager',
Extends: PanelMenu.Button,
_entries: [],
_init: function() {
this._drives = [ ];
this._volumes = [ ];
this._mounts = [ ];
//Set a FileMonitor on the config-File. So the Config-File is only
//read, when it changed.
this.GF = Gio.File.new_for_path(ConfFile);
//this._monitorConf = this.GF.monitor_file(Gio.FileMonitorFlags.NONE,null,null,null)
this._monitorConf = this.GF.monitor_file(Gio.FileMonitorFlags.NONE,null)
this._monitorConf.connect("changed", Lang.bind(this, function(monitor, file, o, event) {
// without this test, _loadConfig() is called more than once!!
if (event == Gio.FileMonitorEvent.CHANGES_DONE_HINT && ! /~$/.test(file.get_basename())) {
this._loadConfig();
}
}));
this._loadConfig();
this.watchvolume = this._run_command('systemd-escape --path '+this.bkpmnt)+'.mount'
PanelMenu.Button.prototype._init.call(this, 0.0);
var hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });
MainIcon = new St.Icon({icon_name: 'drive-harddisk-usb-symbolic',
style_class: 'system-status-icon'});
ExtIcon = new St.Icon({icon_name: 'drive-harddisk-usb-symbolic',
style_class: 'system-status-icon'});
ExtIcon.hide();
MainLabel = new St.Label({ text: '---',
});
hbox.add_child(MainLabel);
hbox.add_child(MainIcon);
hbox.add_child(ExtIcon);
hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
this.actor.add_actor(hbox);
this.actor.add_style_class_name('panel-status-button');
this.actor.connect('button-press-event', Lang.bind(this, function() {
this._refresh();
}));
Main.panel.addToStatusArea('backupManager', this);
//this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
// Fill the Menu
// First a Entry to open backup-Location
var bkpitem = this.menu.addAction(_("Open Backups"), function(event) {
var context = global.create_app_launch_context(event.get_time(), -1);
var GF = Gio.File.new_for_path('backup');
Gio.AppInfo.launch_default_for_uri(GF.get_uri(),context);
});
this.extItem = new PopupTargetItem(extMediaName, this._check_service('mkbackup@BKP.target','active'));
this.extItem.connect('toggled', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand('mkbackup@BKP.target',
(this._check_service('mkbackup@BKP.target','active') ? 'stop' : 'start'),
'system'));
}));
this.menu.addMenuItem(this.extItem);
this.snapitem = new PopupMenu.PopupMenuItem(_("Take snapshot now (tag: manually)"));
this.snapitem.connect('activate', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand('mkbackup@manually.service', 'restart', 'system'));
this.menu.close();
}));
this.menu.addMenuItem(this.snapitem);
this.bkpsubmenu = new PopupMenu.PopupSubMenuMenuItem(_("Backup-Intervalle"), true);
this.bkpsubmenu.icon.icon_name = 'system-run-symbolic';
this.menu.addMenuItem(this.bkpsubmenu);
//this.descrsubmenu = new PopupMenu.PopupSubMenuMenuItem(_("Info"), true);
//this.descrsubmenu.icon.icon_name = 'system-run-symbolic';
//this.bkpsubmenu.addMenuItem(this.descrsubmenu);
if(this._entries.length > 0)
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._refreshID = Mainloop.timeout_add_seconds(refreshTime, Lang.bind(this, this._refresh_panel));
// New Volumes to use as backup-media
this._monitor = Gio.VolumeMonitor.get();
this._addedDriveId = this._monitor.connect('drive-connected', Lang.bind(this, function(monitor, drive) {
log('DRIVE CONNECTED',drive.get_name());
this._DriveAdded(drive);
}));
this._removedDriveId = this._monitor.connect('drive-disconnected', Lang.bind(this, function(monitor, drive) {
log('DRIVE DISCONNECTED',drive.get_name(),this._removedDriveId)
this._DriveRemoved(drive);
//this.extItem.label.text = _("external backup-drive");
//this.drvsubmenu.destroy();
}));
this._addedVolumeId = this._monitor.connect('volume-added', Lang.bind(this, function(monitor, volume){
this._addVolume(volume);
this._VolumeAdded(volume);
log('VOLUMES',JSON.stringify(this._drives))
}))
this._removedVolumeId = this._monitor.connect('volume-removed', Lang.bind(this, function(monitor, volume){
//this._VolumeRemoved(volume);
}))
/*this._monitor.get_volumes().forEach(Lang.bind(this, function(volume) {
this._VolumeAdded(volume);
}));
this._addedMountId = this._monitor.connect('mount-added', Lang.bind(this, function(monitor, mount) {
log('MOUNT CONNECTED',mount.get_name())
log(mount.get_name())
var volume = mount.get_volume()
log(volume.get_name(),volume.get_uuid())
//log(mount.get_name())
//this._showDrive(drive);
}));*/
//log(Gio.UnixMountPoint.get_device_path('/var/cache/backup'))
return;
},
_DriveAdded: function(drive) {
var Dident = drive.enumerate_identifiers();
var u_dev = drive.get_identifier('unix-device');
var d_name = drive.get_name();
//log('DRIVE unix-device',u_dev,d_name)
this._drives[d_name] = new Object()
this._drives[d_name]['drive'] = drive;
this._drives[d_name]['device'] = drive.get_identifier('unix-device');
//this._drives[d_name]['uuid'] = drive.get_identifier('uuid');
this._drives[d_name]['volumes'] = new Object()
log("ABCDE",this._drives[d_name]['device']);
/*if (drive.has_volumes()) {
log('DHV',drive.get_volumes());
var VList = drive.get_volumes();
VList.forEach(Lang.bind(this, function(volume){
var v_name = volume.get_name();
log(v_name)
this._drives[d_name]['volumes'][v_name] = this._addVolume(volume);
}));
//VList.free()
} else {
log('DHNV',drive.get_volumes());
};
log('VOLUMES',JSON.stringify(this._drives))
log('X',this._drives['ST1000LM024 HN-M101MBB']['device'])
*/
/*
this.drvsubmenu = new PopupMenu.PopupSubMenuMenuItem(drive.get_name(), true);
this.drvsubmenu.icon.icon_name = 'drive-harddisk-usb-symbolic';
this.menu.addMenuItem(this.drvsubmenu);
*/
//log('Drive added',drive.get_name())
//this.drvsubmenu = new PopupMenu.PopupSubMenuMenuItem(_(drive.get_name()), true);
//this.drvsubmenu.icon.icon_name = 'drive-harddisk-usb-symbolic';
//this.drvsubmenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
},
_addVolume: function(volume) {
var drives = volume.get_drive();
//log('D',drives.get_name())
//log('V',volume.get_name())
//this._drives[drives.get_name()][volume.get_name()] = volume;
volume.enumerate_identifiers();
var vol = new Object()
//log(volume.get_identifier('uuid'))
//log(volume.get_identifier('unix-device'))
//log(volume.get_identifier('class'))
//log(volume.get_identifier('label'))
//vol['volume'] = volume;
//vol['uuid'] = volume.get_identifier('uuid');
//vol['u_device'] = volume.get_identifier('unix-device');
//vol['vclass'] = volume.get_identifier('class');
//vol['label'] = volume.get_identifier('label');
//this._drives[drives.get_name()]['volumes'][volume.get_name()] = v_name;
return vol
},
_DriveRemoved: function(drive) {
var me = this.menu._getMenuItems();
this._removeItemByLabel(this.menu, drive.get_name());
//this.drvsubmenu.destroy();
},
_VolumeRemoved: function(volume) {
//Volume is removed, after it's mounted from system(d)
log('VOLUME REMOVED',volume.get_name())
},
_VolumeAdded: function(volume) {
//Volume is added, after it's unmounted from system(d)
log('VOLUME CONNECTED',volume.get_name())
log('ID',this._addedVolumeId)
if (volume.get_drive() == null)
return
var drive = volume.get_drive()
if ( !volume.can_mount() || !drive.is_removable())
return
log(volume.get_mount())
log(volume.enumerate_identifiers())
log(volume.get_identifier('uuid'))
log(volume.get_identifier('unix-device'))
log(volume.get_identifier('class'))
log(volume.get_identifier('label'))
var dr = volume.get_drive()
this._drives[dr.get_name()] = dr
log(dr.get_name())
log(dr.enumerate_identifiers())
log(dr.get_identifier('unix-device'))
var me
try {
me = this.drvsubmenu.menu._getMenuItems();
} catch(e) {
this._DriveAdded(drive)
}
//me = this.drvsubmenu.menu._getMenuItems();
var menuItem = new VolMenuItem(volume, false);
var VF = Gio.File.new_for_path('/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules');
if (VF.query_exists(null)) {
log('UDEV exists','/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules')
menuItem.setToggleState(true);
} else {
log('UDEV not exists','/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules')
menuItem.setToggleState(false);
}
//this._removeItemByLabel(this.drvsubmenu.menu, volume.get_name());
//this.drvsubmenu.menu.addMenuItem(menuItem);
var connID = menuItem.connect('toggled', Lang.bind(this, function() {
log('ACTIVE?',menuItem.state,menuItem.label.text)
if (menuItem.state) {
reg = 'register'
} else {
reg = 'unregister'
}
GLib.spawn_command_line_async(
this._getCommand('mkbackup-'+reg+'@'+volume.get_identifier('unix-device')+'.service', 'start', 'system'));
}));
log(connID,volume.get_name())
//this.menu.addMenuItem(this.drvsubmenu,1);
},
_removeItemByLabel: function(menu, label) {
log('LAB',label)
var children = menu._getMenuItems();
for (var i = 0; i < children.length; i++) {
var item = children[i];
log('REM',item.label.text,label)
if (item.label.text == label)
log('DESTROY',item.label.text)
//item.destroy();
}
},
_addMount: function(mount) {
var item = new MountMenuItem(mount);
this._mounts.unshift(item);
this.menu.addMenuItem(item, 0);
},
_removeMount: function(mount) {
for (var i = 0; i < this._mounts.length; i++) {
var item = this._mounts[i];
if (item.mount == mount) {
item.destroy();
this._mounts.splice(i, 1);
return;
}
}
log ('Removing a mount that was never added to the menu');
},
_run_command: function(COMMAND) {
var output = "";
try {
//output = GLib.spawn_command_line_sync(COMMAND, null, null, null, null);
output = GLib.spawn_command_line_sync(COMMAND);
} catch(e) {
throw e;
}
return output[1].toString().replace(/\n$/, "") + "";
},
_showVolume: function(volume) {
var drive = volume.get_drive();
var mount = volume.get_mount();
if (drive != null && drive.is_removable()){
log('SVDRIVE',drive.get_name(),drive.is_removable())
log('SVVOL',volume.get_name(),volume.get_uuid(),drive.is_removable());
}
},
_showDrive: function(drive) {
//extMediaName = 'external backup-drive';
log('SDDRIVE',drive.get_name(),drive.get_volumes(),drive.has_media(),drive.is_removable(),drive.has_volumes(),drive.enumerate_identifiers());
if (drive.is_removable() && drive.has_volumes()) {
extMediaName = drive.get_name();
log('SDDREMOV')
drive.get_volumes().forEach(Lang.bind(this, function(volume) {
if (volume.can_mount()){
log('SDVOL',volume.get_name());
if ( volume.get_mount() != null ) {
var mount = volume.get_mount();
log('SDMR',mount.get_root());
}
}
}));
} else {
log(drive.is_removable(),drive.has_volumes(),drive.get_volumes());
}
},
_checkMount: function(mount) {
log('CHECK MOUNT '+mount.can_unmount()+' '+mount.can_eject());
log('VOLUME '+mount.get_volume());
return(mount.can_unmount() || mount.can_eject())
},
_getCommand: function(service, action, type) {
var command = "systemctl"
command += " " + action
command += " " + service
command += " --" + type
if (type == "system" && (action != 'is-active' && action != 'is-enabled'))
command = "pkexec --user root " + command
return 'sh -c "' + command + '; exit;"'
},
_getDriveMounted: function(udevice) {
command = "/bin/grep"
command += " " + udevice
command += "/proc/mounts"
return 'sh -c "' + command + '; exit:"'
},
_refresh_panel : function() {
//log('YY',this._drives['ST1000LM024 HN-M101MBB']['volumes'])
//log('YY',this._drives['ST1000LM024 HN-M101MBB'])
var active = false;
var volumes = []
var mounted = false;
volumes.push(this.watchvolume)
// TODO: find all Volumes on the drive, holding the backup and add this
// volumes to the list
volumes.push('home-jakob-Videos-extern.mount')
volumes.push('home-media.mount')
//for (var d in this._drives['ST1000LM024 HN-M101MBB']) {
/*for (var d in this._drives) {
log("D",d,this._drives[d].get_name());
};*/
this.aout = GLib.spawn_command_line_sync(
this._getCommand(this.services.join(' '), 'is-active', 'system'))[1].toString().split('\n');
//log(this.aout.indexOf('active'));
var apos = this.aout.indexOf('active')
active = this.aout.indexOf('active') >= 0
var vout = GLib.spawn_command_line_sync(
this._getCommand(volumes.join(' '), 'is-active', 'system'))[1].toString().split('\n');
mounted = vout.indexOf('active') >= 0
this.bkpsubmenu.icon.style = (active ? "color: #ff0000;" : "color: revert;");
//this.bkpsubmenu.icon.style_class = (active ? "system-status-icon" : "system-status-icon-red");
MainIcon.style = (mounted ? "color: #ff0000;" : "color: revert;");
ExtIcon.style = (mounted ? "color: #ff0000;" : "color: revert;");
(mounted ? ExtIcon.show() : ExtIcon.hide());
//ExtIcon.actor = (mounted ? "visibile = true;" : "visible = false;");
var mlabel = (mounted ? _("mounted") : "");
var alabel = (active ? this.services[apos] : "");
MainLabel.set_text(mlabel + ' ' + alabel);
//MainLabel.set_text(mounted ? _("mounted") : "");
//MainLabel.set_text(active ? this.services[apos] : "");
if (this.menu.isOpen) {
//Menu is open
var me = this.menu._getMenuItems();
me.forEach(Lang.bind(this, function(item) {
//log(item.label.text)
if ( item.label.text == extMediaName ) {
item.setToggleState(this._check_service('mkbackup@BKP.target','active'));
}
}));
if (this.bkpsubmenu.menu.isOpen) {
//Submenu Backu-intervals open
this._refresh();
};
this._refresh();
}
if (this._refreshID != 0)
Mainloop.source_remove(this._refreshID);
this._refreshID = Mainloop.timeout_add_seconds(refreshTime, Lang.bind(this, this._refresh_panel));
GLib.Source.set_name_by_id(this._refreshID, '[gnome-shell] this._refresh_panel');
return false;
},
_check_service: function(service,stat) {
var [_, aout, aerr, astat] = GLib.spawn_command_line_sync(
this._getCommand(service, 'is-'+stat, 'system'));
return (astat == 0);
},
_refresh: function() {
var me = this.bkpsubmenu.menu._getMenuItems();
//log(this.services.join(' '))
var eout = GLib.spawn_command_line_sync(
this._getCommand(this.services.join(' '), 'is-enabled', 'system'))[1].toString().split('\n');
//log(eout);
this.aout = GLib.spawn_command_line_sync(
this._getCommand(this.services.join(' '), 'is-active', 'system'))[1].toString().split('\n');
this._entries.forEach(Lang.bind(this, function(service,index,arr) {
if (! arr[index].enabled == (eout[index] == 'enabled'))
arr[index].changed = true
arr[index].enabled = (eout[index] == 'enabled' ? true : false)
if (! arr[index].active == (this.aout[index] == 'active'))
arr[index].changed = true
arr[index].active = (this.aout[index] == 'active' ? true : false)
}));
this._entries.forEach(Lang.bind(this, function(service,index,arr) {
var serviceItem
me.forEach(Lang.bind(this, function(item) {
if ( item.label.text == service['descr']+' ('+service['name'] + ')' ) {
arr[index].found = true;
serviceItem = item;
me.splice(me.indexOf(item),1);
}
}));
if ( arr[index].changed ) {
if (arr[index].found) {
if ( service.staticserv ) {
serviceItem.setToggleState(service.active);
} else {
serviceItem.setToggleState(service.enabled);
}
} else {
if ( service.staticserv ) {
serviceItem = new PopupTargetItem(service['descr']+' ('+service['name'] + ')', service.active);
this.bkpsubmenu.menu.addMenuItem(serviceItem);
serviceItem.connect('toggled', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand(service['service'], (this._check_service(service.service, 'active') ? 'stop' : 'start'), service["type"]));
}));
} else {
serviceItem = new PopupServiceItem(service['descr']+' ('+service['name'] + ')', service.enabled);
this.bkpsubmenu.menu.addMenuItem(serviceItem);
serviceItem.connect('toggled', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand(service['service'], (this._check_service(service.service, 'enabled') ? 'disable' : 'enable'), service["type"]));
}));
serviceItem.actionButton.connect('clicked', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand(service['service'], (this._check_service(service.service, 'active') ? 'stop' : 'start'), service["type"]));
this.menu.close();
}));
}
}
if (serviceItem.actionButton.child) {
serviceItem.actionButton.child.icon_name = (service.active ? EnabledIcon : DisabledIcon);
serviceItem.actionButton.child.style = (service.active ? "color: #ff0000;" : "color: revert;");
};
if (serviceItem.transferButton) {
serviceItem.transferButton.style = (service.tr ? "text-decoration: revert;" : "text-decoration: line-through;");
};
if (serviceItem.descriptionLabel) {
serviceItem.descriptionLabel.label = service.descr;
};
//log('Changed',service.service)
}
//log('X',arr[index].service,arr[index].enabled,arr[index].active,arr[index].changed)
arr[index].changed = false;
}));
this.bkpsubmenu.menu._getMenuItems().forEach(Lang.bind(this, function(item) {
var mic = 0
if ( me.length > mic ) {
for (var i = mic; i < me.length; i++) {
if (item == me[i]) {
log('DESTROY',me[i].label.text);
item.destroy();
}
}
}
}));
return true;
},
_loadConfig: function() {
//log('LOAD CONFIG')
var intervals
this.services = []
var kf = new GLib.KeyFile()
var obj = new Object();
this._entries = [];
if(kf.load_from_file(ConfFile,GLib.KeyFileFlags.NONE)){
//intervals = kf.get_groups()[0];
this.bkpmnt = kf.get_value('DEFAULT','bkpmnt')
//log('BKP',this.bkppath)
kf.get_groups()[0].forEach(Lang.bind(this, function(interval) {
var obj = new Object();
var i = ""
if (interval === 'DEFAULT')
i = 'misc'
else
i = interval
obj.name = i +' backups';
obj.interval = interval;
obj.service = "mkbackup@"+i+".service";
this.services.push(obj.service)
obj.type = "system";
obj.enabled = false;
obj.active = false;
obj.changed = true;
obj.found = false;
obj.staticserv = false;
try {
obj.tr = (kf.get_value(interval,"transfer").toLowerCase() === "true");
} catch(err) {
obj.tr = (kf.get_value("DEFAULT","transfer").toLowerCase() === "true");
}
try {
obj.descr = (kf.get_value(interval,"description"));
} catch(err) {
try {
obj.descr = (kf.get_value("DEFAULT","description"));
} catch(err) {
obj.descr = "";
}
}
this._entries.push(obj)}));
}
}
});
var backupManager;
function init(extensionMeta) {
//Convenience.initTranslations();
var theme = imports.gi.Gtk.IconTheme.get_default();
theme.append_search_path(extensionMeta.path + "/icons");
}
function enable() {
backupManager = new BackupManager();
}
function disable() {
backupManager.destroy();
}

View file

@ -0,0 +1,576 @@
var GLib = imports.gi.GLib;
var Lang = imports.lang;
var Main = imports.ui.main;
var PanelMenu = imports.ui.panelMenu;
var PopupMenu = imports.ui.popupMenu;
var St = imports.gi.St;
var Shell = imports.gi.Shell;
var Gettext = imports.gettext.domain('gnome-shell-extensions');
var _ = Gettext.gettext;
var ExtensionUtils = imports.misc.extensionUtils;
var Me = ExtensionUtils.getCurrentExtension();
var Convenience = Me.imports.convenience;
var Util = imports.misc.util;
var PopupServiceItem = Me.imports.popupServiceItem.PopupServiceItem;
var PopupTargetItem = Me.imports.popupTargetItem.PopupTargetItem;
var PopupMenuItem = Me.imports.popupManuallyItem.PopupServiceItem;
var MountMenuItem = Me.imports.popupMountItem.MountMenuItem;
var DriveMenuItem = Me.imports.popupDriveItem.DriveMenuItem;
var VolMenuItem = Me.imports.popupBkpVolumItem.PopupBKPItem;
var Gio = imports.gi.Gio;
var Mainloop = imports.mainloop;
var refreshTime = 3.0;
var MainLabel;
var icon;
var MainIcon;
var extMediaName = 'external backup-drive';
var Drives = new Object();
//var DisabledIcon = 'my-caffeine-off-symbolic';
//var EnabledIcon = 'my-caffeine-on-symbolic';
var DisabledIcon = 'system-run-symbolic';
var EnabledIcon = 'system-run-symbolic';
var BackupManager = new Lang.Class({
Name: 'BackupManager',
Extends: PanelMenu.Button,
_entries: [],
_init: function() {
this._drives = [ ];
this._volumes = [ ];
this._mounts = [ ];
//Set a FileMonitor on the config-File. So the Config-File is only
//read, when it changed.
this.GF = Gio.File.new_for_path('/etc/mksnapshot.conf');
this._monitorConf = this.GF.monitor_file(Gio.FileMonitorFlags.NONE,null,null,null)
this._monitorConf.connect("changed", Lang.bind(this, function(monitor, file, o, event) {
// without this test, _loadConfig() is called more than once!!
if (event == Gio.FileMonitorEvent.CHANGES_DONE_HINT && ! /~$/.test(file.get_basename())) {
this._loadConfig();
}
}));
this._loadConfig();
this.watchvolume = this._run_command('systemd-escape --path '+this.bkpmnt)+'.mount'
PanelMenu.Button.prototype._init.call(this, 0.0);
var hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });
MainIcon = new St.Icon({icon_name: 'drive-harddisk-usb-symbolic',
style_class: 'system-status-icon'});
MainLabel = new St.Label({ text: '---',
});
hbox.add_child(MainLabel);
hbox.add_child(MainIcon);
hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
this.actor.add_actor(hbox);
this.actor.add_style_class_name('panel-status-button');
this.actor.connect('button-press-event', Lang.bind(this, function() {
this._refresh();
}));
Main.panel.addToStatusArea('backupManager', this);
//this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
// Fill the Menu
// First a Entry to open backup-Location
var bkpitem = this.menu.addAction(_("Open Backups"), function(event) {
var context = global.create_app_launch_context(event.get_time(), -1);
var GF = Gio.File.new_for_path('backup');
Gio.AppInfo.launch_default_for_uri(GF.get_uri(),context);
});
this.extItem = new PopupTargetItem(extMediaName, this._check_service('mkbackup@BKP.target','active'));
this.extItem.connect('toggled', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand('mkbackup@BKP.target',
(this._check_service('mkbackup@BKP.target','active') ? 'stop' : 'start'),
'system'));
}));
this.menu.addMenuItem(this.extItem);
this.snapitem = new PopupMenu.PopupMenuItem(_("Take snapshot now"));
this.snapitem.connect('activate', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand('mkbackup@manually.service', 'restart', 'system'));
this.menu.close();
}));
this.menu.addMenuItem(this.snapitem);
this.bkpsubmenu = new PopupMenu.PopupSubMenuMenuItem(_("Backup-Intervalle"), true);
this.bkpsubmenu.icon.icon_name = 'system-run-symbolic';
this.menu.addMenuItem(this.bkpsubmenu);
if(this._entries.length > 0)
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._refreshID = Mainloop.timeout_add_seconds(refreshTime, Lang.bind(this, this._refresh_panel));
// New Volumes to use as backup-media
this._monitor = Gio.VolumeMonitor.get();
this._removedDriveId = this._monitor.connect('drive-disconnected', Lang.bind(this, function(monitor, drive) {
log('DRIVE DISCONNECTED',drive.get_name(),this._removedDriveId)
this._DriveRemoved(drive);
//this.extItem.label.text = _("external backup-drive");
//this.drvsubmenu.destroy();
}));
this._addedDriveId = this._monitor.connect('drive-connected', Lang.bind(this, function(monitor, drive) {
log('DRIVE CONNECTED',drive.get_name());
this._DriveAdded(drive);
}));
this._addedVolumeId = this._monitor.connect('volume-added', Lang.bind(this, function(monitor, volume){
this._VolumeAdded(volume);
log('VOLUMES',JSON.stringify(this._drives))
}))
this._removedVolumeId = this._monitor.connect('volume-removed', Lang.bind(this, function(monitor, volume){
this._VolumeRemoved(volume);
}))
/*this._monitor.get_volumes().forEach(Lang.bind(this, function(volume) {
this._VolumeAdded(volume);
}));
this._addedMountId = this._monitor.connect('mount-added', Lang.bind(this, function(monitor, mount) {
log('MOUNT CONNECTED',mount.get_name())
log(mount.get_name())
var volume = mount.get_volume()
log(volume.get_name(),volume.get_uuid())
//log(mount.get_name())
//this._showDrive(drive);
}));*/
//log(Gio.UnixMountPoint.get_device_path('/var/cache/backup'))
return;
},
_DriveAdded: function(drive) {
var Dident = drive.enumerate_identifiers();
var u_dev = drive.get_identifier('unix-device');
var d_name = drive.get_name();
log('DRIVE unix-device',u_dev,d_name)
this._drives[d_name] = new Object()
this._drives[d_name]['drive'] = drive;
this._drives[d_name]['device'] = drive.get_identifier('unix-device');
//this._drives[d_name]['uuid'] = drive.get_identifier('uuid');
this._drives[d_name]['volumes'] = new Object()
if (drive.has_volumes()) {
log(drive.get_volumes());
var VList = drive.get_volumes();
VList.forEach(Lang.bind(this, function(volume){
var v_name = volume.get_name();
log(v_name)
this._drives[d_name]['volumes'][v_name] = this._addVolume(volume);
}));
//VList.free()
};
log('VOLUMES',JSON.stringify(this._drives))
this.drvsubmenu = new PopupMenu.PopupSubMenuMenuItem(drive.get_name(), true);
this.drvsubmenu.icon.icon_name = 'drive-harddisk-usb-symbolic';
this.menu.addMenuItem(this.drvsubmenu);
//log('Drive added',drive.get_name())
//this.drvsubmenu = new PopupMenu.PopupSubMenuMenuItem(_(drive.get_name()), true);
//this.drvsubmenu.icon.icon_name = 'drive-harddisk-usb-symbolic';
//this.drvsubmenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
},
_addVolume: function(volume) {
volume.enumerate_identifiers();
var vol = new Object()
log(volume.get_identifier('uuid'))
log(volume.get_identifier('unix-device'))
log(volume.get_identifier('class'))
log(volume.get_identifier('label'))
vol['volume'] = volume;
vol['uuid'] = volume.get_identifier('uuid');
vol['u_device'] = volume.get_identifier('unix-device');
vol['vclass'] = volume.get_identifier('class');
vol['label'] = volume.get_identifier('label');
return vol
},
_DriveRemoved: function(drive) {
var me = this.menu._getMenuItems();
this._removeItemByLabel(this.menu, drive.get_name());
//this.drvsubmenu.destroy();
},
_VolumeRemoved: function(volume) {
//Volume is removed, after it's mounted from system(d)
log('VOLUME REMOVED',volume.get_name())
},
_VolumeAdded: function(volume) {
//Volume is added, after it's unmounted from system(d)
log('VOLUME CONNECTED',volume.get_name())
log('ID',this._addedVolumeId)
if (volume.get_drive() == null)
return
var drive = volume.get_drive()
if ( !volume.can_mount() || !drive.is_removable())
return
log(volume.get_mount())
log(volume.enumerate_identifiers())
log(volume.get_identifier('uuid'))
log(volume.get_identifier('unix-device'))
log(volume.get_identifier('class'))
log(volume.get_identifier('label'))
var dr = volume.get_drive()
log(dr.get_name())
log(dr.enumerate_identifiers())
log(dr.get_identifier('unix-device'))
var me
try {
me = this.drvsubmenu.menu._getMenuItems();
} catch(e) {
this._DriveAdded(drive)
}
me = this.drvsubmenu.menu._getMenuItems();
var menuItem = new VolMenuItem(volume, false);
var VF = Gio.File.new_for_path('/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules');
if (VF.query_exists(null)) {
log('UDEV exists','/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules')
menuItem.setToggleState(true);
} else {
log('UDEV not exists','/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules')
menuItem.setToggleState(false);
}
//this._removeItemByLabel(this.drvsubmenu.menu, volume.get_name());
this.drvsubmenu.menu.addMenuItem(menuItem);
var connID = menuItem.connect('toggled', Lang.bind(this, function() {
log('ACTIVE?',menuItem.state,menuItem.label.text)
if (menuItem.state) {
reg = 'register'
} else {
reg = 'unregister'
}
GLib.spawn_command_line_async(
this._getCommand('mkbackup-'+reg+'@'+volume.get_identifier('unix-device')+'.service', 'start', 'system'));
}));
log(connID,volume.get_name())
this.menu.addMenuItem(this.drvsubmenu,1);
},
_removeItemByLabel: function(menu, label) {
log('LAB',label)
var children = menu._getMenuItems();
for (var i = 0; i < children.length; i++) {
var item = children[i];
log('REM',item.label.text,label)
if (item.label.text == label)
log('DESTROY',item.label.text)
//item.destroy();
}
},
_addMount: function(mount) {
var item = new MountMenuItem(mount);
this._mounts.unshift(item);
this.menu.addMenuItem(item, 0);
},
_removeMount: function(mount) {
for (var i = 0; i < this._mounts.length; i++) {
var item = this._mounts[i];
if (item.mount == mount) {
item.destroy();
this._mounts.splice(i, 1);
return;
}
}
log ('Removing a mount that was never added to the menu');
},
_run_command: function(COMMAND) {
var output = "";
try {
output = GLib.spawn_command_line_sync(COMMAND, null, null, null, null);
} catch(e) {
throw e;
}
return output[1].toString().replace(/\n$/, "") + "";
},
_showVolume: function(volume) {
var drive = volume.get_drive();
var mount = volume.get_mount();
if (drive != null && drive.is_removable()){
log('SVDRIVE',drive.get_name(),drive.is_removable())
log('SVVOL',volume.get_name(),volume.get_uuid(),drive.is_removable());
}
},
_showDrive: function(drive) {
//extMediaName = 'external backup-drive';
log('SDDRIVE',drive.get_name(),drive.get_volumes(),drive.has_media(),drive.is_removable(),drive.has_volumes(),drive.enumerate_identifiers());
if (drive.is_removable() && drive.has_volumes()) {
//extMediaName = drive.get_name();
log('SDDREMOV')
drive.get_volumes().forEach(Lang.bind(this, function(volume) {
if (volume.can_mount()){
log('SDVOL',volume.get_name());
if ( volume.get_mount() != null ) {
var mount = volume.get_mount();
log('SDMR',mount.get_root());
}
}
}));
} else {
log(drive.is_removable(),drive.has_volumes(),drive.get_volumes());
}
},
_checkMount: function(mount) {
log('CHECK MOUNT '+mount.can_unmount()+' '+mount.can_eject());
log('VOLUME '+mount.get_volume());
return(mount.can_unmount() || mount.can_eject())
},
_getCommand: function(service, action, type) {
var command = "systemctl"
command += " " + action
command += " " + service
command += " --" + type
if (type == "system" && (action != 'is-active' && action != 'is-enabled'))
command = "pkexec --user root " + command
return 'sh -c "' + command + '; exit;"'
},
_refresh_panel : function() {
var active = false;
var volumes = []
var mounted = false;
volumes.push(this.watchvolume)
// TODO: find all Volumes on the drive, holding the backup and add this
// volumes to the list
volumes.push('home-jakob-Videos-extern.mount')
volumes.push('home-media.mount')
this.aout = GLib.spawn_command_line_sync(
this._getCommand(this.services.join(' '), 'is-active', 'system'))[1].toString().split('\n');
//log(this.aout);
active = this.aout.indexOf('active') >= 0
var vout = GLib.spawn_command_line_sync(
this._getCommand(volumes.join(' '), 'is-active', 'system'))[1].toString().split('\n');
mounted = vout.indexOf('active') >= 0
this.bkpsubmenu.icon.style = (active ? "color: #ff0000;" : "color: revert;");
MainIcon.style = (mounted ? "color: #ff0000;" : "color: revert;");
MainLabel.set_text(mounted ? _("mounted") : "");
if (this.menu.isOpen) {
//Menu is open
var me = this.menu._getMenuItems();
me.forEach(Lang.bind(this, function(item) {
//log(item.label.text)
if ( item.label.text == extMediaName ) {
item.setToggleState(this._check_service('mkbackup@BKP.target','active'));
}
}));
if (this.bkpsubmenu.menu.isOpen) {
//Submenu Backu-intervals open
this._refresh();
}
}
if (this._refreshID != 0)
Mainloop.source_remove(this._refreshID);
this._refreshID = Mainloop.timeout_add_seconds(refreshTime, Lang.bind(this, this._refresh_panel));
GLib.Source.set_name_by_id(this._refreshID, '[gnome-shell] this._refresh_panel');
return false;
},
_check_service: function(service,stat) {
var [_, aout, aerr, astat] = GLib.spawn_command_line_sync(
this._getCommand(service, 'is-'+stat, 'system'));
return (astat == 0);
},
_refresh: function() {
var me = this.bkpsubmenu.menu._getMenuItems();
//log(this.services.join(' '))
var eout = GLib.spawn_command_line_sync(
this._getCommand(this.services.join(' '), 'is-enabled', 'system'))[1].toString().split('\n');
//log(eout);
this._entries.forEach(Lang.bind(this, function(service,index,arr) {
if (! arr[index].enabled == (eout[index] == 'enabled'))
arr[index].changed = true
arr[index].enabled = (eout[index] == 'enabled' ? true : false)
if (! arr[index].active == (this.aout[index] == 'active'))
arr[index].changed = true
arr[index].active = (this.aout[index] == 'active' ? true : false)
}));
this._entries.forEach(Lang.bind(this, function(service,index,arr) {
var serviceItem
me.forEach(Lang.bind(this, function(item) {
if ( item.label.text == service['name'] ) {
arr[index].found = true;
serviceItem = item;
me.splice(me.indexOf(item),1);
}
}));
if ( arr[index].changed ) {
if (arr[index].found) {
//log('update',service['name']);
if ( service.staticserv ) {
serviceItem.setToggleState(service.active);
} else {
serviceItem.setToggleState(service.enabled);
}
} else {
//log('new',service['name']);
if ( service.staticserv ) {
serviceItem = new PopupTargetItem(service['name'], service.active);
this.bkpsubmenu.menu.addMenuItem(serviceItem);
serviceItem.connect('toggled', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand(service['service'], (this._check_service(service.service, 'active') ? 'stop' : 'start'), service["type"]));
}));
} else {
serviceItem = new PopupServiceItem(service['name'], service.enabled);
this.bkpsubmenu.menu.addMenuItem(serviceItem);
serviceItem.connect('toggled', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand(service['service'], (this._check_service(service.service, 'enabled') ? 'disable' : 'enable'), service["type"]));
}));
serviceItem.actionButton.connect('clicked', Lang.bind(this, function() {
GLib.spawn_command_line_async(
this._getCommand(service['service'], (this._check_service(service.service, 'active') ? 'stop' : 'start'), service["type"]));
this.menu.close();
}));
}
}
if (serviceItem.actionButton.child) {
serviceItem.actionButton.child.icon_name = (service.active ? EnabledIcon : DisabledIcon);
serviceItem.actionButton.child.style = (service.active ? "color: #ff0000;" : "color: revert;");
};
if (serviceItem.transferButton) {
serviceItem.transferButton.style = (service.tr ? "text-decoration: revert;" : "text-decoration: line-through;");
};
//log('Changed',service.service)
}
//log('X',arr[index].service,arr[index].enabled,arr[index].active,arr[index].changed)
arr[index].changed = false;
}));
this.bkpsubmenu.menu._getMenuItems().forEach(Lang.bind(this, function(item) {
var mic = 0
if ( me.length > mic ) {
for (var i = mic; i < me.length; i++) {
if (item == me[i]) {
log('DESTROY',me[i].label.text);
item.destroy();
}
}
}
}));
return true;
},
_loadConfig: function() {
//log('LOAD CONFIG')
var intervals
this.services = []
var kf = new GLib.KeyFile()
var obj = new Object();
this._entries = [];
if(kf.load_from_file('/etc/mksnapshot.conf',GLib.KeyFileFlags.NONE)){
//intervals = kf.get_groups()[0];
this.bkpmnt = kf.get_value('DEFAULT','bkpmnt')
//log('BKP',this.bkppath)
kf.get_groups()[0].forEach(Lang.bind(this, function(interval) {
var obj = new Object();
var i = ""
if (interval === 'DEFAULT')
i = 'misc'
else
i = interval
obj.name = i +' backups';
obj.interval = interval;
obj.service = "mkbackup@"+i+".service";
this.services.push(obj.service)
obj.type = "system";
obj.enabled = false;
obj.active = false;
obj.changed = true;
obj.found = false;
obj.staticserv = false;
try {
obj.tr = (kf.get_value(interval,"transfer").toLowerCase() === "true");
} catch(err) {
obj.tr = (kf.get_value("DEFAULT","transfer").toLowerCase() === "true");
}
this._entries.push(obj)}));
}
}
});
var backupManager;
function init(extensionMeta) {
//Convenience.initTranslations();
var theme = imports.gi.Gtk.IconTheme.get_default();
theme.append_search_path(extensionMeta.path + "/icons");
}
function enable() {
backupManager = new BackupManager();
}
function disable() {
backupManager.destroy();
}

View file

@ -0,0 +1,178 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="caffeine-on-symbolic.svg"
height="16"
id="svg7384"
inkscape:version="0.48.3.1 r9886"
version="1.1"
width="16">
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Gnome Symbolic Icon Theme</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:bbox-paths="false"
bordercolor="#666666"
borderopacity="1"
inkscape:current-layer="layer11"
inkscape:cx="6.0760965"
inkscape:cy="6.333427"
gridtolerance="10"
inkscape:guide-bbox="true"
guidetolerance="10"
id="namedview88"
inkscape:object-nodes="false"
inkscape:object-paths="false"
objecttolerance="10"
pagecolor="#555753"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
showborder="false"
showgrid="true"
showguides="true"
inkscape:snap-bbox="true"
inkscape:snap-bbox-midpoints="false"
inkscape:snap-global="true"
inkscape:snap-grids="true"
inkscape:snap-nodes="false"
inkscape:snap-others="false"
inkscape:snap-to-guides="true"
inkscape:window-height="741"
inkscape:window-maximized="1"
inkscape:window-width="1366"
inkscape:window-x="0"
inkscape:window-y="-3"
inkscape:zoom="32">
<inkscape:grid
empspacing="2"
enabled="true"
id="grid4866"
originx="-42.000009px"
originy="412px"
snapvisiblegridlinesonly="true"
spacingx="1px"
spacingy="1px"
type="xygrid"
visible="true" />
<sodipodi:guide
orientation="0,1"
position="3.4692426,9.4354561"
id="guide4029" />
</sodipodi:namedview>
<title
id="title9167">Gnome Symbolic Icon Theme</title>
<defs
id="defs7386" />
<g
inkscape:groupmode="layer"
id="layer9"
inkscape:label="status"
style="display:inline"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer10"
inkscape:label="devices"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer11"
inkscape:label="apps"
style="display:inline"
transform="translate(-283.00021,-629)">
<path
inkscape:connector-curvature="0"
d="m 283.12476,634.39715 c 0.0901,2.39982 -2.5e-4,4.90096 0.90536,7.16242 0.56561,1.4515 1.83385,2.55431 3.30794,2.90589 1.73814,0.41114 3.64376,0.40412 5.29688,-0.34623 1.60312,-0.72608 2.57953,-2.39439 2.98402,-4.09315 0.48766,-1.97915 0.54999,-4.03862 0.59785,-6.06897 -0.18011,-1.15523 -1.25844,-1.84731 -2.20555,-2.3009 -2.30084,-1.01772 -4.91035,-1.09851 -7.34309,-0.60961 -1.29974,0.30095 -2.77781,0.86169 -3.36661,2.20531 -0.1485,0.35955 -0.20354,0.75631 -0.1768,1.14524 z m 11.6374,0.11136 c -0.0441,0.91126 -0.96505,1.35565 -1.68872,1.6268 -1.99848,0.66073 -4.17612,0.68978 -6.22022,0.24168 -0.87456,-0.24018 -1.9114,-0.56182 -2.32332,-1.47943 -0.29096,-0.86897 0.46051,-1.62212 1.18574,-1.91228 1.63288,-0.67936 3.44794,-0.70226 5.17963,-0.56565 1.19133,0.15082 2.47787,0.35598 3.42954,1.17357 0.24954,0.23294 0.4418,0.55854 0.43735,0.91531 z"
id="path24839-7-0-8"
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.15345359;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new;font-family:Sans;-inkscape-font-specification:Sans" />
<path
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:1.64499998;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
d="m 296.01831,634.29869 c -0.42246,0.1058 -0.81557,0.29843 -1.20581,0.48706 0.25,0.48958 0.5,0.97917 0.75,1.46875 0.52953,-0.26079 1.07925,-0.48948 1.65621,-0.62136 0.19598,0.29034 0.0613,0.65559 0.0965,0.98001 -0.0265,0.76535 -0.0891,1.53332 -0.2618,2.281 -0.19796,0.24661 -0.50311,0.0211 -0.71411,-0.0864 -0.30932,-0.0721 -0.53729,-0.31117 -0.83024,-0.41286 -0.12503,-0.008 -0.14438,0.15951 -0.217,0.23727 -0.23278,0.37411 -0.46557,0.74823 -0.69835,1.12235 0.81565,0.49866 1.72225,0.99381 2.70984,0.93005 0.59894,-0.0615 1.14074,-0.51865 1.25926,-1.11639 0.18671,-0.66571 0.27615,-1.35495 0.35899,-2.03989 0.0715,-0.82121 0.0983,-1.67218 -0.1491,-2.46792 -0.15932,-0.48309 -0.5391,-0.9535 -1.0785,-0.99879 -0.5706,-0.0824 -1.12444,0.12918 -1.67588,0.23714 z"
id="path6047"
inkscape:connector-curvature="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="on"
style="display:none">
<path
id="path4114"
style="fill:none;stroke:#ffffff;stroke-width:1.29999995;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
d="m 4.2625534,6.1750293 c 0,0 1.1491587,-0.6278903 1.1863897,-1.6606918 C 5.4861331,3.4826637 4.4603025,2.976995 4.4672485,2.0317235 4.4736485,1.1539862 5.6337624,0.28329388 5.6337624,0.28329388"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cscc" />
<use
x="0"
y="0"
xlink:href="#path4114"
id="use4118"
transform="translate(3.3258259,0)"
width="16"
height="16" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="off"
style="display:inline">
<path
inkscape:connector-curvature="0"
d="m 6.555174,3.6689621 c -1.97865,-0.004 -4.74917,0.59448 -4.36893,1.9299 0.27853,0.9782 2.7468,1.37112 4.2822,1.34057 2.39093,-0.0476 3.99564,-0.15186 4.49936,-1.16589 0.66845,-1.34563 -2.32411,-2.10075 -4.41263,-2.10458 z"
id="path6309"
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.70000005;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
sodipodi:nodetypes="sssss" />
</g>
<g
inkscape:groupmode="layer"
id="layer13"
inkscape:label="places"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer14"
inkscape:label="mimetypes"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer15"
inkscape:label="emblems"
style="display:inline"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="g71291"
inkscape:label="emotes"
style="display:inline"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="g4953"
inkscape:label="categories"
style="display:inline"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer12"
inkscape:label="actions"
style="display:inline"
transform="translate(-283.00021,-629)" />
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -0,0 +1,178 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="caffeine-on-symbolic.svg"
height="16"
id="svg7384"
inkscape:version="0.48.3.1 r9886"
version="1.1"
width="16">
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Gnome Symbolic Icon Theme</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:bbox-paths="false"
bordercolor="#666666"
borderopacity="1"
inkscape:current-layer="layer11"
inkscape:cx="6.0760965"
inkscape:cy="6.333427"
gridtolerance="10"
inkscape:guide-bbox="true"
guidetolerance="10"
id="namedview88"
inkscape:object-nodes="false"
inkscape:object-paths="false"
objecttolerance="10"
pagecolor="#555753"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
showborder="false"
showgrid="true"
showguides="true"
inkscape:snap-bbox="true"
inkscape:snap-bbox-midpoints="false"
inkscape:snap-global="true"
inkscape:snap-grids="true"
inkscape:snap-nodes="false"
inkscape:snap-others="false"
inkscape:snap-to-guides="true"
inkscape:window-height="741"
inkscape:window-maximized="1"
inkscape:window-width="1366"
inkscape:window-x="0"
inkscape:window-y="-3"
inkscape:zoom="32">
<inkscape:grid
empspacing="2"
enabled="true"
id="grid4866"
originx="-42.000009px"
originy="412px"
snapvisiblegridlinesonly="true"
spacingx="1px"
spacingy="1px"
type="xygrid"
visible="true" />
<sodipodi:guide
orientation="0,1"
position="3.4692426,9.4354561"
id="guide4029" />
</sodipodi:namedview>
<title
id="title9167">Gnome Symbolic Icon Theme</title>
<defs
id="defs7386" />
<g
inkscape:groupmode="layer"
id="layer9"
inkscape:label="status"
style="display:inline"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer10"
inkscape:label="devices"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer11"
inkscape:label="apps"
style="display:inline"
transform="translate(-283.00021,-629)">
<path
inkscape:connector-curvature="0"
d="m 283.12476,634.39715 c 0.0901,2.39982 -2.5e-4,4.90096 0.90536,7.16242 0.56561,1.4515 1.83385,2.55431 3.30794,2.90589 1.73814,0.41114 3.64376,0.40412 5.29688,-0.34623 1.60312,-0.72608 2.57953,-2.39439 2.98402,-4.09315 0.48766,-1.97915 0.54999,-4.03862 0.59785,-6.06897 -0.18011,-1.15523 -1.25844,-1.84731 -2.20555,-2.3009 -2.30084,-1.01772 -4.91035,-1.09851 -7.34309,-0.60961 -1.29974,0.30095 -2.77781,0.86169 -3.36661,2.20531 -0.1485,0.35955 -0.20354,0.75631 -0.1768,1.14524 z m 11.6374,0.11136 c -0.0441,0.91126 -0.96505,1.35565 -1.68872,1.6268 -1.99848,0.66073 -4.17612,0.68978 -6.22022,0.24168 -0.87456,-0.24018 -1.9114,-0.56182 -2.32332,-1.47943 -0.29096,-0.86897 0.46051,-1.62212 1.18574,-1.91228 1.63288,-0.67936 3.44794,-0.70226 5.17963,-0.56565 1.19133,0.15082 2.47787,0.35598 3.42954,1.17357 0.24954,0.23294 0.4418,0.55854 0.43735,0.91531 z"
id="path24839-7-0-8"
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.15345359;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new;font-family:Sans;-inkscape-font-specification:Sans" />
<path
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:1.64499998;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
d="m 296.01831,634.29869 c -0.42246,0.1058 -0.81557,0.29843 -1.20581,0.48706 0.25,0.48958 0.5,0.97917 0.75,1.46875 0.52953,-0.26079 1.07925,-0.48948 1.65621,-0.62136 0.19598,0.29034 0.0613,0.65559 0.0965,0.98001 -0.0265,0.76535 -0.0891,1.53332 -0.2618,2.281 -0.19796,0.24661 -0.50311,0.0211 -0.71411,-0.0864 -0.30932,-0.0721 -0.53729,-0.31117 -0.83024,-0.41286 -0.12503,-0.008 -0.14438,0.15951 -0.217,0.23727 -0.23278,0.37411 -0.46557,0.74823 -0.69835,1.12235 0.81565,0.49866 1.72225,0.99381 2.70984,0.93005 0.59894,-0.0615 1.14074,-0.51865 1.25926,-1.11639 0.18671,-0.66571 0.27615,-1.35495 0.35899,-2.03989 0.0715,-0.82121 0.0983,-1.67218 -0.1491,-2.46792 -0.15932,-0.48309 -0.5391,-0.9535 -1.0785,-0.99879 -0.5706,-0.0824 -1.12444,0.12918 -1.67588,0.23714 z"
id="path6047"
inkscape:connector-curvature="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="on"
style="display:inline">
<path
id="path4114"
style="fill:none;stroke:#ffffff;stroke-width:1.29999995;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
d="m 4.2625534,6.1750293 c 0,0 1.1491587,-0.6278903 1.1863897,-1.6606918 C 5.4861331,3.4826637 4.4603025,2.976995 4.4672485,2.0317235 4.4736485,1.1539862 5.6337624,0.28329388 5.6337624,0.28329388"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cscc" />
<use
x="0"
y="0"
xlink:href="#path4114"
id="use4118"
transform="translate(3.3258259,0)"
width="16"
height="16" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="off"
style="display:none">
<path
inkscape:connector-curvature="0"
d="m 6.555174,3.6689621 c -1.97865,-0.004 -4.74917,0.59448 -4.36893,1.9299 0.27853,0.9782 2.7468,1.37112 4.2822,1.34057 2.39093,-0.0476 3.99564,-0.15186 4.49936,-1.16589 0.66845,-1.34563 -2.32411,-2.10075 -4.41263,-2.10458 z"
id="path6309"
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.70000005;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
sodipodi:nodetypes="sssss" />
</g>
<g
inkscape:groupmode="layer"
id="layer13"
inkscape:label="places"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer14"
inkscape:label="mimetypes"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer15"
inkscape:label="emblems"
style="display:inline"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="g71291"
inkscape:label="emotes"
style="display:inline"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="g4953"
inkscape:label="categories"
style="display:inline"
transform="translate(-283.00021,-629)" />
<g
inkscape:groupmode="layer"
id="layer12"
inkscape:label="actions"
style="display:inline"
transform="translate(-283.00021,-629)" />
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -0,0 +1,16 @@
{
"_generated": "Generated by SweetTooth, do not edit",
"description": "Toggle systemd services on/off from a popup menu in the top gnome panel. Can be used to start services like apache2, mysql, postgres. It uses `pkexec' to run `sytemctl'. If you want to start services without entering a password you have to polkit policy file. An example policy file can be found in the github repository.",
"name": "Zeitmaschine",
"settings-schema": "org.gnome.shell.extensions.zeitmaschine",
"shell-version": [
"3.14",
"3.16",
"3.18",
"3.20",
"3.22"
],
"url": "https://github.com/xundeenergie/gnome-shell-extension-zeitmaschine",
"uuid": "zeitmaschine@xundeenergie.at",
"version": 11
}

View file

@ -0,0 +1,113 @@
var Lang = imports.lang;
var PopupMenu = imports.ui.popupMenu;
var St = imports.gi.St;
var Clutter = imports.gi.Clutter;
var Gio = imports.gi.Gio;
var Util = imports.misc.util;
var Gtk = imports.gi.Gtk;
var ExtensionSystem = imports.ui.extensionSystem;
var ExtensionUtils = imports.misc.extensionUtils;
var DisabledIcon = 'my-caffeine-off-symbolic';
//var DisabledIcon = 'gnome-spinner';
var PopupBKPItem = new Lang.Class({
Name: 'PopupBKPItem',
Extends: PopupMenu.PopupSwitchMenuItem,
_init: function(mount, active, params) {
this.parent(mount.get_name(), active, params);
log('YY',mount.get_name())
this.label = new St.Label({ text: mount.get_name()});
this.actor.add(this.label, { expand: true });
this.actor.label_actor = this.label;
this.mount = mount;
var ejectIcon = new St.Icon({ icon_name: 'media-eject-symbolic',
style_class: 'popup-menu-icon ' });
var ejectButton = new St.Button({ child: ejectIcon });
ejectButton.connect('clicked', Lang.bind(this, this._eject));
this.actor.add(ejectButton);
this._changedId = mount.connect('changed', Lang.bind(this, this._syncVisibility));
this._syncVisibility();
},
destroy: function() {
if (this._changedId) {
this.mount.disconnect(this._changedId);
this._changedId = 0;
}
this.parent();
},
_isInteresting: function() {
//return true
if (!this.mount.can_eject() && !this.mount.can_unmount())
return false;
if (this.mount.is_shadowed())
return false;
var volume = this.mount.get_volume();
if (volume == null) {
// probably a GDaemonMount, could be network or
// local, but we can't tell; assume it's local for now
return true;
}
return volume.get_identifier('class') != 'network';
},
_syncVisibility: function() {
this.actor.visible = this._isInteresting();
},
_eject: function() {
var mountOp = new ShellMountOperation.ShellMountOperation(this.mount);
if (this.mount.can_eject())
this.mount.eject_with_operation(Gio.MountUnmountFlags.NONE,
mountOp.mountOp,
null, // Gio.Cancellable
Lang.bind(this, this._ejectFinish));
else
this.mount.unmount_with_operation(Gio.MountUnmountFlags.NONE,
mountOp.mountOp,
null, // Gio.Cancellable
Lang.bind(this, this._unmountFinish));
},
_unmountFinish: function(mount, result) {
try {
mount.unmount_with_operation_finish(result);
} catch(e) {
this._reportFailure(e);
}
},
_ejectFinish: function(mount, result) {
try {
mount.eject_with_operation_finish(result);
} catch(e) {
this._reportFailure(e);
}
},
_reportFailure: function(exception) {
var msg = _("Ejecting drive '%s' failed:").format(this.mount.get_name());
Main.notifyError(msg, exception.message);
},
/*activate: function(event) {
var context = global.create_app_launch_context(event.get_time(), -1);
Gio.AppInfo.launch_default_for_uri(this.mount.get_root().get_uri(),
context);
this.parent(event);
}*/
});

View file

@ -0,0 +1,35 @@
var Lang = imports.lang;
var PopupMenu = imports.ui.popupMenu;
var St = imports.gi.St;
var Clutter = imports.gi.Clutter;
var Util = imports.misc.util;
var Gtk = imports.gi.Gtk;
var ExtensionSystem = imports.ui.extensionSystem;
var ExtensionUtils = imports.misc.extensionUtils;
var DriveMenuItem = new Lang.Class({
Name: 'DriveMenuItem',
Extends: PopupMenu.PopupBaseMenuItem,
_init: function(drive) {
this.parent();
this.label = new St.Label({ text: drive.get_name() });
this.actor.add(this.label, { expand: true });
this.actor.label_actor = this.label;
this.drive = drive;
var ejectIcon = new St.Icon({ icon_name: 'drive-harddisk-usb-symbolic',
style_class: 'popup-menu-icon ' });
//var ejectIcon = mount.get_icon();
var ejectButton = new St.Button({ child: ejectIcon });
// ejectButton.connect('clicked', Lang.bind(this, this._eject));
this.actor.add(ejectButton);
// this._changedId = mount.connect('changed', Lang.bind(this, this._syncVisibility));
// this._syncVisibility();
}
});

View file

@ -0,0 +1,42 @@
var Lang = imports.lang;
var PopupMenu = imports.ui.popupMenu;
var St = imports.gi.St;
var Clutter = imports.gi.Clutter;
var Util = imports.misc.util;
var Gtk = imports.gi.Gtk;
var ExtensionSystem = imports.ui.extensionSystem;
var ExtensionUtils = imports.misc.extensionUtils;
var DisabledIcon = 'my-caffeine-off-symbolic';
//var DisabledIcon = 'gnome-spinner';
var PopupServiceItem = new Lang.Class({
Name: 'PopupServiceItem',
Extends: PopupMenu.PopupMenuItem,
_init: function(text, active, params) {
this.parent(text, active, params);
this.actionButton = new St.Button({
x_align: 1,
reactive: true,
can_focus: true,
track_hover: true,
accessible_name: 'restart',
style_class: 'system-menu-action services-systemd-button-reload' });
var icon = new St.Icon({ icon_name: DisabledIcon })
this.actionButton.child = icon;
this.actor.add(this.actionButton, { expand: false, x_align: St.Align.END });
/*this.ejectButton = new St.Button({ x_align: 1,
reactive: true,
can_focus: true,
track_hover: true,
accessible_name: 'eject',
style_class: 'system-menu-action services-systemd-button-reload' });
this.ejectButton.child = new St.Icon({ icon_name: 'media-eject-symbolic' });
this.actor.add(this.ejectButton, { expand: false, x_align: St.Align.END });*/
}
});

View file

@ -0,0 +1,108 @@
var Lang = imports.lang;
var PopupMenu = imports.ui.popupMenu;
var St = imports.gi.St;
var Clutter = imports.gi.Clutter;
var Util = imports.misc.util;
var Gtk = imports.gi.Gtk;
var ExtensionSystem = imports.ui.extensionSystem;
var ExtensionUtils = imports.misc.extensionUtils;
var MountMenuItem = new Lang.Class({
Name: 'MountMenuItem',
Extends: PopupMenu.PopupBaseMenuItem,
_init: function(mount) {
this.parent();
this.label = new St.Label({ text: mount.get_name() });
this.actor.add(this.label, { expand: true });
this.actor.label_actor = this.label;
this.mount = mount;
var ejectIcon = new St.Icon({ icon_name: 'media-eject-symbolic',
style_class: 'popup-menu-icon ' });
//var ejectIcon = mount.get_icon();
var ejectButton = new St.Button({ child: ejectIcon });
ejectButton.connect('clicked', Lang.bind(this, this._eject));
this.actor.add(ejectButton);
this._changedId = mount.connect('changed', Lang.bind(this, this._syncVisibility));
this._syncVisibility();
},
destroy: function() {
if (this._changedId) {
this.mount.disconnect(this._changedId);
this._changedId = 0;
}
this.parent();
},
_isInteresting: function() {
if (!this.mount.can_eject() && !this.mount.can_unmount())
return false;
if (this.mount.is_shadowed())
return false;
var volume = this.mount.get_volume();
if (volume == null) {
// probably a GDaemonMount, could be network or
// local, but we can't tell; assume it's local for now
return true;
}
return volume.get_identifier('class') != 'network';
},
_syncVisibility: function() {
this.actor.visible = this._isInteresting();
},
_eject: function() {
var mountOp = new ShellMountOperation.ShellMountOperation(this.mount);
if (this.mount.can_eject())
this.mount.eject_with_operation(Gio.MountUnmountFlags.NONE,
mountOp.mountOp,
null, // Gio.Cancellable
Lang.bind(this, this._ejectFinish));
else
this.mount.unmount_with_operation(Gio.MountUnmountFlags.NONE,
mountOp.mountOp,
null, // Gio.Cancellable
Lang.bind(this, this._unmountFinish));
},
_unmountFinish: function(mount, result) {
try {
mount.unmount_with_operation_finish(result);
} catch(e) {
this._reportFailure(e);
}
},
_ejectFinish: function(mount, result) {
try {
mount.eject_with_operation_finish(result);
} catch(e) {
this._reportFailure(e);
}
},
_reportFailure: function(exception) {
var msg = _("Ejecting drive '%s' failed:").format(this.mount.get_name());
Main.notifyError(msg, exception.message);
},
activate: function(event) {
var context = global.create_app_launch_context(event.get_time(), -1);
Gio.AppInfo.launch_default_for_uri(this.mount.get_root().get_uri(),
context);
this.parent(event);
}
});

View file

@ -0,0 +1,55 @@
var Lang = imports.lang;
var PopupMenu = imports.ui.popupMenu;
var St = imports.gi.St;
var Clutter = imports.gi.Clutter;
var Util = imports.misc.util;
var Gtk = imports.gi.Gtk;
var ExtensionSystem = imports.ui.extensionSystem;
var ExtensionUtils = imports.misc.extensionUtils;
var DisabledIcon = 'my-caffeine-off-symbolic';
var description = "Beschreibung";
//var DisabledIcon = 'gnome-spinner';
var PopupServiceItem = new Lang.Class({
Name: 'PopupServiceItem',
Extends: PopupMenu.PopupSwitchMenuItem,
_init: function(text, active, params) {
this.parent(text, active, params);
/* this.descriptionLabel = new St.Button({
label: description,
reactive: false,
x_align: St.Align.START,
can_focus: false,
accessible_name: 'description'});
this.actor.add(this.descriptionLabel, {expand: true});
*/
this.actionButton = new St.Button({
x_align: 1,
reactive: true,
can_focus: true,
track_hover: true,
accessible_name: 'restart',
style_class: 'system-menu-action services-systemd-button-reload' });
var icon = new St.Icon({ icon_name: DisabledIcon })
this.actionButton.child = icon;
this.actor.add(this.actionButton, { expand: false, x_align: St.Align.END });
this.transferButton = new St.Button({
label: 'transfer',
x_align: 1,
reactive: true,
can_focus: true,
track_hover: true,
accessible_name: 'transfer',
style_class: 'system-menu-action services-systemd-button-transfer' });
//this.transferButton.child = new St.Icon({ icon_name: 'media-eject-symbolic' });
this.actor.add(this.transferButton, { expand: false, x_align: St.Align.END });
},
});

View file

@ -0,0 +1,27 @@
var Lang = imports.lang;
var PopupMenu = imports.ui.popupMenu;
var St = imports.gi.St;
var Clutter = imports.gi.Clutter;
var Util = imports.misc.util;
var Gtk = imports.gi.Gtk;
var ExtensionSystem = imports.ui.extensionSystem;
var ExtensionUtils = imports.misc.extensionUtils;
var PopupTargetItem = new Lang.Class({
Name: 'PopupServiceItem',
Extends: PopupMenu.PopupSwitchMenuItem,
_init: function(text, active, params) {
this.parent(text, active, params);
this.actionButton = new St.Button({ x_align: 1,
reactive: true,
can_focus: true,
track_hover: true,
accessible_name: 'restart',
style_class: 'system-menu-action services-systemd-button-reload' });
}});

View file

@ -0,0 +1,108 @@
var Lang = imports.lang;
var PopupMenu = imports.ui.popupMenu;
var St = imports.gi.St;
var Clutter = imports.gi.Clutter;
var Util = imports.misc.util;
var Gtk = imports.gi.Gtk;
var ExtensionSystem = imports.ui.extensionSystem;
var ExtensionUtils = imports.misc.extensionUtils;
var MountMenuItem = new Lang.Class({
Name: 'MountMenuItem',
Extends: PopupMenu.PopupBaseMenuItem,
_init: function(mount) {
this.parent();
this.label = new St.Label({ text: mount.get_name() });
this.actor.add(this.label, { expand: true });
this.actor.label_actor = this.label;
this.mount = mount;
var ejectIcon = new St.Icon({ icon_name: 'media-eject-symbolic',
style_class: 'popup-menu-icon ' });
//var ejectIcon = mount.get_icon();
var ejectButton = new St.Button({ child: ejectIcon });
ejectButton.connect('clicked', Lang.bind(this, this._eject));
this.actor.add(ejectButton);
this._changedId = mount.connect('changed', Lang.bind(this, this._syncVisibility));
this._syncVisibility();
},
destroy: function() {
if (this._changedId) {
this.mount.disconnect(this._changedId);
this._changedId = 0;
}
this.parent();
},
_isInteresting: function() {
if (!this.mount.can_eject() && !this.mount.can_unmount())
return false;
if (this.mount.is_shadowed())
return false;
var volume = this.mount.get_volume();
if (volume == null) {
// probably a GDaemonMount, could be network or
// local, but we can't tell; assume it's local for now
return true;
}
return volume.get_identifier('class') != 'network';
},
_syncVisibility: function() {
this.actor.visible = this._isInteresting();
},
_eject: function() {
var mountOp = new ShellMountOperation.ShellMountOperation(this.mount);
if (this.mount.can_eject())
this.mount.eject_with_operation(Gio.MountUnmountFlags.NONE,
mountOp.mountOp,
null, // Gio.Cancellable
Lang.bind(this, this._ejectFinish));
else
this.mount.unmount_with_operation(Gio.MountUnmountFlags.NONE,
mountOp.mountOp,
null, // Gio.Cancellable
Lang.bind(this, this._unmountFinish));
},
_unmountFinish: function(mount, result) {
try {
mount.unmount_with_operation_finish(result);
} catch(e) {
this._reportFailure(e);
}
},
_ejectFinish: function(mount, result) {
try {
mount.eject_with_operation_finish(result);
} catch(e) {
this._reportFailure(e);
}
},
_reportFailure: function(exception) {
var msg = _("Ejecting drive '%s' failed:").format(this.mount.get_name());
Main.notifyError(msg, exception.message);
},
activate: function(event) {
var context = global.create_app_launch_context(event.get_time(), -1);
Gio.AppInfo.launch_default_for_uri(this.mount.get_root().get_uri(),
context);
this.parent(event);
}
});

View file

@ -0,0 +1,338 @@
var GLib = imports.gi.GLib;
var Gio = imports.gi.Gio;
var Gtk = imports.gi.Gtk;
var GObject = imports.gi.GObject;
var Lang = imports.lang;
var ExtensionUtils = imports.misc.extensionUtils;
var Me = ExtensionUtils.getCurrentExtension();
var Convenience = Me.imports.convenience;
var ServicesSystemdSettings = new GObject.Class({
Name: 'Services-Systemd-Settings',
Extends: Gtk.Grid,
_init : function(params) {
// Gtk Grid init
this.parent(params);
this.set_orientation(Gtk.Orientation.VERTICAL);
this.margin = 20;
// Open settings
this._settings = Convenience.getSettings();
this._settings.connect('changed', Lang.bind(this, this._refresh));
this._changedPermitted = false;
// Label
var treeViewLabel = new Gtk.Label({ label: '<b>' + "Listed systemd Services:" + '</b>',
use_markup: true,
halign: Gtk.Align.START })
this.add(treeViewLabel);
// TreeView
this._store = new Gtk.ListStore();
this._store.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING]);
this._treeView = new Gtk.TreeView({ model: this._store,
hexpand: true, vexpand: true });
var selection = this._treeView.get_selection();
selection.set_mode(Gtk.SelectionMode.SINGLE);
selection.connect ('changed', Lang.bind (this, this._onSelectionChanged));
var appColumn = new Gtk.TreeViewColumn({ expand: true,
title: "Label" });
var nameRenderer = new Gtk.CellRendererText;
appColumn.pack_start(nameRenderer, true);
appColumn.add_attribute(nameRenderer, "text", 0);
this._treeView.append_column(appColumn);
var appColumn = new Gtk.TreeViewColumn({ expand: true,
title: "Service" });
var nameRenderer = new Gtk.CellRendererText;
appColumn.pack_start(nameRenderer, true);
appColumn.add_attribute(nameRenderer, "text", 1);
this._treeView.append_column(appColumn);
var appColumn = new Gtk.TreeViewColumn({ expand: true,
title: "Type" });
var nameRenderer = new Gtk.CellRendererText;
appColumn.pack_start(nameRenderer, true);
appColumn.add_attribute(nameRenderer, "text", 2);
this._treeView.append_column(appColumn);
this.add(this._treeView);
// Devare Toolbar
var toolbar = new Gtk.Toolbar();
toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);
toolbar.halign = 2;
this.add(toolbar);
var upButton = new Gtk.ToolButton({ stock_id: Gtk.STOCK_GO_UP });
upButton.connect('clicked', Lang.bind(this, this._up));
toolbar.add(upButton);
var downButton = new Gtk.ToolButton({ stock_id: Gtk.STOCK_GO_DOWN });
downButton.connect('clicked', Lang.bind(this, this._down));
toolbar.add(downButton);
var delButton = new Gtk.ToolButton({ stock_id: Gtk.STOCK_DELETE });
delButton.connect('clicked', Lang.bind(this, this._devare));
toolbar.add(delButton);
this._selDepButtons = [upButton, downButton, delButton]
// Add Grid
var grid = new Gtk.Grid();
//// Label
var labelName = new Gtk.Label({label: "Label: "});
labelName.halign = 2;
this._displayName = new Gtk.Entry({ hexpand: true,
margin_top: 5 });
this._displayName.set_placeholder_text("Name in menu");
var labelService = new Gtk.Label({label: "Service: "});
labelService.halign = 2;
this._availableSystemdServices = {
//'system': this._getSystemdServicesList("system"),
//'user': this._getSystemdServicesList("user"),
'system': this._getSystemdTargetsList("system"),
'user': this._getSystemdTargetsList("user"),
}
this._availableSystemdServices['all'] = this._availableSystemdServices['system'].concat(this._availableSystemdServices['user'])
//this._availableSystemdServices['all'] = this._availableSystemdServices['systemtargets'].concat(this._availableSystemdServices['all'])
//this._availableSystemdServices['all'] = this._availableSystemdServices['usertargets'].concat(this._availableSystemdServices['all'])
var sListStore = new Gtk.ListStore();
sListStore.set_column_types([GObject.TYPE_STRING, GObject.TYPE_INT]);
for (var i in this._availableSystemdServices['all'])
sListStore.set (sListStore.append(), [0], [this._availableSystemdServices['all'][i]]);
this._systemName = new Gtk.Entry()
this._systemName.set_placeholder_text("Systemd service name");
var compvarion = new Gtk.EntryCompvarion()
this._systemName.set_compvarion(compvarion)
compvarion.set_model(sListStore)
compvarion.set_text_column(0)
grid.attach(labelName, 1, 1, 1, 1);
grid.attach_next_to(this._displayName, labelName, 1, 1, 1);
grid.attach(labelService, 1, 2, 1, 1);
grid.attach_next_to(this._systemName,labelService, 1, 1, 1);
this.add(grid);
var toolbar = new Gtk.Toolbar();
toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);
toolbar.halign = 2;
this.add(toolbar);
var addButton = new Gtk.ToolButton({ stock_id: Gtk.STOCK_ADD,
label: "Add",
is_important: true });
addButton.connect('clicked', Lang.bind(this, this._add));
toolbar.add(addButton);
this._changedPermitted = true;
this._refresh();
this._onSelectionChanged();
},
_getSystemdTargetsList: function(type) {
var [_, out, err, stat] = GLib.spawn_command_line_sync('sh -c "systemctl --' + type + ' list-unit-files --type=target | tail -n +2 | head -n -2 | awk \'{print $1}\'"');
var allFiltered = out.toString().split("\n");
return allFiltered.sort(
function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase());
})
},
_getSystemdServicesList: function(type) {
var [_, out, err, stat] = GLib.spawn_command_line_sync('sh -c "systemctl --' + type + ' list-unit-files --type=service | tail -n +2 | head -n -2 | awk \'{print $1}\'"');
var allFiltered = out.toString().split("\n");
return allFiltered.sort(
function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase());
})
},
_getTypeOfService: function(service) {
var type = "undefined"
if (this._availableSystemdServices['systemtargets'].indexOf(service) != -1 && this._availableSystemdServices['system'].indexOf(service) != -1)
type = "system"
else if (this._availableSystemdServices['usertargets'].indexOf(service) != -1 && this._availableSystemdServices['user'].indexOf(service) != -1)
type = "user"
return type
},
_getIdFromIter: function(iter) {
var displayName = this._store.get_value(iter, 0);
var serviceName = this._store.get_value(iter, 1);
var type = this._store.get_value(iter, 2);
return JSON.stringify({"name": displayName, "service": serviceName, "type": type});
},
_add: function() {
var displayName = this._displayName.text
var serviceName = this._systemName.text
if (displayName.trim().length > 0 && serviceName.trim().length > 0 ) {
var type = this._getTypeOfService(serviceName)
if (type == "undefined") {
this._messageDialog = new Gtk.MessageDialog ({
title: "Warning",
modal: true,
buttons: Gtk.ButtonsType.OK,
message_type: Gtk.MessageType.WARNING,
text: "Service does not exist."
});
this._messageDialog.connect('response', Lang.bind(this, function() {
this._messageDialog.close();
}));
this._messageDialog.show();
} else {
var id = JSON.stringify({"name": displayName, "service": serviceName, "type": type})
var currentItems = this._settings.get_strv("zeitmaschine");
var index = currentItems.indexOf(id);
if (index < 0) {
this._changedPermitted = false;
currentItems.push(id);
this._settings.set_strv("zeitmaschine", currentItems);
this._store.set(this._store.append(), [0, 1, 2], [displayName, serviceName, type]);
this._changedPermitted = true;
}
this._displayName.text = ""
this._systemName.text = ""
}
} else {
this._messageDialog = new Gtk.MessageDialog ({
//parent: this.get_toplevel(),
title: "Warning",
modal: true,
buttons: Gtk.ButtonsType.OK,
message_type: Gtk.MessageType.WARNING,
text: "No label and/or service specified."
});
this._messageDialog.connect ('response', Lang.bind(this, function() {
this._messageDialog.close();
}));
this._messageDialog.show();
}
},
_up: function() {
var [any, model, iter] = this._treeView.get_selection().get_selected();
if (any) {
var index = this._settings.get_strv("zeitmaschine").indexOf(this._getIdFromIter(iter));
this._move(index, index - 1)
}
},
_down: function() {
var [any, model, iter] = this._treeView.get_selection().get_selected();
if (any) {
var index = this._settings.get_strv("zeitmaschine").indexOf(this._getIdFromIter(iter));
this._move(index, index + 1)
}
},
_move: function(oldIndex, newIndex) {
var currentItems = this._settings.get_strv("zeitmaschine");
if (oldIndex < 0 || oldIndex >= currentItems.length ||
newIndex < 0 || newIndex >= currentItems.length)
return;
currentItems.splice(newIndex, 0, currentItems.splice(oldIndex, 1)[0]);
this._settings.set_strv("zeitmaschine", currentItems);
this._treeView.get_selection().unselect_all();
this._treeView.get_selection().select_path(Gtk.TreePath.new_from_string(String(newIndex)));
},
_devare: function() {
var [any, model, iter] = this._treeView.get_selection().get_selected();
if (any) {
var currentItems = this._settings.get_strv("zeitmaschine");
var index = currentItems.indexOf(this._getIdFromIter(iter));
if (index < 0)
return;
currentItems.splice(index, 1);
this._settings.set_strv("zeitmaschine", currentItems);
this._store.remove(iter);
}
},
_onSelectionChanged: function() {
var [any, model, iter] = this._treeView.get_selection().get_selected();
if (any) {
this._selDepButtons.forEach(function(value) {
value.set_sensitive(true)
});
} else {
this._selDepButtons.forEach(function(value) {
value.set_sensitive(false)
});
}
},
_refresh: function() {
if (!this._changedPermitted)
return;
this._store.clear();
var currentItems = this._settings.get_strv("zeitmaschien");
var validItems = [ ];
for (var i = 0; i < currentItems.length; i++) {
var entry = JSON.parse(currentItems[i]);
// REMOVE NOT EXISTING ENTRIES
if (this._availableSystemdServices["all"].indexOf(entry["service"]) < 0)
continue;
// COMPABILITY
if(!("type" in entry))
entry["type"] = this._getTypeOfService(entry["service"])
validItems.push(JSON.stringify(entry));
var iter = this._store.append();
this._store.set(iter,
[0, 1, 2],
[entry["name"], entry["service"], entry["type"]]);
}
this._changedPermitted = false
this._settings.set_strv("zeitmaschine", validItems);
this._changedPermitted = true
}
});
function init() {
}
function buildPrefsWidget() {
var widget = new ServicesSystemdSettings();
widget.show_all();
return widget;
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="gnome-shell-extensions-zeitmaschine">
<schema path="/org/gnome/shell/extensions/services/" id="org.gnome.shell.extensions.zeitmaschine">
<key type="as" name="zeitmaschine">
<default>[ ]</default>
<summary>Systemd service list</summary>
<description>A list of serrvice which are shown</description>
</key>
</schema>
</schemalist>

View file

@ -0,0 +1,23 @@
.system-menu-action.services-systemd-button-transfer {
height: 18px;
padding: 0px;
border: 0px;
}
.system-menu-action.services-systemd-button-reload {
height: 18px;
width: 18px;
padding: 0px;
border: 0px;
}
.disk-active-icon {
icon-size: 1.09em;
padding: 0 5px;
color: #ff0000;
}
.system-status-icon-red {
icon-size: 1.09em;
padding: 0 5px;
color: #ff0000;
}

View file

@ -0,0 +1,107 @@
[Desktop Entry]
Version=1.0
Name=Notifications service for mkbackup
Name[ar]=تنبيهات
Name[ast]=Notificaciones
Name[bg]=Уведомления
Name[ca]=Notificacions
Name[cs]=Oznámení
Name[da]=Beskeder
Name[de]=Benachrichtigungen für mkbackup
Name[el]=Ειδοποιήσεις
Name[en_AU]=Notifications
Name[en_GB]=Notifications
Name[es]=Notificaciones
Name[eu]=Berri-emateak
Name[fi]=Ilmoitukset
Name[fr]=Notifications
Name[gl]=Notificacións
Name[he]=התראות
Name[hr]=Obavijesti
Name[hu]=Értesítések
Name[id]=Notifikasi
Name[is]=Tilkynningar
Name[it]=Notifiche
Name[ja]=
Name[kk]=Хабарламалар
Name[ko]=
Name[lt]=Pranešimai
Name[lv]=Paziņojumi
Name[ms]=Pemberitahuan
Name[nb]=Varsling
Name[nl]=Meldingen
Name[oc]=Notificacions
Name[pa]=ਿ
Name[pl]=Powiadomienia
Name[pt]=Notificações
Name[pt_BR]=Notificações
Name[ro]=Notificări
Name[ru]=Оповещения
Name[sk]=Oznámenia
Name[sl]=Obvestila
Name[sq]=Njoftime
Name[sr]=Обавештења
Name[sv]=Notifieringar
Name[th]=
Name[tr]=Bildiriler
Name[ug]=ئۇقتۇرۇشلار
Name[uk]=Сповіщення
Name[vi]=Thông báo
Name[zh_CN]=
Name[zh_TW]=
Comment=Customize how notifications appear on your screen
Comment[ar]=خصص كيف تظهر التنبيهات على الشاشة
Comment[ast]=Personaliza cómo apaecen les notificaciones na to pantalla
Comment[bg]=Настройване на изгледа на уведомленията на екрана
Comment[ca]=Personalitzeu com es mostren les notificacions
Comment[cs]=Upravte způsob, jakým se budou oznámení zobrazovat
Comment[da]=Tilpas hvordan beskeder fremkommer på din skærm
Comment[de]=Das Erscheinungsbild von Benachrichtigungen anpassen
Comment[el]=Προσαρμογή του τρόπου εμφάνισης των ειδοποιήσεων στην οθόνη
Comment[en_AU]=Customise how notifications appear on your screen
Comment[en_GB]=Customise how notifications appear on your screen
Comment[es]=Personalice cómo aparecen las notificaciones en pantalla
Comment[eu]=Pertsonalizatu berri-emateak zure pantailan nola agertuko diren
Comment[fi]=Mukauta näytöllesi ilmestyvien ilmoitusten toimintaa
Comment[fr]=Personnaliser la manière dont les notifications apparaissent sur votre écran
Comment[gl]=Personalice como se mostran as notificacións na súa pantalla
Comment[he]=התאם מראה התראות על המסך שלך
Comment[hr]=Prilagodite kako će se obavijesti prikazivati na vašem ekranu
Comment[hu]=A képernyőn megjelenő értesítések megjelenésének személyre szabása
Comment[id]=Sesuaikan bagaimana notifikasi tampak di layar anda
Comment[is]=Sérsníða hvernig tilkynningar birtast á skjánum þínum
Comment[it]=Personalizzazione del modo in cui le notifiche appaiono sullo schermo
Comment[ja]=
Comment[kk]=Хабарламалар көрсетілуін таңдаңыз
Comment[ko]=
Comment[lt]=Tinkinti kaip atrodys pranešimai jūsų ekrane
Comment[lv]=Pielāgojiet, kā paziņojumu parādās uz jūsu ekrāna
Comment[ms]=Suaikan bagaimana pemberitahuan muncul atas skrin anda
Comment[nb]=Tilpass visning av varsler på skjermen din
Comment[nl]=Weergave van meldingen aanpassen
Comment[oc]=Personalizar lo biais que las notificacions apareisson sus vòstre ecran
Comment[pa]= ਿ ਿ ਿ
Comment[pl]=Konfiguruje ustwienia powiadamiania
Comment[pt]=Personalizar o aspeto das notificações no ecrã
Comment[pt_BR]=Personalize como as notificações devem aparecer na sua tela
Comment[ro]=Personalizați cum apar pe ecran notificările
Comment[ru]=Настройка отображения оповещений на вашем экране
Comment[sk]=Prispôsobiť spôsob upozornenia na obrazovke
Comment[sl]=Prilagodite prikaz obvestil na zaslonu
Comment[sq]=Përshtasni mënyrën se si shfaqen njoftimet në ekranin tuaj
Comment[sr]=Прилагодите начин приказа обавештења на екрану
Comment[sv]=Anpassa hur notifieringar ska visas på din skärm
Comment[th]=
Comment[tr]=Bildirilerin görünümünü özelleştirin
Comment[ug]=ئۇقتۇرۇشلارنىڭ كۆرۈنۈش ئۇسۇلىنى ئۆزلەشتۈر
Comment[uk]=Налаштуйте показ сповіщень на Вашому екрані
Comment[vi]=Tùy chnh cách thông báo n trên màn hình
Comment[zh_CN]=
Comment[zh_TW]=
Exec=/usr/lib/mkbackup-btrfs/mkbackup-desktop-notifications.py
TryExec=/usr/lib/mkbackup-btrfs/mkbackup-desktop-notifications.py
Icon=xfce4-notifyd
Terminal=false
StartupNotify=false
Type=Application
Categories=GTK;Settings;DesktopSettings;

0
register Normal file
View file