├── README ├── appimagedemo ├── app.yaml ├── index.yaml └── main.py ├── appmaildemo ├── app.yaml ├── index.yaml └── main.py └── libs └── php ├── appimage ├── README ├── appimage.config.php ├── appimage.core.php ├── helpers │ ├── appimage.rest.php │ └── appimage.xml.php └── tests │ ├── appimage.core.cakemate.test.php │ ├── appimage.core.phpunit.test.php │ ├── big.jpg │ └── small.jpg └── appmail ├── README ├── appmail.config.php ├── appmail.core.php ├── helpers └── appmail.rest.php └── tests ├── appmail.core.cakemate.test.php └── appmail.core.phpunit.test.php /README: -------------------------------------------------------------------------------- 1 | RELEASE HISTORY 2 | ------------------------------------------------------------------------------------------------------------ 3 | 4 | Update - Released Jan 4th 2011 5 | ------------------------------------------------------ 6 | - AppMail: Added support for HTML emails with plain text fallback. Instead of passing "body" param, you now pass "plain" and optionally "html" POST params. 7 | 8 | AppImage Update: On December 2nd, the GAE team released a significant update that now allows for images upto 32MB to be sent to your GAE apps. This basically renders AppImage no longer necessary. The code was originally written as a work around for the old 1MB limit. See Google's blog post about the newest updates to GAE: http://googleappengine.blogspot.com/2010/12/happy-holidays-from-app-engine-team-140.html 9 | 10 | 11 | AppImage Version 2.0 - Released Feb 16th 2010 12 | ------------------------------------------------------ 13 | - GAE: Includes support for working with files larger than 1mb (using GAE Blobstore - Billing enabled accounts only) 14 | - GAE: XML now used in request responses making it easier to check success and failure 15 | - GAE: Examples added to illustrate storage of multiple thumbnail sizes 16 | - GAE: Logging added to make it easier to see how the application is running in GAE Dashboard (should be removed for production environments) 17 | - PHP: Upload method now requires a filesize argument to be passed (not backwards compatible with previous release) 18 | - PHP: REST helper POST method updated to follow redirections during CURL operation in order for Blobstore calls to work 19 | - PHP: XML helper added to read responses 20 | - PHP: Tests updated to include both a small and large image and assertions to check image md5 hashes 21 | 22 | Corresponding blog post: http://developinginthedark.com/posts/large-image-resizing-for-google-app-engine 23 | 24 | 25 | PROJECT DETAILS 26 | ------------------------------------------------------------------------------------------------------------ 27 | 28 | About 29 | ------------------------------------------------------ 30 | A collection of Google App Engine web services for outsourcing common tasks for web applications to a larger service provider - in this case Google's App Engine infrastructure. Currently the collection includes an Email app and Image Manipulation app with PHP/CakePHP libraries for interacting with them. 31 | 32 | Current Web Services 33 | ------------------------------------------------------ 34 | AppImage - A Google App Engine app that allows you to store, view and manipulate images. 35 | AppMail - A Google App Engine app that simply allows you to send emails. 36 | 37 | Setup 38 | ------------------------------------------------------ 39 | If you haven't already change the name of the folders from "appimagedemo"/"appmaildemo" to the names of your GAE apps. You'll also need to modify app.yaml "application:" config variable to the same name. 40 | 41 | You'll need to set the "AUTH" constant in main.py to include your IP address and API Key. The API Key is just a random string, you just need to ensure that in your requests you pass the same random string to the App Methods that require it. 42 | 43 | Authentication 44 | ------------------------------------------------------ 45 | Both applications include an authentication aspect to stop others using the app, data, and bandwidth. The authentication works by requiring an API Key to be passed when making requests to certain application methods. This API Key is then looked up in the "AUTH" dictionary in main.py which has a corresponding IP Address. If the requesters IP Address matches the one corresponding the API Key provided then the request will continue, if they don\'t match an exception is raised and the request is denied. 46 | 47 | Libraries 48 | ------------------------------------------------------ 49 | Currently a PHP library is included for both web services. These libraries were designed as CakePHP vendors but should work fine in any PHP project. It would be great to see some more libraries for other languages, so please feel free to contribute new libraries and/or modify the existing ones. 50 | 51 | References 52 | ------------------------------------------------------ 53 | For more info, check out the corresponding blog post at http://developinginthedark.com/posts/revving-with-google-app-engine 54 | Information about AppImage Version 2.0: http://developinginthedark.com/posts/appimage-updated-large-image-support-for-google-app-engine 55 | 56 | AppImage is based on Appspotimage: http://code.google.com/p/appspotimage/ 57 | They have an excellent tutorial and show how a Google App Engine app can be integrated with 3Scale (an SaaS API Management tool) 58 | 59 | Also check out Google's docs 60 | Google App Engine Mail Docs: http://code.google.com/appengine/docs/python/mail/sendingmail.html 61 | Google App Engine Image Docs: http://code.google.com/appengine/docs/python/images/imageclass.html 62 | Google App Engine Quotas Information: http://code.google.com/appengine/docs/quotas.html 63 | 64 | -------------------------------------------------------------------------------- /appimagedemo/app.yaml: -------------------------------------------------------------------------------- 1 | application: appimagedemo 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: .* 8 | script: main.py 9 | -------------------------------------------------------------------------------- /appimagedemo/index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | 13 | -------------------------------------------------------------------------------- /appimagedemo/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | #!/usr/bin/env python 4 | # 5 | # Copyright 2007 Google Inc. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | 20 | # Based on Appspotimage at http://code.google.com/p/appspotimage/ written by Carlos Merida-Campos 21 | # This version has been modified to add new models, remove 3scale integration and reduce methods to 22 | # resize and crop with options to specify "pixels" and "percentages". 23 | 24 | 25 | import cgi 26 | import os 27 | import logging 28 | import contextlib 29 | from xml.dom import minidom 30 | from xml.dom.minidom import Document 31 | import exceptions 32 | import warnings 33 | import imghdr 34 | from google.appengine.ext import blobstore 35 | from google.appengine.api import images 36 | from google.appengine.api import users 37 | from google.appengine.api import urlfetch 38 | from google.appengine.ext import db 39 | from google.appengine.ext import webapp 40 | from google.appengine.ext.webapp.util import run_wsgi_app 41 | from google.appengine.ext.webapp import template 42 | from google.appengine.ext.webapp import blobstore_handlers 43 | 44 | import wsgiref.handlers 45 | 46 | # START Constants 47 | CONTENT_TYPE_HEADER = "Content-Type" 48 | CONTENT_TYPE_TEXT = "text/plain" 49 | CONTENT_TYPE_PNG = "image/png" 50 | CONTENT_TYPE_JPEG = "image/jpeg" 51 | XML_CONTENT_TYPE = "application/xml" 52 | XML_ENCODING = "utf-8" 53 | """ 54 | Allows you to specify IP addresses and associated "api_key"s to prevent others from using your app. 55 | Storage and Manipulation methods will check for this "api_key" in the POST/GET params. 56 | Retrieval methods don't use it (however you could enable them to use it, but maybe rewrite so you have a "read" key and a "write" key to prevent others from manipulating your data). 57 | 58 | Set "AUTH = False" to disable (allowing anyone use your app and CRUD your data). 59 | 60 | To generate a hash/api_key visit https://www.grc.com/passwords.htm 61 | To find your ip visit http://www.whatsmyip.org/ 62 | """ 63 | AUTH = { 64 | '000.000.000.000':'v5na39h3uO2aGHoC4gTy5R80eWn3U71Hdqzx', 65 | } 66 | 67 | # END Constants 68 | 69 | # START Exception Handling 70 | class Error(StandardError): 71 | pass 72 | class ImageNotFound(Error): #Exception raised when no image is found 73 | pass 74 | class Forbidden(Error): 75 | pass 76 | 77 | logging.getLogger().setLevel(logging.DEBUG) 78 | 79 | @contextlib.contextmanager 80 | def imageExcpHandler(ctx): 81 | try: 82 | yield {} 83 | except (images.LargeImageError, images.BadImageError, images.TransformationError), exc: 84 | logging.error(exc.message) 85 | xml_response(ctx, 'app.invalid_image', 'The image provided is too big or corrupt: ' + exc.message) 86 | except (ImageNotFound), exc: 87 | logging.error(exc.message) 88 | xml_response(ctx, 'app.not_found', 'The image requested has not been found: ' + exc.message) 89 | except (images.NotImageError), exc: 90 | logging.error(exc.message) 91 | xml_response(ctx, 'app.invalid_encoding', 'The indicated encoding is not supported, valid encodings are PNG and JPEG: ' + exc.message) 92 | except (ValueError, images.BadRequestError), exc: 93 | logging.error(exc.message) 94 | xml_response(ctx, 'app.invalid_parameters', 'The indicated parameters to manipulate the image are not valid: ' + exc.message) 95 | except (Forbidden), exc: 96 | logging.error(exc.message) 97 | xml_response(ctx, 'app.forbidden', 'You don\'t have permission to perform this action: ' + exc.message) 98 | except (Exception), exc: 99 | logging.error(exc.message) 100 | xml_response(ctx, 'system.other', 'An unexpected error in the web service has happened: ' + exc.message) 101 | 102 | def xml_response(handle, response_status, response_msg): 103 | doc = Document() 104 | responsecard = doc.createElement("response") 105 | statuscard = doc.createElement("status") 106 | messagecard = doc.createElement("message") 107 | 108 | statustext = doc.createTextNode(response_status) 109 | messagetext = doc.createTextNode(response_msg) 110 | 111 | statuscard.appendChild(statustext) 112 | messagecard.appendChild(messagetext) 113 | 114 | responsecard.appendChild(statuscard) 115 | responsecard.appendChild(messagecard) 116 | doc.appendChild(responsecard) 117 | 118 | handle.response.headers[CONTENT_TYPE_HEADER] = XML_CONTENT_TYPE 119 | handle.response.out.write(doc.toxml(XML_ENCODING)) 120 | 121 | # END Exception Handling 122 | 123 | # START Helper Methods 124 | def loadModel(model): 125 | if model.lower() == 'avatar': 126 | return Avatar 127 | elif model.lower() == 'photo': 128 | return Photo 129 | else: 130 | raise images.BadRequestError("Unknown type") 131 | 132 | def isAuth(ip = None, key = None): 133 | if AUTH == False: 134 | return True 135 | elif AUTH.has_key(ip) and key == AUTH[ip]: 136 | return True 137 | else: 138 | return False 139 | 140 | # END Helper Methods 141 | 142 | # START Model Definitions 143 | class Avatar(db.Model): 144 | """ 145 | Storage for an avatar and its associated metadata. 146 | 147 | Properties: 148 | img_id: identificator for the avatar 149 | src_data: original image in full size 150 | transformed_data: manipulated version of original (ie, cropped, etc) 151 | thumbnail_data: jpeg format image in thumbnail size 152 | thumbnail_props: settings to apply when generating thumbnails 153 | max_dimension: the maximum size for storage - all uploaded will be resized if too large 154 | """ 155 | img_id = db.StringProperty() 156 | src_data = db.BlobProperty() 157 | transformed_data = db.BlobProperty() 158 | large_thumbnail_data = db.BlobProperty() 159 | large_thumbnail_props = {'width':200,'height':200,'type':'PNG'} 160 | medium_thumbnail_data = db.BlobProperty() 161 | medium_thumbnail_props = {'width':100,'height':100,'type':'PNG'} 162 | small_thumbnail_data = db.BlobProperty() 163 | small_thumbnail_props = {'width':50,'height':50,'type':'PNG'} 164 | max_dimension = 500 165 | 166 | class Photo(db.Model): 167 | """ 168 | Storage for a photo and its associated metadata. 169 | 170 | Properties: 171 | img_id: identificator for the photo 172 | src_data: original image in full size 173 | transformed_data: manipulated version of original (ie, cropped, etc) 174 | thumbnail_data: jpeg format image in thumbnail size 175 | thumbnail_props: settings to apply when generating thumbnails 176 | max_dimension: the maximum size for storage - all uploaded will be resized if too large 177 | """ 178 | img_id = db.StringProperty() 179 | src_data = db.BlobProperty() 180 | transformed_data = db.BlobProperty() 181 | large_thumbnail_data = db.BlobProperty() 182 | large_thumbnail_props = {'width':600,'height':600,'type':'JPEG'} 183 | small_thumbnail_data = db.BlobProperty() 184 | small_thumbnail_props = {'width':50,'height':50,'type':'JPEG'} 185 | max_dimension = 800 186 | 187 | # END Model Definitions 188 | 189 | # START Request Handlers 190 | class Upload(webapp.RequestHandler): 191 | def post(self, model): 192 | """ 193 | Uploads an image to be processed. 194 | 195 | Returns image key 196 | 197 | Args: 198 | model: (required) string of the model name in which to store the image (ie, avatar, photo) 199 | 200 | POST Args: 201 | api_key: (required) the api key specified in the dictionary defined in the constants 202 | img_id: (required) user defined identifier for the specific image 203 | 204 | One (and only one) of the following is required 205 | image: the image file that it is being uploaded (< 1mb) 206 | OR 207 | blob_key: a key from an existing image being stored in the blobstore api (> 1mb) 208 | """ 209 | with imageExcpHandler(self): 210 | # check authorised 211 | if isAuth(self.request.remote_addr,self.request.get('api_key')) == False: 212 | raise Forbidden("Invalid Credentials") 213 | 214 | logging.debug('uploading') 215 | 216 | # read data from request 217 | user_id = cgi.escape(self.request.get('user_id')) 218 | img_id = cgi.escape(self.request.get('img_id')) 219 | 220 | # choose which path based on provided params 221 | args = self.request.arguments() 222 | logging.debug(args) 223 | logging.debug(img_id) 224 | 225 | if 'image' in args: # path 1 - image < 1mb 226 | logging.debug('path 1 - normal upload') 227 | img_data = self.request.POST.get('image').file.read() 228 | img = images.Image(img_data) 229 | 230 | # check its an accepted image type 231 | img_type = imghdr.what('filename', img_data) 232 | if img_type != 'png' and img_type != 'jpeg': 233 | raise images.NotImageError("Unknown image file type") 234 | elif 'blob_key' in args: # path 2 - image was > 1mb therefore uploaded to blobstore 235 | logging.debug('path 2 - blobstore api') 236 | blob_key = self.request.get("blob_key") 237 | blob_info = blobstore.get(blob_key) 238 | img = images.Image(blob_key=blob_key) 239 | 240 | # check its an accepted image type 241 | img_type = blob_info.content_type 242 | logging.debug('img_type: '+img_type) 243 | 244 | if img_type == "image/jpeg" or img_type == "image/jpg": 245 | img_type = 'jpeg' 246 | elif img_type == "image/png" or img_type == "image/x-png": 247 | img_type = 'png' 248 | else: 249 | logging.debug('Image type not recognised, assuming JPEG') 250 | img_type = 'jpeg' 251 | 252 | else: # neither path - no image or blob_key specified 253 | raise Exception("No image provided/specified") 254 | 255 | # set the image model to use 256 | ImageModel = loadModel(model) 257 | 258 | # generate source data by resizing to model max dimension prop if the image is larger 259 | max_d = ImageModel.max_dimension 260 | # blobstore images don't have width or height props, however because they are using the 261 | # blobstore it is assumed that they are large files which are most likely bigger than the 262 | # max dimension - seeking any better ideas on approaching this 263 | if 'blob_key' in args or img.width > max_d or img.height > max_d: 264 | logging.debug('resizing to generate src_data (image > max_dimension)') 265 | img.resize(width = max_d, height = max_d) 266 | src_data = img.execute_transforms(eval('images.' + img_type.upper())) 267 | else: 268 | logging.debug('directly using img as src_data (image < max_dimension)') 269 | img.resize(width = img.width, height = img.height) 270 | src_data = img.execute_transforms(eval('images.' + img_type.upper())) 271 | 272 | # generate thumbnails only if they are part of the model properties 273 | props = ImageModel.properties() 274 | 275 | large_thumbnail_data = None 276 | if 'large_thumbnail_data' in props: 277 | img.resize(ImageModel.large_thumbnail_props['width'], ImageModel.large_thumbnail_props['height']) 278 | large_thumbnail_data = img.execute_transforms(eval('images.' + ImageModel.large_thumbnail_props['type'])) 279 | 280 | medium_thumbnail_data = None 281 | if 'medium_thumbnail_data' in props: 282 | img.resize(ImageModel.medium_thumbnail_props['width'], ImageModel.medium_thumbnail_props['height']) 283 | medium_thumbnail_data = img.execute_transforms(eval('images.' + ImageModel.medium_thumbnail_props['type'])) 284 | 285 | small_thumbnail_data = None 286 | if 'small_thumbnail_data' in props: 287 | img.resize(ImageModel.small_thumbnail_props['width'], ImageModel.small_thumbnail_props['height']) 288 | small_thumbnail_data = img.execute_transforms(eval('images.' + ImageModel.small_thumbnail_props['type'])) 289 | 290 | # check if an image with that id already exists and delete it 291 | query = ImageModel.all(keys_only=True) # true means retrieve keys only 292 | query.filter('img_id = ', img_id) 293 | results = query.fetch(limit=1) 294 | if len(results) > 0: 295 | db.delete(results) 296 | 297 | # add new image 298 | # note: adding the null thumbnail data values on properties that don't exist on the model 299 | # does not affect the saving process 300 | reference = ImageModel( 301 | img_id = img_id, 302 | user_id = user_id, 303 | src_data = src_data, 304 | large_thumbnail_data = large_thumbnail_data, 305 | medium_thumbnail_data = medium_thumbnail_data, 306 | small_thumbnail_data = small_thumbnail_data 307 | ).put() 308 | 309 | # delete blobstore object to prevent being orphaned 310 | if 'blob_key' in args: 311 | blobstore.delete(blob_key) 312 | 313 | # return the auto generated GAE UUID for the image stored 314 | xml_response(self, 'app.success', str(reference)) 315 | 316 | 317 | class View(webapp.RequestHandler): 318 | def get(self, model, display_type, img_id): 319 | """ 320 | Serves an image from the datastore based on the model, display_type and img_id. 321 | 322 | Args: 323 | model: (required) string of the model name in which to retrieve the image from (ie, avatar, photo) 324 | display_type: (required) a string describing the type of image to serve (image or thumbnail) 325 | img_id: (required) user defined identifier for the specific image 326 | 327 | """ 328 | with imageExcpHandler(self): 329 | # set the image model to use 330 | ImageModel = loadModel(model) 331 | 332 | # pull image from database 333 | query = ImageModel.all() 334 | query.filter('img_id = ', img_id) 335 | results = query.fetch(limit = 1) 336 | if len(results) == 0: 337 | raise ImageNotFound("No existing image with image id") 338 | image = results[0] 339 | 340 | if display_type == 'source': 341 | img_data = image.src_data 342 | elif display_type == 'image': 343 | img_data = image.transformed_data 344 | if img_data == None: 345 | img_data = image.src_data 346 | elif display_type == 'large': 347 | img_data = image.large_thumbnail_data 348 | elif display_type == 'medium': 349 | img_data = image.medium_thumbnail_data 350 | elif display_type == 'small': 351 | img_data = image.small_thumbnail_data 352 | else: 353 | raise images.BadRequestError 354 | 355 | img_type = imghdr.what('filename',img_data) 356 | if(img_type == 'png'): 357 | self.response.headers[CONTENT_TYPE_HEADER] = CONTENT_TYPE_PNG 358 | else: 359 | self.response.headers[CONTENT_TYPE_HEADER] = CONTENT_TYPE_JPEG 360 | self.response.out.write(img_data) 361 | 362 | class Manipulate(webapp.RequestHandler): 363 | def get(self, task, model): 364 | """ 365 | Identifies the type of operation to be done, and passes flow to the appropriate function 366 | 367 | Args: 368 | model: (required) string of the model name in which to retrieve the image from (ie, avatar, photo) 369 | task: (required) a string describing the task to perform (ie, resize, crop) 370 | 371 | POST Args 372 | api_key: (required) the api key specified in the dictionary defined in the constants 373 | img_id: (required) user defined identifier for the specific image 374 | 375 | Extra POST Args (resize) - Read descriptions in "def resize(self, image):" 376 | width,height,unit 377 | 378 | Extra POST Args (crop) - Read descriptions in "def crop(self, image):" 379 | x1,y1,x2,y2,unit 380 | """ 381 | with imageExcpHandler(self): 382 | # check authorised 383 | if isAuth(self.request.remote_addr,self.request.get('api_key')) == False: 384 | raise Forbidden("Invalid Credentials") 385 | 386 | # get data 387 | img_id = cgi.escape(self.request.get('img_id')) 388 | 389 | # set the image model to use 390 | ImageModel = loadModel(model) 391 | 392 | # find the image from datastore 393 | query = ImageModel.all() 394 | query.filter('img_id = ', img_id) 395 | results = query.fetch(limit=1) 396 | if len(results) == 0: 397 | raise ImageNotFound("No existing image with image id") 398 | image = results[0] 399 | 400 | # perform manipulation task 401 | if 'resize' == cgi.escape(task.lower()): 402 | transformed = self.resize(image.src_data) 403 | elif 'crop' == cgi.escape(task.lower()): 404 | transformed = self.crop(image.src_data) 405 | 406 | image.transformed_data = transformed 407 | 408 | thumbnail = images.Image(transformed) 409 | 410 | props = ImageModel.properties() 411 | 412 | large_thumbnail_data = None 413 | if 'large_thumbnail_data' in props: 414 | thumbnail.resize(ImageModel.large_thumbnail_props['width'], ImageModel.large_thumbnail_props['height']) 415 | image.large_thumbnail_data = thumbnail.execute_transforms(eval('images.' + ImageModel.large_thumbnail_props['type'])) 416 | 417 | medium_thumbnail_data = None 418 | if 'medium_thumbnail_data' in props: 419 | thumbnail.resize(ImageModel.medium_thumbnail_props['width'], ImageModel.medium_thumbnail_props['height']) 420 | image.medium_thumbnail_data = thumbnail.execute_transforms(eval('images.' + ImageModel.medium_thumbnail_props['type'])) 421 | 422 | small_thumbnail_data = None 423 | if 'small_thumbnail_data' in props: 424 | thumbnail.resize(ImageModel.small_thumbnail_props['width'], ImageModel.small_thumbnail_props['height']) 425 | image.small_thumbnail_data = thumbnail.execute_transforms(eval('images.' + ImageModel.small_thumbnail_props['type'])) 426 | 427 | reference = image.put() 428 | 429 | xml_response(self, 'app.success', str(reference)) 430 | 431 | def resize(self, image): 432 | """ 433 | Resizes an image based on specified width/height (maintaining aspect ratio - see below) 434 | 435 | NOTE: From http://code.google.com/appengine/docs/python/images/imageclass.html#Image_resize 436 | Resizes an image, scaling down or up to the given width and height. The resize transform preserves 437 | the aspect ratio of the image. If both the width and the height arguments are provided, the transform 438 | uses the dimension that results in a smaller image. 439 | 440 | Args: 441 | image: image file that has been retrieved from datastore 442 | 443 | POST Args: 444 | width: width to resize the image (pixels or percentage based on unit) 445 | height: height to resize the image (pixels or percentage based on unit) 446 | unit: 'pixels' or 'percentage' to indicate the type of measurement provided for width & height 447 | """ 448 | width = float(cgi.escape(self.request.get('width'))) 449 | height = float(cgi.escape(self.request.get('height'))) 450 | unit = cgi.escape(self.request.get('unit')) 451 | 452 | # determine image type 453 | img_type = imghdr.what('filename',image) 454 | if(img_type == 'png'): 455 | output_encoding = images.PNG 456 | else: 457 | output_encoding = images.JPEG 458 | 459 | img = images.Image(image) 460 | if unit == 'pixels': 461 | img.resize(width = int(width), height = int(height)) 462 | elif unit == 'percentage': 463 | img.resize(width = int(img.width * width), height = int(img.height * height)) 464 | else: 465 | raise images.BadRequestError("unit must be 'pixels' or 'percentage'") 466 | 467 | transformed = img.execute_transforms(output_encoding) 468 | return transformed 469 | 470 | def crop(self, image): 471 | """ 472 | Crops an image based on specified pixels/percentage 473 | 474 | Args: 475 | image: image file that has been retrieved from datastore 476 | 477 | POST Args 478 | x1,y1,x2,y2: 479 | If unit is 'percentage' - proportion of the image width/height specified as a float value from 0.0 to 1.0 (inclusive). 480 | If unit is 'pixels' - coordinate within image width/height specified as a pixel value (inclusive). 481 | 482 | unit: 'pixels' or 'percentage' to indicate the type of measurement provided for x1,y1,x2,y2 483 | """ 484 | x1 = float(cgi.escape(self.request.get('x1'))) 485 | y1 = float(cgi.escape(self.request.get('y1'))) 486 | x2 = float(cgi.escape(self.request.get('x2'))) 487 | y2 = float(cgi.escape(self.request.get('y2'))) 488 | unit = cgi.escape(self.request.get('unit')) 489 | 490 | # determine image type 491 | img_type = imghdr.what('filename',image) 492 | if(img_type == 'png'): 493 | output_encoding = images.PNG 494 | else: 495 | output_encoding = images.JPEG 496 | 497 | img = images.Image(image) 498 | if unit == 'pixels': 499 | img.crop(x1/img.width,y1/img.height,x2/img.width,y2/img.height) 500 | elif unit == 'percentage': 501 | img.crop(x1,y1,x2,y2) 502 | else: 503 | raise images.BadRequestError("unit must be 'pixels' or 'percentage'") 504 | 505 | transformed = img.execute_transforms(output_encoding) 506 | return transformed 507 | 508 | 509 | # Gets a url to post a large file to 510 | class BlobstoreUrl(webapp.RequestHandler): 511 | def post(self): 512 | # check authorised 513 | if isAuth(self.request.remote_addr,self.request.get('api_key')) == False: 514 | raise Forbidden("Invalid Credentials") 515 | 516 | upload_url = blobstore.create_upload_url('/blobstore/upload') 517 | xml_response(self, 'app.success', upload_url) 518 | 519 | # Send a POST request to upload your file 520 | class BlobstoreUpload(blobstore_handlers.BlobstoreUploadHandler): 521 | def post(self): 522 | # check authorised 523 | #if isAuth(self.request.remote_addr,self.request.get('api_key')) == False: 524 | # raise Forbidden("Invalid Credentials") 525 | 526 | upload_files = self.get_uploads() 527 | blob_info = upload_files[0] 528 | logging.info(blob_info.key()) 529 | self.redirect('/blobstore/response/%s' % blob_info.key()) # has to return 301, 302, 303 530 | 531 | class BlobstoreResponse(webapp.RequestHandler): 532 | def get(self, blob_key): 533 | xml_response(self, 'app.success', blob_key) 534 | 535 | # END Request Handlers 536 | 537 | # START Application 538 | application = webapp.WSGIApplication([ 539 | ('/upload/(avatar|photo)', Upload), 540 | ('/(resize|crop)/(avatar|photo)', Manipulate), 541 | ('/view/(avatar|photo)/(large|medium|small|image|source)/([-\w]+)', View), 542 | ('/blobstore/url', BlobstoreUrl), 543 | ('/blobstore/upload', BlobstoreUpload), 544 | ('/blobstore/response/([^/]+)?', BlobstoreResponse) 545 | ],debug=True) 546 | 547 | def main(): 548 | run_wsgi_app(application) 549 | 550 | if __name__ == '__main__': 551 | main() 552 | 553 | # END Application -------------------------------------------------------------------------------- /appmaildemo/app.yaml: -------------------------------------------------------------------------------- 1 | application: appmaildemo 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: .* 8 | script: main.py 9 | -------------------------------------------------------------------------------- /appmaildemo/index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | 13 | -------------------------------------------------------------------------------- /appmaildemo/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | #!/usr/bin/env python 4 | # 5 | # Copyright 2007 Google Inc. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | import cgi 20 | import os 21 | import logging 22 | import contextlib 23 | from xml.dom import minidom 24 | from xml.dom.minidom import Document 25 | import exceptions 26 | import warnings 27 | import imghdr 28 | from google.appengine.api import images 29 | from google.appengine.api import users 30 | from google.appengine.ext import db 31 | from google.appengine.ext import webapp 32 | from google.appengine.ext.webapp.util import run_wsgi_app 33 | from google.appengine.ext.webapp import template 34 | from google.appengine.api import mail 35 | import wsgiref.handlers 36 | 37 | # START Constants 38 | CONTENT_TYPE_HEADER = "Content-Type" 39 | CONTENT_TYPE_TEXT = "text/plain" 40 | XML_CONTENT_TYPE = "application/xml" 41 | XML_ENCODING = "utf-8" 42 | """ 43 | Allows you to specify IP addresses and associated "api_key"s to prevent others from using your app. 44 | Storage and Manipulation methods will check for this "api_key" in the POST/GET params. 45 | Retrieval methods don't use it (however you could enable them to use it, but maybe rewrite so you have a "read" key and a "write" key to prevent others from manipulating your data). 46 | 47 | Set "AUTH = False" to disable (allowing anyone use your app and CRUD your data). 48 | 49 | To generate a hash/api_key visit https://www.grc.com/passwords.htm 50 | To find your ip visit http://www.whatsmyip.org/ 51 | """ 52 | AUTH = { 53 | '000.000.000.000':'JLQ7P5SnTPq7AJvLnUysJmXSeXTrhgaJ', 54 | } 55 | # END Constants 56 | 57 | # START Exception Handling 58 | class Error(StandardError): 59 | pass 60 | class Forbidden(Error): 61 | pass 62 | 63 | logging.getLogger().setLevel(logging.DEBUG) 64 | 65 | @contextlib.contextmanager 66 | def mailExcpHandler(ctx): 67 | try: 68 | yield {} 69 | except (ValueError), exc: 70 | xml_error_response(ctx, 400 ,'app.invalid_parameters', 'The indicated parameters are not valid: ' + exc.message) 71 | except (Forbidden), exc: 72 | xml_error_response(ctx, 403 ,'app.forbidden', 'You don\'t have permission to perform this action: ' + exc.message) 73 | except (Exception), exc: 74 | xml_error_response(ctx, 500 ,'system.other', 'An unexpected error in the web service has happened: ' + exc.message) 75 | 76 | def xml_error_response(ctx, status, error_id, error_msg): 77 | ctx.error(status) 78 | doc = Document() 79 | errorcard = doc.createElement("error") 80 | errorcard.setAttribute("id", error_id) 81 | doc.appendChild(errorcard) 82 | ptext = doc.createTextNode(error_msg) 83 | errorcard.appendChild(ptext) 84 | ctx.response.headers[CONTENT_TYPE_HEADER] = XML_CONTENT_TYPE 85 | ctx.response.out.write(doc.toxml(XML_ENCODING)) 86 | # END Exception Handling 87 | 88 | # START Helper Methods 89 | def isAuth(ip = None, key = None): 90 | if AUTH == False: 91 | return True 92 | elif AUTH.has_key(ip) and key == AUTH[ip]: 93 | return True 94 | else: 95 | return False 96 | 97 | # END Helper Methods 98 | 99 | 100 | # START Request Handlers 101 | class Send(webapp.RequestHandler): 102 | def post(self): 103 | """ 104 | Sends an email based on POST params. It will queue if resources are unavailable at the time. 105 | 106 | Returns "Success" 107 | 108 | POST Args: 109 | to: the receipent address 110 | from: the sender address (must be a registered GAE email) 111 | subject: email subject 112 | body: email body content 113 | """ 114 | with mailExcpHandler(self): 115 | # check authorised 116 | if isAuth(self.request.remote_addr,self.request.POST.get('api_key')) == False: 117 | raise Forbidden("Invalid Credentials") 118 | 119 | # read data from request 120 | mail_to = str(self.request.POST.get('to')) 121 | mail_from = str(self.request.POST.get('from')) 122 | mail_subject = str(self.request.POST.get('subject')) 123 | mail_plain = str(self.request.POST.get('plain')) 124 | mail_html = str(self.request.POST.get('html')) 125 | 126 | message = mail.EmailMessage() 127 | message.sender = mail_from 128 | message.to = mail_to 129 | message.subject = mail_subject 130 | message.body = mail_plain 131 | if mail_html != None and mail_html != "": 132 | message.html = mail_html 133 | 134 | message.send() 135 | 136 | self.response.headers[CONTENT_TYPE_HEADER] = CONTENT_TYPE_TEXT 137 | self.response.out.write("Success") 138 | 139 | 140 | # END Request Handlers 141 | 142 | # START Application 143 | application = webapp.WSGIApplication([ 144 | ('/send', Send) 145 | ],debug=True) 146 | 147 | def main(): 148 | run_wsgi_app(application) 149 | 150 | if __name__ == '__main__': 151 | main() 152 | 153 | # END Application -------------------------------------------------------------------------------- /libs/php/appimage/README: -------------------------------------------------------------------------------- 1 | AppImage 2 | 3 | A PHP library that allows interaction with an instance of AppImage hosted on Google App Engine. Designed to be used in PHP projects or as a CakePHP vendor. 4 | 5 | SOURCE 6 | ---------------------------------------------------------------------------------------- 7 | http://github.com/benjaminpearson/gae-web-services 8 | 9 | 10 | USAGE 11 | ---------------------------------------------------------------------------------------- 12 | 13 | CakePHP: 14 | App::import('Vendor', 'AppImageCore', array('file' => 'appimage'.DS.'appimage.core.php')); 15 | $AppImageCore = new AppImageCore(); 16 | $AppImageCore->upload('avatar', $_FILES['tmp_name'], 'johnsmith'); 17 | 18 | 19 | PHP: 20 | require_once "appimage".DIRECTORY_SEPARATOR."appimage.core.php"; 21 | $AppImageCore = new AppImageCore(); 22 | $AppImageCore->upload('avatar', $_FILES['tmp_name'], 'johnsmith'); 23 | 24 | 25 | See test files for more examples. -------------------------------------------------------------------------------- /libs/php/appimage/appimage.config.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/php/appimage/appimage.core.php: -------------------------------------------------------------------------------- 1 | url = $url; 23 | $this->api_key = $api_key; 24 | $this->AppImageRest = new AppImageRest($this->url); 25 | $this->XMLHelper = new XMLHelper(); 26 | } 27 | 28 | /** 29 | * Uploads an image into the GAE datastore. 30 | * 31 | * @param type either "avatar" or "photo" (or any other model you've custom added) 32 | * @param file the tmp location ($_FILES['tmp_name']) of the uploaded php file (ie, /private/var/tmp/phpRDJST9) 33 | * @param filesize the filesize ($_FILES['size']) of the uploaded php file (ie, /private/var/tmp/phpRDJST9) 34 | * @param img_id a unique id to assign to the image, for an avatar this might relate to a username/user_id in your db. 35 | * 36 | * @return image_key the internal datastore key of the newly created GAE record or false on fail 37 | */ 38 | function upload($type, $file, $filesize, $img_id) { 39 | $image = '@'.$file; 40 | $api_key = $this->api_key; 41 | 42 | // if > 1mb, upload image to blobstore first 43 | if ($filesize > 1000000) { 44 | $blob_url = $this->_getBlobstoreUrl(); 45 | $blob_key = $this->_uploadToBlobstore($blob_url, $image); 46 | $response = $this->AppImageRest->post('upload/'.$type, compact('api_key','blob_key','img_id','user_id')); 47 | } else { // else < 1mb, upload directly 48 | $response = $this->AppImageRest->post('upload/'.$type, compact('api_key','image','img_id','user_id')); 49 | } 50 | 51 | $response = $this->XMLHelper->toArray($response); 52 | return $response['status'] == 'app.success' ? $response['message'] : false; 53 | } 54 | 55 | /** 56 | * Method for retrieving a GAE Blobstore API url that can later be used 57 | * to POST an image to. 58 | * 59 | * @scope private 60 | * 61 | * @return url an unique url ready for an image to be POSTed to it or false on fail 62 | */ 63 | function _getBlobstoreUrl() { 64 | $api_key = $this->api_key; 65 | $response = $this->AppImageRest->post('blobstore/url', compact('api_key')); 66 | $response = $this->XMLHelper->toArray($response); 67 | return $response['status'] == 'app.success' ? $response['message'] : false; 68 | } 69 | 70 | /** 71 | * Method for retrieving a GAE Blobstore API url that can later be used 72 | * to POST an image to. 73 | * 74 | * @scope private 75 | * 76 | * @param blob_url the url generated by _getBlobstoreUrl() method 77 | * @param image the image file to upload 78 | * 79 | * @return blob_key a unique key identifying the blobstore record that was just created or false on fail 80 | */ 81 | function _uploadToBlobstore($blob_url, $image) { 82 | $api_key = $this->api_key; 83 | $response = $this->AppImageRest->post(str_replace($this->url,"",$blob_url), compact('image')); 84 | $response = $this->XMLHelper->toArray($response); 85 | return $response['status'] == 'app.success' ? $response['message'] : false; 86 | } 87 | 88 | /** 89 | * Returns an array of URLs for viewing the different image sizes. 90 | * You can now use it in your php rendering (ie, thumbnail) 91 | * 92 | * @param type either "avatar" or "photo" (or any other model you've custom added) 93 | * @param img_id unique id for the image (assigned on upload). This is not a GAE UUID. 94 | */ 95 | function getUrls($type, $img_id) { 96 | $urls = array( 97 | 'source' => $this->url.'view/'.$type.'/source/'.$img_id, 98 | 'image' => $this->url.'view/'.$type.'/image/'.$img_id, 99 | 'large' => $this->url.'view/'.$type.'/large/'.$img_id, 100 | 'medium' => $this->url.'view/'.$type.'/medium/'.$img_id, 101 | 'small' => $this->url.'view/'.$type.'/small/'.$img_id 102 | ); 103 | return $urls; 104 | } 105 | 106 | /** 107 | * Crops the image specified using the original uploaded source file as the basis. 108 | * After transforming it regenerates the thumbnail image. 109 | * 110 | * @param type either "avatar" or "photo" (or any other model you've custom added) 111 | * @param img_id unique id for the image (assigned on upload). This is not a GAE UUID. 112 | * @param x1,y1,x2,y2 coordinates (in pixels or percentages). If percentages, then number between 0.0 and 1.0. 113 | * @param unit either "pixels" or "percentage" 114 | */ 115 | function crop($type, $img_id, $x1, $y1, $x2, $y2, $unit) { 116 | $params = array('img_id' => $img_id, 117 | 'x1' => $x1, 118 | 'y1' => $y1, 119 | 'x2' => $x2, 120 | 'y2' => $y2, 121 | 'unit' => $unit, 122 | 'api_key' => $this->api_key); 123 | 124 | $response = $this->AppImageRest->get('crop/'.$type, $params); 125 | $response = $this->XMLHelper->toArray($response); 126 | return $response['status'] == 'app.success' ? $response['message'] : false; 127 | } 128 | 129 | /** 130 | * Resizes the image specified using the original uploaded source file as the basis. 131 | * After transforming it regenerates the thumbnail image. 132 | * 133 | * NOTE: From http://code.google.com/appengine/docs/python/images/imageclass.html#Image_resize 134 | * Resizes an image, scaling down or up to the given width and height. The resize transform preserves 135 | * the aspect ratio of the image. If both the width and the height arguments are provided, the transform 136 | * uses the dimension that results in a smaller image. 137 | * 138 | * @param type either "avatar" or "photo" (or any other model you've custom added) 139 | * @param img_id unique id for the image (assigned on upload). This is not a GAE UUID. 140 | * @param width,height resize to size (in pixels or percentages). If percentages, then number between 0.0 and 1.0. 141 | * @param unit either "pixels" or "percentage" 142 | */ 143 | function resize($type, $img_id, $width, $height, $unit) { 144 | $params = array('img_id' => $img_id, 145 | 'width' => $width, 146 | 'height' => $height, 147 | 'unit' => $unit, 148 | 'api_key' => $this->api_key); 149 | 150 | $response = $this->AppImageRest->get('resize/'.$type, $params); 151 | $response = $this->XMLHelper->toArray($response); 152 | return $response['status'] == 'app.success' ? $response['message'] : false; 153 | } 154 | } 155 | ?> 156 | -------------------------------------------------------------------------------- /libs/php/appimage/helpers/appimage.rest.php: -------------------------------------------------------------------------------- 1 | url = $url; 11 | } 12 | 13 | function post($method, $params = array()) { 14 | try { 15 | $url = $this->url.$method; 16 | $response = $this->_httpPost($url, $params); 17 | return $response; 18 | } catch (Exception $e) { 19 | return null; 20 | } 21 | } 22 | 23 | function get($method, $params = array()) { 24 | try { 25 | $url = $this->url.$method.'?'.$this->_queryString($params); 26 | $response = $this->_httpGet($url, $params); 27 | return $response; 28 | } catch (Exception $e) { 29 | return null; 30 | } 31 | } 32 | 33 | function _queryString($params) { 34 | $url_params = array(); 35 | foreach($params as $key => $value) { 36 | $url_params[] = urlencode($key).'='.urlencode($value); 37 | } 38 | $query_string = implode('&',$url_params); 39 | 40 | return $query_string; 41 | } 42 | 43 | function _httpGet($url) { 44 | $c = curl_init(); 45 | curl_setopt($c, CURLOPT_URL, $url); 46 | curl_setopt($c, CURLOPT_HTTPGET, true); 47 | // some environments have difficulty with CURLOPT_SSL_VERIFYPEER being set to "true". 48 | // if this is the case, set to "false" but research the implications. 49 | // http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTSSLVERIFYPEER 50 | curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true); 51 | curl_setopt($c, CURLOPT_RETURNTRANSFER, 1); 52 | $output = curl_exec($c); 53 | curl_close($c); 54 | return $output; 55 | } 56 | 57 | function _httpPost($url, $data) { 58 | $c = curl_init(); 59 | curl_setopt($c, CURLOPT_URL, $url); 60 | curl_setopt($c, CURLOPT_POST, 1); 61 | curl_setopt($c, CURLOPT_FOLLOWLOCATION, true); // has to be used for blobstore redirect on upload 62 | curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true); 63 | curl_setopt($c, CURLOPT_POSTFIELDS, $data); 64 | curl_setopt($c, CURLOPT_RETURNTRANSFER, 1); 65 | $output = curl_exec($c); 66 | curl_close($c); 67 | return $output; 68 | } 69 | 70 | } 71 | ?> 72 | -------------------------------------------------------------------------------- /libs/php/appimage/helpers/appimage.xml.php: -------------------------------------------------------------------------------- 1 | <$rootNodeName/>"); 23 | } 24 | 25 | // loop through the data passed in. 26 | foreach($data as $key => $value) { 27 | // no numeric keys in our xml please! 28 | $numeric = 0; 29 | if(is_numeric($key)) { 30 | $numeric = 1; 31 | $key = $rootNodeName; 32 | } 33 | 34 | // delete any char not allowed in XML element names 35 | $key = preg_replace('/[^a-z0-9\-\_\.\:]/i', '', $key); 36 | 37 | // if there is another array found recrusively call this function 38 | if(is_array($value)) { 39 | $node = XMLHelper::isAssoc($value) || $numeric ? $xml->addChild($key) : $xml; 40 | 41 | // recrusive call. 42 | if($numeric) { 43 | $key = 'anon'; 44 | } 45 | XMLHelper::toXml($value, $key, $node); 46 | } else { 47 | // add single node. 48 | $value = htmlentities($value); 49 | $xml->addChild($key, $value); 50 | } 51 | } 52 | 53 | // pass back as XML 54 | return $xml->asXML(); 55 | } 56 | 57 | 58 | /** 59 | * Convert an XML document to a multi dimensional array 60 | * Pass in an XML document (or SimpleXMLElement object) and this recrusively loops through and builds a representative array 61 | * 62 | * @param string $xml - XML document - can optionally be a SimpleXMLElement object 63 | * @return array ARRAY 64 | */ 65 | public static function toArray( $xml ) { 66 | if(is_string($xml)) { 67 | $xml = new SimpleXMLElement( $xml ); 68 | } 69 | 70 | $children = $xml->children(); 71 | 72 | if(!$children) { 73 | return (string) $xml; 74 | } 75 | 76 | $arr = array(); 77 | foreach($children as $key => $node) { 78 | $node = XMLHelper::toArray($node); 79 | 80 | // support for 'anon' non-associative arrays 81 | if($key == 'anon') { 82 | $key = count($arr); 83 | } 84 | 85 | // ensures all keys are in lowercase underscore just for ease of use 86 | $key = XMLHelper::_fromCamelCase($key); 87 | 88 | // if the node is already set, put it into an array 89 | if(isset($arr[$key])) { 90 | if(!is_array($arr[$key]) || (isset($arr[$key][0]) && $arr[$key][0] == null)) { 91 | $arr[$key] = array( $arr[$key] ); 92 | } 93 | $arr[$key][] = $node; 94 | } else { 95 | $arr[$key] = $node; 96 | } 97 | } 98 | return $arr; 99 | } 100 | 101 | // determine if a variable is an associative array 102 | public static function isAssoc( $array ) { 103 | return (is_array($array) && 0 !== count(array_diff_key($array, array_keys(array_keys($array))))); 104 | } 105 | 106 | /** 107 | * Translates a camel case string into a string with underscores (e.g. firstName = first_name) 108 | * Source: http://www.paulferrett.com/2009/php-camel-case-functions/ 109 | * 110 | * @param string $str String in camel case format 111 | * @return string $str Translated into underscore format 112 | */ 113 | private static function _fromCamelCase($str) { 114 | $str[0] = strtolower($str[0]); 115 | $func = create_function('$c', 'return "_" . strtolower($c[1]);'); 116 | return preg_replace_callback('/([A-Z])/', $func, $str); 117 | } 118 | } 119 | ?> -------------------------------------------------------------------------------- /libs/php/appimage/tests/appimage.core.cakemate.test.php: -------------------------------------------------------------------------------- 1 | AppImageCore = new AppImageCore(); 26 | } 27 | 28 | function testUploadAvatarSmall() { 29 | $response = $this->AppImageCore->upload('avatar', $this->small_file, filesize($this->small_file), $this->small_img_id); 30 | 31 | $this->assertTrue($response); 32 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 33 | $this->assertEqual(md5(file_get_contents($urls['source'])),"5ad7d3f96b182d3770b1cd56339e8131"); 34 | } 35 | 36 | function testUploadAvatarBig() { 37 | $response = $this->AppImageCore->upload('avatar', $this->big_file, filesize($this->big_file), $this->big_img_id); 38 | 39 | $this->assertTrue($response); 40 | $urls = $this->AppImageCore->getUrls('avatar', $this->big_img_id); 41 | $this->assertEqual(md5(file_get_contents($urls['source'])),"ffd83af30f249186488f2d819f29b09b"); 42 | } 43 | 44 | function testUploadPhoto() { 45 | $response = $this->AppImageCore->upload('photo', $this->big_file, filesize($this->big_file), $this->big_img_id); 46 | 47 | $this->assertTrue($response); 48 | $urls = $this->AppImageCore->getUrls('photo', $this->big_img_id); 49 | $this->assertEqual(md5(file_get_contents($urls['source'])),"67199e0cb90eb8d6ff22a6c4a1a5f468"); 50 | } 51 | 52 | function testGetUrlsAvatar() { 53 | $response = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 54 | //debug($response); 55 | } 56 | 57 | function testGetUrlsPhoto() { 58 | $response = $this->AppImageCore->getUrls('photo', $this->small_img_id); 59 | //debug($response); 60 | } 61 | 62 | function testCropPercentage() { 63 | $x1 = 0.1; 64 | $y1 = 0.2; 65 | $x2 = 0.5; 66 | $y2 = 0.8; 67 | $unit = "percentage"; 68 | $response = $this->AppImageCore->crop('avatar', $this->small_img_id, $x1, $y1, $x2, $y2, $unit); 69 | 70 | $this->assertTrue($response); 71 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 72 | $this->assertEqual(md5(file_get_contents($urls['image'])),"58bd13d32e8101645c293cdeff0a5ec1"); 73 | } 74 | 75 | function testCropPixels() { 76 | $x1 = 20; 77 | $y1 = 80; 78 | $x2 = 260; 79 | $y2 = 300; 80 | $unit = "pixels"; 81 | $response = $this->AppImageCore->crop('avatar', $this->small_img_id, $x1, $y1, $x2, $y2, $unit); 82 | 83 | $this->assertTrue($response); 84 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 85 | $this->assertEqual(md5(file_get_contents($urls['image'])),"5353c2b9ad9e60cd9ecbe921dab2184f"); 86 | } 87 | 88 | function testResizePercentage() { 89 | // note: keeps aspect ratio, using dimensions that produce smallest image. 90 | // using the image provided will result in 123 x 154px image. 91 | $width = 0.4; 92 | $height = 0.5; 93 | $unit = "percentage"; 94 | $response = $this->AppImageCore->resize('avatar', $this->small_img_id, $width, $height, $unit); 95 | 96 | $this->assertTrue($response); 97 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 98 | $this->assertEqual(md5(file_get_contents($urls['image'])),"7689c434fa853d0a9460c3a28cef0ebb"); 99 | } 100 | 101 | function testResizePixels() { 102 | // note: keeps aspect ratio, using dimensions that produce smallest image. 103 | // using the image provided will result in 160 x 200px image. 104 | $width = 300; 105 | $height = 200; 106 | $unit = "pixels"; 107 | $response = $this->AppImageCore->resize('avatar', $this->small_img_id, $width, $height, $unit); 108 | 109 | $this->assertTrue($response); 110 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 111 | $this->assertEqual(md5(file_get_contents($urls['image'])),"ed7c7e03524889af6f94b9bfb811b4ae"); 112 | } 113 | 114 | } 115 | ?> 116 | -------------------------------------------------------------------------------- /libs/php/appimage/tests/appimage.core.phpunit.test.php: -------------------------------------------------------------------------------- 1 | AppImageCore = new AppImageCore(); 23 | } 24 | 25 | function testUploadAvatarSmall() { 26 | $response = $this->AppImageCore->upload('avatar', $this->small_file, filesize($this->small_file), $this->small_img_id); 27 | 28 | $this->assertTrue(!empty($response)); 29 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 30 | $this->assertEquals(md5(file_get_contents($urls['source'])),"5ad7d3f96b182d3770b1cd56339e8131"); 31 | } 32 | 33 | function testUploadAvatarBig() { 34 | $response = $this->AppImageCore->upload('avatar', $this->big_file, filesize($this->big_file), $this->big_img_id); 35 | 36 | $this->assertTrue(!empty($response)); 37 | $urls = $this->AppImageCore->getUrls('avatar', $this->big_img_id); 38 | $this->assertEquals(md5(file_get_contents($urls['source'])),"ffd83af30f249186488f2d819f29b09b"); 39 | } 40 | 41 | function testUploadPhoto() { 42 | $response = $this->AppImageCore->upload('photo', $this->big_file, filesize($this->big_file), $this->big_img_id); 43 | 44 | $this->assertTrue(!empty($response)); 45 | $urls = $this->AppImageCore->getUrls('photo', $this->big_img_id); 46 | $this->assertEquals(md5(file_get_contents($urls['source'])),"67199e0cb90eb8d6ff22a6c4a1a5f468"); 47 | } 48 | 49 | function testGetUrlsAvatar() { 50 | $response = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 51 | //print_r($response); 52 | } 53 | 54 | function testGetUrlsPhoto() { 55 | $response = $this->AppImageCore->getUrls('photo', $this->small_img_id); 56 | //print_r($response); 57 | } 58 | 59 | function testCropPercentage() { 60 | $x1 = 0.1; 61 | $y1 = 0.2; 62 | $x2 = 0.5; 63 | $y2 = 0.8; 64 | $unit = "percentage"; 65 | $response = $this->AppImageCore->crop('avatar', $this->small_img_id, $x1, $y1, $x2, $y2, $unit); 66 | 67 | $this->assertTrue(!empty($response)); 68 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 69 | $this->assertEquals(md5(file_get_contents($urls['image'])),"58bd13d32e8101645c293cdeff0a5ec1"); 70 | } 71 | 72 | function testCropPixels() { 73 | $x1 = 20; 74 | $y1 = 80; 75 | $x2 = 260; 76 | $y2 = 300; 77 | $unit = "pixels"; 78 | $response = $this->AppImageCore->crop('avatar', $this->small_img_id, $x1, $y1, $x2, $y2, $unit); 79 | 80 | $this->assertTrue(!empty($response)); 81 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 82 | $this->assertEquals(md5(file_get_contents($urls['image'])),"5353c2b9ad9e60cd9ecbe921dab2184f"); 83 | } 84 | 85 | function testResizePercentage() { 86 | // note: keeps aspect ratio, using dimensions that produce smallest image. 87 | // using the image provided will result in 123 x 154px image. 88 | $width = 0.4; 89 | $height = 0.5; 90 | $unit = "percentage"; 91 | $response = $this->AppImageCore->resize('avatar', $this->small_img_id, $width, $height, $unit); 92 | 93 | $this->assertTrue(!empty($response)); 94 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 95 | $this->assertEquals(md5(file_get_contents($urls['image'])),"7689c434fa853d0a9460c3a28cef0ebb"); 96 | } 97 | 98 | function testResizePixels() { 99 | // note: keeps aspect ratio, using dimensions that produce smallest image. 100 | // using the image provided will result in 160 x 200px image. 101 | $width = 300; 102 | $height = 200; 103 | $unit = "pixels"; 104 | $response = $this->AppImageCore->resize('avatar', $this->small_img_id, $width, $height, $unit); 105 | 106 | $this->assertTrue(!empty($response)); 107 | $urls = $this->AppImageCore->getUrls('avatar', $this->small_img_id); 108 | $this->assertEquals(md5(file_get_contents($urls['image'])),"ed7c7e03524889af6f94b9bfb811b4ae"); 109 | } 110 | } 111 | ?> 112 | -------------------------------------------------------------------------------- /libs/php/appimage/tests/big.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjaminpearson/gae-web-services/5e688336d199c24db51367f6e890cebdf9ad9142/libs/php/appimage/tests/big.jpg -------------------------------------------------------------------------------- /libs/php/appimage/tests/small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjaminpearson/gae-web-services/5e688336d199c24db51367f6e890cebdf9ad9142/libs/php/appimage/tests/small.jpg -------------------------------------------------------------------------------- /libs/php/appmail/README: -------------------------------------------------------------------------------- 1 | AppMail 2 | 3 | A PHP library that allows interaction with an instance of AppMail hosted on Google App Engine. Designed to be used in PHP projects or as a CakePHP vendor. 4 | 5 | SOURCE 6 | ---------------------------------------------------------------------------------------- 7 | http://github.com/benjaminpearson/gae-web-services 8 | 9 | 10 | USAGE 11 | ---------------------------------------------------------------------------------------- 12 | 13 | CakePHP: 14 | App::import('Vendor', 'AppMailCore', array('file' => 'appmail'.DS.'appmail.core.php')); 15 | $AppMailCore = new AppMailCore(); 16 | $AppMailCore->send("John Recipient ", "John Sender ", "Subject", "Message"); 17 | 18 | 19 | PHP: 20 | require_once "appmail".DIRECTORY_SEPARATOR."appmail.core.php"; 21 | $AppMailCore = new AppMailCore(); 22 | $AppMailCore->send("John Recipient ", "John Sender ", "Subject", "Message"); 23 | 24 | 25 | See test files for more examples. -------------------------------------------------------------------------------- /libs/php/appmail/appmail.config.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/php/appmail/appmail.core.php: -------------------------------------------------------------------------------- 1 | url = $url; 21 | $this->api_key = $api_key; 22 | $this->AppMailRest = new AppMailRest($this->url); 23 | } 24 | 25 | /** 26 | * Asynchronously sends an email using Google App Engine 27 | * 28 | * Params are fairly self explanatory. However, note that the "from" address must be a registered email with 29 | * your Google App Engine account. 30 | */ 31 | function send($to, $from, $subject, $body) { 32 | $api_key = $this->api_key; 33 | $status = $this->AppMailRest->post('send', compact('api_key','to','from','subject','body')); 34 | return $status; 35 | } 36 | } 37 | ?> 38 | -------------------------------------------------------------------------------- /libs/php/appmail/helpers/appmail.rest.php: -------------------------------------------------------------------------------- 1 | url = $url; 11 | } 12 | 13 | function post($method, $params = array()) { 14 | try { 15 | $url = $this->url.$method; 16 | $response = $this->_httpPost($url, $params); 17 | return $response; 18 | } catch (Exception $e) { 19 | return null; 20 | } 21 | } 22 | 23 | function _queryString($params) { 24 | $url_params = array(); 25 | foreach($params as $key => $value) { 26 | $url_params[] = urlencode($key).'='.urlencode($value); 27 | } 28 | $query_string = implode('&',$url_params); 29 | 30 | return $query_string; 31 | } 32 | 33 | function _httpPost($url, $data) { 34 | $c = curl_init(); 35 | curl_setopt($c, CURLOPT_URL, $url); 36 | curl_setopt($c, CURLOPT_POST, 1); 37 | curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true); 38 | curl_setopt($c, CURLOPT_POSTFIELDS, $data); 39 | curl_setopt($c, CURLOPT_RETURNTRANSFER, 1); 40 | $output = curl_exec($c); 41 | curl_close($c); 42 | return $output; 43 | } 44 | 45 | } 46 | ?> 47 | -------------------------------------------------------------------------------- /libs/php/appmail/tests/appmail.core.cakemate.test.php: -------------------------------------------------------------------------------- 1 | AppMailCore = new AppMailCore(); 18 | } 19 | 20 | function testSend() { 21 | $to = "John Smith "; 22 | $from = "John Smith "; // has to be a registered GAE email 23 | $subject = "Test email"; 24 | $body = "This is a test message"; 25 | $status = $this->AppMailCore->send($to, $from, $subject, $body); 26 | $this->assertEqual(sizeof($status), "Success"); 27 | debug($status); 28 | } 29 | } 30 | ?> 31 | -------------------------------------------------------------------------------- /libs/php/appmail/tests/appmail.core.phpunit.test.php: -------------------------------------------------------------------------------- 1 | AppMailCore = new AppMailCore(); 19 | } 20 | 21 | function testSend() { 22 | $to = "John Smith "; 23 | $from = "John Smith "; // has to be a registered GAE email 24 | $subject = "Test email"; 25 | $body = "This is a test message"; 26 | $status = $this->AppMailCore->send($to, $from, $subject, $body); 27 | $this->assertEquals(sizeof($status), "Success"); 28 | print_r($status); 29 | } 30 | } 31 | ?> 32 | --------------------------------------------------------------------------------