kopia lustrzana https://github.com/jedie/PyInventory
Use "serve_media_app" to serve users uploads
see: https://github.com/jedie/django-tools/tree/master/django_tools/serve_media_apppull/30/head
rodzic
f85acd5acd
commit
49f427849b
|
@ -187,6 +187,7 @@ Files are separated into: "/src/" and "/development/"
|
|||
== history
|
||||
|
||||
* [[https://github.com/jedie/PyInventory/compare/v0.7.0...master|compare v0.7.0...master]] **dev**
|
||||
** Outsource the "MEDIA file serve" part into [[https://github.com/jedie/django-tools/tree/master/django_tools/serve_media_app#readme|django.tools.serve_media_app]]
|
||||
** tbc
|
||||
* [[https://github.com/jedie/PyInventory/compare/v0.6.0...v0.7.0|v0.7.0 - 23.11.2020]]
|
||||
** Change deployment setup:
|
||||
|
|
|
@ -254,6 +254,8 @@ history
|
|||
|
||||
* `compare v0.7.0...master <https://github.com/jedie/PyInventory/compare/v0.7.0...master>`_ **dev**
|
||||
|
||||
* Outsource the "MEDIA file serve" part into `django.tools.serve_media_app <https://github.com/jedie/django-tools/tree/master/django_tools/serve_media_app#readme>`_
|
||||
|
||||
* tbc
|
||||
|
||||
* `v0.7.0 - 23.11.2020 <https://github.com/jedie/PyInventory/compare/v0.6.0...v0.7.0>`_
|
||||
|
@ -375,4 +377,4 @@ donation
|
|||
|
||||
------------
|
||||
|
||||
``Note: this file is generated from README.creole 2020-11-23 17:53:10 with "python-creole"``
|
||||
``Note: this file is generated from README.creole 2020-12-06 19:12:25 with "python-creole"``
|
|
@ -361,14 +361,14 @@ django-reversion = "*"
|
|||
|
||||
[[package]]
|
||||
name = "django-tagulous"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
description = "Fabulous Tagging for Django"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
Django = ">=1.11"
|
||||
Django = ">=2.2"
|
||||
|
||||
[package.extras]
|
||||
dev = ["tox", "jasmine"]
|
||||
|
@ -377,7 +377,7 @@ i18n = ["unidecode"]
|
|||
|
||||
[[package]]
|
||||
name = "django-tools"
|
||||
version = "0.48.0"
|
||||
version = "0.48.2"
|
||||
description = "miscellaneous tools for django"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -1271,7 +1271,7 @@ postgres = ["psycopg2-binary"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = ">=3.7,<4.0.0"
|
||||
content-hash = "bb178e486925b95face37987e21f66c19a2e82194a0bc508d1c69eda4f105ce3"
|
||||
content-hash = "53350bdeb15638ddf111098e991f6ef65c9426369c0c29385fa6bbd303b6d1ec"
|
||||
|
||||
[metadata.files]
|
||||
appdirs = [
|
||||
|
@ -1494,11 +1494,12 @@ django-reversion-compare = [
|
|||
{file = "django_reversion_compare-0.12.2-py3-none-any.whl", hash = "sha256:5ce8d402add477a3c38aae8335af22b3abdfffa83ef5333c06c865abb89e9cbd"},
|
||||
]
|
||||
django-tagulous = [
|
||||
{file = "django-tagulous-1.0.0.tar.gz", hash = "sha256:9b4fa1773845a1cf33d21b27f9cdafc6f3fe29a480428bdd8f8717e7d4742396"},
|
||||
{file = "django-tagulous-1.1.0.tar.gz", hash = "sha256:9bc9d1d066c486fac1a3ec351531e440bc239c459b043e9180d99d7846e45fd6"},
|
||||
{file = "django_tagulous-1.1.0-py3-none-any.whl", hash = "sha256:de2a56ed92374b79358275ac0b7910af2c3d2823f44a847bef91ca9e456353ba"},
|
||||
]
|
||||
django-tools = [
|
||||
{file = "django-tools-0.48.0.tar.gz", hash = "sha256:637e0137d232abaca9f5e1af44e63b299d1e561bd7881d791ce854cfc0e74031"},
|
||||
{file = "django_tools-0.48.0-py3-none-any.whl", hash = "sha256:26556cb0f03ea34d7c3a48a0ff69858f4e6600ea0c886652893d1d102cb8c5e2"},
|
||||
{file = "django-tools-0.48.2.tar.gz", hash = "sha256:76965bb71f70965fb7b56152836e76116c02e74b81635cf0eda2819d4ad594e9"},
|
||||
{file = "django_tools-0.48.2-py3-none-any.whl", hash = "sha256:0d4d141a5f20df79139c17279a95a022aad4f2fa4eb8236d8ab61c358c7206ef"},
|
||||
]
|
||||
docker = [
|
||||
{file = "docker-4.4.0-py2.py3-none-any.whl", hash = "sha256:317e95a48c32de8c1aac92a48066a5b73e218ed096e03758bcdd799a7130a1a1"},
|
||||
|
|
|
@ -44,7 +44,7 @@ django-processinfo = "*" # https://github.com/jedie/django-processinfo/
|
|||
django-debug-toolbar = "*" # http://django-debug-toolbar.readthedocs.io/en/stable/changes.html
|
||||
django-import-export = "*" # https://github.com/django-import-export/django-import-export
|
||||
django-dbbackup = "*" # https://github.com/django-dbbackup/django-dbbackup
|
||||
django-tools = "*" # https://github.com/jedie/django-tools/
|
||||
django-tools = ">=0.48.2" # https://github.com/jedie/django-tools/
|
||||
django-reversion-compare = "*" # https://github.com/jedie/django-reversion-compare/
|
||||
django-ckeditor = "*" # https://github.com/django-ckeditor/django-ckeditor
|
||||
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# Generated by Django 2.2.17 on 2020-12-06 14:07
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Tee:
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __call__(self, line):
|
||||
if not isinstance(line, str):
|
||||
line = str(line)
|
||||
print(line)
|
||||
self.f.write(line)
|
||||
self.f.write('\n')
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
|
||||
|
||||
def forward_code(apps, schema_editor):
|
||||
print()
|
||||
log_file_path = Path(settings.MEDIA_ROOT, 'migrate.log')
|
||||
print('Generate log file here:', log_file_path)
|
||||
with log_file_path.open('w+') as log, Tee(log) as log:
|
||||
log('-' * 100)
|
||||
log(timezone.now())
|
||||
|
||||
from django_tools.serve_media_app.models import generate_media_path
|
||||
ItemImageModel = apps.get_model('inventory', 'ItemImageModel')
|
||||
|
||||
qs = ItemImageModel.objects.all()
|
||||
|
||||
for instance in qs:
|
||||
log('_' * 100)
|
||||
log(f'Migrate {instance}')
|
||||
user = instance.user
|
||||
image = instance.image
|
||||
|
||||
file_path = Path(str(image.file))
|
||||
log(f'Old path: {file_path}')
|
||||
|
||||
media_path=generate_media_path(user, filename=file_path.name)
|
||||
|
||||
new_file_path = Path(settings.MEDIA_ROOT, media_path)
|
||||
log(f'New path: {new_file_path}')
|
||||
|
||||
os.makedirs(new_file_path.parent, exist_ok=True)
|
||||
os.link(file_path, new_file_path)
|
||||
|
||||
instance.image = media_path
|
||||
instance.save(update_fields=('image',))
|
||||
|
||||
log('All new path created via hardlinks!')
|
||||
log('Old path can be deleted.')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0004_item_user_images'),
|
||||
('serve_media_app', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward_code, reverse_code=migrations.RunPython.noop),
|
||||
]
|
|
@ -6,8 +6,8 @@ from bx_py_utils.filename import clean_filename
|
|||
from ckeditor_uploader.fields import RichTextUploadingField
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_tools.serve_media_app.models import user_directory_path
|
||||
|
||||
from inventory.models.base import BaseModel
|
||||
from inventory.models.links import BaseLink
|
||||
|
@ -177,17 +177,6 @@ class ItemLinkModel(BaseLink):
|
|||
ordering = ('position',)
|
||||
|
||||
|
||||
def user_directory_path(instance, filename):
|
||||
"""
|
||||
Upload to /MEDIA_ROOT/...
|
||||
"""
|
||||
random_string = get_random_string()
|
||||
filename = clean_filename(filename)
|
||||
filename = f'user_{instance.user.id}/{random_string}/{filename}'
|
||||
logger.info(f'Upload filename: {filename!r}')
|
||||
return filename
|
||||
|
||||
|
||||
class ItemImageModel(BaseModel):
|
||||
"""
|
||||
Store Images to Items
|
||||
|
|
|
@ -3,6 +3,7 @@ from unittest import mock
|
|||
|
||||
from django.http import FileResponse
|
||||
from django.test import TestCase, override_settings
|
||||
from django_tools.serve_media_app.models import UserMediaTokenModel
|
||||
from model_bakery import baker
|
||||
|
||||
from inventory.models import ItemImageModel
|
||||
|
@ -11,53 +12,51 @@ from inventory.tests.fixtures.users import get_normal_pyinventory_user
|
|||
|
||||
class ItemImagesTestCase(TestCase):
|
||||
def test_basics(self):
|
||||
pyinventory_user1 = get_normal_pyinventory_user(id=1)
|
||||
pyinventory_user2 = get_normal_pyinventory_user(id=2)
|
||||
with mock.patch('secrets.token_urlsafe', return_value='user1token'):
|
||||
pyinventory_user1 = get_normal_pyinventory_user(id=1)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir, override_settings(MEDIA_ROOT=tmpdir):
|
||||
print(tmpdir)
|
||||
with mock.patch('secrets.token_urlsafe', return_value='user2token'):
|
||||
pyinventory_user2 = get_normal_pyinventory_user(id=2)
|
||||
|
||||
with self.assertLogs('inventory') as logs:
|
||||
with mock.patch('inventory.models.item.get_random_string', return_value='DrgCCsMrdIBJ'):
|
||||
token1_instance = UserMediaTokenModel.objects.get(user=pyinventory_user1)
|
||||
assert repr(token1_instance) == (
|
||||
f"<UserMediaTokenModel: user:1 token:'user1token' ({token1_instance.pk})>"
|
||||
)
|
||||
token2_instance = UserMediaTokenModel.objects.get(user=pyinventory_user2)
|
||||
assert repr(token2_instance) == (
|
||||
f"<UserMediaTokenModel: user:2 token:'user2token' ({token2_instance.pk})>"
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
with override_settings(MEDIA_ROOT=temp):
|
||||
with mock.patch('secrets.token_urlsafe', return_value='12345678901234567890'):
|
||||
image_instance = baker.make(
|
||||
ItemImageModel,
|
||||
user=pyinventory_user1,
|
||||
_create_files=True
|
||||
)
|
||||
|
||||
assert image_instance.image is not None
|
||||
url = image_instance.image.url
|
||||
# url = f'/media/{image_instance.image}'
|
||||
assert url == '/media/user_1/DrgCCsMrdIBJ/mock_img.jpeg'
|
||||
assert logs.output == [
|
||||
"INFO:inventory.models.item:"
|
||||
"Upload filename: 'user_1/DrgCCsMrdIBJ/mock_img.jpeg'"
|
||||
]
|
||||
assert url == '/media/user1token/12345678901234567890/mock_img.jpeg'
|
||||
|
||||
# Anonymous user can't access:
|
||||
|
||||
with self.assertLogs('inventory') as logs, self.assertLogs('django'):
|
||||
response = self.client.get(url)
|
||||
# Anonymous has no access:
|
||||
response = self.client.get('/media/user1token/12345678901234567890/mock_img.jpeg')
|
||||
assert response.status_code == 403
|
||||
assert logs.output == [
|
||||
'ERROR:inventory.views.media_files:Anonymous try to access files from: 1'
|
||||
]
|
||||
|
||||
# Wrong user should not access:
|
||||
|
||||
self.client.force_login(user=pyinventory_user2)
|
||||
|
||||
with self.assertLogs('inventory') as logs, self.assertLogs('django'):
|
||||
response = self.client.get(url)
|
||||
# Can't access with wrong user:
|
||||
self.client.force_login(pyinventory_user2)
|
||||
response = self.client.get('/media/user1token/12345678901234567890/mock_img.jpeg')
|
||||
assert response.status_code == 403
|
||||
assert logs.output == [
|
||||
'ERROR:inventory.views.media_files:Wrong user ID: 2 is not 1'
|
||||
]
|
||||
|
||||
# The right user should access:
|
||||
# Can access with the right user:
|
||||
self.client.force_login(pyinventory_user1)
|
||||
response = self.client.get('/media/user1token/12345678901234567890/mock_img.jpeg')
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response, FileResponse)
|
||||
assert response.getvalue() == image_instance.image.open('rb').read()
|
||||
|
||||
self.client.force_login(user=pyinventory_user1)
|
||||
|
||||
response = self.client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response, FileResponse)
|
||||
assert response.getvalue() == image_instance.image.read()
|
||||
# Test whats happen, if token was deleted
|
||||
UserMediaTokenModel.objects.all().delete()
|
||||
response = self.client.get('/media/user1token/12345678901234567890/mock_img.jpeg')
|
||||
assert response.status_code == 400 # SuspiciousOperation -> HttpResponseBadRequest
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import View
|
||||
from django.views.static import serve
|
||||
|
||||
from inventory.models import ItemImageModel
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserMediaView(View):
|
||||
"""
|
||||
Serve MEDIA_URL files, but check the current user:
|
||||
"""
|
||||
|
||||
def get(self, request, user_id, path):
|
||||
media_path = f'user_{user_id}/{path}'
|
||||
|
||||
if not request.user.is_superuser:
|
||||
if request.user.id != user_id:
|
||||
# A user tries to access a file from a other use?
|
||||
if request.user.id is None:
|
||||
logger.error(f'Anonymous try to access files from: {user_id!r}')
|
||||
else:
|
||||
logger.error(f'Wrong user ID: {request.user.id!r} is not {user_id!r}')
|
||||
raise PermissionDenied
|
||||
|
||||
# Check if the image really exists:
|
||||
qs = ItemImageModel.objects.filter(
|
||||
user_id=request.user.id,
|
||||
image=media_path
|
||||
)
|
||||
if not qs.exists():
|
||||
raise Http404(_('Image "%(path)s" does not exist') % {'path': media_path})
|
||||
|
||||
# Send the file to the user:
|
||||
return serve(
|
||||
request,
|
||||
path=media_path,
|
||||
document_root=settings.MEDIA_ROOT,
|
||||
show_indexes=False
|
||||
)
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
'''
|
||||
Base Django settings
|
||||
"""
|
||||
'''
|
||||
|
||||
import logging
|
||||
from pathlib import Path as __Path
|
||||
|
@ -76,6 +76,9 @@ INSTALLED_APPS = [
|
|||
'axes', # https://github.com/jazzband/django-axes
|
||||
'django_processinfo', # https://github.com/jedie/django-processinfo/
|
||||
|
||||
# https://github.com/jedie/django-tools/tree/master/django_tools/serve_media_app
|
||||
'django_tools.serve_media_app.apps.UserMediaFilesConfig',
|
||||
|
||||
'inventory.apps.InventoryConfig',
|
||||
]
|
||||
|
||||
|
@ -109,7 +112,7 @@ MIDDLEWARE = [
|
|||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
"DIRS": [str(__Path(PROJECT_PATH, 'inventory_project', 'templates'))],
|
||||
'DIRS': [str(__Path(PROJECT_PATH, 'inventory_project', 'templates'))],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
|
@ -342,6 +345,7 @@ LOGGING = {
|
|||
'': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False},
|
||||
'django': {'handlers': ['console'], 'level': 'INFO', 'propagate': False},
|
||||
'axes': {'handlers': ['console'], 'level': 'WARNING', 'propagate': False},
|
||||
'django_tools': {'handlers': ['console'], 'level': 'INFO', 'propagate': False},
|
||||
'inventory': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ from django.contrib import admin
|
|||
from django.urls import path
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from inventory.views.media_files import UserMediaView
|
||||
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
|
@ -15,7 +13,7 @@ urlpatterns = [ # Don't use i18n_patterns() here
|
|||
url(r'^$', RedirectView.as_view(url='/admin/')),
|
||||
|
||||
path('ckeditor/', include('ckeditor_uploader.urls')), # TODO: check permissions?
|
||||
path('media/user_<int:user_id>/<path:path>', UserMediaView.as_view())
|
||||
path(settings.MEDIA_URL.lstrip('/'), include('django_tools.serve_media_app.urls')),
|
||||
]
|
||||
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue