Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix removing initial letter in files #262

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions example/cat/migrations/0002_alter_cat_custom_filename.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 3.2.11 on 2022-01-07 15:27

from django.db import migrations
import s3direct.fields


class Migration(migrations.Migration):

dependencies = [
('cat', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='cat',
name='custom_filename',
field=s3direct.fields.S3DirectField(blank=True, null=True),
),
]
8 changes: 4 additions & 4 deletions example/cat/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@


class Cat(models.Model):
custom_filename = S3DirectField(dest='custom_filename', blank=True)
custom_filename = S3DirectField(dest='custom_filename')


class Kitten(models.Model):
mother = models.ForeignKey('Cat', on_delete=models.CASCADE)

video = S3DirectField(dest='videos', blank=True)
image = S3DirectField(dest='images', blank=True)
pdf = S3DirectField(dest='pdfs', blank=True)
video = S3DirectField(dest='videos')
image = S3DirectField(dest='images')
pdf = S3DirectField(dest='pdfs')
198 changes: 189 additions & 9 deletions s3direct/fields.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,196 @@
from django.db.models import Field
import cgi
import boto3
import botocore
import json
from urllib.parse import unquote
from email.message import EmailMessage

from django import forms

from django.db.models import JSONField
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models.fields import Field
from django.utils.translation import gettext as _

from s3direct.widgets import S3DirectWidget
from s3direct.utils import (
get_aws_credentials,
get_s3direct_destinations,
get_presigned_url,
)


def validate_url(value, dest):
if not isinstance(value, str):
raise Exception(_("Not a string."))

dest = get_s3direct_destinations().get(dest)

aws_access_key_id = getattr(settings, "AWS_ACCESS_KEY_ID", None)
aws_secret_access_key = getattr(settings, "AWS_SECRET_ACCESS_KEY", None)
bucket = dest.get("bucket", getattr(settings, "AWS_STORAGE_BUCKET_NAME", None))

region = dest.get("region", getattr(settings, "AWS_S3_REGION_NAME", None))
endpoint = dest.get("endpoint", getattr(settings, "AWS_S3_ENDPOINT_URL", None))

services = [{"source": "AWS"}]
if aws_access_key_id and aws_secret_access_key:
# This is an hybrid OCI deployment
services = [
{
"source": "AWS",
"region": getattr(settings, "ORIGINAL_AWS_S3_REGION_NAME", None),
"endpoint": getattr(settings, "ORIGINAL_AWS_S3_ENDPOINT_URL", None),
},
{
"source": "OCI",
"aws_access_key_id": aws_access_key_id,
"aws_secret_access_key": aws_secret_access_key,
},
]

# This is OCI native deployment
aws_access_key_id = getattr(settings, "ORIGINAL_AWS_ACCESS_KEY_ID", None)
aws_secret_access_key = getattr(settings, "ORIGINAL_SECRET_ACCESS_KEY", None)

if aws_access_key_id and aws_secret_access_key:
services[0].update({
"aws_access_key_id": aws_access_key_id,
"aws_secret_access_key": aws_secret_access_key,
})


# HACK: We need to check all the authentications possible to validate the file
# in AWS and Oracle

no_such_key = False
for service in services:
source = service.pop("source")

endpoint_url = service.pop("endpoint", endpoint)
region_name = service.pop("region", region)

s3_client = boto3.client(
"s3",
endpoint_url=endpoint_url,
region_name=region_name,
config=botocore.client.Config(signature_version="s3v4"),
**service,
)

try:
head_object = s3_client.head_object(
Bucket=bucket,
Key=value,
)

content_disposition = head_object["ContentDisposition"]
header_value, header_params = cgi.parse_header(content_disposition)

msg = EmailMessage()
msg["Content-Disposition"] = content_disposition
filename = msg.get_filename()

mimetype = head_object["ContentType"]
return {
"key": value,
"filename": filename,
"mimetype": mimetype,
"source": source,
}
except Exception as e:
print(e)
no_such_key = True

if no_such_key:
raise ValidationError(_("Object not found."))


class S3DirectDescriptor(object):
def __init__(self, field):
self.field = field

def __get__(self, obj, type=None):
try:
value = obj.__dict__.get(self.field.name, None)
assert len(value["key"]) > 0
except:
return None

file_url = get_presigned_url(
self.field.dest,
value["key"],
)

return {
**value,
"url": file_url,
}

def __set__(self, obj, value):
# presigned url not supported

if value is None or (isinstance(value, str) and len(value) == 0):
obj.__dict__[self.field.name] = None
return

current_value = obj.__dict__.get(self.field.name, None)

if value == current_value or (
current_value is not None and current_value["key"] == value
):
return

if not isinstance(value, (str, dict)):
raise Exception(_("Not a str or a dict."))

if isinstance(value, dict):
# Got from database, or added manually
obj.__dict__[self.field.name] = value
return

try:
obj.__dict__[self.field.name] = validate_url(value, self.field.dest)
except:
if current_value is None:
raise Exception(_("Invalid input."))
return
else:
new_file_url = get_presigned_url(
self.field.dest,
value,
)
current_file_url = current_value.split("?")[0]
new_file_url = new_file_url.split("?")[0]
if current_file_url == new_file_url:
return
else:
raise Exception(_("Object not found."))
return


class S3DirectField(JSONField):
def __init__(
self,
*args,
**kwargs,
):
self.dest = kwargs.pop("dest", None)
self.expires_in = kwargs.pop("expires_in", 3600)
self.widget = S3DirectWidget(dest=self.dest)
kwargs["null"] = True
kwargs["blank"] = True
super().__init__(*args, **kwargs)

class S3DirectField(Field):
def __init__(self, *args, **kwargs):
dest = kwargs.pop('dest', None)
self.widget = S3DirectWidget(dest=dest)
super(S3DirectField, self).__init__(*args, **kwargs)
def contribute_to_class(self, cls, name):
super().contribute_to_class(cls, name)
setattr(cls, name, S3DirectDescriptor(self))

def get_internal_type(self):
return 'TextField'
return "JSONField"

def formfield(self, *args, **kwargs):
kwargs['widget'] = self.widget
return super(S3DirectField, self).formfield(*args, **kwargs)
return Field.formfield(
self, *args, widget=self.widget, form_class=forms.CharField
)
2 changes: 1 addition & 1 deletion s3direct/static/s3direct/dist/index.js

Large diffs are not rendered by default.

39 changes: 27 additions & 12 deletions s3direct/templates/s3direct/s3direct-widget.tpl
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
<div class="s3direct" data-policy-url="{{ policy_url }}" data-signing-url="{{ signing_url }}">
<a class="file-link" target="_blank" href="{{ file_url }}">{{ file_name }}</a>
<a class="file-remove" href="#remove">Remove</a>
<input class="csrf-cookie-name" type="hidden" value="{{ csrf_cookie_name }}">
<input class="file-url" type="hidden" value="{{ file_url }}" id="{{ element_id }}" name="{{ name }}" />
<input class="file-dest" type="hidden" value="{{ dest }}">
<input class="file-input" type="file" style="{{ style }}"/>
<div class="progress progress-striped active">
<div class="bar"></div>
<a href="#cancel" class="cancel-button">&times;</a>
</div>
</div>
<div class="s3direct"
data-policy-url="{{ policy_url }}"
data-signing-url="{{ signing_url }}">
<a class="file-link"
target="_blank"
href="{{ file_url }}">{{ file_name }}</a>
<a class="file-remove"
href="#remove">Remove</a>
<input class="csrf-cookie-name"
type="hidden"
value="{{ csrf_cookie_name }}">
<input class="file-url"
type="hidden"
value="{{ file_key }}"
id="{{ element_id }}"
name="{{ name }}" />
<input class="file-dest"
type="hidden"
value="{{ dest }}">
<input class="file-input"
type="file" />
<div class="progress progress-striped active">
<div class="bar"></div>
<a href="#cancel"
class="cancel-button">&times;</a>
</div>
</div>
Loading
Loading