Skip to content

Document the project and file upload limits in pypi on the FAQ page #16200

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

Merged
merged 11 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/dev/development/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,8 @@ Building the docs requires Python 3.8. If it is not installed, the
make: *** [.state/env/pyvenv.cfg] Error 127


.. _building-translations:

Building translations
---------------------

Expand Down
3 changes: 3 additions & 0 deletions docs/dev/translations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,6 @@ the view's configuration:
has_translations=True,
)
class SampleViews:


You may have to :ref:`rebuild the translation files <building-translations>`.
20 changes: 11 additions & 9 deletions tests/unit/admin/views/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from pyramid.httpexceptions import HTTPBadRequest, HTTPMovedPermanently, HTTPSeeOther
from sqlalchemy.orm import joinedload

import warehouse.constants

from tests.common.db.oidc import GitHubPublisherFactory
from warehouse.admin.views import projects as views
from warehouse.observations.models import ObservationKind
Expand Down Expand Up @@ -102,10 +104,10 @@ def test_gets_project(self, db_request):
"maintainers": roles,
"journal": journals[:30],
"oidc_publishers": oidc_publishers,
"ONE_MB": views.ONE_MB,
"MAX_FILESIZE": views.MAX_FILESIZE,
"MAX_PROJECT_SIZE": views.MAX_PROJECT_SIZE,
"ONE_GB": views.ONE_GB,
"ONE_MIB": views.ONE_MIB,
"MAX_FILESIZE": warehouse.constants.MAX_FILESIZE,
"MAX_PROJECT_SIZE": warehouse.constants.MAX_PROJECT_SIZE,
"ONE_GIB": views.ONE_GIB,
"UPLOAD_LIMIT_CAP": views.UPLOAD_LIMIT_CAP,
"observation_kinds": ObservationKind,
"observations": [],
Expand Down Expand Up @@ -575,11 +577,11 @@ def test_sets_total_size_limitwith_integer(self, db_request):
pretend.call("Set the total size limit on 'foo'", queue="success")
]

assert project.total_size_limit == 150 * views.ONE_GB
assert project.total_size_limit == 150 * views.ONE_GIB

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

db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/projects/"
Expand Down Expand Up @@ -627,7 +629,7 @@ def test_sets_limitwith_integer(self, db_request):
flash=pretend.call_recorder(lambda *a, **kw: None)
)
db_request.matchdict["project_name"] = project.normalized_name
new_upload_limit = views.MAX_FILESIZE // views.ONE_MB
new_upload_limit = warehouse.constants.MAX_FILESIZE // views.ONE_MIB
db_request.POST["upload_limit"] = str(new_upload_limit)

views.set_upload_limit(project, db_request)
Expand All @@ -636,11 +638,11 @@ def test_sets_limitwith_integer(self, db_request):
pretend.call("Set the upload limit on 'foo'", queue="success")
]

assert project.upload_limit == new_upload_limit * views.ONE_MB
assert project.upload_limit == new_upload_limit * views.ONE_MIB

def test_sets_limit_with_none(self, db_request):
project = ProjectFactory.create(name="foo")
project.upload_limit = 90 * views.ONE_MB
project.upload_limit = 90 * views.ONE_MIB

db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/projects/"
Expand Down
42 changes: 25 additions & 17 deletions tests/unit/forklift/test_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
from trove_classifiers import classifiers
from webob.multidict import MultiDict

import warehouse.constants

from warehouse.accounts.utils import UserContext
from warehouse.admin.flags import AdminFlag, AdminFlagValue
from warehouse.classifiers.models import Classifier
Expand Down Expand Up @@ -236,7 +238,9 @@ def test_zipfile_exceeds_compression_threshold(self, tmpdir):

with zipfile.ZipFile(f, "w") as zfp:
zfp.writestr("PKG-INFO", b"this is the package info")
zfp.writestr("1.dat", b"0" * 65 * legacy.ONE_MB, zipfile.ZIP_DEFLATED)
zfp.writestr(
"1.dat", b"0" * 65 * warehouse.constants.ONE_MIB, zipfile.ZIP_DEFLATED
)

