larsklevan-attachment_fu_app_engine 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Lars Klevan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,62 @@
1
+ AttachmentFuAppEngine
2
+ =====================
3
+
4
+ Extension for AttachmentFu (http://github.com/technoweenie/attachment_fu) which uses the Google App Engine for storage and image resizing. Removes the need to install ImageMagick and allows you to add or change thumbnail sizes without migrating previous data.
5
+
6
+ The Google App Engine backend code is included so you can run it in your own App Engine instance.
7
+
8
+ Example
9
+ =======
10
+
11
+ class Photo < ActiveRecord::Base
12
+ has_attachment :storage => :app_engine, :thumbnails => {:small_square => "45x45!"}
13
+ end
14
+
15
+ AttachmentFuAppEngine attempts to reproduce the resize format from ImageMagick.
16
+
17
+ * "100x100" - scale the image to fit within a 100x100 box (the larger dimension will be 100, such as 100x75), preserving the aspect ratio. Will scale up small images.
18
+ * "100x100!" - scale the image to be exactly 100x100 by scaling proportionately and cropping off the edges. Will not distort the image but may hide part of it.
19
+ * "100x100>" - scale the image to be at most 100x100 - smaller images will not be enlarged.
20
+
21
+ In addition to the standard thumbnail specification:
22
+ Image.find(1).public_filename(:small_square)
23
+
24
+ you can also use:
25
+ Image.find(1).public_filename('40x40!')
26
+
27
+
28
+ Installation
29
+ ============
30
+
31
+ gem install larsklevan-attachment_fu_app_engine -s http://gems.github.com
32
+ OR
33
+ script/plugin install git://github.com/larsklevan/attachment_fu_app_engine.git
34
+
35
+
36
+ Migration
37
+ =========
38
+
39
+ You can use the provided rake task to move data from S3 to the App Engine. Just run:
40
+ rake ATTACHMENT_CLASS=Image migrate_s3_to_app_engine
41
+
42
+
43
+ Configuration
44
+ =============
45
+
46
+ You can use your own instance of the App Engine code to store images so you have your own quotas. Use
47
+ the provided app engine code and configure the URL as follows:
48
+
49
+ Technoweenie::AttachmentFu::Backends::AppEngineBackend.base_url = "http://attachment-fu-gae.appspot.com"
50
+
51
+
52
+ Limitations
53
+ ===========
54
+
55
+ * Tested with Rails 2.1 only
56
+ * Google App Engine is in early release, so if you go past your daily storage/bandwidth/CPU limit you are out of luck.
57
+ * Google App Engine does not support HTTPS.
58
+ * Google App Engine currently has a limit of 1MB per reocrd
59
+
60
+ For more information about the status of the App Engine see http://googleappengine.blogspot.com/2008/04/were-up-and-running.html
61
+
62
+ Copyright (c) 2008 Lars Klevan, released under the MIT license
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the attachment_fu_app_engine plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the attachment_fu_app_engine plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'AttachmentFuAppEngine'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,8 @@
1
+ application: attachment-fu-gae
2
+ version: 1
3
+ runtime: python
4
+ api_version: 1
5
+
6
+ handlers:
7
+ - url: /.*
8
+ script: attachment.py
@@ -0,0 +1,219 @@
1
+ import os
2
+ import cgi
3
+ import logging
4
+ from StringIO import StringIO
5
+ import struct
6
+
7
+ import wsgiref.handlers
8
+ from datetime import date
9
+
10
+ from google.appengine.api import images
11
+ from google.appengine.api import memcache
12
+ from google.appengine.ext import db
13
+ from google.appengine.ext import webapp
14
+ from google.appengine.ext.webapp import template
15
+
16
+ class Attachment(db.Model):
17
+ path = db.StringProperty()
18
+
19
+ filename = db.StringProperty()
20
+ uploaded_data = db.BlobProperty()
21
+ content_type = db.StringProperty()
22
+ height = db.IntegerProperty()
23
+ width = db.IntegerProperty()
24
+ size = db.IntegerProperty()
25
+
26
+ # FROM http://www.google.com/codesearch?hl=en&q=+getImageInfo+show:RjgT7H1iBVM:V39CptbrGJ8:XcXNaKeZR3k&sa=N&cd=2&ct=rc&cs_p=http://www.zope.org/Products/Zope3/3.0.0final/ZopeX3-3.0.0.tgz&cs_f=ZopeX3-3.0.0/Dependencies/zope.app.file-ZopeX3-3.0.0/zope.app.file/image.py#l88
27
+ def extract_image_attributes(self,data):
28
+ data = str(data)
29
+ size = len(data)
30
+ height = -1
31
+ width = -1
32
+ content_type = ''
33
+
34
+ # handle GIFs
35
+ if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):
36
+ # Check to see if content_type is correct
37
+ content_type = 'image/gif'
38
+ w, h = struct.unpack("<HH", data[6:10])
39
+ width = int(w)
40
+ height = int(h)
41
+
42
+ # See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
43
+ # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
44
+ # and finally the 4-byte width, height
45
+ elif ((size >= 24) and data.startswith('\211PNG\r\n\032\n') and (data[12:16] == 'IHDR')):
46
+ content_type = 'image/png'
47
+ w, h = struct.unpack(">LL", data[16:24])
48
+ width = int(w)
49
+ height = int(h)
50
+
51
+ # Maybe this is for an older PNG version.
52
+ elif (size >= 16) and data.startswith('\211PNG\r\n\032\n'):
53
+ # Check to see if we have the right content type
54
+ content_type = 'image/png'
55
+ w, h = struct.unpack(">LL", data[8:16])
56
+ width = int(w)
57
+ height = int(h)
58
+
59
+ # handle JPEGs
60
+ elif (size >= 2) and data.startswith('\377\330'):
61
+ content_type = 'image/jpeg'
62
+ jpeg = StringIO(data)
63
+ jpeg.read(2)
64
+ b = jpeg.read(1)
65
+ try:
66
+ while (b and ord(b) != 0xDA):
67
+ while (ord(b) != 0xFF): b = jpeg.read(1)
68
+ while (ord(b) == 0xFF): b = jpeg.read(1)
69
+ if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
70
+ jpeg.read(3)
71
+ h, w = struct.unpack(">HH", jpeg.read(4))
72
+ break
73
+ else:
74
+ jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2)
75
+ b = jpeg.read(1)
76
+ width = int(w)
77
+ height = int(h)
78
+ except struct.error:
79
+ pass
80
+ except ValueError:
81
+ pass
82
+
83
+ return height,width
84
+
85
+ def update_uploaded_data(self, data, content_type):
86
+ if content_type.startswith('image'):
87
+ self.height, self.width = self.extract_image_attributes(data)
88
+ if not self.height:
89
+ #if we can't determine the image attributes in the original format try converting it to a PNG with a no-op rotate
90
+ image = images.Image(data)
91
+ image.rotate(0)
92
+ self.height, self.width = self.extract_image_attributes(image.execute_transforms(output_encoding=images.PNG))
93
+
94
+ self.content_type = content_type
95
+ self.uploaded_data = data
96
+ self.size = len(data)
97
+
98
+ # I'm attempting to replicate the resize format from http://www.imagemagick.org/Usage/resize/#resize
99
+ # at least enough to be usable for avatar or photo gallery thumbnails
100
+ def resize(self, format):
101
+ preserve_aspect_ratio = True
102
+ allow_scale_up = True
103
+ if format.endswith("!"):
104
+ preserve_aspect_ratio = False
105
+ format = format.rstrip("!")
106
+ elif format.endswith(">"):
107
+ allow_scale_up = False
108
+ format = format.rstrip(">")
109
+
110
+ width,height = format.split('x')
111
+
112
+ img = images.Image(self.uploaded_data)
113
+ if not preserve_aspect_ratio:
114
+ requested_aspect = float(height)/float(width)
115
+ aspect = float(self.height)/float(self.width)
116
+
117
+ ratio = requested_aspect / aspect
118
+ if (ratio < 1):
119
+ left_x = 0.0
120
+ right_x = 1.0
121
+ top_y = 0.5 - (ratio / 2)
122
+ bottom_y = 0.5 + (ratio / 2)
123
+ else:
124
+ top_y = 0.0
125
+ bottom_y = 1.0
126
+ left_x = 0.5 - ((1/ratio) / 2)
127
+ right_x = 0.5 + ((1/ratio) / 2)
128
+
129
+ # seem to have issues with small rounding errors for larger images - request for 2000x2000 can end up at 1998x2000
130
+ # presumably rounding errors - the 0-1 scale for cropping is weird...
131
+ img.crop(left_x=left_x,top_y=top_y,right_x=right_x,bottom_y=bottom_y)
132
+
133
+ if allow_scale_up or int(width) < self.width or int(height) < self.height:
134
+ img.resize(width=int(width), height=int(height))
135
+
136
+ output_encoding, content_type = images.PNG, 'image/png'
137
+ if self.content_type == 'image/jpeg' or self.content_type == 'image/jpg':
138
+ output_encoding, content_type = images.JPEG, 'image/jpeg'
139
+
140
+ img.rotate(0) #no-op so that we don't break if we haven't done any transforms
141
+ return img.execute_transforms(output_encoding), content_type
142
+
143
+ class UploadAttachmentPage(webapp.RequestHandler):
144
+ def get(self):
145
+ path = os.path.join(os.path.dirname(__file__), 'new.html')
146
+ self.response.out.write(template.render(path, {}))
147
+
148
+ class AttachmentPage(webapp.RequestHandler):
149
+ def get(self):
150
+ attachment = None
151
+ try:
152
+ id = self.request.path.split('/')[-1]
153
+ attachment = Attachment.get(db.Key(id))
154
+ except:
155
+ None
156
+
157
+ if not attachment:
158
+ attachment = db.Query(Attachment).filter("path =", self.request.path[1::]).get()
159
+
160
+ if not attachment:
161
+ # Either "id" wasn't provided, or there was no attachment with that ID
162
+ # in the datastore.
163
+ self.error(404)
164
+ return
165
+
166
+ today = date.today()
167
+ self.response.headers.add_header("Expires", date(year=today.year + 1,month=today.month, day=today.day).ctime())
168
+ format = self.request.get("resize")
169
+ if format:
170
+ memcache_client = memcache.Client()
171
+ cache_key = "attachment-" + str(attachment.key()) + "-" + format
172
+ result = memcache_client.get(cache_key)
173
+ if not result:
174
+ data, content_type = attachment.resize(format)
175
+ memcache_client.set(cache_key, [data, content_type])
176
+ else:
177
+ data, content_type = result[0], result[1]
178
+ self.response.headers['Content-Type'] = content_type
179
+ self.response.out.write(data)
180
+ else:
181
+ self.response.headers['Content-Type'] = str(attachment.content_type)
182
+ self.response.out.write(attachment.uploaded_data)
183
+
184
+ def post(self):
185
+ form = cgi.FieldStorage()
186
+
187
+ path = form.getvalue('path')
188
+ attachment = db.Query(Attachment).filter("path =", path).get()
189
+ if not attachment:
190
+ attachment = Attachment()
191
+ attachment.path = path
192
+
193
+ uploaded_data = form['uploaded_data']
194
+ attachment.filename = uploaded_data.filename
195
+
196
+ attachment.update_uploaded_data(uploaded_data.value, uploaded_data.type)
197
+ attachment.put()
198
+
199
+
200
+ logging.debug('Added attachment with path: ' + attachment.path + ' id: ' + str(attachment.key()))
201
+ self.redirect('/attachments/' + str(attachment.key()))
202
+
203
+
204
+ class RedirectPage(webapp.RequestHandler):
205
+ def get(self):
206
+ self.redirect('/attachments/new')
207
+
208
+ def main():
209
+ logging.getLogger().setLevel(logging.DEBUG)
210
+
211
+ application = webapp.WSGIApplication(
212
+ [('/attachments/new', UploadAttachmentPage),
213
+ ('/attachments.*', AttachmentPage),
214
+ ('/.*', RedirectPage)],
215
+ debug=True)
216
+ wsgiref.handlers.CGIHandler().run(application)
217
+
218
+ if __name__ == "__main__":
219
+ main()
@@ -0,0 +1,17 @@
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
+ # Unused in query history -- copied from input.
14
+ - kind: Attachment
15
+ properties:
16
+ - name: path
17
+ direction: desc
@@ -0,0 +1,17 @@
1
+ <html>
2
+ <body>
3
+ Upload a file:
4
+ <br/>
5
+ <br/>
6
+ <form action="/attachments" method="post" enctype="multipart/form-data">
7
+ <label for="file">Select a file:</label>
8
+ <br/>
9
+ <input type="file" name="uploaded_data" />
10
+ <br/>
11
+ <label for="file">Path:</label>
12
+ <input type="text" name="path" />
13
+ <br/>
14
+ <input type="submit" value="Submit" />
15
+ </form>
16
+ </body>
17
+ </html>
@@ -0,0 +1,13 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "attachment_fu_app_engine"
3
+ s.version = "0.2.0"
4
+ s.date = "2008-08-02"
5
+ s.summary = "Extension for AttachmentFu which uses the Google App Engine for storage"
6
+ s.email = "tastybyte@gmail.com"
7
+ s.homepage = "http://github.com/larsklevan/attachment_fu_app_engine"
8
+ s.description = "Extension for AttachmentFu (http://github.com/technoweenie/attachment_fu) which uses the Google App Engine for storage and image resizing."
9
+ s.has_rdoc = true
10
+ s.authors = ["Lars Klevan"]
11
+ s.files = %w{app_engine/attachment_fu_app_engine/app.yaml app_engine/attachment_fu_app_engine/attachment.py app_engine/attachment_fu_app_engine/index.yaml app_engine/attachment_fu_app_engine/new.html app_engine/attachment_fu_app_engine/photo.pyc attachment_fu_app_engine.gemspec init.rb initializer.rb.tpl install.rb lib/multipart_post.rb lib/tasks/migrate.rake lib/technoweenie/attachment_fu/backends/app_engine_backend.rb MIT-LICENSE Rakefile README}
12
+ end
13
+
data/init.rb ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ # if you're using this in production I'd recommend creating your own app engine app with the provided backend code
2
+ # see http://code.google.com/appengine/
3
+ Technoweenie::AttachmentFu::Backends::AppEngineBackend.base_url = "http://attachment-fu-gae.appspot.com"
4
+
5
+ # storage prefix prevents collision between multiple apps using the same app engine for storage
6
+ Technoweenie::AttachmentFu::Backends::AppEngineBackend.storage_prefix = "<%= rand(10e12) %>"
@@ -0,0 +1,10 @@
1
+ require 'erb'
2
+
3
+ initializer = File.dirname(__FILE__) + '/../../../config/initializers/app_engine_backend.rb'
4
+
5
+ unless File.exist? initializer
6
+ initializer_template = IO.read(File.dirname(__FILE__) + '/initializer.rb.tpl')
7
+ File.open(initializer, 'w') { |f| f << ERB.new(initializer_template).result }
8
+ end
9
+
10
+ puts IO.read(File.join(File.dirname(__FILE__), 'README'))
@@ -0,0 +1,30 @@
1
+ class MultipartPost
2
+ # see http://www.realityforge.org/articles/2006/03/02/upload-a-file-via-post-with-net-http for file upload with http
3
+ def self.post(uri, params=[])
4
+ chunks = []
5
+ params.each do |param|
6
+ param[:name]
7
+ chunks << if param[:mime_type]
8
+ "Content-Disposition: form-data; name=\"#{CGI::escape(param[:name])}\"; filename=\"#{param[:filename]}\"\r\n" +
9
+ "Content-Transfer-Encoding: binary\r\n" +
10
+ "Content-Type: #{param[:mime_type]}\r\n" +
11
+ "\r\n#{param[:value]}\r\n"
12
+ else
13
+ "Content-Disposition: form-data; name=\"#{CGI::escape(param[:name])}\"\r\n" +
14
+ "\r\n#{param[:value]}\r\n"
15
+ end
16
+ end
17
+ boundary = "349832898984244898448024464570528145"
18
+ post_body = ""
19
+ chunks.each do |chunk|
20
+ post_body << "--#{boundary}\r\n"
21
+ post_body << chunk
22
+ end
23
+ post_body << "--#{boundary}--\r\n"
24
+
25
+ uri = URI.parse(uri)
26
+ Net::HTTP.new(uri.host, uri.port).start do |http|
27
+ http.request_post(uri.path, post_body, "Content-type" => "multipart/form-data; boundary=" + boundary)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ # move from s3 to gae
2
+
3
+ desc 'Migrate file data from s3 storage to app engine'
4
+ task :migrate_s3_to_app_engine => :environment do
5
+ require 'open-uri'
6
+ require 'multipart_post'
7
+
8
+ raise 'You should specify the class to migrate with ATTACHMENT_CLASS=Image'
9
+ clazz = ENV['ATTACHMENT_CLASS'].constantize
10
+
11
+ raise 'You should set the :storage option from your class to :s3 when you run this migration' unless clazz.attachment_options[:storage].to_s == 's3'
12
+
13
+ app_engine_config = Technoweenie::AttachmentFu::Backends::AppEngineBackend
14
+
15
+ failures = []
16
+ clazz.find(:all).each do |o|
17
+ next if o.respond_to?(:parent) && o.parent
18
+ begin
19
+ puts "Migrating #{o.public_filename}"
20
+
21
+ temp_data = open(o.public_filename).read
22
+
23
+ app_engine_path = ['attachments', app_engine_config.storage_prefix, clazz.attachment_options[:path_prefix], o.id, o.filename].compact.join('/')
24
+
25
+ response = MultipartPost.post(app_engine_config.base_url + '/attachments', [
26
+ {:name => 'uploaded_data', :filename => o.filename, :mime_type => o.content_type, :value => temp_data},
27
+ {:name => 'path', :value => app_engine_path}
28
+ ])
29
+ raise response.body unless response.is_a? Net::HTTPRedirection
30
+ rescue => err
31
+ puts "Failed to migrate #{o.public_filename} - #{err.message}"
32
+ failures << o.id
33
+ next
34
+ end
35
+ end
36
+ puts "#{failures.size} files failed to migrate" unless failures.empty?
37
+ end
@@ -0,0 +1,72 @@
1
+ require 'net/http'
2
+ require 'multipart_post'
3
+
4
+ module Technoweenie # :nodoc:
5
+ module AttachmentFu # :nodoc:
6
+ module Backends
7
+ # store in Google App Engine
8
+ module AppEngineBackend
9
+ mattr_accessor :base_url, :storage_prefix
10
+ @@base_url = "http://attachment-fu-gae.appspot.com"
11
+ @@storage_prefix = nil
12
+
13
+ def public_filename(thumbnail = nil)
14
+ thumbnails = HashWithIndifferentAccess.new(attachment_options[:thumbnails])
15
+
16
+ query = if thumbnail && thumbnails[thumbnail]
17
+ "?resize=#{thumbnails[thumbnail]}"
18
+ elsif thumbnail.is_a?(String)
19
+ "?resize=#{thumbnail}"
20
+ elsif attachment_options[:resize]
21
+ "?resize=#{attachment_options[:resize]}"
22
+ else
23
+ ''
24
+ end
25
+ "#{AppEngineBackend.base_url}/#{full_filename}#{query}"
26
+ end
27
+
28
+ def create_temp_file
29
+ write_to_temp_file current_data
30
+ end
31
+
32
+ # Gets the current data from the database
33
+ def current_data
34
+ uri = URI.parse(AppEngineBackend.base_url)
35
+ Net::HTTP.new(uri.host, uri.port).start do |http|
36
+ http.get(filename).response_body
37
+ end
38
+ end
39
+
40
+ #TODO: HACK??
41
+ def process_attachment
42
+ @saved_attachment = true
43
+ end
44
+
45
+ # The full path to the file relative to the bucket name
46
+ # Example: <tt>:table_name/:id/:filename</tt>
47
+ def full_filename
48
+ ['attachments', storage_prefix, attachment_options[:path_prefix].gsub('public/', ''), id.to_s, filename].compact.join('/')
49
+ end
50
+
51
+ def create_or_update_thumbnail(*args)
52
+ #ignore
53
+ end
54
+ protected
55
+ def destroy_file
56
+ #ignore
57
+ end
58
+
59
+ def save_to_storage
60
+ if save_attachment?
61
+ response = MultipartPost.post(AppEngineBackend.base_url + '/attachments', [
62
+ {:name => 'uploaded_data', :value => temp_data, :mime_type => content_type, :filename => filename},
63
+ {:name => 'path', :value => full_filename}
64
+ ])
65
+ raise response.body unless response.is_a? Net::HTTPRedirection
66
+ end
67
+ true
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: larsklevan-attachment_fu_app_engine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Lars Klevan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-02 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Extension for AttachmentFu (http://github.com/technoweenie/attachment_fu) which uses the Google App Engine for storage and image resizing.
17
+ email: tastybyte@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - app_engine/attachment_fu_app_engine/app.yaml
26
+ - app_engine/attachment_fu_app_engine/attachment.py
27
+ - app_engine/attachment_fu_app_engine/index.yaml
28
+ - app_engine/attachment_fu_app_engine/new.html
29
+ - app_engine/attachment_fu_app_engine/photo.pyc
30
+ - attachment_fu_app_engine.gemspec
31
+ - init.rb
32
+ - initializer.rb.tpl
33
+ - install.rb
34
+ - lib/multipart_post.rb
35
+ - lib/tasks/migrate.rake
36
+ - lib/technoweenie/attachment_fu/backends/app_engine_backend.rb
37
+ - MIT-LICENSE
38
+ - Rakefile
39
+ - README
40
+ has_rdoc: true
41
+ homepage: http://github.com/larsklevan/attachment_fu_app_engine
42
+ post_install_message:
43
+ rdoc_options: []
44
+
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.2.0
63
+ signing_key:
64
+ specification_version: 2
65
+ summary: Extension for AttachmentFu which uses the Google App Engine for storage
66
+ test_files: []
67
+