Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to add total project size limit(internal changes) #8128

Merged
merged 9 commits into from
Jun 24, 2020
7 changes: 7 additions & 0 deletions tests/unit/admin/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ def test_includeme():
traverse="/{project_name}",
domain=warehouse,
),
pretend.call(
"admin.project.set_total_size_limit",
"/admin/projects/{project_name}/set_total_size_limit/",
factory="warehouse.packaging.models:ProjectFactory",
traverse="/{project_name}",
domain=warehouse,
),
pretend.call(
"admin.project.add_role",
"/admin/projects/{project_name}/add_role/",
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/admin/views/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ def test_gets_project(self, db_request):
"squattees": [squattee],
"ONE_MB": views.ONE_MB,
"MAX_FILESIZE": views.MAX_FILESIZE,
"MAX_PROJECT_SIZE": views.MAX_PROJECT_SIZE,
"ONE_GB": views.ONE_GB,
}

def test_non_normalized_name(self, db_request):
Expand Down Expand Up @@ -332,6 +334,66 @@ def test_non_normalized_name(self, db_request):
views.journals_list(project, db_request)


class TestProjectSetTotalSizeLimit:
def test_sets_total_size_limitwith_integer(self, db_request):
project = ProjectFactory.create(name="foo")

db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/projects/"
)
db_request.session = pretend.stub(
flash=pretend.call_recorder(lambda *a, **kw: None)
)
db_request.matchdict["project_name"] = project.normalized_name
db_request.POST["total_size_limit"] = "150"

views.set_total_size_limit(project, db_request)

assert db_request.session.flash.calls == [
pretend.call("Set the total size limit on 'foo'", queue="success")
]

assert project.total_size_limit == 150 * views.ONE_GB

def test_sets_total_size_limitwith_none(self, db_request):
project = ProjectFactory.create(name="foo")
project.total_size_limit = 150 * views.ONE_GB

db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/projects/"
)
db_request.session = pretend.stub(
flash=pretend.call_recorder(lambda *a, **kw: None)
)
db_request.matchdict["project_name"] = project.normalized_name

views.set_total_size_limit(project, db_request)

assert db_request.session.flash.calls == [
pretend.call("Set the total size limit on 'foo'", queue="success")
]

assert project.total_size_limit is None

def test_sets_total_size_limitwith_non_integer(self, db_request):
project = ProjectFactory.create(name="foo")

db_request.matchdict["project_name"] = project.normalized_name
db_request.POST["total_size_limit"] = "meep"

with pytest.raises(HTTPBadRequest):
views.set_total_size_limit(project, db_request)

def test_sets_total_size_limit_with_less_than_minimum(self, db_request):
project = ProjectFactory.create(name="foo")

db_request.matchdict["project_name"] = project.normalized_name
db_request.POST["total_size_limit"] = "9"

with pytest.raises(HTTPBadRequest):
views.set_total_size_limit(project, db_request)


