Skip to content

Commit c51e96e

Browse files
authored
Merge pull request readthedocs#5698 from rtfd/davidfischer/storage-delete
Storage updates
2 parents ae14393 + b959218 commit c51e96e

File tree

9 files changed

+114
-35
lines changed

9 files changed

+114
-35
lines changed

readthedocs/builds/models.py

+25
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
GITLAB_URL,
2828
PRIVACY_CHOICES,
2929
PRIVATE,
30+
MEDIA_TYPES,
3031
)
3132
from readthedocs.projects.models import APIProject, Project
3233
from readthedocs.projects.version_handling import determine_stable_version
@@ -262,6 +263,11 @@ def delete(self, *args, **kwargs): # pylint: disable=arguments-differ
262263
task=tasks.remove_dirs,
263264
args=[self.get_artifact_paths()],
264265
)
266+
267+
# Remove build artifacts from storage
268+
storage_paths = self.get_storage_paths()
269+
tasks.remove_build_storage_paths.delay(storage_paths)
270+
265271
project_pk = self.project.pk
266272
super().delete(*args, **kwargs)
267273
broadcast(
@@ -343,6 +349,25 @@ def get_artifact_paths(self):
343349

344350
return paths
345351

352+
def get_storage_paths(self):
353+
"""
354+
Return a list of all build artifact storage paths for this version.
355+
356+
:rtype: list
357+
"""
358+
paths = []
359+
360+
for type_ in MEDIA_TYPES:
361+
paths.append(
362+
self.project.get_storage_path(
363+
type_=type_,
364+
version_slug=self.slug,
365+
include_file=False,
366+
)
367+
)
368+
369+
return paths
370+
346371
def clean_build_path(self):
347372
"""
348373
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

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

45+
from .constants import (
46+
MEDIA_TYPES,
47+
MEDIA_TYPE_PDF,
48+
MEDIA_TYPE_EPUB,
49+
MEDIA_TYPE_HTMLZIP,
50+
)
51+
4552

4653
log = logging.getLogger(__name__)
47-
storage = get_storage_class()()
4854

4955

5056
class ProjectRelationship(models.Model):
@@ -463,6 +469,29 @@ def save(self, *args, **kwargs): # pylint: disable=arguments-differ
463469
except Exception:
464470
log.exception('Error creating default branches')
465471

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

@@ -746,32 +775,30 @@ def has_versions(self):
746775
def has_aliases(self):
747776
return self.aliases.exists()
748777

749-
def has_pdf(self, version_slug=LATEST):
778+
def has_media(self, type_, version_slug=LATEST):
750779
path = self.get_production_media_path(
751-
type_='pdf', version_slug=version_slug
752-
)
753-
storage_path = self.get_storage_path(
754-
type_='pdf', version_slug=version_slug
780+
type_=type_, version_slug=version_slug
755781
)
756-
return os.path.exists(path) or storage.exists(storage_path)
782+
if os.path.exists(path):
783+
return True
784+
785+
if settings.RTD_BUILD_MEDIA_STORAGE:
786+
storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()
787+
storage_path = self.get_storage_path(
788+
type_=type_, version_slug=version_slug
789+
)
790+
return storage.exists(storage_path)
791+
792+
return False
793+
794+
def has_pdf(self, version_slug=LATEST):
795+
return self.has_media(MEDIA_TYPE_PDF, version_slug=version_slug)
757796

758797
def has_epub(self, version_slug=LATEST):
759-
path = self.get_production_media_path(
760-
type_='epub', version_slug=version_slug
761-
)
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)
798+
return self.has_media(MEDIA_TYPE_EPUB, version_slug=version_slug)
766799

767800
def has_htmlzip(self, version_slug=LATEST):
768-
path = self.get_production_media_path(
769-
type_='htmlzip', version_slug=version_slug
770-
)
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)
801+
return self.has_media(MEDIA_TYPE_HTMLZIP, version_slug=version_slug)
775802

776803
@property
777804
def sponsored(self):

readthedocs/projects/tasks.py

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

16611661

1662+
@app.task(queue='web')
1663+
def remove_build_storage_paths(paths):
1664+
"""
1665+
Remove artifacts from build media storage (cloud or local storage)
1666+
1667+
:param paths: list of paths in build media storage to delete
1668+
"""
1669+
if settings.RTD_BUILD_MEDIA_STORAGE:
1670+
storage = get_storage_class(settings.RTD_BUILD_MEDIA_STORAGE)()
1671+
for storage_path in paths:
1672+
log.info('Removing %s from media storage', storage_path)
1673+
storage.delete_directory(storage_path)
1674+
1675+
16621676
@app.task(queue='web')
16631677
def sync_callback(_, version_pk, commit, *args, **kwargs):
16641678
"""

readthedocs/projects/views/private.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,7 @@ def project_delete(request, project_slug):
215215
}
216216

217217
if request.method == 'POST':
218-
broadcast(
219-
type='app',
220-
task=tasks.remove_dirs,
221-
args=[(project.doc_path,)],
222-
)
218+
# Delete the project and all related files
223219
project.delete()
224220
messages.success(request, _('Project deleted'))
225221
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)