├── 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,
)
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 |
--------------------------------------------------------------------------------