summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbugreport%peshkin.net <>2005-02-21 02:03:09 +0100
committerbugreport%peshkin.net <>2005-02-21 02:03:09 +0100
commit30a52319c024fefb2b4e4aa7bd974e4af7af30cd (patch)
tree4ee72b234e27753d291454020d97cc03e8f51a94
parentc1d16e426c2d1f2fe5563c0d20b3b8eb256d8c77 (diff)
downloadbugzilla-30a52319c024fefb2b4e4aa7bd974e4af7af30cd.tar.gz
bugzilla-30a52319c024fefb2b4e4aa7bd974e4af7af30cd.tar.xz
Bug 252272: Allow extremely large attachments to be stored locally
r=wurblzap.a=justdave
-rw-r--r--Bugzilla/Attachment.pm12
-rw-r--r--Bugzilla/Config.pm4
-rwxr-xr-xattachment.cgi91
-rwxr-xr-xchecksetup.pl20
-rw-r--r--defparams.pl11
-rw-r--r--template/en/default/attachment/create.html.tmpl12
-rw-r--r--template/en/default/global/user-error.html.tmpl9
7 files changed, 143 insertions, 16 deletions
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index e7b3ffe86..5f491f315 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -33,6 +33,7 @@ package Bugzilla::Attachment;
# Use the Flag module to handle flags.
use Bugzilla::Flag;
+use Bugzilla::Config qw(:locations);
############################################################################
# Functions
@@ -92,6 +93,17 @@ sub query
# Retrieve a list of flags for this attachment.
$a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'},
'is_active' => 1 });
+
+ # A zero size indicates that the attachment is stored locally.
+ if ($a{'datasize'} == 0) {
+ my $attachid = $a{'attachid'};
+ my $hash = ($attachid % 100) + 100;
+ $hash =~ s/.*(\d\d)$/group.$1/;
+ if (open(AH, "$attachdir/$hash/attachment.$attachid")) {
+ $a{'datasize'} = (stat(AH))[7];
+ close(AH);
+ }
+ }
# We will display the edit link if the user can edit the attachment;
# ie the are the submitter, or they have canedit.
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 5c070e372..3849f146b 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -55,6 +55,7 @@ use Bugzilla::Util;
our $libpath = '.';
our $localconfig = "$libpath/localconfig";
our $datadir = "$libpath/data";
+our $attachdir = "$datadir/attachments";
our $templatedir = "$libpath/template";
our $webdotdir = "$datadir/webdot";
@@ -72,7 +73,8 @@ our $webdotdir = "$datadir/webdot";
(
admin => [qw(GetParamList UpdateParams SetParam WriteParams)],
db => [qw($db_driver $db_host $db_port $db_name $db_user $db_pass $db_sock)],
- locations => [qw($libpath $localconfig $datadir $templatedir $webdotdir)],
+ locations => [qw($libpath $localconfig $attachdir
+ $datadir $templatedir $webdotdir)],
);
Exporter::export_ok_tags('admin', 'db', 'locations');
diff --git a/attachment.cgi b/attachment.cgi
index 3522f9e26..0a296609b 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -40,6 +40,7 @@ use vars qw(
# Include the Bugzilla CGI and general utility library.
require "CGI.pl";
+use Bugzilla::Config qw(:locations);
# Use these modules to handle flags.
use Bugzilla::Constants;
@@ -360,12 +361,18 @@ sub validateData
{
my $maxsize = $::FORM{'ispatch'} ? Param('maxpatchsize') : Param('maxattachmentsize');
$maxsize *= 1024; # Convert from K
-
- my $fh = $cgi->upload('data');
+ my $fh;
+ # Skip uploading into a local variable if the user wants to upload huge
+ # attachments into local files.
+ if (!$::FORM{'bigfile'})
+ {
+ $fh = $cgi->upload('data');
+ }
my $data;
# We could get away with reading only as much as required, except that then
# we wouldn't have a size to print to the error handler below.
+ if (!$::FORM{'bigfile'})
{
# enable 'slurp' mode
local $/;
@@ -373,10 +380,11 @@ sub validateData
}
$data
+ || ($::FORM{'bigfile'})
|| ThrowUserError("zero_length_file");
# Make sure the attachment does not exceed the maximum permitted size
- my $len = length($data);
+ my $len = $data ? length($data) : 0;
if ($maxsize && $len > $maxsize) {
my $vars = { filesize => sprintf("%.0f", $len/1024) };
if ( $::FORM{'ispatch'} ) {
@@ -504,6 +512,23 @@ sub view
# Return the appropriate HTTP response headers.
$filename =~ s/^.*[\/\\]//;
my $filesize = length($thedata);
+ # A zero length attachment in the database means the attachment is
+ # stored in a local file
+ if ($filesize == 0)
+ {
+ my $attachid = $::FORM{'id'};
+ my $hash = ($attachid % 100) + 100;
+ $hash =~ s/.*(\d\d)$/group.$1/;
+ if (open(AH, "$attachdir/$hash/attachment.$attachid")) {
+ binmode AH;
+ $filesize = (stat(AH))[7];
+ }
+ }
+ if ($filesize == 0)
+ {
+ ThrowUserError("attachment_removed");
+ }
+
# escape quotes and backslashes in the filename, per RFCs 2045/822
$filename =~ s/\\/\\\\/g; # escape backslashes
@@ -513,7 +538,15 @@ sub view
-content_disposition=> "inline; filename=\"$filename\"",
-content_length => $filesize);
- print $thedata;
+ if ($thedata) {
+ print $thedata;
+ } else {
+ while (<AH>) {
+ print $_;
+ }
+ close(AH);
+ }
+
}
sub interdiff
@@ -771,7 +804,7 @@ sub viewall
$privacy = "AND isprivate < 1 ";
}
SendSQL("SELECT attach_id, DATE_FORMAT(creation_ts, '%Y.%m.%d %H:%i'),
- mimetype, description, ispatch, isobsolete, isprivate,
+ mimetype, description, ispatch, isobsolete, isprivate,
LENGTH(thedata)
FROM attachments WHERE bug_id = $::FORM{'bugid'} $privacy
ORDER BY attach_id");
@@ -779,7 +812,7 @@ sub viewall
while (MoreSQLData())
{
my %a; # the attachment hash
- ($a{'attachid'}, $a{'date'}, $a{'contenttype'},
+ ($a{'attachid'}, $a{'date'}, $a{'contenttype'},
$a{'description'}, $a{'ispatch'}, $a{'isobsolete'}, $a{'isprivate'},
$a{'datasize'}) = FetchSQLData();
$a{'isviewable'} = isViewable($a{'contenttype'});
@@ -889,11 +922,39 @@ sub insert
# Retrieve the ID of the newly created attachment record.
my $attachid = $dbh->bz_last_key('attachments', 'attach_id');
+ # If the file is to be stored locally, stream the file from the webserver
+ # to the local file without reading it into a local variable.
+ if ($::FORM{'bigfile'})
+ {
+ my $fh = $cgi->upload('data');
+ my $hash = ($attachid % 100) + 100;
+ $hash =~ s/.*(\d\d)$/group.$1/;
+ mkdir "$attachdir/$hash", 0770;
+ chmod 0770, "$attachdir/$hash";
+ open(AH, ">$attachdir/$hash/attachment.$attachid");
+ binmode AH;
+ my $sizecount = 0;
+ my $limit = (Param("maxlocalattachment") * 1048576);
+ while (<$fh>) {
+ print AH $_;
+ $sizecount += length($_);
+ if ($sizecount > $limit) {
+ close AH;
+ close $fh;
+ unlink "$attachdir/$hash/attachment.$attachid";
+ ThrowUserError("local_file_too_large");
+ }
+ }
+ close AH;
+ close $fh;
+ }
+
+
# Insert a comment about the new attachment into the database.
my $comment = "Created an attachment (id=$attachid)\n$::FORM{'description'}\n";
$comment .= ("\n" . $::FORM{'comment'}) if $::FORM{'comment'};
- AppendComment($::FORM{'bugid'},
+ AppendComment($::FORM{'bugid'},
Bugzilla->user->login,
$comment,
$isprivate,
@@ -906,7 +967,7 @@ sub insert
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
VALUES ($::FORM{'bugid'}, $obsolete_id, $::userid, $sql_timestamp, $fieldid, '0', '1')");
# If the obsolete attachment has pending flags, migrate them to the new attachment.
- if (Bugzilla::Flag::count({ 'attach_id' => $obsolete_id ,
+ if (Bugzilla::Flag::count({ 'attach_id' => $obsolete_id ,
'status' => 'pending',
'is_active' => 1 })) {
Bugzilla::Flag::migrate($obsolete_id, $attachid, $timestamp);
@@ -1009,11 +1070,11 @@ sub edit
# Get a list of flag types that can be set for this attachment.
SendSQL("SELECT product_id, component_id FROM bugs WHERE bug_id = $bugid");
my ($product_id, $component_id) = FetchSQLData();
- my $flag_types = Bugzilla::FlagType::match({ 'target_type' => 'attachment' ,
- 'product_id' => $product_id ,
+ my $flag_types = Bugzilla::FlagType::match({ 'target_type' => 'attachment' ,
+ 'product_id' => $product_id ,
'component_id' => $component_id });
foreach my $flag_type (@$flag_types) {
- $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id' => $flag_type->{'id'},
+ $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id' => $flag_type->{'id'},
'attach_id' => $::FORM{'id'},
'is_active' => 1 });
}
@@ -1087,10 +1148,10 @@ sub update
# Update the attachment record in the database.
# Sets the creation timestamp to itself to avoid it being updated automatically.
SendSQL("UPDATE attachments
- SET description = $quoteddescription ,
- mimetype = $quotedcontenttype ,
+ SET description = $quoteddescription ,
+ mimetype = $quotedcontenttype ,
filename = $quotedfilename ,
- ispatch = $::FORM{'ispatch'} ,
+ ispatch = $::FORM{'ispatch'},
isobsolete = $::FORM{'isobsolete'} ,
isprivate = $::FORM{'isprivate'}
WHERE attach_id = $::FORM{'id'}
@@ -1143,7 +1204,7 @@ sub update
# Unlock all database tables now that we are finished updating the database.
$dbh->bz_unlock_tables();
- # If the user submitted a comment while editing the attachment,
+ # If the user submitted a comment while editing the attachment,
# add the comment to the bug.
if ( $::FORM{'comment'} )
{
diff --git a/checksetup.pl b/checksetup.pl
index 9c35b5a98..2aa2a6cc1 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -904,6 +904,14 @@ unless (-d $datadir && -e "$datadir/nomail") {
open FILE, '>>', "$datadir/mail"; close FILE;
}
+
+ unless (-d $attachdir) {
+ print "Creating local attachments directory ...\n";
+ # permissions for non-webservergroup are fixed later on
+ mkdir $attachdir, 0770;
+ }
+
+
# 2000-12-14 New graphing system requires a directory to put the graphs in
# This code copied from what happens for the data dir above.
# If the graphs dir is not present, we assume that they have been using
@@ -1088,6 +1096,17 @@ END
}
}
+ if (!-e "$attachdir/.htaccess") {
+ print "Creating $attachdir/.htaccess...\n";
+ open HTACCESS, ">$attachdir/.htaccess";
+ print HTACCESS <<'END';
+# nothing in this directory is retrievable unless overriden by an .htaccess
+# in a subdirectory;
+deny from all
+END
+ close HTACCESS;
+ chmod $fileperm, "$attachdir/.htaccess";
+ }
if (!-e "Bugzilla/.htaccess") {
print "Creating Bugzilla/.htaccess...\n";
open HTACCESS, '>', 'Bugzilla/.htaccess';
@@ -1428,6 +1447,7 @@ if ($^O !~ /MSWin32/i) {
fixPerms("$datadir/duplicates", $<, $webservergid, 027, 1);
fixPerms("$datadir/mining", $<, $webservergid, 027, 1);
fixPerms("$datadir/template", $<, $webservergid, 007, 1); # webserver will write to these
+ fixPerms($attachdir, $<, $webservergid, 007, 1); # webserver will write to these
fixPerms($webdotdir, $<, $webservergid, 007, 1);
fixPerms("$webdotdir/.htaccess", $<, $webservergid, 027);
fixPerms("$datadir/params", $<, $webservergid, 017);
diff --git a/defparams.pl b/defparams.pl
index 3f91aabe2..99b942ce6 100644
--- a/defparams.pl
+++ b/defparams.pl
@@ -1270,6 +1270,17 @@ Reason: %reason%
},
{
+ name => 'maxlocalattachment',
+ desc => 'The maximum size (in Megabytes) of attachments identified by ' .
+ 'the user as "Big Files" to be stored locally on the webserver. ' .
+ 'If set to zero, attachments will never be kept on the local ' .
+ 'filesystem.',
+ type => 't',
+ default => '0',
+ checker => \&check_numeric
+ },
+
+ {
name => 'chartgroup',
desc => 'The name of the group of users who can use the "New Charts" ' .
'feature. Administrators should ensure that the public categories ' .
diff --git a/template/en/default/attachment/create.html.tmpl b/template/en/default/attachment/create.html.tmpl
index 82ad73ce1..43af6e638 100644
--- a/template/en/default/attachment/create.html.tmpl
+++ b/template/en/default/attachment/create.html.tmpl
@@ -65,6 +65,18 @@
<input type="file" id="data" name="data" size="50">
</td>
</tr>
+ [% IF Param("maxlocalattachment") %]
+ <tr>
+ <th>BigFile:</th>
+ <td>
+ <input type="checkbox" id="bigfile"
+ name="bigfile" value="bigfile">
+ <label for="bigfile">
+ Big File - Stored locally and may be purged
+ </label>
+ </td>
+ </tr>
+ [% END %]
<tr>
<th><label for="description">Description:</label></th>
<td>
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index 6a29f975d..ac2cba6d3 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -156,6 +156,10 @@
[% title = "Access Denied" %]
You are not authorized to access this attachment.
+ [% ELSIF error == "attachment_removed" %]
+ [% title = "Attachment Removed" %]
+ The attachment you are attempting to access has been removed.
+
[% ELSIF error == "bug_access_denied" %]
[% title = "Access Denied" %]
You are not authorized to access [% terms.bug %] #[% bug_id FILTER html %].
@@ -604,6 +608,11 @@
[% title = "Invalid Keyword Name" %]
You may not use commas or whitespace in a keyword name.
+ [% ELSIF error == "local_file_too_large" %]
+ [% title = "Local File Too Large" %]
+ Local file uploads must not exceed
+ [% Param('maxlocalattachment') %] MB in size.
+
[% ELSIF error == "login_needed_for_password_change" %]
[% title = "Login Name Required" %]
You must enter a login name when requesting to change your password.