Skip to content

Commit e64b005

Browse files
committed
Storage updates
- Delete from storage when a project or version is deleted - Project delete now handled in a method on the model - Define media type constants (next step: use them everywhere) - A few places still using default storage instead of RTD_BUILD_MEDIA_STORAGE
1 parent 0bac470 commit e64b005

File tree

9 files changed

+124
-27
lines changed

9 files changed

+124
-27
lines changed

readthedocs/builds/models.py

+25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
GITLAB_URL,
2525
PRIVACY_CHOICES,
2626
PRIVATE,
27+
MEDIA_TYPES,
2728
)
2829
from readthedocs.projects.models import APIProject, Project
2930
from readthedocs.projects.version_handling import determine_stable_version
@@ -259,6 +260,11 @@ def delete(self, *args, **kwargs): # pylint: disable=arguments-differ
259260
task=tasks.remove_dirs,
260261
args=[self.get_artifact_paths()],
261262
)
263+
264+
# Remove build artifacts from storage
265+
storage_paths = self.get_storage_paths()
266+
tasks.remove_build_storage_paths.delay(storage_paths)
267+
262268
project_pk = self.project.pk
263269
super().delete(*args, **kwargs)
264270
broadcast(
@@ -340,6 +346,25 @@ def get_artifact_paths(self):
340346

341347
return paths
342348

349+
def get_storage_paths(self):
350+
"""
351+
Return a list of all build artifact storage paths for this version.
352+
353+
:rtype: list
354+
"""
355+
paths = []
356+
357+
for type_ in MEDIA_TYPES:
358+
paths.append(
359+
self.project.get_storage_path(
360+
type_=type_,
361+
version_slug=self.slug,
362+
include_file=False,
363+
)
364+
)
365+
366+
return paths
367+
343368
def clean_build_path(self):
344369
"""
345370
Clean build path for project version.

readthedocs/builds/storage.py

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
from pathlib import Path
33

4+
from django.core.exceptions import SuspiciousFileOperation
45
from django.core.files.storage import FileSystemStorage
56
from storages.utils import safe_join, get_available_overwrite_name
67

@@ -51,6 +52,9 @@ def delete_directory(self, path):
5152
5253
:param path: the path to the directory to remove
5354
"""
55+
if path in ('', '/'):
56+
raise SuspiciousFileOperation('Deleting all storage cannot be right')
57+
5458
log.debug('Deleting directory %s from media storage', path)
5559
folders, files = self.listdir(self._dirpath(path))
5660
for folder_name in folders:

readthedocs/builds/syncers.py

-2
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@
1010
import shutil
1111

1212
from django.conf import settings
13-
from django.core.files.storage import get_storage_class
1413

1514
from readthedocs.core.utils import safe_makedirs
1615
from readthedocs.core.utils.extend import SettingsOverrideObject
1716

1817

1918
log = logging.getLogger(__name__)
20-
storage = get_storage_class()()
2119

2220

2321
class BaseSyncer:

readthedocs/projects/constants.py

+13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@
1919
('sphinx_singlehtml', _('Sphinx Single Page HTML')),
2020
)
2121

