Module: disk-usage Synopsis: Report directory contents in human readable and log formats Author: Carl Gay Copyright: Original Code is Copyright (c) 1995-2004 Functional Objects, Inc. All rights reserved. License: Functional Objects Library Public License Version 1.0 Dual-license: GNU Lesser General Public License Warranty: Distributed WITHOUT WARRANTY OF ANY KIND // The code in this file is responsible for producing reports of disk usage // in human readable and log formats, and for parsing the logs back into // a usable form. The format is very simple, but it could probably be made // much simpler by using DOOD. define constant = one-of(#"log", // A format suitable for parsing by test-report #"detailed", // A line for each file or directory #"brief", // A line for each directory #"none"); // No report, just return a total define thread variable *report* :: = #"brief"; define constant $file-name-column-width :: = 45; define constant $log-file-header :: = "--------Disk Usage Log Report--------"; // report-disk-usage // define method report-disk-usage (dir-info :: , #key report :: = *report*, sort-by :: one-of(#"name", #"size", #"none")) => (total-bytes :: ) dynamic-bind(*report* = report) if (*report* == #"log") format-out("\n%s\nROOT %s\n", $log-file-header, dir-info.file-name); display-disk-usage-log(dir-info); elseif (report ~== #"none") let infos = if (sort-by == #"none") flatten-file-info(dir-info) else sort!(flatten-file-info(dir-info), test: select (sort-by) #"name" => info-name-less?; #"size" => info-size-greater?; end) end if; do(method (info) if (instance?(info, ) | report == #"detailed") print-file-info(info); end if; end method, infos); end if; dir-info.file-size end dynamic-bind end method report-disk-usage; define method display-disk-usage-log (info :: ) => () format-out("\nDIRECTORY %d %s\n", info.file-size, info.file-name); for (subinfo in info.directory-data) display-disk-usage-log(subinfo); end for; format-out("END\n"); end method display-disk-usage-log; define method display-disk-usage-log (info :: ) => () format-out("FILE %d %s\n", info.file-size, as(, info.file-name)); end method display-disk-usage-log; define function flatten-file-info (info :: ) => (infos :: ) local method flatten (info :: , results :: ) => (results :: ) add!(results, info); for (item in info.directory-data) if (instance?(item, )) flatten(item, results); else add!(results, item); end if; end for; results end method; flatten(info, make()) end function flatten-file-info; define class () slot file-locator :: , required-init-keyword: #"locator"; slot %file-name :: false-or() = #f; constant slot file-size :: , required-init-keyword: #"size"; end class ; define class () constant slot directory-data :: , required-init-keyword: #"data"; end class ; define function file-name (info :: ) => (name :: ) info.%file-name | (info.%file-name := as(, info.file-locator)) end function file-name; define method print-file-info (info :: ) => () format-out("%s %s (%s)\n", pad(info.file-name, $file-name-column-width, #t), pad(integer-to-string(info.file-size), 9, #f), kbytes(info.file-size)); end method print-file-info; define method do-file-info (f :: , info :: ) => () f(info) end method do-file-info; define method do-file-info (f :: , info :: ) => () f(info); do(curry(do-file-info, f), info.directory-data); end method do-file-info; // The following two are sort test functions define method info-name-less? (info1 :: , info2 :: ) => (less? :: ) string-less?(info1.file-name, info2.file-name); end method info-name-less?; define method info-size-greater? (info1 :: , info2 :: ) => (less? :: ) info1.file-size > info2.file-size; end method info-size-greater?; // Given a directory, return a object to use for comparisons. // All stored filenames (except the top-level dir) are relative to the // initial directory. // define function dir-infoify (root :: ) => (info :: , total-size :: ) local method do-one-directory (directory) let data = make(); let total-size :: = 0; local method do-one-file (dir, name, type) let dir = as(, dir); if (type == #"directory") let subdir = subdirectory-locator(dir, name); if (name ~= "." & name ~= "..") let (info, dsize) = do-one-directory(subdir); total-size := total-size + dsize; add!(data, info); end if; else let file = merge-locators(as(, name), dir); let fsize = file-property(file, #"size"); total-size := total-size + fsize; add!(data, make(, locator: relative-locator(file, root), size: fsize)); end if; end method do-one-file; do-directory(do-one-file, directory); values(make(, locator: relative-locator(directory, root), size: total-size, data: data), total-size) end method do-one-directory; let (info, total-size) = do-one-directory(root); info.file-locator := root; values(info, total-size) end function dir-infoify; /* Log file format looks like this: ROOT DIRECTORY (may be blank, if dir1 = root) FILE DIRECTORY FILE FILE END DIRECTORY FILE FILE END DIRECTORY The number of bytes for the file/dir is given before the pathname to make dealing with spaces in pathnames easier. Blank lines are ignored. */ // Top-level function for reading a disk-usage log file. // define function read-disk-usage-file (file-name :: ) => (info :: ) with-open-file (in = file-name, direction: #"input") find-line-starting-with(in, $log-file-header); let line1 = find-line-starting-with(in, "ROOT "); ignore(line1); let line2 = find-line-starting-with(in, "DIRECTORY "); let (bytes, locator) = parse-dir-line(line2); let contents = read-directory-from-log(in); make(, size: bytes, locator: locator, data: contents) end end function read-disk-usage-file; define function line-starts-with (line :: , s :: ) => (b :: ) block (return) let len = size(line); for (i from 0 below size(s)) if (i >= len | line[i] ~= s[i]) return(#f); end if; end for; #t end block end function line-starts-with; define method find-line-starting-with (in :: , s :: ) => (line :: ) block (return) while (#t) let line = read-line(in, on-end-of-stream: #f); if (~line) error("End of file reached without finding a line starting with %=.", s); elseif (line-starts-with(line, s)) return(line); end if; end while; end block; end method find-line-starting-with; // When this is called the file stream is positioned following the DIRECTORY line. // define function read-directory-from-log (in :: ) => (contents :: ) block (return) let contents = make(); while (#t) let line = read-line(in, on-end-of-stream: #f); if (~line) error("Reached end of stream without finding end of directory."); elseif (line-starts-with(line, "FILE")) add!(contents, parse-file-line(line)); elseif (line-starts-with(line, "DIRECTORY")) let (bytes, locator) = parse-dir-line(line); let subcontents = read-directory-from-log(in); add!(contents, make(, size: bytes, locator: locator, data: subcontents)); elseif (line-starts-with(line, "END")) return(contents); else #f // ignore unrecognized (e.g., blank) lines end if; end while; end block; end function read-directory-from-log; // Parse a line like "FILE 21345 " // define function parse-file-line (line :: ) => (info :: ) let (file-size, file-name) = parse-file-line-internal(line, "FILE "); make(, size: file-size, locator: as(, file-name)) end function parse-file-line; define function parse-file-line-internal (line :: , initial-substring :: ) => (size :: , name :: ) let (file-size, _end) = string-to-integer(line, start: size(initial-substring)); values(file-size, copy-sequence(line, start: _end + 1)) end function parse-file-line-internal; define function parse-dir-line (line :: ) => (size :: , locator :: ) let (size, dir-name) = parse-file-line-internal(line, "DIRECTORY "); values(size, as(, dir-name)) end function parse-dir-line; define function kbytes (bytes :: , #key sign? :: ) => (kbytes :: ) let sign = if (bytes < 0) "-" elseif (sign?) "+" else "" end; format-to-string("%s%dK", sign, ceiling/(abs(bytes), 1024)); end function kbytes;