Skip to content

Commit

Permalink
Backend changes to handle serving content from a remote URL.
Browse files Browse the repository at this point in the history
  • Loading branch information
rtibbles committed May 6, 2021
1 parent 870d334 commit e131cda
Show file tree
Hide file tree
Showing 11 changed files with 36 additions and 191 deletions.
41 changes: 13 additions & 28 deletions kolibri/core/content/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import requests
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.db.models import Exists
from django.db.models import OuterRef
from django.db.models import Q
Expand Down Expand Up @@ -52,7 +51,6 @@
get_channel_stats_from_studio,
)
from kolibri.core.content.utils.paths import get_channel_lookup_url
from kolibri.core.content.utils.paths import get_content_file_name
from kolibri.core.content.utils.paths import get_info_url
from kolibri.core.content.utils.paths import get_local_content_storage_file_url
from kolibri.core.content.utils.stopwords import stopwords_set
Expand Down Expand Up @@ -231,25 +229,18 @@ def map_lang(obj):
return output


def map_file(file, obj):
url_lookup = {
"available": file["available"],
"id": file["checksum"],
"extension": file["extension"],
}
download_filename = models.get_download_filename(
obj["title"],
models.PRESET_LOOKUP.get(file["preset"], _("Unknown format")),
file["extension"],
)
file["download_url"] = reverse(
"kolibri:core:downloadcontent",
kwargs={
"filename": get_content_file_name(url_lookup),
"new_filename": download_filename,
},
def map_file(file):
file["checksum"] = file.pop("local_file__id")
file["available"] = file.pop("local_file__available")
file["file_size"] = file.pop("local_file__file_size")
file["extension"] = file.pop("local_file__extension")
file["storage_url"] = get_local_content_storage_file_url(
{
"available": file["available"],
"id": file["checksum"],
"extension": file["extension"],
}
)
file["storage_url"] = get_local_content_storage_file_url(url_lookup)
file["lang"] = map_lang(file)
return file

Expand Down Expand Up @@ -331,11 +322,7 @@ def consolidate(self, items, queryset):
):
if f["contentnode"] not in files:
files[f["contentnode"]] = []
f["checksum"] = f.pop("local_file__id")
f["available"] = f.pop("local_file__available")
f["file_size"] = f.pop("local_file__file_size")
f["extension"] = f.pop("local_file__extension")
files[f["contentnode"]].append(f)
files[f["contentnode"]].append(map_file(f))

