Timeline view of some directories of the hard drive (Common Lisp)

Un article de Caverne des 1001 nuits.

[modifier] Purpose

Having a timeline view of several folders in the hard drive.

This is my first Common Lisp program, so don't be too harsh :-)

[modifier] Code


;=============================HEADER====================================
; champ.lisp "Timeline explorer" v 1.1
; Copyleft 1001nuits (http://www.1001nuits.org - August 2010
;=======================================================================
(defpackage "TIMELINE-EXPLORER"
            (:use "COMMON-LISP")
            (:nicknames "CHAMP")
            (:export "MAIN"))

(in-package timeline-explorer)

;=====================General constants =====================
(defparameter *symbols* (list 'get-file-info 'test-get-file-info
                      'remove-singletons 'test-remove-singletons) "General symbols of the file")
(defparameter *DIR-WC* "*/" "Wild card default for directories. Could be implementation specific")
(defparameter *DIR* "/")
(defparameter *FILE-WC* "*" "Wild card default for files.")
(defparameter *months* (vector "January" "February" "March" "April" "May" "June" 
                        "July" "August" "September" "October" "November" "December"))

;dictionary of fi-info instances
(defparameter *dict* nil)

(defun doc ()
    (dolist (elt *symbols*)
        (format t "Documentation for '~A': ~A~%" elt (documentation elt 'function))))

;=====================Get file info=====================
(defclass fi-info ()
    ((fi-univ :accessor univ
              :initarg :univ)
     (fi-name :accessor name
              :initarg :name)
     (fi-year :accessor year
              :initarg :year)
     (fi-month :accessor month
               :initarg :month) 
     (fi-day :accessor day
             :initarg :day)))

(defun get-file-info (f)
    "fi-finfo construct: gets file information and put them in a fi-info object"
    (let* ((n (with-open-file (s f) (file-write-date s)))
           (l (multiple-value-list (decode-universal-time n)))
           (day (fourth l))
           (month (fifth l))
           (year (sixth l)))
        (make-instance 'fi-info :univ n :name f :day day :month month :year year)))

(defmethod fi-more-recent? ((a fi-info) (b fi-info))
    "fi-finfo comparison operator"
    (if (> (univ a) (univ b)) t nil)) 

(defmethod format-link ((a fi-info))
    "fi-info link formatter"
    (let* ((fname (concatenate 'string (pathname-name (pathname (name a))) "." (pathname-type (pathname (name a)))))
           (fdir (subseq (name a) 0 (- (length (name a)) (length fname)))))
        (format nil "<a href=\"file://~A\" target=\"new\">~A</a> - <a href=\"file://~A\" target=\"new\">Folder</a>"
            (name a) fname fdir)))
            
(defmethod get-time ((a fi-info))
    "fi-finto time formatter"
    (let ((timeobj (multiple-value-list (decode-universal-time (univ a)))))
        (format nil "~2,'0d:~2,'0d:~2,'0d" (third timeobj) (second timeobj) (first timeobj))))

;===========================runfrom=======================
(defun runfrom (dir)
    "Get files in a directory and recurse on sub directories"
    (let ((files (directory (concatenate 'string dir *FILE-WC*))))
        (dolist (file files 'done-files)
            (push (get-file-info (namestring file)) *dict*)))
    (let ((subdirs (directory (concatenate 'string dir *DIR-WC*))))
        (dolist (subdir subdirs 'done-dirs)
            (runfrom (concatenate 'string (namestring subdir) *DIR*))))) ;recurse

;===========================formatting=======================
(defun html-format-dict (outputfile dict)
    "Prints a list of fi-info instances"
    (with-open-file (strea outputfile :direction :output
                                    :if-exists :supersede)
        (format strea "<html><head><title>Timeline Explorer</title></head>~%")
        (let ((tyear 0) (tmonth 0) (tday 0))
            (dolist (obj dict 'end)
                (let ( (tempyear (year obj))
                       (tempmonth (month obj))
                       (tempday (day obj))
                       (changeofyear nil)
                       (changeofmonth nil)
                       (changeofday nil))
                    (if (equal tyear 0)
                        (progn 
                            (setf tyear tempyear) ;init loop
                            (setf changeofyear t))
                        (if (not (equal tyear tempyear))
                            (progn (setf changeofyear t) (setf tyear tempyear))))
                    (if (equal tmonth 0)
                        (progn
                            (setf tmonth tempmonth) ;init loop
                            (setf changeofmonth t))
                        (if (not (equal tmonth tempmonth))
                            (progn (setf changeofmonth t) (setf tmonth tempmonth))))
                    (if (equal tday 0)
                        (progn
                            (setf tday tempday) ;init loop
                            (setf changeofday t))
                        (if (not (equal tday tempday))
                            (progn 
                                (setf changeofday t) (setf tday tempday))))
                    (if changeofyear (format strea "<hr><h1>Year ~d</h1>~%"
                            (year obj)))
                    (if changeofmonth (format strea "<hr><h2>~A ~d</h2>~%" 
                            (svref *months* (- (month obj) 1))(year obj)))
                    (if changeofday (format strea "<p><font color=\"blue\"><b><i>~d ~A ~d</i></b></font></p>~%" 
                            (day obj) (svref *months* (- (month obj) 1))(year obj)))
                    (format strea "<font size=\"-1\"><b>~A</b> - ~A<br></font>~%" 
                        (get-time obj)(format-link obj))
                    )))
        (format strea "</html>")))

;=====================main=====================
(defun champ-usage ()
    "Usage function"
    (format t "Timeline Explorer usage:~%  >(load \"champ.lisp\")(timeline-explorer:main \"output.html\" \"dir1/\" [...])~%")
    (format t "[...] can be other directories.~%Info: directories should end with '/' like '/home/user1/'")
    (abort))

(defun main(outputfilename &rest others)
    "Main entry point"
    (if (equal others nil) 
        (champ-usage)) 
    (if (not (equal (length *dict*) 0))
        (progn (setf *dict* nil)
            (format t "Info: memory has been cleaned up...~%")))
    (let ( (start (get-universal-time)) )
        (dolist (dirtemp others)
            (if (not (stringp dirtemp))
                (progn (format t "Invalid input parameter, string expected.~%") (champ-usage)))
            (runfrom dirtemp))
        (format t "~A records treated in ~A seconds~%" (length *dict*)(- (get-universal-time) start))
        (let ((temp (sort *dict* #'fi-more-recent?)))
            (format t "Sorting done. Writing output to file...~%")
            (html-format-dict outputfilename temp)
            (format t "Output written~%Total time: ~A seconds~%" (- (get-universal-time) start))))
    t)

[modifier] Comments

Tested on CLISP. This is important because wild cards for directories are implementation specific.

Usage:

[1]> (load "champ.lisp")
;; Loading file champ.lisp ...
;; Loaded file champ.lisp
T
[2]> (timeline-explorer:main "/home/foo/temp/output.html" "/home/foo/Documents" "/home/bar/Things")
7637 records treated in 4 seconds
Sorting done. Writing output to file...
Output written
Total time: 8 seconds
T
[3]>

The program generates a very basic html page that you can bookmark in order to get it when you refresh it.