class TestProjectSetLimit:
def test_sets_limitwith_integer(self, db_request):
project = ProjectFactory.create(name="foo")
Expand Down
7 changes: 7 additions & 0 deletions warehouse/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ def includeme(config):
traverse="/{project_name}",
domain=warehouse,
)
config.add_route(
"admin.project.set_total_size_limit",
"/admin/projects/{project_name}/set_total_size_limit/",
factory="warehouse.packaging.models:ProjectFactory",
traverse="/{project_name}",
domain=warehouse,
)
config.add_route(
"admin.project.add_role",
"/admin/projects/{project_name}/add_role/",
Expand Down
42 changes: 42 additions & 0 deletions warehouse/admin/templates/admin/projects/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ <h4>Attributes:</h4>
{% endif %}
</td>
</tr>
<tr>
<td>Total size limit</td>
<td>
{% if project.total_size_limit %}
{{ project.total_size_limit|filesizeformat(binary=True) }}
{% else %}
Default ({{(MAX_PROJECT_SIZE)|filesizeformat(binary=True) }})
{% endif %}
</td>
</tr>
</table>
</div>
<h4>Maintainers:</h4>
Expand Down Expand Up @@ -315,4 +325,36 @@ <h3 class="box-title">Set upload limit</h3>
</div>
</form>
</div>

<!--Total project size limit form -->
<div class="box box-secondary collapsed-box">
<div class="box-header with-border">
<h3 class="box-title">Set total size limit</h3>
<div class="box-tools">
<button class="btn btn-box-tool" data-widget="collapse" data-toggle="tooltip" title="Expand"><i class="fa fa-plus"></i></button>
</div>
</div>
<form method="POST" action="{{ request.route_path('admin.project.set_total_size_limit', project_name=project.normalized_name) }}">
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
<div class="box-body">
<div class="form-group col-sm-12">
<label for="uploadLimit">Total size limit (in gigabytes)</label>
{% if project.total_size_limit %}
{% set total_size_limit_value = project.total_size_limit // ONE_GB %}
{% else %}
{% set total_size_limit_value = '' %}
{% endif %}
<input type="number" name="total_size_limit" class="form-control" id="totalSizeLimit" min="{{ MAX_PROJECT_SIZE // ONE_GB }}" value="{{total_size_limit_value}}">
</div>
</div>

<div class="box-footer">
<div class="pull-right">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>


{% endblock %}
46 changes: 45 additions & 1 deletion warehouse/admin/views/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
from sqlalchemy.orm.exc import NoResultFound

from warehouse.accounts.models import User
from warehouse.forklift.legacy import MAX_FILESIZE
from warehouse.forklift.legacy import MAX_FILESIZE, MAX_PROJECT_SIZE
from warehouse.packaging.models import JournalEntry, Project, Release, Role
from warehouse.utils.paginate import paginate_url_factory
from warehouse.utils.project import confirm_project, remove_project

ONE_MB = 1024 * 1024 # bytes
ONE_GB = 1024 * 1024 * 1024 # bytes


@view_config(
Expand Down Expand Up @@ -143,6 +144,8 @@ def project_detail(project, request):
"squattees": squattees,
"ONE_MB": ONE_MB,
"MAX_FILESIZE": MAX_FILESIZE,
"ONE_GB": ONE_GB,
"MAX_PROJECT_SIZE": MAX_PROJECT_SIZE,
}


Expand Down Expand Up @@ -306,6 +309,47 @@ def set_upload_limit(project, request):
)


@view_config(
route_name="admin.project.set_total_size_limit",
permission="moderator",
request_method="POST",
uses_session=True,
require_methods=False,
)
def set_total_size_limit(project, request):
total_size_limit = request.POST.get("total_size_limit", "")

if not total_size_limit:
total_size_limit = None
else:
try:
total_size_limit = int(total_size_limit)
except ValueError:
raise HTTPBadRequest(
f"Invalid value for total size limit: {total_size_limit}, "
f"must be integer or empty string."
)

# The form is in GB, but the database field is in bytes.
total_size_limit *= ONE_GB

if total_size_limit < MAX_PROJECT_SIZE:
raise HTTPBadRequest(
f"Total project size can not be less than the default limit of "
f"{MAX_PROJECT_SIZE / ONE_GB}GB."
)

project.total_size_limit = total_size_limit

request.session.flash(
f"Set the total size limit on {project.name!r}", queue="success"
)

return HTTPSeeOther(
request.route_path("admin.project.detail", project_name=project.normalized_name)
)


@view_config(
route_name="admin.project.add_role",
permission="admin",
Expand Down
2 changes: 1 addition & 1 deletion warehouse/forklift/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@

MAX_FILESIZE = 60 * 1024 * 1024 # 60M
MAX_SIGSIZE = 8 * 1024 # 8K

MAX_PROJECT_SIZE = 10 * 1024 * 1024 * 1024 # 10GB
PATH_HASHER = "blake2_256"


Expand Down
35 changes: 35 additions & 0 deletions warehouse/migrations/versions/bc8f7b526961_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
text

Revision ID: bc8f7b526961
Revises: 19ca1c78e613
Create Date: 2020-06-16 21:14:53.343466
"""

import sqlalchemy as sa

from alembic import op

revision = "bc8f7b526961"
down_revision = "19ca1c78e613"


def upgrade():
op.add_column(
"projects", sa.Column("total_size_limit", sa.BigInteger(), nullable=True)
)


def downgrade():
op.drop_column("projects", "total_size_limit")
1 change: 1 addition & 0 deletions warehouse/packaging/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class Project(SitemapMixin, db.Model):
)
has_docs = Column(Boolean)
upload_limit = Column(Integer, nullable=True)
total_size_limit = Column(BigInteger, nullable=True)
last_serial = Column(Integer, nullable=False, server_default=sql.text("0"))
zscore = Column(Float, nullable=True)

Expand Down