Skip to content

Commit 7d40ba9

Browse files
humitosstsewd
andauthored
Build: rename PDF/ePUB filename to valid one automatically (#11198)
* Build: rename PDF/ePUB filename to valid one automatically Currently, we _require_ the user to save the PDF or ePUB file with a pretty specific filename: `{project.slug}-{version.slug}.{extension}`. This has caused a lot of confusions to users. Since we only support 1 file for PDF/ePUB for now, we can rename this file to the valid filename automatically. Closes #10873 * Rename the offline format file properly * Initial attempt to write a test case * Revert "Initial attempt to write a test case" This reverts commit 76ab170. * Test case and filename fix * Remove the conditional that's not needed * Assert the path is inside OUTPUT dir before calling `shutil.move` * Revert "Assert the path is inside OUTPUT dir before calling `shutil.move`" This reverts commit e9a0f08. * Assert the path is inside the DOCROOT before calling `shutil.move` * Update readthedocs/projects/tasks/builds.py Co-authored-by: Santos Gallegos <[email protected]> --------- Co-authored-by: Santos Gallegos <[email protected]>
1 parent e0a45ef commit 7d40ba9

File tree

3 files changed

+60
-2
lines changed

3 files changed

+60
-2
lines changed

readthedocs/core/utils/filesystem.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def assert_path_is_inside_docroot(path):
4242
log.error(
4343
"Suspicious operation outside the docroot directory.",
4444
path_resolved=str(resolved_path),
45+
docroot=settings.DOCROOT,
4546
)
4647
raise SuspiciousFileOperation(path)
4748

readthedocs/projects/tasks/builds.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
rebuilding documentation.
66
"""
77
import os
8+
import shutil
89
import signal
910
import socket
1011
import subprocess
1112
from dataclasses import dataclass, field
13+
from pathlib import Path
1214

1315
import structlog
1416
from celery import Task
@@ -40,6 +42,7 @@
4042
from readthedocs.builds.utils import memcache_lock
4143
from readthedocs.config.config import BuildConfigV2
4244
from readthedocs.config.exceptions import ConfigError
45+
from readthedocs.core.utils.filesystem import assert_path_is_inside_docroot
4346
from readthedocs.doc_builder.director import BuildDirector
4447
from readthedocs.doc_builder.environments import (
4548
DockerBuildEnvironment,
@@ -605,7 +608,8 @@ def get_valid_artifact_types(self):
605608
# These output format does not support multiple files yet.
606609
# In case multiple files are found, the upload for this format is not performed.
607610
if artifact_type in ARTIFACT_TYPES_WITHOUT_MULTIPLE_FILES_SUPPORT:
608-
artifact_format_files = len(os.listdir(artifact_directory))
611+
list_dir = os.listdir(artifact_directory)
612+
artifact_format_files = len(list_dir)
609613
if artifact_format_files > 1:
610614
log.error(
611615
"Multiple files are not supported for this format. "
@@ -626,6 +630,18 @@ def get_valid_artifact_types(self):
626630
},
627631
)
628632

633+
# Rename file as "<project_slug>-<version_slug>.<artifact_type>",
634+
# which is the filename that Proxito serves for offline formats.
635+
filename = list_dir[0]
636+
_, extension = filename.rsplit(".")
637+
path = Path(artifact_directory) / filename
638+
destination = (
639+
Path(artifact_directory) / f"{self.data.project.slug}.{extension}"
640+
)
641+
assert_path_is_inside_docroot(path)
642+
assert_path_is_inside_docroot(destination)
643+
shutil.move(path, destination)
644+
629645
# If all the conditions were met, the artifact is valid
630646
valid_artifacts.append(artifact_type)
631647

readthedocs/projects/tests/test_build_tasks.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import os
22
import pathlib
33
import textwrap
4+
import uuid
5+
from pathlib import Path
46
from unittest import mock
57

68
import django_dynamic_fixture as fixture
79
import pytest
810
from django.conf import settings
11+
from django.test.utils import override_settings
912

1013
from readthedocs.builds.constants import (
1114
BUILD_STATUS_FAILURE,
@@ -260,6 +263,7 @@ def test_build_respects_formats_mkdocs(self, build_docs_class, load_yaml_config)
260263

261264
build_docs_class.assert_called_once_with("mkdocs") # HTML builder
262265

266+
@override_settings(DOCROOT="/tmp/readthedocs-tests/git-repository/")
263267
@mock.patch("readthedocs.doc_builder.director.load_yaml_config")
264268
def test_build_updates_documentation_type(self, load_yaml_config):
265269
assert self.version.documentation_type == "sphinx"
@@ -403,6 +407,8 @@ def test_get_env_vars(self, load_yaml_config, build_environment, config, externa
403407
expected_build_env_vars["PRIVATE_TOKEN"] = "a1b2c3"
404408
assert build_env_vars == expected_build_env_vars
405409

410+
@override_settings(DOCROOT="/tmp/readthedocs-tests/git-repository/")
411+
@mock.patch("readthedocs.projects.tasks.builds.shutil")
406412
@mock.patch("readthedocs.projects.tasks.builds.index_build")
407413
@mock.patch("readthedocs.projects.tasks.builds.build_complete")
408414
@mock.patch("readthedocs.projects.tasks.builds.send_external_build_status")
@@ -417,6 +423,7 @@ def test_successful_build(
417423
send_external_build_status,
418424
build_complete,
419425
index_build,
426+
shutilmock,
420427
):
421428
load_yaml_config.return_value = get_build_config(
422429
{
@@ -433,12 +440,16 @@ def test_successful_build(
433440
# Create the artifact paths, so it's detected by the builder
434441
os.makedirs(self.project.artifact_path(version=self.version.slug, type_="html"))
435442
os.makedirs(self.project.artifact_path(version=self.version.slug, type_="json"))
443+
filename = str(uuid.uuid4())
436444
for f in ("htmlzip", "epub", "pdf"):
445+
extension = "zip" if f == "htmlzip" else f
437446
os.makedirs(self.project.artifact_path(version=self.version.slug, type_=f))
438447
pathlib.Path(
439448
os.path.join(
440449
self.project.artifact_path(version=self.version.slug, type_=f),
441-
f"{self.project.slug}.{f}",
450+
# Use a random name for the offline format.
451+
# We will automatically rename this file to filename El Proxito expects.
452+
f"{filename}.{extension}",
442453
)
443454
).touch()
444455

@@ -452,6 +463,36 @@ def test_successful_build(
452463

453464
self._trigger_update_docs_task()
454465

466+
# Offline formats were renamed to the correct filename.
467+
shutilmock.move.assert_has_calls(
468+
[
469+
mock.call(
470+
Path(
471+
f"/tmp/readthedocs-tests/git-repository/_readthedocs/htmlzip/{filename}.zip"
472+
),
473+
Path(
474+
f"/tmp/readthedocs-tests/git-repository/_readthedocs/htmlzip/{self.project.slug}.zip"
475+
),
476+
),
477+
mock.call(
478+
Path(
479+
f"/tmp/readthedocs-tests/git-repository/_readthedocs/pdf/{filename}.pdf"
480+
),
481+
Path(
482+
f"/tmp/readthedocs-tests/git-repository/_readthedocs/pdf/{self.project.slug}.pdf"
483+
),
484+
),
485+
mock.call(
486+
Path(
487+
f"/tmp/readthedocs-tests/git-repository/_readthedocs/epub/{filename}.epub"
488+
),
489+
Path(
490+
f"/tmp/readthedocs-tests/git-repository/_readthedocs/epub/{self.project.slug}.epub"
491+
),
492+
),
493+
]
494+
)
495+
455496
# It has to be called twice, ``before_start`` and ``after_return``
456497
clean_build.assert_has_calls(
457498
[mock.call(mock.ANY), mock.call(mock.ANY)] # the argument is an APIVersion

0 commit comments

Comments
 (0)