Commit 3c61e66f authored by Frédéric Buclin's avatar Frédéric Buclin

Bug 490930: Always store attachments locally if they are over X size (and below…

Bug 490930: Always store attachments locally if they are over X size (and below some threshold!), don't ever display "Big File" checkbox r=mkanat a=LpSolit
parent 3c64c526
...@@ -59,6 +59,9 @@ use Bugzilla::Util; ...@@ -59,6 +59,9 @@ use Bugzilla::Util;
use Bugzilla::Field; use Bugzilla::Field;
use Bugzilla::Hook; use Bugzilla::Hook;
use File::Copy;
use List::Util qw(max);
use base qw(Bugzilla::Object); use base qw(Bugzilla::Object);
############################### ###############################
...@@ -108,7 +111,6 @@ use constant VALIDATORS => { ...@@ -108,7 +111,6 @@ use constant VALIDATORS => {
isprivate => \&_check_is_private, isprivate => \&_check_is_private,
isurl => \&_check_is_url, isurl => \&_check_is_url,
mimetype => \&_check_content_type, mimetype => \&_check_content_type,
store_in_file => \&_check_store_in_file,
}; };
use constant UPDATE_VALIDATORS => { use constant UPDATE_VALIDATORS => {
...@@ -538,52 +540,29 @@ sub _check_content_type { ...@@ -538,52 +540,29 @@ sub _check_content_type {
sub _check_data { sub _check_data {
my ($invocant, $params) = @_; my ($invocant, $params) = @_;
my $data; my $data = $params->{data};
if ($params->{isurl}) { if ($params->{isurl}) {
$data = $params->{data};
($data && $data =~ m#^(http|https|ftp)://\S+#) ($data && $data =~ m#^(http|https|ftp)://\S+#)
|| ThrowUserError('attachment_illegal_url', { url => $data }); || ThrowUserError('attachment_illegal_url', { url => $data });
$params->{mimetype} = 'text/plain'; $params->{mimetype} = 'text/plain';
$params->{ispatch} = 0; $params->{ispatch} = 0;
$params->{store_in_file} = 0; $params->{filesize} = length($data);
} }
else { else {
if ($params->{store_in_file} || !ref $params->{data}) { $params->{filesize} = ref $data ? -s $data : length($data);
# If it's a filehandle, just store it, not the content of the file
# itself as the file may be quite large. If it's not a filehandle,
# it already contains the content of the file.
$data = $params->{data};
}
else {
# The file will be stored in the DB. We need the content of the file.
local $/;
my $fh = $params->{data};
$data = <$fh>;
}
} }
Bugzilla::Hook::process('attachment_process_data', { data => \$data, Bugzilla::Hook::process('attachment_process_data', { data => \$data,
attributes => $params }); attributes => $params });
# Do not validate the size if we have a filehandle. It will be checked later. $params->{filesize} || ThrowUserError('zero_length_file');
return $data if ref $data;
$data || ThrowUserError('zero_length_file');
# Make sure the attachment does not exceed the maximum permitted size. # Make sure the attachment does not exceed the maximum permitted size.
my $len = length($data); my $max_size = max(Bugzilla->params->{'maxlocalattachment'} * 1048576,
my $max_size = $params->{store_in_file} ? Bugzilla->params->{'maxlocalattachment'} * 1048576 Bugzilla->params->{'maxattachmentsize'} * 1024);
: Bugzilla->params->{'maxattachmentsize'} * 1024;
if ($len > $max_size) { if ($params->{filesize} > $max_size) {
my $vars = { filesize => sprintf("%.0f", $len/1024) }; my $vars = { filesize => sprintf("%.0f", $params->{filesize}/1024) };
if ($params->{ispatch}) { ThrowUserError('file_too_large', $vars);
ThrowUserError('patch_too_large', $vars);
}
elsif ($params->{store_in_file}) {
ThrowUserError('local_file_too_large');
}
else {
ThrowUserError('file_too_large', $vars);
}
} }
return $data; return $data;
} }
...@@ -642,15 +621,6 @@ sub _check_is_url { ...@@ -642,15 +621,6 @@ sub _check_is_url {
return $is_url ? 1 : 0; return $is_url ? 1 : 0;
} }
sub _check_store_in_file {
my ($invocant, $store_in_file) = @_;
if ($store_in_file && !Bugzilla->params->{'maxlocalattachment'}) {
ThrowCodeError('attachment_local_storage_disabled');
}
return $store_in_file ? 1 : 0;
}
=pod =pod
=head2 Class Methods =head2 Class Methods
...@@ -815,9 +785,6 @@ Params: takes a hashref with the following keys: ...@@ -815,9 +785,6 @@ Params: takes a hashref with the following keys:
the attachment is private. the attachment is private.
C<isurl> - boolean (optional, default false) - true if the C<isurl> - boolean (optional, default false) - true if the
attachment is a URL pointing to some external ressource. attachment is a URL pointing to some external ressource.
C<store_in_file> - boolean (optional, default false) - true
if the attachment must be stored in data/attachments/ instead
of in the DB.
Returns: The new attachment object. Returns: The new attachment object.
...@@ -833,53 +800,44 @@ sub create { ...@@ -833,53 +800,44 @@ sub create {
# Extract everything which is not a valid column name. # Extract everything which is not a valid column name.
my $bug = delete $params->{bug}; my $bug = delete $params->{bug};
$params->{bug_id} = $bug->id; $params->{bug_id} = $bug->id;
my $fh = delete $params->{data}; my $data = delete $params->{data};
my $store_in_file = delete $params->{store_in_file}; my $size = delete $params->{filesize};
my $attachment = $class->insert_create_data($params); my $attachment = $class->insert_create_data($params);
my $attachid = $attachment->id; my $attachid = $attachment->id;
# We only use $data here in this INSERT with a placeholder, # The file is too large to be stored in the DB, so we store it locally.
# so it's safe. if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) {
my $sth = $dbh->prepare("INSERT INTO attach_data
(id, thedata) VALUES ($attachid, ?)");
my $data = $store_in_file ? "" : $fh;
trick_taint($data);
$sth->bind_param(1, $data, $dbh->BLOB_TYPE);
$sth->execute();
# If the file is to be stored locally, stream the file from the web server
# to the local file without reading it into a local variable.
if ($store_in_file) {
my $attachdir = bz_locations()->{'attachdir'}; my $attachdir = bz_locations()->{'attachdir'};
my $hash = ($attachid % 100) + 100; my $hash = ($attachid % 100) + 100;
$hash =~ s/.*(\d\d)$/group.$1/; $hash =~ s/.*(\d\d)$/group.$1/;
mkdir "$attachdir/$hash", 0770; mkdir "$attachdir/$hash", 0770;
chmod 0770, "$attachdir/$hash"; chmod 0770, "$attachdir/$hash";
open(AH, '>', "$attachdir/$hash/attachment.$attachid"); if (ref $data) {
binmode AH; copy($data, "$attachdir/$hash/attachment.$attachid");
if (ref $fh) { close $data;
my $limit = Bugzilla->params->{"maxlocalattachment"} * 1048576;
my $sizecount = 0;
while (<$fh>) {
print AH $_;
$sizecount += length($_);
if ($sizecount > $limit) {
close AH;
close $fh;
unlink "$attachdir/$hash/attachment.$attachid";
ThrowUserError("local_file_too_large");
}
}
close $fh;
} }
else { else {
print AH $fh; open(AH, '>', "$attachdir/$hash/attachment.$attachid");
binmode AH;
print AH $data;
close AH;
} }
close AH; $data = ''; # Will be stored in the DB.
}
# If we have a filehandle, we need its content to store it in the DB.
elsif (ref $data) {
local $/;
$data = <$data>;
} }
my $sth = $dbh->prepare("INSERT INTO attach_data
(id, thedata) VALUES ($attachid, ?)");
trick_taint($data);
$sth->bind_param(1, $data, $dbh->BLOB_TYPE);
$sth->execute();
# Return the new attachment object. # Return the new attachment object.
return $attachment; return $attachment;
} }
......
...@@ -485,7 +485,6 @@ sub insert { ...@@ -485,7 +485,6 @@ sub insert {
isprivate => scalar $cgi->param('isprivate'), isprivate => scalar $cgi->param('isprivate'),
isurl => scalar $cgi->param('attachurl'), isurl => scalar $cgi->param('attachurl'),
mimetype => $content_type, mimetype => $content_type,
store_in_file => scalar $cgi->param('bigfile'),
}); });
foreach my $obsolete_attachment (@obsolete_attachments) { foreach my $obsolete_attachment (@obsolete_attachments) {
......
...@@ -332,9 +332,7 @@ ...@@ -332,9 +332,7 @@
<para> <para>
<emphasis>Attachments:</emphasis> <emphasis>Attachments:</emphasis>
You can attach files (e.g. testcases or patches) to bugs. If there You can attach files (e.g. testcases or patches) to bugs. If there
are any attachments, they are listed in this section. Attachments are are any attachments, they are listed in this section.
normally stored in the Bugzilla database, unless they are marked as
Big Files, which are stored directly on disk.
</para> </para>
</listitem> </listitem>
...@@ -862,20 +860,6 @@ ...@@ -862,20 +860,6 @@
</para> </para>
<para> <para>
If you have a really large attachment, something that does not need to
be recorded forever (as most attachments are), or something that is too
big for your database, you can mark your attachment as a
<quote>Big File</quote>, assuming the administrator of the installation
has enabled this feature. Big Files are stored directly on disk instead
of in the database. The maximum size of a <quote>Big File</quote> is
normally larger than the maximum size of a regular attachment. Independently
of the storage system used, an administrator can delete these attachments
at any time. Nevertheless, if these files are stored in the database, the
<quote>allow_attachment_deletion</quote> parameter (which is turned off
by default) must be enabled in order to delete them.
</para>
<para>
Also, if the administrator turned on the <quote>allow_attach_url</quote> Also, if the administrator turned on the <quote>allow_attach_url</quote>
parameter, you can enter the URL pointing to the attachment instead of parameter, you can enter the URL pointing to the attachment instead of
uploading the attachment itself. For example, this is useful if you want to uploading the attachment itself. For example, this is useful if you want to
......
...@@ -56,8 +56,7 @@ function setContentTypeDisabledState(form) ...@@ -56,8 +56,7 @@ function setContentTypeDisabledState(form)
function URLFieldHandler() { function URLFieldHandler() {
var field_attachurl = document.getElementById("attachurl"); var field_attachurl = document.getElementById("attachurl");
var greyfields = new Array("data", "ispatch", "autodetect", var greyfields = new Array("data", "ispatch", "autodetect",
"list", "manual", "bigfile", "list", "manual", "contenttypeselection",
"contenttypeselection",
"contenttypeentry"); "contenttypeentry");
var i, thisfield; var i, thisfield;
if (field_attachurl.value.match(/^\s*$/)) { if (field_attachurl.value.match(/^\s*$/)) {
...@@ -103,8 +102,6 @@ function clearAttachmentFields() { ...@@ -103,8 +102,6 @@ function clearAttachmentFields() {
document.getElementById('data').value = ''; document.getElementById('data').value = '';
DataFieldHandler(); DataFieldHandler();
if ((element = document.getElementById('bigfile')))
element.checked = '';
if ((element = document.getElementById('attachurl'))) { if ((element = document.getElementById('attachurl'))) {
element.value = ''; element.value = '';
URLFieldHandler(); URLFieldHandler();
......
...@@ -205,7 +205,6 @@ if (defined($cgi->upload('data')) || $cgi->param('attachurl')) { ...@@ -205,7 +205,6 @@ if (defined($cgi->upload('data')) || $cgi->param('attachurl')) {
isprivate => scalar $cgi->param('isprivate'), isprivate => scalar $cgi->param('isprivate'),
isurl => scalar $cgi->param('attachurl'), isurl => scalar $cgi->param('attachurl'),
mimetype => $content_type, mimetype => $content_type,
store_in_file => scalar $cgi->param('bigfile'),
}); });
}; };
Bugzilla->error_mode($error_mode_cache); Bugzilla->error_mode($error_mode_cache);
......
...@@ -64,13 +64,16 @@ ...@@ -64,13 +64,16 @@
"specify a URL when creating an attachment and " _ "specify a URL when creating an attachment and " _
"treat the URL itself as if it were an attachment.", "treat the URL itself as if it were an attachment.",
maxattachmentsize => "The maximum size (in kilobytes) of attachments. " _ maxattachmentsize => "The maximum size (in kilobytes) of attachments to be stored " _
"$terms.Bugzilla will not accept attachments greater than this number " _ "in the database. If a file larger than this size is attached " _
"of kilobytes in size. Setting this parameter to 0 will prevent " _ "to ${terms.abug}, $terms.Bugzilla will look at the " _
"attaching files to ${terms.bugs}.", "<a href='#maxlocalattachment'><tt>maxlocalattachment</tt> parameter</a> " _
"to determine if the file can be stored locally on the web server. " _
"If the file size exceeds both limits, then the attachment is rejected. " _
"Settings both parameters to 0 will prevent attaching files to ${terms.bugs}.",
maxlocalattachment => "The maximum size (in megabytes) of attachments identified by " _ maxlocalattachment => "The maximum size (in megabytes) of attachments to be stored " _
"the user as 'Big Files' to be stored locally on the webserver. " _ "locally on the web server. If set to a value lower than the " _
"If set to zero, attachments will never be kept on the local " _ "<a href='#maxattachmentsize'><tt>maxattachmentsize</tt> parameter</a>, " _
"filesystem." } "attachments will never be kept on the local filesystem." }
%] %]
...@@ -32,18 +32,6 @@ ...@@ -32,18 +32,6 @@
> >
</td> </td>
</tr> </tr>
[% IF Param("maxlocalattachment") %]
<tr class="expert_fields">
<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 %]
[% IF Param("allow_attach_url") %] [% IF Param("allow_attach_url") %]
<tr class="expert_fields"> <tr class="expert_fields">
<th><label for="attachurl">AttachURL</label>:</th> <th><label for="attachurl">AttachURL</label>:</th>
......
...@@ -37,11 +37,7 @@ ...@@ -37,11 +37,7 @@
[% DEFAULT title = "Internal Error" %] [% DEFAULT title = "Internal Error" %]
[% error_message = BLOCK %] [% error_message = BLOCK %]
[% IF error == "attachment_local_storage_disabled" %] [% IF error == "attachment_url_disabled" %]
[% title = "Local Storage Disabled" %]
You cannot store attachments locally. This feature is disabled.
[% ELSIF error == "attachment_url_disabled" %]
[% title = "Attachment URL Disabled" %] [% title = "Attachment URL Disabled" %]
You cannot attach a URL. This feature is currently disabled. You cannot attach a URL. This feature is currently disabled.
......
...@@ -574,9 +574,13 @@ ...@@ -574,9 +574,13 @@
[% ELSIF error == "file_too_large" %] [% ELSIF error == "file_too_large" %]
[% title = "File Too Large" %] [% title = "File Too Large" %]
[%# Convert maxlocalattachment from Mb to Kb %]
[% max_local = Param('maxlocalattachment') * 1024 %]
[% max_limit = [Param('maxattachmentsize'), max_local] %]
The file you are trying to attach is [% filesize FILTER html %] The file you are trying to attach is [% filesize FILTER html %]
kilobytes (KB) in size. Non-patch attachments cannot be more than kilobytes (KB) in size. Attachments cannot be more than
[%+ Param('maxattachmentsize') %] KB. <br> [%# Hack to get the max value between both limits %]
[%+ max_limit.nsort.last FILTER html %] KB. <br>
We recommend that you store your attachment elsewhere We recommend that you store your attachment elsewhere
[% IF Param("allow_attach_url") %] [% IF Param("allow_attach_url") %]
and then specify the URL to this file on the attachment and then specify the URL to this file on the attachment
...@@ -1023,11 +1027,6 @@ ...@@ -1023,11 +1027,6 @@
[% title = "Invalid Keyword Name" %] [% title = "Invalid Keyword Name" %]
You may not use commas or whitespace in a 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" %] [% ELSIF error == "login_needed_for_password_change" %]
[% title = "Login Name Required" %] [% title = "Login Name Required" %]
You must enter a login name when requesting to change your password. You must enter a login name when requesting to change your password.
...@@ -1312,13 +1311,6 @@ ...@@ -1312,13 +1311,6 @@
The password must be at least The password must be at least
[%+ constants.USER_PASSWORD_MIN_LENGTH FILTER html %] characters long. [%+ constants.USER_PASSWORD_MIN_LENGTH FILTER html %] characters long.
[% ELSIF error == "patch_too_large" %]
[% title = "File Too Large" %]
The file you are trying to attach is [% filesize FILTER html %]
kilobytes (KB) in size.
Patches cannot be more than [% Param('maxattachmentsize') %] KB in size.
Try splitting your patch into several pieces.
[% ELSIF error == "product_access_denied" %] [% ELSIF error == "product_access_denied" %]
Either the product Either the product
[%+ IF id.defined %] [%+ IF id.defined %]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment