Source code for assembl.views.api2.attachments

from builtins import str
from datetime import datetime, timedelta

from pyramid.compat import url_quote
from pyramid.view import view_config
from pyramid.response import Response, FileIter, _BLOCK_SIZE
from pyramid.httpexceptions import (
    HTTPServerError, HTTPNotAcceptable, HTTPRequestRangeNotSatisfiable)
from pyramid.security import authenticated_userid, Everyone
from pyramid.settings import asbool

from assembl.lib import config
from assembl.auth import P_READ, P_ADD_POST
from assembl.models import File, Document, Discussion
from assembl.views.traversal import InstanceContext, CollectionContext
from assembl.lib.raven_client import capture_message
from . import MULTIPART_HEADER, update_from_form


@view_config(context=InstanceContext, request_method='DELETE',
             permission=P_READ, ctx_instance_class=Document,
             renderer='json')
def delete_file(request):
    # If there is no attachment, delete it and return positive.
    # Else, just return blank.
    # This API endpoint should never fail
    ctx = request.context
    db = Document.default_db
    document = ctx._instance
    attachments = document.attachments
    identity = None
    if isinstance(document, File):
        identity = document.file_identity
        count = document.count_other_files()
    try:
        if not attachments:
            db.delete(document)
            db.flush()
    except:
        capture_message("[HTTP DELETE] Failed to delete Document %d" %
                        document.id)
    if identity and not count:
        document.delete_file_by_id(identity)

    return {}


def disposition(title):
    escaped_double_quotes_filename = title.replace(u'"', u'\\"'
        ).encode('iso-8859-1', 'replace').decode('iso-8859-1')
    url_quoted_utf8_filename = url_quote(title.encode('utf-8'))
    return 'attachment; filename="%s"; filename*=utf-8\'\'%s' % (
        escaped_double_quotes_filename, url_quoted_utf8_filename)


@view_config(context=InstanceContext, request_method='HEAD',
             permission=P_READ, ctx_instance_class=File,
             name='data')
def get_file_header(request):
    ctx = request.context
    document = ctx._instance
    f = File.get(document.id)
    if f.infected:
        raise HTTPNotAcceptable("Infected with a virus")
    handoff_to_nginx = asbool(config.get('handoff_to_nginx', False))
    return Response(
        content_length=f.file_size,
        content_type=str(f.mime_type),
        last_modified=f.creation_date,
        expires=datetime.now() + timedelta(days=365),
        accept_ranges="bytes" if handoff_to_nginx else "none",
        content_disposition=disposition(f.title),  # RFC 6266
    )


@view_config(context=InstanceContext, request_method='GET',
             permission=P_READ, ctx_instance_class=File,
             name='data')
def get_file(request):
    # TODO: Add a route that enables the call to have the filename
    # appended to the end. This is so that gmail can read the services
    # Read more here:
    # http://stackoverflow.com/questions/20903967/gmails-new-image-caching-is-breaking-image-links-in-newsletter
    if request.method == 'HEAD':
        # GET view_config captures HEAD...
        return get_file_header(request)
    ctx = request.context
    document = ctx._instance
    f = File.get(document.id)
    if f.infected:
        raise HTTPNotAcceptable("Infected with a virus")
    handoff_to_nginx = asbool(config.get('handoff_to_nginx', False))
    if handoff_to_nginx:
        kwargs = dict()
    else:
        if 'Range' in request.headers:
            raise HTTPRequestRangeNotSatisfiable()
        fs = f.file_stream
        app_iter = None
        environ = request.environ
        if 'wsgi.file_wrapper' in environ and f.path:
            app_iter = environ['wsgi.file_wrapper'](fs, _BLOCK_SIZE)
        if app_iter is None:
            app_iter = FileIter(fs, _BLOCK_SIZE)
        kwargs = dict(app_iter=app_iter)

    r = Response(
        content_length=f.file_size,
        content_type=str(f.mime_type),
        last_modified=f.creation_date,
        expires=datetime.now() + timedelta(days=365),
        accept_ranges="bytes" if handoff_to_nginx else "none",
        content_disposition=disposition(f.title),  # RFC 6266
        **kwargs
    )
    if handoff_to_nginx:
        r.headers['X-Accel-Redirect'] = f.handoff_url.decode('ascii')
    return r



# Maybe have a permission for uploading content??


[docs]@view_config(context=CollectionContext, request_method='POST', header=MULTIPART_HEADER, permission=P_ADD_POST, ctx_collection_class=Document, renderer='json') def upload_file(request): """ POSTing a file upload is very different than any other endpoint in assembl API because all of the content will be passed in using a MULTIPART_HEADER, with all of data as well as the file (along with its metadata) """ # Testing purposes on front-end # raise Exception("Upload file exception occured!") db = Document.default_db ctx = request.context user_id = authenticated_userid(request) or Everyone discusison_id = ctx.get_discussion_id() discussion = Discussion.get(discusison_id) mime = request.POST['mime_type'] file_name = request.POST['name'] # Check if the file has previously existed, if so, change the name by appending "(n)" # to it's name try: blob = File(discussion=discussion, mime_type=mime, title=file_name) db.add(blob) with request.POST['file'].file as f: blob.add_file_data(f) db.flush() except Exception as e: raise HTTPServerError(e) view = 'default' return blob.generic_json(view, user_id, ctx.get_permissions())
@view_config(context=InstanceContext, request_method=('PUT', 'PATCH'), header=MULTIPART_HEADER, permission=P_ADD_POST, ctx_instance_class=Document, renderer='json') def update_upload_file(request): ctx = request.context instance = ctx._instance user_id = authenticated_userid(request) or Everyone try: form_data = request.POST # form_data['title'] = form_data['name'] # with request.POST['file'].file as f: # data = f.read() # form_data['data'] = data # discussion = discussionssion.get(discussion_id) # form_data['discussion'] = discussion new_form = { 'title': form_data['name'] } # On a PUT operation, remove all of the keys from the form_data, except for # title (the name of the file. That's the only thing that the user can change # in the future) update_from_form(instance, new_form) except: raise HTTPServerError