From e99b5ef61f53daa01ac59556b641f1cd01c25b93 Mon Sep 17 00:00:00 2001 From: jakewaldron Date: Wed, 11 Mar 2015 12:54:09 -0700 Subject: [PATCH] Added Support for Cloudinary --- scripts/cloudinary/.gitignore | 1 + scripts/cloudinary/__init__.py | 125 +++++ scripts/cloudinary/__init__.pyc | Bin 0 -> 5768 bytes scripts/cloudinary/api.py | 243 ++++++++++ scripts/cloudinary/api.pyc | Bin 0 -> 13541 bytes scripts/cloudinary/compat.py | 36 ++ scripts/cloudinary/compat.pyc | Bin 0 -> 1685 bytes scripts/cloudinary/forms.py | 114 +++++ scripts/cloudinary/models.py | 78 ++++ scripts/cloudinary/poster/__init__.py | 34 ++ scripts/cloudinary/poster/__init__.pyc | Bin 0 -> 621 bytes scripts/cloudinary/poster/encode.py | 441 ++++++++++++++++++ scripts/cloudinary/poster/encode.pyc | Bin 0 -> 16307 bytes scripts/cloudinary/poster/streaminghttp.py | 201 ++++++++ scripts/cloudinary/poster/streaminghttp.pyc | Bin 0 -> 8428 bytes .../templates/cloudinary_direct_upload.html | 12 + .../templates/cloudinary_includes.html | 14 + .../templates/cloudinary_js_config.html | 3 + scripts/cloudinary/templatetags/__init__.py | 1 + scripts/cloudinary/templatetags/cloudinary.py | 67 +++ scripts/cloudinary/uploader.py | 234 ++++++++++ scripts/cloudinary/uploader.pyc | Bin 0 -> 8887 bytes scripts/cloudinary/utils.py | 373 +++++++++++++++ scripts/cloudinary/utils.pyc | Bin 0 -> 16051 bytes 24 files changed, 1977 insertions(+) create mode 100644 scripts/cloudinary/.gitignore create mode 100644 scripts/cloudinary/__init__.py create mode 100644 scripts/cloudinary/__init__.pyc create mode 100644 scripts/cloudinary/api.py create mode 100644 scripts/cloudinary/api.pyc create mode 100644 scripts/cloudinary/compat.py create mode 100644 scripts/cloudinary/compat.pyc create mode 100644 scripts/cloudinary/forms.py create mode 100644 scripts/cloudinary/models.py create mode 100644 scripts/cloudinary/poster/__init__.py create mode 100644 scripts/cloudinary/poster/__init__.pyc create mode 100644 scripts/cloudinary/poster/encode.py create mode 100644 scripts/cloudinary/poster/encode.pyc create mode 100644 scripts/cloudinary/poster/streaminghttp.py create mode 100644 scripts/cloudinary/poster/streaminghttp.pyc create mode 100644 scripts/cloudinary/templates/cloudinary_direct_upload.html create mode 100644 scripts/cloudinary/templates/cloudinary_includes.html create mode 100644 scripts/cloudinary/templates/cloudinary_js_config.html create mode 100644 scripts/cloudinary/templatetags/__init__.py create mode 100644 scripts/cloudinary/templatetags/cloudinary.py create mode 100644 scripts/cloudinary/uploader.py create mode 100644 scripts/cloudinary/uploader.pyc create mode 100644 scripts/cloudinary/utils.py create mode 100644 scripts/cloudinary/utils.pyc diff --git a/scripts/cloudinary/.gitignore b/scripts/cloudinary/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/scripts/cloudinary/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/scripts/cloudinary/__init__.py b/scripts/cloudinary/__init__.py new file mode 100644 index 0000000..d483c65 --- /dev/null +++ b/scripts/cloudinary/__init__.py @@ -0,0 +1,125 @@ +from __future__ import absolute_import + +import os +import sys + +CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net" +OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net" +AKAMAI_SHARED_CDN = "res.cloudinary.com" +SHARED_CDN = AKAMAI_SHARED_CDN +CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" + +VERSION = "1.0.21" +USER_AGENT = "cld-python-" + VERSION + +from cloudinary import utils +from cloudinary.compat import urlparse, parse_qs + +def import_django_settings(): + try: + import django.conf + from django.core.exceptions import ImproperlyConfigured + try: + if 'CLOUDINARY' in dir(django.conf.settings): + return django.conf.settings.CLOUDINARY + else: + return None + except ImproperlyConfigured: + return None + except ImportError: + return None + +class Config(object): + def __init__(self): + django_settings = import_django_settings() + if django_settings: + self.update(**django_settings) + elif os.environ.get("CLOUDINARY_CLOUD_NAME"): + self.update( + cloud_name = os.environ.get("CLOUDINARY_CLOUD_NAME"), + api_key = os.environ.get("CLOUDINARY_API_KEY"), + api_secret = os.environ.get("CLOUDINARY_API_SECRET"), + secure_distribution = os.environ.get("CLOUDINARY_SECURE_DISTRIBUTION"), + private_cdn = os.environ.get("CLOUDINARY_PRIVATE_CDN") == 'true' + ) + elif os.environ.get("CLOUDINARY_URL"): + uri = urlparse(os.environ.get("CLOUDINARY_URL").replace("cloudinary://", "http://")) + for k, v in parse_qs(uri.query).items(): + self.__dict__[k] = v[0] + self.update( + cloud_name = uri.hostname, + api_key = uri.username, + api_secret = uri.password, + private_cdn = uri.path != '' + ) + if uri.path != '': + self.update(secure_distribution = uri.path[1:]) + def __getattr__(self, i): + if i in self.__dict__: + return self.__dict__[i] + else: + return None + + def update(self, **keywords): + for k, v in keywords.items(): + self.__dict__[k] = v + +_config = Config() + +def config(**keywords): + global _config + _config.update(**keywords) + return _config + +def reset_config(): + global _config + _config = Config() + +class CloudinaryImage(object): + def __init__(self, public_id = None, format = None, version = None, + signature = None, url_options = {}, metadata = None, type = None): + + self.metadata = metadata + metadata = metadata or {} + self.public_id = public_id or metadata.get('public_id') + self.format = format or metadata.get('format') + self.version = version or metadata.get('version') + self.signature = signature or metadata.get('signature') + self.type = type or metadata.get('type') or "upload" + self.url_options = url_options + + def __unicode__(self): + return self.public_id + + def validate(self): + expected = utils.api_sign_request({"public_id": self.public_id, "version": self.version}, config().api_secret) + return self.signature == expected + + @property + def url(self): + return self.build_url(**self.url_options) + + def __build_url(self, **options): + combined_options = dict(format = self.format, version = self.version, type = self.type) + combined_options.update(options) + return utils.cloudinary_url(self.public_id, **combined_options) + + def build_url(self, **options): + return self.__build_url(**options)[0] + + def image(self, **options): + src, attrs = self.__build_url(**options) + responsive = attrs.pop("responsive", False) + hidpi = attrs.pop("hidpi", False) + if responsive or hidpi: + attrs["data-src"] = src + classes = "cld-responsive" if responsive else "cld-hidpi" + if "class" in attrs: classes += " " + attrs["class"] + attrs["class"] = classes + src = attrs.pop("responsive_placeholder", config().responsive_placeholder) + if src == "blank": src = CL_BLANK + + attrs = sorted(attrs.items()) + if src: attrs.insert(0, ("src", src)) + + return u"".format(' '.join([u"{0}='{1}'".format(key, value) for key, value in attrs if value])) diff --git a/scripts/cloudinary/__init__.pyc b/scripts/cloudinary/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f50d39f832642c8354934860f57866482072f640 GIT binary patch literal 5768 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 Lj33{)Iy&?}kNg>y literal 0 HcmV?d00001 diff --git a/scripts/cloudinary/compat.py b/scripts/cloudinary/compat.py new file mode 100644 index 0000000..ff2b749 --- /dev/null +++ b/scripts/cloudinary/compat.py @@ -0,0 +1,36 @@ +# Copyright Cloudinary +import sys + +PY3 = (sys.version_info[0] == 3) + +if PY3: + import http.client as httplib + NotConnected = httplib.NotConnected + import urllib.request as urllib2 + import urllib.error + HTTPError = urllib.error.HTTPError + from io import StringIO, BytesIO + from urllib.parse import urlencode, unquote, urlparse, parse_qs, quote_plus + to_bytes = lambda s: s.encode('utf8') + to_bytearray = lambda s: bytearray(s, 'utf8') + to_string = lambda b: b.decode('utf8') + string_types = (str) +else: + import httplib + from httplib import NotConnected + from io import BytesIO + import StringIO + import urllib2 + HTTPError = urllib2.HTTPError + from urllib import urlencode, unquote, quote_plus + from urlparse import urlparse, parse_qs + to_bytes = str + to_bytearray = str + to_string = str + string_types = (str, unicode) + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() diff --git a/scripts/cloudinary/compat.pyc b/scripts/cloudinary/compat.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8ca06c82ebdd12d27de0a682d7013f80cc2a2f1 GIT binary patch literal 1685 zcmbtTOK;Oa5dQ2mY4d9Oe)EtxkW&wEK^3YHRV8{!E4LD0Rf=4D15W)&z3ZxxxRnbR z&RqCA{0jaBKLBRNNt>P!#habX&hE~9^UZAOSF`pl`FY}F^p)xV6;1us0s{J3hyWK* zK;J@OV?8M0+=gpI*~os350%iLf?I|vqhEnr8QZ7PpMhH)FKck?P<8Yh=r`du#Xf~- z7I6!13-KJ>IanOIjA$P50^9|}i*OeaFTq_xybO04@e14(I#eGoq|R zZNS|`@_ih+%;?x}6;Tww``(n`-G$oOTNG!agG90&OsV?Df`Z^(L z2umc|HVHFB;@D(i^5NjnOw;1^&?tRy*yeJYDv3OcRN|+BlI=XX$Wx=_9_r(4whnp#erb&5cI zDN$FZSh3sC+%FBoDR{x9m-wn18kKn_&7P1=vsYN{$0QA7)2n3r;U%s47U9!REd#h9I4z4ycLCK4)>B_K@Wk}+nKt^l{WHDCiPRN=zmY3sct&2Xv3~&6ALu%;9Er=Gir4?0B&fjJKr{?cbkfoCbNMb~#nOL!x0X k+7+v1RcSV8tXOT*W!MdCjs{z+))wtlWHjuORkNDbAFWYG!T\d+)/)?(?P.*?)(\.(?P[^.]+))?$', value) + return CloudinaryImage(type = self.type, **m.groupdict()) + + def upload_options_with_filename(self, model_instance, filename): + return self.upload_options(model_instance); + + def upload_options(self, model_instance): + return {} + + def pre_save(self, model_instance, add): + value = super(CloudinaryField, self).pre_save(model_instance, add) + if isinstance(value, UploadedFile): + options = {"type": self.type} + options.update(self.upload_options_with_filename(model_instance, value.name)) + instance_value = uploader.upload_image(value, **options) + setattr(model_instance, self.attname, instance_value) + return self.get_prep_value(instance_value) + else: + return value + + def get_prep_value(self, value): + prep = '' + if not value: + return None + if isinstance(value, CloudinaryImage): + if value.version: prep = prep + 'v' + str(value.version) + '/' + prep = prep + value.public_id + if value.format: prep = prep + '.' + value.format + return prep + else: + return value + + def formfield(self, **kwargs): + options = {"type": "upload"} + options.update(kwargs.pop('options', {})) + defaults = {'form_class': self.default_form_class, 'options': options} + defaults.update(kwargs) + return super(CloudinaryField, self).formfield(autosave=False, **defaults) diff --git a/scripts/cloudinary/poster/__init__.py b/scripts/cloudinary/poster/__init__.py new file mode 100644 index 0000000..9110fa4 --- /dev/null +++ b/scripts/cloudinary/poster/__init__.py @@ -0,0 +1,34 @@ +# MIT licensed code copied from https://bitbucket.org/chrisatlee/poster +# +# Copyright (c) 2011 Chris AtLee +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""poster module + +Support for streaming HTTP uploads, and multipart/form-data encoding + +```poster.version``` is a 3-tuple of integers representing the version number. +New releases of poster will always have a version number that compares greater +than an older version of poster. +New in version 0.6.""" + +import cloudinary.poster.streaminghttp +import cloudinary.poster.encode + +version = (0, 8, 2) # Thanks JP! diff --git a/scripts/cloudinary/poster/__init__.pyc b/scripts/cloudinary/poster/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5450364045d666c830f4a9f545b37dd3facd290c GIT binary patch literal 621 zcmZ8e!Ab)$5KXsQWvO>B;@s>(*Mi_dLow81Kc@>);?F z&N@QWE)aUCoN5fF>1$Kh&J#?Xhd>@>rR@x!Mxz&KYU88|=OC?usxhQ%>FJUys|zJb z0@~6kE>F|RWU}VX-q8o`EC)abkZ^TD%!A;jpe^By)8Mi87|>E|MRSA}1h%Q(&}Zp0 zF1W~`3>YG^oq3^+0cjR;8DK8ok?D7lOeP6RSMi{XobfumYsyi}OMo+qH{U?Eyz742 zZZpH|I@3RVo;9l`^kFm!n?cn6e{7#)6y+@n<;tRa8kR=-W!4I_6?aZlQ@T?_ z$2->zWTh{zxZ9!QlZ!tShJ1av$*0<2$XVu($4Y9G|8>vT-SeW*R#Q=A^^)Q`x9j)6 PJ7GWc*z}Tqr=R=+0}8nR literal 0 HcmV?d00001 diff --git a/scripts/cloudinary/poster/encode.py b/scripts/cloudinary/poster/encode.py new file mode 100644 index 0000000..0104cd6 --- /dev/null +++ b/scripts/cloudinary/poster/encode.py @@ -0,0 +1,441 @@ +# MIT licensed code copied from https://bitbucket.org/chrisatlee/poster +"""multipart/form-data encoding module + +This module provides functions that faciliate encoding name/value pairs +as multipart/form-data suitable for a HTTP POST or PUT request. + +multipart/form-data is the standard way to upload files over HTTP""" + +__all__ = ['gen_boundary', 'encode_and_quote', 'MultipartParam', + 'encode_string', 'encode_file_header', 'get_body_size', 'get_headers', + 'multipart_encode'] + +try: + from io import UnsupportedOperation +except ImportError: + UnsupportedOperation = None + +try: + import uuid + def gen_boundary(): + """Returns a random string to use as the boundary for a message""" + return uuid.uuid4().hex +except ImportError: + import random, sha + def gen_boundary(): + """Returns a random string to use as the boundary for a message""" + bits = random.getrandbits(160) + return sha.new(str(bits)).hexdigest() + +import re, os, mimetypes +from cloudinary.compat import (PY3, string_types, to_bytes, to_string, + to_bytearray, quote_plus, advance_iterator) +try: + from email.header import Header +except ImportError: + # Python 2.4 + from email.Header import Header + +if PY3: + def encode_and_quote(data): + if data is None: + return None + return quote_plus(to_bytes(data)) + +else: + def encode_and_quote(data): + """If ``data`` is unicode, return quote_plus(data.encode("utf-8")) otherwise return quote_plus(data)""" + if data is None: + return None + + if isinstance(data, unicode): + data = data.encode("utf-8") + return quote_plus(data) + +if PY3: + def _strify(s): + if s is None: + return None + return to_bytes(s) +else: + def _strify(s): + """If s is a unicode string, encode it to UTF-8 and return the results, otherwise return str(s), or None if s is None""" + if s is None: + return None + if isinstance(s, unicode): + return s.encode("utf-8") + return str(s) + +class MultipartParam(object): + """Represents a single parameter in a multipart/form-data request + + ``name`` is the name of this parameter. + + If ``value`` is set, it must be a string or unicode object to use as the + data for this parameter. + + If ``filename`` is set, it is what to say that this parameter's filename + is. Note that this does not have to be the actual filename any local file. + + If ``filetype`` is set, it is used as the Content-Type for this parameter. + If unset it defaults to "text/plain; charset=utf8" + + If ``filesize`` is set, it specifies the length of the file ``fileobj`` + + If ``fileobj`` is set, it must be a file-like object that supports + .read(). + + Both ``value`` and ``fileobj`` must not be set, doing so will + raise a ValueError assertion. + + If ``fileobj`` is set, and ``filesize`` is not specified, then + the file's size will be determined first by stat'ing ``fileobj``'s + file descriptor, and if that fails, by seeking to the end of the file, + recording the current position as the size, and then by seeking back to the + beginning of the file. + + ``cb`` is a callable which will be called from iter_encode with (self, + current, total), representing the current parameter, current amount + transferred, and the total size. + """ + def __init__(self, name, value=None, filename=None, filetype=None, + filesize=None, fileobj=None, cb=None): + self.name = Header(name).encode() + self.value = _strify(value) + if filename is None: + self.filename = None + else: + if PY3: + byte_filename = filename.encode("ascii", "xmlcharrefreplace") + self.filename = to_string(byte_filename) + encoding = 'unicode_escape' + else: + if isinstance(filename, unicode): + # Encode with XML entities + self.filename = filename.encode("ascii", "xmlcharrefreplace") + else: + self.filename = str(filename) + encoding = 'string_escape' + self.filename = self.filename.encode(encoding).replace(to_bytes('"'), to_bytes('\\"')) + self.filetype = _strify(filetype) + + self.filesize = filesize + self.fileobj = fileobj + self.cb = cb + + if self.value is not None and self.fileobj is not None: + raise ValueError("Only one of value or fileobj may be specified") + + if fileobj is not None and filesize is None: + # Try and determine the file size + try: + self.filesize = os.fstat(fileobj.fileno()).st_size + except (OSError, AttributeError, UnsupportedOperation): + try: + fileobj.seek(0, 2) + self.filesize = fileobj.tell() + fileobj.seek(0) + except: + raise ValueError("Could not determine filesize") + + def __cmp__(self, other): + attrs = ['name', 'value', 'filename', 'filetype', 'filesize', 'fileobj'] + myattrs = [getattr(self, a) for a in attrs] + oattrs = [getattr(other, a) for a in attrs] + return cmp(myattrs, oattrs) + + def reset(self): + if self.fileobj is not None: + self.fileobj.seek(0) + elif self.value is None: + raise ValueError("Don't know how to reset this parameter") + + @classmethod + def from_file(cls, paramname, filename): + """Returns a new MultipartParam object constructed from the local + file at ``filename``. + + ``filesize`` is determined by os.path.getsize(``filename``) + + ``filetype`` is determined by mimetypes.guess_type(``filename``)[0] + + ``filename`` is set to os.path.basename(``filename``) + """ + + return cls(paramname, filename=os.path.basename(filename), + filetype=mimetypes.guess_type(filename)[0], + filesize=os.path.getsize(filename), + fileobj=open(filename, "rb")) + + @classmethod + def from_params(cls, params): + """Returns a list of MultipartParam objects from a sequence of + name, value pairs, MultipartParam instances, + or from a mapping of names to values + + The values may be strings or file objects, or MultipartParam objects. + MultipartParam object names must match the given names in the + name,value pairs or mapping, if applicable.""" + if hasattr(params, 'items'): + params = params.items() + + retval = [] + for item in params: + if isinstance(item, cls): + retval.append(item) + continue + name, value = item + if isinstance(value, cls): + assert value.name == name + retval.append(value) + continue + if hasattr(value, 'read'): + # Looks like a file object + filename = getattr(value, 'name', None) + if filename is not None: + filetype = mimetypes.guess_type(filename)[0] + else: + filetype = None + + retval.append(cls(name=name, filename=filename, + filetype=filetype, fileobj=value)) + else: + retval.append(cls(name, value)) + return retval + + def encode_hdr(self, boundary): + """Returns the header of the encoding of this parameter""" + boundary = encode_and_quote(boundary) + + headers = ["--%s" % boundary] + + if self.filename: + disposition = 'form-data; name="%s"; filename="%s"' % (self.name, + to_string(self.filename)) + else: + disposition = 'form-data; name="%s"' % self.name + + headers.append("Content-Disposition: %s" % disposition) + + if self.filetype: + filetype = to_string(self.filetype) + else: + filetype = "text/plain; charset=utf-8" + + headers.append("Content-Type: %s" % filetype) + + headers.append("") + headers.append("") + + return "\r\n".join(headers) + + def encode(self, boundary): + """Returns the string encoding of this parameter""" + if self.value is None: + value = self.fileobj.read() + else: + value = self.value + + if re.search("^--%s$" % re.escape(boundary), value, re.M): + raise ValueError("boundary found in encoded string") + + return "%s%s\r\n" % (self.encode_hdr(boundary), value) + + def iter_encode(self, boundary, blocksize=4096): + """Yields the encoding of this parameter + If self.fileobj is set, then blocks of ``blocksize`` bytes are read and + yielded.""" + total = self.get_size(boundary) + current = 0 + if self.value is not None: + block = to_bytes(self.encode(boundary)) + current += len(block) + yield block + if self.cb: + self.cb(self, current, total) + else: + block = to_bytes(self.encode_hdr(boundary)) + current += len(block) + yield block + if self.cb: + self.cb(self, current, total) + last_block = to_bytearray("") + encoded_boundary = "--%s" % encode_and_quote(boundary) + boundary_exp = re.compile(to_bytes("^%s$" % re.escape(encoded_boundary)), + re.M) + while True: + block = self.fileobj.read(blocksize) + if not block: + current += 2 + yield to_bytes("\r\n") + if self.cb: + self.cb(self, current, total) + break + last_block += block + if boundary_exp.search(last_block): + raise ValueError("boundary found in file data") + last_block = last_block[-len(to_bytes(encoded_boundary))-2:] + current += len(block) + yield block + if self.cb: + self.cb(self, current, total) + + def get_size(self, boundary): + """Returns the size in bytes that this param will be when encoded + with the given boundary.""" + if self.filesize is not None: + valuesize = self.filesize + else: + valuesize = len(self.value) + + return len(self.encode_hdr(boundary)) + 2 + valuesize + +def encode_string(boundary, name, value): + """Returns ``name`` and ``value`` encoded as a multipart/form-data + variable. ``boundary`` is the boundary string used throughout + a single request to separate variables.""" + + return MultipartParam(name, value).encode(boundary) + +def encode_file_header(boundary, paramname, filesize, filename=None, + filetype=None): + """Returns the leading data for a multipart/form-data field that contains + file data. + + ``boundary`` is the boundary string used throughout a single request to + separate variables. + + ``paramname`` is the name of the variable in this request. + + ``filesize`` is the size of the file data. + + ``filename`` if specified is the filename to give to this field. This + field is only useful to the server for determining the original filename. + + ``filetype`` if specified is the MIME type of this file. + + The actual file data should be sent after this header has been sent. + """ + + return MultipartParam(paramname, filesize=filesize, filename=filename, + filetype=filetype).encode_hdr(boundary) + +def get_body_size(params, boundary): + """Returns the number of bytes that the multipart/form-data encoding + of ``params`` will be.""" + size = sum(p.get_size(boundary) for p in MultipartParam.from_params(params)) + return size + len(boundary) + 6 + +def get_headers(params, boundary): + """Returns a dictionary with Content-Type and Content-Length headers + for the multipart/form-data encoding of ``params``.""" + headers = {} + boundary = quote_plus(boundary) + headers['Content-Type'] = "multipart/form-data; boundary=%s" % boundary + headers['Content-Length'] = str(get_body_size(params, boundary)) + return headers + +class multipart_yielder: + def __init__(self, params, boundary, cb): + self.params = params + self.boundary = boundary + self.cb = cb + + self.i = 0 + self.p = None + self.param_iter = None + self.current = 0 + self.total = get_body_size(params, boundary) + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def next(self): + """generator function to yield multipart/form-data representation + of parameters""" + if self.param_iter is not None: + try: + block = advance_iterator(self.param_iter) + self.current += len(block) + if self.cb: + self.cb(self.p, self.current, self.total) + return block + except StopIteration: + self.p = None + self.param_iter = None + + if self.i is None: + raise StopIteration + elif self.i >= len(self.params): + self.param_iter = None + self.p = None + self.i = None + block = to_bytes("--%s--\r\n" % self.boundary) + self.current += len(block) + if self.cb: + self.cb(self.p, self.current, self.total) + return block + + self.p = self.params[self.i] + self.param_iter = self.p.iter_encode(self.boundary) + self.i += 1 + return advance_iterator(self) + + def reset(self): + self.i = 0 + self.current = 0 + for param in self.params: + param.reset() + +def multipart_encode(params, boundary=None, cb=None): + """Encode ``params`` as multipart/form-data. + + ``params`` should be a sequence of (name, value) pairs or MultipartParam + objects, or a mapping of names to values. + Values are either strings parameter values, or file-like objects to use as + the parameter value. The file-like objects must support .read() and either + .fileno() or both .seek() and .tell(). + + If ``boundary`` is set, then it as used as the MIME boundary. Otherwise + a randomly generated boundary will be used. In either case, if the + boundary string appears in the parameter values a ValueError will be + raised. + + If ``cb`` is set, it should be a callback which will get called as blocks + of data are encoded. It will be called with (param, current, total), + indicating the current parameter being encoded, the current amount encoded, + and the total amount to encode. + + Returns a tuple of `datagen`, `headers`, where `datagen` is a + generator that will yield blocks of data that make up the encoded + parameters, and `headers` is a dictionary with the assoicated + Content-Type and Content-Length headers. + + Examples: + + >>> datagen, headers = multipart_encode( [("key", "value1"), ("key", "value2")] ) + >>> s = "".join(datagen) + >>> assert "value2" in s and "value1" in s + + >>> p = MultipartParam("key", "value2") + >>> datagen, headers = multipart_encode( [("key", "value1"), p] ) + >>> s = "".join(datagen) + >>> assert "value2" in s and "value1" in s + + >>> datagen, headers = multipart_encode( {"key": "value1"} ) + >>> s = "".join(datagen) + >>> assert "value2" not in s and "value1" in s + + """ + if boundary is None: + boundary = gen_boundary() + else: + boundary = quote_plus(boundary) + + headers = get_headers(params, boundary) + params = MultipartParam.from_params(params) + + return multipart_yielder(params, boundary, cb), headers diff --git a/scripts/cloudinary/poster/encode.pyc b/scripts/cloudinary/poster/encode.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4639555963c60aef53b1f6e66eb784442c1c0de5 GIT binary patch literal 16307 zcmcIr&u<(@cCMZwDGo)7`bkNa*J?AXXq2UBuh&@{wrs2|X<-3tTHB;8FD+@znN<|d_!gwbj!|jKtWFh`(9_`} zJ#AK}l|EwHGp2LItR69)S+hE8I!DdwQPVkQR*#v^akF~dbWWJn6Q*<0te!N1pVO>q zpE8{}vpUB)y<86P6-+5$->7i9sKD_gM@;s9&^oFio-tiiIp)*+bSG#XSN5zvo$zUJ zHhIBpfXgRY^3<3gt)4TjQ~LZOXUPBO%;y!e`VG@Mt@4*l>x>fTO$$6lwHM09VbSKC z+Puu{a{U`z)qlmSGic{&KhE-3HhNj-ax2c`FzGgXt+cxtc6zNrJDHkV*-EX?g#E1d zAZ;Z!+!%D5dD`pRFyD&va3gM}?KIAlVe@X>NnUvnw+BhskJHRf#h|iJe|C`O@p?N6 zF@!Jl=Eo~xmfRmCHeZ~Y+OIW+mTx7Y&Esw>&RXHacqhzz;h^8{ z#jS87ZDXvx2T3Nn`84LdnRFZLy#cH5Gs-SHY^wawZMedHP@YSBILJ{3Ko< zckQ6x?`3(?`lz2|F&Vv7>LsgP#bY_B+?efvg0fw~!t#^7f>)H}gA9ukhZ*M8>wrNV zwOC`55KBR>6>RXV?iA=ZFT05B)wgbo*SGua zJj_WZLLxtPpocMB`L=HUb^iM#gKEZzZa%?LWkGb_Uw_<^Xz;mG8+3V;6@;3YJ4Q!E`VdJUisK@rgsO z6hnsUV8|%KsGn9$_HCviY^CgV@u?8JWzECJAaY|PTwCLy*VaN9qCq!>=}Z=2vZ#IG zkhgVKT6FSXpC9BKm*1SfbSdmX3A2YO6mtI-m#mbJ)tBl_M(6O-x}`Sl(&#mloC4vy zmgu@}#9y&CdJz>4&Q_|qrO22H*a}HJK4dL5Pl-_7lG@jaz<2#PzfuGic8S39-qp|09;~XR1-pDSvJunH=oc8wPmG57E zGlT&t7MYeeODr}RyYQ?CKw0bd5zFAulk2Pp4x{A57f%m;D1Y!8Xv3ERDlxpIPJb2 zHn-vob>4wleRF_4!s*o9)hmnq*;BXPZeD&7q5x&`8 z_%Lm^MK6oFam3*#gxJ@!3<48d3kV9ZxOX*nk9^n}0jgsU`zlz|S|D$_>V-_=NZT-lq>v={ zJWOTB3AiGab7NR=^G=$*Ob!aGGzVD*F$w`OQ_j2)FAmoA!l{hXUXPpiC_^Y0z4c@> z?RMGgNLQYLYirGQ$DcTaq_^d#Jlsl~TO}h{OeSOi*+fkq(c?Q7xNej7h8wmYC|KLe z<2DR@Rv0Nwcyua-rCKOw7kA*WFlH`JDYJnD2sW}N#$i12d`YO%h z-(rIWpnmLtvpfcD%8d%40U9wo~R6{LOk`$y$wCtSp$HLYQA?gA7gWT-JW1ak8Aw2J#FAt zVM{=oi3uh^))C?O#|jEZ%;RyD&zi>*N*pzh4=Hhs?`>>9m|e|e3jwo&t1b(N4jT+8 z|6X7K_t4ze{2~BN&yz2Lo%1F?VYW}Q!uBcp9~|MF@ATIFFM_fBw8_ty{H)oiD(>-D zj1la-sunN!%?ra1NZ}kQa3UJ;wmySkaO?hGvlRyg7HUu^`4r?o#o#cSZ~0riXfiCk3gGx!pMuX*`mx99C;WIpP)cS52M zsDyLjVO_jg3_I9cXqLPoZKY^nQaNESB6YPlXt%V1lr}<5sgZS7hzJseRpexHABzYy zG#B;eU@m2teX7Cy41=ZjvY8}JhCK*b^%|{l8V&NP-Q^g6ij0neIZ8Le%%}!rle?*@_D=)9iDHbYO^kYn{cAXtIvyzIQk_TG@ z4!Y662pWw)K+;lfOmHTc$A3+B%ZH2=L$4u0qvhNy*syt@Md-(QszLW96_qypdz3c- zZf8LX%TWcPC26hwO(d`Nx)<~CUbpu!+`@yck>D9Hau;-T{-Vi%mybQWKE8m4^h7OaT5vf16vwq?}A0(gxkz8XRI#Fz0X$7S%{#o zge^iWfka{{k>-}dk})So_PgNwFoK3SPq;fASb)4H;-7goXd+JM008%5VPfsVM6lG{ z5Kz!TB8S;MxZr13OzX6!5JG7(TZAkYcsBb~zUmiDD@0G5ji6?A;W~=?-m~1p&TR!)YH zvy&w%$SP!2j#%b-IO;-oWz5Gwqt{QmE~-SqXtrI%OU{`#4I0*i$zu%^2<>Ene}#;b z$?0>DNyI;>n^aov#ga+tfrDApNI)MbB=CXo))N5>m_8W5NB_z4Yj^%b1`tvU6ay%M z2Fnyk#GV{13hsYPXryXx18xBwL1a-|AcwX8kZFoJj#w(jKBWZ%fbk>*nLG)O3OEL0 zj%HH<1z#>Woav_kZO8)gObQ$QBT?NpaF~!5^yci7L#C_GNO;pvbu!oDuwz8m+9_NKfR26j4bCf3b__QGM*;|ap=K$0T<`@C zR&?I5u$M>?FtZMjQ*(%F9LY}H?|UDBl;mX!i`b!RtU$Y6S&4xZRmTZ~Lr2~&Fszg9 z?cMo>*5PyZz3&tiX5cvq*wBe{_!%@Qo9P4iFm7CkUZ80Av^6Mq%EF%gTo8fS2r;d8 z+N9sIs4WdHO24Tz0PvX&PAN58IgZ?1WukH>I8k}QX*xG2njLBSUm)Q$9Wo8ohQ33i=Qfdn zf?}HvO!gl2jcMm*sSvd{DrOG2rwd<3YLCy%Q(LfG%LkjIqOj$-$$l&!O=)&DCRH3~ z!nc6#jTzU%k&qt~_^&Pl)rL=nB9QQ!_c~@!>~fSccR*Em!3D~6`O%n{jv_8!zF_Td zfI@k2__hS}o%suP{%vi0lvic#$*-tqPvb)o2)dTq!ij$?1aY2vd_|b&@|%{}zi7!2 z5}RN-u&}hrGgEG(A%1dO(nY4^Kk)ELf-ZsEMqI@~8{t37XmKj0?WQ=jqX@NzBNTyy z2_?M&7piY;wX(lM&c28jqU)6zLG3yGJ%-eANa3-{OfXYva&A>Tr4$a3p_pKB*a2_= z^RkyW>>y9I)8ORURe)Gx#t%{rE}e85ye{0sJiotbhb$w5w8 z7GY6>K!+_7nH8JFS#wLt5$E>9h#)n(gO~7vzSIZ$&N2{w6YdvUk8*LIx{tjUC>C$+>~FBm zxcNkeV!~u!`UL23!-WLk2FCp_gKiu!N&@QGpMOnSf2sPAkx^Q3AWG=~Vu1Bxn8!&G zkOAP@_F)d2P#fuKVggDS9_D9=JrFZ`^rgX`k^Q}fJIg4HvrC-bxUrazyhTor#Dqf1 zNk;ok5(B%52S5Y_-XNHPxkJQ9O|#!o97RJZgF9d%d7?rL?=WD@ne%M;HT-oJc@IzS zgT;u~VAZ@?{e7(QkJ6;wauMdQwxi`9egj85h!ZaQ*hCp&hEYc368^+JAPgQgy0mi^ zbiy0P8PAe%(*cpdlHLw`Pg;v^WB-&gM}r}g&BR@3pNrQxTWP6I&uXGv7am3|)fs&e zPUl4i!tW6^4{9Nr7;p+3WP-6GdzM6tk2=TFC~93w%(a zqXu8{E2%j{ZIydaazTJbp-)HIg@RS8I&vS{F@iQO%RR%Jm*EW%Ds3_MM)IhCAX}jO z0wYo2e+O~bWTI@PWuCG@!Ve}^Uhm$3S7~WR4&TI(a#EaT(H%C* z;?$uKsNyq)gyx2S)vwrw(~;*<56bic)q@Fh@|1Iu zsWdF$U;74LP>`R(O_YZWY=m$+qqH(T*PaSH(qWc|{4 zSaxko$-ehF3h1TP7l3m9KLnSj=-}jH6XREeox}YO9?Od~7~P$dbn0O(vAw!seG=S< zZu8`gZI)cbdmTx4u0WPAzN*85zWYW~Bm?vu%(cWlS8!Doh$MUW`-KsjFex3VIvEW3 zis24A&H`$?Y4RJ3qZPh50HG@dkCE|VdX_)Om?Fy3)pEhB7j`fBRZ@KiuT*}gvx`{Z zVQj=%$SdHH10(bt6p-MDs=0zc%bJ?q0hwai16KI-@D2)roxfxFG& zeRikx&}eG$e)oL}`EcO6kc~w-ru)=?yWG6qL3o0=vFMip2um1I6u5!7r8vX65PFJ# zdzL5q3?C#U&nqbr1>y!*g#iUqWfbd3a5m6+a5;;H^9eFai2hNA-^XKr8?U6ex%Iib zyoIY;NI=@%RV{d9Rh1xipnF=lu8-?h6o)1ykq@O#67Vq+1Q~!#NJ!Hg$jbl3bpvjc z_#{nY0wkqxxMko!!H>k90#p-BVIAB7E3WyvV=DzGhcfOCOF5&|;WNk{9ZqAkrn<}; zZ6T`_h&GXum^r_qR27jq*jf-6E^{ro-Uhgg2c~)>C0oN$@)57dI83mxD9HVv9i&Zy z7LIp$A*FGbBcgpW`;38X!WdxYo(@OC5z2DF{0O^-R_;2_Q;=4OE9incf}pqV z|H6!6Q2c4i&-6PJC!>mMpf8%+a)tUa4Q4`L*lkA013d5uZGQjpCh&ck*q<>NImD~F5Whs&2|rcw%* z^SUD0c}5>1Mthx(Hkn%Fi)t)w)kTQqyw|@0YU(F?#BWkv#UBLw>0>#kfU(>N`_KVR z>rX%d%Vq3qNEkSbRk`pr+CLUZv*>L8HfQb7CQdl{+5S`*2 zo|8Q4et!`4IEX%9GKJ6)p-jLIDJHXsLn3&oa5#odMTEg~kv+Hbpk|ekZvX3& zE$p(_`NOCL9!!B0=m?iP?C>6O=^vE!hPXZkaxc#=!#e#Me`t9L`hts;&z>AQ^r~B> z?qCsK;#VG4I-lSZUI~Shj^JPdzy!eT0`nsmm;FJePAch^j9y;V;itQltj>v_N@aEp zJanAgOe z2OXw@nCO&E{CNitLvaV$xxCNtiqR6Sh`XfgT@Mk`9p07~7)`afZ(3Y29u*y4HP!9a zksGK8$m(V)CPLp(!D5ky0ajc=LLkX)aZ-6XO@DFK-BZ=_miJA|ORA!jb^*X)b-h1a z-ncEUslt!qHy;Gl-QhhKSIcjrxBJqnyV}Y<#u4z$7^43izvYt)y~g=q-PRVuH4pKT zL;w!-}5O(KD_I zRRw{-9vPOnh$8qu7{u1 z=kFyu^9$j;qIKV%#|`7%MX$|Yx)WZ~q)3%y=I0l6{^=Pz{OB%{ds?r}Q?S8SC}%-k zCBq^1LF~g_znFZc@3Ldx+5dhz{r|V&|5`)(i5UD=$$}@(%S>Li|2l>)xwC7*B5Z8N z{Zfu@-iWKuz0o-_0sz840!^Rs1D@v&Jwyz5|D4+{U0hS=ZBPWtah~bO&Z988kC%^& z!zdv}PKcBC+|5_U!*6u>`&D-tL^EMPgGcWsBH8E&Upy4jWw7W2q~x&J+ulcG3} zbo2KlYYOS<4F5E#n?V68BT4Cu%jk$7_>w|6DtSdUJ^O*G|0g-wD4)p8x;= literal 0 HcmV?d00001 diff --git a/scripts/cloudinary/poster/streaminghttp.py b/scripts/cloudinary/poster/streaminghttp.py new file mode 100644 index 0000000..d8af521 --- /dev/null +++ b/scripts/cloudinary/poster/streaminghttp.py @@ -0,0 +1,201 @@ +# MIT licensed code copied from https://bitbucket.org/chrisatlee/poster +"""Streaming HTTP uploads module. + +This module extends the standard httplib and urllib2 objects so that +iterable objects can be used in the body of HTTP requests. + +In most cases all one should have to do is call :func:`register_openers()` +to register the new streaming http handlers which will take priority over +the default handlers, and then you can use iterable objects in the body +of HTTP requests. + +**N.B.** You must specify a Content-Length header if using an iterable object +since there is no way to determine in advance the total size that will be +yielded, and there is no way to reset an interator. + +Example usage: + +>>> from StringIO import StringIO +>>> import urllib2, poster.streaminghttp + +>>> opener = poster.streaminghttp.register_openers() + +>>> s = "Test file data" +>>> f = StringIO(s) + +>>> req = urllib2.Request("http://localhost:5000", f, +... {'Content-Length': str(len(s))}) +""" + +import sys, socket +from cloudinary.compat import httplib, urllib2, NotConnected + +__all__ = ['StreamingHTTPConnection', 'StreamingHTTPRedirectHandler', + 'StreamingHTTPHandler', 'register_openers'] + +if hasattr(httplib, 'HTTPS'): + __all__.extend(['StreamingHTTPSHandler', 'StreamingHTTPSConnection']) + +class _StreamingHTTPMixin: + """Mixin class for HTTP and HTTPS connections that implements a streaming + send method.""" + def send(self, value): + """Send ``value`` to the server. + + ``value`` can be a string object, a file-like object that supports + a .read() method, or an iterable object that supports a .next() + method. + """ + # Based on python 2.6's httplib.HTTPConnection.send() + if self.sock is None: + if self.auto_open: + self.connect() + else: + raise NotConnected() + + # send the data to the server. if we get a broken pipe, then close + # the socket. we want to reconnect when somebody tries to send again. + # + # NOTE: we DO propagate the error, though, because we cannot simply + # ignore the error... the caller will know if they can retry. + if self.debuglevel > 0: + print("send:", repr(value)) + try: + blocksize = 8192 + if hasattr(value, 'read') : + if hasattr(value, 'seek'): + value.seek(0) + if self.debuglevel > 0: + print("sendIng a read()able") + data = value.read(blocksize) + while data: + self.sock.sendall(data) + data = value.read(blocksize) + elif hasattr(value, 'next'): + if hasattr(value, 'reset'): + value.reset() + if self.debuglevel > 0: + print("sendIng an iterable") + for data in value: + self.sock.sendall(data) + else: + self.sock.sendall(value) + except socket.error: + e = sys.exc_info()[1] + if e[0] == 32: # Broken pipe + self.close() + raise + +class StreamingHTTPConnection(_StreamingHTTPMixin, httplib.HTTPConnection): + """Subclass of `httplib.HTTPConnection` that overrides the `send()` method + to support iterable body objects""" + +class StreamingHTTPRedirectHandler(urllib2.HTTPRedirectHandler): + """Subclass of `urllib2.HTTPRedirectHandler` that overrides the + `redirect_request` method to properly handle redirected POST requests + + This class is required because python 2.5's HTTPRedirectHandler does + not remove the Content-Type or Content-Length headers when requesting + the new resource, but the body of the original request is not preserved. + """ + + handler_order = urllib2.HTTPRedirectHandler.handler_order - 1 + + # From python2.6 urllib2's HTTPRedirectHandler + def redirect_request(self, req, fp, code, msg, headers, newurl): + """Return a Request or None in response to a redirect. + + This is called by the http_error_30x methods when a + redirection response is received. If a redirection should + take place, return a new Request to allow http_error_30x to + perform the redirect. Otherwise, raise HTTPError if no-one + else should try to handle this url. Return None if you can't + but another Handler might. + """ + m = req.get_method() + if (code in (301, 302, 303, 307) and m in ("GET", "HEAD") + or code in (301, 302, 303) and m == "POST"): + # Strictly (according to RFC 2616), 301 or 302 in response + # to a POST MUST NOT cause a redirection without confirmation + # from the user (of urllib2, in this case). In practice, + # essentially all clients do redirect in this case, so we + # do the same. + # be conciliant with URIs containing a space + newurl = newurl.replace(' ', '%20') + newheaders = dict((k, v) for k, v in req.headers.items() + if k.lower() not in ( + "content-length", "content-type") + ) + return urllib2.Request(newurl, + headers=newheaders, + origin_req_host=req.get_origin_req_host(), + unverifiable=True) + else: + raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) + +class StreamingHTTPHandler(urllib2.HTTPHandler): + """Subclass of `urllib2.HTTPHandler` that uses + StreamingHTTPConnection as its http connection class.""" + + handler_order = urllib2.HTTPHandler.handler_order - 1 + + def http_open(self, req): + """Open a StreamingHTTPConnection for the given request""" + return self.do_open(StreamingHTTPConnection, req) + + def http_request(self, req): + """Handle a HTTP request. Make sure that Content-Length is specified + if we're using an interable value""" + # Make sure that if we're using an iterable object as the request + # body, that we've also specified Content-Length + if req.has_data(): + data = req.get_data() + if hasattr(data, 'read') or hasattr(data, 'next'): + if not req.has_header('Content-length'): + raise ValueError( + "No Content-Length specified for iterable body") + return urllib2.HTTPHandler.do_request_(self, req) + +if hasattr(httplib, 'HTTPS'): + class StreamingHTTPSConnection(_StreamingHTTPMixin, + httplib.HTTPSConnection): + """Subclass of `httplib.HTTSConnection` that overrides the `send()` + method to support iterable body objects""" + + class StreamingHTTPSHandler(urllib2.HTTPSHandler): + """Subclass of `urllib2.HTTPSHandler` that uses + StreamingHTTPSConnection as its http connection class.""" + + handler_order = urllib2.HTTPSHandler.handler_order - 1 + + def https_open(self, req): + return self.do_open(StreamingHTTPSConnection, req) + + def https_request(self, req): + # Make sure that if we're using an iterable object as the request + # body, that we've also specified Content-Length + if req.has_data(): + data = req.get_data() + if hasattr(data, 'read') or hasattr(data, 'next'): + if not req.has_header('Content-length'): + raise ValueError( + "No Content-Length specified for iterable body") + return urllib2.HTTPSHandler.do_request_(self, req) + + +def get_handlers(): + handlers = [StreamingHTTPHandler, StreamingHTTPRedirectHandler] + if hasattr(httplib, "HTTPS"): + handlers.append(StreamingHTTPSHandler) + return handlers + +def register_openers(): + """Register the streaming http handlers in the global urllib2 default + opener object. + + Returns the created OpenerDirector object.""" + opener = urllib2.build_opener(*get_handlers()) + + urllib2.install_opener(opener) + + return opener diff --git a/scripts/cloudinary/poster/streaminghttp.pyc b/scripts/cloudinary/poster/streaminghttp.pyc new file mode 100644 index 0000000000000000000000000000000000000000..744177055c21f93fa81e1b2ff2fd0515efa2ff1c GIT binary patch literal 8428 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 + {% for name, value in params.items %} + + {% endfor %} + {% block extra %} {% endblock %} + {% block file %} + + {% endblock %} + {% block submit %} + + {% endblock %} + diff --git a/scripts/cloudinary/templates/cloudinary_includes.html b/scripts/cloudinary/templates/cloudinary_includes.html new file mode 100644 index 0000000..be1fd15 --- /dev/null +++ b/scripts/cloudinary/templates/cloudinary_includes.html @@ -0,0 +1,14 @@ +{% load staticfiles %} + + + + + + +{% if processing %} + + + + + +{% endif %} diff --git a/scripts/cloudinary/templates/cloudinary_js_config.html b/scripts/cloudinary/templates/cloudinary_js_config.html new file mode 100644 index 0000000..dc7f489 --- /dev/null +++ b/scripts/cloudinary/templates/cloudinary_js_config.html @@ -0,0 +1,3 @@ + diff --git a/scripts/cloudinary/templatetags/__init__.py b/scripts/cloudinary/templatetags/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/scripts/cloudinary/templatetags/__init__.py @@ -0,0 +1 @@ +# diff --git a/scripts/cloudinary/templatetags/cloudinary.py b/scripts/cloudinary/templatetags/cloudinary.py new file mode 100644 index 0000000..b7afd0f --- /dev/null +++ b/scripts/cloudinary/templatetags/cloudinary.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import + +import json + +from django import template +from django.forms import Form + +import cloudinary +from cloudinary import CloudinaryImage, utils, uploader +from cloudinary.forms import CloudinaryJsFileField, cl_init_js_callbacks + +register = template.Library() + +@register.simple_tag(takes_context=True) +def cloudinary_url(context, source, options_dict={}, **options): + options = dict(options_dict, **options) + try: + if context['request'].is_secure() and 'secure' not in options: + options['secure'] = True + except KeyError: + pass + if not isinstance(source, CloudinaryImage): + source = CloudinaryImage(source) + return source.build_url(**options) + +@register.simple_tag(name='cloudinary', takes_context=True) +def cloudinary_tag(context, image, options_dict={}, **options): + options = dict(options_dict, **options) + try: + if context['request'].is_secure() and 'secure' not in options: + options['secure'] = True + except KeyError: + pass + if not isinstance(image, CloudinaryImage): + image = CloudinaryImage(image) + return image.image(**options) + +@register.simple_tag +def cloudinary_direct_upload_field(field_name="image", request=None): + form = type("OnTheFlyForm", (Form,), {field_name : CloudinaryJsFileField() })() + if request: + cl_init_js_callbacks(form, request) + return unicode(form[field_name]) + +"""Deprecated - please use cloudinary_direct_upload_field, or a proper form""" +@register.inclusion_tag('cloudinary_direct_upload.html') +def cloudinary_direct_upload(callback_url, **options): + params = utils.build_upload_params(callback=callback_url, **options) + params = utils.sign_request(params, options) + + api_url = utils.cloudinary_api_url("upload", resource_type=options.get("resource_type", "image"), upload_prefix=options.get("upload_prefix")) + + return {"params": params, "url": api_url} + +@register.inclusion_tag('cloudinary_includes.html') +def cloudinary_includes(processing=False): + return {"processing": processing} + +CLOUDINARY_JS_CONFIG_PARAMS = ("api_key", "cloud_name", "private_cdn", "secure_distribution", "cdn_subdomain") +@register.inclusion_tag('cloudinary_js_config.html') +def cloudinary_js_config(): + config = cloudinary.config() + return dict( + params = json.dumps(dict( + (param, getattr(config, param)) for param in CLOUDINARY_JS_CONFIG_PARAMS if getattr(config, param, None) + )) + ) diff --git a/scripts/cloudinary/uploader.py b/scripts/cloudinary/uploader.py new file mode 100644 index 0000000..aa1d002 --- /dev/null +++ b/scripts/cloudinary/uploader.py @@ -0,0 +1,234 @@ +# Copyright Cloudinary +import json, re, sys +from os.path import basename +import urllib +import cloudinary +import socket +from cloudinary import utils +from cloudinary.api import Error +from cloudinary.poster.encode import multipart_encode +from cloudinary.poster.streaminghttp import register_openers +from cloudinary.compat import urllib2, BytesIO, string_types, urlencode, to_bytes, to_string, PY3, HTTPError +_initialized = False + +def upload(file, **options): + params = utils.build_upload_params(**options) + return call_api("upload", params, file = file, **options) + +def unsigned_upload(file, upload_preset, **options): + return upload(file, upload_preset=upload_preset, unsigned=True, **options) + +def upload_image(file, **options): + result = upload(file, **options) + return cloudinary.CloudinaryImage(result["public_id"], version=str(result["version"]), + format=result.get("format"), metadata=result) + +def upload_large(file, **options): + """ Upload large raw files. Note that public_id should include an extension for best results. """ + with open(file, 'rb') as file_io: + upload = upload_id = None + index = 1 + public_id = options.get("public_id") + chunk = file_io.read(20000000) + while (chunk): + chunk_io = BytesIO(chunk) + chunk_io.name = basename(file) + chunk = file_io.read(20000000) + upload = upload_large_part(chunk_io, public_id=public_id, + upload_id=upload_id, part_number=index, final=chunk == "", **options) + upload_id = upload.get("upload_id") + public_id = upload.get("public_id") + index += 1 + return upload + +def upload_large_part(file, **options): + """ Upload large raw files. Note that public_id should include an extension for best results. """ + params = { + "timestamp": utils.now(), + "type": options.get("type"), + "backup": options.get("backup"), + "final": options.get("final"), + "part_number": options.get("part_number"), + "upload_id": options.get("upload_id"), + "tags": options.get("tags") and ",".join(utils.build_array(options["tags"])), + "public_id": options.get("public_id") + } + return call_api("upload_large", params, resource_type="raw", file=file, **options) + + +def destroy(public_id, **options): + params = { + "timestamp": utils.now(), + "type": options.get("type"), + "invalidate": options.get("invalidate"), + "public_id": public_id + } + return call_api("destroy", params, **options) + +def rename(from_public_id, to_public_id, **options): + params = { + "timestamp": utils.now(), + "type": options.get("type"), + "overwrite": options.get("overwrite"), + "from_public_id": from_public_id, + "to_public_id": to_public_id + } + return call_api("rename", params, **options) + +def explicit(public_id, **options): + params = { + "timestamp": utils.now(), + "type": options.get("type"), + "public_id": public_id, + "callback": options.get("callback"), + "headers": utils.build_custom_headers(options.get("headers")), + "eager": utils.build_eager(options.get("eager")), + "tags": options.get("tags") and ",".join(utils.build_array(options["tags"])), + "face_coordinates": utils.encode_double_array(options.get("face_coordinates")), + "custom_coordinates": utils.encode_double_array(options.get("custom_coordinates"))} + return call_api("explicit", params, **options) + +def generate_sprite(tag, **options): + params = { + "timestamp": utils.now(), + "tag": tag, + "async": options.get("async"), + "notification_url": options.get("notification_url"), + "transformation": utils.generate_transformation_string(fetch_format=options.get("format"), **options)[0] + } + return call_api("sprite", params, **options) + +def multi(tag, **options): + params = { + "timestamp": utils.now(), + "tag": tag, + "format": options.get("format"), + "async": options.get("async"), + "notification_url": options.get("notification_url"), + "transformation": utils.generate_transformation_string(**options)[0] + } + return call_api("multi", params, **options) + +def explode(public_id, **options): + params = { + "timestamp": utils.now(), + "public_id": public_id, + "format": options.get("format"), + "notification_url": options.get("notification_url"), + "transformation": utils.generate_transformation_string(**options)[0] + } + return call_api("explode", params, **options) + +# options may include 'exclusive' (boolean) which causes clearing this tag from all other resources +def add_tag(tag, public_ids = [], **options): + exclusive = options.pop("exclusive", None) + command = "set_exclusive" if exclusive else "add" + return call_tags_api(tag, command, public_ids, **options) + +def remove_tag(tag, public_ids = [], **options): + return call_tags_api(tag, "remove", public_ids, **options) + +def replace_tag(tag, public_ids = [], **options): + return call_tags_api(tag, "replace", public_ids, **options) + +def call_tags_api(tag, command, public_ids = [], **options): + params = { + "timestamp": utils.now(), + "tag": tag, + "public_ids": utils.build_array(public_ids), + "command": command, + "type": options.get("type") + } + return call_api("tags", params, **options) + +TEXT_PARAMS = ["public_id", "font_family", "font_size", "font_color", "text_align", "font_weight", "font_style", "background", "opacity", "text_decoration"] +def text(text, **options): + params = {"timestamp": utils.now(), "text": text} + for key in TEXT_PARAMS: + params[key] = options.get(key) + return call_api("text", params, **options) + +def call_api(action, params, **options): + try: + file_io = None + return_error = options.get("return_error") + if options.get("unsigned"): + params = utils.cleanup_params(params) + else: + params = utils.sign_request(params, options) + + param_list = [] + for k, v in params.items(): + if isinstance(v, list): + for vv in v: + param_list.append((k+"[]", vv)) + elif v: + param_list.append((k, v)) + + api_url = utils.cloudinary_api_url(action, **options) + + global _initialized + if not _initialized: + _initialized = True + # Register the streaming http handlers with urllib2 + register_openers() + + datagen = [] + headers = {} + if "file" in options: + file = options["file"] + if not isinstance(file, string_types): + datagen, headers = multipart_encode({'file': file}) + elif not re.match(r'^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$', file): + file_io = open(file, "rb") + datagen, headers = multipart_encode({'file': file_io}) + else: + param_list.append(("file", file)) + + if _is_gae(): + # Might not be needed in the future but for now this is needed in GAE + datagen = "".join(datagen) + + request = urllib2.Request(api_url + "?" + urlencode(param_list), datagen, headers) + request.add_header("User-Agent", cloudinary.USER_AGENT) + + kw = {} + if 'timeout' in options: + kw['timeout'] = options['timeout'] + + code = 200 + try: + response = urllib2.urlopen(request, **kw).read() + except HTTPError: + e = sys.exc_info()[1] + if not e.code in [200, 400, 500]: + raise Error("Server returned unexpected status code - %d - %s" % (e.code, e.read())) + code = e.code + response = e.read() + except socket.error: + e = sys.exc_info()[1] + raise Error("Socket error: %s" % str(e)) + + try: + result = json.loads(to_string(response)) + except Exception: + e = sys.exc_info()[1] + # Error is parsing json + raise Error("Error parsing server response (%d) - %s. Got - %s", code, response, e) + + if "error" in result: + if return_error: + result["error"]["http_code"] = code + else: + raise Error(result["error"]["message"]) + + return result + finally: + if file_io: file_io.close() + +def _is_gae(): + if PY3: + return False + else: + import httplib + return 'appengine' in str(httplib.HTTP) diff --git a/scripts/cloudinary/uploader.pyc b/scripts/cloudinary/uploader.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d992bcac759124a59aff7c3c02871c6a2a6db968 GIT binary patch literal 8887 zcmcIq+ix6K8UJS2UVAro>^M#wr)j#mcGFzi(kdmFwuHo#g6by6DM>e%)p%#@%zAca zGc%jm!Ir9~LLeGJB@jIFfW#aB0Iwh+@gE>wdE*V<;rIK_&aUeOsZG+jbN1ZNcfR}g zofiK+UioeGkE>1T{j1>jJNVK+IwBtab0ie0_v?<-oxHyw^@7$55*DRBB=sS2dTWNI zKAg`hNxhWUN2ETY^`e9&X^%>M)UFwluq^G0)GM|9(v~obaNv#|dZ--V6i8rN{!?FyF z?Uc`et|MBX*7{Mc@6!6qTHmepns_tf?GX>K0V==)NB{#)$GKHm240SdcR)9PMZAMr zIWFEIt(+j47HWq&)^Q~9mxDC(lSbU}BR@%N+&&xOj!75>i{H}vch@pMegDI34E<@A1krLM zTkH5~R<;xEt_oTfHx@ZthtROovmz#5{`d{;{r=UfmleP!FT{}&z8~UCkD$mzT8^wc z@{HSC-QP~f&x zg|q2~VZ-eNcKUYA)SJegEQq62H@35D7!obgW2jv`dy}W6H#?!fI@flC@MbfNyIv5v z$=XeOj-Qm#R-Uy-m>npd_7d3y7UyU6usDUTl*fwGCm^{-Z#0LB zy%7oMC;=U5fpkj7S>CYNqvq^G+*li}Ay(KN;@Cr{xKJZk&nv%lx{G1ZYy_TC=pMKf zBvJ}3#Yx+>2x$#b$>nuX3^#-)jt6c1p>ixF6>?>P>L-iNU0W&Dp^bFt}((}{^ zq{#Hh7x2hkXs2Xbazxc)kWxfMD+d3LN}nel7UUOyArhg}hBk__0YnvKW0=eWq*|nH zl!&xek=O$cH%4UjXRgUD=2x>jZdUCRzM9^NyP;PNqGs6j{HhyO{ngBe48&0tOi*2f!l_zH zrKhS;8PIHt0hNH6K@rHw*Bz^@^icBBHuS4XjJoYbKLK{AKO#2-^1*pl5mI!i8CAIE z5^ zEo-|hr)f06f)!OW+i3G)?Q_!sy{bz2n`S{0JPX?3OSj!ohM~n&jk4%ASGpZT!)^|- zP~wx-GIu$J0T4N%9AI8$9}NIlJMJb;U(K#+D+rA8Q%zY=%^gik6yG1PXe$mP<#THU z-6V0>46XhaJ!h!WpO&x8D_DF2UpkFK3Poq!ne6|aaCR1&gpR=RimKy&fJ!j*H8}{3 z2F4+Aq3NKZtUfA0rw|46<~5Y&brczOOc333!vGe+x57l5>wy|cyjCL|>LHl7SfdPV zLJJj!s{dz0%Qr4&yx$3Z4U&>qJzQ!@qtbz zR-k*;o<1U0c;n4jAxFlrp7)a=^D_#;QWCcteI={j9Xz%{2l1L%NAv%pOb6b^MDq>{ zYPV;@+q>A|626oW4)8YO0CPDbQBn4dWl-LNIcpjRstA~Rf(D{80kZ65)kDNIaM{-~ z#J?&e4s6v)U#L#9L<8DY6K&E-+ys`>?yj1Nn?N~zX`n)t#es@tY#88en;Dy|vx$V3 zlj&>Gd!-stLvja}2u_jO3m+~rWvTextkmN zs~z}SLC;_j-vb%RrqQ&jq_;XhnYPpp*z>>5b!Sjy^gwNN(C}jTaeiZ~7J5$NHiv`e z@uj;_ceY$^kvHkgJOO>TG3lB3TN#>Kah3|Ohc4)~f$gQz z=%Gs`S<$rxQ?x`>EX+Jm_O{yIEx3A48i9U!2j63i223$PAS8WF@D>^jqQDza#|V=U zWoev~AX`(A2%@gh7eWhqlIn$G{v!l8>Q2~5)n7$?k_PvDb=nZCHwHa)fLza0*Gpx; z6L-wZ=+wEIj-dOeISEA)Sw>x%zCli{G%j#}IBmx5wi|g*>$&M30I7j13{<*E*TX_}9@?*q<6y>P>q9q5TA{JOx1j zh}*9SDe;#0oe(zh8v>Qauy+DseTZfbYYt~$zdh0Ov-*t&2FzyN80)`i;DlIP?GD`r zdVX-DZLF?w_*Tzvfc}pGhq}k=9c-1uGaMZ_!hHrbx~xyXXLIey6+a<52Cf~o*9c++ z>)H@?OUUgsmfUs_u4%-s?KF7ct1M`%8HaJA%?xRP2K@YG8|UcgeLq;flMQC4*;;59 z(_>mr;%;O!NpZ)8r?aL8N;mTSW(>;+JKCh(B$fJFBox6UuaruI| z@WH|+3UP(G!=mqUt5#X@*Pd3$bh|IGWjcyNoV|$TW~?fp^!EkqE&zYE2D5@6k}2plhf;=ltTfE8N)4I~rviYp~aK6h|eG-o%)6h@TegfTti zz%fJaX&re71gy9@?^5CFyPrGYw$>yi7sH%=&q2nBQ{h0W&FL+Mj6c5E-i0uX<#z#m z@aF+(@&`Jet(~&2Q3Vd<(6qF6$$FVS&P=wO$N)Sun6kc29>Ss_F)}VKJX7HEd!#i> zARJzGdoO{)t4e`at;p=U_MXSah*LHuWNn|Gu$P-)3`lIfKv*^=i0iOfzM&)6*JdRCm&omS%2;1nVYX3zZt!K>-e$5<^YzOBPcWn^=?X| z`%&sACojN%&(vLDN-M@=(_b;9`PJZ;P?f=dP^7P+Z^2KHVyIdWeXrV$;Q0B?3{_;p zvTj=C`&0E~^@ztJRY!IqZm#%QRWEdwov&jjeIcqMd79$6sR}6UzG(-~Gk&#p#5<-N zoT|PTXIiuIFOi~lgD60F>-h>d5^*U)W7WA9_&(gVV>nGcF{@sw`Zcs_hQ1qhJNc^- zgK<8(G!p-A7g4mHhd>JtN<>ruIB~_O=|jJ!Uh~JB0+PMy&P`@X=B;-jRxFQ(AsM(fQD*ZVWX30 zV|DXF-uKbMoM~KmZ*Kmou0g2DM7N>wFubZ-lXT6#mcfk$luNN*jYu_wN$-~`@eBl1 z%!}+K@U0Y9obd2zE;YoD5@l|+>FW!d4&rep^=s-`GS%cZX?oSgutx1TI?W2#asz#i zYbahxq7`M*dy1TU_w)`qQi&PzJh;|eKwSkxXXKD1r0G=HKqS_o9HF@uRNHXrygoZC-IR%!gQyq3(N9wfe$NdN8n@cE>)+kf-b` zwsO6B$f=v8JAbu31-`NTyazJ|FNUW&F`vdy+5D9iMRLaDGmuvH;ju>aJ3A`9`V6#b zB0@C)W+)Q&(MYA60&2=E$l!)(RoOGYVr`YhI1B22Q)R&y0nNCZbF490YUWs6X2Ed6 z@bU)#Z- 0 and isinstance(array[0], list): + return "|".join([",".join([str(i) for i in build_array(inner)]) for inner in array]) + else: + return ",".join([str(i) for i in array]) + +def encode_dict(arg): + if isinstance(arg, dict): + if PY3: + items = arg.items() + else: + items = arg.iteritems() + return "|".join((k + "=" + v) for k, v in items) + else: + return arg + +def generate_transformation_string(**options): + responsive_width = options.pop("responsive_width", cloudinary.config().responsive_width) + size = options.pop("size", None) + if size: + options["width"], options["height"] = size.split("x") + width = options.get("width") + height = options.get("height") + has_layer = ("underlay" in options) or ("overlay" in options) + + crop = options.pop("crop", None) + angle = ".".join([str(value) for value in build_array(options.pop("angle", None))]) + no_html_sizes = has_layer or angle or crop == "fit" or crop == "limit" or responsive_width + + if width and (width == "auto" or float(width) < 1 or no_html_sizes): + del options["width"] + if height and (float(height) < 1 or no_html_sizes): + del options["height"] + + background = options.pop("background", None) + if background: + background = background.replace("#", "rgb:") + color = options.pop("color", None) + if color: + color = color.replace("#", "rgb:") + + base_transformations = build_array(options.pop("transformation", None)) + if any(isinstance(bs, dict) for bs in base_transformations): + recurse = lambda bs: generate_transformation_string(**bs)[0] if isinstance(bs, dict) else generate_transformation_string(transformation=bs)[0] + base_transformations = list(map(recurse, base_transformations)) + named_transformation = None + else: + named_transformation = ".".join(base_transformations) + base_transformations = [] + + effect = options.pop("effect", None) + if isinstance(effect, list): + effect = ":".join([str(x) for x in effect]) + elif isinstance(effect, dict): + effect = ":".join([str(x) for x in list(effect.items())[0]]) + + border = options.pop("border", None) + if isinstance(border, dict): + border = "%(width)spx_solid_%(color)s" % {"color": border.get("color", "black").replace("#", "rgb:"), "width": str(border.get("width", 2))} + + flags = ".".join(build_array(options.pop("flags", None))) + dpr = options.pop("dpr", cloudinary.config().dpr) + + params = {"w": width, "h": height, "t": named_transformation, "b": background, "co": color, "e": effect, "c": crop, "a": angle, "bo": border, "fl": flags, "dpr": dpr} + for param, option in {"q": "quality", "g": "gravity", "p": "prefix", "x": "x", + "y": "y", "r": "radius", "d": "default_image", "l": "overlay", "u": "underlay", "o": "opacity", + "f": "fetch_format", "pg": "page", "dn": "density", "dl": "delay", "cs": "color_space"}.items(): + params[param] = options.pop(option, None) + + transformation = ",".join(sorted([param + "_" + str(value) for param, value in params.items() if (value or value == 0)])) + if "raw_transformation" in options: + transformation = transformation + "," + options.pop("raw_transformation") + transformations = base_transformations + [transformation] + if responsive_width: + responsive_width_transformation = cloudinary.config().responsive_width_transformation or DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION + transformations += [generate_transformation_string(**responsive_width_transformation)[0]] + url = "/".join([trans for trans in transformations if trans]) + + if width == "auto" or responsive_width: + options["responsive"] = True + if dpr == "auto": + options["hidpi"] = True + return (url, options) + +def cleanup_params(params): + return dict( [ (k, __safe_value(v)) for (k,v) in params.items() if not v is None and not v == ""] ) + +def sign_request(params, options): + api_key = options.get("api_key", cloudinary.config().api_key) + if not api_key: raise ValueError("Must supply api_key") + api_secret = options.get("api_secret", cloudinary.config().api_secret) + if not api_secret: raise ValueError("Must supply api_secret") + + params = cleanup_params(params) + params["signature"] = api_sign_request(params, api_secret) + params["api_key"] = api_key + + return params + +def api_sign_request(params_to_sign, api_secret): + to_sign = "&".join(sorted([(k+"="+(",".join(v) if isinstance(v, list) else str(v))) for k, v in params_to_sign.items() if v])) + return hashlib.sha1(to_bytes(to_sign + api_secret)).hexdigest() + +def finalize_source(source, format, url_suffix): + source = re.sub(r'([^:])/+', r'\1/', source) + if re.match(r'^https?:/', source): + source = smart_escape(source) + source_to_sign = source + else: + source = unquote(source) + if not PY3: source = source.decode('utf8') + source = smart_escape(source) + source_to_sign = source + if url_suffix != None: + if re.search(r'[\./]', url_suffix): raise ValueError("url_suffix should not include . or /") + source = source + "/" + url_suffix + if format != None: + source = source + "." + format + source_to_sign = source_to_sign + "." + format + + return (source, source_to_sign) + +def finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten): + type = type or "upload" + if url_suffix != None: + if resource_type == "image" and type == "upload": + resource_type = "images" + type = None + elif resource_type == "raw" and type == "upload": + resource_type = "files" + type = None + else: + raise ValueError("URL Suffix only supported for image/upload and raw/upload") + + if use_root_path: + if (resource_type == "image" and type == "upload") or (resource_type == "images" and type == None): + resource_type = None + type = None + else: + raise ValueError("Root path only supported for image/upload") + + if shorten and resource_type == "image" and type == "upload": + resource_type = "iu" + type = None + + return (resource_type, type) + +def unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution): + """cdn_subdomain and secure_cdn_subdomain + 1) Customers in shared distribution (e.g. res.cloudinary.com) + if cdn_domain is true uses res-[1-5].cloudinary.com for both http and https. Setting secure_cdn_subdomain to false disables this for https. + 2) Customers with private cdn + if cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for http + if secure_cdn_domain is true uses cloudname-res-[1-5].cloudinary.com for https (please contact support if you require this) + 3) Customers with cname + if cdn_domain is true uses a[1-5].cname for http. For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.""" + shared_domain = not private_cdn + shard = __crc(source) + if secure: + if secure_distribution == None or secure_distribution == cloudinary.OLD_AKAMAI_SHARED_CDN: + secure_distribution = cloud_name + "-res.cloudinary.com" if private_cdn else cloudinary.SHARED_CDN + + shared_domain = shared_domain or secure_distribution == cloudinary.SHARED_CDN + if secure_cdn_subdomain == None and shared_domain: + secure_cdn_subdomain = cdn_subdomain + + if secure_cdn_subdomain: + secure_distribution = re.sub('res.cloudinary.com', "res-" + shard + ".cloudinary.com", secure_distribution) + + prefix = "https://" + secure_distribution + elif cname: + subdomain = "a" + shard + "." if cdn_subdomain else "" + prefix = "http://" + subdomain + cname + else: + subdomain = cloud_name + "-res" if private_cdn else "res" + if cdn_subdomain: subdomain = subdomain + "-" + shard + prefix = "http://" + subdomain + ".cloudinary.com" + + if shared_domain: prefix += "/" + cloud_name + + return prefix + +def cloudinary_url(source, **options): + original_source = source + + type = options.pop("type", "upload") + if type == 'fetch': + options["fetch_format"] = options.get("fetch_format", options.pop("format", None)) + transformation, options = generate_transformation_string(**options) + + resource_type = options.pop("resource_type", "image") + version = options.pop("version", None) + format = options.pop("format", None) + cdn_subdomain = options.pop("cdn_subdomain", cloudinary.config().cdn_subdomain) + secure_cdn_subdomain = options.pop("secure_cdn_subdomain", cloudinary.config().secure_cdn_subdomain) + cname = options.pop("cname", cloudinary.config().cname) + shorten = options.pop("shorten", cloudinary.config().shorten) + + cloud_name = options.pop("cloud_name", cloudinary.config().cloud_name or None) + if cloud_name is None: + raise ValueError("Must supply cloud_name in tag or in configuration") + secure = options.pop("secure", cloudinary.config().secure) + private_cdn = options.pop("private_cdn", cloudinary.config().private_cdn) + secure_distribution = options.pop("secure_distribution", cloudinary.config().secure_distribution) + sign_url = options.pop("sign_url", cloudinary.config().sign_url) + api_secret = options.pop("api_secret", cloudinary.config().api_secret) + url_suffix = options.pop("url_suffix", None) + use_root_path = options.pop("use_root_path", cloudinary.config().use_root_path) + + if url_suffix and not private_cdn: + raise ValueError("URL Suffix only supported in private CDN") + + + if (not source) or type == "upload" and re.match(r'^https?:', source): + return (original_source, options) + + resource_type, type = finalize_resource_type(resource_type, type, url_suffix, use_root_path, shorten) + source, source_to_sign = finalize_source(source, format, url_suffix) + + + if source_to_sign.find("/") >= 0 and not re.match(r'^https?:/', source_to_sign) and not re.match(r'^v[0-9]+', source_to_sign) and not version: + version = "1" + if version: version = "v" + str(version) + + transformation = re.sub(r'([^:])/+', r'\1/', transformation) + + signature = None + if sign_url: + to_sign = "/".join(__compact([transformation, source_to_sign])) + signature = "s--" + to_string(base64.urlsafe_b64encode( hashlib.sha1(to_bytes(to_sign + api_secret)).digest() )[0:8]) + "--" + + prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution) + source = "/".join(__compact([prefix, resource_type, type, signature, transformation, version, source])) + return (source, options) + +def cloudinary_api_url(action = 'upload', **options): + cloudinary_prefix = options.get("upload_prefix", cloudinary.config().upload_prefix) or "https://api.cloudinary.com" + cloud_name = options.get("cloud_name", cloudinary.config().cloud_name) + if not cloud_name: raise ValueError("Must supply cloud_name") + resource_type = options.get("resource_type", "image") + return "/".join([cloudinary_prefix, "v1_1", cloud_name, resource_type, action]) + +# Based on ruby's CGI::unescape. In addition does not escape / : +def smart_escape(string): + pack = lambda m: to_bytes('%' + "%".join(["%02X" % x for x in struct.unpack('B'*len(m.group(1)), m.group(1))]).upper()) + return to_string(re.sub(to_bytes(r"([^a-zA-Z0-9_.\-\/:]+)"), pack, to_bytes(string))) + +def random_public_id(): + return base64.urlsafe_b64encode(hashlib.sha1(uuid.uuid4()).digest())[0:16] + +def signed_preloaded_image(result): + filename = ".".join([x for x in [result["public_id"], result["format"]] if x]) + path = "/".join([result["resource_type"], "upload", "v" + result["version"], filename]) + return path + "#" + result["signature"] + +def now(): + return str(int(time.time())) + +def private_download_url(public_id, format, **options): + cloudinary_params = sign_request({ + "timestamp": now(), + "public_id": public_id, + "format": format, + "type": options.get("type"), + "attachment": options.get("attachment"), + "expires_at": options.get("expires_at") + }, options) + + return cloudinary_api_url("download", **options) + "?" + urlencode(cloudinary_params) + +def zip_download_url(tag, **options): + cloudinary_params = sign_request({ + "timestamp": now(), + "tag": tag, + "transformation": generate_transformation_string(**options)[0] + }, options) + + return cloudinary_api_url("download_tag.zip", **options) + "?" + urlencode(cloudinary_params) + +def build_eager(transformations): + eager = [] + for tr in build_array(transformations): + format = tr.get("format") + single_eager = "/".join([x for x in [generate_transformation_string(**tr)[0], format] if x]) + eager.append(single_eager) + return "|".join(eager) + +def build_custom_headers(headers): + if headers == None: + return None + elif isinstance(headers, list): + pass + elif isinstance(headers, dict): + headers = [k + ": " + v for k, v in headers.items()] + else: + return headers + return "\n".join(headers) + +def build_upload_params(**options): + params = {"timestamp": now(), + "transformation": generate_transformation_string(**options)[0], + "public_id": options.get("public_id"), + "callback": options.get("callback"), + "format": options.get("format"), + "type": options.get("type"), + "backup": options.get("backup"), + "faces": options.get("faces"), + "image_metadata": options.get("image_metadata"), + "exif": options.get("exif"), + "colors": options.get("colors"), + "headers": build_custom_headers(options.get("headers")), + "eager": build_eager(options.get("eager")), + "use_filename": options.get("use_filename"), + "unique_filename": options.get("unique_filename"), + "discard_original_filename": options.get("discard_original_filename"), + "invalidate": options.get("invalidate"), + "notification_url": options.get("notification_url"), + "eager_notification_url": options.get("eager_notification_url"), + "eager_async": options.get("eager_async"), + "proxy": options.get("proxy"), + "folder": options.get("folder"), + "overwrite": options.get("overwrite"), + "tags": options.get("tags") and ",".join(build_array(options["tags"])), + "allowed_formats": options.get("allowed_formats") and ",".join(build_array(options["allowed_formats"])), + "face_coordinates": encode_double_array(options.get("face_coordinates")), + "custom_coordinates": encode_double_array(options.get("custom_coordinates")), + "context": encode_dict(options.get("context")), + "moderation": options.get("moderation"), + "raw_convert": options.get("raw_convert"), + "ocr": options.get("ocr"), + "categorization": options.get("categorization"), + "detection": options.get("detection"), + "similarity_search": options.get("similarity_search"), + "background_removal": options.get("background_removal"), + "upload_preset": options.get("upload_preset"), + "phash": options.get("phash"), + "return_delete_token": options.get("return_delete_token"), + "auto_tagging": options.get("auto_tagging") and float(options.get("auto_tagging"))} + return params + +def __safe_value(v): + if isinstance(v, (bool)): + if v: + return "1" + else: + return "0" + else: + return v +def __crc(source): + return str((zlib.crc32(to_bytearray(source)) & 0xffffffff)%5 + 1) + +def __compact(array): + return filter(lambda x: x, array) + diff --git a/scripts/cloudinary/utils.pyc b/scripts/cloudinary/utils.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d658a75a57153cd299fe130a28ee2ce03d3ddc24 GIT binary patch literal 16051 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 VIlKk=o?@SBi|8{{JchLAe*x>*!cPDI literal 0 HcmV?d00001