# vim: set cindent expandtab ts=4 sw=4:
#
# Copyright (c) 1998-2005 Chi-Keung Ho. All rights reserved.
#
# This programe 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.
#
# Extmail - a high-performance webmail to maildir
# $Id$
package Ext::Storage::Fileman;
use strict;
# fileman specification
#
# XXX - fileman derive the Maildir++ protocol
#
# Contents of filesize
# filesize contains two or more lines terminated by newline characters.
#
# The first line contains a copy of the quota definition as used by the
# system's mail server. Each application that uses the maildir must know
# what it's quota is. Instead of configuring each application with the
# quota logic, and making sure that every application's quota definition
# for the same maildir is exactly the same, the quota specification used
# by the system mail server is saved as the first line of the maildirsize
# file. All other application that enforce the maildir quota simply read
# the first line of maildirsize.
#
# The quota definition is a list, separate by commas. Each member of the
# list consists of an integer followed by a letter, specifying the nature
# of the quota. Currently defined quota types are 'S' - total size of all
# messages, and 'C' - the maximum count of messages in the maildir.
#
# For example, 10000000S,1000C specifies a quota of 10,000,000 bytes or
# 1,000 messages, whichever comes first.
#
# All remaining lines all contain two integers separated by a single space.
# The first integer is interpreted as a byte count. The second integer is
# interpreted as a file count. A fileman writer can add up all byte
# counts and file counts from maildirsize and enforce a quota based either
# on number of messages or the total size of all the messages.
#
# Update 2005-07-28, experimantal OO design, now use global varibles to
# store configuration and some information instead of using object, for
# performance reason. OO object new/del a hundred thousand times will
# be much slower than direct reference :-(
use Fcntl ':flock';
use Exporter;
use vars qw(@ISA @EXPORT %CFG $SORTORDER);
@ISA = qw(Exporter);
@EXPORT = qw(
fget_dirlist op_rename fixslash
op_addfile op_getfile op_mkdir op_rmdir op_move
op_rmfile fget_quota fget_curquota freset_quota
fre_calculate fupdate_quota fis_overquota fscan_dir
get_files_list ext2mime fixpath fupdate_quota_s);
%CFG=();
use Ext::Utils qw(untaint); # import the untaint func
use Ext::RFC822 qw(str2time); # import str2time
use Ext::Storage::Maildir qw(valid_dirname); # import valid_dirname
# init - init a fileman, for read/write and quota maintains
#
# XXX $path is the base path, every I/O can't over collapse this
# limitaion, or it will become a big security hole.
sub init {
my ($path, $mode, $warnlv) = @_;
# XXX the magic to set default permission
umask(0077);
$CFG{path} = untaint ($path ? $path : "./fileman/"); # relative path
$CFG{mode} = $mode ? $mode : "O_RW";
$CFG{warnlv} = $warnlv ? $warnlv : "0.9"; # 90% default
$CFG{ctrlfile} = fixslash("$CFG{path}/filesize");
my $file = $CFG{path}.'/filesize';
if (! -d $CFG{path}) {
mkdir untaint ($CFG{path}), 0700; # raw mkdir call, so $CFG{path} must
# be a security path
}
chdir($CFG{path}) or die "Can't chdir to $CFG{path}, $!\n";
# re-calculate if not exist or bigger than 5120 bytes
if(! -e $file || (stat $file)[7] >= 5120) {
fre_calculate();
}
1;
}
# op_mkdir - create a directory
sub op_mkdir {
my $folder = fixslash(fixpath($_[0]));
return "$folder exists" if(-d "$CFG{path}/$folder");
mkdir untaint("./$folder"), 0700;
0;
}
# op_rmdir - delete a directory
sub op_rmdir {
my $folder = untaint(fixpath($_[0]));
my $flag = $_[1];
if($folder =~/^(filesize|\.\.)$/) {
# Ignore default folder
return 'Can\'t remove system file';
}
# realdir => the real full path directory name
# folder => the relative path name
my $realdir = fixslash("$CFG{path}/$folder");
if(-d $realdir) {
my @entries = ();
opendir DIR, $realdir or die "Can't opendir, $!\n";
@entries = grep { !/^\.$/ && !/^\..$/ } readdir(DIR);
closedir DIR;
for my $f (@entries) {
if(-d "$realdir/$f") {
# the $flag is to indicate that whether we
# are in recursive mode, if yes ignore all checks
op_rmdir("$folder/$f", 1);
next;
}
unlink untaint("$realdir/$f") or print "error: $!\n";
}
rmdir "$realdir" || return "op_rmdir() $folder: $!\n"; # this remove dir left
return 0;
}
0;
}
# op_move - move a file or directory to another directory
#
# from => file or directory
# to => destination directory
sub op_move {
my ($from, $to) = @_;
$from = fixpath($from);
$to = fixpath($to);
my $rfrom = fixslash("$CFG{path}/$from");
my $rto = fixslash("$CFG{path}/$to");
my $fromname;
if ($rfrom eq $CFG{ctrlfile} || $rto eq $CFG{ctrlfile}) {
return 'Can\'t move system control file';
}
# whether it's directory or file, get the basename
if (-r $rfrom) {
$from =~ s#/+$##;
$rto =~ s#/+$##; # XXX fix fromname and rto
$from =~ m#([^\/]+)$#;
$fromname = $1 || $from;
} else {
return "$from not exists";
}
return "$to or $from invalid" if ($to =~ m#^\.$# or $from =~ m#^\.\.$#);
return "Directory $to not exists" unless (-d $rto);
return 'Source equal Destination, Abort!' unless ($rfrom ne $rto);
if ($from =~ m#^/filesize$#) {
return 'Source or Destination name invalid';
}
rename(untaint($rfrom), untaint("$rto/$fromname")) ||
return "rename $from to $to error, $!";
return 0;
}
sub op_rename {
my ($from, $to) = @_;
$from = untaint($from);
$to = untaint($to);
my $rfrom = fixslash(fixpath("$CFG{path}/$from"));
my $rto = fixslash(fixpath("$CFG{path}/$to"));
$from = fixslash(fixpath($from));
$to = fixslash(fixpath($from));
if ($rfrom eq $CFG{ctrlfile} || $rto eq $CFG{ctrlfile}) {
return "Can't rename system control file";
}
if (!-r $rfrom) {
return "$from not exists";
} elsif (-r $rto) {
return "$to already exists";
}
rename($rfrom, $rto) || return "rename fail, $!\n";
return 0;
}
sub op_addfile {
my ($from, $dist, $op) = @_;
$from = untaint (fixpath($from));
$dist = untaint (fixslash("$CFG{path}/".fixpath($dist)));
my $size = (stat $from)[7]; # size contain CGI header ?
my $rv = fis_overquota($size, '1');
my $dist_size;
if ($rv eq 2) {
return "Storage over quota, abort!";
}
# ignore the filesize at the topdir, override it
# is forbidden and dangous! XXX FIXME
if ($dist eq $CFG{ctrlfile}) {
return "Can't override filesize, abort!";
}
if (-r $dist) {
# dist file exists, override!!
$dist_size = (stat $dist)[7];
}
open (IN, "< $from") or return "$from open fail, $!\n";
open (OUT, "> $dist") or return "$dist write fail, $!\n";
if ($op && $op eq 'strip_header') {
my $CRLF = "\015\012"; # \r\n # XXX
local $/ = $CRLF.$CRLF;
<IN>; # strip the header for some Ext::CGI upload file
}
my $pos_start = tell IN;
while (<IN>) {
print OUT $_;
}
my $pos_end = tell IN;
close IN;
close OUT;
$size = $pos_end - $pos_start;
if (defined $dist_size && $dist_size>=0) {
fupdate_quota($dist_size, -1);
}
fupdate_quota($size >0 ? $size : 0 , 1);
0;
}
sub op_getfile {
my $file = shift;
$file = "$CFG{path}/$file";
$file = fixpath(fixslash($file));
open (my $stream , " < $file") or die "$file open fail, $!\n";
return $stream;
}
sub op_rmfile {
my $file = untaint (fixpath($_[0])); # remove dangerous characters
$file = "$CFG{path}/$file";
$file = fixslash($file);
return "Can't remove $file" if ($file eq $CFG{ctrlfile});
my $size = (stat $file)[7];
unlink $file or return "$file remove fail, $!";
fupdate_quota("-$size", '-1');
0; # success
}
sub fget_dirlist {
my $dir = fixpath($_[0] || '/');
my $rv = ['/'];
push @$rv, @{_dirlist($dir)};
$rv;
}
# private function
sub _dirlist {
my $dir = fixpath($_[0]);
my @entries;
my $realdir = fixslash("$CFG{path}/$dir");
if (-d $realdir) {
opendir DIR, $realdir or die "Can't opendir, $!\n";
my @rv = sort grep { !/^\./ && -d "$realdir/$_" } readdir(DIR);
closedir DIR;
for my $d (@rv) {
push @entries, fixslash("$dir/$d/");
my $rv2 = _dirlist("$dir/$d/");
push @entries, @$rv2;
}
}
\@entries;
}
# fget_quota - to get a Maildir quota limitation info, return a HASH
sub fget_quota {
my $path = $CFG{path};
my ($smax, $cmax);
$path = untaint($path);
# Update 2005-07-26, check maildirsize first, fallback to
# $ENV{DEFAULT_QUOTA} if not present.
if(-e "$path/filesize") {
open(my $FD, "< $path/filesize") or
die "Can't open filesize, $!\n";
$_=<$FD> || "";
close FD;
}elsif($ENV{FILEMAN_QUOTA}) {
$_=$ENV{FILEMAN_QUOTA};
}else {
$_=$ENV{DEFAULT_QUOTA}; # default quota
}
if(!length($_)) {
return {
size => undef,
count => undef
}
}
if(/(\d+)C/i) {
# has a count quota
$cmax = $1;
}elsif(/(\d+)S/i) {
# has a size quota
$smax = $1;
}
return {
size =>$smax,
count => $cmax
}; # return a ref of HASH
}
# fget_curquota - to get current quota usage, return a ARRAY not HASH
sub fget_curquota {
my $path = $CFG{path};
my ($size, $cnt) = (0,0);
$path = untaint($path);
open(FD, "<$path/filesize") or
die "Can't open maildirsize, $!\n";
my $s = <FD>; # omit the first line;
seek(FD,0,0) unless($s=~/S|C/); # unget if no quota limit
while(<FD>) {
chomp;
/\s*([\-]*\d+)\s+([\-]*\d+)/;
$cnt = $cnt+$2;
$size = $size+$1; # include -xxx, perl will automaticlly
# handle nagetive value :)
}
return {
size => $size,
count => $cnt
}; # return a ref of HASH
}
# set_quota - reset quota to a new value, mostly used by ADMIN API
sub freset_quota {
my ($q_size, $q_cnt) = @_;
my $path = $CFG{path};
$path = untaint($path);
open(FD, "< $path/filesize") or
die "Can't open filesize, $!\n";
my $s = <FD>;# omit the first line
seek(FD,0,0) unless($s=~/S|C/); # unget if no quota limit
local $/= undef;
$s = <FD>;
close FD;
open(FD, "> $path/filesize.tmp") or
die "Can't open filesize.tmp for write, $!\n";
print FD "$q_size"."S";
if($q_cnt) {
print FD ",$q_cnt"."C";
}
print FD "\n"; # newline
print FD $s;
close FD;
unlink untaint($path."/filesize") || die "unlink fail: $!\n";
rename untaint($path."/filesize.tmp"), untaint($path."/filesize") or
die "Can't rename:$!\n";
1;
}
sub fre_calculate {
my $inf2 = fscan_dir();
my $inf = fget_quota;
open(FD, "> $CFG{ctrlfile}.tmp") or
die "Can't open filesize.tmp: $!\n";
flock (FD, LOCK_EX);
if($inf->{size} and $inf->{count}) {
print FD $inf->{size}."S,".$inf->{count}."C\n";
}elsif($inf->{size}) {
print FD $inf->{size}."S\n";
}elsif($inf->{count}) {
print FD $inf->{count}."C\n";
}
my $str = _fmt2mds($inf2->{sizes}, $inf2->{files});
print FD $str;
flock (FD, LOCK_UN);
close FD;
rename (untaint($CFG{path}."/filesize.tmp"),
untaint($CFG{path}."/filesize"))
or die "Can't rename filesize, $!\n";
}
# fupdate_quota - do an append action to filesize file.
sub fupdate_quota {
my ($size, $count) = @_;
my $file = defined $CFG{mdspath} ?
$CFG{mdspath}: $CFG{path}.'/filesize';
$file = untaint($file);
open(FD, ">> $file") or
die "Can't open filesize, $!\n";
flock(FD, LOCK_EX);
my $str = _fmt2mds($size, $count);
print FD $str;
flock(FD, LOCK_UN);
close FD;
# after update, check filesize file size;
if((stat $file)[7] >= 5120) {
fre_calculate();
}
}
# fupdate_quota_s - update bunch of quota records
sub fupdate_quota_s {
my $ref = $_[0];
my $file = defined $CFG{mdspath} ?
$CFG{mdspath}: $CFG{path}.'/filesize';
$file = untaint($file);
open(my $FD, ">> $file") or
die "Can't open filesize, $!\n";
flock($FD, LOCK_EX);
foreach(keys %$ref) {
my($s,$c) = split(/\s/, $ref->{$_});
my $str = _fmt2mds($s,$c);
print $FD $str;
}
flock($FD, LOCK_UN);
close $FD;
# after update, check filesize file size;
if((stat $file)[7] >= 5120) {
fre_calculate();
}
}
# _fmt2mds format given params into filesize record
sub _fmt2mds {
my $smaxlen = 8; # recommand 10 digitals
my $cmaxlen = 5; # recommand 6 digitals
my $put = "";
my($s,$c) = @_; # size can be nagetive, like -1260
if(length($s) < $smaxlen) {
my $delta = $smaxlen - length($s);
$put .= " " x $delta . "$s";
}else {
$put .="$s";
}
if(length($c) < $cmaxlen) {
my $delta = $cmaxlen - length($c);
$put .=" ". " " x $delta . "$c";
}else {
$put .=" $c";
}
return "$put\n";
}
# fis_overquota - check whether a Maildir is over quota, need fget_quota
# this function will automatically set overquota flag to a file:
# $HOME/Maildir/quotawarn.
#
# Tricks: if any of (size, count) is 'undef' or '0', means no limit!
#
# Update: 2005-07-31 use SOFT/HARD_OVER to identify whether a maildir
# is nearly overquota or already overquota
#
# 2005-08-05 add two params to calculate where it's overquota, while
# writing a new email
use constant NO_OVERQT => 0;
use constant SOFT_OVER => 1;
use constant HARD_OVER => 2;
sub fis_overquota {
my ($addsize, $addcnt) = @_;
my $cur = fget_curquota();
my ($q_size, $q_cnt);
my $sig = 0; # XXX NOT_OVER
my $inf = fget_quota();
$q_size = $inf->{size} ? $inf->{size} : 0;
$q_cnt = $inf->{count} ? $inf->{count} : 0;
$cur->{size} += $addsize if(defined $addsize && $addsize>0);
$cur->{count} += $addcnt if(defined $addcnt && $addcnt>0);
if($q_cnt) { # quota set
if($cur->{count} >= $q_cnt) {
$sig = 2; # XXX HARD_OVER
}elsif($cur->{count} >= int($q_cnt*$CFG{warnlv})) {
$sig = 1; # XXX SOFT_OVER
}
}
if($q_size) { # quota set
# XXX all SOFT_OVER
unless ($sig>1) { # if not HARD_OVER
if($cur->{size} >= $q_size) { # HARD_OVER
$sig = 2;
}else {
if($cur->{size} >= $q_size*$CFG{warnlv}) {
$sig = 1;
}
}
}
}
$sig;
}
sub fscan_dir {
my $rv = get_dir_cnt(''); # the top path?
return $rv;
}
# get_files_list - a public func to get a formated files list
sub get_files_list {
my $dir = fixpath(shift);
my $path = fixslash($CFG{path}."/$dir");
opendir DIR, $path || die "Can't opendir $path, $!\n";
my @dir = sort {$a cmp $b} grep { !/^\.$/ && !/^\..$/ } readdir DIR;
close DIR;
if ($dir =~ m#^\s*/*\s*$#) {
# means we are in topdir, ignore ctrlfile
@dir = grep { !/^filesize$/ } @dir;
}
@dir;
}
my %ext_maps = (
'gz' => 'zip',
'zip' => 'zip',
'rar' => 'zip',
'tar' => 'zip',
'tgz' => 'zip',
'bz2' => 'zip',
'jpg' => 'pic',
'jpeg' => 'pic',
'gif' => 'pic',
'exe' => 'exe',
'com' => 'exe',
'bin' => 'exe',
'pl' => 'txt',
'php' => 'txt',
'jsp' => 'txt',
'asp' => 'txt',
'py' => 'txt',
'h' => 'txt',
'cpp' => 'txt',
'c' => 'txt',
'in' => 'txt',
'ini' => 'ini',
'html' => 'html',
'css' => 'css',
'js' => 'txt',
'csv' => 'xls',
'xls' => 'xls',
'ppt' => 'ppt',
'doc' => 'doc',
'pdf' => 'pdf',
'chm' => 'chm',
'au' => 'au',
'mp3' => 'mp3',
'rm' => 'rm',
'wav' => 'au',
'wmv' => 'stream',
);
sub ext2mime {
my $file = $_[0];
if ($file =~ m!/!) {
$file =~ s#.*/([^\/]+)$#$1#;
}
$file =~ m#\.([^\.]+)$#;
if (my $ext = lc $1) {
return $ext_maps{$ext} if ($ext_maps{$ext});
}
'txt';
}
sub _length_fmt {
my ($s, $len) = @_;
my $delta = 0;
if(length($s)<$len) {
$delta = $len - length($s);
}
return ('0' x $delta).$s;
}
sub _parse_cache {
my $s = $_[0];
my %info = ();
my @a = split(/\n/, $s);
foreach(@a) {
/^([^=]+)=(.*)/;
$info{$1}=$2;
}
\%info;
}
# get_dir_cnt - get a specific directory sizes and file count
sub get_dir_cnt {
my $dir = fixpath($_[0]); # XXX should be relative path?!
my $rdir = fixslash("$CFG{path}/$dir");
my %stat = (dirs => 0, files => 0, sizes => 0);
opendir DIR, $rdir or die "$rdir open fail, $!\n";
my @lists = grep { !/^\.$/ && !/^\.\.$/ } readdir DIR;
close DIR;
if ($dir =~ m#^\s*/*\s*$#) {
# means we are in topdir, ignore ctrlfile
@lists = grep { !/^filesize$/ } @lists;
}
for (@lists) {
if (-d "$rdir/$_") { # a directory
$stat{dirs} ++;
my $rv = get_dir_cnt("$dir/$_");
if ($rv) {
$stat{dirs} += $rv->{dirs};
$stat{files} += $rv->{files};
$stat{sizes} += $rv->{sizes};
}
}else {
$stat{files}++;
$stat{sizes} += (stat "$rdir/$_")[7];
}
}
return {
dirs => $stat{dirs},
files => $stat{files},
sizes => $stat{sizes},
};
}
# Utils funct*
#
# name2mdir - convert a given folder name, aka 'Inbox' or 'Trash' etc,
# to a dir, which makes sense to low level operation.
sub fixslash {
my $path = shift;
$path =~ s#/{2,}#/#g;
$path;
}
# fixpath - an important function to remove dangerous
sub fixpath {
my $path = shift;
# fix bug, old bug code: $path =~ s#\.{2,}/##g;
$path =~ s#/\.+##g; # strip /.. or /... etc
$path =~ s#\.+/##g; # strip ../ or .../ etc
$path =~ s#\\\.+##g; # strip \. or \... etc
$path =~ s#\\+##g; # strip \\ or \\\ etc
$path;
}
1;
syntax highlighted by Code2HTML, v. 0.9.1