22+
MEDIA_TYPE_HTML = 'html'
23+
MEDIA_TYPE_PDF = 'pdf'
24+
MEDIA_TYPE_EPUB = 'epub'
25+
MEDIA_TYPE_HTMLZIP = 'htmlzip'
26+
MEDIA_TYPE_JSON = 'json'
27+
MEDIA_TYPES = (
28+
MEDIA_TYPE_HTML,
29+
MEDIA_TYPE_PDF,
30+
MEDIA_TYPE_EPUB,
31+
MEDIA_TYPE_HTMLZIP,
32+
MEDIA_TYPE_JSON,
33+
)
34+
2235
SAMPLE_FILES = (
2336
('Installation', 'projects/samples/installation.rst.html'),
2437
('Getting started', 'projects/samples/getting_started.rst.html'),

readthedocs/projects/models.py

+58-13
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@
4242
from readthedocs.vcs_support.backends import backend_cls
4343
from readthedocs.vcs_support.utils import Lock, NonBlockingLock
4444

45+
from .constants import MEDIA_TYPES
46+
4547

4648
log = logging.getLogger(__name__)
47-
storage = get_storage_class()()
4849

4950

5051
class ProjectRelationship(models.Model):
@@ -463,6 +464,29 @@ def save(self, *args, **kwargs): # pylint: disable=arguments-differ
463464
except Exception:
464465
log.exception('Error creating default branches')
465466

467+
def delete(self, *args, **kwargs): # pylint: disable=arguments-differ
468+
from readthedocs.projects import tasks
469+
470+
# Remove local FS build artifacts on the web servers
471+
broadcast(
472+
type='app',
473+
task=tasks.remove_dirs,
474+
args=[(self.doc_path,)],
475+
)
476+
477+
# Remove build artifacts from storage
478+
storage_paths = []
479+
for type_ in MEDIA_TYPES:
480+
storage_paths.append(
481+
'{}/{}'.format(
482+
type_,
483+
self.slug,
484+
)
485+
)
486+
tasks.remove_build_storage_paths.delay(storage_paths)
487+
488+
super().delete(*args, **kwargs)
489+
466490
def get_absolute_url(self):
467491
return reverse('projects_detail', args=[self.slug])
468492

@@ -750,28 +774,49 @@ def has_pdf(self, version_slug=LATEST):
750774
path = self.get_production_media_path(
751775
type_='pdf', version_slug=version_slug
752776
)
753-
storage_path = self.get_storage_path(
754-
type_='pdf', version_slug=version_slug
755-
)
756-
return os.path.exists(path) or storage.exists(storage_path)
777+
if os.path.exists(path):
778+
return True
779+
780+
if settings.RTD_BUILD_MEDIA_STORAGE:
781+
storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()
782+
storage_path = self.get_storage_path(
783+
type_='pdf', version_slug=version_slug
784+
)
785+
return storage.exists(storage_path)
786+
787+
return False
757788

758789
def has_epub(self, version_slug=LATEST):
759790
path = self.get_production_media_path(
760791
type_='epub', version_slug=version_slug
761792
)
762-
storage_path = self.get_storage_path(
763-
type_='epub', version_slug=version_slug
764-
)
765-
return os.path.exists(path) or storage.exists(storage_path)
793+
if os.path.exists(path):
794+
return True
795+
796+
if settings.RTD_BUILD_MEDIA_STORAGE:
797+
storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()
798+
storage_path = self.get_storage_path(
799+
type_='epub', version_slug=version_slug
800+
)
801+
return storage.exists(storage_path)
802+
803+
return False
766804

767805
def has_htmlzip(self, version_slug=LATEST):
768806
path = self.get_production_media_path(
769807
type_='htmlzip', version_slug=version_slug
770808
)
771-
storage_path = self.get_storage_path(
772-
type_='htmlzip', version_slug=version_slug
773-
)
774-
return os.path.exists(path) or storage.exists(storage_path)
809+
if os.path.exists(path):
810+
return True
811+
812+
if settings.RTD_BUILD_MEDIA_STORAGE:
813+
storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()
814+
storage_path = self.get_storage_path(
815+
type_='htmlzip', version_slug=version_slug
816+
)
817+
return storage.exists(storage_path)
818+
819+
return False
775820

776821
@property
777822
def sponsored(self):

readthedocs/projects/tasks.py

+14
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,20 @@ def remove_dirs(paths):
16801680
shutil.rmtree(path, ignore_errors=True)
16811681

16821682

1683+
@app.task()
1684+
def remove_build_storage_paths(paths):
1685+
"""
1686+
Remove artifacts from build media storage (cloud or local storage)
1687+
1688+
:param paths: list of paths in build media storage to delete
1689+
"""
1690+
if settings.RTD_BUILD_MEDIA_STORAGE:
1691+
storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()
1692+
for storage_path in paths:
1693+
log.info('Removing %s from media storage', storage_path)
1694+
storage.delete_directory(storage_path)
1695+
1696+
16831697
@app.task(queue='web')
16841698
def sync_callback(_, version_pk, commit, *args, **kwargs):
16851699
"""

readthedocs/projects/views/private.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,7 @@ def project_delete(request, project_slug):
210210
)
211211

212212
if request.method == 'POST':
213-
broadcast(
214-
type='app',
215-
task=tasks.remove_dirs,
216-
args=[(project.doc_path,)],
217-
)
213+
# Delete the project and all related files
218214
project.delete()
219215
messages.success(request, _('Project deleted'))
220216
project_dashboard = reverse('projects_dashboard')

readthedocs/projects/views/public.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
log = logging.getLogger(__name__)
3434
search_log = logging.getLogger(__name__ + '.search')
3535
mimetypes.add_type('application/epub+zip', '.epub')
36-
storage = get_storage_class()()
3736

3837

3938
class ProjectIndex(ListView):
@@ -217,11 +216,14 @@ def project_download_media(request, project_slug, type_, version_slug):
217216
)
218217

219218
if settings.DEFAULT_PRIVACY_LEVEL == 'public' or settings.DEBUG:
220-
storage_path = version.project.get_storage_path(
221-
type_=type_, version_slug=version_slug
222-
)
223-
if storage.exists(storage_path):
224-
return HttpResponseRedirect(storage.url(storage_path))
219+
220+
if settings.RTD_BUILD_MEDIA_STORAGE:
221+
storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()
222+
storage_path = version.project.get_storage_path(
223+
type_=type_, version_slug=version_slug
224+
)
225+
if storage.exists(storage_path):
226+
return HttpResponseRedirect(storage.url(storage_path))
225227

226228
media_path = os.path.join(
227229
settings.MEDIA_URL,

readthedocs/rtd_tests/tests/test_project_views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def test_delete_project(self):
404404
response = self.client.get('/dashboard/pip/delete/')
405405
self.assertEqual(response.status_code, 200)
406406

407-
with patch('readthedocs.projects.views.private.broadcast') as broadcast:
407+
with patch('readthedocs.projects.models.broadcast') as broadcast:
408408
response = self.client.post('/dashboard/pip/delete/')
409409
self.assertEqual(response.status_code, 302)
410410
self.assertFalse(Project.objects.filter(slug='pip').exists())

0 commit comments

Comments
 (0)