# Copyright (C) 2004 Nicolas Delon # All Rights Reserved # # This file is part of the Prelude program. # # 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, 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; see the file COPYING. If not, write to # the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. import sys import os from os.path import abspath import string import time import getopt import threading import Queue from stat import * from copy import copy import DB import Conf, FileSummary, Report def _fix_file_path(filename): """ Since prelude-fic (the shell script generated by install.py) has changed directory in order to call prelude-fic.py, we must fix relative filename path with $OLDPWD, so that filename points to the correct file """ if filename[0] == "/": return filename try: oldpwd = os.environ["OLDPWD"] if not oldpwd: oldpwd = os.environ["PWD"] except KeyError: oldpwd = os.environ["PWD"] return "%s/%s" % (oldpwd, filename) class _CurrentSnapshotDB: def __init__(self, filename): self.db = DB.DBM(filename, DB.MODE_READ) self.files = self.db.getFiles() def __iter__(self): return self def next(self): if len(self.files) == 0: raise StopIteration file = self.files.pop(0) return self.db[file] class _CurrentSnapshotFS: def __init__(self): conf = Conf.Conf() self.files = conf.getFiles() def __iter__(self): return self def next(self): if len(self.files) == 0: raise StopIteration() file = self.files.pop(0) return file.getSummary() class Error(Exception): pass class WrongCommandError(Exception): def __init__(self, command): self._command = command def __str__(self): return """prelude-fic: invalid command -- %s Try 'prelude-fic --help' for more information.""" % self._command class WrongOptionError(Exception): def __init__(self, command, option): self._command = command self._option = option def __str__(self): return """prelude-fic: invalid option for %s -- %s Try 'prelude-fic --help' for more information.""" % (self._command, self._option) _DEFAULT_DBFILE = "../../etc/prelude-fic/prelude-fic.db" _COMMAND_CREATE = 1 _COMMAND_CHECK = 2 _COMMAND_CHECK_UPDATE = 3 _COMMAND_CHECK_CREATE = 4 _COMMAND_PRINT = 5 _USAGE = """ prelude-fic COMMAND [OPTIONS] Commands: --create [dbfile] create a new filesystem snapshot if no dbfile is given the default database file ${install_prefix}/etc/prelude-fic/prelude-fic.db will be used --check [-o/--original dbfile1] [-c/--current dbfile2] [--report reporter1] [--report reporter2] perform a check between two filesystem snapshots if no --original is given the default database file will be used if no --current is given, check will be performed directly against the filesystem different reporter can be used at the same time; stdout, prelude and none (no reporting) are available as report backend, if no backend at all is given, stdout will be used --check-update [dbfile] [--report ...] check the filesystem against the dbfile and update this dbfile in the same time --check-create [-o/--original dbfile1] [-n/--new dbfile2] [--report ...] check the filesystem against dbfile1 and create a new snapshot dbfile2 in the same time --print [dbfile] print the filesystem snapshot stored in dbfile --version print the version number of prelude-fic --help print this help """ _VERSION = "prelude-fic 0.9-dev" class FIC: def __init__(self): self._loadCmdLine() def _handleVersion(self, options): print _VERSION sys.exit(0) def _handleHelp(self, options): print _USAGE.strip() sys.exit(0) def _handleCreate(self, options): self._command = _COMMAND_CREATE self._newSnapshot = DB.DBM(options and _fix_file_path(options[0]) or _DEFAULT_DBFILE, DB.MODE_TRUNC) self._currentSnapshot = _CurrentSnapshotFS() def _handleCheck(self, options): self._command = _COMMAND_CHECK self._reporters = [ ] self._originalSnapshot = None self._currentSnapshot = None opts, args = getopt.gnu_getopt(options, "o:c:r:", [ "original=", "current=", "report=" ]) if args: raise WrongOptionError("--check", args[0]) for opt, arg in opts: if opt in ("-o", "--original"): self._originalSnapshot = DB.DBM(_fix_file_path(arg), DB.MODE_READ) elif opt in ("-c", "--current"): self._currentSnapshot = _CurrentSnapshotDB(_fix_file_path(arg)) elif opt in ("-r", "--report"): self._reporters.append(Report.Report(arg)) else: raise WrongOptionError("--check", opt) if not self._originalSnapshot: self._originalSnapshot = DB.DBM(_DEFAULT_DBFILE, DB.MODE_READ) if not self._currentSnapshot: self._currentSnapshot = _CurrentSnapshotFS() if not self._reporters: self._reporters.append(Report.Report()) def _handleCheckUpdate(self, options): self._command = _COMMAND_CHECK_UPDATE self._reporters = [ ] self._originalSnapshot = None self._currentSnapshot = _CurrentSnapshotFS() opts, args = getopt.gnu_getopt(options, "r:", "report=") for opt, arg in opts: if opt in ("-r", "--report"): self._reporters.append(Report.Report(arg)) else: raise WrongOptionError("--check-update", opt) if len(args) == 0: self._originalSnapshot = DB.DBM(_DEFAULT_DBFILE, DB.MODE_RW) elif len(args) == 1: self._originalSnapshot = DB.DBM(_fix_file_path(args[0]), DB.MODE_RW) else: raise WrongOptionError("--check-update", args[1]) if not self._reporters: self._reporters.append(Report.Report()) def _handleCheckCreate(self, options): self._command = _COMMAND_CHECK_CREATE self._reporters = [ ] original = new = abspath(_DEFAULT_DBFILE) opts, args = getopt.gnu_getopt(options, "o:n:r:", [ "original=", "new=", "report=" ]) for opt, arg in opts: if opt in ("-o", "--original"): original = arg elif opt in ("-n", "--new"): new = arg elif opt in ("-r", "--reporter"): self._reporters.append(Report.Report(arg)) else: raise WrongOptionError("--check-create", opt) if original == new: raise Error("original and new database files cannot be the same file") self._originalSnapshot = DB.DBM(original, DB.MODE_READ) self._newSnapshot = DB.DBM(_fix_file_path(new), DB.MODE_TRUNC) self._currentSnapshot = _CurrentSnapshotFS() if not self._reporters: self._reporters.append(Report.Report()) def _handlePrint(self, options): self._command = _COMMAND_PRINT self._originalSnapshot = DB.DBM(options and _fix_file_path(options[0]) or _DEFAULT_DBFILE, DB.MODE_READ) # FIXME: check for the right number of arguments def _loadCmdLine(self): """ pyfic command [options] pyfic --create [file.db] pyfic --check [--original file1.db] [--current file2.db] [--report reporter1] [--report reporter2] pyfic --check-update [file1.db] [--report reporter1] [--report reporter2] pyfic --check-create [--original file1.db] [--new file2.db] [--report reporter1] [--report reporter2] pyfic --print pyfic --help pyfic --version """ commands = { "--create": self._handleCreate, "--check": self._handleCheck, "--check-update": self._handleCheckUpdate, "--check-create": self._handleCheckCreate, "--print": self._handlePrint, "--version": self._handleVersion, "--help": self._handleHelp, } try: command, options = sys.argv[1], sys.argv[2:] except IndexError: self._handleHelp(()) sys.exit(1) try: commands[command](options) except KeyError: raise WrongCommandError(command) def _reportFileModified(self, changed_properties, original_file, current_file): for reporter in self._reporters: reporter.fileModified(changed_properties, original_file, current_file) def _reportFileAdded(self, file): for reporter in self._reporters: reporter.fileAdded(file) def _reportFileRemoved(self, file): for reporter in self._reporters: reporter.fileRemoved(file) def _create(self): for file in self._currentSnapshot: self._newSnapshot[file.filename] = file def _compareFiles(self, original_file, current_file): changed_properties = 0 if current_file.properties & FileSummary.INODE and original_file.inode != current_file.inode: changed_properties |= FileSummary.INODE if current_file.properties & FileSummary.MODE and original_file.mode != current_file.mode: changed_properties |= FileSummary.MODE else: if S_ISLNK(original_file.mode): if (current_file.properties & FileSummary.LINK and current_file.link_target_filename != original_file.link_target_filename): changed_properties |= FileSummary.LINK if current_file.properties & FileSummary.LINKS and original_file.links != current_file.links: changed_properties |= FileSummary.LINKS if current_file.properties & FileSummary.UID and original_file.uid != current_file.uid: changed_properties |= FileSummary.UID if current_file.properties & FileSummary.GID and original_file.gid != current_file.gid: changed_properties |= FileSummary.GID if current_file.properties & FileSummary.SIZE and original_file.size != current_file.size: changed_properties |= FileSummary.SIZE if current_file.properties & FileSummary.CTIME and original_file.ctime != current_file.ctime: changed_properties |= FileSummary.CTIME if current_file.properties & FileSummary.ATIME and original_file.atime != current_file.atime: changed_properties |= FileSummary.ATIME if current_file.properties & FileSummary.MTIME and original_file.mtime != current_file.mtime: changed_properties |= FileSummary.MTIME if current_file.properties & FileSummary.MD5 and original_file.md5_digest != current_file.md5_digest: changed_properties |= FileSummary.MD5 if current_file.properties & FileSummary.SHA and original_file.sha_digest != current_file.sha_digest: changed_properties |= FileSummary.SHA if changed_properties == 0: return 0 self._reportFileModified(changed_properties, original_file, current_file) if self._command == _COMMAND_CHECK_UPDATE: self._originalSnapshot[current_file.filename] = current_file return 1 def _fileAdded(self, file): self._reportFileAdded(file) if self._command == _COMMAND_CHECK_UPDATE: self._originalSnapshot[file.filename] = file def _fileRemoved(self, file): self._reportFileRemoved(file) if self._command == _COMMAND_CHECK_UPDATE: del self._originalSnapshot[file.filename] def _check(self): for current_file in self._currentSnapshot: if self._command == _COMMAND_CHECK_CREATE: self._newSnapshot[current_file.filename] = current_file try: original_file = self._originalSnapshot[current_file.filename] except KeyError: self._fileAdded(current_file) continue self._compareFiles(original_file, current_file) for filename in self._originalSnapshot.getRemainings(): self._fileRemoved(self._originalSnapshot[filename]) def _print(self): self._originalSnapshot.print_() def run(self): if self._command == _COMMAND_PRINT: self._print() elif self._command in (_COMMAND_CHECK, _COMMAND_CHECK_UPDATE, _COMMAND_CHECK_CREATE): self._check() else: self._create()