[WIP] External postgres as mediafile store

This commit is contained in:
FinnStutzenstein 2019-12-04 14:24:30 +01:00
parent fbe5ea2056
commit 7204d59d66
16 changed files with 329 additions and 136 deletions

3
.gitignore vendored
View File

@ -13,6 +13,9 @@
node_modules/* node_modules/*
bower_components/* bower_components/*
# OS4-Submodules
openslides-*
# Local user data (settings, database, media, search index, static files) # Local user data (settings, database, media, search index, static files)
personal_data/* personal_data/*
openslides/static/* openslides/static/*

View File

@ -138,7 +138,7 @@ export class MediaUploadContentComponent implements OnInit {
input.set('title', fileData.title); input.set('title', fileData.title);
const access_groups_id = fileData.form.value.access_groups_id || []; const access_groups_id = fileData.form.value.access_groups_id || [];
if (access_groups_id.length > 0) { if (access_groups_id.length > 0) {
input.set('access_groups_id', '' + access_groups_id); input.set('access_groups_id', JSON.stringify(access_groups_id));
} }
if (this.selectedDirectoryId) { if (this.selectedDirectoryId) {
input.set('parent_id', '' + this.selectedDirectoryId); input.set('parent_id', '' + this.selectedDirectoryId);

View File

@ -1,11 +1,7 @@
import { BaseModelWithListOfSpeakers } from '../base/base-model-with-list-of-speakers'; import { BaseModelWithListOfSpeakers } from '../base/base-model-with-list-of-speakers';
interface FileMetadata { interface PdfInformation {
name: string; pages?: number;
type: string;
// Only for PDFs
pages: number;
encrypted?: boolean; encrypted?: boolean;
} }
@ -13,7 +9,9 @@ export interface MediafileWithoutNestedModels extends BaseModelWithListOfSpeaker
id: number; id: number;
title: string; title: string;
media_url_prefix: string; media_url_prefix: string;
pdf_information: PdfInformation;
filesize?: string; filesize?: string;
mimetype?: string;
access_groups_id: number[]; access_groups_id: number[];
create_timestamp: string; create_timestamp: string;
parent_id: number | null; parent_id: number | null;
@ -31,7 +29,6 @@ export interface MediafileWithoutNestedModels extends BaseModelWithListOfSpeaker
export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> { export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> {
public static COLLECTIONSTRING = 'mediafiles/mediafile'; public static COLLECTIONSTRING = 'mediafiles/mediafile';
public id: number; public id: number;
public mediafile?: FileMetadata;
public get has_inherited_access_groups(): boolean { public get has_inherited_access_groups(): boolean {
return typeof this.inherited_access_groups_id !== 'boolean'; return typeof this.inherited_access_groups_id !== 'boolean';

View File

@ -8,6 +8,19 @@ import { ViewGroup } from 'app/site/users/models/view-group';
export const IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/gif']; export const IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/gif'];
export const FONT_MIMETYPES = ['font/ttf', 'font/woff', 'application/font-woff', 'application/font-sfnt']; export const FONT_MIMETYPES = ['font/ttf', 'font/woff', 'application/font-woff', 'application/font-sfnt'];
export const PDF_MIMETYPES = ['application/pdf']; export const PDF_MIMETYPES = ['application/pdf'];
export const VIDEO_MIMETYPES = [
'video/quicktime',
'video/mp4',
'video/webm',
'video/ogg',
'video/x-flv',
'application/x-mpegURL',
'video/MP2T',
'video/3gpp',
'video/x-msvideo',
'video/x-ms-wmv',
'video/x-matroska'
];
export interface MediafileTitleInformation { export interface MediafileTitleInformation {
title: string; title: string;
@ -26,12 +39,8 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
return this.title; return this.title;
} }
public get type(): string | null {
return this.mediafile.mediafile ? this.mediafile.mediafile.type : '';
}
public get pages(): number | null { public get pages(): number | null {
return this.mediafile.mediafile ? this.mediafile.mediafile.pages : null; return this.mediafile.pdf_information.pages || null;
} }
public get timestamp(): string { public get timestamp(): string {
@ -39,7 +48,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
} }
public formatForSearch(): SearchRepresentation { public formatForSearch(): SearchRepresentation {
const type = this.is_directory ? 'directory' : this.type; const type = this.is_directory ? 'directory' : this.mimetype;
const properties = [ const properties = [
{ key: 'Title', value: this.getTitle() }, { key: 'Title', value: this.getTitle() },
{ key: 'Path', value: this.path }, { key: 'Path', value: this.path },
@ -91,7 +100,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
* @returns true or false * @returns true or false
*/ */
public isImage(): boolean { public isImage(): boolean {
return IMAGE_MIMETYPES.includes(this.type); return IMAGE_MIMETYPES.includes(this.mimetype);
} }
/** /**
@ -100,7 +109,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
* @returns true or false * @returns true or false
*/ */
public isFont(): boolean { public isFont(): boolean {
return FONT_MIMETYPES.includes(this.type); return FONT_MIMETYPES.includes(this.mimetype);
} }
/** /**
@ -109,7 +118,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
* @returns true or false * @returns true or false
*/ */
public isPdf(): boolean { public isPdf(): boolean {
return PDF_MIMETYPES.includes(this.type); return PDF_MIMETYPES.includes(this.mimetype);
} }
/** /**
@ -118,19 +127,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
* @returns true or false * @returns true or false
*/ */
public isVideo(): boolean { public isVideo(): boolean {
return [ return VIDEO_MIMETYPES.includes(this.mimetype);
'video/quicktime',
'video/mp4',
'video/webm',
'video/ogg',
'video/x-flv',
'application/x-mpegURL',
'video/MP2T',
'video/3gpp',
'video/x-msvideo',
'video/x-ms-wmv',
'video/x-matroska'
].includes(this.type);
} }
public getIcon(): string { public getIcon(): string {

View File

@ -23,7 +23,7 @@ export class MediafilesSortListService extends BaseSortListService<ViewMediafile
private mediafilesSortOptions: OsSortingOption<ViewMediafile>[] = [ private mediafilesSortOptions: OsSortingOption<ViewMediafile>[] = [
{ property: 'title' }, { property: 'title' },
{ {
property: 'type', property: 'mimetype',
label: this.translate.instant('Type') label: this.translate.instant('Type')
}, },
{ {

View File

@ -1,5 +1,5 @@
export interface MediafileSlideData { export interface MediafileSlideData {
path: string; path: string;
type: string; mimetype: string;
media_url_prefix: string; media_url_prefix: string;
} }

View File

@ -20,11 +20,11 @@ export class MediafileSlideComponent extends BaseSlideComponent<MediafileSlideDa
} }
public get isImage(): boolean { public get isImage(): boolean {
return IMAGE_MIMETYPES.includes(this.data.data.type); return IMAGE_MIMETYPES.includes(this.data.data.mimetype);
} }
public get isPdf(): boolean { public get isPdf(): boolean {
return PDF_MIMETYPES.includes(this.data.data.type); return PDF_MIMETYPES.includes(this.data.data.mimetype);
} }
public constructor() { public constructor() {

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.8 on 2019-12-11 14:11
import jsonfield.encoder
import jsonfield.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("mediafiles", "0006_directories_and_permissions_3"),
]
operations = [
migrations.AddField(
model_name="mediafile",
name="filesize",
field=models.CharField(default="", max_length=255),
),
migrations.AddField(
model_name="mediafile",
name="mimetype",
field=models.CharField(default="", max_length=255),
),
migrations.AddField(
model_name="mediafile",
name="pdf_information",
field=jsonfield.fields.JSONField(
default=dict,
dump_kwargs={
"cls": jsonfield.encoder.JSONEncoder,
"separators": (",", ":"),
},
load_kwargs={},
),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.8 on 2019-12-11 14:12
import mimetypes
from django.db import migrations
from ..utils import bytes_to_human, get_pdf_information
def fill_new_values(apps, schema_editor):
Mediafile = apps.get_model("mediafiles", "Mediafile")
for mediafile in Mediafile.objects.all():
if not mediafile.is_directory:
mediafile.filesize = bytes_to_human(mediafile.mediafile.size)
mediafile.mimetype = mimetypes.guess_type(mediafile.mediafile.name)[0]
if mediafile.mimetype == "application/pdf":
mediafile.pdf_information = get_pdf_information(mediafile.mediafile)
else:
mediafile.pdf_information = {}
mediafile.save(skip_autoupdate=True)
class Migration(migrations.Migration):
dependencies = [
("mediafiles", "0007_external_storage_1"),
]
operations = [migrations.RunPython(fill_new_values)]

View File

@ -3,13 +3,24 @@ import uuid
from typing import List, cast from typing import List, cast
from django.conf import settings from django.conf import settings
from django.db import models from django.db import connections, models
from jsonfield import JSONField
from openslides.utils import logging
from ..agenda.mixins import ListOfSpeakersMixin from ..agenda.mixins import ListOfSpeakersMixin
from ..core.config import config from ..core.config import config
from ..utils.models import RESTModelMixin from ..utils.models import RESTModelMixin
from ..utils.rest_api import ValidationError from ..utils.rest_api import ValidationError
from .access_permissions import MediafileAccessPermissions from .access_permissions import MediafileAccessPermissions
from .utils import bytes_to_human
logger = logging.getLogger(__name__)
if "mediafiles" in connections:
use_mediafile_database = True
logger.info("Using a standalone mediafile database")
class MediafileManager(models.Manager): class MediafileManager(models.Manager):
@ -33,7 +44,6 @@ class MediafileManager(models.Manager):
def get_file_path(mediafile, filename): def get_file_path(mediafile, filename):
mediafile.original_filename = filename
ext = filename.split(".")[-1] ext = filename.split(".")[-1]
filename = "%s.%s" % (uuid.uuid4(), ext) filename = "%s.%s" % (uuid.uuid4(), ext)
return os.path.join("file", filename) return os.path.join("file", filename)
@ -42,6 +52,10 @@ def get_file_path(mediafile, filename):
class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model): class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
""" """
Class for uploaded files which can be delivered under a certain url. Class for uploaded files which can be delivered under a certain url.
This model encapsulte directories and mediafiles. If is_directory is True, the `title` is
the directory name. Else, there might be a mediafile, except when using a mediafile DB. In
this case, also mediafile is None.
""" """
objects = MediafileManager() objects = MediafileManager()
@ -54,6 +68,12 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
for more information. for more information.
""" """
filesize = models.CharField(max_length=255, default="")
mimetype = models.CharField(max_length=255, default="")
pdf_information = JSONField(default=dict)
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
"""A string representing the title of the file.""" """A string representing the title of the file."""
@ -103,14 +123,11 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
""" """
`unique_together` is not working with foreign keys with possible `null` values. `unique_together` is not working with foreign keys with possible `null` values.
So we do need to check this here. So we do need to check this here.
self.original_filename is not yet set, but if is_file is True, the actual
filename is self.mediafile.file
""" """
title_or_original_filename = models.Q(title=self.title) title_or_original_filename = models.Q(title=self.title)
if self.is_file: if self.is_file:
title_or_original_filename = title_or_original_filename | models.Q( title_or_original_filename = title_or_original_filename | models.Q(
original_filename=self.mediafile.name original_filename=self.original_filename
) )
if ( if (
@ -135,8 +152,11 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
def delete(self, skip_autoupdate=False): def delete(self, skip_autoupdate=False):
mediafiles_to_delete = self.get_children_deep() mediafiles_to_delete = self.get_children_deep()
mediafiles_to_delete.append(self) mediafiles_to_delete.append(self)
deleted_ids = []
for mediafile in mediafiles_to_delete: for mediafile in mediafiles_to_delete:
if mediafile.is_file: deleted_ids.append(mediafile.id)
# Do not check for is_file, this might be wrong to delete the actual mediafile
if mediafile.mediafile:
# To avoid Django calling save() and triggering autoupdate we do not # To avoid Django calling save() and triggering autoupdate we do not
# use the builtin method mediafile.mediafile.delete() but call # use the builtin method mediafile.mediafile.delete() but call
# mediafile.mediafile.storage.delete(...) directly. This may have # mediafile.mediafile.storage.delete(...) directly. This may have
@ -145,6 +165,8 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
mediafile.mediafile.storage.delete(mediafile.mediafile.name) mediafile.mediafile.storage.delete(mediafile.mediafile.name)
mediafile._db_delete(skip_autoupdate=skip_autoupdate) mediafile._db_delete(skip_autoupdate=skip_autoupdate)
return deleted_ids
def _db_delete(self, *args, **kwargs): def _db_delete(self, *args, **kwargs):
""" Captures the original .delete() method. """ """ Captures the original .delete() method. """
return super().delete(*args, **kwargs) return super().delete(*args, **kwargs)
@ -200,24 +222,15 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
""" """
Transforms bytes to kilobytes or megabytes. Returns the size as string. Transforms bytes to kilobytes or megabytes. Returns the size as string.
""" """
# TODO: Read http://stackoverflow.com/a/1094933 and think about it.
try: try:
size = self.mediafile.size size = self.mediafile.size
except OSError: except OSError:
size_string = "unknown" return "unknown"
except ValueError: except ValueError:
# happens, if this is a directory and no file exists # happens, if this is a directory and no file exists
return None return None
else: else:
if size < 1024: return bytes_to_human(size)
size_string = "< 1 kB"
elif size >= 1024 * 1024:
mB = size / 1024 / 1024
size_string = "%d MB" % mB
else:
kB = size / 1024
size_string = "%d kB" % kB
return size_string
@property @property
def is_logo(self): def is_logo(self):
@ -243,6 +256,7 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
@property @property
def is_file(self): def is_file(self):
""" Do not check the self.mediafile, becuase this is not a valid indicator. """
return not self.is_directory return not self.is_directory
def get_list_of_speakers_title_information(self): def get_list_of_speakers_title_information(self):

View File

@ -32,7 +32,7 @@ async def mediafile_slide(
return { return {
"path": mediafile["path"], "path": mediafile["path"],
"type": mediafile["mediafile"]["type"], "mimetype": mediafile["mimetype"],
"media_url_prefix": mediafile["media_url_prefix"], "media_url_prefix": mediafile["media_url_prefix"],
} }

View File

@ -1,14 +1,10 @@
import mimetypes
from django.conf import settings from django.conf import settings
from django.db import models as dbmodels
from PyPDF2 import PdfFileReader
from PyPDF2.utils import PdfReadError
from ..utils.auth import get_group_model from ..utils.auth import get_group_model
from ..utils.rest_api import ( from ..utils.rest_api import (
FileField, CharField,
IdPrimaryKeyRelatedField, IdPrimaryKeyRelatedField,
JSONField,
ModelSerializer, ModelSerializer,
SerializerMethodField, SerializerMethodField,
ValidationError, ValidationError,
@ -16,70 +12,28 @@ from ..utils.rest_api import (
from .models import Mediafile from .models import Mediafile
class AngularCompatibleFileField(FileField):
def to_internal_value(self, data):
if data == "":
return None
return super(AngularCompatibleFileField, self).to_internal_value(data)
def to_representation(self, value):
if value is None or value.name is None:
return None
filetype = mimetypes.guess_type(value.name)[0]
result = {"name": value.name, "type": filetype}
if filetype == "application/pdf":
try:
if (
settings.DEFAULT_FILE_STORAGE
== "storages.backends.sftpstorage.SFTPStorage"
):
remote_path = value.storage._remote_path(value.name)
file_handle = value.storage.sftp.open(remote_path, mode="rb")
else:
file_handle = open(value.path, "rb")
result["pages"] = PdfFileReader(file_handle).getNumPages()
except FileNotFoundError:
# File was deleted from server. Set 'pages' to 0.
result["pages"] = 0
except PdfReadError:
# File could be encrypted but not be detected by PyPDF.
result["pages"] = 0
result["encrypted"] = True
return result
class MediafileSerializer(ModelSerializer): class MediafileSerializer(ModelSerializer):
""" """
Serializer for mediafile.models.Mediafile objects. Serializer for mediafile.models.Mediafile objects.
""" """
media_url_prefix = SerializerMethodField() media_url_prefix = SerializerMethodField()
filesize = SerializerMethodField() pdf_information = JSONField(required=False)
access_groups = IdPrimaryKeyRelatedField( access_groups = IdPrimaryKeyRelatedField(
many=True, required=False, queryset=get_group_model().objects.all() many=True, required=False, queryset=get_group_model().objects.all()
) )
original_filename = CharField(write_only=True, required=False, allow_null=True)
def __init__(self, *args, **kwargs):
"""
This constructor overwrites the FileField field serializer to return the file meta data in a way that the
angualarjs upload module likes
"""
super(MediafileSerializer, self).__init__(*args, **kwargs)
self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField
# Make some fields read-oinly for updates (not creation)
if self.instance is not None:
self.fields["mediafile"].read_only = True
class Meta: class Meta:
model = Mediafile model = Mediafile
fields = ( fields = (
"id", "id",
"title", "title",
"mediafile", "original_filename",
"media_url_prefix", "media_url_prefix",
"filesize", "filesize",
"mimetype",
"pdf_information",
"access_groups", "access_groups",
"create_timestamp", "create_timestamp",
"is_directory", "is_directory",
@ -89,7 +43,7 @@ class MediafileSerializer(ModelSerializer):
"inherited_access_groups_id", "inherited_access_groups_id",
) )
read_only_fields = ("path",) read_only_fields = ("path", "filesize", "mimetype", "pdf_information")
def validate(self, data): def validate(self, data):
title = data.get("title") title = data.get("title")
@ -122,8 +76,5 @@ class MediafileSerializer(ModelSerializer):
validated_data.pop("parent", None) validated_data.pop("parent", None)
return super().update(instance, validated_data) return super().update(instance, validated_data)
def get_filesize(self, mediafile):
return mediafile.get_filesize()
def get_media_url_prefix(self, mediafile): def get_media_url_prefix(self, mediafile):
return settings.MEDIA_URL return settings.MEDIA_URL

View File

@ -0,0 +1,27 @@
from PyPDF2 import PdfFileReader
from PyPDF2.utils import PdfReadError
def bytes_to_human(size):
# TODO: Read http://stackoverflow.com/a/1094933 and think about it.
if size < 1024:
size_string = "< 1 kB"
elif size >= 1024 * 1024:
mB = size / 1024 / 1024
size_string = "%d MB" % mB
else:
kB = size / 1024
size_string = "%d kB" % kB
return size_string
def get_pdf_information(mediafile):
result = {}
try:
pdf = PdfFileReader(mediafile)
result["pages"] = pdf.getNumPages()
except PdfReadError:
# File could be encrypted but not be detected by PyPDF.
result["pages"] = 0
result["encrypted"] = True
return result

View File

@ -1,15 +1,39 @@
from django.http import HttpResponseForbidden, HttpResponseNotFound import json
from django.conf import settings
from django.db import connections
from django.http import HttpResponseForbidden, HttpResponseNotFound, JsonResponse
from django.http.request import QueryDict from django.http.request import QueryDict
from django.views.decorators.csrf import csrf_exempt
from django.views.static import serve from django.views.static import serve
from openslides.core.models import Projector from openslides.core.models import Projector
from openslides.utils import logging
from openslides.utils.auth import has_perm, in_some_groups
from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.rest_api import (
ModelViewSet,
Response,
ValidationError,
list_route,
status,
)
from ..utils.auth import has_perm, in_some_groups
from ..utils.autoupdate import inform_changed_data
from ..utils.rest_api import ModelViewSet, Response, ValidationError, list_route
from .access_permissions import MediafileAccessPermissions from .access_permissions import MediafileAccessPermissions
from .config import watch_and_update_configs from .config import watch_and_update_configs
from .models import Mediafile from .models import Mediafile
from .utils import bytes_to_human, get_pdf_information
logger = logging.getLogger(__name__)
use_mediafile_database = "mediafiles" in connections
if use_mediafile_database:
logger.info("Using a standalone mediafile database")
max_upload_size = getattr(
settings, "MEDIAFILE_MAX_SIZE", 100 * 1024 * 1024
) # default: 100mb
# Viewsets for the REST API # Viewsets for the REST API
@ -47,6 +71,23 @@ class MediafileViewSet(ModelViewSet):
result = False result = False
return result return result
def convert_access_groups(self, request):
# convert formdata json representation of "[id, id, ...]" to an real object
if "access_groups_id" in request.data and isinstance(request.data, QueryDict):
access_groups_id = request.data.get("access_groups_id")
if access_groups_id:
try:
access_groups_id = json.loads(access_groups_id)
request.data.setlist("access_groups_id", access_groups_id)
except json.decoder.JSONDecodeError:
raise ValidationError(
{
"detail": "The provided access groups ({access_groups_id}) is not JSON"
}
)
else:
del request.data["access_groups_id"]
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" """
Customized view endpoint to upload a new file. Customized view endpoint to upload a new file.
@ -55,30 +96,77 @@ class MediafileViewSet(ModelViewSet):
if isinstance(request.data, QueryDict): if isinstance(request.data, QueryDict):
request.data._mutable = True request.data._mutable = True
# convert formdata string "<id, <id>, id>" to a list of numbers. self.convert_access_groups(request)
if "access_groups_id" in request.data and isinstance(request.data, QueryDict):
access_groups_id = request.data.get("access_groups_id")
if access_groups_id:
request.data.setlist(
"access_groups_id", [int(x) for x in access_groups_id.split(", ")]
)
else:
del request.data["access_groups_id"]
is_directory = bool(request.data.get("is_directory", False)) is_directory = bool(request.data.get("is_directory", False))
if is_directory and request.data.get("mediafile"): mediafile = request.data.get("mediafile")
# Check, that it is either a file or a directory
if is_directory and mediafile:
raise ValidationError( raise ValidationError(
{"detail": "Either create a path or a file, but not both"} {"detail": "Either create a path or a file, but not both"}
) )
if not request.data.get("mediafile") and not is_directory: if not mediafile and not is_directory:
raise ValidationError({"detail": "You forgot to provide a file."}) raise ValidationError({"detail": "You forgot to provide a file."})
return super().create(request, *args, **kwargs) if mediafile:
if mediafile.size > max_upload_size:
max_size_for_humans = bytes_to_human(max_upload_size)
raise ValidationError(
{"detail": f"The maximum upload file size is {max_size_for_humans}"}
)
# set original filename
request.data["original_filename"] = mediafile.name
# Remove mediafile from request.data, we will put it into the storage ourself.
request.data.pop("mediafile", None)
# Create mediafile
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
db_mediafile = serializer.save()
# Set filesize, mimetype and check for pdfs.
if mediafile:
db_mediafile.filesize = bytes_to_human(mediafile.size)
db_mediafile.mimetype = mediafile.content_type
if db_mediafile.mimetype == "application/pdf":
db_mediafile.pdf_information = get_pdf_information(mediafile)
else:
db_mediafile.pdf_information = {}
db_mediafile.save()
# Custom modifications for the database storage: Set original filename here and
# insert the file into the foreing storage
if use_mediafile_database:
with connections["mediafiles"].cursor() as cursor:
cursor.execute(
"INSERT INTO mediafile_data (id, data, mimetype) VALUES (%s, %s, %s)",
[
db_mediafile.id,
mediafile.open().read(),
mediafile.content_type,
],
)
else:
db_mediafile.mediafile = mediafile
db_mediafile.save()
return Response(data={"id": db_mediafile.id}, status=status.HTTP_201_CREATED)
def destroy(self, *args, **kwargs): def destroy(self, *args, **kwargs):
with watch_and_update_configs(): with watch_and_update_configs():
response = super().destroy(*args, **kwargs) mediafile = self.get_object()
return response deleted_ids = mediafile.delete()
if use_mediafile_database:
with connections["mediafiles"].cursor() as cursor:
cursor.execute(
"DELETE FROM mediafile_data WHERE id IN %s",
[tuple(id for id in deleted_ids)],
)
return Response(status=status.HTTP_204_NO_CONTENT)
def update(self, *args, **kwargs): def update(self, *args, **kwargs):
with watch_and_update_configs(): with watch_and_update_configs():
@ -257,3 +345,19 @@ def protected_serve(request, path, document_root=None, show_indexes=False):
return serve(request, mediafile.mediafile.name, document_root, show_indexes) return serve(request, mediafile.mediafile.name, document_root, show_indexes)
else: else:
return HttpResponseForbidden(content="Forbidden.") return HttpResponseForbidden(content="Forbidden.")
@csrf_exempt
def check_serve(request, path):
"""
Checks, if the mediafile could be delivered: Responds with a 403 if the access is
forbidden *or* the file was not found. Responds 200 with the mediafile id, if
the access is granted.
"""
try:
mediafile = get_mediafile(request, path)
if not mediafile:
raise Mediafile.DoesNotExist()
except Mediafile.DoesNotExist:
return HttpResponseForbidden()
return JsonResponse({"id": mediafile.id})

View File

@ -2,7 +2,7 @@ from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.views.generic import RedirectView from django.views.generic import RedirectView
from openslides.mediafiles.views import protected_serve from openslides.mediafiles.views import check_serve, protected_serve
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .core import views as core_views from .core import views as core_views
@ -15,6 +15,7 @@ urlpatterns = [
protected_serve, protected_serve,
{"document_root": settings.MEDIA_ROOT}, {"document_root": settings.MEDIA_ROOT},
), ),
url(r"^check-media/(?P<path>.*)$", check_serve),
# URLs for the rest system, redirect /rest to /rest/ # URLs for the rest system, redirect /rest to /rest/
url(r"^rest$", RedirectView.as_view(url="/rest/", permanent=True)), url(r"^rest$", RedirectView.as_view(url="/rest/", permanent=True)),
url(r"^rest/", include(router.urls)), url(r"^rest/", include(router.urls)),

View File

@ -1,3 +1,5 @@
import json
import pytest import pytest
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse from django.urls import reverse
@ -22,6 +24,7 @@ def test_mediafiles_db_queries():
for index in range(10): for index in range(10):
Mediafile.objects.create( Mediafile.objects.create(
title=f"some_file{index}", title=f"some_file{index}",
original_filename=f"some_file{index}",
mediafile=SimpleUploadedFile(f"some_file{index}", b"some content."), mediafile=SimpleUploadedFile(f"some_file{index}", b"some content."),
) )
@ -56,6 +59,7 @@ class TestCreation(TestCase):
self.assertEqual(mediafile.title, "test_title_ahyo1uifoo9Aiph2av5a") self.assertEqual(mediafile.title, "test_title_ahyo1uifoo9Aiph2av5a")
self.assertTrue(mediafile.is_directory) self.assertTrue(mediafile.is_directory)
self.assertEqual(mediafile.mediafile.name, "") self.assertEqual(mediafile.mediafile.name, "")
self.assertEqual(mediafile.original_filename, "")
self.assertEqual(mediafile.path, mediafile.title + "/") self.assertEqual(mediafile.path, mediafile.title + "/")
def test_file_and_directory(self): def test_file_and_directory(self):
@ -166,9 +170,8 @@ class TestCreation(TestCase):
reverse("mediafile-list"), reverse("mediafile-list"),
{ {
"title": "test_title_dggjwevBnUngelkdviom", "title": "test_title_dggjwevBnUngelkdviom",
"is_directory": True, "mediafile": self.file,
# This is the format, if it would be provided by JS `FormData`. "access_groups_id": json.dumps([2, 4]),
"access_groups_id": "2, 4",
}, },
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@ -177,6 +180,32 @@ class TestCreation(TestCase):
self.assertEqual( self.assertEqual(
sorted([group.id for group in mediafile.access_groups.all()]), [2, 4] sorted([group.id for group in mediafile.access_groups.all()]), [2, 4]
) )
self.assertTrue(mediafile.mediafile.name)
self.assertEqual(mediafile.path, mediafile.original_filename)
def test_with_access_groups_wrong_json(self):
response = self.client.post(
reverse("mediafile-list"),
{
"title": "test_title_dggjwevBnUngelkdviom",
"is_directory": True,
"access_groups_id": json.dumps({"a": 324}),
},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(Mediafile.objects.exists())
def test_with_access_groups_wrong_json2(self):
response = self.client.post(
reverse("mediafile-list"),
{
"title": "test_title_dggjwevBnUngelkdviom",
"is_directory": True,
"access_groups_id": "_FWEpwwfkwk",
},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(Mediafile.objects.exists())
# TODO: List and retrieve # TODO: List and retrieve
@ -194,13 +223,18 @@ class TestUpdate(TestCase):
self.client = APIClient() self.client = APIClient()
self.client.login(username="admin", password="admin") self.client.login(username="admin", password="admin")
self.dir = Mediafile.objects.create(title="dir", is_directory=True) self.dir = Mediafile.objects.create(title="dir", is_directory=True)
self.fileA = SimpleUploadedFile("some_fileA.ext", b"some content.") fileA_name = "some_fileA.ext"
self.fileA = SimpleUploadedFile(fileA_name, b"some content.")
self.mediafileA = Mediafile.objects.create( self.mediafileA = Mediafile.objects.create(
title="mediafileA", mediafile=self.fileA, parent=self.dir title="mediafileA",
original_filename=fileA_name,
mediafile=self.fileA,
parent=self.dir,
) )
self.fileB = SimpleUploadedFile("some_fileB.ext", b"some content.") fileB_name = "some_fileB.ext"
self.fileB = SimpleUploadedFile(fileB_name, b"some content.")
self.mediafileB = Mediafile.objects.create( self.mediafileB = Mediafile.objects.create(
title="mediafileB", mediafile=self.fileB title="mediafileB", original_filename=fileB_name, mediafile=self.fileB
) )
def test_update(self): def test_update(self):