ancestors = queryset.get_ancestors().values(
"id", "title", "lft", "rght", "tree_id"
Expand All @@ -356,10 +343,8 @@ def consolidate(self, items, queryset):

for item in items:
item["assessmentmetadata"] = assessmentmetadata.get(item["id"])
item["files"] = list(
map(lambda x: map_file(x, item), files.get(item["id"], []))
)
item["tags"] = tags.get(item["id"], [])
item["files"] = files.get(item["id"], [])

lft = item.pop("lft")
rght = item.pop("rght")
Expand Down
4 changes: 4 additions & 0 deletions kolibri/core/content/management/commands/exportcontent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
import os

from django.core.management.base import CommandError

from ...utils import paths
from ...utils import transfer
from kolibri.core.content.errors import InvalidStorageFilenameError
Expand Down Expand Up @@ -60,6 +62,8 @@ def update_job_metadata(self, total_bytes_to_transfer, total_resource_count):
job.save_meta()

def handle_async(self, *args, **options):
if paths.using_remote_storage():
raise CommandError("Cannot export files when using remote file storage")
channel_id = options["channel_id"]
data_dir = os.path.realpath(options["destination"])
node_ids = options["node_ids"]
Expand Down
6 changes: 4 additions & 2 deletions kolibri/core/content/management/commands/importcontent.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,10 @@ def _transfer( # noqa: max-complexity=16
overall_progress_update(f.file_size)
continue

# if the file already exists, add its size to our overall progress, and skip
if os.path.isfile(dest) and os.path.getsize(dest) == f.file_size:
# if the file already exists, or we are using remote storage, add its size to our overall progress, and skip
if paths.using_remote_storage() or (
os.path.isfile(dest) and os.path.getsize(dest) == f.file_size
):
overall_progress_update(f.file_size)
file_checksums_to_annotate.append(f.id)
transferred_file_size += f.file_size
Expand Down
32 changes: 0 additions & 32 deletions kolibri/core/content/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,12 @@
import os
from gettext import gettext as _

from django.core.urlresolvers import reverse
from django.db import connection
from django.db import models
from django.db.models import Min
from django.db.models import Q
from django.db.models import QuerySet
from django.utils.encoding import python_2_unicode_compatible
from django.utils.text import get_valid_filename
from le_utils.constants import content_kinds
from le_utils.constants import format_presets
from mptt.managers import TreeManager
Expand Down Expand Up @@ -194,15 +192,6 @@ def __str__(self):
return self.lang_name or ""


def get_download_filename(title, preset, extension):
"""
Return a valid filename to be downloaded as.
"""
filename = "{} ({}).{}".format(title, preset, extension)
valid_filename = get_valid_filename(filename)
return valid_filename


class File(base_models.File):
"""
The second to bottom layer of the contentDB schema, defines the basic building brick for content.
Expand Down Expand Up @@ -230,27 +219,6 @@ def get_preset(self):
"""
return PRESET_LOOKUP.get(self.preset, _("Unknown format"))

def get_download_filename(self):
"""
Return a valid filename to be downloaded as.
"""
return get_download_filename(
self.contentnode.title, self.get_preset(), self.get_extension()
)

def get_download_url(self):
"""
Return the download url.
"""
new_filename = self.get_download_filename()
return reverse(
"kolibri:core:downloadcontent",
kwargs={
"filename": self.local_file.get_filename(),
"new_filename": new_filename,
},
)


class LocalFileQueryset(models.QuerySet, FilterByUUIDQuerysetMixin):
def delete_unused_files(self):
Expand Down
5 changes: 0 additions & 5 deletions kolibri/core/content/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ class Meta:
class FileSerializer(serializers.ModelSerializer):
checksum = serializers.CharField(source="local_file_id")
storage_url = serializers.SerializerMethodField()
download_url = serializers.SerializerMethodField()
extension = serializers.SerializerMethodField()
file_size = serializers.SerializerMethodField()
lang = LanguageSerializer()
Expand All @@ -142,9 +141,6 @@ class FileSerializer(serializers.ModelSerializer):
def get_storage_url(self, target_node):
return target_node.get_storage_url()

def get_download_url(self, target_node):
return target_node.get_download_url()

def get_extension(self, target_node):
return target_node.get_extension()

Expand All @@ -165,7 +161,6 @@ class Meta:
"lang",
"supplementary",
"thumbnail",
"download_url",
)


Expand Down
82 changes: 0 additions & 82 deletions kolibri/core/content/test/test_downloadcontent.py

This file was deleted.

7 changes: 0 additions & 7 deletions kolibri/core/content/urls.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
from django.conf.urls import url

from .views import ContentPermalinkRedirect
from .views import DownloadContentView

urlpatterns = [
url(
r"^downloadcontent/(?P<filename>[^/]+)/(?P<new_filename>.*)",
DownloadContentView.as_view(),
{},
"downloadcontent",
),
url(r"^viewcontent$", ContentPermalinkRedirect.as_view(), name="contentpermalink"),
]
3 changes: 2 additions & 1 deletion kolibri/core/content/utils/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from .paths import get_content_file_name
from .paths import get_content_storage_file_path
from .paths import using_remote_storage
from .sqlalchemybridge import Bridge
from .sqlalchemybridge import filter_by_uuids
from kolibri.core.content.apps import KolibriContentConfig
Expand Down Expand Up @@ -434,7 +435,7 @@ def _check_file_availability(files):
for file in files:
try:
# Update if the file exists, *and* the localfile is set as unavailable.
if os.path.exists(
if using_remote_storage() or os.path.exists(
get_content_storage_file_path(
get_content_file_name({"id": file[0], "extension": file[2]})
)
Expand Down
4 changes: 4 additions & 0 deletions kolibri/core/content/utils/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ def get_content_storage_file_path(filename, datafolder=None, contentfolder=None)
return backup_path or primary_path


def using_remote_storage():
return conf.OPTIONS["Deployment"]["REMOTE_CONTENT"]


# URL PATHS


Expand Down
34 changes: 0 additions & 34 deletions kolibri/core/content/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,17 @@
from __future__ import unicode_literals

import logging
import mimetypes
import os

from django.http import Http404
from django.http import HttpResponseRedirect
from django.http.response import FileResponse
from django.views.generic.base import View

from .models import ContentNode
from .utils.paths import get_content_storage_file_path
from kolibri.core.content.hooks import ContentNodeDisplayHook

logger = logging.getLogger(__name__)


class DownloadContentView(View):
def get(self, request, filename, new_filename):
"""
Handles GET requests and serves a static file as an attachment.
"""

# calculate the local file path of the file
path = get_content_storage_file_path(filename)

# if the file does not exist on disk, return a 404
if not os.path.exists(path):
raise Http404(
'"%(filename)s" does not exist locally' % {"filename": filename}
)

# generate a file response
response = FileResponse(open(path, "rb"))

# set the content-type by guessing from the filename
response["Content-Type"] = mimetypes.guess_type(filename)[0]

# set the content-disposition as attachment to force download
response["Content-Disposition"] = "attachment;"

# set the content-length to the file size
response["Content-Length"] = os.path.getsize(path)

return response


def get_by_node_id(node_id):
"""
Function to return a content node based on a node id
Expand Down
9 changes: 9 additions & 0 deletions kolibri/utils/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,15 @@ def url_prefix(value):
retrieving alternate origin URLs.
""",
},
"REMOTE_CONTENT": {
"type": "boolean",
"default": False,
"description": """
Boolean flag that causes content import processes to skip trying to import any
content, as it is assumed that the remote source has everything available.
Server configuration should handle ensuring that the files are properly served.
""",
},
},
"Python": {
"PICKLE_PROTOCOL": {
Expand Down

0 comments on commit e131cda

Please sign in to comment.