assert not legacy._is_valid_dist_file(f, "")

Expand Down Expand Up @@ -1626,8 +1630,8 @@ def test_upload_fails_with_too_large_project_size_default_limit(
EmailFactory.create(user=user)
project = ProjectFactory.create(
name="foobar",
upload_limit=legacy.MAX_FILESIZE,
total_size=legacy.MAX_PROJECT_SIZE - 1,
upload_limit=warehouse.constants.MAX_FILESIZE,
total_size=warehouse.constants.MAX_PROJECT_SIZE - 1,
)
release = ReleaseFactory.create(project=project, version="1.0")
RoleFactory.create(user=user, project=project)
Expand Down Expand Up @@ -1673,10 +1677,13 @@ def test_upload_fails_with_too_large_project_size_custom_limit(
one_megabyte = 1 * 1024 * 1024
project = ProjectFactory.create(
name="foobar",
upload_limit=legacy.MAX_FILESIZE,
total_size=legacy.MAX_PROJECT_SIZE,
total_size_limit=legacy.MAX_PROJECT_SIZE
+ one_megabyte, # Custom Limit for the project
upload_limit=warehouse.constants.MAX_FILESIZE,
total_size=warehouse.constants.MAX_PROJECT_SIZE,
total_size_limit=(
warehouse.constants.MAX_PROJECT_SIZE
+ one_megabyte
# Custom Limit for the project
),
)
release = ReleaseFactory.create(project=project, version="1.0")
RoleFactory.create(user=user, project=project)
Expand Down Expand Up @@ -1726,10 +1733,11 @@ def test_upload_succeeds_custom_project_size_limit(
one_megabyte = 1 * 1024 * 1024
project = ProjectFactory.create(
name="foobar",
upload_limit=legacy.MAX_FILESIZE,
total_size=legacy.MAX_PROJECT_SIZE,
total_size_limit=legacy.MAX_PROJECT_SIZE
+ (one_megabyte * 60), # Custom Limit for the project
upload_limit=warehouse.constants.MAX_FILESIZE,
total_size=warehouse.constants.MAX_PROJECT_SIZE,
total_size_limit=(
warehouse.constants.MAX_PROJECT_SIZE + (one_megabyte * 60)
), # Custom Limit for the project
)
release = ReleaseFactory.create(project=project, version="1.0")
RoleFactory.create(user=user, project=project)
Expand Down Expand Up @@ -2167,7 +2175,7 @@ def test_upload_fails_with_invalid_filetype(
}[filetype],
"content": pretend.stub(
filename=filename,
file=io.BytesIO(b"a" * (legacy.MAX_FILESIZE + 1)),
file=io.BytesIO(b"a" * (warehouse.constants.MAX_FILESIZE + 1)),
type="application/tar",
),
}
Expand Down Expand Up @@ -2205,7 +2213,7 @@ def test_upload_fails_with_invalid_extension(self, pyramid_config, db_request):
"md5_digest": "nope!",
"content": pretend.stub(
filename=filename,
file=io.BytesIO(b"a" * (legacy.MAX_FILESIZE + 1)),
file=io.BytesIO(b"a" * (warehouse.constants.MAX_FILESIZE + 1)),
type="application/tar",
),
}
Expand Down Expand Up @@ -2246,7 +2254,7 @@ def test_upload_fails_with_unsafe_filename(
"md5_digest": "nope!",
"content": pretend.stub(
filename=filename,
file=io.BytesIO(b"a" * (legacy.MAX_FILESIZE + 1)),
file=io.BytesIO(b"a" * (warehouse.constants.MAX_FILESIZE + 1)),
type="application/tar",
),
}
Expand Down Expand Up @@ -2283,7 +2291,7 @@ def test_upload_fails_with_disallowed_in_filename(
"md5_digest": "nope!",
"content": pretend.stub(
filename=filename,
file=io.BytesIO(b"a" * (legacy.MAX_FILESIZE + 1)),
file=io.BytesIO(b"a" * (warehouse.constants.MAX_FILESIZE + 1)),
type="application/tar",
),
}
Expand Down Expand Up @@ -2322,7 +2330,7 @@ def test_upload_fails_without_user_permission(self, pyramid_config, db_request):
"md5_digest": "nope!",
"content": pretend.stub(
filename=filename,
file=io.BytesIO(b"a" * (legacy.MAX_FILESIZE + 1)),
file=io.BytesIO(b"a" * (warehouse.constants.MAX_FILESIZE + 1)),
type="application/tar",
),
}
Expand Down Expand Up @@ -2364,7 +2372,7 @@ def test_upload_fails_without_oidc_publisher_permission(
"md5_digest": "nope!",
"content": pretend.stub(
filename=filename,
file=io.BytesIO(b"a" * (legacy.MAX_FILESIZE + 1)),
file=io.BytesIO(b"a" * (warehouse.constants.MAX_FILESIZE + 1)),
type="application/tar",
),
}
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/manage/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
TokenExpired,
)
from warehouse.admin.flags import AdminFlagValue
from warehouse.constants import MAX_FILESIZE, MAX_PROJECT_SIZE
from warehouse.events.tags import EventTag
from warehouse.forklift.legacy import MAX_FILESIZE, MAX_PROJECT_SIZE
from warehouse.macaroons import caveats
from warehouse.macaroons.interfaces import IMacaroonService
from warehouse.manage import views
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ def __init__(self):
"reconcile_file_storages.batch_size": 100,
"metadata_backfill.batch_size": 500,
"gcloud.service_account_info": {},
"warehouse.forklift.legacy.MAX_FILESIZE_MIB": 100,
"warehouse.forklift.legacy.MAX_PROJECT_SIZE_GIB": 10,
}
if environment == config.Environment.development:
expected_settings.update(
Expand Down
24 changes: 11 additions & 13 deletions warehouse/admin/views/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,16 @@
from warehouse.accounts.interfaces import IUserService
from warehouse.accounts.models import User
from warehouse.authnz import Permissions
from warehouse.constants import MAX_FILESIZE, MAX_PROJECT_SIZE, ONE_GIB, ONE_MIB
from warehouse.events.tags import EventTag
from warehouse.forklift.legacy import MAX_FILESIZE, MAX_PROJECT_SIZE
from warehouse.observations.models import OBSERVATION_KIND_MAP, ObservationKind
from warehouse.packaging.models import JournalEntry, Project, Release, Role
from warehouse.packaging.tasks import update_release_description
from warehouse.search.tasks import reindex_project as _reindex_project
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
UPLOAD_LIMIT_CAP = 1073741824 # 1 GiB
UPLOAD_LIMIT_CAP = ONE_GIB


@view_config(
Expand Down Expand Up @@ -142,9 +140,9 @@ def project_detail(project, request):
"maintainers": maintainers,
"journal": journal,
"oidc_publishers": project.oidc_publishers,
"ONE_MB": ONE_MB,
"ONE_MIB": ONE_MIB,
"MAX_FILESIZE": MAX_FILESIZE,
"ONE_GB": ONE_GB,
"ONE_GIB": ONE_GIB,
"MAX_PROJECT_SIZE": MAX_PROJECT_SIZE,
"UPLOAD_LIMIT_CAP": UPLOAD_LIMIT_CAP,
"observation_kinds": ObservationKind,
Expand Down Expand Up @@ -476,19 +474,19 @@ def set_upload_limit(project, request):
f"must be integer or empty string."
)

# The form is in MB, but the database field is in bytes.
upload_limit *= ONE_MB
# The form is in MiB, but the database field is in bytes.
upload_limit *= ONE_MIB

if upload_limit > UPLOAD_LIMIT_CAP:
raise HTTPBadRequest(
f"Upload limit can not be more than the overall limit of "
f"{UPLOAD_LIMIT_CAP / ONE_MB}MiB."
f"{UPLOAD_LIMIT_CAP / ONE_MIB}MiB."
)

if upload_limit < MAX_FILESIZE:
raise HTTPBadRequest(
f"Upload limit can not be less than the default limit of "
f"{MAX_FILESIZE / ONE_MB}MB."
f"{MAX_FILESIZE / ONE_MIB}MiB."
)

project.upload_limit = upload_limit
Expand Down Expand Up @@ -521,13 +519,13 @@ def set_total_size_limit(project, request):
f"must be integer or empty string."
)

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

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."
f"{MAX_PROJECT_SIZE / ONE_GIB}GiB."
)

project.total_size_limit = total_size_limit
Expand Down
5 changes: 5 additions & 0 deletions warehouse/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pyramid_rpc.xmlrpc import XMLRPCRenderer

from warehouse.authnz import Permissions
from warehouse.constants import MAX_FILESIZE, MAX_PROJECT_SIZE, ONE_GIB, ONE_MIB
from warehouse.utils.static import ManifestCacheBuster
from warehouse.utils.wsgi import ProxyFixer, VhmRootRemover

Expand Down Expand Up @@ -216,6 +217,10 @@ def from_base64_encoded_json(configuration):
def configure(settings=None):
if settings is None:
settings = {}
settings["warehouse.forklift.legacy.MAX_FILESIZE_MIB"] = MAX_FILESIZE / ONE_MIB
settings["warehouse.forklift.legacy.MAX_PROJECT_SIZE_GIB"] = (
MAX_PROJECT_SIZE / ONE_GIB
)

# Allow configuring the log level. See `warehouse/logging.py` for more
maybe_set(settings, "logging.level", "LOG_LEVEL")
Expand Down
17 changes: 17 additions & 0 deletions warehouse/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 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.

ONE_MIB = 1 * 1024 * 1024
ONE_GIB = 1 * 1024 * 1024 * 1024
MAX_FILESIZE = 100 * ONE_MIB
MAX_SIGSIZE = 8 * 1024
MAX_PROJECT_SIZE = 10 * ONE_GIB
13 changes: 4 additions & 9 deletions warehouse/forklift/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from warehouse.admin.flags import AdminFlagValue
from warehouse.authnz import Permissions
from warehouse.classifiers.models import Classifier
from warehouse.constants import MAX_FILESIZE, MAX_PROJECT_SIZE, ONE_GIB, ONE_MIB
from warehouse.email import (
send_api_token_used_in_trusted_publisher_project_email,
send_two_factor_not_yet_enabled_email,
Expand All @@ -70,15 +71,9 @@
from warehouse.rate_limiting.interfaces import RateLimiterException
from warehouse.utils import readme

ONE_MB = 1 * 1024 * 1024
ONE_GB = 1 * 1024 * 1024 * 1024

MAX_FILESIZE = 100 * ONE_MB
MAX_PROJECT_SIZE = 10 * ONE_GB

PATH_HASHER = "blake2_256"

COMPRESSION_RATIO_MIN_SIZE = 64 * ONE_MB
COMPRESSION_RATIO_MIN_SIZE = 64 * ONE_MIB

# If the zip file decompressed to 50x more space
# than it is uncompressed, consider it a ZIP bomb.
Expand Down Expand Up @@ -853,7 +848,7 @@ def file_upload(request):
HTTPBadRequest,
"File too large. "
+ "Limit for project {name!r} is {limit} MB. ".format(
name=project.name, limit=file_size_limit // ONE_MB
name=project.name, limit=file_size_limit // ONE_MIB
)
+ "See "
+ request.help_url(_anchor="file-size-limit")
Expand All @@ -864,7 +859,7 @@ def file_upload(request):
HTTPBadRequest,
"Project size too large. Limit for "
+ "project {name!r} total size is {limit} GB. ".format(
name=project.name, limit=project_size_limit // ONE_GB
name=project.name, limit=project_size_limit // ONE_GIB
)
+ "See "
+ request.help_url(_anchor="project-size-limit"),
Expand Down
Loading